// ==UserScript== // @name Tidy up your Dashboard - Modified // @namespace https://greasyfork.org/users/10154 // @grant none // @run-at document-start // @match https://*.war.app/* // @description Tidy Up Your Dashboard is a Userscript which brings along a lot of features for improving the user experience on War.app (formerly Warzone). // @version 3.41.3 // @author KKND (maintainer); Muli (original creator) // @icon https://raw.githubusercontent.com/pdxLife/warlight-muli-script/main/assets/icon.png // @homepageURL https://pdxlife.github.io/warlight-muli-script/ // @supportURL https://github.com/pdxLife/warlight-muli-script/issues // @require https://code.jquery.com/jquery-1.11.2.min.js#sha256-Ls0pXSlb7AYs7evhd+VLnWsZ/AqEHcXBeMZUycz/CcA= // @require https://code.jquery.com/ui/1.11.3/jquery-ui.min.js#sha256-xI/qyl9vpwWFOXz7+x/9WkG5j/SVnSw21viy8fWwbeE= // @require https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js#sha256-/7zo27Ll/hVKhCsE+y8m2SS5bhFPEQFheTCL87HuumA= // @require https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/dataTables.bootstrap.min.js#sha256-c0sghAryOuWj4sftUftYNtoAyyI4bahkv1+wopyUhFU= // @downloadURL https://raw.githubusercontent.com/pdxLife/warlight-muli-script/main/Tidy_up_your_Dashboard.user.js // @updateURL https://raw.githubusercontent.com/pdxLife/warlight-muli-script/main/Tidy_up_your_Dashboard.user.js // ==/UserScript== window.timeUserscriptStart = new Date().getTime(); window.MULIS_USERSCRIPT = true; var version = (typeof GM_info !== 'undefined') ? GM_info.script.version : '3.41.3'; var DISABLED_FEATURES_KEY = "tidyUpYourDashboard.disabledFeatures"; // Every domain war.app has used; the old ones now redirect to war.app. Bookmarks saved on any of them open on the current domain. var SITE_HOSTS = ["war.app", "warzone.com", "warlight.net"]; var TURN_CHECK_INTERVAL = 5 * 60 * 1000; var AWP_SITE = "https://awp.wl-stats.com"; var CUSTOM_SITE = "https://custom.wl-stats.com"; var WL_STATS_CACHE_MS = 60 * 60 * 1000; var SERIES_CACHE_MS = 10 * 60 * 1000; var GAMES_CACHE_MS = 5 * 60 * 1000; var CLAN_SERIES_KINDS = ["clan_league", "clan_cup", "nations_cup"]; var STAGED_SERIES_KINDS = ["clan_cup", "nations_cup"]; var SCRIPT_URL = "https://raw.githubusercontent.com/pdxLife/warlight-muli-script/main/Tidy_up_your_Dashboard.user.js"; var UPDATE_CHECK_KEY = "tidyUpYourDashboard.updateCheck"; var MAIL_PREVIEWS_KEY = "tidyUpYourDashboard.mailPreviews"; var MAIL_PREVIEW_WORDS = 15; var IDLE_VOLUME_KEY = "tidyUpYourDashboard.idleVolume"; var PLAYER_SEARCH_KEY = "tidyUpYourDashboard.playerSearch"; var IDLE_LOG_KEY = "tidyUpYourDashboard.idleLog"; var IDLE_STORED_KEY = "tidyUpYourDashboard.idleStoredArtifacts"; var SITE_URL = "https://pdxlife.github.io/warlight-muli-script/"; var REPO_URL = "https://github.com/pdxLife/warlight-muli-script"; var MENU_SECTIONS_KEY = "tidyUpYourDashboard.menuSections"; // The settings menu's sections, in order: the key a setting or feature names, and its heading. var MENU_SECTIONS = [["Dashboard", "Dashboard"], ["Games", "Games"], ["Tournaments", "Tournaments"], ["Community", "Ladders & community"], ["Profile", "Profile"], ["Clans", "Clans"], ["Idle", "Idle"], ["Forum", "Forum"], ["Maps & levels", "Maps & levels"], ["Other", "Other"], ["Third-party data", "Third-party data"]]; var UPDATE_CHECK_MS = 24 * 60 * 60 * 1000; var UPDATE_RETRY_MS = 60 * 60 * 1000; this.$$$ = jQuery.noConflict(true); window.wlerror = function () { }; setupImages(); console.log("Running Muli's userscript"); if (pageIsDashboard()) { createSelector(".container-fluid", "display: none"); createSelector("body", "overflow: hidden") } setupDatabase(); log("indexedDB setup complete"); if (document.readyState === 'complete' || document.readyState === 'interactive') { log("Readystate complete|interactive"); DOM_ContentReady(); } else { window.document.addEventListener("DOMContentLoaded", DOM_ContentReady); log("Readystate loading") } function removePlayerDataCookie() { $.removeCookie("muli_wl_player"); } function loadPlayerData() { let deferred = $.Deferred(); let cookieValue = $.cookie('muli_wl_player'); if (cookieValue) { try { window.WlPlayer = JSON.parse(cookieValue); log("Skipping reloading player data " + $.cookie('wlPlayerCacheValid')); return deferred.resolve(window.WlPlayer).promise(); } catch (e) { log("Discarding unreadable player data cookie"); removePlayerDataCookie(); } } let $profileLink = $(".dropdown-menu a[href*='Profile']"); if (!$profileLink.length) { log("Profile link not found — user may not be logged in"); return deferred.reject().promise(); } let player = { Name: null, PlayerId: null, ProfileId: null, ProfileToken: [], isMember: null, OnVacationUntil: null } let profileUrl = $profileLink.prop("href"); $.ajax({ url: profileUrl }).done(function (response) { try { let page = $(new DOMParser().parseFromString(response, "text/html")); player.Name = page.find("#AccountDropDown").text().trim(); player.PlayerId = extractPlayerId(profileUrl); player.ProfileId = extractProfileId(profileUrl); player.ProfileToken = extractProfileToken(profileUrl); player.isMember = page.find("#MemberIcon").length > 0; player.OnVacationUntil = getVacationEndDate(page); window.WlPlayer = player; $.cookie("muli_wl_player", JSON.stringify(player), { expires: new Date(new Date().getTime() + 1 * 60 * 60 * 1000) }); deferred.resolve(player); } catch (e) { log("Error loading player data: " + e); deferred.reject(); } }).fail(function (xhr) { log("Error loading player data: " + xhr.statusText); deferred.reject(); }); return deferred.promise(); } function getVacationEndDate(page) { let vacationText = page.find("img[src*='Vacation']") .parent() .contents() .filter(function () { if (this.nodeType === 3) { return this.nodeValue.includes("vacation") } return false; }) .text() .trim(); let match = vacationText.match(/(\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2})/); if (match) { return new Date(match[1]); } return null; } /** * Extracts the full id of a player. The player id is built as following: * - first 2 digits of the profile token * - the profile id * - last 2 digits of the profile token */ function extractPlayerId(url) { let match = url.match(/p=(\d+)/); if (match != null) { return match[1]; } return null; } function extractProfileId(url) { let match = url.match(/p=(\d+)/); if (match != null) { return match[1].slice(2, -2); } return null; } function extractProfileToken(url) { let match = url.match(/p=(\d+)/); if (match != null) { return [match[1].slice(0, 2), match[1].slice(-2)]; } return null; } if (typeof wljs_WaitDialogJS === 'undefined') { wljs_WaitDialogJS = { Start: function () { }, Stop: function () { } } } var logData = ""; function log(entry) { var time = new Date().toTimeString().slice(0, 8); logData += `\n${time} | ${entry}`; } function logError(entry) { var time = new Date().toTimeString().slice(0, 8); logData += `\n${time} | ${entry}`; } //ctrl+shift+2 $$$(document).keydown(function (e) { if (e.which === 50 && e.ctrlKey && e.shiftKey) { getLog() } }); window.logDialog = undefined; window.getLog = function () { if (logDialog) { logDialog.html($$$(' Download Text-File `); $("body").append(` `); createSelector("#exportSettingsBox, #importSettingsBox", "width:100%; height: 300px"); $("#exportSettings").on("click", function () { exportSettings(); }); $("#importSettings").on("click", function () { importSettings(); }); $("#turnNotifications, #idleNotifications").on("change", function () { if (this.checked && "Notification" in window && Notification.permission === "default") { Notification.requestPermission(); } }); $("#resetHiddenThreads").on("click", function () { window.undoIgnore(); }); getSortTables(function (tables) { var tableCode = ''; $.each(tables, function (key, table) { tableCode += '
' + table.name + '
▲▼
' }); createSelector(".sortableLadder", "border: 1px gray solid;margin: 5px;padding: 5px;background-color:rgb(25, 25, 25);"); createSelector(".tableSortNavigation", "display: inline-block;float: right;margin-top: -2px;"); createSelector(".tableSortNavigation span", "padding: 3px 10px; cursor: pointer"); createSelector(".tableSortNavigation span:hover", "color: #C0D0FF"); createSelector(".sortTableHighlight", "background-color: rgb(60, 60, 60)"); createSelector(".tableSortHideShow img", "height: 10px"); createSelector(".tableSortHidden", "opacity: 0.2;"); $("body").append(` `); $(".tableSortUp").on("click", function () { $(".sortTableHighlight").removeClass("sortTableHighlight"); var table = $(this).closest(".sortableLadder"); table.addClass("sortTableHighlight"); var prev = table.prev(); table = table.detach(); prev.before(table) }); $(".tableSortDown").on("click", function () { $(".sortTableHighlight").removeClass("sortTableHighlight"); var table = $(this).closest(".sortableLadder"); table.addClass("sortTableHighlight"); var next = table.next(); table = table.detach(); next.after(table) }); $(".tableSortHideShow").on("click", function () { $(".sortTableHighlight").removeClass("sortTableHighlight"); var table = $(this).closest(".sortableLadder"); table.addClass("sortTableHighlight"); table.toggleClass("tableSortHidden") }); checkUserscriptMenuButtons(); }) } function importSettings() { $('.modal').modal("hide"); var settings; try { settings = parseSettingsBackup($("#importSettingsBox").val().trim()); } catch (e) { log(e); CreateModal("mulisuserscript", "Error", "There was an error importing the settings."); return; } wljs_WaitDialogJS.Start(null, "Importing Settings...") Promise.all(Object.values(Database.Exports).map(function (table) { return Database.clear(table); })).then(function () { var additions = []; settings.forEach(function (data) { data.data.forEach(function (value) { additions.push(Database.add(data.table, value)); }); }); return Promise.all(additions); }).then(function () { window.location.reload(); }).catch(function (e) { log(e); wljs_WaitDialogJS.Stop(); CreateModal("mulisuserscript", "Error", "There was an error importing the settings."); }); } function parseSettingsBackup(backup) { var settings = JSON.parse(atob(backup)); var importableTables = Object.values(Database.Exports); var isValid = Array.isArray(settings) && settings.every(function (entry) { return entry && importableTables.indexOf(entry.table) !== -1 && Array.isArray(entry.data); }); if (!isValid) { throw new Error("Invalid settings backup"); } return settings; } function exportSettings() { Promise.all(Object.values(Database.Exports).map(function (table) { return Database.readAll(table).then(function (data) { return { table: table, data: data }; }); })).then(function (settings) { var settingsString = btoa(JSON.stringify(settings)); $("#exportSettingsBox").html(settingsString); $("#downloadExportSettingsFile").click(function () { this.href = "data:text/plain;charset=UTF-8," + settingsString; }); }); } function getSortTables(callback) { var defaultTables = [ { id: "#BookmarkTable", name: "Bookmarks", hidden: false, order: 0 }, { id: "#MapOfTheWeekTable", name: "Map of the Week", hidden: false, order: 4 }, { id: "#ForumTable", name: "Forum Posts", hidden: false, order: 5 }, { id: "#ClanForumTable", name: "Clan Forum Posts", hidden: false, order: 6 }, { id: "#BlogTable", name: "Recent Blog Posts", hidden: false, order: 7 }, { id: "#LeaderboardTable", name: "Coin Leaderboard", hidden: false, order: 8 } ]; if ($("#ShopTable").length > 0) { defaultTables.push({ id: "#ShopTable", name: "War.app Shop", hidden: false, order: -1 }) } if ($(".dataTable thead td:contains('Quickmatch')").length > 0) { defaultTables.push({ id: ".dataTable thead:contains('Quickmatch')", name: "Quickmatch", hidden: false, order: 1 }) } Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, "tableSort", function (tableData) { if (tableData && tableData.value.length > 3) { // war.app no longer has a tournaments table on the dashboard. var tables = tableData.value.filter(function (table) { return table.id !== "#MyTournamentsTable"; }); if ($("#ShopTable").length > 0 && !arrayHasObjWithId(tables, "#ShopTable")) { tables.push({ id: "#ShopTable", name: "War.app Shop", hidden: false, order: -1 }) } if ($(".dataTable thead td:contains('Quickmatch')").length > 0 && !arrayHasObjWithId(tables, ".dataTable thead:contains('Quickmatch')")) { tables.push({ id: ".dataTable thead:contains('Quickmatch')", name: "Quickmatch", hidden: false, order: 1 }) } callback($(tables).sort(compareTable)); } else { callback($(defaultTables).sort(compareTable)) } }) } function arrayHasObjWithId(arr, id) { var found = false; $.each(arr, function (key, val) { if (val.id == id) { found = true; } }); return found; } window.saveTableSort = function () { var tables = []; $.each($("#dashboardTableSortMenu div.sortableLadder"), function (key, table) { var order = key; var id = $(table).attr('data-tableId'); var hidden = $(table).hasClass("tableSortHidden"); var name = $(table).attr('data-name'); tables.push({ id: id, name: name, hidden: hidden, order: order }) }); var tableSort = { name: "tableSort", value: tables }; Database.update(Database.Table.Settings, tableSort, undefined, function () { // A hidden table is removed from the page, so only a reload can bring it back. location.reload(); }) }; function compareTable(a, b) { if (a.order < b.order) return -1; if (a.order > b.order) return 1; return 0; } window.getSettingInfo = function (id) { var help = ""; $.each(userscriptSettings, function (key, setting) { if (setting.id == id) { help = setting.help; } }); return help; }; function checkUserscriptMenuButtons() { $.each(userscriptSettings, function (key, set) { Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, set.id, function (setting) { if (setting) { $("#" + setting.name).prop("checked", setting.value); } else { $("#" + set.id).prop("checked", set.selected); } }) }); } // One foldable section per area, holding that area's settings and then its feature switches. function renderMenuSections(settings, features, disabled, open, helpIcon) { var order = MENU_SECTIONS.map(function (entry) { return entry[0]; }); var groups = {}; var add = function (section, html) { if (order.indexOf(section) === -1) { order.push(section); } (groups[section] = groups[section] || []).push(html); }; settings.forEach(function (setting) { add(setting.section || "Other", '
' + setting.text + helpIcon(setting) + '
'); }); features.forEach(function (feature) { add(feature.section, '
' + htmlEscape(feature.name) + '
'); }); return order.filter(function (section) { return groups[section]; }).map(function (section) { var label = (MENU_SECTIONS.filter(function (entry) { return entry[0] === section; })[0] || [section, section])[1]; return menuSection(section, label, groups[section].length, groups[section].join(""), open.indexOf(section) !== -1); }).join(""); } function menuSection(key, label, count, body, open) { return ''; } function openMenuSections() { try { var stored = JSON.parse(localStorage.getItem(MENU_SECTIONS_KEY)); if (Array.isArray(stored)) { return stored; } } catch (e) { } return ["Dashboard"]; } function setupMenuSections() { var menu = $("#userscriptMenu"); menu.on("click", ".menuSectionHeader", function () { if (menu.hasClass("menuSearching")) { return; } $(this).parent().toggleClass("open"); try { localStorage.setItem(MENU_SECTIONS_KEY, JSON.stringify(menu.find(".menuSection.open").map(function () { return $(this).attr("data-section"); }).get())); } catch (e) { } }); menu.on("input", "#menuSearch", function () { filterMenu(menu, $(this).val()); }); } // While searching, only matching switches show, in their sections opened; a section whose heading // matches shows all of its switches and its buttons (e.g. "backup"). function filterMenu(menu, query) { var needle = String(query || "").trim().toLowerCase(); menu.toggleClass("menuSearching", needle !== ""); menu.find(".menuSection").each(function () { var section = $(this); var headingMatches = needle !== "" && section.children(".menuSectionHeader").text().toLowerCase().indexOf(needle) !== -1; var any = headingMatches; section.find(".settingsListItem").each(function () { var hit = needle === "" || headingMatches || $(this).text().toLowerCase().indexOf(needle) !== -1; $(this).toggleClass("menuItemHidden", !hit); any = any || hit; }); section.toggleClass("menuSectionHidden", !any).toggleClass("menuSectionMatch", needle !== "" && any).toggleClass("menuHeadingMatch", headingMatches); }); } function storeFeatureToggles() { var disabled = []; $("#userscriptMenu .featureToggle").each(function () { if (!$(this).prop("checked")) { disabled.push($(this).attr("data-feature")); } }); return saveDisabledFeatures(disabled); } function storeSettingsVariables() { storeFeatureToggles(); $.each(userscriptSettings, function (key, set) { var isEnabled = $("#" + set.id).prop("checked"); var setting = { name: set.id, value: isEnabled }; Database.update(Database.Table.Settings, setting, undefined, function () { }) }); } function setupSettingsDatabase() { wljs_WaitDialogJS.Start(null, "Setting up Muli's Userscript...") Promise.all(userscriptSettings.map(function (set) { return Database.update(Database.Table.Settings, { name: set.id, value: set.selected }); })).then(function () { sessionStorage.setItem("showUserscriptMenu", true); window.setTimeout(function () { window.location.reload(); }, 2000) }) } function ifSettingIsEnabled(setting, positive, negative, cb) { Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, setting, function (setting) { if (setting && setting.value) { positive(); if (typeof cb == "function") { cb(); } } else { if (typeof negative == 'function') { negative(); } if (typeof cb == 'function') { cb(); } } }) } function pageIsOpenGamesPage() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/OpenGames(?:[?#].*)?$/i); } function pageIsPointsPage() { return location.href.match(/^https?:\/\/[^\/]+\/Points.*/i); } var GAME_CHAT_FRAME_ID = "tuydGameChatFrame"; var GAME_MAP_FRAME_ID = "tuydGameMapFrame"; // A hidden copy of a game would otherwise play the game's sounds a second time: a chat bell for every new // message, the your-turn sound. war.app plays them through SoundManager 2, which uses HTML audio. function muteScriptFrame() { window.HTMLMediaElement.prototype.play = function () { return Promise.resolve(); }; var mute = function () { if (window.soundManager && typeof window.soundManager.mute === "function") { window.soundManager.mute(); } }; mute(); window.setInterval(mute, 2000); } // The script's own hidden game frames: the dashboard's game chat, and Past Games looking up a game's map. function inScriptFrame() { try { return !!window.frameElement && [GAME_CHAT_FRAME_ID, GAME_MAP_FRAME_ID].indexOf(window.frameElement.id) !== -1; } catch (e) { return false; } } // My Games rows that are games (an Auto Game's lobby has no GameID yet), with the cell of the name link. function myGamesChatTargets() { return $("#MyGamesTable tr.GameRow").get().map(function (row) { var link = $(row).find("a[href*='GameID=']").filter(function () { return /[?&]GameID=\d+/i.test($(this).attr("href") || ""); }).last(); var match = /[?&]GameID=(\d+)/i.exec(link.attr("href") || ""); return match ? { row: row, cell: link.closest("td")[0], id: match[1], name: link.text().trim() } : null; }).filter(Boolean); } // What the hidden game shows in its chat: public chat is `ujs_ChatTab`, team chat `ujs_ChatTab_2`, // each with its own box and Send button, shown only where you can write. function gameChatFrameState(doc) { var byId = function (id) { return doc.getElementById(id); }; var chat = byId("ujs_Chat"); var tab = function (suffix) { var element = byId("ujs_ChatTab" + suffix); var older = element ? $(element).find("[id^='ujs_LoadOlderMessagesBtn'], [id^='ujs_ShowOlderChatBtn']").filter(function () { return /^ujs_(?:LoadOlderMessagesBtn|ShowOlderChatBtn)(?:_\d+)?$/.test(this.id) && shownUjsElement(this); })[0] : null; return { messages: element ? chatMessages(element) : [], canSend: shownUjsElement(byId("ujs_SendChatContainer" + suffix)), older: older ? older.id : null }; }; return { ready: !!chat && !!byId("ujs_ChatCollapseButton_btn"), open: !!chat && chat.style.display !== "none", hasTeam: shownUjsElement(byId("ujs_TeamChatTab")), archived: shownUjsElement(byId("ujs_NoChatDueToArchiveMessage")), public: tab(""), team: tab("_2") }; } var GAME_CHAT_RECENT = 20; // The newest `limit` messages, and a note when there are more, in the game or not yet loaded from it. function renderGameChatMessages(messages, muted, limit, hasOlder) { if (!messages.length) { return '
No messages yet.
'; } var hidden = Math.max(0, messages.length - limit); var more = hidden || hasOlder ? '
Showing the last ' + Math.min(limit, messages.length) + " messages" + (hasOlder ? "" : " of " + messages.length) + '. Show all
' : ""; return more + messages.slice(hidden).map(function (message) { if (muted.indexOf(message.name) !== -1) { return '
muted
'; } return '
' + htmlEscape(message.name) + ' ' + htmlEscape(message.time) + '' + '
' + htmlEscape(message.text) + '
'; }).join(""); } // Per kind of game: the profile's Play Speed section, which of its turn-time ranges count as slow, and the // limit box's unit and default. var LOBBY_PACE_KINDS = { multiDay: { heading: /Multi-Day Games/i, slow: /1 day - 1 week|>\s*1 week/i, over: "over a day", overLong: "took over a day", unit: "h", unitHours: 1, fallback: 24, step: 1, key: "tidyUpYourDashboard.lobbyPaceHours" }, realTime: { heading: /Real-Time Games/i, slow: /2 - 5 mins|5 - 10 mins|>\s*10 mins/i, over: "over 2 min", overLong: "took over 2 minutes", unit: "min", unitHours: 1 / 60, fallback: 3, step: 0.5, key: "tidyUpYourDashboard.lobbyPaceMinutesRealTime" } }; function durationHours(text) { var units = { day: 24, hour: 1, minute: 1 / 60, second: 1 / 3600 }; var total = 0; var found = false; String(text || "").replace(/(\d+(?:\.\d+)?)\s*(day|hour|minute|second)s?/gi, function (all, amount, unit) { total += parseFloat(amount) * units[unit.toLowerCase()]; found = true; }); return found ? total : null; } function shortDuration(text) { return String(text || "").replace(/(\d+)\s*days?/gi, "$1d").replace(/(\d+)\s*hours?/gi, "$1h").replace(/(\d+)\s*minutes?/gi, "$1m").replace(/(\d+)\s*seconds?/gi, "$1s").trim(); } // What a profile page says about a player's pace: games in progress, boot rate, and the Play Speed // sections for multi-day and real-time games (an average, and a bar per range whose `title` is the // number of turns). function parseProfilePace(doc) { var text = doc.body ? doc.body.textContent.replace(/\s+/g, " ") : ""; var games = /Currently in ([\d,]+) multi-day games?/i.exec(text); var booted = /Booted [\d,]+ times? \((\d+)% of their last/i.exec(text); var section = function (kind) { var average = null; var ranges = []; var heading = $(doc).find("h5").filter(function () { return kind.heading.test($(this).text()); })[0]; if (heading) { var node = heading.nextSibling; var between = ""; for (; node && node.nodeName !== "TABLE" && node.nodeName !== "H5"; node = node.nextSibling) { between += node.textContent; } var match = /Average:\s*(.*\S)/i.exec(between.replace(/\s+/g, " ")); average = match ? match[1] : null; if (node && node.nodeName === "TABLE") { $(node).find("tr").each(function () { var cells = $(this).children("td"); var bar = cells.last().children("div[title]"); if (cells.length === 2 && bar.length) { ranges.push({ label: cells.first().text().replace(/\u00a0/g, " ").trim(), turns: parseInt(bar.attr("title"), 10) || 0 }); } }); } } var count = function (list) { return list.reduce(function (sum, range) { return sum + range.turns; }, 0); }; var turns = count(ranges); var slow = count(ranges.filter(function (range) { return kind.slow.test(range.label); })); return { average: average, averageHours: durationHours(average), slowPercent: turns ? Math.round(slow / turns * 100) : null }; }; return { games: games ? Number(games[1].replace(/,/g, "")) : null, bootedPercent: booted ? Number(booted[1]) : null, multiDay: section(LOBBY_PACE_KINDS.multiDay), realTime: section(LOBBY_PACE_KINDS.realTime) }; } // A boot time of up to an hour is a real-time game; anything longer is multi-day (Past Games uses the same rule). function lobbyGameKind(bootText) { var hours = durationHours(String(bootText || "").split(".")[0]); return hours !== null && hours <= 1 ? "realTime" : "multiDay"; } // The multi-day games count is only shown in multi-day lobbies; real-time lobbies show real-time turns. function lobbyPaceText(pace, kindName, long) { var kind = LOBBY_PACE_KINDS[kindName]; var speed = pace[kindName]; var label = kindName === "realTime" ? "real-time" : "multi-day"; var parts = []; if (kindName === "multiDay" && pace.games !== null) { parts.push(long ? "in " + pace.games + " multi-day game" + (pace.games === 1 ? "" : "s") + ", per their profile" : "in " + pace.games + " game" + (pace.games === 1 ? "" : "s")); } if (speed.averageHours !== null) { parts.push(long ? "average " + label + " turn " + speed.average : "avg turn " + shortDuration(speed.average)); } if (speed.slowPercent !== null) { parts.push(speed.slowPercent + "% " + (long ? "of their " + label + " turns " + kind.overLong : kind.over)); } if (pace.bootedPercent !== null) { parts.push(long ? "booted from " + pace.bootedPercent + "% of their recent games" : "booted " + pace.bootedPercent + "%"); } return parts.length ? parts.join(" · ") : "no " + label + " turns yet"; } // war.app's game data has each player's name, ID and a 4-digit token; war.app builds profile links as // the token's first two digits + ID + its last two. The data's field names are minified and change // between war.app updates, so players are found by the names the lobby shows, and each possible link // is tried largest ID first (the level is a small number too). function lobbyProfileCandidates(root, names) { var found = {}; var seen = new Set(); var queue = [root]; for (var visited = 0; queue.length && visited < 5000; visited++) { var object = queue.shift(); if (!object || typeof object !== "object" || seen.has(object)) { continue; } seen.add(object); var keys = Object.keys(object); if (keys.length > 2000) { continue; } var name = null; var numbers = []; keys.forEach(function (key) { var value = object[key]; if (typeof value === "string" && names.indexOf(value) !== -1) { name = value; } else if (typeof value === "number" && value > 0 && Math.floor(value) === value) { numbers.push(value); } else if (value && typeof value === "object") { queue.push(value); } }); if (name) { var links = found[name] || (found[name] = []); numbers.filter(function (token) { return token >= 1000 && token <= 9999; }).forEach(function (token) { numbers.filter(function (id) { return id !== token; }).forEach(function (id) { var link = String(token).slice(0, 2) + id + String(token).slice(2); if (links.indexOf(link) === -1) { links.push(link); } }); }); links.sort(function (a, b) { return b.length - a.length || (b < a ? -1 : b > a ? 1 : 0); }); } } return found; } var LOBBY_BOOT_PERCENT_KEY = "tidyUpYourDashboard.lobbyBootPercent"; function lobbyBootLimit() { try { var value = parseFloat(localStorage.getItem(LOBBY_BOOT_PERCENT_KEY)); return isNaN(value) ? 10 : Math.max(0, value); } catch (e) { return 10; } } // Why a player is flagged, if at all: their average turn is over the limit, or they're booted more often. function lobbyPaceFlags(pace, kindName, turnLimit, bootLimit) { var kind = LOBBY_PACE_KINDS[kindName]; var speed = pace[kindName]; var flags = []; if (turnLimit > 0 && speed.averageHours !== null && speed.averageHours > turnLimit * kind.unitHours) { flags.push("average turn over " + turnLimit + " " + kind.unit); } if (bootLimit > 0 && pace.bootedPercent !== null && pace.bootedPercent > bootLimit) { flags.push("booted over " + bootLimit + "%"); } return flags; } function lobbyPaceLimit(kindName) { var kind = LOBBY_PACE_KINDS[kindName]; try { var value = parseFloat(localStorage.getItem(kind.key)); return isNaN(value) ? kind.fallback : Math.max(0, value); } catch (e) { return kind.fallback; } } // In a lobby, each player gets a line with their pace from their profile, and the host can flag slow // players to remove them with war.app's own Add/Remove Players. Nothing here changes the game. function setupLobbyPace() { addCSS(` .tuydPace { position: absolute; bottom: 2px; overflow: hidden; color: #9a9a9a; font-size: 12px; line-height: 20px; text-overflow: ellipsis; white-space: nowrap; pointer-events: auto; } .tuydPace.tuydPaceSlow { color: #ff6b6b; } .tuydPaceLimit { position: absolute; white-space: nowrap; color: #9a9a9a; font-size: 12px; pointer-events: auto; } .tuydPaceLimit input { width: 48px; margin: 0 3px; border: 1px solid #555; border-radius: 3px; background: #111; color: #eee; font-size: 12px; } `); var paces = {}; var pending = {}; var kindName = "multiDay"; var chosen = {}; var timer = null; var rows = function () { return $("[id^='ujs_PlayerRow']").filter(function () { return /^ujs_PlayerRow(?:_\d+)?$/.test(this.id) && shownUjsElement(this); }).get().map(function (row) { var label = $(row).find("[id^='ujs_Player'] .ujsTextInner").filter(function () { return $(this).text().trim(); }).first(); return { element: row, name: label.text().trim() }; }).filter(function (row) { return row.name; }); }; var profile = function (name, links) { if (pending[name] || paces[name]) { return; } pending[name] = true; var tryLink = function (i) { if (i >= links.length) { paces[name] = { missing: true }; schedule(); return; } $.ajax({ url: "/Profile?p=" + links[i], dataType: "text" }).done(function (html) { var doc = new DOMParser().parseFromString(html, "text/html"); // A wrong link gives war.app's page without a player; a player's page is titled "Name - War.app". if ((doc.title || "").indexOf(name + " - ") !== 0) { tryLink(i + 1); return; } paces[name] = $.extend(parseProfilePace(doc), { link: links[i] }); schedule(); }).fail(function () { log("Player pace: couldn't read " + name + "'s profile"); paces[name] = { missing: true }; schedule(); }); }; tryLink(0); }; var draw = function () { timer = null; var lobby = rows(); if (!lobby.length) { return; } var boot = $(".ujsTextInner").filter(function () { return /^Boot time:/i.test($(this).text().trim()); }).first().text().trim(); kindName = lobbyGameKind(boot.replace(/^Boot time:\s*/i, "")); var kind = LOBBY_PACE_KINDS[kindName]; var limit = kindName in chosen ? chosen[kindName] : lobbyPaceLimit(kindName); var bootLimit = "boot" in chosen ? chosen.boot : lobbyBootLimit(); var names = lobby.map(function (row) { return row.name; }); var unknown = names.filter(function (name) { return !paces[name] && !pending[name]; }); if (unknown.length && window.UJS_Hooks && window.UJS_Hooks.za) { var candidates = lobbyProfileCandidates(window.UJS_Hooks.za, unknown); unknown.forEach(function (name) { if (candidates[name] && candidates[name].length) { profile(name, candidates[name]); } }); } var section = $(lobby[0].element).closest("[id^='ujs_PlayersSection']"); var box = section.children("[id$='_img']")[0] || section[0]; var right = box ? box.getBoundingClientRect().right - 14 : 0; // One column for all rows, right of the widest name, level or Nudge button. var column = 0; lobby.forEach(function (row) { var left = row.element.getBoundingClientRect().left; $(row.element).find("*").not(".tuydPace, .tuydPace *").each(function () { var rect = this.getBoundingClientRect(); if (rect.width) { column = Math.max(column, rect.right - left); } }); }); lobby.forEach(function (row) { var pace = paces[row.name]; var line = $(row.element).children(".tuydPace"); if (!pace || pace.missing) { line.remove(); return; } if (!line.length) { line = $('').appendTo(row.element); } var flags = lobbyPaceFlags(pace, kindName, limit, bootLimit); var slow = flags.length > 0; var left = column + 16; var width = right - row.element.getBoundingClientRect().left - left; line.attr({ href: "/Profile?p=" + pace.link, title: lobbyPaceText(pace, kindName, true) + (slow ? ". Flagged: " + flags.join(", ") : "") + " (Muli's Userscript)" }) .text((slow ? "⚠ " : "") + lobbyPaceText(pace, kindName)).toggleClass("tuydPaceSlow", slow) .css({ left: left, maxWidth: Math.max(0, width), display: width < 120 ? "none" : "" }); }); var heading = section.find(".ujsTextInner").filter(function () { return $(this).text().trim() === "Players"; }).first().parent(); if (heading.length) { var control = heading.children(".tuydPaceLimit"); if (control.attr("data-kind") !== kindName) { control.remove(); control = $('').attr({ "data-kind": kindName, title: "Players whose average " + (kindName === "realTime" ? "real-time" : "multi-day") + " turn is longer, or who are booted more often, are marked ⚠ (0 = off; Muli's Userscript)" }).appendTo(heading); control.find("[data-limit=turn]").attr("step", kind.step).val(limit); control.find("[data-limit=boot]").val(bootLimit); control.find("span").text(kind.unit); } // war.app's text holders are zero-size anchors; the control goes at the right end of the heading's text box. var anchor = heading[0].getBoundingClientRect(); var title = heading.children(".ujsTextInner")[0]; if (title) { var box = title.getBoundingClientRect(); control.css({ left: box.right - anchor.left - control.outerWidth() - 10, top: box.top - anchor.top + (box.height - control.outerHeight()) / 2 }); } } }; var schedule = function () { if (!timer) { timer = setTimeout(draw, 300); } }; $(document).on("change input", ".tuydPaceLimit input", function () { var value = parseFloat(this.value); var boot = $(this).attr("data-limit") === "boot"; var name = boot ? "boot" : $(this).closest(".tuydPaceLimit").attr("data-kind") || kindName; chosen[name] = isNaN(value) ? 0 : Math.max(0, value); try { localStorage.setItem(boot ? LOBBY_BOOT_PERCENT_KEY : LOBBY_PACE_KINDS[name].key, String(chosen[name])); } catch (e) { // A blocked localStorage only means the limit isn't remembered. } schedule(); }); new MutationObserver(function (mutations) { var ours = mutations.every(function (mutation) { return $(mutation.target).closest(".tuydPace, .tuydPaceLimit").length || Array.prototype.every.call(mutation.addedNodes, function (node) { return $(node).is(".tuydPace, .tuydPaceLimit"); }) && mutation.addedNodes.length; }); if (!ours) { schedule(); } }).observe(document.body, { childList: true, subtree: true }); $(window).on("resize", schedule); schedule(); } // A game's chat on the dashboard: the game loads in a hidden frame, its chat is opened there, and the // panel shows that chat and writes through the game's own chat box and Send button. war.app sends a // game's chat only over the game's own connection, so there's nothing lighter to load. function setupDashboardGameChat() { var table = document.getElementById("MyGamesTable"); if (!table) { return; } addCSS(` .tuydHasGameChat { position: relative; } .tuydGameChatCorner { position: absolute; right: 6px; bottom: 6px; z-index: 2; line-height: 16px; pointer-events: auto; } .tuydGameChatBtn { display: inline-block; padding: 1px 6px; border: 1px solid #555; border-radius: 3px; background: rgba(0, 0, 0, 0.55); color: #ddd !important; font-size: 12px; text-decoration: none !important; } .tuydGameChatBtn:hover, .tuydGameChatBtn.tuydActive { border-color: #e0c060; color: #fff !important; } #tuydGameChat { position: fixed; right: 16px; bottom: 16px; z-index: 1040; display: flex; flex-direction: column; width: 380px; max-width: calc(100vw - 32px); height: min(520px, calc(100vh - 96px)); border: 1px solid #555; border-radius: 6px; background: #1b1b1b; color: #ddd; font-size: 13px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6); } #tuydGameChat .tuydGameChatHead { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid #333; } #tuydGameChat .tuydGameChatTitle { flex: 1; overflow: hidden; color: #fff; font-weight: bold; text-overflow: ellipsis; white-space: nowrap; } #tuydGameChat .tuydGameChatClose { color: #aaa; font-size: 16px; text-decoration: none; } #tuydGameChat .tuydGameChatTabs { display: flex; gap: 6px; padding: 6px 10px 0; } #tuydGameChat .tuydGameChatTab { padding: 1px 10px; border: 1px solid #444; border-radius: 3px; color: #aaa; text-decoration: none; } #tuydGameChat .tuydGameChatTab.tuydActive { border-color: #e0c060; color: #fff; } #tuydGameChat .tuydGameChatList { flex: 1; overflow-y: auto; padding: 6px 10px; } #tuydGameChat .tuydGameChatMsg { margin: 0 0 7px; } #tuydGameChat .tuydGameChatName { color: #fff; font-weight: bold; } #tuydGameChat .tuydGameChatMore { margin: 0 0 8px; padding-bottom: 6px; border-bottom: 1px solid #333; color: #8d8d8d; font-size: 12px; } #tuydGameChat .tuydGameChatTime, #tuydGameChat .tuydGameChatNote, #tuydGameChat .tuydGameChatMutedMsg { color: #8d8d8d; font-size: 11px; } #tuydGameChat .tuydGameChatText { white-space: pre-wrap; overflow-wrap: anywhere; } #tuydGameChat .tuydGameChatStatus { padding: 4px 10px; color: #8d8d8d; } #tuydGameChat .tuydGameChatForm { display: flex; gap: 6px; padding: 8px 10px; border-top: 1px solid #333; } #tuydGameChat .tuydGameChatInput { flex: 1; min-height: 38px; resize: vertical; border: 1px solid #555; border-radius: 4px; background: #111; color: #eee; font: inherit; } #tuydGameChatFrame { position: fixed; left: -10000px; top: 0; width: 1000px; height: 800px; border: 0; } `); var panel = null; var frame = null; var game = null; var tabName = "public"; var waitTimer = null; var renderTimer = null; var observer = null; var lastHtml = ""; var muted = []; var limit = GAME_CHAT_RECENT; var olderTimer = null; readMutedPlayers(function (names) { muted = names; }); var frameDoc = function () { try { return frame && frame.contentDocument; } catch (e) { return null; } }; var frameQuery = function () { try { return frame && frame.contentWindow && frame.contentWindow.$; } catch (e) { return null; } }; var status = function (text) { panel.find(".tuydGameChatStatus").text(text || "").toggle(!!text); }; var close = function () { clearInterval(waitTimer); clearInterval(olderTimer); clearTimeout(renderTimer); waitTimer = renderTimer = olderTimer = null; if (observer) { observer.disconnect(); observer = null; } $(frame).remove(); $(panel).remove(); frame = panel = game = null; lastHtml = ""; $(".tuydGameChatBtn.tuydActive").removeClass("tuydActive"); }; var render = function (toBottom) { var doc = frameDoc(); if (!panel || !doc) { return; } var state = gameChatFrameState(doc); if (!state.hasTeam) { tabName = "public"; } panel.find(".tuydGameChatTabs").toggle(state.hasTeam); panel.find(".tuydGameChatTab").each(function () { $(this).toggleClass("tuydActive", $(this).attr("data-tab") === tabName); }); var current = state[tabName]; var list = panel.find(".tuydGameChatList")[0]; var html = renderGameChatMessages(current.messages, muted, limit, limit !== Infinity && !!current.older); if (html !== lastHtml) { // Older messages arrive above, so a reader scrolled up keeps their place. var fromBottom = list.scrollHeight - list.scrollTop - list.clientHeight; list.innerHTML = html; lastHtml = html; list.scrollTop = toBottom || fromBottom < 40 ? list.scrollHeight : list.scrollHeight - list.clientHeight - fromBottom; } panel.find(".tuydGameChatForm").toggle(current.canSend); status(olderTimer ? "Loading older messages…" : current.canSend ? "" : state.archived ? "This game's chat is archived." : "Read only: you can't write in this chat."); }; // war.app keeps older messages behind "Load Older Messages"; pressed until it goes away or adds nothing. var showAll = function () { limit = Infinity; lastHtml = ""; clearInterval(olderTimer); var presses = 0; var lastSize = -1; var idle = 0; var stop = function () { clearInterval(olderTimer); olderTimer = null; render(false); }; olderTimer = setInterval(function () { var doc = frameDoc(); var query = frameQuery(); if (!panel || !doc || !query) { stop(); return; } var current = gameChatFrameState(doc)[tabName]; if (!current.older || presses >= 30) { stop(); } else if (current.messages.length !== lastSize) { lastSize = current.messages.length; idle = 0; presses++; query("#" + current.older + "_btn").trigger("click"); } else if (++idle > 4) { stop(); } }, 1500); render(false); }; var schedule = function () { if (!renderTimer) { renderTimer = setTimeout(function () { renderTimer = null; render(false); }, 300); } }; var open = function (target, button) { close(); game = target; tabName = "public"; limit = GAME_CHAT_RECENT; panel = $('').appendTo("body"); var url = "/MultiPlayer?GameID=" + encodeURIComponent(target.id); panel.find(".tuydGameChatTitle, .tuydGameChatOpen").attr("href", url); panel.find(".tuydGameChatTitle").text(target.name || "Game").attr("title", target.name || ""); status("Loading the game…"); $(button).addClass("tuydActive"); frame = $('').attr({ id: GAME_CHAT_FRAME_ID, src: url }).appendTo("body")[0]; var started = Date.now(); var clicked = 0; waitTimer = setInterval(function () { var doc = frameDoc(); var query = frameQuery(); var state = doc && query ? gameChatFrameState(doc) : null; if (!state || !state.ready) { if (Date.now() - started > 60000) { clearInterval(waitTimer); waitTimer = null; status("The game didn't load here. Open it instead."); } return; } // The chat starts closed; it's clicked again only while it's still closed, so it can't toggle shut. if (!state.open) { if (Date.now() - clicked > 2500) { clicked = Date.now(); query("#ujs_ChatCollapseButton_btn").trigger("click"); } return; } clearInterval(waitTimer); waitTimer = null; observer = new MutationObserver(schedule); observer.observe(doc.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["style"] }); render(true); }, 500); }; var send = function () { var box = panel.find(".tuydGameChatInput"); var text = box.val().trim(); var doc = frameDoc(); var query = frameQuery(); if (!text || !doc || !query) { return; } var suffix = tabName === "team" ? "_2" : ""; var input = doc.getElementById("ujs_SendChatInput" + suffix + "_input"); if (!input) { status("Couldn't find the game's chat box. Open the game to write."); return; } input.value = text; input.dispatchEvent(new frame.contentWindow.Event("input", { bubbles: true })); query("#ujs_SendChatButton" + suffix + "_btn").trigger("click"); box.prop("disabled", true); var tries = 0; var check = setInterval(function () { if (!panel) { clearInterval(check); return; } if (!input.value) { clearInterval(check); box.val("").prop("disabled", false).focus(); status(""); } else if (++tries > 15) { clearInterval(check); box.prop("disabled", false); status("Not sent. Open the game and send it there."); } }, 300); }; var mark = function () { myGamesChatTargets().forEach(function (target) { var cell = $(target.cell); var corner = cell.children(".tuydGameChatCorner"); if (corner.attr("data-game-id") === target.id) { return; } corner.remove(); cell.addClass("tuydHasGameChat").append('' + '💬 Chat'); }); }; var tableObserver = new MutationObserver(function () { apply(); }); var apply = function () { tableObserver.disconnect(); mark(); tableObserver.observe(table, { childList: true, subtree: true }); }; apply(); $(document).on("tuyd:gamesRefreshed", function (e, selector) { if (selector === "#MyGamesTable") { apply(); } }); $(document).on("click", ".tuydGameChatBtn", function (event) { event.preventDefault(); event.stopPropagation(); var id = $(this).closest(".tuydGameChatCorner").attr("data-game-id"); if (game && game.id === id) { close(); return; } var target = myGamesChatTargets().filter(function (candidate) { return candidate.id === id; })[0]; if (target) { open(target, this); } }).on("click", "#tuydGameChat .tuydGameChatClose", function (event) { event.preventDefault(); close(); }).on("click", "#tuydGameChat .tuydGameChatTab", function (event) { event.preventDefault(); tabName = $(this).attr("data-tab"); limit = GAME_CHAT_RECENT; clearInterval(olderTimer); olderTimer = null; var query = frameQuery(); if (query) { query(tabName === "team" ? "#ujs_TeamChatTab_btn" : "#ujs_PublicChatTab_btn").trigger("click"); } lastHtml = ""; render(true); }).on("click", "#tuydGameChat .tuydGameChatShowAll", function (event) { event.preventDefault(); showAll(); }).on("click", "#tuydGameChat .tuydGameChatSend", function () { send(); }).on("keydown", "#tuydGameChat .tuydGameChatInput", function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); send(); } else if (event.key === "Escape") { close(); } }); } function pageIsDashboard() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/(?:#|\?|$).*$/i); } function pageIsProfile() { return location.href.match(/^https?:\/\/[^\/]+\/profile\?p=[0-9]+/i); } function pageIsLevelOverview() { return location.href.match(/^https?:\/\/[^\/]+\/SinglePlayer\/Level\?ID=[0-9]+$/i); } function pageIsMapsPage() { return location.href.match(/^https?:\/\/[^\/]+\/maps/i); } function pageIsForumThread() { return location.href.match(/^https?:\/\/[^\/]+\/Forum\/[0-9]+.*/i); } function pageIsForumOverview() { return location.href.match(/^https?:\/\/[^\/]+\/Forum\/Forum.*/i); } function pageIsThread() { return location.href.match(/^https?:\/\/[^\/]+\/(Forum|Discussion|Clans\/CreateThread).*/i); } function pageIsSubForum() { return location.href.match(/^https?:\/\/[^\/]+\/Forum\/[A-Z]+.*/i); } function pageIsClanForumThread() { return location.href.match(/^https?:\/\/[^\/]+\/Discussion\/\?ID=[0-9]+.*/); } function pageIsClanPage() { return location.href.match(/^https?:\/\/[^\/]+\/Clans\/?\?(?:[^#]*&)?ID=\d+/i); } function pageIsClanList() { return location.href.match(/^https?:\/\/[^\/]+\/Clans\/List/i); } function pageIsTournament() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/Tournament\?ID=[0-9]+/i); } function pageIsGame() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\?GameID=[0-9]+/i); } function pageIsExamineMap() { return location.href.match(/^https?:\/\/[^\/]+\/SinglePlayer\?PreviewMap.*/i); } function pageIsDesignMap() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\?DesignMaps.*/i); } function pageIsCommonGames() { return location.href.match(/^https?:\/\/[^\/]+\/CommonGames\?p=[0-9]+$/i); } function pageIsCommunityLevels() { return location.href.match(/^https?:\/\/[^\/]+\/SinglePlayer\/CommunityLevels/i); } function pageIsCommunity() { return location.href.match(/^https?:\/\/[^\/]+\/Community/i); } function pageIsCommunityEvents() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/?\?(?:[^#]*&)?CommunityEvents=1(?:[&#]|$)/i); } function pageIsLadders() { return location.href.match(/^https?:\/\/[^\/]+\/Ladders\/?(?:[?#]|$)/i); } function pageIsMapOfTheWeek() { return location.href.match(/^https?:\/\/[^\/]+\/MapOfTheWeek.*/i); } function pageIsChatRoom() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/?\?(?:[^#]*&)?ChatRoom=/i); } function pageIsQuickmatch() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/?\?(?:[^#]*&)?Quickmatch=/i); } function pageIsPlayerSearch() { return location.href.match(/^https?:\/\/[^\/]+\/Community\/PlayerSearch(?:[?#].*)?$/i); } function pageIsPastGames() { return location.href.match(/^https?:\/\/[^\/]+\/MultiPlayer\/PastGames(?:[?#].*)?$/i); } function pageIsIdle() { return location.href.match(/^https?:\/\/[^\/]+\/Idle(?:[\/?#].*)?$/i); } function pageIsMyMail() { return location.href.match(/^https?:\/\/[^\/]+\/Discussion\/MyMail(?:[?#].*)?$/i); } function pageIsBlacklistPage() { return location.href.match(/^https?:\/\/[^\/]+\/ManageBl(?:ack|ock)List.*/i); } // war.app's clan page is drawn client-side; the panel hangs below Clan Wars, inside war.app's // scrolling viewport, so a long panel extends the page's scroll. In war.app's one-column layout // Clan Members comes after Clan Wars, so the panel moves below whatever is lowest in that column. function setupClanStandings() { var clans = {}; var check = function () { var scene = pageIsClanPage() && ujsElement("ClanScene"); var name = scene && $(ujsElement("ClanNameLabel", "_tmp")).text().trim(); if (!name || $("#clanStandings").attr("data-clan") === name) { return; } if (!(name in clans)) { clans[name] = null; fetchWlStats(CUSTOM_SITE + "/api/v1/series?clan=" + encodeURIComponent(name), SERIES_CACHE_MS).done(function (data) { clans[name] = data && Array.isArray(data.series) ? data.series : []; check(); }).fail(function (xhr) { log("Error loading the clan's custom.wl-stats.com series: " + (xhr && xhr.statusText)); }); return; } var rows = inClanSeriesOrder(clans[name] || []).filter(function (series) { return series.current && Array.isArray(series.squads) && series.squads.length; }).map(renderClanSeriesRow); var dialog = $(ujsElement("ClanWarsSection")).find("[id^='ujs_DialogBackgroundForDropShadow'][id$='_img']").last(); $("#clanStandings").remove(); if (!rows.length || !dialog.length) { return; } addCommunityLaddersStyles(); var panel = $(renderClanStandings(rows)).attr("data-clan", name); var art = warappArt(scene); Object.keys(art).forEach(function (key) { if (art[key]) { panel[0].style.setProperty(key, art[key]); } }); dialog.parent().append(panel); placeBelowColumn(panel, dialog, scene); if (window.ResizeObserver) { new ResizeObserver(function () { placeBelowColumn(panel, dialog, scene); }).observe(dialog[0]); } }; check(); new MutationObserver(check).observe(document.body, { childList: true, subtree: true }); } function placeBelowColumn(panel, dialog, scene) { var box = dialog[0].getBoundingClientRect(); var lowest = box.bottom; $(scene).find("[id^='ujs_DialogBackgroundForDropShadow'][id$='_img']").each(function () { var other = this.getBoundingClientRect(); if (this !== dialog[0] && other.height && other.top >= box.bottom - 1 && other.left < box.right && other.right > box.left) { lowest = Math.max(lowest, other.bottom); } }); panel.css({ width: dialog.outerWidth() + "px", top: Math.round(lowest - box.bottom) + 10 + "px" }); } // Clan League is the biggest clan event, so it leads; other series keep the server's order. function inClanSeriesOrder(list) { var rank = function (series) { var i = CLAN_SERIES_KINDS.indexOf(series.kind); return i === -1 ? CLAN_SERIES_KINDS.length : i; }; return list.slice().sort(function (a, b) { return rank(a) - rank(b); }); } function renderClanStandings(rows) { return '

Community events

' + rows.join("") + '
Added by Muli\'s Userscript
'; } function renderClanSeriesRow(series) { // A clan can enter several Clan Cup squads; its best one stands for the clan. var squads = (series.kind === "clan_cup" ? series.squads.slice(0, 1) : series.squads).map(function (squad) { var name = squad.squad || squad.name; var parts = standingParts(series.kind, squad); return '' + htmlEscape(name) + '' + (parts.length ? ": " + parts.join(" · ") : ""); }); var titles = series.titles && series.titles.count > 0 && Array.isArray(series.titles.editions) ? series.titles : null; if (titles) { squads.push('🏆 Champion ×' + htmlEscape(titles.count) + ': ' + titles.editions.map(function (edition) { return '' + htmlEscape(edition.name) + ''; }).join(", ") + ''); } return communityLadderRow({ name: series.name, url: series.url, statusHtml: seriesStatusHtml(series), detailHtml: seriesDetailHtml(series, Date.now()), youHtml: squads.join("
") }); } // war.app's My Games rows each link to their game, so community games are found by war.app game ID. function setupEventBadges() { var table = document.getElementById("MyGamesTable"); if (!table) { return; } signedInPlayerId().done(function (playerId) { if (!playerId) { return; } var games = {}; var observer = new MutationObserver(function () { apply(); }); var apply = function () { observer.disconnect(); markEventGames(games); observer.observe(table, { childList: true, subtree: true }); }; var load = function () { $.when( settleRequest(fetchWlStats(CUSTOM_SITE + "/api/v1/players/" + playerId + "/games", GAMES_CACHE_MS), "community games"), settleRequest(fetchWlStats(AWP_SITE + "/api/v1/players/" + playerId + "/games", GAMES_CACHE_MS), "AWP World Tour games") ).done(function (custom, awp) { games = {}; [[custom, "custom"], [awp, "awp"]].forEach(function (source) { (source[0] && Array.isArray(source[0].games) ? source[0].games : []).forEach(function (game) { games[String(game.game_id)] = $.extend({}, game, { source: source[1] }); }); }); addCommunityLaddersStyles(); apply(); }); }; load(); $(document).on("tuyd:gamesRefreshed", function (e, selector) { if (selector === "#MyGamesTable") { load(); } }); }); } // The badge sits in the top-right corner of the game's cell, in its own element: the dashboard // stretches every link directly in that cell over the whole cell (clickable rows). The name link // gets room on the right so a long name wraps before running under the badge. function markEventGames(games) { $("#MyGamesTable tr.GameRow").each(function () { var link = $(this).find("a[href*='GameID=']").last(); var match = /[?&]GameID=(\d+)/i.exec(link.attr("href") || ""); var game = match && games[match[1]]; var cell = link.closest("td"); var corner = cell.children(".tuydEventBadgeCorner"); if (!game) { corner.remove(); cell.removeClass("tuydHasEventBadge"); link.removeClass("tuydNameBesideBadge"); } else if (corner.attr("data-game-id") !== match[1]) { corner.remove(); cell.addClass("tuydHasEventBadge").append('' + renderEventBadge(game) + ''); link.addClass("tuydNameBesideBadge"); } }); } // The badge shows the series' short name; the full label is its tooltip. function renderEventBadge(game) { var label = String(game.label || ""); var series = String(game.series || ""); var full = series && label.toLowerCase().indexOf(series.toLowerCase()) === -1 ? series + " · " + label : label || series; var text = { "AWP World Tour": "AWP Tour" }[series] || series || label; return '' + htmlEscape(text) + ''; } function addCSS(css) { var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (head) { if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } head.appendChild(style); } else { $$$(document).ready(function () { addCSS(css) }) } } /** * Create a CSS selector * @param name The name of the object, which the rules are applied to * @param rules The CSS rules */ function createSelector(name, rules) { var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (head) { head.appendChild(style); if (!(style.sheet || {}).insertRule) { (style.styleSheet || style.sheet).addRule(name, rules); } else { style.sheet.insertRule(name + "{" + rules + "}", 0); } } else { $$$(document).ready(function () { createSelector(name, rules) }) } } function setGlobalStyles() { /** Warzone **/ addCSS(` body > .container-fluid { font-size: 15px; } .modal-1000 .modal-dialog { max-width: 1000px; } .modal-750 .modal-dialog { max-width: 750px; } .modal-500 .modal-dialog { max-width: 500px; } .modal-dialog { border: 1px gray solid; } .modal-header { background-image: radial-gradient(circle at center,#88888855,transparent),url(https://warzonecdn.com/Images/Warzone/MainNavBacking2.jpg); background-repeat: no-repeat,repeat; background-size: 100% 100%,194px 194px; } .modal-content { background-image: linear-gradient(-10deg,#33333322 0%,transparent 70%),url(https://warzonecdn.com/Images/Warzone/Background2.jpg); background-repeat: no-repeat,repeat; background-size: 100% 100%,10px 15px; color: grey; } .modal-content a { color: #5a9da5; } .modal-content .title { border-bottom: 1px solid #928f59; margin-top: 25px; } .p-4 table { width: 100%; } .GameRow p { margin: 0 !important; } h2, h3 { font-size: 25px; } img[src*='/Images/X.png'] { width: 100%; height: 100%; } `); /** Warlight **/ $("tr:contains('WarLight Shop'), tr:contains('War.app Shop')").closest(".dataTable").attr("id", "ShopTable"); createSelector('.help-icon', 'display:inline-block;position:absolute; margin-left:10px;margin-top: 2px;cursor:pointer; height: 15px; width: 15px;'); var winHeight = $(window).height(); createSelector(".newSetting", "color: gold;font-weight: bold;"); createSelector(".userscript-menu img", "height: 18px;display: inline-block;position: relative;margin-bottom: -5px;margin-right: 7px;"); createSelector(".context-menu", "display: none;z-index: 100;position: absolute;overflow: hidden;border: 1px solid #CCC;white-space: nowrap;font-family: sans-serif;background: #FFF;color: #333;border-radius: 5px;padding: 0;"); createSelector(".context-menu li", "padding: 8px 12px;cursor: pointer;list-style-type: none;"); createSelector(".context-menu li:hover", "background-color: #DEF;"); createSelector("#MyGamesTable thead tr", "text-align: right"); } function loadDataTableCSS() { var styles = document.createElement("style"); styles.type = "text/css"; styles.innerHTML = getDataTableCSS(); document.body.appendChild(styles); } function getDataTableCSS() { return `table.dataTable thead td,table.dataTable thead th{padding:6px 18px 6px 6px}table.dataTable tfoot td,table.dataTable tfoot th{padding:10px 18px 6px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url(https://cdn.datatables.net/1.10.10/images/sort_both.png)}table.dataTable thead .sorting_asc{background-image:url(https://cdn.datatables.net/1.10.10/images/sort_asc.png)}table.dataTable thead .sorting_desc{background-image:url(https://cdn.datatables.net/1.10.10/images/sort_desc.png)}table.dataTable thead .sorting_asc_disabled{background-image:url(https://cdn.datatables.net/1.10.10/images/sort_asc_disabled.png)}table.dataTable thead .sorting_desc_disabled{background-image:url(https://cdn.datatables.net/1.10.10/images/sort_desc_disabled.png)}.dataTables_wrapper{position:relative;clear:both;zoom:1}#PlayersContainer td{white-space:nowrap} .dataTables_filter { float: right; } .dataTables_filter label { display: inline!important; } .dataTables_filter input { padding: 3px; border-radius: 5px; margin: 5px; } .dataTables_info { clear: both; padding-top: 10px; } .pagination { display: inline-block; float: right; } #foundClansTable_wrapper .row { width: 100%; } .paginate_button { display: inline; padding: 5px; }.paginate_button.active { text-decoration: underline; }` } function domRefresh() { $("body").hide(0).show(0); $(window).trigger('resize') } function htmlEscape(str) { return String(str) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } function safeUrl(url) { var value = String(url == null ? "" : url).trim(); return /^(https?:\/\/|\/(?!\/))/i.test(value) ? value : "#"; } function hideTable(selector) { $(selector).closest("table").remove() } function browserIsFirefox() { return navigator.userAgent.toLowerCase().indexOf('firefox') > -1 } function setupImages() { window.IMAGES = { EYE: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAUCAYAAADlep81AAAAAXNSR0IArs4c6QAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAJKADAAQAAAABAAAAFAAAAAC4oG9NAAAACXBIWXMAAAsTAAALEwEAmpwYAAADH0lEQVRIDa2WS0hWQRTHuy6KKKOSMnu4theYJYgRX/SmB21qEQSFLVxkREgFRUEtW0hQRFBRC2tRm0LMoOj9IiGKHiBkYWQZFZGImalfv/91Rqe5k35oB37fOfM/M/eeO3dm7heNGKKl0+lChk4cYPiTKIraBsgHU1FQDYgUUIxcBJtNej5+tIlD7hXid2iCanhIga344RmFlMJF6IDhWAODd8CEIVXEwBRcht/g200EMZh1BTo0olVC8HUHXxmdy3iKU5DlPM0N4no4D6+NfgX/BbpNWy4fVpr2W/wBWAXLYQpYu0+wkdfYYoWEp5BcuA7dYO0DwSFIFI82DhbBBofpxCn4BLLboH55cBJc+0ijPFGIBBI5UO/2Jj4Gk4MDjEj+gTemhXYZzIH3Jldj7hHRTkGz0eV6IFkU4n5lHatyC0FfB9thmqevRXNnlGZsW/hNgXKdUGLHEReCLZYw3QQzbF6zs1OqY0f7kgToeg32qRYT6xXNhFHqhw8tcN1Q4+6AzK6r+NK0NYOflTD2Ap+ji+WD1ok1JfxZWGqT+BLQTdrgKWyC1fATfNORcdWItXElzg/6PnCPk9PaRQXgFlDDym92xins8drttMfAPLgAR0C7zTeNs5sh20/SPgvfHH2bCtIWfu6I8YJ02gp/QafR5Eea2LrZBP1rwKq9vss0f/wtx609/E41uvr1LhWmbaGZVuv0/vWtio04Cx6b5EG8TtzBrJEORaAFK9NZ1Ge0q2K1/6fOTUboFaAtaE2LeIHtRLzbJLStZ8Fd0/6X09o6bJLv8Hm6Fl730nHimo6b5AwjloO7hXXzZaAZygY9tewEzIW9YGeAMB5bi18D2o1fQVZhiskj9g/HR2i59sETnmQZuEXRTJ+BAtgKrSDTzlwCxbDesAKv4+A4WKsjUPHaifb0trl7BJMSRfgCnVJwC3y7hnDJEzWL5wzKu/aGhmbEnmE2pw92JQQ/sH49cZvO40GLtwFcc9eZq4dif6Y76KS/MqXBm2YiMlgfRq0tTW87DMVeMqga9Cfv/xkX1DdI6+YZZGK76KT+YzOt4g/tMcO6R7Vx2QAAAABJRU5ErkJggg==', EYE_ICON: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAABGWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8XAxcDLIMogxWCWmFxc4BgQ4MMABDAaFXy7xsAIoi/rgszClMcLuFJSi5OB9B8gzk4uKCphYGDMALKVy0sKQOweIFskKRvMXgBiFwEdCGRvAbHTIewTYDUQ9h2wmpAgZyD7A5DNlwRmM4Hs4kuHsAVAbKi9ICDomJKflKoA8r2GoaWlhSaJfiAISlIrSkC0c35BZVFmekaJgiMwpFIVPPOS9XQUjAyMzBgYQOEOUf05EByejGJnEGIIgBCbI8HA4L+UgYHlD0LMpJeBYYEOAwP/VISYmiEDg4A+A8O+OcmlRWVQYxiZjBkYCPEBav9Kfqb50DUAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOgAAFIIAAEVWAAAOpcAABdv11ofkAAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAIqADAAQAAAABAAAAIgAAAAAhsoNuAAAGHklEQVRYCcWXaUxcVRTHZ4bZYIAitCRQW0LB2GZkEdm3FA0xjQkpy6fahDRB49JGozFNjF+ssYlWTeMHo8ZoYgzVNhZIK9o0asu+FU0/tKkkWuKASKACwzAwbP7O7Tw68wZRPzC85M59796z/M//nHPfG6NhC6/u7u59ERERl1ZWVqJMW4Wjs7MzzWQyfWm1WneBwbolQASExWJpcjgcmQsLCyOrq6s15nAzooGIiorKmJubGyEttUVFRb1hZUQPYnFxsVpACBnGcDGiB7G8vKyY0PyHhZFAEF6v1xXIRNiABIKgJkbn5+frSkpK+jUA2vy/U9PR0RFjNBoTzWZzMoW2g3PAwix2vKxLB4zFxMSMO51Onw7ECDLV+fn5ISAEzH8C0tXVlU7PP86owFE2IwmnkYBhumuCNcPS0tIi8yxrtwF3nTk3OjraKd1Bm9aWlZWpwhTH+mtDIERfyIFzFKUn6Ps47g3kWBy6WYtmBOkLKBmwZAC0GnJOIPePTLCnriBD2iIMxGPsdYw20O92cS4OqPQWHJwGiOyfIXqrMLHeJaBhZYTCVN3R29tbxtph2DlJjQzrdUK6pr29PQvKLwHgKCzYPR7PTwDwMqYAcywvL+8KcxFGNwSBvMvn89XIOUFge9FpstlsT2PzR94x+zcEgkCO3W5vAUQu1T1DNC/AwDMAkxP4GoX2+8DAgAWjj+FIb0s940iYG+Whtri4uI+CTYGZaOycmp2ddWM/FTabenp6KgMNrDECdSJwDsEUUjEMiCocv4/RXQCzoCS5NrjdbgdTAsblMejyp0OdEzCnQMBCC3Hszc3NfQtbVdTMMD7i8NUImEc0AwpIa2urjVx/hMM9gBhj1BDNVb+QgDCwHyVzQkKCERB2fW1oTEhNCBPSaQC7EBkZmYXakuiSpiuk6yBsuwCznaWPYXib7Ckg8fHxDWxUIrBAIR4pLy8flE25oHQUYFKR2dCcODMzk0hqPAy1Lz+BTGggYKGJIS82SZV0mboo1J8Joh5fHgLPYe9F2TBRnPcxvyJlQDQfYOg7peH/oVhvsD4GoHRkuhi9gEjBgJLQM0HLP4hMM+sPoScgZgjuRqDNgoKCHwDzrqzB7vPoJJtQeknqgkIaRvlkoILcV1RUTDBJxQs78jGzDSOKST0T5DwTuYvYdFILiil0L8PwbeagixS9A1s3YWUH8m+YiC5HaMaBkc24IOl7D6cxPCFyWpHqmRAQ2GimCNOxow4zGPHy/DZ6IYcNQcQhr30P7TMhfJh8DfC1tBtkZ6Xd7vm/e1dYWDgEvc/x5BYwwgSUq+6QmqDjsnAoNZGqgeBeQL8m3aO3RyHvZO0rjv8H8N2PbJWJ8/8voq2X1ADmYSJtRjBDrwyAfsDcIY3qnCAA6aw+mCgksosA2SM14U8heHyvUgvv6e1Iy+KjFRBF1N9N0vwkrT2xVvo4FxBfk7NUBMYQOM458gUAVmixNBg4TyvKN6YLQHVyYmK0AgDn0YkTJhjzyF9l/xQgvw8EMTQ0ZJucnGwA9AlAxOPjGowdAuwvIrcGRB6oXie0f4rhfCk2IvyW5c8Zx3m1Z6M8ytpB7XsCIJWAL8GxGeC/IjdAGq8zr10EKG/pAyy8DFvFyBmwfRadY9gZ1wSDgMiitDNgToD8KQzYUDDgTKIdgRX1AsMYgYcWoGZUDimidSJXyajBXqaklECGsfcmYD/R64cA0Yzx3qkCSCPDAQD5nqgWJijMQ6w9CzN/4OQ3DI7j1ArwWJ7v53knIw07u2NjY+WTQT4dXDx/RjAflpaWynso5NLaJ2gDymMxdgQmHBoTfhCPUhPyKpBvkXUvwEjk0jFj09PTfci3cP8NLPy5roJ/MQQItG5HsZEOqiQS+Z5QTIg8B9AgtXJgamoqAwbScZoMYDvykisPIi7WpVZusXfr35yLTe0KSo0fxDmqej/tvFYTmvBmzuqoFgdtbW1J0qIwoUAIE9qfn80EoNmOkJvBwcFkpiZoLya/igk5rDShcMwRcnaQzzOAKKAG5NhWJ2Y4nAf6oBPNlzkxk2DCRYvWceSv+78jUGkz7qVGrLToHVip3+h/x2Y4D7T5N9q5xw15GyfaAAAAAElFTkSuQmCC', EYE_ICON_HOVER: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAABGWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8XAxcDLIMogxWCWmFxc4BgQ4MMABDAaFXy7xsAIoi/rgszClMcLuFJSi5OB9B8gzk4uKCphYGDMALKVy0sKQOweIFskKRvMXgBiFwEdCGRvAbHTIewTYDUQ9h2wmpAgZyD7A5DNlwRmM4Hs4kuHsAVAbKi9ICDomJKflKoA8r2GoaWlhSaJfiAISlIrSkC0c35BZVFmekaJgiMwpFIVPPOS9XQUjAyMzBgYQOEOUf05EByejGJnEGIIgBCbI8HA4L+UgYHlD0LMpJeBYYEOAwP/VISYmiEDg4A+A8O+OcmlRWVQYxiZjBkYCPEBav9Kfqb50DUAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOgAAFIIAAEVWAAAOpcAABdv11ofkAAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAIqADAAQAAAABAAAAIgAAAAAhsoNuAAAG+ElEQVRYCcVXa2wUVRSeuTuzO7MttSCQAEJTqAqplIdEAYPQlrYhxgYK8QeSEBIU290tjYaQGH9UjBBEm7Lbh2uMJsagQGRprAiWttQoEQpo+AFBE6SxrRVIVcq+5ul3pp1muiVVE1ombO887jnnu9/5zrkXnnuAV0d9/QKT508ZhuFlDwpHazA4z+S4z92CMJvnefcDAUIgXC5XxCtJeUlV7dF1vYyfaEYcIBbGkskepGVjgd9/bkKBpILQTHPDWp+vk8iYsNSkgiAmbBATBsQJIq4o3cQEpcMpi3FPjRNELJHoNUyzLBUEAfrfQJr275+ULsvTUXozmcs1DQ5E3TR55DjOm2YPZ5p9t/r7b75QXa2MAAFhOjXhZOM/A2mvr89BgBIY5APAYowzUPuy4HJhGFwLcs7phqFi3l2O52+YpnkZc5elS1Iu0tGTVJSNJVVVI9LhBDMmIy3B4HK3KPoR5Dk0nkxRELiEonCaYQwgYDocjbAnUPSCMca58KOR+gTmblgzVB3O4M77EY7sDydraqZ4RPFN3uXaLns8UiKZxCJ5WnETZxi1JmNTGM9/hlW78bPNRowEGgvoSaA6SiDMM8HgKpfbvUXRtL2Ffn/XiMl4GFW+bcHgIlmWT3ll2Q/qJQjsRwCIo/v9hcmB/EDgDNCvQKCxQZhmt6ZpZQSiPRSazwlCRHK7XxYYa8fzmjGBtNbVLWWC0ASDZUjBHUVVd4LiVwBIwLovrvb5fguHwyJWWghgqb6s5yEmejVV3VhYWXkePrNUjksH+APRRGLA43Zn84xFTtfVFTkdDDPSEQplQ3pHkYqseDLZpWtaKcosiFXNxjsRee4hw8disTSk6eF7pYRAoIKIiQ02COwpTQgyf3VFxX5V10uR5i7J48kEM4dOHTz4pA3GAnIiGPQYPB+WJWluPJHoQyrKCiorO2gSgopDo5dGwePhwY6UqgybCX2ICao0LOzLNI9nEcSikW0R0qpw3HqA6cbipoou1wcXwuGH6JsFRGZsOygrwoQkAmyDmC7RR7oMjutF+ZEkF3/f2DhdVZTp6BdRUD18WSAcTBAILCCC97SxcThzDNiTi32+n7DQrWA9it136YCmVdE31tzQMBlBdlG5Ie8N+T7fSduIxpgoXkG++1CKOUldPwvRnQNNWXBmTbNA6PqwJtpqax/Hh+PQ1ROqpnGart8B21esyUN/CgOBNujsPaQb/0zfd6HQTCYbxqsQZxaE1MUnk3udBnT//I4dt9EQIugn1BvmoWyJSotJAmFpQtctTZxuaMhjbnczKM+F0DmyAXMtxX7/DfLlvNyy/C5YuYrY0xSef4th4lKagBXznCxnOifb9xBfraIot6mXYK+wXttM2JogEIJpEhM56BVWM1NVNY7nd2A3aGQ7xKgnEpmoHoFe4eMCJjK2BaV6AfmaA1BHqNwc863b4p07f0EqKvAwQGAsJnS9G7RbTFDvgaIjgiBkExN2Z9V5/o1ny8vPp/prbWychcQeTpOkR9F5O5kolrJVFRV/Il9bo/F4F5rYEtB/vCUUWphqjFV2QkP96LjUMXtRBmVUou0NDctdotiM4HNJE/Qd9wruXy+oqKhJ9dOGkhUM4wQ2zhWQw1WmaS/mI/3D4se+sgT7yRco4exYPN6HD7vbb936tLq62qBdFKs9hsrKo9IzVHVTATaw0/X1+SLPHwObmVgZh+AJaKgD9wfAYqsTBLUIWRC2Q1x7sOApiHFRNYzNa/3+n2neMBB6aKupyWWS9BFq/CmiGNR/DTF+AuPdWMFiiKtXNc319snqDLoj9qNnoCEBKbtuaNqFtVVVl8mXfZ2tqZETHs86AHwNbK2kRphQ1SNqLBYo2bXrpj1vBBB62bxv3+S0jIw9cPwS1O8BGEsTSM3wQRfO8Hm0AG2nLWhSoDwXCyiCEMvgJw/VwVH6kd63CwKBD1PtRwGxnbWFQqU4+ByCkzS05uFDDd5vFkWxHIz9DuNfDcZussFdOAO2j2DlsxB8HgLNmeT1EquclU6O+xjHh/eLA4FeO4ZztMrH+YLufwgGM+KMbYNm0oiJoUNNZ2soVOAShDBSl45fqpmVZypv6qYQdN/daPQ8wDYhdV8hZX+MMnC8GAWkPRyeGte0Q9BEEY4AFhM4WQ0e+TXtEs4U6/6ORhciQA5+M9FlJWqPGKOI3o3ecB26umZq2rV/C+7AMVKsBIJT1aPpXu8alFaPgeMdVYfTYLzusajB69tweAa2/sNgYhWBGOuga9vcz9FFzr7BpoMdNQIQKy0mcLzDDjyqI97PwKm+WFtdXS6aQCRNlp+GyO75n59Uo/F4pkbUgkYzA52uG4rfhE5nCXM8go3lExum6UZr7tcZ21pYXj4hwrwXoH8AK1jLr8C3vZYAAAAASUVORK5CYII=', QUESTION: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAwwAAAABAAADDAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAABguP1VAAAACXBIWXMAAHf1AAB39QG+gBuGAAADrElEQVRYCb2XzUtUURiHZ0pTCFEKgjAQo2iRH+QiBEUXbQq1P6D+gShxqRGBi1pUzCJatRFpVQvBTUg7wYQIRKLPZWhtooWafYHZ9Pyu972893rvnZmCXnjmvOf9OmfmnnvOmUKhBimXyy0wCGOwBibSZRuElhpKVhdK0T6YglWoJIpRbF911XOiKNINM7AFSZFtMyTLr9zunCGyXSTqJ/0CXl7SuQPnoQPaQqTLJp9ivKjGWPZIKR4SSr4C+iKMwL6U8JhJMWGscryUYoFpHaKL4Af/Tn8cKg6crKecMPcnrYlqF5OxUR/nqEXSrsO5yOkU7I1wFHpCjjh3TMU/DP6NuRILsA5BnaBFJfkBZ81nLTbF3IO38A22QzTZeRixWN9iHwL7JTbQO7w/0DHOgsn4rgAMOK9ZQEart+FiRu6Ey5mNxeDoBXuVnqGnPnPsHaBvnScrOHdtRti0JlRborF6o0nQmQaJig9FjoSCT0Vm4Dr0wwA8hKSkbkIE6VHYF5gOymNoho8geQ31iXFjXfyxVUy/FbQevAzEksIOAfWgMSQas3kPvi5oDWPmisXiVqinNvjLCcdx+g3Oton+3vUjNaw9Fxo0Zpcm0BNFFAoLTq+o8g0aCboBe13wIwb64PpJ9akz9Ghl6xWSaGGcdM6KKvF3lejkHfqhvET8XWALfl0TMPmK0p6X7H3Eamv+bcm0n+GUj0nTiWkHjRXIX02ATC2m5zslgk9N5ELagEkbcbsmUPMjoMgJsJ0NtbwEfh0kx436xMUegRbhZOito632ERwk1q/8Fyy87bBOpaaNAI0lmZSyHKg7H3p/H7t+lrqK4yboldU3X4Jqpd8FLmsN1LQRWTJ5++E0HDZbpZbYBngDkmAjCnLoTMuC5G7FNgBxZ8B2T52gl82X1xKno9nenOkoFmNVh5ESiNWlZR68fKJzICqYouDPPozCwv44nkipEZgoVAf2M6IGovuDFlem4M8+jpVFgL+Q6BXLOxV1KfGiY9a/FbGJ4PMXEj2yzliAdXCMgskayrD5fIu9CW7DAjyAY97vdXx67qplMur9MZ0IPd+SRdLql/h/l1KbTWISms8iaO9PvSlZnlpidGHVfwTleCn5ONNjlwszqiXzEs0taFY/lFe0T2ARVkBnv6QJtAi1yegm3QEmGyhX2Snvm6Hqlklo354BOz5RI/mFppuQkJ4U5ShXF55/E4r0wRT83z+nyWkzgRYYhDHwK1u6bIOw60acrOP7fwAawRQpHCf63QAAAABJRU5ErkJggg==', PLUS: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAABuwAAAABAAAG7AAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACi3Qz1AAAACXBIWXMAARCEAAEQhAE/RUVgAAABbUlEQVRYCe2Xu0oDURCGPRo0oFgErKzsfAPBh0jrU1iKL2Fr4VMIgp2dD6GlhaRMIUm8RY/fv8rKCXtuZtdA2IEpZs4/lx1mfhKzkinW2g4hfXRvJnSAfWmMeZnx12vSwAnqk7N6q1Vko/Ktrzr+O1QTSpbVZGQa0AIzadBvVN0N5NQusG0D7QSWcgKfOadQkgYEskbgekJwaGrigE1yvUfyTKHsAlOQBgFHBJyi25FAPe+iXQ/uDf8jKkIKyYTHc5q4MBTvYdyjO6GIBt5eybmvceqrtxooEEu5AaCnBjSurMWJZU58V10bWqjEPPPB1IAWcRGNqK5R4Sd0hP63aAmHHU5hyCUcYyziDB8KHtCn00QqEd0AP1RMheicD9BkIiqZkEl8EPhckdRx0WjoYrTZ459cTpzPaGL5snJmgX1fMY+/baCdwNJNQMQW+zHiHM1fJnDlZHCNa0ho6rrCVknFYdjvK0wo9qzt7/kXIMaNW1QFn78AAAAASUVORK5CYII=' } } function setupWLError() { window.wlerror = window.onerror; window.onerror = windowError; window.timeDomContentReady = new Date().getTime(); log("Time DOM content ready " + (timeDomContentReady - timeUserscriptStart) / 1000); log("DOM content ready"); window.WLError = function (a, b) { logError(a); null == a && (a = ""); console.log("WLError: " + a + ", silent=" + b); if (-1 != a.indexOf("NotAuth")) { location.reload(); } else if (-1 != a.indexOf("WarLight Server returned CouldNotConnect")) { if (typeof CNCDialog === 'function') CNCDialog(); } else if ( -1 == a.indexOf("TopLine is not defined") && -1 == a.indexOf("_TPIHelper") && -1 == a.indexOf("Syntax error, unrecognized expression: a[href^=http://]:not([href*=") && -1 == a.indexOf("y2_cc2242") && -1 == a.indexOf("Error calling method on NPObject") ) { if (-1 != a.indexOf("WARLIGHTERROR48348927984712893471394")) a = "ServerError"; else if (-1 != a.indexOf("WARLIGHTHEAVYLOAD48348927984712893471394")) a = "HeavyLoad"; if (typeof ReportError === 'function') ReportError(a); if (!b && typeof PopErrorDialog === 'function') PopErrorDialog(a); } } } function hideCoinsGlobally() { $("#LeaderboardTable").prev().remove(); $("#LeaderboardTable").css({ opacity: 0, cursor: 'default' }); $("#LeaderboardTable a").css('display', 'none'); $("body").find("a[href='/Coins/']").css('display', 'none'); $("a[href='/Win-Money']").css('display', 'none'); $("#OpenTournamentTable").css('display', 'none'); } const totalPointsPerLevel = { 1: 0, 2: 7000, 3: 14300, 4: 22100, 5: 30200, 6: 38800, 7: 47900, 8: 57400, 9: 67400, 10: 77900, 11: 89000, 12: 100700, 13: 113000, 14: 125900, 15: 139500, 16: 153800, 17: 168900, 18: 184800, 19: 201500, 20: 219100, 21: 237600, 22: 257000, 23: 277500, 24: 299100, 25: 321800, 26: 345700, 27: 370800, 28: 397200, 29: 425100, 30: 454400, 31: 485200, 32: 517700, 33: 551900, 34: 587800, 35: 625700, 36: 665500, 37: 707400, 38: 751600, 39: 798000, 40: 846900, 41: 898300, 42: 952400, 43: 1009400, 44: 1069400, 45: 1132500, 46: 1198900, 47: 1268800, 48: 1342400, 49: 1419800, 50: 1501300, 51: 1587100, 52: 1677400, 53: 1772400, 54: 1872400, 55: 2092800, 56: 2571800, 57: 3402400, 58: 4710900, 59: 6668800, 60: 9509500, 61: 13549800, 62: 19220700, 63: 27107600, 64: 38006500, 65: 52998900, 66: 73554900, 67: 101672700, 68: 140067600, 69: 192430500, 70: 263777500 }; function displayTotalPointsEarned() { var $status = $(".container.px-4").first(); var match = /level\s+(\d+)[^(]*\(([\d,]+)\s*\/\s*[\d,]+\)/i.exec($status.text()); if (!match || totalPointsPerLevel[match[1]] === undefined) { return; } var totalPoints = totalPointsPerLevel[match[1]] + Number(match[2].replace(/,/g, "")); $status.append(`
In total, you've earned ${totalPoints.toLocaleString("en")} points.`); } function hideExtraBlanks() { var content = $(".container .my-2:first-of-type div.p-3"); var replacement = '

'; content.html(content.html().replace(/(){3,}/gi, replacement)) } function foldProfileStats() { addCSS(` h3.expander { cursor: pointer; } `); $.each($("big").parent().contents(), function (key, val) { if (val.nodeType == 3) { $(val).wrap(""); } }); $.each($(".container .my-2:first-of-type div.p-3 h3"), function (key, val) { $(val).addClass("expander"); $(val).nextUntil("h3").wrapAll("
") }); $('h3.expander').click(function (e) { $(this).next().slideToggle(); }); } function loadCommunityLevelRecords() { var playerId = location.href.match(/p=(\d+)/)[1]; $.ajax({ type: 'GET', url: `https://maak.ch/wl/v2/api.php?player=${playerId}`, dataType: 'jsonp', crossDomain: true }).done(function (response) { if (response.data) { var records = response.data; $("h3:contains('Single-player stats')").after(`Community Levels: ${htmlEscape(records)} record${records != 1 ? "s" : ""}`); } }).fail(function (e) { log("Error loading community level records: " + e.statusText); }); } function loadPrivateNotes() { var url = $(".container a[href*='Discussion/Notes']").prop("href"); if (!url) { return; } log("Loading private notes"); $("#FeedbackMsg").after('

Private Notes

Loading private notes...

'); var content = $("#privateNotes .content"); $.ajax({ url: url, dataType: "text" }).done(function (html) { var note = new DOMParser().parseFromString(html, "text/html").getElementById("PostForDisplay_0"); if (note && note.textContent.trim()) { $(note).find("script").remove(); content.empty().append(document.importNode(note, true).childNodes); } else { content.text("You don't have any Private Notes."); } }).fail(function (xhr) { log("Error loading private notes: " + (xhr && xhr.statusText)); content.text("Couldn't load your private notes."); }); } // Resolves with null for a player or tournament wl-stats doesn't know (404), so nothing is shown for it. function fetchWlStats(url, maxAgeMs) { var key = "tidyUpYourDashboard.wlStats:" + url; try { var cached = JSON.parse(sessionStorage.getItem(key)); if (cached && Date.now() - cached.at < (maxAgeMs || WL_STATS_CACHE_MS)) { return $.Deferred().resolve(cached.data).promise(); } } catch (e) { } var remember = function (data) { try { sessionStorage.setItem(key, JSON.stringify({ at: Date.now(), data: data })); } catch (e) { } return data; }; // jQuery 1.x's then() keeps a failure failed unless the filter returns a resolved promise. return $.ajax({ url: url, dataType: "json" }).then(remember, function (xhr) { return xhr && xhr.status === 404 ? $.Deferred().resolve(remember(null)) : $.Deferred().reject(xhr); }); } function setupAwpProfileStats() { var match = location.href.match(/[?&]p=(\d+)/i); if (!match) { return; } fetchWlStats(AWP_SITE + "/api/v1/players/" + match[1] + "/stats").done(function (player) { if (player && player.stats && !$("#awpStats").length) { placeProfileBox("awpStats", renderAwpStats(player)); } }).fail(function (xhr) { log("Error loading AWP World Tour stats: " + (xhr && xhr.statusText)); }); } // The boxes from other sites keep this order below the feedback area, whichever answers first. function placeProfileBox(id, html) { var order = ["privateNotes", "awpStats", "customStats"]; var above = order.slice(0, order.indexOf(id)).map(function (other) { return $("#" + other); }).filter(function (box) { return box.length > 0; }).pop(); (above || $("#FeedbackMsg")).first().after(html); } function setupCustomProfileStats() { var match = location.href.match(/[?&]p=(\d+)/i); if (!match) { return; } fetchWlStats(CUSTOM_SITE + "/api/v1/players/" + match[1] + "/stats").done(function (player) { if (player && player.stats && !$("#customStats").length) { placeProfileBox("customStats", renderCustomStats(player, CUSTOM_SITE + "/players/" + match[1])); } }).fail(function (xhr) { log("Error loading custom.wl-stats.com stats: " + (xhr && xhr.statusText)); }); } function renderCustomStats(player, pageUrl) { var stats = player.stats; var rows = [ ["Games", stats.games ? htmlEscape(stats.games) + " (" + htmlEscape(stats.wins) + " W, " + htmlEscape(stats.losses) + " L, " + htmlEscape(stats.win_pct) + "%)" : "None yet"], ["Tournaments", htmlEscape(stats.tournaments || 0)], ["Titles", htmlEscape(stats.titles || 0) + (stats.finals ? " (" + htmlEscape(stats.finals) + " final" + (stats.finals == 1 ? "" : "s") + ")" : "")] ]; if (stats.best_placement) { rows.push(["Best placement", htmlEscape(getRankText(stats.best_placement))]); } if (stats.organized) { rows.push(["Organized", htmlEscape(stats.organized) + " tournament" + (stats.organized == 1 ? "" : "s")]); } var formats = (Array.isArray(stats.by_format) ? stats.by_format.slice() : []).filter(function (format) { return format && format.played; }).sort(function (a, b) { return b.played - a.played; }).slice(0, 3); formats.forEach(function (format) { var lost = Number(format.played) - Number(format.won); rows.push([htmlEscape(format.label), htmlEscape(format.won) + "–" + htmlEscape(isNaN(lost) ? "?" : lost) + " (" + htmlEscape(format.win_pct) + "%)"]); }); return '

Community tournaments

' + rows.map(function (row) { return ''; }).join("") + '
' + row[0] + '' + row[1] + '
More on custom.wl-stats.com
'; } function renderAwpStats(player) { var stats = player.stats; var rank = function (value) { return value ? "#" + htmlEscape(value) : "Unranked"; }; var rows = [ [htmlEscape(player.rank_year || "") + " race", rank(player.rank)], ["Last 12 months", rank(player.rank_12_months)], ["Games", stats.games ? htmlEscape(stats.games) + " (" + htmlEscape(stats.wins) + " W, " + htmlEscape(stats.losses) + " L, " + htmlEscape(stats.win_pct) + "%)" : "None yet"], ["Tournaments", htmlEscape(stats.tournaments || 0)], ["Titles", htmlEscape(stats.titles || 0) + (stats.finals ? " (" + htmlEscape(stats.finals) + " final" + (stats.finals == 1 ? "" : "s") + ")" : "")] ]; if (stats.world_titles) { rows.push(["World titles", htmlEscape(stats.world_titles)]); } if (stats.best_placement) { rows.push(["Best placement", htmlEscape(getRankText(stats.best_placement))]); } return '

AWP World Tour

' + rows.map(function (row) { return ''; }).join("") + '
' + row[0] + '' + row[1] + '
More on awp.wl-stats.com
'; } function signedInPlayerId() { var result = $.Deferred(); playerDataReady.done(function (player) { result.resolve(player && player.PlayerId ? String(player.PlayerId) : null); }).fail(function () { result.resolve(null); }); return result.promise(); } // war.app draws the Ladders page client-side, in a 600px viewport that clips what overflows it. The // panel sits in the full-width area around that viewport instead, below the ladders dialog, so on a // wide window its two lists (players, clans) go side by side; its rows scroll when the window is short. function setupCommunityLadders() { var rows = null; var community = null; var show = function () { var scene = pageIsLadders() && ujsElement("LaddersScene"); if (!scene || !community) { return; } addCommunityLaddersStyles(); var dialog = $(scene).find("[id^='ujs_DialogBackgroundForDropShadow'][id$='_img']").last(); if (!dialog.length || !(rows.players.length + rows.clans.length) || $("#communityLadders").length) { return; } var panel = $(renderCommunityLadders(rows)); var art = warappArt(scene); Object.keys(art).forEach(function (name) { if (art[name]) { panel[0].style.setProperty(name, art[name]); } }); var holder = $("#UjsCanvasHolder"); (holder.length ? holder : dialog.parent().parent()).append(panel); fitCommunityLadders(panel, dialog); // war.app re-lays out the dialog shortly after a resize, so follow the dialog and the page area. if (window.ResizeObserver) { var observer = new ResizeObserver(function () { fitCommunityLadders(panel, dialog); }); observer.observe(dialog[0]); observer.observe(panel.parent()[0]); } }; var load = function () { if (rows || !pageIsLadders()) { return; } rows = { players: [], clans: [] }; loadCommunityData().done(function (data) { if (data.rankings) { rows.players.push(renderAwpLadderRow(data.rankings, data.playerId)); } inClanSeriesOrder(data.series).forEach(function (series) { if (series.current) { rows[CLAN_SERIES_KINDS.indexOf(series.kind) !== -1 ? "clans" : "players"].push(renderSeriesLadderRow(series, Date.now())); } }); community = data; show(); }); }; var check = function () { load(); show(); }; check(); new MutationObserver(check).observe(document.body, { childList: true, subtree: true }); } // Two columns need about 940px; below that the panel takes the dialog's width and its lists stack. function fitCommunityLadders(panel, dialog) { var box = dialog[0].getBoundingClientRect(); var viewport = panel.closest("[id^='ujs_Viewport']"); if (viewport.length) { panel.css("width", box.width + "px"); } else { var area = panel.parent()[0].getBoundingClientRect(); var room = area.width - 32; var width = room >= 940 ? Math.min(room, 1040) : box.width; var left = Math.max(0, Math.min(box.left - area.left + (box.width - width) / 2, area.width - width)); panel.css({ width: width + "px", left: Math.round(left) + "px", top: Math.round(box.bottom - area.top + 10) + "px" }); } var bottom = viewport.length ? viewport[0].getBoundingClientRect().bottom : Math.min(window.innerHeight, panel.parent()[0].getBoundingClientRect().bottom); var list = panel.find(".communityLaddersRows"); var room = bottom - list[0].getBoundingClientRect().top - panel.find(".communityLaddersCredit").outerHeight() - 20; list.css("max-height", Math.max(66, room) + "px"); } // The panels wear war.app's own dialog, row and button art, found by file name on the page so they // follow war.app's updates. Their buttons use war.app's blue tint instead of its green, so the // panels read as additions; war.app's CDN serves only the tints the game itself uses. function warappArt(scene) { var sources = $(scene).find("[id$='_img']").map(function () { var value = $(this).css("border-image-source") || ""; return /^url\(/.test(value) ? value : null; }).get(); var find = function (pattern) { return sources.filter(function (value) { return pattern.test(value); })[0] || ""; }; var button = find(/UIButton-[0-9A-Fa-f]{6}\.png/); return { "--tuyd-dialog": find(/\/DialogBacking\.png/), "--tuyd-row": find(/\/DialogForeground\.png/), "--tuyd-button": button.replace(/UIButton-[0-9A-Fa-f]{6}/, "UIButton-242D9A") }; } function addCommunityLaddersStyles() { if (addCommunityLaddersStyles.done) { return; } addCommunityLaddersStyles.done = true; addCSS(` .tuydCommunityPanel { container: tuydLadders / inline-size; position: absolute; top: 10px; left: 0; box-sizing: border-box; pointer-events: auto; color: #babbbb; font-size: 14px; line-height: 18px; border-style: solid; border-color: rgba(0, 0, 0, 0.6); border-width: 54px 18px 12px 17px; border-image: var(--tuyd-dialog, none) 82 27 19 26 fill / 54px 18px 12px 17px stretch; } .tuydCommunityPanel h3 { position: absolute; top: -46px; left: 0; right: 0; height: 38px; margin: 0; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; font-weight: 700; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; } .tuydCommunityPanel .communityLaddersRows { overflow-y: auto; scrollbar-width: thin; scrollbar-color: #555 transparent; } .tuydCommunityPanel .communityLaddersSection h4 { margin: 2px 4px 6px; color: #fff; font-size: 14px; font-weight: 700; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; } @container tuydLadders (min-width: 900px) { .tuydCommunityPanel .communityLaddersRows { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 0 14px; } } .tuydCommunityPanel .communityLadder { display: flex; align-items: center; gap: 8px; min-height: 66px; box-sizing: border-box; margin-bottom: 6px; border-style: solid; border-color: rgba(255, 255, 255, 0.08); border-width: 12px 10px 10px; border-image: var(--tuyd-row, none) 19 15 15 16 fill / 12px 10px 10px stretch; } .tuydCommunityPanel .communityLadderButton { flex: 0 0 160px; max-width: 35%; min-height: 50px; margin: -4px 0 -2px -2px; padding: 0 4px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #fff; font-size: 15px; font-weight: 700; line-height: 16px; text-decoration: none; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; border-style: solid; border-color: #242d9a; border-width: 5px 5px 4px; background: #242d9a padding-box; border-image: var(--tuyd-button, none) 15 15 13 fill / 5px 5px 4px stretch; } .tuydCommunityPanel .communityLadderButton:hover { filter: brightness(1.15); } .tuydCommunityPanel .communityLadderText { flex: 1; min-width: 0; } .tuydCommunityPanel .tuydMuted { color: #8d8d8d; } .tuydCommunityPanel .communityLadderYou { color: #fff; } .tuydCommunityPanel .communityLaddersCredit { text-align: right; font-size: 11px; color: #777; } .tuydCommunityPanel .communityLadderSignups, .tuydCommunityPanel .tuydClanTitles, .tuydCommunityPanel .tuydClanTitles a { color: #e0c060; } .tuydCommunityPanel .tuydClanTitles a { white-space: nowrap; } .tuydEventStanding, .tuydEventMore { position: absolute; right: 12px; text-align: right; pointer-events: auto; color: #fff; font-size: 13px; line-height: 20px; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; } .tuydEventStanding { top: 6px; max-width: calc(100% - 300px); } .tuydEventStanding .tuydEventLine:first-child { color: #babbbb; } .tuydEventMore { top: 62px; max-width: calc(100% - 190px); } .tuydEventLine { margin-left: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tuydEventStanding .communityLadderSignups { color: #e0c060; } .tuydEventStanding .tuydMuted { color: #8d8d8d; } .tuydHasEventBadge { position: relative; } .tuydEventBadgeCorner { position: absolute; top: 6px; right: 6px; z-index: 2; line-height: 16px; } .tuydNameBesideBadge { box-sizing: border-box; padding-right: 120px !important; } .tuydEventBadge { display: inline-block; max-width: 110px; padding: 1px 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; border-radius: 3px; background: #242d9a; color: #fff !important; font-size: 12px; line-height: 16px; text-decoration: none !important; } .tuydEventBadge:hover { filter: brightness(1.2); } .tuydEventBadge.tuydEventBadgeAwp { background: #e0c060; color: #1b1b1b !important; } @container tuydEventCard (max-width: 599.98px) { .tuydEventStanding, .tuydEventMore { display: none; } } @media (max-width: 600px) { .tuydCommunityPanel h3 { font-size: 14px; white-space: nowrap; } .tuydCommunityPanel .communityLadderText { font-size: 13px; line-height: 16px; } } `); } function renderCommunityLadders(rows) { var section = function (title, list) { return list.length ? '

' + title + '

' + list.join("") + '
' : ""; }; return '

Community ladders & leagues

' + section("Players", rows.players) + section("Clans & nations", rows.clans) + '
Added by Muli\'s Userscript
'; } // statusHtml, detailHtml and youHtml must already be escaped. function communityLadderRow(row) { return '
' + htmlEscape(row.name) + '' + '
' + (row.statusHtml ? '
' + row.statusHtml + '
' : "") + (row.detailHtml ? '
' + row.detailHtml + '
' : "") + (row.youHtml ? '
' + row.youHtml + '
' : "") + '
'; } // Resolves with the AWP ranking and the custom.wl-stats.com series, each left empty when its site // can't be reached, so one being down doesn't hide the other. function settleRequest(request, what) { var settled = $.Deferred(); request.done(function (data) { settled.resolve(data); }).fail(function (xhr) { log("Error loading " + what + ": " + (xhr && xhr.statusText)); settled.resolve(null); }); return settled.promise(); } function loadCommunityData() { var result = $.Deferred(); signedInPlayerId().done(function (playerId) { $.when( settleRequest(fetchWlStats(AWP_SITE + "/api/v1/rankings"), "the AWP World Tour rankings"), settleRequest(fetchWlStats(CUSTOM_SITE + "/api/v1/series" + (playerId ? "?player=" + playerId : ""), SERIES_CACHE_MS), "the custom.wl-stats.com series") ).done(function (rankings, series) { result.resolve({ playerId: playerId, rankings: rankings && rankings.rankings ? rankings : null, series: series && Array.isArray(series.series) ? series.series : [] }); }); }); return result.promise(); } function formatCount(value) { return htmlEscape(typeof value === "number" ? value.toLocaleString("en-US") : value); } // custom.wl-stats.com sends UTC times without a zone; read them as UTC, not local time. function parseUtcTime(iso) { var text = String(iso || ""); var date = new Date(/(?:Z|[+-]\d\d:?\d\d)$/i.test(text) ? text : text + "Z"); return isNaN(date.getTime()) ? null : date; } function formatEventTime(iso) { var date = parseUtcTime(iso); return date ? date.toLocaleString(undefined, { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) : ""; } function timeUntil(iso, now) { var date = parseUtcTime(iso); if (!date) { return ""; } var minutes = Math.round((date.getTime() - now) / 60000); if (minutes <= 0) { return "now"; } if (minutes < 60) { return "in " + minutes + " min"; } var hours = Math.round(minutes / 60); if (hours < 36) { return "in " + hours + " hour" + (hours === 1 ? "" : "s"); } var days = Math.round(hours / 24); return "in " + days + " day" + (days === 1 ? "" : "s"); } function seriesLink(link) { return link && link.name ? '' + htmlEscape(link.name) + '' : ""; } function awpStanding(rankings, playerId) { return playerId && rankings ? rankings.rankings.filter(function (entry) { return String(entry.player_id) === playerId; })[0] : null; } function signupsHtml(current) { return 'Signups open' + (current.signups_close_at ? " until " + htmlEscape(formatEventTime(current.signups_close_at)) : "") + ''; } function seriesYouHtml(series) { var you = series.you; var who = { nation: "Your country ", clan: "Your clan " }[you.as]; var link = function (text) { return '' + htmlEscape(text) + ''; }; var label = who ? who + link(you.name) : link("You"); if (you.status === "signed_up") { return label + ": signed up"; } var parts = standingParts(series.kind, you); return parts.length ? label + ": " + parts.join(" · ") : label; } function standingParts(kind, you) { var parts = []; if (you.division) { // Clan League sends a bare division letter ("A"); leagues send "Division A". parts.push(htmlEscape(/^[A-Z0-9]{1,2}$/i.test(String(you.division)) ? "Division " + you.division : you.division)); } if (you.group && you.group !== you.division) { parts.push(htmlEscape(you.group)); } if (you.place) { parts.push(htmlEscape(getRankText(you.place)) + (you.of ? " of " + htmlEscape(you.of) : "")); } if (you.status === "signed_up") { parts.push("signed up"); } if (you.wins != null && you.losses != null) { parts.push(htmlEscape(you.wins) + "–" + htmlEscape(you.losses)); } if (you.points != null) { parts.push(kind === "ladder" ? "rating " + formatCount(you.points) : formatCount(you.points) + " points"); } if (["promoted", "relegated", "eliminated", "champion"].indexOf(you.status) !== -1) { parts.push(htmlEscape(you.status)); } return parts; } function seriesStatusHtml(series) { var current = series.current; if (series.kind === "events") { return weeklyEventStatusHtml(current); } var units = { nations_cup: "teams", clan_cup: "clans", clan_league: "clans" }[series.kind] || "players"; var status = [htmlEscape(current.name), current.status === "signups" ? signupsHtml(current) : current.status === "running" ? "running" : "finished"]; if (current.players) { status.push(formatCount(current.players) + " " + units); } if (current.progress) { status.push(formatCount(current.progress.played) + "/" + formatCount(current.progress.total) + (series.kind === "clan_cup" ? " matches" : " games")); } return status.join(" · "); } function seriesDetailHtml(series, now) { var current = series.current; var details = []; var finished = current.status === "finished"; var lead = series.kind === "events" ? null : finished ? current.winner : current.leader; // Cups played in rounds or stages have too many names to list; the stage, linking to the bracket, says where they stand. var standings = STAGED_SERIES_KINDS.indexOf(series.kind) !== -1 ? (current.status === "running" && current.stage ? siteLink(current.url, current.stage) : "") : standingsHtml(current.standings); if (standings) { details.push(standings); } else if (lead && lead.name) { details.push((finished ? "Winner: " : "Leader: ") + seriesLink(lead)); } if (current.next_event_at && parseUtcTime(current.next_event_at)) { details.push("Next: " + htmlEscape(formatEventTime(current.next_event_at)) + " (" + htmlEscape(timeUntil(current.next_event_at, now)) + ")"); } return details.join(" · "); } // A weekly series is about its last (or current) event and the next one, not a winner. function weeklyEventStatusHtml(current) { var running = current.status === "running"; var status = [(running ? "Now: " : "Last: ") + seriesLink({ name: current.name, url: current.url })]; if (current.players) { status.push(formatCount(current.players) + " players"); } if (running && current.progress) { status.push(formatCount(current.progress.played) + "/" + formatCount(current.progress.total) + " games"); } return status.join(" · "); } // custom.wl-stats.com's short standings for formats with no single leader: a division's top // three or each group's leader. function standingsHtml(standings) { var entries = standings && Array.isArray(standings.entries) ? standings.entries.filter(function (entry) { return entry && entry.name; }) : []; if (!entries.length) { return ""; } return (standings.label ? htmlEscape(standings.label) + ": " : "") + entries.map(function (entry) { var prefix = entry.group ? '' + htmlEscape(entry.group) + ' ' : entry.place ? htmlEscape(entry.place) + ". " : ""; var score = entry.points != null ? " " + formatCount(entry.points) : entry.wins != null && entry.losses != null ? " " + htmlEscape(entry.wins) + "–" + htmlEscape(entry.losses) : ""; return prefix + (entry.url ? seriesLink(entry) : htmlEscape(entry.name)) + score; }).join(" · "); } function renderSeriesLadderRow(series, now) { var current = series.current; var you = series.you ? seriesYouHtml(series) : current.status === "signups" ? 'Sign up on custom.wl-stats.com' : ""; return communityLadderRow({ name: series.name, url: series.url, statusHtml: seriesStatusHtml(series), detailHtml: seriesDetailHtml(series, now), youHtml: you }); } function renderAwpLadderRow(rankings, playerId) { var mine = awpStanding(rankings, playerId); var you = !playerId ? "" : mine ? "You: #" + htmlEscape(mine.rank) + " · " + formatCount(mine.points) + " points" : "You: not in the " + htmlEscape(rankings.year) + " race yet"; return communityLadderRow({ name: "AWP World Tour", url: AWP_SITE + "/rankings", statusHtml: htmlEscape(rankings.year) + " race · " + htmlEscape(rankings.rankings.length) + " players", youHtml: you }); } // war.app's Community Events page lists the events as client-drawn cards; each card that matches a // series gets your line in it, at the top right next to war.app's title. function setupCommunityEventsStanding() { var community = null; var loading = false; var check = function () { if (!pageIsCommunityEvents()) { return; } if (!community) { if (!loading) { loading = true; loadCommunityData().done(function (data) { community = data; check(); }); } return; } var scene = ujsElement("CommunityEventsScene"); if (!scene) { return; } addCommunityLaddersStyles(); $(scene).find("[id^='ujs_EventRow']").filter(function () { return /^ujs_EventRow(?:_\d+)?$/.test(this.id); }).each(function () { var title = $(this).find("[id^='ujs_vert'] .ujsTextInner").first().text().trim(); markJoinedEvent(this, joinedEventLabel(title, community)); var box = $(this).children("[id$='_img']").first(); if (box.length && box.children(".tuydEventStanding").length) { // Only when the card's width changed: war.app redraws the scene often. var width = Math.round(box[0].getBoundingClientRect().width); if (box.attr("data-tuyd-width") !== String(width)) { box.attr("data-tuyd-width", width); fitEventLines(this, box[0]); } return; } if (!box.length) { return; } var html = communityEventHtml(title, community); if (html) { box.css("container", "tuydEventCard / inline-size").append(html); // The lines don't wrap, so war.app's own text keeps its room; a cut line shows in full on hover. box.find(".tuydEventLine").each(function () { this.title = this.textContent; }); box.attr("data-tuyd-width", Math.round(box[0].getBoundingClientRect().width)); fitEventLines(this, box[0]); } }); }; check(); new MutationObserver(check).observe(document.body, { childList: true, subtree: true }); } // The room a line on an event card has, left of it, beside war.app's own text at the same height: // from the card's right edge to the end of the furthest war.app text the line sits next to. function eventLineRoom(line, texts, right) { var furthest = texts.filter(function (text) { return text.bottom > line.top + 2 && text.top < line.bottom - 2; }).reduce(function (max, text) { return Math.max(max, text.right); }, -Infinity); return furthest === -Infinity ? null : right - furthest - 16; } function fitEventLines(row, box) { if (typeof Range === "undefined" || typeof Range.prototype.getClientRects !== "function") { return; } var texts = []; $(row).find("[id^='ujs_vert'] .ujsTextInner").each(function () { var range = document.createRange(); range.selectNodeContents(this); Array.prototype.forEach.call(range.getClientRects(), function (rect) { texts.push(rect); }); }); var right = box.getBoundingClientRect().right - 12; $(box).find(".tuydEventLine").each(function () { this.style.display = ""; this.style.maxWidth = ""; var room = eventLineRoom(this.getBoundingClientRect(), texts, right); if (room !== null) { this.style.maxWidth = Math.max(0, room) + "px"; this.style.display = room < 60 ? "none" : ""; } }); } // war.app tells everyone "You can join!" in green; where you already take part, the card says so // instead, in its own colour. function joinedEventLabel(title, community) { var colors = { in: "#e0c060", signedUp: "#6cc4f0", tookPart: "#babbbb" }; if (/^AWP World Tour/i.test(title)) { return community.rankings && awpStanding(community.rankings, community.playerId) ? { text: "You're in the race!", color: colors.in } : null; } var series = community.series.filter(function (entry) { return entry.warapp_event && entry.current && title.toLowerCase().indexOf(String(entry.warapp_event).toLowerCase()) === 0; })[0]; if (!series || !series.you) { return null; } return series.you.status === "signed_up" ? { text: "You're signed up!", color: colors.signedUp } : series.current.status === "finished" ? { text: "You took part", color: colors.tookPart } : { text: "You're in!", color: colors.in }; } function markJoinedEvent(row, label) { if (!label) { return; } $(row).find("[id^='ujs_vert'] .ujsTextInner").filter(function () { return $(this).text().trim() === "You can join!"; }).text(label.text).css({ width: "auto", whiteSpace: "nowrap", color: label.color }); } // Each card gets the event's standings beside war.app's title and player count, and your place, // or a link to the full standings, beside "You can join!". function communityEventHtml(title, community) { var lines = /^AWP World Tour/i.test(title) ? awpEventLines(community) : seriesEventLines(title, community); if (!lines) { return ""; } var line = function (html) { return html ? '
' + html + '
' : ""; }; return '
' + line(lines.status) + line(lines.detail) + '
' + (lines.bottom ? '
' + line(lines.bottom) + '
' : ""); } function siteLink(url, text) { return '' + htmlEscape(text) + ' ›'; } function awpEventLines(community) { var rankings = community.rankings; if (!rankings) { return null; } var mine = awpStanding(rankings, community.playerId); // The race's top 16 qualify for the AWP Tour Finals. return { status: htmlEscape(rankings.year) + " race · " + formatCount(rankings.rankings.length) + " players", detail: siteLink(AWP_SITE + "/rankings", "Rankings: Top 16"), bottom: mine ? "You: #" + htmlEscape(mine.rank) + " · " + formatCount(mine.points) + " points" : community.playerId ? "You: not in the " + htmlEscape(rankings.year) + " race yet" : "" }; } function seriesEventLines(title, community) { var series = community.series.filter(function (entry) { return entry.warapp_event && entry.current && title.toLowerCase().indexOf(String(entry.warapp_event).toLowerCase()) === 0; })[0]; if (!series) { return null; } var current = series.current; var status = seriesStatusHtml(series); // war.app's title often is the edition's name already ("Clan League 19"). if (current.name && title.toLowerCase() === String(current.name).toLowerCase()) { status = status.slice(htmlEscape(current.name).length + " · ".length); status = status.charAt(0).toUpperCase() + status.slice(1); } return { status: status, detail: seriesDetailHtml(series, Date.now()), bottom: series.you ? seriesYouHtml(series) : moreLinkHtml(series) }; } // The card's link beside "You can join!", unless a line above already opens the same page. function moreLinkHtml(series) { var current = series.current; if (series.kind === "events") { return siteLink(series.url, "All events on custom.wl-stats.com"); } if (STAGED_SERIES_KINDS.indexOf(series.kind) !== -1 && current.status === "running" && current.stage) { return ""; } return siteLink(current.url, current.status === "signups" ? "Sign up on custom.wl-stats.com" : "Full standings on custom.wl-stats.com"); } function displayTrophies() { var trophies = { 5286630035: ["Get on the immediate roadmap"] }; Object.keys(trophies).forEach(playerId => { if (window.location.href.indexOf(playerId) != -1) { trophies[playerId].forEach(text => { $("h3:contains('Achievements ')").next().find("tbody").prepend(' Trophy: ' + text + ' ') }) } }); } function databaseReady() { log("Running main"); runFeatures(mainFeatures()); } function setupMaakFeatures() { runFeatures(maakFeatures()); } function setupForumOverviewFilters() { ifSettingIsEnabled("hideOffTopic", function () { hideOffTopicThreads() }); ifSettingIsEnabled("hideWarzoneIdle", function () { hideWarzoneIdleThreads() }); formatHiddenThreads(); } function setupForumThreadHiding() { setupSpammersBeGone(); } function setupDashboard() { playerDataReady.done(setupVacationAlert); hideBlacklistedThreads(); setupBasicDashboardStyles(); var dashboard = fetchDashboard(); refreshMyGames(dashboard); ifSettingIsEnabled('hideCoinsGlobally', function () { hideCoinsGlobally() }); ifSettingIsEnabled('useDefaultBootLabel', function () { createSelector(".BootTimeLabel", "z-index:50;"); }, function () { createSelector(".BootTimeLabel", "color:white !important;font-weight:normal!important;font-style:italic;font-size:13px!important;z-index:50;"); }); ifSettingIsEnabled("hideMyGamesIcons", function () { createSelector("#MyGamesTable td div > img, #MyGamesTable td div a > img", "display:none;"); }); ifSettingIsEnabled("scrollGames", function () { setupFixedWindowWithScrollableGames(); }, function () { createSelector("body", "overflow: auto"); addCSS(` @media (max-width: 1050px) { #MyGamesTable > thead > tr * { font-size: 14px; } #MyGamesTable > thead > tr > td > div:nth-of-type(1) { margin-top: 5px!important; display: block; float: left; padding-right: 5px; } } @media (max-width: 750px) { #MyGamesTable > thead > tr > td > div:nth-of-type(1) { display:none; } }`) }, function () { setupRightColumn(true); refreshOpenGames(dashboard); }); $("label#MultiDayRadio").on("click", function () { registerGameTabClick() }); $("label#RealTimeRadio").on("click", function () { registerGameTabClick() }); $("label#BothRadio").on("click", function () { registerGameTabClick() }); $(window).resize(function () { ifSettingIsEnabled("scrollGames", function () { refreshSingleColumnSize(); }); }); window.setTimeout(setupRefreshFunction, 0); } function pageFeatures() { return [ { name: "dashboard loader", match: pageIsDashboard, run: showDashboardLoader }, { name: "Formatting toolbar for posts", section: "Forum", match: pageIsThread, run: setupTextarea }, { name: "Bookmark link and layout tweaks in threads", section: "Forum", match: isAnyForumThread, run: setupForumThreadTweaks }, { name: "Level previews in posts", section: "Forum", match: isAnyForumThread, run: parseForumSPLevels }, { name: "Bookmark link and player search", section: "Tournaments", run: setupTournamentPage }, { name: "custom.wl-stats.com link", section: "Tournaments", run: setupCustomTournamentLink }, { name: "Community ladders on the Ladders page", section: "Community", run: setupCommunityLadders }, { name: "Your standing on Community Events", section: "Community", run: setupCommunityEventsStanding }, { name: "Community event standings on clan pages", section: "Clans", run: setupClanStandings }, { name: "Sortable Common Games table", section: "Other", match: pageIsCommonGames, run: setupCommonGamesDataTable }, { name: "Clan list search", section: "Clans", match: pageIsClanList, run: setupClanListSearch }, { name: "Map search", section: "Maps & levels", match: pageIsMapsPage, run: setupMapSearch }, { name: "Level bookmark link", section: "Maps & levels", match: pageIsLevelOverview, run: setupLevelBookmark }, { name: "Collapse blank lines", section: "Profile", match: pageIsProfile, run: hideExtraBlanks }, { name: "Trophies", section: "Profile", match: pageIsProfile, run: displayTrophies }, { name: "Collapsible stat sections", section: "Profile", match: pageIsProfile, run: foldProfileStats }, { name: "AWP World Tour stats", section: "Profile", match: pageIsProfile, run: setupAwpProfileStats }, { name: "custom.wl-stats.com stats", section: "Profile", match: pageIsProfile, run: setupCustomProfileStats }, { name: "Sort and filter player search results", section: "Community", match: pageIsPlayerSearch, run: setupPlayerSearchSort }, { name: "Tags on players in search results", section: "Community", match: pageIsPlayerSearch, run: setupPlayerSearchTags }, { name: "Open a pasted profile link from player search", section: "Community", match: pageIsPlayerSearch, run: setupPlayerSearchShortcuts }, { name: "Your rank next to your rating on Quickmatch leaderboards", section: "Community", match: pageIsQuickmatch, run: setupLeaderboardRank }, { name: "Load the whole chat in games", section: "Games", match: pageIsGame, run: setupFullGameChat }, { name: "Player pace in game lobbies", section: "Games", match: pageIsGame, run: setupLobbyPace }, { name: "Private chat list: players still in the game first", section: "Games", match: pageIsGame, run: setupPrivateChatList }, { name: "Idle volume slider", section: "Idle", match: pageIsIdle, run: setupIdleVolume }, { name: "Log of Idle's pop-ups", section: "Idle", match: pageIsIdle, run: setupIdlePopupLog }, { name: "Check big market purchases in Idle", section: "Idle", match: pageIsIdle, run: setupIdleMarketCheck }, { name: "Confirm before using powers and artifacts in Idle", section: "Idle", match: pageIsIdle, run: setupIdleActionConfirm }, { name: "Better artifact copies and an artifact list in Idle", section: "Idle", match: pageIsIdle, run: setupIdleArtifactHelpers }, { name: "Warn when Idle is open in another tab", section: "Idle", match: pageIsIdle, run: setupIdleTabWarning }, { name: "Time to afford and best army camp in Idle", section: "Idle", match: pageIsIdle, run: setupIdleUpgradeHints }, { name: "What a tech is missing and its market cost in Idle", section: "Idle", match: pageIsIdle, run: setupIdleTechCost }, { name: "Count of things Idle needs you for in the tab title", section: "Idle", match: pageIsIdle, run: setupIdleAttention }, { name: "global styles", run: setGlobalStyles }, { name: "Map of the Week table fix", section: "Maps & levels", match: pageIsMapOfTheWeek, run: setupMapOfTheWeekStyles } ]; } function mainFeatures() { return [ { name: "Hidden thread formatting", section: "Forum", match: pageIsForumOverview, run: setupForumOverviewFilters }, { name: "communitylevels.online banner", section: "Maps & levels", match: pageIsCommunityLevels, run: setupCommunityLevels }, { name: "Hide-thread buttons", section: "Forum", match: function () { return pageIsForumOverview() || pageIsSubForum(); }, run: setupForumThreadHiding }, { name: "private notes", match: function () { return pageIsProfile() && $("#BlockListLink").length > 0; }, run: function () { ifSettingIsEnabled('showPrivateNotesOnProfile', loadPrivateNotes); } }, { name: "Hide ignored threads on Community page", section: "Community", match: pageIsCommunity, run: hideIgnoredForumThreadsFromCommnuityList }, { name: "Blacklist counter", section: "Other", match: pageIsBlacklistPage, run: setupBlacklistCounter }, { name: "Block list search", section: "Other", match: pageIsBlacklistPage, run: setupBlockListSearch }, { name: "Mail search by player", section: "Other", match: pageIsMyMail, run: setupMailSearch }, { name: "Total points earned", section: "Other", match: pageIsPointsPage, run: displayTotalPointsEarned }, { name: "dashboard", match: pageIsDashboard, run: setupDashboard }, { name: "Chat button on the dashboard", section: "Dashboard", match: pageIsDashboard, run: setupDashboardChatLink }, { name: "Game chat from the dashboard", section: "Dashboard", match: pageIsDashboard, run: setupDashboardGameChat }, { name: "Your-turn count in tab title and background checks", section: "Dashboard", match: pageIsDashboard, run: setupTurnAlerts }, { name: "Sort your-turn games by time left before boot", section: "Dashboard", match: pageIsDashboard, run: setupBootTimeSort }, { name: "Community event badges on My Games", section: "Dashboard", match: pageIsDashboard, run: setupEventBadges }, { name: "Open Games filter", section: "Dashboard", match: function () { return pageIsDashboard() || pageIsOpenGamesPage(); }, run: setupOpenGamesFilter }, { name: "Mark Open Games you joined", section: "Dashboard", match: function () { return pageIsDashboard() || pageIsOpenGamesPage(); }, run: setupJoinedOpenGames }, { name: "Refresh Open Games every few minutes", section: "Dashboard", match: pageIsDashboard, run: setupOpenGamesAutoRefresh }, { name: "Filters on Past Games", section: "Games", match: pageIsPastGames, run: setupPastGamesFilter }, { name: "Notes in games", section: "Games", match: pageIsGame, run: setupGameNotes }, { name: "commit confirmation", match: pageIsGame, run: function () { ifSettingIsEnabled('confirmCommit', setupCommitConfirm); } }, { name: "Read chat messages aloud", section: "Games", match: function () { return pageIsGame() || pageIsChatRoom(); }, run: setupChatSpeech }, { name: "Mute players and highlight mentions in game chat", section: "Games", match: pageIsGame, run: setupGameChatTools }, { name: "Chat rooms: hide events, mute players, highlight mentions", section: "Community", match: pageIsChatRoom, run: setupChatRoomTools }, { name: "Suggest names after @ in chat rooms", section: "Community", match: pageIsChatRoom, run: setupChatRoomMentionSuggest }, { name: "Suggest names after @ in game chat", section: "Games", match: pageIsGame, run: setupGameMentionSuggest }, { name: "hide coins", match: function () { return !pageIsDashboard(); }, run: function () { ifSettingIsEnabled('hideCoinsGlobally', hideCoinsGlobally); } }, { name: "maak.ch features", run: function () { ifSettingIsEnabled('useMaakServices', setupMaakFeatures); } }, { name: "New version notice", section: "Other", run: setupUpdateNotice } ]; } function maakFeatures() { return [ { name: "MTL ladder", match: pageIsCommunity, run: setupMtlLadderTable }, { name: "MTL forum tables", match: isAnyForumThread, run: setupMtlForumTable }, { name: "MTL profile stats", match: pageIsProfile, run: setupMtlProfile }, { name: "community level records", match: pageIsProfile, run: loadCommunityLevelRecords } ]; } function toggleableFeatures() { return pageFeatures().concat(mainFeatures()).filter(function (feature) { return feature.section; }); } function disabledFeatures() { try { var stored = JSON.parse(localStorage.getItem(DISABLED_FEATURES_KEY)); return Array.isArray(stored) ? stored : []; } catch (e) { return []; } } function saveDisabledFeatures(names) { try { localStorage.setItem(DISABLED_FEATURES_KEY, JSON.stringify(names)); } catch (e) { log("Could not mirror disabled features: " + e); } return Database.update(Database.Table.Settings, { name: "disabledFeatures", value: names }, undefined); } function syncDisabledFeatures() { Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, "disabledFeatures", function (setting) { try { localStorage.setItem(DISABLED_FEATURES_KEY, JSON.stringify(setting && Array.isArray(setting.value) ? setting.value : [])); } catch (e) { log("Could not mirror disabled features: " + e); } }); } function runFeature(name, run) { try { run(); } catch (e) { logError("Feature \"" + name + "\" failed: " + (e && e.stack ? e.stack : e)); } } function runFeatures(features) { var disabled = disabledFeatures(); features.forEach(function (feature) { if (feature.section && disabled.indexOf(feature.name) !== -1) { log("Feature \"" + feature.name + "\" is turned off"); return; } runFeature(feature.name, function () { if (!feature.match || feature.match()) { feature.run(); } }); }); } function foldForSearch(text) { return String(text).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); } function filterBlockList(query) { var needle = foldForSearch(query.trim()); var rows = $("li[id^='ListItem_']"); var shown = 0; rows.each(function () { var matches = needle === "" || foldForSearch($(this).find("a[href*='Profile']").text()).indexOf(needle) !== -1; $(this).toggleClass("tuyd-filtered", !matches); if (matches && this.style.display !== "none") { shown++; } }); $("#BlockListSearchCount").text(needle === "" ? "" : shown + " matching"); } function setupBlockListSearch() { var $list = $("li[id^='ListItem_']").first().parent("ul"); if ($list.length === 0) { return; } addCSS(` li.tuyd-filtered { display: none; } #BlockListSearch { margin: 8px 0; } #BlockListSearchInput { padding: 4px 8px; width: 240px; max-width: 60%; background: #222; color: #ddd; border: 1px solid #555; } #BlockListSearchCount { margin-left: 6px; white-space: nowrap; } `); $list.before(`
`); $("#BlockListSearchInput").on("input", function () { filterBlockList($(this).val()); }); } function setupBlacklistCounter() { var $blacklist = $("li[id^='ListItem_']").first().parent("ul"); $blacklist.before(`You have players on your block list.`); // war.app hides removed players with an inline display: none; the search hides rows with a class. var updateBlacklistCount = function () { $("#numBlacklisted b").text($blacklist.find("li").filter(function () { return this.style.display !== "none"; }).length); }; updateBlacklistCount(); var blacklistObserver = new MutationObserver(updateBlacklistCount); $blacklist.each(function () { blacklistObserver.observe(this, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "class"] }); }); } function DOM_ContentReady() { $(".order-xl-2").addClass("SideColumn"); log("DOM content ready"); if ($(".navbar").length > 0) { log("Unity is not full screen") } else { log("Unity is full screen"); return; } // The script's hidden game frames are driven from the page that opened them, and kept silent. if (inScriptFrame()) { muteScriptFrame(); return; } runFeature("DataTables sorting", registerDataTableSorting); setupWLError(); createSelector('body > footer', 'display:none'); $.fn.outerHTML = function (s) { return s ? this.before(s).remove() : jQuery("

").append(this.eq(0).clone()).html(); }; window.playerDataReady = loadPlayerData(); runFeatures(pageFeatures()); Database.init(function () { log("database is ready"); if (pageIsDashboard()) { wljs_WaitDialogJS.Start(null, "Tidying Up...") } runFeatures([ { name: "userscript menu", run: setupUserscriptMenu }, { name: "bookmark menu", run: setupBookmarkMenu }, { name: "feature toggle sync", run: syncDisabledFeatures }, { name: "version check", run: checkVersion }, { name: "main", run: databaseReady } ]); }); } function registerDataTableSorting() { $.extend($$$.fn.dataTableExt.oSort, { "rank-pre": function (a) { return a.match(/([0-9]*)/)[1] || 9999; }, "rank-asc": function (a, b) { return a < b; }, "rank-desc": function (a, b) { return a > b; } }); $.extend($$$.fn.dataTableExt.oSort, { "numeric-comma-pre": function (a) { return Number(a.replace(/,/g, "")) }, "numeric-comma-asc": function (a, b) { return a < b; }, "numeric-comma-desc": function (a, b) { return a > b; } }); } function showDashboardLoader() { $("body").append("

"); $(".container-fluid").show(); window.lastClick = new Date(); } function isAnyForumThread() { return pageIsForumThread() || pageIsClanForumThread(); } function setupForumThreadTweaks() { $("[href='#Reply']").after(" | Bookmark"); $("#PostReply").after(" | Bookmark"); $(".region a[href*='2211733141']:contains('Muli')").closest("td").find("a:contains('Report')").before("Script Creator
"); $("[id^=PostForDisplay]").find("img").css("max-width", "100%"); $(".region td:first-of-type").css("padding-top", "10px"); addCSS(` img[src*='Images/Thumbs'] { height: 25px; width: 25px; } `) } // war.app draws the tournament page client-side with absolutely positioned labels, so extras sit // to the right of the "Created by" text instead of flowing into the labels below it. function currentTournamentId() { var match = location.href.match(/\/MultiPlayer\/Tournament\?ID=(\d+)/i); return match ? match[1] : null; } // war.app opens a tournament from its tournaments list without loading a new page, and draws it // client-side, so tournament features re-check on every change instead of running once at load. // apply() must do nothing once its additions are in place, or it would trigger itself. function whileOnTournament(apply) { var check = function () { var tournamentId = currentTournamentId(); if (tournamentId) { apply(tournamentId); } }; check(); new MutationObserver(check).observe(document.body, { childList: true, subtree: true }); } // The labels are absolutely positioned, so extras sit to the right of the "Created by" text // instead of flowing into the labels below it. function tournamentInfoSlot(tournamentId) { var label = ujsElement("CreatorLabel", "_tmp"); if (!label) { return null; } var slot = $(label).children("#tournamentInfoSlot"); if (slot.length && slot.attr("data-tournament-id") !== tournamentId) { slot.remove(); slot = $(); } if (slot.length === 0) { slot = $('').attr("data-tournament-id", tournamentId); $(label).append(slot); } return slot; } function siteName() { var meta = document.querySelector("meta[property='og:site_name']"); return (meta && meta.getAttribute("content")) || "War.app"; } function stripSiteSuffix(title) { var name = siteName().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return title.replace(new RegExp("\\s+-\\s+" + name + "(?:\\s+-.*)?$", "i"), "").trim(); } function siteRelativeUrl(url) { var match = /^https?:\/\/([^\/?#]+)(.*)$/i.exec(String(url).trim()); if (!match) { return url; } var host = match[1].replace(/:\d+$/, "").toLowerCase(); var onSite = SITE_HOSTS.some(function (siteHost) { return host === siteHost || host.endsWith("." + siteHost); }); return onSite ? (match[2] || "/") : url; } function tournamentNameFromTitle(title) { return stripSiteSuffix(title).replace(/^Tournament:\s*/i, "").trim(); } function setupTournamentPage() { whileOnTournament(function (tournamentId) { var slot = tournamentInfoSlot(tournamentId); if (slot && slot.children(".tournamentBookmark").length === 0) { slot.prepend("| Bookmark"); } addTournamentPlayerControls(tournamentId); }); } function setupCustomTournamentLink() { var tournaments = {}; var show = function (tournamentId) { var tournament = tournaments[tournamentId]; var slot = tournament && tournamentInfoSlot(tournamentId); if (slot && slot.children(".customTournamentLink").length === 0) { slot.append(renderCustomTournamentLink(tournament)); } }; whileOnTournament(function (tournamentId) { if (!(tournamentId in tournaments)) { tournaments[tournamentId] = null; fetchWlStats(CUSTOM_SITE + "/api/v1/warapp-tournaments/" + tournamentId).done(function (tournament) { tournaments[tournamentId] = tournament; if (currentTournamentId() === tournamentId) { show(tournamentId); } }).fail(function (xhr) { log("Error loading the custom.wl-stats.com tournament: " + (xhr && xhr.statusText)); }); } show(tournamentId); }); } function renderCustomTournamentLink(tournament) { var link = 'custom.wl-stats.com'; var series = tournament.series && tournament.series.name ? ' (' + htmlEscape(tournament.series.name) + ')' : ""; return ' | ' + link + series + ''; } function addTournamentPlayerControls(tournamentId) { var panel = ujsElement("PlayersTabContents", "_img"); if (!panel || !ujsElement("PlayersContainer")) { return; } var toolbar = $("#ujsPlayerToolbar"); if (toolbar.length && toolbar.attr("data-tournament-id") === tournamentId && $.contains(panel, toolbar[0])) { return; } toolbar.remove(); $("[data-tuyd-bottom]").removeAttr("data-tuyd-bottom"); setupUjsPlayerControls(); $("#ujsPlayerToolbar").attr("data-tournament-id", tournamentId); } function setupMapOfTheWeekStyles() { addCSS(` .dataTable table { display: block; } `) } window.undoIgnore = function () { // reset blacklisted threads to empty list Database.clear(Database.Table.BlacklistedForumThreads, function () { if (pageIsForumOverview() || pageIsSubForum()) { $(".table tr").show(); } else if (pageIsDashboard()) { $("#ForumTable tr").show(); } else { location.reload(); } }) }; function replaceAndFilterForumTable(tableHTML) { var table = $.parseHTML(tableHTML); var promises = []; $.each($(table).find("tr"), function (key, row) { if (threadId = $(row).html().match(/href="\/Forum\/([^-]*)/mi)) { promises[key] = $.Deferred(); Database.readIndex(Database.Table.BlacklistedForumThreads, Database.Row.BlacklistedForumThreads.ThreadId, threadId[1], function (thread) { if (thread) { $(row).hide(); } promises[key].resolve(); }) } }); $.when.apply($, promises).done(function () { $("#ForumTable").replaceWith($(table).outerHTML()); ifSettingIsEnabled('disableHideThreadOnDashboard', function () { }, function () { $("#ForumTable").off(); $("#ForumTable").on("contextmenu", function (event) { $(".highlightedBookmark").removeClass("highlightedBookmark"); var row = $(event.target).closest("tr"); row.addClass("highlightedBookmark"); // Avoid the real one if (row.is(":last-child")) { return; } event.preventDefault(); threadId = row.html().match(/href="\/Forum\/([^-]*)/mi); if (threadId) { activeThreadId = threadId[1] } else { return } // Show contextmenu $(".thread-context").finish().toggle(100).// In the right position (the mouse) css({ top: event.pageY + "px", left: event.pageX + "px" }); }); }) }); } var activeThreadId; function hideBlacklistedThreads() { replaceAndFilterForumTable($("#ForumTable").outerHTML()) } window.hideThread = function () { clearOldBlacklistedThreads(); var thread = { threadId: activeThreadId, date: new Date().getTime() }; Database.add(Database.Table.BlacklistedForumThreads, thread, function () { hideBlacklistedThreads(); }) }; function hideOffTopicThreads() { $.each($(".table tbody tr:visible"), function (key, row) { if ($(row).find("td:first-of-type").text().trim() == "Off-topic") { var threadId = $(row).html().match(/href="\/Forum\/([^-]*)/mi); Database.add(Database.Table.BlacklistedForumThreads, { threadId: threadId[1], date: new Date().getTime() }, function () { $(row).hide() }) } }) } function hideWarzoneIdleThreads() { $.each($(".table tbody tr:visible"), function (key, row) { var category = $(row).find("td:first-of-type").text().trim(); if (category == "Warzone Idle" || category == "War.app Idle") { var threadId = $(row).html().match(/href="\/Forum\/([^-]*)/mi); Database.add(Database.Table.BlacklistedForumThreads, { threadId: threadId[1], date: new Date().getTime() }, function () { $(row).hide() }) } }) } function formatHiddenThreads() { let $row = $("#HiddenThreadsRow td"); $row.attr("colspan", ""); $row.before(""); $row.css("text-align", "left") } function setupSpammersBeGone() { var newColumnCountOnPage; if (pageIsForumOverview()) { newColumnCountOnPage = 6; showIgnoreCheckBox(newColumnCountOnPage); hideIgnoredThreads(); } if (pageIsSubForum()) { newColumnCountOnPage = 5; showIgnoreCheckBox(newColumnCountOnPage); hideIgnoredThreads(); } $(".thread-hide.eye-icon").on("click", function () { clearOldBlacklistedThreads(); var threadId = $(this).closest("tr").html().match(/href="\/Forum\/([^-]*)/mi); Database.add(Database.Table.BlacklistedForumThreads, { threadId: threadId[1], date: new Date().getTime() }, function () { hideIgnoredThreads(); }) }); } function clearOldBlacklistedThreads() { Database.readAll(Database.Table.BlacklistedForumThreads, function (threads) { $.each(threads, function (key, thread) { if (thread.date < (new Date() - 60 * 24 * 60 * 60 * 1000)) { Database.delete(Database.Table.BlacklistedForumThreads, thread.id, function () { }) } }) }) } /** * Inserts a new column of check boxes for each Forum thread. */ function showIgnoreCheckBox(columnCountOnPage) { var $row = " Hide"; var header = $(".table tr:first"); if (header.children("th").length < columnCountOnPage) { header.append($row); } var allPosts = $('.table tr').not(':first'); allPosts.each(function (index, post) { if ($(this).children("td").length < columnCountOnPage) { if (postId = $(this).find('a:first').attr('href')) { $(this).append("
"); } } }); } addCSS(` .eye-icon { background-image: url(${IMAGES.EYE_ICON}); height: 17px; width: 17px; cursor: pointer; background-size: contain; margin: auto; background-repeat: no-repeat; } .eye-icon:hover { background-image: url(${IMAGES.EYE_ICON_HOVER}); }`); /** * Hides all threads marked as "ignored" by a user. */ function hideIgnoredThreads() { var allPosts = $('.table tr').not(':first'); $.each(allPosts, function (key, row) { if (threadId = $(row).html().match(/href="\/Forum\/([^-]*)/mi)) { Database.readIndex(Database.Table.BlacklistedForumThreads, Database.Row.BlacklistedForumThreads.ThreadId, threadId[1], function (thread) { if (thread) { $(row).hide(); } }) } }) } //hide ingored forum threads on the community page function hideIgnoredForumThreadsFromCommnuityList() { var allPosts = $("h3:contains('Notable Forum Posts')").next().find("li"); $.each(allPosts, function (key, li) { if (threadId = $(li).html().match(/href="\/Forum\/([^-]*)/mi)) { Database.readIndex(Database.Table.BlacklistedForumThreads, Database.Row.BlacklistedForumThreads.ThreadId, threadId[1], function (thread) { if (thread) { $(li).hide(); } }) } }) } function setupTextarea() { var controls_default = [ { title: "B", class: ["tag"], openClose: true, tag: "b" }, { title: "I", class: ["tag"], openClose: true, tag: "i" }, { title: "code", class: ["tag"], openClose: true, tag: "code" }, { title: "img", class: ["tag"], openClose: true, tag: "img" }, { title: "hr", class: ["tag"], openClose: false, tag: "hr" }, { title: "quote", class: ["tag"], openClose: true, tag: "quote" }, { title: "list", class: ["tag"], openClose: true, tag: "list" }, { title: "*", class: ["tag"], openClose: false, tag: "*" } ]; var controls = ""; $.each(controls_default, function (key, control) { controls += `${control.title}` }); $(".region textarea").before(`
${controls}
`); $("textarea").attr("style", ""); addCSS(` .editor { color: white; padding: 5px; background: #A28958; margin: 5px 5px 0 0; } .editor .button { margin-right: 10px; background: rgb(122,97,48);; padding: 3px 5px; border-radius: 5px; cursor: pointer; } textarea { padding: 5px 0 0 5px; box-sizing: border-box; width: calc(100% - 5px); height: 300px } `); createSelector("pre, textarea", "-moz-tab-size: 8;-o-tab-size: 8;tab-size: 8;"); $(document).on("click", ".editor .tag", function (e) { var areaId = $(this).closest(".editor").next().attr("id"); var area = document.getElementById(areaId); var tag = $(e.target).closest(".tag").attr("data-tag"); if (area) { var startPos = area.selectionStart || 0; var endPos = area.selectionEnd || 0; if ($(this).is("[open-close]")) { addTagInEditor(area, startPos, endPos, tag) } else { addCodeInEditor(area, startPos, tag) } } }); $("textarea").on('keydown', function (e) { var keyCode = e.keyCode || e.which; if (keyCode == 9) { e.preventDefault(); var areaId = $(this).attr("id"); var area = document.getElementById(areaId); if (area) { var oldVal = $(area).val(); var start = area.selectionStart || 0; var end = area.selectionEnd || 0; var newVal = oldVal.substring(0, start) + "\t" + oldVal.substring(end); if (browserIsFirefox()) { $(area).val(newVal); area.setSelectionRange(start + 1, start + 1) } else { document.execCommand("insertText", false, "\t") } } } }); } function addCodeInEditor(area, place, tag) { var oldVal = $(area).val(); var newVal = oldVal.substring(0, place) + "[" + tag + "]" + oldVal.substring(place); $(area).focus(); if (browserIsFirefox()) { $(area).val(newVal) } else { document.execCommand("insertText", false, "[" + tag + "]") } area.setSelectionRange(place + tag.length + 2, place + tag.length + 2); $(area).focus(); } function addTagInEditor(area, start, end, tag) { var oldVal = $(area).val(); var selection = oldVal.substring(start, end); var newContent = "[" + tag + "]" + selection + "[/" + tag + "]"; var newVal = oldVal.substring(0, start) + newContent + oldVal.substring(end); $(area).focus(); if (browserIsFirefox()) { $(area).val(newVal) } else { document.execCommand("insertText", false, newContent) } if (start == end) { area.setSelectionRange(start + tag.length + 2, start + tag.length + 2) } else { area.setSelectionRange(end + 5 + (2 * tag.length), end + 5 + (2 * tag.length)) } $(area).focus(); } /** * Reloads all Games */ var OPEN_GAMES_REFRESH_KEY = "tidyUpYourDashboard.openGamesRefreshMinutes"; function openGamesRefreshMinutes() { try { var minutes = parseFloat(localStorage.getItem(OPEN_GAMES_REFRESH_KEY)); return isNaN(minutes) ? 5 : Math.max(0, minutes); } catch (e) { return 5; } } // A refresh is due once the minutes have passed since the list was last loaded, but only while you can // see it and aren't in the middle of reading it (pointer over the list, or the Filter panel open). function openGamesRefreshDue(now, last, minutes, shown, busy) { return minutes > 0 && shown && !busy && now - last >= minutes * 60 * 1000; } // Open Games on the dashboard refreshes by itself every few minutes while it's on screen. A refresh by // R, the Refresh button or coming back to the tab counts too. function setupOpenGamesAutoRefresh() { var table = $("#OpenGamesTable"); if (!table.length) { return; } var last = Date.now(); var minutes = openGamesRefreshMinutes(); $(document).on("tuyd:gamesRefreshed", function (e, selector) { if (selector === "#OpenGamesTable") { last = Date.now(); } }); var shown = function () { if (document.visibilityState === "hidden") { return false; } var rect = table[0].getBoundingClientRect(); return rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < window.innerHeight; }; var busy = function () { return table.is(":hover") || $("#OpenGamesFilterPanel").is(":visible"); }; window.setInterval(function () { if (openGamesRefreshDue(Date.now(), last, minutes, shown(), busy())) { last = Date.now(); log("Open Games refreshed by itself"); refreshOpenGames(fetchDashboard()); } }, 15000); // The choice sits in the Open Games Filter panel, which that feature draws once your filter is read. var tries = 0; var place = function () { var panel = $("#OpenGamesFilterPanel td").first(); if (!panel.length) { if (++tries < 50) { setTimeout(place, 200); } return; } $('') .appendTo(panel).find("input").val(minutes).on("change input", function () { var value = parseFloat(this.value); minutes = isNaN(value) ? 0 : Math.max(0, value); try { localStorage.setItem(OPEN_GAMES_REFRESH_KEY, String(minutes)); } catch (e) { // A blocked localStorage only means the choice isn't remembered. } }); }; place(); } function refreshAllGames() { log("Reloading Games"); ifSettingIsEnabled('scrollGames', function () { $("#openGamesContainer tbody").scrollTop(0); $("#myGamesContainer tbody").scrollTop(0); }); var dashboard = fetchDashboard(); refreshMyGames(dashboard); refreshOpenGames(dashboard); refreshPastGames(); } function fetchDashboard() { return $.ajax({ url: "/MultiPlayer/", dataType: "text" }).then(function (html) { return new DOMParser().parseFromString(html, "text/html"); }); } function refreshMyGames(dashboard) { refreshGameTable("#MyGamesTable", dashboard); } function refreshOpenGames(dashboard) { refreshGameTable("#OpenGamesTable", dashboard); } function refreshGameTable(tableSelector, dashboard) { let tableBody = $(tableSelector).children("tbody").first(); tableBody.fadeTo('fast', 0.1); $.when(dashboard || fetchDashboard()).done(function (page) { var rows = page.querySelector(tableSelector + " > tbody"); if (rows) { $(rows).find("script").remove(); tableBody.empty().append($(document.importNode(rows, true)).children()); } updateGameCounts(page); rerunDashboardInit(page); $(document).trigger("tuyd:gamesRefreshed", [tableSelector]); }).fail(function (xhr) { log("Error refreshing " + tableSelector + ": " + (xhr && xhr.statusText)); }).always(function () { tableBody.fadeTo('fast', 1); }); } function findGamesAwaitingTurn(root) { return $(root).find("#MyGamesTable tr.GameRow").filter(function () { return $(this).find("span").filter(function () { return /^\s*Your turn\b/.test($(this).text()); }).length > 0; }).map(function () { var link = $(this).find("a[href*='GameID=']").last(); return { id: $(this).attr("gameid"), name: link.text().trim(), href: link.attr("href") }; }).get(); } function bootMinutesLeft(row) { var turnText = $(row).find("span").filter(function () { return /^\s*Your turn for\b/.test($(this).text()); }).first().text(); if (!turnText) { return null; } // war.app's label is the boot time plus your banked time; "Never" means the game has no boot. var allowed = parseBootTime($(row).find(".BootTimeLabel").text()); return allowed === null ? Infinity : allowed - (parseBootTime(turnText) || 0); } function sortMyGamesByBootTime() { var rows = $("#MyGamesTable tr.GameRow").filter(function () { return bootMinutesLeft(this) !== null; }).get(); var left = rows.map(bootMinutesLeft); var sorted = rows.map(function (row, i) { return i; }).sort(function (a, b) { return left[a] === left[b] ? a - b : left[a] < left[b] ? -1 : 1; }).map(function (i) { return rows[i]; }); var slots = rows.map(function (row) { var slot = document.createComment(""); row.parentNode.insertBefore(slot, row); return slot; }); slots.forEach(function (slot, i) { slot.parentNode.replaceChild(sorted[i], slot); }); } function setupBootTimeSort() { var table = document.getElementById("MyGamesTable"); if (!table) { return; } // war.app's Filter button re-draws My Games without tuyd:gamesRefreshed, so watch the table. // The observer is paused while sorting so that moving rows doesn't trigger it again. var observer = new MutationObserver(function () { observer.disconnect(); sortMyGamesByBootTime(); observer.observe(table, { childList: true, subtree: true }); }); sortMyGamesByBootTime(); observer.observe(table, { childList: true, subtree: true }); } function updateTurnBadge(games) { var title = document.title.replace(/^\(\d+\) /, ""); document.title = games.length > 0 ? "(" + games.length + ") " + title : title; } function notifyNewTurns(games, knownIds) { var newGames = games.filter(function (game) { return knownIds.indexOf(game.id) === -1; }); if (newGames.length === 0 || !("Notification" in window) || Notification.permission !== "granted") { return; } ifSettingIsEnabled("turnNotifications", function () { newGames.forEach(function (game) { var notification = new Notification("Your turn: " + game.name, { body: siteName(), tag: "tuyd-turn-" + game.id }); notification.onclick = function () { window.focus(); location.href = game.href; }; }); }); } function setupTurnAlerts() { var knownIds = findGamesAwaitingTurn(document).map(function (game) { return game.id; }); updateTurnBadge(findGamesAwaitingTurn(document)); $(document).on("tuyd:gamesRefreshed", function (e, tableSelector) { if (tableSelector !== "#MyGamesTable") { return; } var games = findGamesAwaitingTurn(document); updateTurnBadge(games); notifyNewTurns(games, knownIds); knownIds = games.map(function (game) { return game.id; }); }); window.setInterval(function () { var unattended = document.hidden || !document.hasFocus(); if (unattended && new Date() - lastRefresh >= TURN_CHECK_INTERVAL) { lastRefresh = new Date(); log("Background turn check"); refreshMyGames(); } }, TURN_CHECK_INTERVAL); } function parseBootTime(text) { var units = { second: 1 / 60, minute: 1, hour: 60, day: 1440, week: 10080 }; var minutes = null; String(text).replace(/(\d+)\s*(second|minute|hour|day|week)s?/gi, function (all, amount, unit) { minutes = (minutes || 0) + Number(amount) * units[unit.toLowerCase()]; }); return minutes; } function parseFilterKeywords(text) { return String(text).split(",").map(function (word) { return word.trim().toLowerCase(); }).filter(function (word) { return word.length > 0; }); } function normalizeOpenGamesFilter(value) { value = value || {}; return { hideAutoGames: value.hideAutoGames === true, hideCoinGames: value.hideCoinGames === true, minBootHours: Math.max(0, Number(value.minBootHours) || 0), keywords: Array.isArray(value.keywords) ? parseFilterKeywords(value.keywords.join(",")) : [], minPlayers: Math.max(0, Math.floor(Number(value.minPlayers)) || 0), maxPlayers: Math.max(0, Math.floor(Number(value.maxPlayers)) || 0), teamGames: value.teamGames === "only" || value.teamGames === "none" ? value.teamGames : "", hideSettings: Array.isArray(value.hideSettings) ? value.hideSettings.filter(function (icon) { return openGameSettingIcons().some(function (known) { return known[0] === icon; }); }) : [] }; } // war.app's game setting icons, by file name, with a short label for the filter. function openGameSettingIcons() { return [ ["Commerce.png", "Commerce"], ["LocalDeployment.png", "Local deployments"], ["ManualDistribution.png", "Manual distribution"], ["AutoDist.png", "Automatic distribution"], ["Cards.png", "Cards"], ["Wastelands.png", "Wastelands"], ["Fog.png", "Fog other than normal"], ["NoSplit.png", "No-split mode"], ["OverriddenBonuses.png", "Overridden bonuses"], ["AttackTransferOnly.png", "Attack-only / transfer-only"], ["OneArmyStandGuard.png", "One army must stand guard"], ["AllSkill.png", "Pure skill (no luck)"], ["BankingBoot.png", "Banked boot time"] ]; } function openGameIcons(row) { return $(row).find("img").not(".MyGamesGameThumbnail").map(function () { return String($(this).attr("src") || "").split(/[?#]/)[0].split("/").pop(); }).get(); } function openGameIsFiltered(row, filter) { var link = $(row).find("td").eq(1).find("a").first(); if (filter.hideAutoGames && /[?&]AutoGameID=/i.test(link.attr("href") || "")) { return true; } if (filter.hideCoinGames && /Entry fee:/i.test($(row).find("font").text())) { return true; } var bootMinutes = parseBootTime($(row).find(".BootTimeLabel").text()); if (filter.minBootHours > 0 && bootMinutes !== null && bootMinutes < filter.minBootHours * 60) { return true; } var seats = $(row).find(".MyGamesGameBox").length; if (seats > 0 && ((filter.minPlayers > 0 && seats < filter.minPlayers) || (filter.maxPlayers > 0 && seats > filter.maxPlayers))) { return true; } var icons = openGameIcons(row); var teamGame = icons.indexOf("Teams.png") !== -1; if ((filter.teamGames === "only" && !teamGame) || (filter.teamGames === "none" && teamGame)) { return true; } if (filter.hideSettings.some(function (icon) { return icons.indexOf(icon) !== -1; })) { return true; } var name = link.text(); return filter.keywords.some(function (keyword) { var escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp("(?:^|[^\\p{L}\\p{N}])" + escaped + "(?:$|[^\\p{L}\\p{N}])", "iu").test(name); }); } function applyOpenGamesFilter(filter) { var rows = $("#OpenGamesTable tr.GameRow"); var filtered = rows.filter(function () { return openGameIsFiltered(this, filter); }); rows.not(filtered).removeClass("tuyd-filtered"); filtered.addClass("tuyd-filtered"); $("#OpenGamesFilterCount").text(filtered.length > 0 ? filtered.length + " hidden" : ""); } function readOpenGamesFilterForm(panel) { return normalizeOpenGamesFilter({ hideAutoGames: panel.find("[name=hideAutoGames]").prop("checked"), hideCoinGames: panel.find("[name=hideCoinGames]").prop("checked"), minBootHours: panel.find("[name=minBootHours]").val(), keywords: parseFilterKeywords(panel.find("[name=keywords]").val()), minPlayers: panel.find("[name=minPlayers]").val(), maxPlayers: panel.find("[name=maxPlayers]").val(), teamGames: panel.find("[name=teamGames]").val(), hideSettings: panel.find("[name=hideSetting]:checked").map(function () { return this.value; }).get() }); } function setupOpenGamesFilter() { var table = $("#OpenGamesTable"); if (table.length === 0) { return; } Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, "openGamesFilter", function (setting) { var filter = normalizeOpenGamesFilter(setting && setting.value); addCSS(` #OpenGamesTable tr.tuyd-filtered { display: none; } #OpenGamesFilterCount { color: #999; letter-spacing: 0; margin-left: 4px; } #OpenGamesFilterPanel td { padding: 8px 10px; } #OpenGamesFilterPanel label { display: block; margin: 0 0 6px; font-weight: normal; } #OpenGamesFilterPanel input[type=number] { width: 60px; } #OpenGamesFilterPanel input[type=text] { width: 260px; max-width: 100%; } #OpenGamesFilterPanel input[type=number], #OpenGamesFilterPanel input[type=text], #OpenGamesFilterPanel select { background: #222; color: #ddd; border: 1px solid #555; padding: 1px 4px; } #OpenGamesFilterPanel .tuydFilterSettings { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 2px 0 0 18px; } #OpenGamesFilterPanel .tuydFilterSettings label { margin-bottom: 2px; } `); var filterLink = ` | Filter`; var codeLink = table.find("thead a[href*='GameInviteCode']").first(); if (codeLink.length) { codeLink.after(filterLink); } else { table.find("thead h2").parent().append(filterLink); } var panel = $(`
Hide games with:
${openGameSettingIcons().map(function (icon) { return ''; }).join("")}
`); panel.find("[name=hideAutoGames]").prop("checked", filter.hideAutoGames); panel.find("[name=hideCoinGames]").prop("checked", filter.hideCoinGames); panel.find("[name=minBootHours]").val(filter.minBootHours); panel.find("[name=keywords]").val(filter.keywords.join(", ")); panel.find("[name=minPlayers]").val(filter.minPlayers); panel.find("[name=maxPlayers]").val(filter.maxPlayers); panel.find("[name=teamGames]").val(filter.teamGames); panel.find("[name=hideSetting]").each(function () { this.checked = filter.hideSettings.indexOf(this.value) !== -1; }); table.find("thead").first().append(panel); $("#OpenGamesFilterLink").on("click", function (e) { e.preventDefault(); panel.toggle(); }); panel.find("input, select").on("change", function () { filter = readOpenGamesFilterForm(panel); Database.update(Database.Table.Settings, { name: "openGamesFilter", value: filter }, undefined); applyOpenGamesFilter(filter); }); applyOpenGamesFilter(filter); // The site re-renders rows for "Show more" and the pace buttons; changes in thead are our own. new MutationObserver(function (mutations) { var rowsChanged = mutations.some(function (mutation) { return $(mutation.target).closest("thead").length === 0; }); if (rowsChanged) { applyOpenGamesFilter(filter); } }).observe(table[0], { childList: true, subtree: true }); }); } function openGameHasPlayer(row, name) { return $(row).find(".MyGamesGameBox").filter(function () { var title = $(this).attr("title") || ""; return title === name || title.indexOf(name + ": ") === 0; }).length > 0; } function markJoinedOpenGames(name) { $("#OpenGamesTable tr.GameRow").each(function () { $(this).toggleClass("tuyd-joined", openGameHasPlayer(this, name)); }); } function setupJoinedOpenGames() { var table = $("#OpenGamesTable"); if (table.length === 0) { return; } playerDataReady.done(function (player) { if (!player || !player.Name) { return; } addCSS(` #OpenGamesTable tr.tuyd-joined > td { background: rgba(76, 175, 80, 0.12); } #OpenGamesTable tr.tuyd-joined > td:nth-child(2) > a:first-of-type::after { content: "Joined"; margin-left: 8px; padding: 1px 6px; border-radius: 3px; background: #2e7d32; color: #fff; font-size: 12px; vertical-align: middle; } `); markJoinedOpenGames(player.Name); new MutationObserver(function () { markJoinedOpenGames(player.Name); }).observe(table[0], { childList: true, subtree: true }); }); } function pastGameState(row) { if ($(row).find("span").filter(function () { return /^Your turn/i.test($(this).text().trim()); }).length) { return "yourTurn"; } var status = $(row).find("td p").first().text().replace(/\s+/g, " ").trim(); if (/^Lobby\b/i.test(status)) { return "lobby"; } if (/^(?:Turn \d+|Picking)\b/i.test(status)) { return "running"; } if (/^Finished\b/i.test(status)) { return "finished"; } return "out"; } function pastGameSpeed(row) { var minutes = parseBootTime($(row).find(".BootTimeLabel").text()); return minutes === null ? "" : minutes <= 60 ? "realTime" : "multiDay"; } // A game's thumbnail sits in its map's folder on war.app's CDN (Data/Thumbs/Maps/74965/41_Fogged.jpg); // war.app's map list uses the same folder for each map's own thumbnail, which gives the map's name. // Some games show a picture of the game instead (Data/Thumbs/Games/…/Turn7f.jpg), which says nothing // about the map: those count as "unknown". function pastGameMapKey(row, known) { var match = /\/Thumbs\/Maps\/(\d+)\//.exec($(row).find("img.MyGamesGameThumbnail").attr("src") || ""); var found = known && known[$(row).attr("gameid")]; return match ? match[1] : found || "unknown"; } var GAME_MAPS_KEY = "tidyUpYourDashboard.pastGameMaps"; // Maps found by opening a game, by GameID. A game's map never changes, so they're kept for good. function storedGameMaps() { try { var stored = JSON.parse(localStorage.getItem(GAME_MAPS_KEY)); return stored && typeof stored === "object" ? stored : {}; } catch (e) { return {}; } } // war.app's data for a game has its map's ID and thumbnail folder in two of its minified fields (737 and // 1748 for Medium Earth), next to small numbers such as 1, 2 and 3 that are by chance also a map's folder // and ID (Bracketology is folder 1, ID 1). So a map counts only when its folder and its ID are in two // different fields, and the largest folder wins: a chance match is a small number. function gameMapFolder(map, maps) { var fields = Object.keys(map || {}).filter(function (key) { var value = map[key]; return typeof value === "number" && value > 0 && Math.floor(value) === value; }); var candidates = fields.filter(function (key) { var entry = maps[String(map[key])]; return entry && fields.some(function (other) { return other !== key && map[other] === Number(entry.id); }); }).map(function (key) { return map[key]; }).sort(function (a, b) { return b - a; }); return candidates.length ? String(candidates[0]) : null; } function pastGamePlayers(row) { return $(row).find(".MyGamesGameBox").length; } // war.app's map list (Ajax/EnumerateMaps, every map at once): each map's thumbnail folder, ID and name. function parseMapList(html) { var maps = {}; $(new DOMParser().parseFromString(html, "text/html")).find("img").each(function () { var folder = /\/Thumbs\/Maps\/(\d+)\//.exec($(this).attr("src") || ""); var box = $(this).closest("div"); var link = /\/Map\/(\d+)/.exec(box.find("a").first().attr("href") || ""); var name = box.clone().children("font, a, img").remove().end().text().replace(/\s+/g, " ").trim(); if (folder && link && name) { maps[folder[1]] = { id: link[1], name: name }; } }); return maps; } function pastGameMatches(row, filter) { var state = pastGameState(row); if (filter.state === "running" ? state !== "running" && state !== "yourTurn" : filter.state && state !== filter.state) { return false; } if (filter.speed && pastGameSpeed(row) !== filter.speed) { return false; } if (filter.map && pastGameMapKey(row, filter.knownMaps) !== filter.map) { return false; } var players = pastGamePlayers(row); if ((filter.minPlayers && players < filter.minPlayers) || (filter.maxPlayers && players > filter.maxPlayers) || (filter.size === "oneVsOne" && players !== 2) || (filter.size === "more" && players <= 2)) { return false; } var query = String(filter.query || "").trim().toLowerCase(); if (!query) { return true; } var names = [$(row).find("td").eq(1).find("a").first().text()].concat($(row).find(".MyGamesGameBox").map(function () { return String($(this).attr("title") || "").split(": ")[0]; }).get()); return names.some(function (name) { return name.toLowerCase().indexOf(query) !== -1; }); } function normalizePastGamesFilter(value) { value = value || {}; var count = function (number) { number = parseInt(number, 10); return number > 0 ? number : 0; }; return { state: ["running", "yourTurn", "lobby", "finished", "out"].indexOf(value.state) !== -1 ? value.state : "", speed: value.speed === "realTime" || value.speed === "multiDay" ? value.speed : "", map: /^(?:\d+|unknown)$/.test(String(value.map || "")) ? String(value.map) : "", minPlayers: count(value.minPlayers), maxPlayers: count(value.maxPlayers), size: value.size === "oneVsOne" || value.size === "more" ? value.size : "", reversed: value.reversed === true, query: typeof value.query === "string" ? value.query : "" }; } function orderPastGames(tbody, reversed) { var rows = $(tbody).children("tr.GameRow"); var next = rows.length ? Math.max.apply(null, rows.map(function () { return Number($(this).attr("data-tuyd-order")) || 0; }).get()) + 1 : 0; rows.each(function () { if (!$(this).attr("data-tuyd-order")) { $(this).attr("data-tuyd-order", next++); } }); var sorted = rows.get().sort(function (a, b) { var difference = Number($(a).attr("data-tuyd-order")) - Number($(b).attr("data-tuyd-order")); return reversed ? -difference : difference; }); var changed = sorted.some(function (row, i) { return row !== rows[i]; }); if (changed) { $(tbody).append(sorted); } return changed; } function applyPastGamesFilter(table, filter) { var rows = $(table).find("tbody tr.GameRow"); var shown = rows.filter(function () { return pastGameMatches(this, filter); }); rows.not(shown).addClass("tuyd-pastHidden"); shown.removeClass("tuyd-pastHidden"); $("#PastGamesFilterCount").text(shown.length === rows.length ? "" : shown.length + " of " + rows.length + " shown"); } var MAP_NAMES_KEY = "tidyUpYourDashboard.mapNames"; // Map names by thumbnail folder, kept for a week: the list is one request of about 1.4 MB. function loadMapNames(callback) { try { var cached = JSON.parse(localStorage.getItem(MAP_NAMES_KEY)); if (cached && cached.maps && Date.now() - cached.at < 7 * 24 * 60 * 60 * 1000) { callback(cached.maps); return; } } catch (e) { // An unreadable cache is loaded again. } $.ajax({ url: "/Ajax/EnumerateMaps?Filter=__all&Sort=1&PerPage=100000&Offset=0", dataType: "text" }).done(function (html) { var maps = parseMapList(html); try { localStorage.setItem(MAP_NAMES_KEY, JSON.stringify({ at: Date.now(), maps: maps })); } catch (e) { // Without storage the names are loaded again next time. } callback(maps); }).fail(function () { log("Past Games: couldn't load war.app's map list for map names"); callback({}); }); } // The maps of the games on this page, by name, with how many games each. function pastGameMapOptions(rows, maps, selected, known) { var counts = {}; $(rows).each(function () { var key = pastGameMapKey(this, known); counts[key] = (counts[key] || 0) + 1; }); if (selected && !counts[selected]) { counts[selected] = 0; } return Object.keys(counts).map(function (key) { return { key: key, name: key === "unknown" ? "Map not shown" : maps[key] ? maps[key].name : "Map " + key, games: counts[key] }; }).sort(function (a, b) { return (a.key === "unknown") - (b.key === "unknown") || a.name.localeCompare(b.name); }); } function setupPastGamesFilter() { var table = $("#MyGamesTable"); if (!table.length) { return; } Database.readIndex(Database.Table.Settings, Database.Row.Settings.Name, "pastGamesFilter", function (setting) { var filter = normalizePastGamesFilter(setting && setting.value); filter.query = ""; addCSS(` #MyGamesTable tr.tuyd-pastHidden { display: none; } #PastGamesFilterBar td { padding: 6px 8px 10px; } #PastGamesFilterCount { white-space: nowrap; } #PastGamesFilterBar input, #PastGamesFilterBar select { display: inline-block; width: auto; margin-right: 8px; background: #222; color: #ddd; border: 1px solid #555; padding: 1px 4px; } #PastGamesFilterBar input[type=search] { width: 220px; max-width: 100%; } #PastGamesFilterBar input[type=number] { width: 52px; margin-right: 4px; } #PastGamesFilterBar select[name=map] { max-width: 220px; } #PastGamesFilterBar .tuydPastRow { margin-top: 6px; } #PastGamesFilterBar .tuydMapFind { margin-left: 4px; color: #999; white-space: nowrap; } #PastGamesFilterBar label { font-weight: normal; margin: 0 8px 0 0; } #PastGamesFilterBar input[type=checkbox] { margin: 0 4px 0 0; vertical-align: middle; } #PastGamesFilterCount { color: #999; } `); var bar = $(`
`); bar.find("[name=state]").val(filter.state); bar.find("[name=speed]").val(filter.speed); bar.find("[name=reversed]").prop("checked", filter.reversed); bar.find("[name=size]").val(filter.size); bar.find("[name=minPlayers]").val(filter.minPlayers || ""); bar.find("[name=maxPlayers]").val(filter.maxPlayers || ""); table.children("thead").first().append(bar); var tbody = table.children("tbody").first(); var maps = {}; var known = storedGameMaps(); filter.knownMaps = known; var finding = null; var unknownGames = function () { return tbody.children("tr.GameRow").filter(function () { return pastGameMapKey(this, known) === "unknown" && !(finding && finding.tried[$(this).attr("gameid")]); }).map(function () { return $(this).attr("gameid"); }).get(); }; var showFinder = function () { var place = bar.find(".tuydMapFind").empty(); if (finding) { place.text("Finding maps: " + finding.done + " of " + finding.total + " ").append('Stop'); return; } var missing = unknownGames().length; if (missing) { $('').text("Find the maps of " + missing + " game" + (missing === 1 ? "" : "s")) .attr("title", "Opens each of these games in the background, a couple of seconds each, to read its map. Remembered in this browser.") .appendTo(place); } }; // One game at a time in a hidden frame, until war.app's data for it has its map. var findNext = function () { if (!finding) { return; } var id = unknownGames()[0]; if (!id) { finding = null; update(); return; } finding.tried[id] = true; var frame = $('') .attr({ id: GAME_MAP_FRAME_ID, src: "/MultiPlayer?GameID=" + encodeURIComponent(id) }).appendTo("body"); var started = Date.now(); finding.timer = setInterval(function () { var map = null; try { var hooks = frame[0].contentWindow.UJS_Hooks; map = hooks && hooks.za && hooks.za.Map; } catch (e) { map = null; } if (!map && Date.now() - started < 30000) { return; } clearInterval(finding.timer); frame.remove(); var folder = map ? gameMapFolder(map, maps) : null; if (folder) { known[id] = folder; try { localStorage.setItem(GAME_MAPS_KEY, JSON.stringify(known)); } catch (e) { // Without storage the maps are found again next time. } } else { log("Past Games: couldn't read the map of game " + id); } finding.done++; update(); findNext(); }, 300); }; var fillMaps = function () { showFinder(); var select = bar.find("[name=map]"); var options = pastGameMapOptions(tbody.children("tr.GameRow"), maps, filter.map, known); select.children("option").not("[value='']").remove(); options.forEach(function (option) { select.append($("