// ==UserScript== // @name AWA Toolkit // @namespace https://github.com/UpDownLeftDie/AWA-Toolkit // @version 2.2.0 // @author jaredcat // @description Artifact Optimizer, Control Center tasks, giveaway/vault filters, and UCF reading mode // @license AGPL-3.0-or-later // @icon https://raw.githubusercontent.com/UpDownLeftDie/AWA-Toolkit/main/icon.png // @icon64 https://raw.githubusercontent.com/UpDownLeftDie/AWA-Toolkit/main/icon64.png // @homepageURL https://github.com/UpDownLeftDie/AWA-Toolkit // @supportURL https://github.com/UpDownLeftDie/AWA-Toolkit/issues // @downloadURL https://raw.githubusercontent.com/UpDownLeftDie/AWA-Toolkit/main/dist/awa-toolkit.user.js // @updateURL https://raw.githubusercontent.com/UpDownLeftDie/AWA-Toolkit/main/dist/awa-toolkit.user.js // @match *://*.alienwarearena.com/* // @connect store.steampowered.com // @connect raw.githubusercontent.com // @grant GM.getValue // @grant GM.notification // @grant GM.openInTab // @grant GM.setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @grant window.focus // @run-at document-start // ==/UserScript== (async function() { "use strict"; var _GM = (() => typeof GM != "undefined" ? GM : void 0)(); var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); var STEAM_FREE_CACHE_KEY = "steamAppFreeCache"; var STEAM_FREE_TTL_PERMANENT_MS = 6048e5; var STEAM_FREE_TTL_PRICE_MS = 864e5; var STEAM_FREE_TTL_ERROR_MS = 36e5; var STEAM_LIBRARY_PENDING_HINT = `Free on Steam — add it and play ~${String(10)} min so it shows as owned`; function parseSteamAppId(value) { if (!value) return; const id = Number(value); if (!Number.isSafeInteger(id) || id <= 0) return; return id; } function scrapeSteamAppIdFromDocument(document_) { for (const image of document_.querySelectorAll("img")) { const id = parseSteamAppId(/\/steam\/apps\/(\d{2,10})\//.exec(image.src)?.[1]); if (id !== void 0) return id; } for (const link of document_.querySelectorAll("a[href]")) { const href = link.getAttribute("href") ?? ""; const fromRun = /^steam:\/\/run\/(\d{2,10})/i.exec(href); const fromStore = /store\.steampowered\.com\/app\/(\d{2,10})/i.exec(href); const id = parseSteamAppId(fromRun?.[1] ?? fromStore?.[1]); if (id !== void 0) return id; } } function steamFreeFromDetails(data) { if (data.is_free === true) return { isFree: true, permanent: true }; const price = data.price_overview; return { isFree: price?.final === 0 || (price?.discount_percent ?? 0) >= 100, permanent: false }; } function cacheTtlMs(entry) { if (entry.error) return STEAM_FREE_TTL_ERROR_MS; if (entry.permanent) return STEAM_FREE_TTL_PERMANENT_MS; return STEAM_FREE_TTL_PRICE_MS; } function isCacheFresh$1(entry) { if (!entry) return false; const cachedAt = Date.parse(entry.at); if (!Number.isFinite(cachedAt)) return false; return Date.now() - cachedAt < cacheTtlMs(entry); } async function loadSteamFreeCache() { const raw = await _GM.getValue(STEAM_FREE_CACHE_KEY); if (!raw) return {}; if (typeof raw !== "string") return raw; try { const parsed = JSON.parse(raw); if (typeof parsed === "object" && parsed !== null) return parsed; } catch { return {}; } return {}; } async function saveSteamFreeCache(cache) { await _GM.setValue(STEAM_FREE_CACHE_KEY, JSON.stringify(cache)); } var inflightLookup$1 = {}; function fetchSteamAppDetailsBatch(appIds) { const ids = [...new Set(appIds)].toSorted((left, right) => left - right); if (ids.length === 0) return Promise.resolve({}); return new Promise((resolve) => { _GM_xmlhttpRequest({ method: "GET", url: `https://store.steampowered.com/api/appdetails?appids=${ids.join(",")}&cc=us`, anonymous: true, timeout: 8e3, onload: (response) => { if (response.status < 200 || response.status >= 300) { resolve(void 0); return; } try { const parsed = JSON.parse(response.responseText); if (typeof parsed !== "object" || parsed === null) { resolve(void 0); return; } resolve(parsed); } catch { resolve(void 0); } }, onerror: () => { resolve(void 0); }, ontimeout: () => { resolve(void 0); } }); }); } async function lookupSteamIsCurrentlyFreeMany(appIds) { const unique = [...new Set(appIds)].toSorted((left, right) => left - right); const result = new Map(); if (unique.length === 0) return result; const inflightKey = unique.join(","); if (inflightLookup$1.key === inflightKey && inflightLookup$1.promise !== void 0) return inflightLookup$1.promise; const promise = lookupSteamIsCurrentlyFreeManyUncached(unique, result); inflightLookup$1.key = inflightKey; inflightLookup$1.promise = promise; try { return await promise; } finally { if (inflightLookup$1.key === inflightKey) { delete inflightLookup$1.key; delete inflightLookup$1.promise; } } } async function lookupSteamIsCurrentlyFreeManyUncached(unique, result) { const cache = await loadSteamFreeCache(); const missing = []; const nowIso = new Date().toISOString(); for (const appId of unique) { const cached = cache[String(appId)]; if (isCacheFresh$1(cached)) { result.set(appId, cached?.error ? void 0 : cached?.isFree); continue; } missing.push(appId); } if (missing.length === 0) return result; const payload = await fetchSteamAppDetailsBatch(missing); for (const appId of missing) { const key = String(appId); const entry = payload?.[key]; if (!entry?.success || !entry.data) { cache[key] = { error: true, at: nowIso }; result.set(appId, void 0); continue; } const parsed = steamFreeFromDetails(entry.data); const stored = { isFree: parsed.isFree, at: nowIso }; if (parsed.permanent) stored.permanent = true; cache[key] = stored; result.set(appId, parsed.isFree); } await saveSteamFreeCache(cache); return result; } function requiresSteamFreeLookup(item) { return item.eligibility === "ineligible" && item.steamAppId !== void 0 && item.isFree === void 0; } function requiresSteamFreeHydrate(state) { if ((state.steamQuests?.quests ?? []).some((quest) => requiresSteamFreeLookup(quest))) return true; if (!state.communityEvent) return false; return requiresSteamFreeLookup(communityEventFreeGate(state.communityEvent)); } function communityEventFreeGate(event) { return { eligibility: event.playEligibility ?? "unknown", ...event.steamAppId !== void 0 && { steamAppId: event.steamAppId }, ...event.isFree !== void 0 && { isFree: event.isFree }, ...event.libraryPending === true && { libraryPending: true } }; } function applySteamFreeLookup(item, isFree) { if (isFree === true) return { ...item, eligibility: "eligible", isFree: true, libraryPending: true }; if (isFree === false) return { ...item, isFree: false }; return item; } async function resolveSiteStateSteamFreeToPlay(next) { const quests = next.steamQuests?.quests ?? []; const event = next.communityEvent; const eventGate = event ? communityEventFreeGate(event) : void 0; const appIds = []; for (const quest of quests) if (requiresSteamFreeLookup(quest) && quest.steamAppId !== void 0) appIds.push(quest.steamAppId); if (eventGate && requiresSteamFreeLookup(eventGate) && eventGate.steamAppId !== void 0) appIds.push(eventGate.steamAppId); if (appIds.length === 0) return; const freeByAppId = await lookupSteamIsCurrentlyFreeMany(appIds); if (quests.length > 0) next.steamQuests = { scrapedAt: next.steamQuests?.scrapedAt ?? new Date().toISOString(), quests: quests.map((quest) => { if (!requiresSteamFreeLookup(quest) || quest.steamAppId === void 0) return quest; return applySteamFreeLookup(quest, freeByAppId.get(quest.steamAppId)); }) }; if (!event || !eventGate || !requiresSteamFreeLookup(eventGate) || eventGate.steamAppId === void 0) return; const upgraded = applySteamFreeLookup(eventGate, freeByAppId.get(eventGate.steamAppId)); next.communityEvent = { ...event, playEligibility: upgraded.eligibility, ...upgraded.steamAppId !== void 0 && { steamAppId: upgraded.steamAppId }, ...upgraded.isFree !== void 0 && { isFree: upgraded.isFree }, ...upgraded.libraryPending === true && { libraryPending: true } }; } function pageText(document_ = document) { return document_.body?.textContent ?? ""; } function isElementDisplayNone(element) { const styleAttribute = element.getAttribute("style") ?? ""; if (/display:\s*none/i.test(styleAttribute)) return true; if (element instanceof HTMLElement && element.style.display === "none") return true; return false; } function isElementVisiblyHidden(element) { if (isElementDisplayNone(element) || element.hasAttribute("hidden")) return true; if (element.getAttribute("aria-hidden") === "true") return true; const className = element.getAttribute("class") ?? ""; if (/\b(d-none|hidden|hide|invisible)\b/i.test(className)) return true; const view = element.ownerDocument.defaultView; if (view && element instanceof view.HTMLElement) { const style = view.getComputedStyle(element); if (style.display === "none" || style.visibility === "hidden") return true; } return false; } function controlLabel(element) { return (element.textContent ?? "").replaceAll(/\s+/g, " ").trim(); } function findActivityCard(document_, title) { const header = [...document_.querySelectorAll("h2, h3, h4")].find((element) => title.test(element.textContent?.trim() ?? "")); if (!header) return; return header.closest(".user-profile__profile-card, .aa-card, [class*=\"profile-card\"]") ?? header.parentElement?.parentElement ?? void 0; } function utcDateString(date = new Date()) { return date.toISOString().slice(0, 10); } function parseTimestamp$1(value) { if (!value) return NaN; const ms = Date.parse(value); return Number.isFinite(ms) ? ms : NaN; } var CREDIT_SOURCES = [ { id: "megumin-tools", label: "Megumin's Tools", dateAccessed: "2026-08-10", url: "https://docs.google.com/spreadsheets/d/1VCzq6Trwc9T_wEsvTANpL7yy8FaJ6psSsKYn4O4riw8/edit?usp=sharing", links: [{ label: "Artifact Upgrade C/P", url: "https://docs.google.com/spreadsheets/d/1VCzq6Trwc9T_wEsvTANpL7yy8FaJ6psSsKYn4O4riw8/edit?gid=1046753957#gid=1046753957" }, { label: "ARP Calculator", url: "https://docs.google.com/spreadsheets/d/1VCzq6Trwc9T_wEsvTANpL7yy8FaJ6psSsKYn4O4riw8/edit?gid=1289162159#gid=1289162159" }] }, { id: "megumin-ucf-artifacts-info", label: "【Artifacts】Info", dateAccessed: "2026-08-06", url: "https://www.alienwarearena.com/ucf/show/2167784" }, { id: "asce", label: "ASCE", url: "https://github.com/MarvashMagalli/ASCE" } ]; var ArtifactTier = function(ArtifactTier) { ArtifactTier[ArtifactTier["Rust"] = 0] = "Rust"; ArtifactTier[ArtifactTier["Bronze"] = 1] = "Bronze"; ArtifactTier[ArtifactTier["Silver"] = 2] = "Silver"; ArtifactTier[ArtifactTier["Gold"] = 3] = "Gold"; ArtifactTier[ArtifactTier["Platinum"] = 4] = "Platinum"; ArtifactTier[ArtifactTier["Interstellar"] = 5] = "Interstellar"; return ArtifactTier; }({}); var TIER_LABELS = { [0]: "Rust", [1]: "Bronze", [2]: "Silver", [3]: "Gold", [4]: "Platinum", [5]: "Interstellar" }; var FRAGMENT_COST_TO_TIER = { [0]: 0, [1]: 2, [2]: 5, [3]: 10, [4]: 16, [5]: 25 }; var ArtifactEffectType = function(ArtifactEffectType) { ArtifactEffectType["SteamQuests"] = "SteamQuests"; ArtifactEffectType["WatchTwitch"] = "WatchTwitch"; ArtifactEffectType["DailyCalendar"] = "DailyCalendar"; ArtifactEffectType["TimeOnSite"] = "TimeOnSite"; ArtifactEffectType["DiscordPoll"] = "DiscordPoll"; ArtifactEffectType["MarketDiscountPct"] = "MarketDiscountPct"; ArtifactEffectType["AllArpPct"] = "AllArpPct"; ArtifactEffectType["CommunityPlaytimePct"] = "CommunityPlaytimePct"; ArtifactEffectType["UsernameColor"] = "UsernameColor"; ArtifactEffectType["None"] = "None"; return ArtifactEffectType; }({}); var ARTIFACTS = [ { id: "sylphin-fission-blade", category: "Weapon", tierNames: [ "Broken Sylphin Fission Blade", "Basic Sylphin Fission Blade", "Extended Sylphin Fission Blade", "Sylphin Fission Blade Mk1", "Sylphin Fission Blade Mk3", "Kylorf's Sylphin Fission Blade" ], effects: [ 1, 2, 4, 6, 8, 12 ], effectType: "SteamQuests", effectUnit: "flat" }, { id: "pn295", category: "Tech", tierNames: [ "Pn295 Unstable", "Pn295 Controlled", "Pn295 Fusion", "Pn295 Alloy", "Slyphin Battle Armor", "Pn295 Collapsed Star" ], effects: [ 1, 2, 4, 7, 10, 15 ], effectType: "WatchTwitch", effectUnit: "flat" }, { id: "light-warping", category: "Language", tierNames: [ "Rudimentary Light Warping", "Simplistic Light Warping", "Phase Light Warping", "Bonded Phase Light Warping", "PLW Conduit RX13", "Light Warp Forerunners" ], effects: [ -.01, -.03, -.05, -.08, -.1, -.15 ], effectType: "MarketDiscountPct", effectUnit: "pct" }, { id: "herkow-plasma-chamber", category: "Power", tierNames: [ void 0, void 0, void 0, "H`erkow Plasma Chamber", "H`erkow Control Center", "H`erkow Orb Reactor" ], effects: [ void 0, void 0, void 0, .1, .15, .25 ], effectType: "AllArpPct", effectUnit: "pct" }, { id: "them", category: "Power", tierNames: [ "*** THEM ***", "*** THEM CONTAINED ***", "*** THEM ESCAPED ***", void 0, void 0, void 0 ], effects: [ -.2, -.25, -.25, void 0, void 0, void 0 ], effectType: "AllArpPct", effectUnit: "pct" }, { id: "herkow-warrior-script", category: "Weapon", tierNames: [ "H`erkow Warrior Script", void 0, void 0, void 0, void 0, void 0 ], effects: [ 1, void 0, void 0, void 0, void 0, void 0 ], effectType: "SteamQuests", effectUnit: "flat" }, { id: "scion-of-the-light", category: "Tech", tierNames: [ "Scion of the Light", "Scion of the Light: 2nd Sighting", void 0, void 0, void 0, void 0 ], effects: [ 1, 2, void 0, void 0, void 0, void 0 ], effectType: "WatchTwitch", effectUnit: "flat" }, { id: "mysterious-text", category: "Language", tierNames: [ "Mysterious Text", "Mysterious Text Decipher", void 0, void 0, void 0, void 0 ], effects: [ -.01, -.02, void 0, void 0, void 0, void 0 ], effectType: "MarketDiscountPct", effectUnit: "pct" }, { id: "chai-stones", category: "Precious Gems", tierNames: [ "Chai Stones - Raw", "Chai Stones - Unprocessed", "Chai Stones - Processed", "The Stone of Cromcote`", "H`erkow Fertility Stone", "Chai Stone H`erkow Display" ], effects: [ 1, 2, 3, 4, 5, 6 ], effectType: "DailyCalendar", effectUnit: "flat" }, { id: "herkow-fertility-robes", category: "Clothing", tierNames: [ void 0, void 0, void 0, "H`erkow Fertility Robes", void 0, void 0 ], effects: [ void 0, void 0, void 0, "Pink", void 0, void 0 ], effectType: "UsernameColor", effectUnit: "cosmetic" }, { id: "pn295-unstable-battery", category: "Weapon", tierNames: [ "Pn 295 Unstable Battery", "Pn 295 Stable Battery", "Pn 295 Contained Battery", "Pn 295 Battery Amplifier", "Pn 295 Cruiser Class Battery Amplifier", "Pn 295 Recycler" ], effects: [ 2, 4, 6, 8, 10, 15 ], effectType: "SteamQuests", effectUnit: "flat" }, { id: "zorathian-cosmotheque", category: "Knowledge", tierNames: [ void 0, "Zorathian Cosmotheque", "Zorathian Data Mine", "5th Dimensional Data", "Crystalline Quantum Shelving", "Zorathian Library" ], effects: [ void 0, 1, 2, 3, 4, 5 ], effectType: "DiscordPoll", effectUnit: "flat" }, { id: "flux", category: "Social", tierNames: [ "Flux", "Advanced Flux", "Spocot Board", "Spocot Flux Epoc", "Spocot Flux Final", "Spocot Flux Champion" ], effects: [ .05, .1, .2, .3, .4, .5 ], effectType: "CommunityPlaytimePct", effectUnit: "pct" }, { id: "bali-arches", category: "Architecture", tierNames: [ void 0, "Ba'li Arches", "Northop Arches", "Golden Arches", "Apotho Arches", "Eye of the Night" ], effects: [ void 0, 1, 2, 3, 4, 6 ], effectType: "TimeOnSite", effectUnit: "flat" }, { id: "gamers-wanted", category: "Architecture", tierNames: [ void 0, void 0, "Gamers Wanted", "They're Out There", "Defy Boundaries", "Rise" ], effects: [ void 0, void 0, 1, 2, 3, 4 ], effectType: "TimeOnSite", effectUnit: "flat" }, { id: "omniversal-override", category: "Language", tierNames: [ void 0, void 0, "Omniversal Override", "Planetary Tranverser", "Dimensional Articulator", "Multi-Planar Transmuter" ], effects: [ void 0, void 0, -.02, -.03, -.04, -.05 ], effectType: "MarketDiscountPct", effectUnit: "pct" }, { id: "the-black-rose", category: "Clothing", tierNames: [ "The Black Rose", void 0, void 0, void 0, void 0, void 0 ], effects: [ "Dark Gray", void 0, void 0, void 0, void 0, void 0 ], effectType: "UsernameColor", effectUnit: "cosmetic" }, { id: "the-fractured-lilly", category: "Precious Gems", tierNames: [ "The Fractured Lilly", void 0, void 0, void 0, void 0, void 0 ], effects: [ 0, void 0, void 0, void 0, void 0, void 0 ], effectType: "None", effectUnit: "flat" }, { id: "the-veiled-thorn", category: "Weapon", tierNames: [ "The Veiled Thorn", void 0, void 0, void 0, void 0, void 0 ], effects: [ 0, void 0, void 0, void 0, void 0, void 0 ], effectType: "None", effectUnit: "flat" }, { id: "audio-archive-stone", category: "Clothing", tierNames: [ void 0, void 0, void 0, void 0, void 0, "Audio Archive Stone" ], effects: [ void 0, void 0, void 0, void 0, void 0, "Tomato" ], effectType: "UsernameColor", effectUnit: "cosmetic" } ]; var TIER_NAME_ALIASES = { "Pn295 Recycler": { id: "pn295-unstable-battery", tier: 5 }, "H'erkow Warrior Script": { id: "herkow-warrior-script", tier: 0 } }; var ARTIFACT_SETS = [ { id: "first-contact", name: "First Contact", memberIds: [ "sylphin-fission-blade", "pn295", "light-warping" ], effects: [{ type: "DailyCalendar", value: 1, unit: "flat" }, { type: "UsernameColor", value: 1, unit: "cosmetic" }] }, { id: "stanley-excavation", name: "The Stanley Excavation", memberIds: [ "chai-stones", "herkow-fertility-robes", "pn295-unstable-battery" ], effects: [{ type: "SteamQuests", value: 5, unit: "flat" }, { type: "MarketDiscountPct", value: -.15, unit: "pct" }] }, { id: "zorathian-renaissance", name: "Zorathian Renaissance", memberIds: [ "zorathian-cosmotheque", "flux", "bali-arches" ], effects: [{ type: "AllArpPct", value: .1, unit: "pct" }, { type: "UsernameColor", value: 1, unit: "cosmetic" }] }, { id: "braxtine-garden", name: "Braxtine Garden", memberIds: [ "the-black-rose", "the-crimsom-t", "the-nebula-c" ], effects: [{ type: "AllArpPct", value: 5, unit: "pct" }, { type: "TimeOnSite", value: 100, unit: "flat" }] } ]; var BASE_ACTIVITY = { days: 1, timeOnSiteBasePerDay: 5, watchTwitchBasePerDay: 15, steamQuestBases: [ 15, 25, 25 ], discordPollBase: 5, discordPollsWhenPending: 2, discordPollPostHourUtc: 16, dailyQuestBase: 7, weekendQuestBase: 5, dailyCalendarBasePerDay: 5, steamCommunityEventReward: 20 }; var MONTHLY_CATEGORY_USES = { ["WatchTwitch"]: 30, ["DailyCalendar"]: 30, ["TimeOnSite"]: 30, ["SteamQuests"]: 12, ["DiscordPoll"]: 20 }; var MONTHLY_ARP_FOR_PCT = 1800; var END_GAME_HPC_UPGRADE_ORDER = [ "herkow-plasma-chamber", "pn295", "chai-stones", "pn295-unstable-battery", "bali-arches", "sylphin-fission-blade", "zorathian-cosmotheque", "scion-of-the-light" ]; var END_GAME_NO_HPC_UPGRADE_ORDER = [ "pn295", "chai-stones", "pn295-unstable-battery", "bali-arches", "sylphin-fission-blade", "zorathian-cosmotheque", "scion-of-the-light" ]; var NEW_GAME_UPGRADE_ORDER = [ "bali-arches", "zorathian-cosmotheque", "flux", "scion-of-the-light" ]; function upgradeFocusOrder(ownedFamilyIds) { if (ownedFamilyIds.has("herkow-plasma-chamber")) return END_GAME_HPC_UPGRADE_ORDER; if (ownedFamilyIds.has("pn295")) return END_GAME_NO_HPC_UPGRADE_ORDER; return NEW_GAME_UPGRADE_ORDER; } var END_GAME_HPC_STANDING = [ "herkow-plasma-chamber", "chai-stones", "pn295" ]; var END_GAME_NO_HPC_STANDING = [ "pn295", "chai-stones", "bali-arches" ]; var NEW_GAME_STANDING = [ "bali-arches", "zorathian-cosmotheque", "flux" ]; function monthlyMetaStandingFamilies(ownedFamilyIds) { if (ownedFamilyIds.has("herkow-plasma-chamber")) return { standing: END_GAME_HPC_STANDING, fillOrder: END_GAME_HPC_UPGRADE_ORDER }; if (ownedFamilyIds.has("pn295")) return { standing: END_GAME_NO_HPC_STANDING, fillOrder: END_GAME_NO_HPC_UPGRADE_ORDER }; return { standing: NEW_GAME_STANDING, fillOrder: NEW_GAME_UPGRADE_ORDER }; } function getArtifactById(id) { return ARTIFACTS.find((a) => a.id === id); } function listArtifactNameEntries() { const seen = new Set(); const entries = []; const push = (name, definition, tier) => { if (seen.has(name)) return; seen.add(name); entries.push({ name, definition, tier }); }; for (const definition of ARTIFACTS) for (const [tier, name] of definition.tierNames.entries()) if (name) push(name, definition, tier); for (const [name, alias] of Object.entries(TIER_NAME_ALIASES)) { const definition = getArtifactById(alias.id); if (definition) push(name, definition, alias.tier); } return entries.toSorted((left, right) => right.name.length - left.name.length); } function resolveArtifactByDisplayName(displayName) { const alias = TIER_NAME_ALIASES[displayName]; if (alias) { const definition = getArtifactById(alias.id); if (definition) return { definition, tier: alias.tier }; } for (const definition of ARTIFACTS) { const index = definition.tierNames.findIndex((name) => name?.toLowerCase() === displayName.toLowerCase()); if (index !== -1) return { definition, tier: index }; } const normalized = normalizeName$1(displayName); for (const definition of ARTIFACTS) { const index = definition.tierNames.findIndex((name) => name !== void 0 && normalizeName$1(name) === normalized); if (index !== -1) return { definition, tier: index }; } } function normalizeName$1(name) { return name.toLowerCase().replaceAll(/[`'’]/g, "").replaceAll(/\s+/g, " ").trim(); } function getNumericEffect(definition, tier) { if (definition.effectUnit === "cosmetic") return 0; const value = definition.effects[tier]; return typeof value === "number" ? value : 0; } function fragmentCostToUpgradeFrom(tier) { if (tier >= 5) return; return FRAGMENT_COST_TO_TIER[tier + 1]; } function displayNameFor(definition, tier) { return definition.tierNames[tier] ?? definition.id; } var MS_PER_DAY$2 = 864e5; function utcAtHour(date, hour) { return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), hour, 0, 0, 0)); } function isUtcWeekday(date) { const day = date.getUTCDay(); return day >= 1 && day <= 5; } function nextDiscordPollPostAt(now = new Date()) { for (let offset = 0; offset <= 7; offset += 1) { const post = utcAtHour(new Date(now.getTime() + offset * MS_PER_DAY$2), BASE_ACTIVITY.discordPollPostHourUtc); if (isUtcWeekday(post) && post.getTime() > now.getTime()) return post; } return utcAtHour(now, BASE_ACTIVITY.discordPollPostHourUtc); } function lastDiscordPollPostAt(now = new Date()) { for (let offset = 0; offset <= 7; offset += 1) { const post = utcAtHour(new Date(now.getTime() - offset * MS_PER_DAY$2), BASE_ACTIVITY.discordPollPostHourUtc); if (isUtcWeekday(post) && post.getTime() <= now.getTime()) return post; } return utcAtHour(now, BASE_ACTIVITY.discordPollPostHourUtc); } function msUntilNextDiscordPollPost(now = new Date()) { return Math.max(0, nextDiscordPollPostAt(now).getTime() - now.getTime()); } var STEAM_QUEST_STATUS_ID_PREFIX = "control-center__steam-quest-status-"; var STEAM_LIBRARY_SYNC_LABEL = /^(Check Game|Visit Steam|Sync Games)$/i; var STEAM_OWNERSHIP_DENIAL = /do not own|don['’]t own|not in your steam library|not in your library|must own this game/i; function hasSteamLibrarySyncControl(document_) { if (document_.querySelector(".btn-check-owned-games")) return true; return [...document_.querySelectorAll("a, button")].some((element) => STEAM_LIBRARY_SYNC_LABEL.test(controlLabel(element))); } function hasSteamOwnershipDenialText(document_) { return STEAM_OWNERSHIP_DENIAL.test(pageText(document_)); } function isChooseYourOwnGameQuest(quest) { return /choose[- ]your[- ]own[- ]game/i.test(`${quest.name} ${quest.href ?? ""}`); } function steamQuestStatusFromText(text) { const trimmed = text.trim(); if (/^complete$/i.test(trimmed)) return "complete"; if (/^incomplete$/i.test(trimmed)) return "incomplete"; } function steamQuestEligibilityFromStatusText(text, quest) { if (/unavailable|ineligible|locked|not owned|unowned/i.test(text.trim())) return "ineligible"; if (isChooseYourOwnGameQuest(quest)) return "eligible"; return "unknown"; } function parseSteamQuestRewardArp(text) { const compact = text.replaceAll(",", ""); const arpAt = compact.toUpperCase().indexOf(" ARP"); if (arpAt === -1) return; const amountToken = compact.slice(0, arpAt).trim().split(" ").at(-1); const reward = Number(amountToken); return Number.isFinite(reward) && reward > 0 ? reward : void 0; } function pathnameFromHref$1(href) { if (!href) return; try { return new URL(href, "https://na.alienwarearena.com").pathname; } catch { return href.startsWith("/") ? href : void 0; } } function buildSteamQuestRow(options) { const { name, href, rewardArp, statusText, id } = options; const identity = { name, ...href && { href } }; const status = steamQuestStatusFromText(statusText) ?? "incomplete"; const row = { name, rewardArp, status, eligibility: status === "complete" ? "eligible" : steamQuestEligibilityFromStatusText(statusText, identity) }; if (id) row.id = id; if (href) row.href = href; return row; } function parseSteamQuestRowFromStatusCell(card, statusCell) { const id = statusCell.id.startsWith(STEAM_QUEST_STATUS_ID_PREFIX) ? statusCell.id.slice(35) : void 0; const row = statusCell.closest("tr") ?? statusCell.parentElement; if (!row) return; const questLink = row.querySelector("a[href*=\"/steam/quests/\"]"); const name = questLink?.textContent?.replaceAll(/\s+/g, " ").trim() || row.querySelector("a")?.textContent?.replaceAll(/\s+/g, " ").trim(); if (!name) return; const rewardArp = parseSteamQuestRewardArp((id ? card.querySelector(`#control-center__steam-quest-reward-${id}`) : void 0)?.textContent ?? row.textContent ?? ""); if (rewardArp === void 0) return; const href = pathnameFromHref$1(questLink?.getAttribute("href") ?? void 0); return buildSteamQuestRow({ name, rewardArp, statusText: statusCell.textContent?.trim() ?? "", ...id && { id }, ...href && { href } }); } function parseSteamQuestRowFromTableRow(row) { const questLink = row.querySelector("a[href*=\"/steam/quests/\"]"); const name = questLink?.textContent?.replaceAll(/\s+/g, " ").trim(); if (!name) return; const rewardArp = parseSteamQuestRewardArp(row.textContent ?? ""); if (rewardArp === void 0) return; const statusCell = [...row.querySelectorAll("td")].find((cell) => steamQuestStatusFromText(cell.textContent ?? "")); const href = pathnameFromHref$1(questLink?.getAttribute("href") ?? void 0); return buildSteamQuestRow({ name, rewardArp, statusText: statusCell?.textContent?.trim() ?? "", ...href && { href } }); } function scrapeSteamQuestRowsFromDocument(document_) { const card = findActivityCard(document_, /^Steam Quests$/i); if (!card) return []; const fromStatusIds = [...card.querySelectorAll("[id^=\"control-center__steam-quest-status-\"]")].map((cell) => parseSteamQuestRowFromStatusCell(card, cell)).filter((row) => row !== void 0); if (fromStatusIds.length > 0) return fromStatusIds; return [...card.querySelectorAll("tr")].map((row) => parseSteamQuestRowFromTableRow(row)).filter((row) => row !== void 0); } function steamQuestsCapFromRows(quests) { if (quests.length === 0) return; return remainingSteamQuestRowsFromList(quests).length > 0 ? "available" : "capped"; } function steamQuestRowKey(row) { return row.id ?? row.href ?? row.name.toLowerCase(); } function mergeSteamQuestRows(scraped, previous) { if (!previous || previous.length === 0) return scraped; const priorByKey = new Map(previous.map((row) => [steamQuestRowKey(row), row])); return scraped.map((row) => { const prior = priorByKey.get(steamQuestRowKey(row)); if (!prior) return row; if (row.eligibility !== "unknown" || prior.status !== row.status) return row; const merged = { ...row, eligibility: prior.eligibility }; if (prior.steamAppId !== void 0) merged.steamAppId = prior.steamAppId; if (prior.isFree !== void 0) merged.isFree = prior.isFree; if (prior.libraryPending === true) merged.libraryPending = true; return merged; }); } function remainingSteamQuestRowsFromList(quests) { return quests.filter((quest) => quest.status === "incomplete" && quest.eligibility !== "ineligible"); } function remainingSteamQuestRows(siteState) { return remainingSteamQuestRowsFromList(siteState.steamQuests?.quests ?? []); } function remainingSteamQuestRewards(siteState) { const scraped = scrapedRemainingSteamQuestRewards(siteState); if (scraped !== void 0) return scraped; return [...BASE_ACTIVITY.steamQuestBases]; } function scrapedRemainingSteamQuestRewards(siteState) { const quests = siteState.steamQuests?.quests; if (!quests || quests.length === 0) return; return remainingSteamQuestRowsFromList(quests).map((quest) => quest.rewardArp); } function requiresSteamQuestEligibilityFetch(state) { return (state.steamQuests?.quests ?? []).some((quest) => { if (quest.status !== "incomplete" || !quest.href || isChooseYourOwnGameQuest(quest)) return false; if (quest.eligibility === "unknown") return true; return quest.eligibility === "ineligible" && quest.isFree === void 0; }); } function scrapeSteamPlayEligibilityFromDocument(document_, options = {}) { if ((options.personalHours ?? 0) > 0) return "eligible"; if (options.href && isChooseYourOwnGameQuest({ name: "", href: options.href })) return "eligible"; const body = pageText(document_); if (/completed this quest/i.test(body)) return "eligible"; if (document_.querySelector(".btn-start-quest, a[href^=\"steam://\"]")) return "eligible"; if ([...document_.querySelectorAll("a, button")].some((element) => /^Launch Game$/i.test(controlLabel(element)))) return "eligible"; const progress = document_.querySelector(":scope .progress-steam-quest [aria-valuenow]"); const played = Number(progress?.getAttribute("aria-valuenow") ?? ""); if (Number.isFinite(played) && played > 0) return "eligible"; if (hasSteamLibrarySyncControl(document_) || hasSteamOwnershipDenialText(document_)) return "ineligible"; return "unknown"; } function applySteamQuestsFromDocument(next, document_) { const scraped = scrapeSteamQuestRowsFromDocument(document_); if (scraped.length === 0) return; const quests = mergeSteamQuestRows(scraped, next.steamQuests?.quests); next.steamQuests = { scrapedAt: new Date().toISOString(), quests }; const cap = steamQuestsCapFromRows(quests); if (cap) next.caps.steamQuests = cap; } function applySteamQuestDetailFromDocument(next, document_, pagePath) { const quests = [...next.steamQuests?.quests ?? []]; if (quests.length === 0) return; const index = quests.findIndex((quest) => quest.href && pagePath.includes(quest.href)); if (index === -1) return; const current = quests[index]; if (!current) return; const isQuestComplete = /completed this quest/i.test(pageText(document_)); const scrapedEligibility = scrapeSteamPlayEligibilityFromDocument(document_, current.href ? { href: current.href } : {}); let eligibility = scrapedEligibility; if (isQuestComplete) eligibility = "eligible"; else if (scrapedEligibility === "unknown") eligibility = current.eligibility; const steamAppId = scrapeSteamAppIdFromDocument(document_) ?? current.steamAppId; const updated = { ...current, eligibility, status: isQuestComplete ? "complete" : current.status }; if (steamAppId !== void 0) updated.steamAppId = steamAppId; quests[index] = updated; next.steamQuests = { scrapedAt: new Date().toISOString(), quests }; const cap = steamQuestsCapFromRows(quests); if (cap) next.caps.steamQuests = cap; } function canEarnCommunityEventArp(event) { return event?.playEligibility !== "ineligible"; } function scrapeLiveCommunityEventBanner(document_) { const bannerLink = document_.querySelector(":scope a.community-event-banner") ?? document_.querySelector(":scope .community-event-banner a[href*='/steam/community-event/']") ?? [...document_.querySelectorAll(":scope a[href*='/steam/community-event/']")].find((link) => /LIVE/i.test(link.textContent ?? "")); if (!bannerLink?.href) return; const path = bannerLink.pathname || bannerLink.getAttribute("href") || ""; if (!path.includes("/steam/community-event/")) return; const title = bannerLink.textContent?.replaceAll(/\s+/g, " ").trim(); const result = { url: path }; if (title) result.title = title; return result; } function isCommunityGateMet(milestone, communityHours) { if (milestone.isCommunityUnlocked) return true; const required = milestone.communityHoursRequired; return required !== void 0 && communityHours !== void 0 && communityHours >= required; } function applyCommunityHoursUnlocks(milestones, communityHours) { if (communityHours === void 0) return milestones; return milestones.map((milestone) => { if (isCommunityGateMet(milestone, communityHours)) return milestone.isCommunityUnlocked ? milestone : { ...milestone, isCommunityUnlocked: true }; return milestone; }); } function milestoneSortKey(milestone) { return milestone.communityHoursRequired ?? milestone.index; } function applySequentialCommunityAwards(milestones) { let lastAwardedKey = Number.NEGATIVE_INFINITY; for (const milestone of milestones) { if (!milestone.isAwarded) continue; const key = milestoneSortKey(milestone); if (key > lastAwardedKey) lastAwardedKey = key; } if (lastAwardedKey === Number.NEGATIVE_INFINITY) return milestones; return milestones.map((milestone) => { if (milestone.isAwarded || milestoneSortKey(milestone) >= lastAwardedKey) return milestone; return { ...milestone, isAwarded: true, isCommunityUnlocked: true }; }); } function personalHoursFromMilestones(milestones, scrapedHours) { let hours = scrapedHours; for (const milestone of milestones) if (milestone.isAwarded && milestone.personalHoursRequired > hours) hours = milestone.personalHoursRequired; return hours; } function isPersonalHoursMet(milestone, personalHours) { return milestone.personalHoursRequired <= personalHours; } function isCommunityEventMilestonePending(milestone, personalHours, communityHours) { if (milestone.isAwarded || milestone.arpReward <= 0) return false; return isPersonalHoursMet(milestone, personalHours) || isCommunityGateMet(milestone, communityHours); } function computePendingCommunityEventArp(personalHours, milestones, communityHours) { return milestones.filter((milestone) => isCommunityEventMilestonePending(milestone, personalHours, communityHours)).reduce((sum, milestone) => sum + milestone.arpReward, 0); } function breakDownCommunityEventPending(event) { let imminentArp = 0; let waitingCommunityArp = 0; let waitingPersonalArp = 0; let pendingCount = 0; for (const milestone of event.milestones) { if (!isCommunityEventMilestonePending(milestone, event.personalHours, event.communityHours)) continue; pendingCount += 1; const isPersonalMet = isPersonalHoursMet(milestone, event.personalHours); if (isPersonalMet && isCommunityGateMet(milestone, event.communityHours)) imminentArp += milestone.arpReward; else if (isPersonalMet) waitingCommunityArp += milestone.arpReward; else waitingPersonalArp += milestone.arpReward; } return { imminentArp, waitingCommunityArp, waitingPersonalArp, pendingCount }; } function formatCommunityEventArp(baseArp, allArpPct = 0) { if (allArpPct > 0) return `~${Math.round(baseArp * (1 + allArpPct))} ARP`; return `${baseArp} ARP`; } function describeWaitingPersonalArp(event, waitingPersonalArp, allArpPct) { const unmet = event.milestones.filter((milestone) => !milestone.isAwarded && milestone.arpReward > 0 && isCommunityGateMet(milestone, event.communityHours) && !isPersonalHoursMet(milestone, event.personalHours)); let needHours = 0; for (const milestone of unmet) if (milestone.personalHoursRequired > needHours) needHours = milestone.personalHoursRequired; const moreHours = Math.max(0, needHours - event.personalHours); const head = formatCommunityEventArp(waitingPersonalArp, allArpPct); if (moreHours <= 0 || needHours <= 0) return `${head} unlocked — not awarded yet`; return `${head} unlocked — play ${moreHours}h more (${event.personalHours}h / ${needHours}h)`; } function describeCommunityEventPendingParts(event, allArpPct = 0) { const { imminentArp, waitingCommunityArp, waitingPersonalArp } = breakDownCommunityEventPending(event); const nextLocked = nextLockedCommunityArpMilestone(event); if (nextLocked === void 0 && imminentArp <= 0 && waitingCommunityArp <= 0 && waitingPersonalArp <= 0) return { text: "no unawarded ARP remaining" }; const parts = []; let later; if (waitingPersonalArp > 0) parts.push(describeWaitingPersonalArp(event, waitingPersonalArp, allArpPct)); const lockedArp = waitingCommunityArp > 0 ? waitingCommunityArp : nextLocked?.arpReward ?? 0; if (lockedArp > 0) { const waiting = describeWaitingCommunityArp(event, lockedArp, allArpPct); parts.push(waiting.text); later = waiting.later; } if (imminentArp > 0) parts.push(`${formatCommunityEventArp(imminentArp, allArpPct)} unlocked — not awarded yet`); if (parts.length === 0) return { text: "no unawarded ARP remaining" }; return later ? { text: parts.join("; "), later } : { text: parts.join("; ") }; } var COMMUNITY_SAMPLE_MAX = 96; var COMMUNITY_SAMPLE_VISIT_MIN_GAP_MS = 9e5; var COMMUNITY_HOURS_REMOTE_SAMPLE_MIN_MS = 36e5; var COMMUNITY_RATE_MIN_SPAN_MS = 9e5; var COMMUNITY_RATE_WINDOW_MS = 864e5; var COMMUNITY_TREND_WINDOW_MS = 1728e5; var COMMUNITY_TREND_HALF_MIN_MS = 648e5; var COMMUNITY_RATIO_MIN = .5; var COMMUNITY_RATIO_MAX = 2; var COMMUNITY_DECAY_TRUST = .5; var COMMUNITY_RATIO_FLAT_EPS = .03; var COMMUNITY_MAX_HOURS_PER_DAY = 8e4; function markCommunityEventEnded(event) { return { scrapedAt: event.scrapedAt, url: event.url, isLive: false, personalHours: event.personalHours, milestones: event.milestones, pendingArp: 0, awardedArp: event.awardedArp, ...event.title !== void 0 && { title: event.title }, ...event.receivedArpFromLog !== void 0 && { receivedArpFromLog: event.receivedArpFromLog } }; } function shouldSkipCommunityHoursSample(options) { const { source, gapMs, hours, lastHours } = options; if (source === "remote") return gapMs < COMMUNITY_HOURS_REMOTE_SAMPLE_MIN_MS; return gapMs < COMMUNITY_SAMPLE_VISIT_MIN_GAP_MS && hours === lastHours; } function appendCommunityHoursSample(samples, hours, atIso = new Date().toISOString(), source = "visit") { const atMs = Date.parse(atIso); if (!Number.isFinite(hours) || hours < 0 || Number.isNaN(atMs)) return samples; const next = [...samples]; const last = next.at(-1); if (last) { if (hours + 1 < last.hours) return [{ at: atIso, hours }]; const lastMs = Date.parse(last.at); if (Number.isFinite(lastMs) && shouldSkipCommunityHoursSample({ source, gapMs: atMs - lastMs, hours, lastHours: last.hours })) return next; } next.push({ at: atIso, hours }); if (next.length > COMMUNITY_SAMPLE_MAX) return next.slice(-96); return next; } function isSparseCommunityEventScrape(scraped, previous) { return scraped.isLive && previous?.isLive === true && previous.milestones.length > 0 && scraped.milestones.length === 0; } function mergeCommunityEventScrape(scraped, previous, options = {}) { if (previous && isSparseCommunityEventScrape(scraped, previous)) return previous; return mergeLiveCommunityEventScrape(scraped, previous, options); } function mergeLiveCommunityEventScrape(scraped, previous, options = {}) { if (!scraped.isLive) return markCommunityEventEnded(previous?.url === scraped.url ? { ...previous, ...scraped, isLive: false, pendingArp: 0 } : scraped); const source = options.source ?? "visit"; const sameEvent = previous && (previous.url === scraped.url || previous.title !== void 0 && scraped.title !== void 0 && previous.title === scraped.title); const hasAsceHistory = Boolean(sameEvent) && previous?.communityHoursSource === "asce"; let samples = sameEvent ? [...previous.communityHoursSamples ?? []] : []; if (!hasAsceHistory && scraped.communityHours !== void 0) samples = appendCommunityHoursSample(samples, scraped.communityHours, scraped.scrapedAt, source); const merged = { ...scraped }; if (samples.length > 0) merged.communityHoursSamples = samples; if (hasAsceHistory) merged.communityHoursSource = "asce"; return carryForwardCommunityEventFields(merged, previous, Boolean(sameEvent)); } function milestoneMergeKey(milestone) { return milestone.communityHoursRequired === void 0 ? `i:${milestone.index}` : `h:${milestone.communityHoursRequired}`; } function preferCommunityEventMilestone(scraped, previous) { if (!previous) return scraped; const arpReward = scraped.arpReward > 0 ? scraped.arpReward : previous.arpReward; const next = { ...previous, ...scraped, arpReward, isAwarded: scraped.arpReward > 0 ? scraped.isAwarded : previous.isAwarded, isCommunityUnlocked: scraped.isCommunityUnlocked || previous.isCommunityUnlocked }; if (scraped.arpReward <= 0 && previous.arpReward > 0) next.rewardLabel = previous.rewardLabel; if (scraped.communityHoursRequired === void 0 && previous.communityHoursRequired !== void 0) next.communityHoursRequired = previous.communityHoursRequired; if (scraped.personalHoursRequired <= 0 && previous.personalHoursRequired > 0) next.personalHoursRequired = previous.personalHoursRequired; return next; } function mergeCommunityEventMilestones(scraped, previous) { if (!previous || previous.length === 0) return scraped; const merged = new Map(); const previousByIndex = new Map(); for (const milestone of previous) { merged.set(milestoneMergeKey(milestone), milestone); previousByIndex.set(milestone.index, milestone); } for (const milestone of scraped) { const key = milestoneMergeKey(milestone); const previousMatch = merged.get(key) ?? (milestone.communityHoursRequired === void 0 ? previousByIndex.get(milestone.index) : void 0); if (previousMatch) merged.delete(milestoneMergeKey(previousMatch)); const next = preferCommunityEventMilestone(milestone, previousMatch); merged.set(milestoneMergeKey(next), next); } return merged.values().toArray().toSorted((left, right) => (left.communityHoursRequired ?? left.index) - (right.communityHoursRequired ?? right.index)); } function inferPersonalHoursRequired(existing) { const known = existing.filter((milestone) => milestone.arpReward > 0).map((milestone) => milestone.personalHoursRequired); if (known.length === 0) return 1; return Math.max(...known); } function splitMilestonesByHours(existing) { const byHours = new Map(); const withoutHours = []; for (const milestone of existing) { const hours = milestone.communityHoursRequired; if (hours === void 0) withoutHours.push(milestone); else byHours.set(hours, milestone); } return { byHours, withoutHours }; } function nextMilestoneIndex(existing) { let nextIndex = 1; for (const milestone of existing) if (milestone.index >= nextIndex) nextIndex = milestone.index + 1; return nextIndex; } function patchMilestoneFromGate(current, gate) { const isUnlocking = gate.unlocked && !current.isCommunityUnlocked; const isFillingArp = current.arpReward <= 0 && gate.arpReward > 0; if (!isUnlocking && !isFillingArp) return current; return { ...current, ...isUnlocking && { isCommunityUnlocked: true }, ...isFillingArp && { arpReward: gate.arpReward, rewardLabel: gate.label } }; } function upsertCommunityEventMilestoneGates(existing, gates) { if (gates.length === 0) return existing; const { byHours, withoutHours } = splitMilestonesByHours(existing); const inferredPersonal = inferPersonalHoursRequired(existing); let nextIndex = nextMilestoneIndex(existing); for (const gate of gates) { const current = byHours.get(gate.hours); if (current) { byHours.set(gate.hours, patchMilestoneFromGate(current, gate)); continue; } byHours.set(gate.hours, { index: nextIndex, personalHoursRequired: inferredPersonal, communityHoursRequired: gate.hours, arpReward: gate.arpReward, rewardLabel: gate.label, isCommunityUnlocked: gate.unlocked, isAwarded: false }); nextIndex += 1; } return [...withoutHours, ...byHours.values()].toSorted((left, right) => (left.communityHoursRequired ?? left.index) - (right.communityHoursRequired ?? right.index)); } function carryForwardCommunityEventFields(merged, previous, isSameEvent) { const next = { ...merged }; if (previous && isSameEvent && merged.personalHours <= 0 && previous.personalHours > 0) next.personalHours = previous.personalHours; if (isSameEvent && previous) { next.milestones = applySequentialCommunityAwards(applyCommunityHoursUnlocks(mergeCommunityEventMilestones(next.milestones, previous.milestones), next.communityHours)); next.personalHours = personalHoursFromMilestones(next.milestones, next.personalHours); next.pendingArp = computePendingCommunityEventArp(next.personalHours, next.milestones, next.communityHours); } if (next.personalHours > 0 || isSameEvent && previous?.playEligibility === "eligible" && merged.playEligibility !== "ineligible") next.playEligibility = "eligible"; if (isSameEvent && previous?.communityHoursSource === "asce" && previous.communityHours !== void 0 && (next.communityHours === void 0 || next.communityHours < previous.communityHours)) { next.communityHours = previous.communityHours; next.communityHoursSource = "asce"; } if (next.steamAppId === void 0 && previous?.steamAppId !== void 0) next.steamAppId = previous.steamAppId; if (next.isFree === void 0 && previous?.isFree !== void 0) next.isFree = previous.isFree; return next; } function lockedCommunityArpMilestones(event) { return event.milestones.filter((milestone) => { if (milestone.isAwarded || milestone.arpReward <= 0) return false; if (milestone.communityHoursRequired === void 0) return false; return !isCommunityGateMet(milestone, event.communityHours); }).toSorted((left, right) => (left.communityHoursRequired ?? Number.POSITIVE_INFINITY) - (right.communityHoursRequired ?? Number.POSITIVE_INFINITY)); } function nextLockedCommunityArpMilestone(event) { return lockedCommunityArpMilestones(event)[0]; } function waitingCommunityMilestones(event) { return lockedCommunityArpMilestones(event).filter((milestone) => isPersonalHoursMet(milestone, event.personalHours)); } function nextCommunityUnlockTarget(event) { return nextLockedCommunityArpMilestone(event)?.communityHoursRequired; } function estimateCommunityUnlockAt(event, targetHours, nowMs = Date.now()) { const currentHours = event.communityHours; if (currentHours === void 0) return; const hoursRemaining = targetHours - currentHours; if (hoursRemaining <= 0) return { targetHours, hoursRemaining: 0, hoursPerDay: 0, etaMs: 0, sampleCount: event.communityHoursSamples?.length ?? 0 }; const samples = event.communityHoursSamples ?? []; const rate = estimateCommunityHoursPerMs(samples, nowMs); if (rate === void 0 || rate <= 0) return; const hoursPerDay = rate * 864e5; if (hoursPerDay > COMMUNITY_MAX_HOURS_PER_DAY) return; const end = samples.at(-1); const measuredRatio = end ? communityDayOverDayRatio(samples, end) : void 0; return { targetHours, hoursRemaining, hoursPerDay, etaMs: communityEtaMs(hoursRemaining, rate, measuredRatio === void 0 ? 1 : optimisticCommunityRatio(measuredRatio)), sampleCount: samples.length }; } function estimateNextCommunityUnlock(event, nowMs = Date.now()) { const targetHours = nextCommunityUnlockTarget(event); if (targetHours === void 0) return; return estimateCommunityUnlockAt(event, targetHours, nowMs); } function parseCommunitySampleMs(sample) { const ms = Date.parse(sample.at); return Number.isFinite(ms) ? ms : void 0; } function sampleAtOrBefore(samples, tMs) { let best; let bestMs = Number.NEGATIVE_INFINITY; for (const sample of samples) { const ms = parseCommunitySampleMs(sample); if (ms !== void 0 && ms <= tMs && ms >= bestMs) { best = sample; bestMs = ms; } } return best; } function communityHoursPerMsBetween(start, end) { const startMs = parseCommunitySampleMs(start); const endMs = parseCommunitySampleMs(end); if (startMs === void 0 || endMs === void 0 || endMs - startMs < 12e4) return; const deltaHours = end.hours - start.hours; if (deltaHours <= 0) return; return deltaHours / (endMs - startMs); } function estimateCommunityHoursPerMs(samples, nowMs) { if (samples.length < 2) return; const end = samples.at(-1); if (!end) return; const endMs = parseCommunitySampleMs(end); if (endMs === void 0 || nowMs - endMs > 2592e5) return; const windowStart = sampleAtOrBefore(samples, endMs - COMMUNITY_RATE_WINDOW_MS); const fromWindow = windowStart ? communityHoursPerMsBetween(windowStart, end) : void 0; if (fromWindow !== void 0) return fromWindow; const first = samples.at(0); if (first && first !== end) { const fromHistory = communityHoursPerMsBetween(first, end); if (fromHistory !== void 0) { const startMs = parseCommunitySampleMs(first); if (startMs !== void 0 && endMs - startMs >= COMMUNITY_RATE_MIN_SPAN_MS) return fromHistory; } } const previous = samples.at(-2); return previous ? communityHoursPerMsBetween(previous, end) : void 0; } function communityDayOverDayRatio(samples, end) { const endMs = parseCommunitySampleMs(end); if (endMs === void 0) return; const mid = sampleAtOrBefore(samples, endMs - COMMUNITY_RATE_WINDOW_MS); const start = sampleAtOrBefore(samples, endMs - COMMUNITY_TREND_WINDOW_MS); if (!mid || !start || start === mid || mid === end) return; const midMs = parseCommunitySampleMs(mid); const startMs = parseCommunitySampleMs(start); if (midMs === void 0 || startMs === void 0 || endMs - midMs < COMMUNITY_TREND_HALF_MIN_MS || midMs - startMs < COMMUNITY_TREND_HALF_MIN_MS) return; const recent = communityHoursPerMsBetween(mid, end); const previous = communityHoursPerMsBetween(start, mid); if (recent === void 0 || previous === void 0 || previous <= 0) return; const ratio = recent / previous; if (!Number.isFinite(ratio)) return; return Math.min(COMMUNITY_RATIO_MAX, Math.max(COMMUNITY_RATIO_MIN, ratio)); } function optimisticCommunityRatio(measured) { if (measured >= 1) return measured; return 1 - (1 - measured) * COMMUNITY_DECAY_TRUST; } function communityEtaMs(remainingHours, ratePerMs, dailyRatio) { const linearMs = remainingHours / ratePerMs; if (Math.abs(dailyRatio - 1) < COMMUNITY_RATIO_FLAT_EPS) return linearMs; const ratePerDay = ratePerMs * 864e5; const lnRatio = Math.log(dailyRatio); const root = 1 + remainingHours * lnRatio / ratePerDay; if (root <= 0) return linearMs; const days = Math.log(root) / lnRatio; if (!Number.isFinite(days) || days <= 0) return linearMs; return days * 864e5; } function formatCommunityEta(etaMs) { if (etaMs <= 0) return "now"; const totalMinutes = Math.round(etaMs / 6e4); if (totalMinutes < 60) return `~${Math.max(1, totalMinutes)}m`; const totalHours = Math.round(etaMs / 36e5); if (totalHours < 48) return `~${totalHours}h`; return `~${(totalHours / 24).toFixed(1)}d`; } function describeWaitingCommunityProgress(event) { const eta = estimateNextCommunityUnlock(event); const target = eta?.targetHours ?? nextCommunityUnlockTarget(event); const parts = []; if (target !== void 0 && event.communityHours !== void 0) parts.push(`${Math.round(event.communityHours).toLocaleString()}/${target.toLocaleString()}h`); else if (event.communityHours !== void 0) parts.push(`${Math.round(event.communityHours).toLocaleString()}h`); if (eta) parts.push(`ETA ${formatCommunityEta(eta.etaMs)}`); return parts.join(" · "); } function describeWaitingCommunityArp(event, waitingCommunityArp, allArpPct = 0) { const locked = lockedCommunityArpMilestones(event); const next = locked[0]; const progress = describeWaitingCommunityProgress(event); const nextArp = next?.arpReward ?? 0; const laterArp = locked.slice(1).reduce((sum, milestone) => sum + milestone.arpReward, 0); const head = formatCommunityEventArp(nextArp > 0 ? nextArp : waitingCommunityArp, allArpPct); const later = nextArp > 0 && laterArp > 0 ? `+${formatCommunityEventArp(laterArp, allArpPct)} later` : void 0; const text = progress ? `${head} · ${progress}` : `${head} on community unlock`; return later ? { text, later } : { text }; } function describeWaitingCommunityArpLine(event, waitingCommunityArp, allArpPct = 0) { const { text, later } = describeWaitingCommunityArp(event, waitingCommunityArp, allArpPct); return later ? `${text} (${later})` : text; } function computeAwardedCommunityEventArp(milestones) { return milestones.filter((milestone) => milestone.isAwarded && milestone.arpReward > 0).reduce((sum, milestone) => sum + milestone.arpReward, 0); } function isCommunityEventRewardAction(action) { return /Steam Community Event Reward/i.test(action); } function sumCommunityEventRewardsFromArpLog(arpLog) { if (!arpLog) return 0; return arpLog.recent.filter((entry) => isCommunityEventRewardAction(entry.action)).reduce((sum, entry) => sum + entry.arp, 0); } function reconcileCommunityEventWithArpLog(event, arpLog) { const receivedArpFromLog = sumCommunityEventRewardsFromArpLog(arpLog); const milestones = event.milestones.map((milestone) => ({ ...milestone, isCommunityUnlocked: milestone.isCommunityUnlocked || milestone.isAwarded })).toSorted((left, right) => left.index - right.index); let remainingReceived = receivedArpFromLog; for (const milestone of milestones) if (milestone.isAwarded && milestone.arpReward > 0) remainingReceived = Math.max(0, remainingReceived - milestone.arpReward); for (const milestone of milestones) { if (milestone.isAwarded || milestone.arpReward <= 0 || !isPersonalHoursMet(milestone, event.personalHours) || remainingReceived < milestone.arpReward) continue; milestone.isAwarded = true; milestone.isCommunityUnlocked = true; remainingReceived -= milestone.arpReward; } const nextMilestones = applySequentialCommunityAwards(milestones); const hours = personalHoursFromMilestones(nextMilestones, event.personalHours); const next = { ...event, personalHours: hours, milestones: nextMilestones, pendingArp: computePendingCommunityEventArp(hours, nextMilestones, event.communityHours), awardedArp: computeAwardedCommunityEventArp(nextMilestones) }; if (receivedArpFromLog > 0) next.receivedArpFromLog = receivedArpFromLog; return next; } function parseLabeledHours(text, label) { const marker = `${label}: `; const start = text.indexOf(marker); if (start === -1) return; const slice = text.slice(start + marker.length); const match = /^([\d.]+)/.exec(slice); return match?.[1] ? Number(match[1]) : void 0; } function parseLeadingCount(text, unit) { const unitIndex = text.indexOf(` ${unit}`); if (unitIndex === -1) return; const token = text.slice(0, unitIndex).trim().split(" ").pop(); const value = token ? Number(token) : NaN; return Number.isFinite(value) ? value : void 0; } function isLabeledRowComplete(cell, label) { const needle = `${label}:`; const other = label === "Personal" ? "Community:" : "Personal:"; const scope = [...cell.querySelectorAll("p, div, li, span, tr, td")].find((node) => { const text = node.textContent ?? ""; return text.includes(needle) && !text.includes(other); }) ?? [...cell.querySelectorAll("p, div, li, span, tr, td")].find((node) => (node.textContent ?? "").includes(needle)) ?? cell; if (scope.querySelector(".fa-check, .fa-check-circle, .bi-check, .bi-check-lg")) return true; return /[✓✔]/.test(scope.textContent ?? ""); } function milestoneCellText(cell) { const parts = [cell.textContent ?? ""]; const sibling = cell.nextElementSibling; if (sibling && !sibling.classList.contains("carousel-cell")) parts.push(sibling.textContent ?? ""); return parts.join(" ").replaceAll(/\s+/g, " ").trim(); } function parseMilestoneCell(cell) { const text = milestoneCellText(cell); const milestoneMarker = text.indexOf("Milestone "); if (milestoneMarker === -1) return; const index = Number(text.slice(milestoneMarker + 10).split(" ", 1)[0]); if (!Number.isFinite(index)) return; const personalHoursRequired = parseLabeledHours(text, "Personal") ?? 0; const communityHours = parseLabeledHours(text, "Community"); const arpReward = parseLeadingCount(text, "ARP") ?? 0; const fragmentCount = parseLeadingCount(text, "Fragment"); const milestone = { index, personalHoursRequired, arpReward, rewardLabel: cell.querySelector(":scope h3")?.textContent?.trim() || cell.querySelector(":scope img[alt]")?.getAttribute("alt") || (arpReward > 0 ? `${arpReward} ARP` : "Reward"), isCommunityUnlocked: /Community Unlocked/i.test(text) || isLabeledRowComplete(cell, "Community"), isAwarded: /\bAwarded\b/i.test(text) && !/\bNot\s+Awarded\b/i.test(text) }; if (communityHours !== void 0) milestone.communityHoursRequired = communityHours; if (fragmentCount !== void 0 && arpReward <= 0) milestone.rewardLabel = `${fragmentCount} Fragments`; return milestone; } function parseCommunityEventPersonalHours(document_) { const hoursFromDom = document_.querySelector("#personal-hours")?.textContent?.trim(); if (hoursFromDom && /\d/.test(hoursFromDom)) { const fromDom = Number(hoursFromDom); if (Number.isFinite(fromDom)) return fromDom; } const body = pageText(document_); const hoursFromText = /Your Total Hours:\s*([\d.]+)/i.exec(body)?.[1]; if (hoursFromText) { const fromText = Number(hoursFromText); if (Number.isFinite(fromText)) return fromText; } const scriptSource = [...document_.querySelectorAll("script:not([src])")].map((script) => script.textContent ?? "").join("\n"); const minutesMatch = /personalPlaytime\s*=\s*(\d+)/i.exec(scriptSource) ?? /personalPlaytime\s*=\s*(\d+)/i.exec(body); if (minutesMatch?.[1]) return Math.floor(Number(minutesMatch[1]) / 60); return 0; } function isAsciiWhitespace(char) { return " \n\r\f\v".includes(char); } function trailingNumberToken(value) { let end = value.length; while (end > 0 && isAsciiWhitespace(value[end - 1] ?? "")) end -= 1; let start = end; while (start > 0) { const char = value[start - 1] ?? ""; if (char === "," || char >= "0" && char <= "9") { start -= 1; continue; } break; } if (start === end) return; return value.slice(start, end); } function leadingNumberToken(value) { let start = 0; while (start < value.length && isAsciiWhitespace(value[start] ?? "")) start += 1; let end = start; while (end < value.length) { const char = value[end] ?? ""; if (char === "," || char >= "0" && char <= "9") { end += 1; continue; } break; } if (start === end) return; return value.slice(start, end); } function parseHoursOfCap(text) { const hourIndex = text.toLowerCase().indexOf("hour"); if (hourIndex === -1) return; const beforeHour = text.slice(0, hourIndex); let leftRaw; let rightRaw; const ofIndex = beforeHour.toLowerCase().lastIndexOf(" of "); if (ofIndex === -1) { const slashIndex = beforeHour.lastIndexOf("/"); if (slashIndex === -1) return; leftRaw = beforeHour.slice(0, slashIndex); rightRaw = beforeHour.slice(slashIndex + 1); } else { leftRaw = beforeHour.slice(0, ofIndex); rightRaw = beforeHour.slice(ofIndex + 4); } const leftToken = trailingNumberToken(leftRaw); const rightToken = leadingNumberToken(rightRaw); if (!leftToken || !rightToken) return; const hours = Number(leftToken.replaceAll(",", "")); const cap = Number(rightToken.replaceAll(",", "")); if (!Number.isFinite(hours) || !Number.isFinite(cap) || hours < 0 || cap <= 0) return; return { hours, cap }; } function parseCommunityEventProgress(document_) { const candidates = [...document_.querySelectorAll("b, strong, .progress, .event-progress")].map((node) => node.textContent?.trim() ?? ""); candidates.push(pageText(document_)); for (const text of candidates) { const parsed = parseHoursOfCap(text); if (!parsed) continue; return { communityHours: parsed.hours, communityHoursCap: parsed.cap }; } return {}; } function parseCommunityEventTitleFromDocumentTitle(documentTitle) { const prefixMatch = /Steam Community Event\s*[-–]\s*/i.exec(documentTitle); if (!prefixMatch) return; let title = documentTitle.slice(prefixMatch.index + prefixMatch[0].length).trim(); const pipeIndex = title.lastIndexOf("|"); if (pipeIndex !== -1) { if (title.slice(pipeIndex + 1).trim().toLowerCase() === "alienware arena") title = title.slice(0, pipeIndex).trim(); } return title.length > 0 ? title : void 0; } function parseCommunityEventTitle(document_) { const fromDocumentTitle = parseCommunityEventTitleFromDocumentTitle(document_.title?.replaceAll(/\s+/g, " ").trim() ?? ""); if (fromDocumentTitle) return fromDocumentTitle; const fromEventLabel = document_.querySelector(".event-title-date, :scope .community-event-view .event-name")?.textContent?.replaceAll(/\s+/g, " ").trim(); if (fromEventLabel && !isCommunityEventLiveDateBar(fromEventLabel)) return fromEventLabel; } function isCommunityEventLiveDateBar(text) { const normalized = text.replaceAll(/\s+/g, " ").trim(); if (!/\bLIVE\b/i.test(normalized)) return false; return /\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/i.test(normalized) || /\bLIVE\s*\|/i.test(normalized); } function readCommunityEventLiveBadge(document_) { if (document_.querySelector(".event-closed")) return false; if (document_.querySelector(".live-text")) return true; if (isCommunityEventLiveDateBar(document_.querySelector(".event-title-date, .live-container")?.textContent?.replaceAll(/\s+/g, " ").trim() ?? "")) return true; } function scrapeCommunityEventFromDocument(document_, url) { const personalHours = parseCommunityEventPersonalHours(document_); const isLive = readCommunityEventLiveBadge(document_) !== false; const milestones = []; let personalHoursFloor = Number.isFinite(personalHours) ? personalHours : 0; for (const cell of document_.querySelectorAll(".carousel-cell")) { const milestone = parseMilestoneCell(cell); if (!milestone) continue; milestones.push(milestone); if (isLabeledRowComplete(cell, "Personal") && milestone.personalHoursRequired > personalHoursFloor) personalHoursFloor = milestone.personalHoursRequired; } milestones.sort((left, right) => left.index - right.index); const awardedMilestones = applySequentialCommunityAwards(milestones); const safeHours = personalHoursFromMilestones(awardedMilestones, personalHoursFloor); const titleMatch = parseCommunityEventTitle(document_); const progress = parseCommunityEventProgress(document_); const playEligibility = scrapeSteamPlayEligibilityFromDocument(document_, { personalHours: safeHours }); const steamAppId = scrapeSteamAppIdFromDocument(document_); const state = { scrapedAt: new Date().toISOString(), url, isLive, personalHours: safeHours, milestones: awardedMilestones, pendingArp: computePendingCommunityEventArp(safeHours, awardedMilestones, progress.communityHours), awardedArp: computeAwardedCommunityEventArp(awardedMilestones), playEligibility }; if (steamAppId !== void 0) state.steamAppId = steamAppId; if (titleMatch) state.title = titleMatch; if (progress.communityHours !== void 0) state.communityHours = progress.communityHours; if (progress.communityHoursCap !== void 0) state.communityHoursCap = progress.communityHoursCap; return state; } var ASCE_CACHE_KEY = "asceCommunityHours"; var ASCE_HOURS_URL = "https://raw.githubusercontent.com/MarvashMagalli/ASCE/main/stored_hours.json"; var ASCE_CONFIG_URL = "https://raw.githubusercontent.com/MarvashMagalli/ASCE/main/configAWA.json"; var ASCE_CACHE_TTL_MS = 15e5; var ASCE_ERROR_TTL_MS = 18e5; var ASCE_SAMPLE_MAX = 96; var FETCH_TIMEOUT_MS = 8e3; var inflightLookup = {}; function hasPendingAsceRefresh() { return inflightLookup.promise !== void 0; } function isRecord$1(value) { return typeof value === "object" && value !== null; } function isAsceCache(value) { return isRecord$1(value) && typeof value.at === "string"; } function gmGetJson(url) { return new Promise((resolve) => { _GM_xmlhttpRequest({ method: "GET", url, anonymous: true, timeout: FETCH_TIMEOUT_MS, onload: (response) => { if (response.status < 200 || response.status >= 300) { resolve(void 0); return; } try { resolve(JSON.parse(response.responseText)); } catch { resolve(void 0); } }, onerror: () => { resolve(void 0); }, ontimeout: () => { resolve(void 0); } }); }); } async function loadAsceCache() { const raw = await _GM.getValue(ASCE_CACHE_KEY, ""); if (typeof raw !== "string" || raw.length === 0) return { at: "" }; try { const parsed = JSON.parse(raw); if (!isAsceCache(parsed)) return { at: "" }; return parsed; } catch { return { at: "" }; } } async function saveAsceCache(cache) { await _GM.setValue(ASCE_CACHE_KEY, JSON.stringify(cache)); } function cacheAgeMs(cache) { const at = Date.parse(cache.at); if (Number.isNaN(at)) return Number.POSITIVE_INFINITY; return Date.now() - at; } function isCacheFresh(cache) { if (!cache.at) return false; const ttl = cache.error ? ASCE_ERROR_TTL_MS : ASCE_CACHE_TTL_MS; return cacheAgeMs(cache) < ttl; } function communityEventSlug(url) { try { const parts = new URL(url, "https://na.alienwarearena.com").pathname.split("/").filter(Boolean); const index = parts.indexOf("community-event"); if (index === -1) return; return parts[index + 1]; } catch { return; } } function isAsceFeedForEvent(feed, eventUrl) { const slug = communityEventSlug(eventUrl); return slug !== void 0 && slug === feed.game; } function asceSlotMs(timestamp, hour) { const match = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(timestamp); if (!match || hour < 0 || hour > 23) return; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); return Date.UTC(year, month - 1, day, hour, 0, 0); } function parseAsceHourPoint(value) { if (!isRecord$1(value)) return; const hours = value.value; const hour = value.hour; const timestamp = value.timestamp; if (typeof hours !== "number" || typeof hour !== "number" || typeof timestamp !== "string" || !Number.isFinite(hours) || hours < 0) return; const slotMs = asceSlotMs(timestamp, hour); if (slotMs === void 0) return; return { slotMs, hours }; } function parseAsceHours(raw) { if (!Array.isArray(raw)) return []; const bySlot = new Map(); for (const row of raw) { const parsed = parseAsceHourPoint(row); if (!parsed) continue; bySlot.set(parsed.slotMs, parsed.hours); } const samples = bySlot.keys().toArray().toSorted((left, right) => left - right).map((slotMs) => ({ at: new Date(slotMs).toISOString(), hours: bySlot.get(slotMs) ?? 0 })); if (samples.length > ASCE_SAMPLE_MAX) return samples.slice(-96); return samples; } function parseAsceArpReward(message) { const arpAt = message.toUpperCase().indexOf(" ARP"); if (arpAt === -1) return 0; const token = message.slice(0, arpAt).trim().split(" ").at(-1); const reward = Number(token); return Number.isFinite(reward) && reward > 0 ? reward : 0; } function byHoursAscending(left, right) { return left.hours - right.hours; } function parseAsceGates(rows) { if (!Array.isArray(rows)) return []; const byHours = new Map(); for (const row of rows) { if (!isRecord$1(row)) continue; const hours = row.current_hours; if (typeof hours !== "number" || !Number.isFinite(hours) || hours <= 0) continue; const label = typeof row.milestone_message === "string" && row.milestone_message.trim().length > 0 ? row.milestone_message.trim() : `${hours.toLocaleString()}h`; byHours.set(hours, { hours, arpReward: parseAsceArpReward(label), label, unlocked: row.unlocked === true }); } return byHours.values().toArray().toSorted(byHoursAscending); } function parseAsceConfig(raw) { if (!isRecord$1(raw)) return { gates: [], unlockedHours: [] }; const game = typeof raw.game === "string" ? raw.game : void 0; const goalHours = typeof raw.goal_hours === "number" && Number.isFinite(raw.goal_hours) ? raw.goal_hours : void 0; const gates = [...parseAsceGates(raw.milestones), ...parseAsceGates(raw.stretch_goals)]; const uniqueGates = new Map(gates.map((gate) => [gate.hours, gate])).values().toArray().toSorted(byHoursAscending); return { ...game && { game }, ...goalHours !== void 0 && { goalHours }, gates: uniqueGates, unlockedHours: uniqueGates.filter((gate) => gate.unlocked).map((gate) => gate.hours) }; } function requiresAsceGateRefresh(feed) { return feed !== void 0 && feed.gates === void 0; } async function fetchAsceFeed() { const [hoursRaw, configRaw] = await Promise.all([gmGetJson(ASCE_HOURS_URL), gmGetJson(ASCE_CONFIG_URL)]); const config = parseAsceConfig(configRaw); const samples = parseAsceHours(hoursRaw); if (!config.game || samples.length === 0) return; return { game: config.game, samples, unlockedHours: config.unlockedHours, gates: config.gates, ...config.goalHours !== void 0 && { goalHours: config.goalHours } }; } async function loadAsceCommunityFeed(options = {}) { const cache = await loadAsceCache(); if (!options.force && isCacheFresh(cache) && (cache.error || !requiresAsceGateRefresh(cache.feed))) { if (cache.error) return; return cache.feed; } if (inflightLookup.promise !== void 0) return inflightLookup.promise; const promise = (async () => { const feed = await fetchAsceFeed(); const at = new Date().toISOString(); if (!feed) { await saveAsceCache({ at, error: true }); return; } await saveAsceCache({ at, feed }); return feed; })(); inflightLookup.promise = promise; try { return await promise; } finally { delete inflightLookup.promise; } } function applyAsceUnlocks(milestones, unlockedHours) { if (unlockedHours.length === 0) return milestones; const unlocked = new Set(unlockedHours); return milestones.map((milestone) => { if (milestone.isCommunityUnlocked) return milestone; const requiredHours = milestone.communityHoursRequired; if (requiredHours === void 0 || !unlocked.has(requiredHours)) return milestone; return { ...milestone, isCommunityUnlocked: true }; }); } function resolveCommunityHours(scraped, asceHours) { if (scraped === void 0) return asceHours; if (asceHours === void 0) return scraped; return Math.max(scraped, asceHours); } function withLiveHoursSample(samples, event) { if (event.communityHours === void 0) return samples; const last = samples.at(-1); if (!last || event.communityHours <= last.hours) return samples; return [...samples, { at: event.scrapedAt, hours: event.communityHours }]; } function applyAsceFeedToEvent(event, feed) { if (!event.isLive || !isAsceFeedForEvent(feed, event.url)) return; const samples = withLiveHoursSample(feed.samples, event); if (samples.length === 0) return; const lastAsceHours = feed.samples.at(-1)?.hours; const communityHours = resolveCommunityHours(event.communityHours, lastAsceHours); const milestones = applySequentialCommunityAwards(applyCommunityHoursUnlocks(upsertCommunityEventMilestoneGates(applyAsceUnlocks(event.milestones, feed.unlockedHours), feed.gates ?? []), communityHours)); const next = { ...event, milestones, pendingArp: computePendingCommunityEventArp(event.personalHours, milestones, communityHours), communityHoursSamples: samples, communityHoursSource: "asce" }; if (communityHours !== void 0) next.communityHours = communityHours; if (next.communityHoursCap === void 0 && feed.goalHours !== void 0) next.communityHoursCap = feed.goalHours; return next; } function asceEventSignature(event) { const last = event.communityHoursSamples?.at(-1); const waitingHours = event.milestones.filter((milestone) => !milestone.isAwarded && milestone.arpReward > 0).map((milestone) => milestone.communityHoursRequired ?? 0).join(","); return [ event.communityHours ?? "", event.pendingArp, event.milestones.length, waitingHours, event.communityHoursSamples?.length ?? 0, last?.hours ?? "", last?.at ?? "" ].join("|"); } function applyFeedIfLive(state, feed) { const event = state.communityEvent; if (!event?.isLive) return; const next = applyAsceFeedToEvent(event, feed); if (!next) return; state.communityEvent = state.arpLog ? reconcileCommunityEventWithArpLog(next, state.arpLog) : next; } async function applyAsceCommunityHours(state) { if (!state.communityEvent?.isLive) return; const cache = await loadAsceCache(); if (cache.feed && !cache.error) applyFeedIfLive(state, cache.feed); if (!isCacheFresh(cache)) { loadAsceCommunityFeed(); return; } if (cache.error || !requiresAsceGateRefresh(cache.feed)) return; const feed = await loadAsceCommunityFeed({ force: true }); if (feed) applyFeedIfLive(state, feed); } async function didRefreshAsceCommunityHours(state, options = {}) { const event = state.communityEvent; if (!event?.isLive) return false; const before = asceEventSignature(event); const feed = await loadAsceCommunityFeed({ force: options.force === true }); if (!feed) return false; applyFeedIfLive(state, feed); const next = state.communityEvent; if (!next) return false; return asceEventSignature(next) !== before; } function pageWindow() { try { return _unsafeWindow; } catch {} return globalThis; } function asFiniteNumber(value) { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value.replaceAll(",", "")); if (Number.isFinite(parsed)) return parsed; } } function readPageNumber(name) { try { return asFiniteNumber(pageWindow()[name]); } catch { return; } } function parseInlineNumber(document_, names) { const pattern = new RegExp(String.raw`(?:var\s+|window\.)?(?:${names.join("|")})\s*=\s*(\d+)`); for (const script of document_.querySelectorAll("script")) { const match = pattern.exec(script.textContent ?? ""); if (match?.[1]) return Number(match[1]); } } function readPageArpTier(document_ = document) { if (document_ === document) { const tier = readPageNumber("arp_tier"); if (tier !== void 0 && tier >= 0) return tier; } const fromScript = parseInlineNumber(document_, ["arp_tier"]); if (fromScript !== void 0) return fromScript; const tierImg = document_.querySelector("img[src*=\"/images/content/tier-tags/\"]"); const tierMatch = /tier-tags\/(\d+)\.png/.exec(tierImg?.src ?? ""); if (!tierMatch?.[1]) return; const tier = Number(tierMatch[1]); return Number.isFinite(tier) ? tier : void 0; } function readPageFragmentBalance(document_ = document) { if (document_ === document) { const fragments = readPageNumber("fragment_balance"); if (fragments !== void 0 && fragments >= 0) return fragments; } const fromScript = parseInlineNumber(document_, ["fragment_balance"]); return fromScript !== void 0 && fromScript >= 0 ? fromScript : void 0; } function readPageRedeemableArp(document_ = document) { const names = [ "arp_balance", "user_arp", "arp_points", "redeemable_arp" ]; if (document_ === document) for (const name of names) { const value = readPageNumber(name); if (value !== void 0 && value >= 0) return value; } const fromScript = parseInlineNumber(document_, names); return fromScript !== void 0 && fromScript >= 0 ? fromScript : void 0; } function giveawayKeyFromUnknown(value) { if (typeof value !== "object" || !value) return; const row = value; const id = row.giveaway_id ?? row.giveawayId ?? row.id; if (id === void 0 || id === null) return; const status = typeof row.status === "string" ? row.status : ""; const entry = { giveawayId: String(id), status }; const remaining = asFiniteNumber(row.remaining); if (remaining !== void 0) entry.remaining = remaining; return entry; } function readPageGiveawayKeys() { let raw; try { raw = pageWindow().giveawayKeys; } catch { return []; } if (!Array.isArray(raw)) return []; const keys = []; for (const item of raw) { const entry = giveawayKeyFromUnknown(item); if (entry) keys.push(entry); } return keys; } function giveawayKeyStatus(giveawayId) { return readPageGiveawayKeys().find((entry) => entry.giveawayId === giveawayId); } var SETTINGS_KEY = "artifactOptimizerSettings"; var HOURS_PER_DAY = 24; var MS_PER_HOUR$1 = 36e5; var COOLDOWN_MS = HOURS_PER_DAY * MS_PER_HOUR$1; var NOTIFICATION_TYPE_KEYS = [ "swap", "community", "vault", "giveaways" ]; var NOTIFICATION_TYPE_COPY = { swap: { title: "Recommended swap", hint: "When a better loadout is waiting on a 24h lock — not every unlock." }, community: { title: "Community Event", hint: "When community hours unlock pending ARP." }, vault: { title: "Game Vault", hint: "When the vault opens or new games appear." }, giveaways: { title: "New giveaways", hint: "Official Alienware key giveaways — not community giveaways." } }; var DEFAULT_NOTIFICATION_TYPES = { swap: true, community: true, vault: true, giveaways: true }; var DEFAULT_ACTIVITIES = { timeOnSite: { enabled: true, frequency: 1 }, steamQuests: { enabled: true, frequency: 1 }, watchTwitch: { enabled: true, frequency: 1 }, dailyCalendar: { enabled: true, frequency: 1 }, discordPoll: { enabled: true, frequency: 1 }, dailyQuests: { enabled: true, frequency: 1 }, steamCommunityEvent: { enabled: true, frequency: 1 } }; var defaultArtifactSettings = { activities: { ...DEFAULT_ACTIVITIES }, pendingVaultPurchaseArp: 0, manualArtifacts: [], preferScraped: true, slotCooldowns: [], preferredTwitchStreamers: [], utcDailyEndBufferHours: 1, browserNotifications: false, notificationTypes: { ...DEFAULT_NOTIFICATION_TYPES }, allowAccountActions: false }; function isPartialSettings(value) { return typeof value === "object" && !!value; } function mergeActivities(base, incoming) { if (!incoming) return base; const legacy = incoming; const next = { ...base }; if (legacy.communityEvent && !legacy.dailyQuests) next.dailyQuests = { enabled: legacy.communityEvent.enabled, frequency: typeof legacy.communityEvent.frequency === "number" ? legacy.communityEvent.frequency : 1 }; for (const key of Object.keys(DEFAULT_ACTIVITIES)) { const value = incoming[key]; if (!value) continue; next[key] = { enabled: value.enabled, frequency: typeof value.frequency === "number" ? value.frequency : 1 }; } return next; } function applyParsedSettings(settings, parsed) { settings.activities = mergeActivities(settings.activities, parsed.activities); if (typeof parsed.pendingVaultPurchaseArp === "number") settings.pendingVaultPurchaseArp = parsed.pendingVaultPurchaseArp; if (typeof parsed.manualFragments === "number") settings.manualFragments = parsed.manualFragments; if (Array.isArray(parsed.manualArtifacts)) settings.manualArtifacts = parsed.manualArtifacts; if (typeof parsed.preferScraped === "boolean") settings.preferScraped = parsed.preferScraped; if (Array.isArray(parsed.slotCooldowns)) settings.slotCooldowns = parsed.slotCooldowns; if (typeof parsed.vaultDiscountDismissedCycle === "string") { if (parsed.vaultDiscountDismissedCycle) settings.vaultDiscountDismissedCycle = parsed.vaultDiscountDismissedCycle; else delete settings.vaultDiscountDismissedCycle; } if (Array.isArray(parsed.preferredTwitchStreamers)) settings.preferredTwitchStreamers = parsePreferredTwitchStreamers(parsed.preferredTwitchStreamers.filter((item) => typeof item === "string").join("\n")); if (typeof parsed.utcDailyEndBufferHours === "number") settings.utcDailyEndBufferHours = clampUtcDailyEndBufferHours(parsed.utcDailyEndBufferHours); if (typeof parsed.browserNotifications === "boolean") settings.browserNotifications = parsed.browserNotifications; if (typeof parsed.allowAccountActions === "boolean") settings.allowAccountActions = parsed.allowAccountActions; settings.notificationTypes = mergeNotificationTypes(settings.notificationTypes, parsed.notificationTypes); } function mergeNotificationTypes(base, incoming) { if (!incoming) return base; const next = { ...base }; for (const key of NOTIFICATION_TYPE_KEYS) if (typeof incoming[key] === "boolean") next[key] = incoming[key]; return next; } function twitchLoginFromInput(value) { let text = value.trim(); if (!text) return ""; text = text.replace(/^https?:\/\//i, ""); text = text.replace(/^(www\.)?twitch\.tv\//i, ""); text = text.replace(/^@/, ""); return (text.split(/[/?#]/, 1)[0] ?? "").trim().toLowerCase(); } function parsePreferredTwitchStreamers(raw) { const logins = []; const seen = new Set(); for (const token of raw.split(/[\n,]+/)) { const login = twitchLoginFromInput(token); if (!login || seen.has(login)) continue; seen.add(login); logins.push(login); } return logins; } function clampUtcDailyEndBufferHours(hours) { if (!Number.isFinite(hours)) return 1; return Math.min(12, Math.max(0, hours)); } function utcDailyEndBufferMs(settings) { return clampUtcDailyEndBufferHours(settings.utcDailyEndBufferHours) * MS_PER_HOUR$1; } async function getArtifactSettings() { const raw = await _GM.getValue(SETTINGS_KEY); const settings = { ...defaultArtifactSettings, activities: { ...DEFAULT_ACTIVITIES }, manualArtifacts: [], slotCooldowns: [], preferredTwitchStreamers: [], notificationTypes: { ...DEFAULT_NOTIFICATION_TYPES } }; if (!raw) return settings; try { const parsedUnknown = typeof raw === "string" ? JSON.parse(raw) : raw; if (!isPartialSettings(parsedUnknown)) return settings; applyParsedSettings(settings, parsedUnknown); } catch (error) { console.error("[Artifact Optimizer] Error parsing settings:", error); } return settings; } async function saveArtifactSettings(patch) { const previous = await getArtifactSettings(); const next = { ...previous, ...patch, activities: patch.activities ? { ...previous.activities, ...patch.activities } : previous.activities, notificationTypes: patch.notificationTypes ? { ...previous.notificationTypes, ...patch.notificationTypes } : previous.notificationTypes, utcDailyEndBufferHours: clampUtcDailyEndBufferHours(patch.utcDailyEndBufferHours ?? previous.utcDailyEndBufferHours) }; await _GM.setValue(SETTINGS_KEY, JSON.stringify(next)); return next; } function findCooldownEntry(settings, position) { return settings.slotCooldowns.find((entry) => entry.position === position); } function isShowroomSlotLocked(position, options = {}) { if (options.equippedSlotLocked === true) return true; if (options.equippedSlotLocked === false) return false; return options.slotLocks?.[position] === true; } function showroomCooldownRemainingMs(settings, position, options = {}) { if (!isShowroomSlotLocked(position, options)) return 0; return cooldownRemainingMs(settings, position, options.now); } function cooldownRemainingMs(settings, position, now = Date.now()) { const entry = findCooldownEntry(settings, position); if (!entry) return 0; const changedAt = Date.parse(entry.changedAt); if (Number.isNaN(changedAt)) return 0; return Math.max(0, COOLDOWN_MS - (now - changedAt)); } async function recordSlotChange(position, artifactInstanceId) { const rest = (await getArtifactSettings()).slotCooldowns.filter((entry) => entry.position !== position); const entry = { position, changedAt: new Date().toISOString() }; if (artifactInstanceId !== void 0) entry.artifactInstanceId = artifactInstanceId; rest.push(entry); await saveArtifactSettings({ slotCooldowns: rest }); } var SLOT_POSITIONS = [ 1, 2, 3 ]; function isCompleteSlotLockMap(slotLocks) { return SLOT_POSITIONS.every((position) => typeof slotLocks[position] === "boolean"); } async function syncSlotLocksFromScrape(slotLocks, now = Date.now()) { const previous = (await getArtifactSettings()).slotCooldowns; const next = []; for (const position of SLOT_POSITIONS) { if (slotLocks[position] !== true) continue; const existing = previous.find((entry) => entry.position === position); if (existing) { next.push(existing); continue; } next.push({ position, changedAt: new Date(now).toISOString(), estimated: true }); } if (!isCompleteSlotLockMap(slotLocks)) { for (const entry of previous) if (slotLocks[entry.position] !== false && next.every((row) => row.position === entry.position)) next.push(entry); } if (JSON.stringify(previous) !== JSON.stringify(next)) await saveArtifactSettings({ slotCooldowns: next }); } function isNotificationTypeEnabled(settings, key) { return settings.notificationTypes[key] ?? true; } function areAccountActionsEnabled(settings) { return settings.allowAccountActions; } var SNAPSHOT_KEY = "artifactSnapshot"; function isArtifactSnapshot(value) { if (typeof value !== "object" || !value) return false; const v = value; return Array.isArray(v.artifacts) && typeof v.fragments === "number"; } async function loadSnapshot() { const raw = await _GM.getValue(SNAPSHOT_KEY); if (!raw) return; try { const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; return isArtifactSnapshot(parsed) ? parsed : void 0; } catch { return; } } async function saveSnapshot(snapshot) { await _GM.setValue(SNAPSHOT_KEY, JSON.stringify(snapshot)); } async function applySnapshotUpgrade(instanceId) { const snapshot = await loadSnapshot(); if (!snapshot) return; const current = snapshot.artifacts.find((artifact) => artifact.instanceId === instanceId); if (!current || current.tier >= ArtifactTier.Interstellar) return snapshot; const toTier = current.tier + 1; const family = getArtifactById(current.familyId); const cost = current.upgradeCost ?? fragmentCostToUpgradeFrom(current.tier) ?? 0; const upgraded = { ...current, tier: toTier, displayName: family ? displayNameFor(family, toTier) : current.displayName, maxLevel: toTier === ArtifactTier.Interstellar }; const nextCost = fragmentCostToUpgradeFrom(toTier); if (nextCost === void 0) delete upgraded.upgradeCost; else upgraded.upgradeCost = nextCost; const next = { ...snapshot, scrapedAt: new Date().toISOString(), fragments: Math.max(0, snapshot.fragments - cost), artifacts: snapshot.artifacts.map((artifact) => artifact.instanceId === instanceId ? upgraded : artifact) }; await saveSnapshot(next); return next; } function readFragmentBalance(document_) { const fromPage = readPageFragmentBalance(document_); if (fromPage !== void 0) return fromPage; const text = document_.body?.textContent ?? ""; const match = /Fragments:\s*([\d,]+)/i.exec(text); if (match?.[1]) return Number(match[1].replaceAll(",", "")); return 0; } function readUsernameFrom(document_, pathHint) { const pathMatch = /\/member\/([^/]+)\/artifacts/.exec(pathHint ?? location.pathname); if (pathMatch?.[1]) return pathMatch[1]; const link = document_.querySelector("a[href*=\"/member/\"][href$=\"/artifacts\"]"); return (link ? /\/member\/([^/]+)\/artifacts/.exec(link.getAttribute("href") ?? "") : void 0)?.[1]; } function readUsername() { return readUsernameFrom(document); } var USER_ARTIFACTS_ROOM_PATH = "/user-artifacts-room"; function resolveShowroomUrl(username) { const name = username ?? readUsername(); if (name) return `/member/${encodeURIComponent(name)}/artifacts`; const link = document.querySelector("a[href*=\"/member/\"][href$=\"/artifacts\"]"); if (link?.pathname) return link.pathname; return USER_ARTIFACTS_ROOM_PATH; } function parseEquippedPosition(card) { const unequip = card.parentElement?.querySelector("button[onclick*=\"unequipArtifact\"]"); if (!unequip) return; const match = /unequipArtifact\s*\(\s*\d+\s*,\s*([123])\s*\)/.exec(unequip.getAttribute("onclick") ?? ""); if (!match?.[1]) return; return Number(match[1]); } function normalizeName(value) { return value.replaceAll(/\s+/g, " ").trim().toLowerCase(); } function isShowcaseSlotLocked(slot) { if (slot.querySelector(":scope i.fa-lock-open, :scope i.fa-unlock")) return false; return Boolean(slot.querySelector(":scope i.fa-lock")); } function scrapeShowcaseSlots(document_) { const root = document_.querySelector(".slots"); const slots = root ? [...root.querySelectorAll(":scope > .slot")] : [...document_.querySelectorAll(".slot")]; const result = []; let position = 1; for (const slot of slots) { if (position > 3) break; const displayName = ((slot.querySelector(":scope .slot-front img") ?? slot.querySelector(":scope img"))?.alt ?? "").trim(); if (!displayName || /^artifact$/i.test(displayName)) { position = position + 1; continue; } result.push({ position, displayName, isLocked: isShowcaseSlotLocked(slot) }); position = position + 1; } return result; } function applyShowcaseEquips(artifacts, showcase) { const slotLocks = { 1: false, 2: false, 3: false }; for (const slot of showcase) { slotLocks[slot.position] = slot.isLocked; const match = artifacts.find((artifact) => normalizeName(artifact.displayName) === normalizeName(slot.displayName)); if (!match) continue; match.equippedPosition = slot.position; match.slotLocked = slot.isLocked; } return slotLocks; } function parseFooterTier(card) { const tip = card.querySelector("img[data-original-title]")?.dataset.originalTitle ?? ""; const tierLabel = /(Weapon|Clothing|Power|Language|Precious Gems|Tech|Knowledge|Social|Architecture)\s*-\s*(Rust|Bronze|Silver|Gold|Platinum|Interstellar)/i.exec(tip)?.[2]?.toLowerCase(); if (!tierLabel) return; return { rust: ArtifactTier.Rust, bronze: ArtifactTier.Bronze, silver: ArtifactTier.Silver, gold: ArtifactTier.Gold, platinum: ArtifactTier.Platinum, interstellar: ArtifactTier.Interstellar }[tierLabel]; } function scrapeShowroomFromDocument(document_, pathHint) { const cards = document_.querySelectorAll("a.artifact-list-item.change-artifact-modal"); const artifacts = []; for (const card of cards) { const instanceId = Number(card.dataset.id); if (Number.isNaN(instanceId)) continue; const displayName = (card.dataset.title ?? "").trim(); if (!displayName) continue; const resolved = resolveArtifactByDisplayName(displayName); const footerTier = parseFooterTier(card); const tier = resolved?.tier ?? footerTier; if (tier === void 0 || !resolved) { console.warn("[Artifact Optimizer] Unrecognized artifact:", displayName); continue; } const upgradeCostRaw = card.dataset.upgradeCost; const parsedUpgradeCost = upgradeCostRaw === void 0 || upgradeCostRaw === "" ? void 0 : Number(upgradeCostRaw); const upgradeCost = Number.isNaN(parsedUpgradeCost) ? void 0 : parsedUpgradeCost; const isMaxLevel = card.dataset.maxLevel === "true" || card.dataset.maxLevel === "1" || upgradeCost === 0; const owned = { instanceId, familyId: resolved.definition.id, displayName, tier, category: resolved.definition.category, maxLevel: isMaxLevel, perkDescription: card.dataset.descriptionPerk ?? "" }; if (upgradeCost !== void 0) owned.upgradeCost = upgradeCost; const equippedPosition = parseEquippedPosition(card); if (equippedPosition !== void 0) owned.equippedPosition = equippedPosition; artifacts.push(owned); } const slotLocks = applyShowcaseEquips(artifacts, scrapeShowcaseSlots(document_)); return { scrapedAt: new Date().toISOString(), username: readUsernameFrom(document_, pathHint), fragments: readFragmentBalance(document_), artifacts, slotLocks }; } function scrapeShowroom() { return scrapeShowroomFromDocument(document, location.pathname); } function isShowroomDocumentReady(document_) { return Boolean(document_.querySelector("a.artifact-list-item.change-artifact-modal, #weapon-section")); } async function waitForShowroomDocument(timeoutMs = 12e3) { if (isShowroomDocumentReady(document)) return; await new Promise((resolve) => { let isSettled = false; const observer = new MutationObserver(() => { if (isShowroomDocumentReady(document)) finish(); }); const timer = setTimeout(finish, timeoutMs); function finish() { if (isSettled) return; isSettled = true; observer.disconnect(); clearTimeout(timer); resolve(); } observer.observe(document.documentElement, { childList: true, subtree: true }); }); } async function scrapeAndPersist() { if (!isShowroomDocumentReady(document)) await waitForShowroomDocument(); if (!isShowroomDocumentReady(document)) { const existing = await loadSnapshot(); if (existing) return existing; } const snapshot = scrapeShowroom(); if (snapshot.artifacts.length === 0) { const existing = await loadSnapshot(); if (existing && existing.artifacts.length > 0) return existing; } await saveSnapshot(snapshot); await syncSlotLocksFromScrape(snapshot.slotLocks ?? {}); return snapshot; } function isArtifactsShowroomPage() { return /\/member\/[^/]+\/artifacts\/?$/.test(location.pathname) || /\/user-artifacts-room\/?$/.test(location.pathname); } var ARP_LOG_ROW_SELECTOR = ".card-table-row"; var ARP_LOG_AFTER_ROWS_SELECTOR = "#arp-logs-per-page, #arp-log-chart"; var ARP_LOG_DATE_RE = /^\d{4}-\d{2}-\d{2}$/; var ARP_LOG_AMOUNT_RE = /^[+]?\d[\d,]*$/; var ARP_LOG_TOGGLE_RE = /^[▼▲^▾▴]$/; function parseRedeemableArpText(text) { const match = /Redeemable ARP:\s*([\d,]+)/i.exec(text); if (!match?.[1]) return; const value = Number(match[1].replaceAll(",", "")); return Number.isFinite(value) ? value : void 0; } function scrapeRedeemableArpFromDocument(document_) { const fromPage = readPageRedeemableArp(document_); if (fromPage !== void 0) return fromPage; return parseRedeemableArpText(pageText(document_)); } function applyRedeemableArpFromDocument(next, document_) { const arp = scrapeRedeemableArpFromDocument(document_); if (arp === void 0) return; next.arpLog = { scrapedAt: next.arpLog?.scrapedAt ?? new Date().toISOString(), recent: next.arpLog?.recent ?? [], ...next.arpLog, redeemableArp: arp }; } function parseArpAmount(text) { const value = Number(text.replaceAll(",", "").replace(/^\+/, "")); return Number.isFinite(value) ? value : void 0; } function scrapeArpLogRowsFromTable(document_) { const entries = []; for (const row of document_.querySelectorAll(ARP_LOG_ROW_SELECTOR)) { const cols = [...row.children].map((element) => (element.textContent ?? "").replaceAll(/\s+/g, " ").trim()); const date = cols.find((col) => ARP_LOG_DATE_RE.test(col)); const arpText = cols.findLast((col) => col !== date && ARP_LOG_AMOUNT_RE.test(col)); const action = cols.find((col) => col.length > 0 && col !== date && col !== arpText && !ARP_LOG_TOGGLE_RE.test(col)); if (!action || arpText === void 0) continue; const arp = parseArpAmount(arpText); if (arp === void 0) continue; const entry = { action, arp }; if (date) entry.date = date; entries.push(entry); } return entries; } function scrapeArpLogRowsFromText(body) { const actionNames = [ "Time On Site", "Game Prize", "Daily Login Calendar", "Daily Login Streak", "Discord Poll", "Steam Community Event Reward", "Steam Quest", "Steam Quests", "Twitch Passive", "Watch Twitch", "Community Event", "Forum Post", "Giveaway", "Battle Pass Reward", "Battle Pass", "Quest" ].join("|"); const rowPattern = new RegExp(String.raw`(${actionNames})\s+(\d+)\s+(\d{4}-\d{2}-\d{2})`, "gi"); const entries = []; for (const match of body.matchAll(rowPattern)) { const entry = { action: match[1] ?? "Unknown", arp: Number(match[2]) }; if (match[3]) entry.date = match[3]; entries.push(entry); } return entries; } function isArpLogDocumentReady(document_) { if (!document_.body) return false; return Boolean(document_.querySelector(`${ARP_LOG_ROW_SELECTOR}, ${ARP_LOG_AFTER_ROWS_SELECTOR}`)); } function arpLogSignature(document_) { if (!isArpLogDocumentReady(document_)) return ""; return scrapeArpLogFromDocument(document_).recent.map((entry) => `${entry.date ?? ""}|${entry.action}|${entry.arp}`).join(";"); } async function waitForArpLogDocument(timeoutMs = 12e3) { if (isArpLogDocumentReady(document)) return; await new Promise((resolve) => { let isSettled = false; const observer = new MutationObserver(() => { if (isArpLogDocumentReady(document)) finish(); }); const timer = setTimeout(finish, timeoutMs); function finish() { if (isSettled) return; isSettled = true; observer.disconnect(); clearTimeout(timer); resolve(); } observer.observe(document.documentElement, { childList: true, subtree: true }); }); } function scrapeArpLogFromDocument(document_) { const body = pageText(document_); const state = { scrapedAt: new Date().toISOString(), recent: [] }; const redeemableArp = parseRedeemableArpText(body); if (redeemableArp !== void 0) state.redeemableArp = redeemableArp; const lifetime = /Lifetime ARP:\s*([\d,]+)/i.exec(body); if (lifetime?.[1]) state.lifetimeArp = Number(lifetime[1].replaceAll(",", "")); const todayTotal = /Total ARP earned today:\s*([\d,]+)/i.exec(body); if (todayTotal?.[1]) state.todayDelta = Number(todayTotal[1].replaceAll(",", "")); else { const plusMatch = /Redeemable ARP:[\s\S]{0,80}?\+\s*([\d,]+)/i.exec(body); if (plusMatch?.[1]) state.todayDelta = Number(plusMatch[1].replaceAll(",", "")); } const fromTable = scrapeArpLogRowsFromTable(document_); state.recent = fromTable.length > 0 ? fromTable : scrapeArpLogRowsFromText(body); return state; } var ARP_LOG_UNSEEN_SCRAPED_AT = new Date(0).toISOString(); function mergeArpLogScrapedAt(scraped, previous) { if (scraped.recent.length > 0) return scraped.scrapedAt; if (previous.recent.length > 0) return previous.scrapedAt; return ARP_LOG_UNSEEN_SCRAPED_AT; } function mergeArpLogScrape(scraped, previous) { if (!previous) { if (scraped.recent.length === 0) return { ...scraped, scrapedAt: ARP_LOG_UNSEEN_SCRAPED_AT }; return scraped; } const seen = new Set(); const recent = []; for (const entry of [...scraped.recent, ...previous.recent]) { const key = `${entry.date ?? ""}|${entry.action}|${entry.arp}`; if (seen.has(key)) continue; seen.add(key); recent.push(entry); } recent.sort((left, right) => (right.date ?? "").localeCompare(left.date ?? "")); const redeemableArp = scraped.redeemableArp ?? previous.redeemableArp; const lifetimeArp = scraped.lifetimeArp ?? previous.lifetimeArp; const todayDelta = scraped.todayDelta ?? previous.todayDelta; return { scrapedAt: mergeArpLogScrapedAt(scraped, previous), ...redeemableArp !== void 0 && { redeemableArp }, ...lifetimeArp !== void 0 && { lifetimeArp }, ...todayDelta !== void 0 && { todayDelta }, recent }; } function serializePostBody(body, encoding) { if (encoding === "json") return JSON.stringify(body); const parameters = new URLSearchParams(); for (const [key, value] of Object.entries(body)) { if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") continue; parameters.set(key, String(value)); } return parameters.toString(); } function resultFromResponse(status, parsed) { if (status < 200 || status >= 300) { const result = { ok: false, status, error: parsed?.message ?? `Request failed (${status})` }; if (parsed?.message) result.message = parsed.message; return result; } if (parsed?.success === false) { const result = { ok: false, status, error: parsed.message ?? "Request rejected (slot may be on 24h cooldown or already set)." }; if (parsed.message) result.message = parsed.message; return result; } const result = { ok: true, status }; if (parsed?.message) result.message = parsed.message; return result; } async function postRequest(path, body, encoding) { try { const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest" }, body: serializePostBody(body, encoding) }); const text = await response.text(); let parsed; try { parsed = JSON.parse(text); } catch { parsed = void 0; } return resultFromResponse(response.status, parsed); } catch (error) { return { ok: false, status: 0, error: error instanceof Error ? error.message : "Network error" }; } } async function postJson(path, body) { return postRequest(path, body, "json"); } async function postForm(path, body) { return postRequest(path, body, "form"); } async function equipArtifact(artifactId, position) { const result = await postJson("/change-user-artifacts", { artifactId, position }); if (result.ok) await recordSlotChange(position, artifactId); return result; } async function upgradeArtifact(artifactId) { return postJson("/upgrade-user-artifact", { artifactId }); } async function claimBattlePassReward(path, body = {}) { return postForm(path, body); } function pickStuckLockNudgeTarget(artifacts) { const maxed = artifacts.filter((artifact) => artifact.maxLevel || artifact.upgradeCost === 0); if (maxed.length === 0) return; const target = maxed.find((artifact) => /warrior script/i.test(artifact.displayName)) ?? maxed[0]; if (!target) return; return { instanceId: target.instanceId, displayName: target.displayName }; } async function nudgeStuckSlotLocks(artifacts) { const target = pickStuckLockNudgeTarget(artifacts); if (!target) { console.info("[Artifact Optimizer] Stuck-lock nudge skipped — no maxed 0-frag artifact"); return; } const result = await upgradeArtifact(target.instanceId); console.info("[Artifact Optimizer] Stuck-lock nudge", { name: target.displayName, id: target.instanceId, ok: result.ok, message: result.message ?? result.error }); return result; } async function applyLoadout(targets, currentlyEquipped) { const results = []; const applied = []; for (const target of targets) { if (currentlyEquipped.some((c) => c.artifactId === target.artifactId && c.position === target.position)) continue; const equipResult = await equipArtifact(target.artifactId, target.position); results.push(equipResult); if (!equipResult.ok) return { results, allOk: false, applied }; applied.push(target); } return { results, allOk: results.every((result) => result.ok), applied }; } function scrapeBattlePassFromDocument(document_) { const body = pageText(document_); const popups = document_.querySelectorAll(".bp-popup[data-milestone-id]"); const tokensMatch = /BATTLE TOKENS\s*([\d,]+)\s*\/\s*([\d,]+)/i.exec(body); if ((body.match(/Ready to claim/gi) ?? []).length === 0 && popups.length === 0) return; const readyClaims = listReadyClaimsFromDocument(document_); const { readyToClaim, readyToClaimArp } = countBattlePassClaims(document_); const state = { readyToClaim, readyToClaimArp, url: "/control-center/battle-pass/1", scrapedAt: new Date().toISOString() }; if (readyClaims.length > 0) state.readyClaims = readyClaims; if (tokensMatch?.[1] && tokensMatch[2]) { state.tokens = Number(tokensMatch[1].replaceAll(",", "")); state.tokensMax = Number(tokensMatch[2].replaceAll(",", "")); } applyBattlePassCountdown(state, body); return state; } var BATTLE_PASS_ENDS_RE = /battle\s*pass\s*ends?\s*in\s*(\d{1,3}(?:\s*:\s*\d{1,2}){2,3})/i; function applyBattlePassCountdown(state, body) { const endsMatch = BATTLE_PASS_ENDS_RE.exec(body); if (!endsMatch?.[1]) return; const raw = endsMatch[1].replaceAll(/\s+/g, " ").trim(); state.endsInText = raw; const remaining = parseBattlePassCountdownMs(raw); if (remaining !== void 0) state.endsAt = new Date(Date.now() + remaining).toISOString(); } function parseBattlePassCountdownMs(text) { const parts = text.trim().split(":").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)); if (parts.length < 3 || parts.length > 4) return; const seconds = parts.at(-1) ?? 0; const minutes = parts.at(-2) ?? 0; const hours = parts.at(-3) ?? 0; return ((((parts.length === 4 ? parts[0] ?? 0 : 0) * 24 + hours) * 60 + minutes) * 60 + seconds) * 1e3; } function battlePassRemainingMs(battlePass, now = Date.now()) { if (!battlePass) return; if (battlePass.endsAt) { const endsAt = Date.parse(battlePass.endsAt); if (!Number.isNaN(endsAt)) return Math.max(0, endsAt - now); } if (!battlePass.endsInText || !battlePass.scrapedAt) return; const parsed = parseBattlePassCountdownMs(battlePass.endsInText); const scrapedAt = Date.parse(battlePass.scrapedAt); if (parsed === void 0 || Number.isNaN(scrapedAt)) return; return Math.max(0, parsed - (now - scrapedAt)); } function mergeBattlePassScrape(scraped, previous) { if (scraped.endsAt || !previous?.endsAt) return scraped; const merged = { ...scraped, endsAt: previous.endsAt }; if (!merged.endsInText && previous.endsInText) merged.endsInText = previous.endsInText; return merged; } function applyBattlePassEndFromDocument(next, document_) { if (!next.battlePass) return; const battlePass = { ...next.battlePass }; applyBattlePassCountdown(battlePass, pageText(document_)); next.battlePass = battlePass; } function listReadyClaimsFromDocument(document_) { const claims = []; const popups = document_.querySelectorAll(".bp-popup[data-milestone-id]"); for (const popup of popups) { if (!(popup instanceof HTMLElement)) continue; if (!(popup.dataset.milestoneId ?? "")) continue; claims.push(...readyClaimsFromPopup(popup)); } return uniqueReadyClaims(claims); } function readyClaimFromButton(button, popup) { const form = button.closest("form"); const claimPath = form?.getAttribute("action")?.trim() || void 0; const csrfToken = form?.querySelector("input[name=\"_csrf_token\"]")?.value; const claim = { milestoneId: (form instanceof HTMLElement ? form.dataset.milestoneId : void 0) ?? popup.dataset.milestoneId ?? "", isArp: isArpClaimPopup(popup) }; if (claimPath) claim.claimPath = claimPath; if (csrfToken) claim.csrfToken = csrfToken; if (!claimPath) claim.body = { ...datasetRecord(popup), ...datasetRecord(button) }; return claim; } function readyClaimsFromPopup(popup) { return [...popup.querySelectorAll(".bp-popup__claim-btn")].flatMap((button) => { if (!(button instanceof HTMLElement)) return []; return [readyClaimFromButton(button, popup)]; }); } function countBattlePassClaims(document_) { const readyClaims = listReadyClaimsFromDocument(document_); if (readyClaims.length > 0) return { readyToClaim: readyClaims.length, readyToClaimArp: readyClaims.filter((claim) => claim.isArp).length }; const legacy = (pageText(document_).match(/Ready to claim/gi) ?? []).length; return { readyToClaim: legacy, readyToClaimArp: legacy }; } function isBattlePassArpRewardTitle(title) { if (/ARP\s*Boost/i.test(title)) return true; return /^\d[\d,]*\s*ARP$/i.test(title.trim()); } function battlePassPopupTitle(popup) { return popup.querySelector(".bp-popup__title")?.textContent?.trim() ?? ""; } function isArpClaimPopup(popup) { return isBattlePassArpRewardTitle(battlePassPopupTitle(popup)); } function claimIdentity(claim) { return claim.claimPath ?? claim.milestoneId; } function uniqueReadyClaims(claims) { const seen = new Set(); const unique = []; for (const claim of claims) { const key = claimIdentity(claim); if (!key || seen.has(key)) continue; seen.add(key); unique.push(claim); } return unique; } function claimButtonIdentity(button, popup) { return button.closest("form")?.getAttribute("action")?.trim() || popup.dataset.milestoneId || ""; } function pushUniqueClaimButton(items, seen, button, popup) { if (!(button instanceof HTMLElement)) return; const key = claimButtonIdentity(button, popup); if (!key || seen.has(key)) return; seen.add(key); items.push({ button, popup }); } function listBattlePassClaimButtons(document_ = document, options = {}) { const shouldSkipArpBoosts = options.shouldSkipArpBoosts === true; const popups = document_.querySelectorAll(".bp-popup[data-milestone-id]"); const items = []; const seen = new Set(); for (const popup of popups) { if (!(popup instanceof HTMLElement)) continue; if (shouldSkipArpBoosts && isArpClaimPopup(popup)) continue; for (const button of popup.querySelectorAll(".bp-popup__claim-btn")) pushUniqueClaimButton(items, seen, button, popup); } return items; } function delay$2(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } var CLAIM_QUEUE_GAP_MS = 1500; async function waitWhile(isWaiting, timeoutMs, intervalMs = 100) { const startedAt = Date.now(); while (isWaiting() && Date.now() - startedAt < timeoutMs) await delay$2(intervalMs); } var claimEndpointCache = {}; function jsonishId(value) { return /^\d+$/.test(value) ? Number(value) : value; } function datasetRecord(element) { const record = {}; for (const [key, value] of Object.entries(element.dataset)) { if (value === void 0 || value === "") continue; record[key] = jsonishId(value); } return record; } function isBattlePassClaimPath(path) { const normalized = path.toLowerCase(); if (/giveaway|marketplace|ucf\/show|community-giveaway|vote\//.test(normalized)) return false; const hasClaim = /claim/.test(normalized); const hasBattlePass = /battle-?pass/.test(normalized); const hasMilestone = /milestone/.test(normalized); return hasClaim && (hasBattlePass || hasMilestone); } function endpointFromHref(raw) { let path = raw.trim(); try { const url = new URL(path, location.origin); if (url.origin !== location.origin) return; path = `${url.pathname}${url.search}`; } catch { return; } if (!isBattlePassClaimPath(path)) return; const hasIdInPath = /\/\d+\/?$/.test(urlPathname(path)); return { path: hasIdInPath ? path.replace(/\/\d+\/?$/, "") : path, hasIdInPath, idParameter: "milestoneId" }; } function urlPathname(path) { const q = path.indexOf("?"); return q === -1 ? path : path.slice(0, q); } function firstClaimEndpoint(candidates) { for (const candidate of candidates) { if (!candidate) continue; const endpoint = endpointFromHref(candidate); if (endpoint) return endpoint; } } function endpointFromClaimMarkup(document_) { for (const item of listBattlePassClaimButtons(document_)) { const endpoint = firstClaimEndpoint([ item.button.closest("form")?.getAttribute("action") ?? void 0, item.button.getAttribute("href") ?? void 0, item.button.getAttribute("formaction") ?? void 0, item.button.dataset.url, item.button.dataset.href, item.button.dataset.action, item.popup.dataset.url, item.popup.dataset.href, item.popup.dataset.claimUrl ]); if (endpoint) return endpoint; } } function idParameterFromSource(source) { const match = /(?:milestoneId|milestone_id|rewardId)\s*:/i.exec(source); return /rewardId/i.test(match?.[0] ?? "") ? "rewardId" : "milestoneId"; } function endpointFromQuotedPath(path, source, hasIdInPathHint = false) { if (!isBattlePassClaimPath(path)) return; const hasIdInPath = hasIdInPathHint || /\/\d+\/?$/.test(urlPathname(path)) || /\$\{|\{id\}|\{milestone/i.test(path); return { path: hasIdInPath ? path.replace(/\/\d+\/?$/, "").replace(/\/$/, "") : path, hasIdInPath, idParameter: idParameterFromSource(source) }; } function endpointFromScripts(source) { const concat = /['"](\/[^'"]*(?:claim[^'"]*(?:battle|milestone)|(?:battle-pass|milestone)[^'"]*claim)[^'"]*)['"]\s*\+/i.exec(source); if (concat?.[1]) { const fromConcat = endpointFromQuotedPath(concat[1], source, true); if (fromConcat) return fromConcat; } const quotedPath = /['"](\/[^'"]*(?:claim[^'"]*(?:battle|milestone)|(?:battle-pass|milestone)[^'"]*claim)[^'"]*)['"]/gi; let match; while (match = quotedPath.exec(source)) { const endpoint = endpointFromQuotedPath(match[1] ?? "", source); if (endpoint) return endpoint; } const ajaxPath = /(?:\.post|\.ajax|fetch)\(\s*['"](\/[^'"]+)['"]/gi; while (match = ajaxPath.exec(source)) { const endpoint = endpointFromQuotedPath(match[1] ?? "", source); if (endpoint) return endpoint; } } function collectInlineScriptText(document_) { return [...document_.querySelectorAll("script:not([src])")].map((script) => script.textContent ?? "").join("\n"); } function pageScriptUrls(document_) { const urls = []; const add = (raw) => { if (!raw) return; try { const url = new URL(raw, location.origin); if (url.origin !== location.origin) return; urls.push(url.href); } catch {} }; for (const script of document_.querySelectorAll("script[src]")) add(script.getAttribute("src")); for (const link of document_.querySelectorAll("link[rel=\"preload\"][as=\"script\"], link[as=\"script\"]")) add(link.getAttribute("href")); return [...new Set(urls)].filter((href) => !/jquery|bootstrap|gtag|gtm|recaptcha|cloudflare|analytics|hotjar|sentry/i.test(href)).slice(0, 24); } function endpointFromJquery(document_) { const readEvents = document_.defaultView?.jQuery?._data; if (typeof readEvents !== "function") return; const roots = [...document_.querySelectorAll(".bp-popup__claim-btn"), document_]; if (document_.body) roots.push(document_.body); for (const root of roots) { const found = endpointFromScripts((readEvents(root, "events")?.click ?? []).map((entry) => String(entry.handler ?? "")).join("\n")); if (found) return found; } } async function endpointFromPageScripts(document_) { for (const href of pageScriptUrls(document_)) try { const text = await (await fetch(href)).text(); const handlerAt = text.search(/bp-popup__claim-btn|claim-btn/i); const fromFile = handlerAt >= 0 ? endpointFromScripts(text.slice(Math.max(0, handlerAt - 2e3), handlerAt + 4e3)) ?? endpointFromScripts(text) : endpointFromScripts(text); if (fromFile) return fromFile; } catch {} } async function fetchBattlePassDocument() { try { const response = await fetch("/control-center/battle-pass/1", { headers: { Accept: "text/html" } }); if (!response.ok) return; return new DOMParser().parseFromString(await response.text(), "text/html"); } catch { return; } } function cacheClaimEndpoint(endpoint) { claimEndpointCache.value = endpoint; console.info("[AWA Toolkit] Battle Pass claim POST", endpoint.path, endpoint.hasIdInPath ? "(id in path)" : ""); return endpoint; } async function searchDocumentForClaimEndpoint(document_) { return endpointFromClaimMarkup(document_) ?? endpointFromScripts(collectInlineScriptText(document_)) ?? endpointFromJquery(document_) ?? await endpointFromPageScripts(document_); } async function discoverBattlePassClaimEndpoint(document_) { if (claimEndpointCache.value) return claimEndpointCache.value; if (document_) { const found = await searchDocumentForClaimEndpoint(document_); if (found) return cacheClaimEndpoint(found); } if (!location.pathname.includes("/battle-pass") && !Boolean(document_ && document_ !== document)) { const fetched = await fetchBattlePassDocument(); if (fetched) { const found = await searchDocumentForClaimEndpoint(fetched); if (found) return cacheClaimEndpoint(found); } } } function resolveClaimPath(endpoint, milestoneId) { if (!endpoint.hasIdInPath) return endpoint.path; const trimmed = endpoint.path.replace(/\/$/, ""); if (trimmed.endsWith(`/${milestoneId}`)) return trimmed; return `${trimmed}/${milestoneId}`; } function resolveClaimBody(endpoint, claim) { if (claim.csrfToken) return { _csrf_token: claim.csrfToken }; const body = { ...claim.body }; if (endpoint && body[endpoint.idParameter] === void 0) body[endpoint.idParameter] = jsonishId(claim.milestoneId); return body; } function claimKey(claim) { return claimIdentity(claim); } function claimPostPath(claim, endpoint) { if (claim.claimPath) return claim.claimPath; if (endpoint && claim.milestoneId) return resolveClaimPath(endpoint, claim.milestoneId); } async function claimReadyViaApi(claims, endpoint) { const seen = new Set(); const postedPaths = new Set(); let claimed = 0; let hasPosted = false; for (const claim of uniqueReadyClaims(claims)) { const path = claimPostPath(claim, endpoint); const key = claimKey(claim); if (!path || seen.has(key)) continue; seen.add(key); if (hasPosted) await delay$2(CLAIM_QUEUE_GAP_MS); hasPosted = true; const result = await claimBattlePassReward(path, resolveClaimBody(endpoint, claim)); postedPaths.add(path); if (!result.ok) continue; claimed += 1; } return { claimed, postedPaths }; } function claimsFromLiveButtons(items) { return uniqueReadyClaims(items.map((item) => readyClaimFromButton(item.button, item.popup))); } async function clickRemainingClaimButtons(document_, options = {}) { const attempted = new WeakSet(); const skipPaths = options.skipPaths ?? new Set(); let claimed = 0; let hasClicked = false; while (claimed < 40) { const next = listBattlePassClaimButtons(document_, options).find((item) => { if (!item.button.isConnected || attempted.has(item.button)) return false; const path = claimButtonIdentity(item.button, item.popup); return !path || !skipPaths.has(path); }); if (!next) break; attempted.add(next.button); const { button, popup } = next; const path = claimButtonIdentity(button, popup); if (path) skipPaths.add(path); if (hasClicked) await delay$2(CLAIM_QUEUE_GAP_MS); hasClicked = true; if (button.offsetParent === null) { popup.click(); await delay$2(250); } button.click(); await waitWhile(() => button.isConnected && popup.contains(button), 4e3); if (!button.isConnected || !popup.contains(button)) claimed += 1; } return claimed; } async function waitForBattlePassClaimButtons(document_, options = {}, timeoutMs = 8e3) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const count = listBattlePassClaimButtons(document_, options).length; if (count > 0) return count; await delay$2(250); } return listBattlePassClaimButtons(document_, options).length; } async function claimAllBattlePassRewards(options = {}) { const shouldSkipArpBoosts = options.shouldSkipArpBoosts === true; const isOnBattlePassPage = location.pathname.includes("/battle-pass"); if (isOnBattlePassPage) await waitForBattlePassClaimButtons(document, { shouldSkipArpBoosts }); const liveDocument = isOnBattlePassPage ? document : void 0; let fetchedDocument; let targets; if (isOnBattlePassPage) targets = claimsFromLiveButtons(listBattlePassClaimButtons(document, { shouldSkipArpBoosts })); else { fetchedDocument = await fetchBattlePassDocument(); targets = fetchedDocument ? listReadyClaimsFromDocument(fetchedDocument).filter((claim) => !shouldSkipArpBoosts || !claim.isArp) : []; if (targets.every((claim) => !claim.claimPath)) targets = (options.readyClaims ?? []).filter((claim) => Boolean(claim.claimPath) && (!shouldSkipArpBoosts || !claim.isArp)); } const endpoint = await discoverBattlePassClaimEndpoint(liveDocument ?? fetchedDocument); const posted = targets.length > 0 ? await claimReadyViaApi(targets, endpoint) : { claimed: 0, postedPaths: new Set() }; let claimed = posted.claimed; if (isOnBattlePassPage) { await waitWhile(() => listBattlePassClaimButtons(document, { shouldSkipArpBoosts }).length > 0, 1500); let remaining = listBattlePassClaimButtons(document, { shouldSkipArpBoosts }).length; if (remaining > 0) { claimed += await clickRemainingClaimButtons(document, { shouldSkipArpBoosts, skipPaths: posted.postedPaths }); await waitWhile(() => listBattlePassClaimButtons(document, { shouldSkipArpBoosts }).length > 0, 1500); remaining = listBattlePassClaimButtons(document, { shouldSkipArpBoosts }).length; } return { claimed, remaining }; } const uniqueTargets = new Set(targets.map((claim) => claimIdentity(claim))).size; return { claimed, remaining: Math.max(0, uniqueTargets - claimed) }; } function battlePassClaimableArp(battlePass) { return battlePass?.readyToClaimArp ?? 0; } function battlePassReadyNonArp(battlePass) { const ready = battlePass?.readyToClaim ?? 0; return Math.max(0, ready - battlePassClaimableArp(battlePass)); } function shouldSkipArpInBattlePassClaimAll(battlePass, shouldWaitForAllArpSwap) { return shouldWaitForAllArpSwap && battlePassClaimableArp(battlePass) > 0; } function shouldShowBattlePassClaimAll(battlePass, shouldWaitForAllArpSwap) { if (battlePassReadyNonArp(battlePass) > 0) return true; return (battlePass?.readyToClaim ?? 0) > 0 && !shouldWaitForAllArpSwap; } function battlePassClaimButtonLabel(shouldSkipArpBoosts, options) { if (shouldSkipArpBoosts) return "Claim rewards"; return options?.compact === true ? "Claim all BP" : "Claim all"; } function scrapeBattlePass() { if (!location.pathname.includes("/battle-pass")) return; return scrapeBattlePassFromDocument(document); } function isBattlePassDocumentReady(document_) { return Boolean(document_.querySelector(".bp-popup[data-milestone-id], .bp-popup__claim-btn, .bp-popup__claimed") || /Ready to claim/i.test(document_.body?.textContent ?? "")); } async function waitForBattlePassDocument(timeoutMs = 12e3) { if (isBattlePassDocumentReady(document)) return; await new Promise((resolve) => { let isSettled = false; const observer = new MutationObserver(() => { if (isBattlePassDocumentReady(document)) finish(); }); const timer = setTimeout(finish, timeoutMs); function finish() { if (isSettled) return; isSettled = true; observer.disconnect(); clearTimeout(timer); resolve(); } observer.observe(document.documentElement, { childList: true, subtree: true }); }); } function battlePassClaimSignature(document_) { const { readyToClaim, readyToClaimArp } = countBattlePassClaims(document_); return `${readyToClaim}:${readyToClaimArp}`; } var DAILY_QUEST_STATUS_SELECTORS = [ "[id^=\"control-center__daily-quest-status-\"]", "[id^=\"control-center__daily-quests-status-\"]", "[id^=\"control-center__weekend-quest-status-\"]", "[id^=\"control-center__quest-status-\"]" ]; var HEADER_NAME = /^(incomplete|complete|status|game|quest|quests|reward|arp)$/i; function dailyQuestStatusFromText(text) { const trimmed = text.trim(); if (/^complete$/i.test(trimmed)) return "complete"; if (/^incomplete$/i.test(trimmed)) return "incomplete"; } function dailyQuestKind(name, href) { return /weekend/i.test(`${name} ${href ?? ""}`) ? "weekend" : "daily"; } function pathnameFromHref(href) { if (!href) return; try { return new URL(href, "https://na.alienwarearena.com").pathname; } catch { return href.startsWith("/") ? href : void 0; } } function questNameFromRow(row) { const name = ([...row.querySelectorAll("a")].find((link) => { const href = link.getAttribute("href") ?? ""; return /\/quests\//i.test(href) && !/\/steam\/quests\//i.test(href); })?.textContent ?? row.querySelector("a")?.textContent ?? [...row.querySelectorAll("td")].find((cell) => { const text = cell.textContent?.replaceAll(/\s+/g, " ").trim() ?? ""; return text.length > 0 && !dailyQuestStatusFromText(text); })?.textContent)?.replaceAll(/\s+/g, " ").trim(); if (!name || HEADER_NAME.test(name)) return; return name; } function statusTextFromRow(row) { return [...row.querySelectorAll("td, th, span, div")].find((cell) => dailyQuestStatusFromText(cell.textContent ?? ""))?.textContent?.trim() ?? ""; } function buildDailyQuestRow(row, statusText) { const name = questNameFromRow(row); const status = dailyQuestStatusFromText(statusText); if (!name || !status) return; const href = pathnameFromHref(row.querySelector("a")?.getAttribute("href") ?? void 0); const parsed = { name, status, kind: dailyQuestKind(name, href) }; if (href) parsed.href = href; return parsed; } function parseDailyQuestRowFromStatusCell(statusCell) { const row = statusCell.closest("tr") ?? statusCell.parentElement; if (!row) return; return buildDailyQuestRow(row, statusCell.textContent?.trim() ?? ""); } function parseDailyQuestRowFromTableRow(row) { return buildDailyQuestRow(row, statusTextFromRow(row)); } function scrapeDailyQuestRowsFromDocument(document_) { const card = findActivityCard(document_, /^Daily Quests$/i); if (!card) return []; const fromStatusIds = []; for (const selector of DAILY_QUEST_STATUS_SELECTORS) fromStatusIds.push(...[...card.querySelectorAll(selector)].map((cell) => parseDailyQuestRowFromStatusCell(cell)).filter((row) => row !== void 0)); if (fromStatusIds.length > 0) return fromStatusIds; const tableRows = [...card.querySelectorAll("tr")].map((row) => parseDailyQuestRowFromTableRow(row)).filter((row) => row !== void 0); if (tableRows.length > 0) return tableRows; return [...card.querySelectorAll("li")].map((row) => parseDailyQuestRowFromTableRow(row)).filter((row) => row !== void 0); } function dailyQuestsCapFromRows(quests) { if (quests.length === 0) return; return remainingDailyQuestRowsFromList(quests).length > 0 ? "available" : "capped"; } function remainingDailyQuestRowsFromList(quests) { return quests.filter((quest) => quest.status === "incomplete"); } function remainingDailyQuestRows(siteState) { return remainingDailyQuestRowsFromList(siteState.dailyQuests?.quests ?? []); } function applyDailyQuestsFromDocument(next, document_) { const scraped = scrapeDailyQuestRowsFromDocument(document_); if (scraped.length === 0) return; next.dailyQuests = { scrapedAt: new Date().toISOString(), quests: scraped }; const cap = dailyQuestsCapFromRows(scraped); if (cap) next.caps.dailyQuests = cap; } function readTimeOnSiteCap(body) { const tosBlock = /Time on Site[\s\S]{0,200}?Max ARP per day:\s*(\d+)[\s\S]{0,80}?Earned ARP:\s*(\d+)/i.exec(body); if (!tosBlock?.[1] || !tosBlock[2]) return; const capArp = Number(tosBlock[1]); const earnedArp = Number(tosBlock[2]); if (!Number.isFinite(capArp) || !Number.isFinite(earnedArp)) return; if (earnedArp >= BASE_ACTIVITY.timeOnSiteBasePerDay) return "capped"; return earnedArp >= capArp ? "capped" : "available"; } function parseTwitchArpStatus(document_) { const status = document_.querySelector("#control-center__twitch-arp-status")?.textContent?.trim() ?? ""; const incompleteArp = /^Incomplete:\s*(\d+)\s*ARP/i.exec(status); if (incompleteArp?.[1] !== void 0) return { cap: "available", earnedArp: Number(incompleteArp[1]) }; if (/^Incomplete\b/i.test(status)) return { cap: "available" }; const completeArp = /^Complete:\s*(\d+)\s*ARP/i.exec(status); if (completeArp?.[1] !== void 0) return { cap: "capped", earnedArp: Number(completeArp[1]) }; if (/^Complete\b/i.test(status)) return { cap: "capped" }; return {}; } function readWatchTwitchCapFromDocument(document_) { const fromStatus = parseTwitchArpStatus(document_).cap; if (fromStatus) return fromStatus; const card = findActivityCard(document_, /^Watch Twitch$/i); if (card && /Incomplete/i.test(card.textContent ?? "")) return "available"; const maxReached = document_.querySelector("#control-center__twitch-max-reached"); if (maxReached && !isElementVisiblyHidden(maxReached) && /Max Cap Reached/i.test(maxReached.textContent ?? "")) return "capped"; return readWatchTwitchCap(pageText(document_)); } function readWatchTwitchCap(body) { if (/Watch Twitch[\s\S]{0,400}?Incomplete:\s*\d+\s*ARP/i.test(body)) return "available"; if (/Watch Twitch[\s\S]{0,400}?\bIncomplete\b/i.test(body)) return "available"; if (/Watch Twitch[\s\S]{0,240}Max Cap Reached/i.test(body) && !/twitch-max-reached[^>]*display:\s*none/i.test(body)) return "capped"; if (/Watch Twitch[\s\S]{0,80}\bComplete\b/i.test(body)) return "capped"; } var TWITCH_MS_PER_ARP$2 = 6e4; function parseDailyArpTwitchData(document_) { const scripts = [...document_.querySelectorAll("script:not([src])")].map((script) => script.textContent ?? "").join("\n"); const assignment = /dailyArpData\s*=\s*(\{[\s\S]*?\});/.exec(scripts)?.[1]; if (!assignment) return; let parsed; try { parsed = JSON.parse(assignment); } catch { return; } if (!parsed || typeof parsed !== "object" || !("twitchData" in parsed)) return; const twitch = parsed.twitchData; if (!twitch || typeof twitch !== "object") return; const data = twitch; const totalPoints = Number(data.totalPoints); if (!Number.isFinite(totalPoints)) return; const timeWatched = Number(data.timeWatched); const bonusPoints = Number(data.bonusPoints); return { totalPoints, timeWatched: Number.isFinite(timeWatched) ? timeWatched : 0, bonusPoints: Number.isFinite(bonusPoints) ? bonusPoints : 0, isUnderCap: isTwitchUnderCapFromData(data) }; } function isTwitchUnderCapFromData(data) { if (typeof data.underCap === "boolean") return data.underCap; if (typeof data.isUnderCap === "boolean") return data.isUnderCap; return true; } function parseTwitchDailyCapArp(document_) { const body = pageText(document_); for (const pattern of [ /only earn up to\s+(\d+)\s*ARP from Twitch/i, /Earn up to\s+(\d+)\s*ARP per day by watching participating Twitch/i, /watching Twitch[\s\S]{0,160}?earn up to\s+(\d+)\s*ARP every day/i, /Watch Twitch[\s\S]{0,240}?Max ARP per day:\s*(\d+)/i ]) { const match = pattern.exec(body); if (!match?.[1]) continue; const value = Number(match[1]); if (value > 0) return value; } } function scrapeWatchTwitchProgressFromDocument(document_, previous) { const twitchData = parseDailyArpTwitchData(document_); const capFromPage = parseTwitchDailyCapArp(document_); const status = parseTwitchArpStatus(document_); if (!twitchData && capFromPage === void 0 && status.earnedArp === void 0 && status.cap === void 0) return previous; const capArp = capFromPage ?? previous?.capArp ?? BASE_ACTIVITY.watchTwitchBasePerDay; let isUnderCap; if (status.cap === "capped") isUnderCap = false; else if (status.cap === "available") isUnderCap = true; else if (twitchData) isUnderCap = twitchData.isUnderCap; else isUnderCap = previous?.isUnderCap ?? true; const parsedArp = status.earnedArp ?? twitchData?.totalPoints ?? previous?.baseArp ?? 0; const baseArp = isUnderCap ? parsedArp : Math.max(parsedArp, capArp); const remainingArp = isUnderCap ? Math.max(0, capArp - baseArp) : 0; return { scrapedAt: new Date().toISOString(), baseArp, bonusArp: twitchData?.bonusPoints ?? previous?.bonusArp ?? 0, timeWatched: twitchData?.timeWatched ?? previous?.timeWatched ?? 0, isUnderCap, capArp, remainingMs: remainingArp * TWITCH_MS_PER_ARP$2 }; } function twitchWatchRemainingMs(state, twitchFlat = 0, now = new Date()) { const progress = state?.watchTwitch; const baseCap = progress?.capArp ?? BASE_ACTIVITY.watchTwitchBasePerDay; const isFreshProgress = progress !== void 0 && utcDateString(new Date(progress.scrapedAt)) === utcDateString(now); if (state?.caps.watchTwitch === "capped" || isFreshProgress && progress && !progress.isUnderCap) return 0; const earned = isFreshProgress && progress ? progress.baseArp : 0; return Math.max(0, baseCap + twitchFlat - earned) * TWITCH_MS_PER_ARP$2; } function readQuestStatusesFromCard(card) { const statuses = [...card.querySelectorAll("td, th, span, div, li")].map((element) => element.textContent?.trim() ?? "").filter((text) => /^(Incomplete|Complete)$/i.test(text)); if (statuses.some((status) => /^Incomplete$/i.test(status))) return "available"; if (statuses.some((status) => /^Complete$/i.test(status))) return "capped"; const text = card.textContent ?? ""; if (/Incomplete/i.test(text)) return "available"; if (/\bComplete\b/i.test(text)) return "capped"; } function readSteamQuestsCap(body) { const steamSection = /Steam Quests([\s\S]{0,8000}?)(?=Watch Twitch|Discord Poll|Battle Pass|Time on Site|$)/i.exec(body); if (!steamSection?.[1]) return; const section = steamSection[1]; if (/Incomplete/i.test(section)) return "available"; if (/\bComplete\b/i.test(section)) return "capped"; } function readCapFromCardOrText(document_, cardTitle, textFallback) { const card = findActivityCard(document_, cardTitle); if (card) return readQuestStatusesFromCard(card) ?? textFallback(pageText(document_)); return textFallback(pageText(document_)); } function readSteamQuestsCapFromDocument(document_) { const fromRows = steamQuestsCapFromRows(scrapeSteamQuestRowsFromDocument(document_)); if (fromRows) return fromRows; return readCapFromCardOrText(document_, /^Steam Quests$/i, readSteamQuestsCap); } function readDailyQuestsCap(body) { const section = /Daily Quests([\s\S]{0,1200}?)(?=Steam Quests|Watch Twitch|OLD SCHOOL|Community Event|$)/i.exec(body); if (!section?.[1]) return; if (/Incomplete/i.test(section[1])) return "available"; if (/\bComplete\b/i.test(section[1])) return "capped"; } function readDailyQuestsCapFromDocument(document_) { const fromRows = dailyQuestsCapFromRows(scrapeDailyQuestRowsFromDocument(document_)); if (fromRows) return fromRows; return readCapFromCardOrText(document_, /^Daily Quests$/i, readDailyQuestsCap); } function readDailyCalendarCap(body) { if (/Daily Login Calendar[\s\S]{0,120}Claimed/i.test(body)) return "capped"; if (/Daily Login Calendar[\s\S]{0,120}\bClaim\b/i.test(body)) return "available"; if (!/Today'?s Reward|28-Day Daily Login Rewards/i.test(body)) return; if (/Today'?s Reward[\s\S]{0,240}Claimed/i.test(body)) return "capped"; if (/Today'?s Reward[\s\S]{0,240}\bClaim\b/i.test(body)) return "available"; return "capped"; } function readDailyCalendarCapFromDocument(document_) { const fromText = readDailyCalendarCap(pageText(document_)); if (fromText) return fromText; const card = findActivityCard(document_, /^(Today'?s Reward|28-Day Daily Login Rewards|Daily Login)/i); if (!card) return; const claimControl = [...card.querySelectorAll("button, a")].find((element) => /^claim$/i.test(element.textContent?.trim() ?? "")); if (!claimControl) return "capped"; if (claimControl instanceof HTMLButtonElement && claimControl.disabled) return "capped"; if (claimControl.getAttribute("aria-disabled") === "true") return "capped"; return "available"; } function hasVotedCurrentDiscordPoll(arpLog, now = new Date()) { if (!arpLog || arpLog.recent.length === 0) return false; const pollStartDate = utcDateString(lastDiscordPollPostAt(now)); return arpLog.recent.some((entry) => /Discord Poll/i.test(entry.action) && entry.date !== void 0 && entry.date >= pollStartDate); } function applyArpLogActivityCaps(caps, arpLog, now = new Date()) { if (!arpLog || arpLog.recent.length === 0) return caps; const today = utcDateString(now); const next = { ...caps }; if (arpLog.recent.filter((entry) => entry.date === today).some((entry) => /Daily Login Calendar/i.test(entry.action))) next.dailyCalendar = "capped"; next.discordPoll = hasVotedCurrentDiscordPoll(arpLog, now) ? "capped" : "available"; return next; } function scrapeControlCenterCapsFromDocument(document_) { const body = pageText(document_); const caps = {}; const timeOnSite = readTimeOnSiteCap(body); if (timeOnSite) caps.timeOnSite = timeOnSite; const watchTwitch = readWatchTwitchCapFromDocument(document_); if (watchTwitch) caps.watchTwitch = watchTwitch; const steamQuests = readSteamQuestsCapFromDocument(document_); if (steamQuests) caps.steamQuests = steamQuests; const dailyCalendar = readDailyCalendarCapFromDocument(document_); if (dailyCalendar) caps.dailyCalendar = dailyCalendar; const dailyQuests = readDailyQuestsCapFromDocument(document_); if (dailyQuests) caps.dailyQuests = dailyQuests; caps.steamCommunityEvent = scrapeLiveCommunityEventBanner(document_) ? "available" : "capped"; return caps; } function scrapeControlCenterCaps() { return scrapeControlCenterCapsFromDocument(document); } var CONTROL_CENTER_WIDGET = "[id^=\"control-center__\"], a.community-event-banner"; function isControlCenterDocumentReady(document_) { return Boolean(document_.querySelector(CONTROL_CENTER_WIDGET)); } function isControlCenterTwitchDataReady(document_) { if (document_.querySelector("#control-center__twitch-arp-status")?.textContent?.trim()) return true; if (document_ !== document) return parseDailyArpTwitchData(document_) !== void 0; return false; } function isControlCenterActivityReady(document_) { return isControlCenterDocumentReady(document_) && isControlCenterTwitchDataReady(document_); } function controlCenterActivitySignature(document_) { const caps = scrapeControlCenterCapsFromDocument(document_); const twitch = scrapeWatchTwitchProgressFromDocument(document_); return [ caps.watchTwitch, caps.steamQuests, caps.timeOnSite, caps.dailyCalendar, caps.dailyQuests, twitch?.baseArp, twitch?.isUnderCap, twitch?.timeWatched, twitch?.bonusArp ].join(":"); } async function waitForControlCenterDocument(timeoutMs = 12e3) { if (isControlCenterActivityReady(document)) return; await new Promise((resolve) => { let isSettled = false; const observer = new MutationObserver(() => { if (isControlCenterActivityReady(document)) finish(); }); const timer = setTimeout(finish, timeoutMs); function finish() { if (isSettled) return; isSettled = true; observer.disconnect(); clearTimeout(timer); resolve(); } observer.observe(document.documentElement, { childList: true, subtree: true }); }); } function isListPriceVaultClaim(game) { return game.isAuction !== true; } function isVaultTierMet(game, userTier) { if (userTier === void 0 || game.minTier === void 0) return true; return userTier >= game.minTier; } function vaultPayArp(price, discountPct = 0) { return Math.ceil(price * (1 - Math.min(1, Math.max(0, discountPct))) - 1e-9); } function vaultGamePayArp(game, discountPct = 0) { return vaultPayArp(game.price, discountPct); } function canAffordVaultPrice(redeemableArp, payArp) { if (redeemableArp === void 0) return true; return redeemableArp >= payArp; } function isPostedListPriceVaultGame(game) { return game.inStock && isListPriceVaultClaim(game); } function isAffordableVaultOffer(game, state, discountPct = 0, availableArp = state.arpLog?.redeemableArp) { if (!isPostedListPriceVaultGame(game)) return false; if (!isVaultTierMet(game, state.userArpTier)) return false; return canAffordVaultPrice(availableArp, vaultGamePayArp(game, discountPct)); } function hasPostedListPriceVaultGames(state) { return state.gameVault.some((game) => isPostedListPriceVaultGame(game)); } function canAffordAnyVaultOffer(state, discountPct = 0, availableArp = state.arpLog?.redeemableArp) { return state.gameVault.some((game) => isAffordableVaultOffer(game, state, discountPct, availableArp)); } function isClaimableVaultGame(game, state, discountPct = 0) { return game.purchasable === true && isAffordableVaultOffer(game, state, discountPct); } function isVaultStockForUser(game, userTier) { return game.purchasable === true && isListPriceVaultClaim(game) && isVaultTierMet(game, userTier); } function isGameVaultStockOpen(state) { return state.gameVault.some((game) => isVaultStockForUser(game, state.userArpTier)); } function isGameVaultCurrentlyOpen(state, discountPct = 0) { return state.gameVault.some((game) => isClaimableVaultGame(game, state, discountPct)); } var GAME_VAULT_EQUIP_BUFFER_MS = 18e5; function gameVaultCycleId(state) { if (state.gameVaultOpensAt) return state.gameVaultOpensAt; if (isGameVaultStockOpen(state)) return "open"; } function gameVaultOpensAtMs(state) { const opensAt = parseTimestamp$1(state.gameVaultOpensAt); return Number.isFinite(opensAt) ? opensAt : void 0; } function willMissDiscountEquipBeforeOpen(lockUntilMs, state, now = Date.now()) { const opensAt = gameVaultOpensAtMs(state); if (opensAt === void 0 || opensAt <= now) return false; return lockUntilMs + GAME_VAULT_EQUIP_BUFFER_MS > opensAt; } function gameVaultCatalogPrice(state, discountPct = 0) { return state.gameVault.find((game) => isClaimableVaultGame(game, state, discountPct))?.price ?? 0; } function scrapeGameVaultTimerMsFromDocument(document_) { const timer = document_.querySelector("#game-vault-timer"); const ms = parseTimestamp$1((timer?.dataset.unlockDate ?? timer?.dataset.endDate ?? timer?.dataset.lockDate ?? timer?.dataset.closeDate)?.trim()); return Number.isFinite(ms) ? ms : void 0; } function scrapeGameVaultFromDocument(document_) { const items = document_.querySelectorAll([".gamevault-marketplace-product[data-product-price]", ".marketplace-game-product[data-product-price]"].join(", ")); const result = []; const seen = new Set(); for (const item of items) { const priceRaw = item.dataset.productPrice; if (priceRaw === void 0) continue; const price = Number(priceRaw); if (Number.isNaN(price) || price <= 0) continue; const id = item.dataset.productId ?? `${price}:${item.dataset.productName ?? ""}`; if (seen.has(id)) continue; seen.add(id); const isAuction = item.dataset.isBlindAuction === "true" || item.classList.contains("auction-game"); const isInStock = item.dataset.productInStock !== "false"; const isDisabled = item.dataset.productDisabled === "true"; const minTierRaw = item.dataset.arpTier; const minTier = minTierRaw === void 0 ? void 0 : Number(minTierRaw); const nextItem = { name: item.dataset.productName?.trim() || item.querySelector(".product-name, .gv-product-name, h3, h4")?.textContent?.trim() || item.getAttribute("title") || "Game Vault item", price, inStock: isInStock && !isAuction, purchasable: isInStock && !isDisabled && !isAuction, isAuction }; if (minTier !== void 0 && Number.isFinite(minTier)) nextItem.minTier = minTier; result.push(nextItem); } return result; } function scrapeUserArpTierFromDocument(document_) { return readPageArpTier(document_); } function applyGameVaultSchedule(next, timerMs, isOpen, now) { if (isOpen) { const existingOpen = parseTimestamp$1(next.gameVaultOpensAt); if (!Number.isFinite(existingOpen) || existingOpen > now) next.gameVaultOpensAt = new Date(now).toISOString(); return; } if (timerMs !== void 0 && timerMs > now) { next.gameVaultOpensAt = new Date(timerMs).toISOString(); return; } delete next.gameVaultOpensAt; } function applyGameVaultDocument(next, document_) { const tier = scrapeUserArpTierFromDocument(document_); if (tier !== void 0) next.userArpTier = tier; applyRedeemableArpFromDocument(next, document_); const vault = scrapeGameVaultFromDocument(document_); const timerMs = scrapeGameVaultTimerMsFromDocument(document_); if (timerMs === void 0 && vault.length === 0) return; if (vault.length > 0) next.gameVault = vault; applyGameVaultSchedule(next, timerMs, vault.some((game) => isVaultStockForUser(game, next.userArpTier)), Date.now()); } var SITE_STATE_KEY = "artifactSiteState"; var DEFAULT_CAPS = { timeOnSite: "unknown", steamQuests: "unknown", watchTwitch: "unknown", dailyCalendar: "unknown", discordPoll: "unknown", dailyQuests: "unknown", steamCommunityEvent: "unknown" }; function normalizeCaps(raw) { const caps = raw ?? {}; return { ...DEFAULT_CAPS, ...caps, dailyQuests: caps.dailyQuests ?? caps.communityEvent ?? "unknown", steamCommunityEvent: caps.steamCommunityEvent ?? "unknown" }; } function isSiteState(value) { return typeof value === "object" && !!value && "caps" in value; } async function loadSiteState() { const raw = await _GM.getValue(SITE_STATE_KEY); if (!raw) return; try { const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; if (!isSiteState(parsed)) return; return { ...parsed, caps: normalizeCaps(parsed.caps) }; } catch { return; } } async function saveSiteState(state) { await _GM.setValue(SITE_STATE_KEY, JSON.stringify(state)); } function applyWatchTwitchFromDocument(next, document_) { const progress = scrapeWatchTwitchProgressFromDocument(document_, next.watchTwitch); if (progress) next.watchTwitch = progress; } function applyControlCenterPage(next) { if (!isControlCenterDocumentReady(document)) return; Object.assign(next.caps, scrapeControlCenterCaps()); applySteamQuestsFromDocument(next, document); applyDailyQuestsFromDocument(next, document); applyWatchTwitchFromDocument(next, document); applyBattlePassEndFromDocument(next, document); const banner = scrapeLiveCommunityEventBanner(document); if (banner) { next.caps.steamCommunityEvent = "available"; if (next.communityEvent && !next.communityEvent.isLive) next.communityEvent = { ...next.communityEvent, isLive: true, url: banner.url, ...banner.title && next.communityEvent.title === void 0 && { title: banner.title } }; return; } if (!next.communityEvent?.isLive) next.caps.steamCommunityEvent = "capped"; } function applyCommunityEventPage(next) { const event = mergeCommunityEventScrape(scrapeCommunityEventFromDocument(document, location.pathname), next.communityEvent, { source: "visit" }); next.communityEvent = event; next.caps.steamCommunityEvent = event.isLive ? "available" : "capped"; } function applyLiveDocumentToSiteState(next) { const path = location.pathname; const userArpTier = scrapeUserArpTierFromDocument(document); if (userArpTier !== void 0) next.userArpTier = userArpTier; applyRedeemableArpFromDocument(next, document); if (path.includes("/control-center") && !path.includes("/battle-pass")) applyControlCenterPage(next); if (path.includes("/steam/questsetup") || path.includes("/rewards/terms") || path.includes("/faq-contact")) applyWatchTwitchFromDocument(next, document); if (path.includes("/marketplace") || path.includes("/game-vault")) applyGameVaultDocument(next, document); if (path.includes("/battle-pass")) { const battlePass = scrapeBattlePass(); if (battlePass) next.battlePass = mergeBattlePassScrape(battlePass, next.battlePass); } if (path.includes("/arp-log") && isArpLogDocumentReady(document)) next.arpLog = mergeArpLogScrape(scrapeArpLogFromDocument(document), next.arpLog); if (path.includes("/steam/community-event")) applyCommunityEventPage(next); if (/\/steam\/quests\/.+/.test(path)) applySteamQuestDetailFromDocument(next, document, path); next.caps = applyArpLogActivityCaps(next.caps, next.arpLog); if (next.communityEvent) next.communityEvent = reconcileCommunityEventWithArpLog(next.communityEvent, next.arpLog); } async function refreshSiteStateFromPage() { const previous = await loadSiteState() ?? { updatedAt: new Date().toISOString(), caps: { ...DEFAULT_CAPS }, gameVault: [] }; if (location.pathname.includes("/arp-log")) await waitForArpLogDocument(); if (location.pathname.includes("/control-center") && !location.pathname.includes("/battle-pass")) await waitForControlCenterDocument(); const next = { ...previous, updatedAt: new Date().toISOString(), caps: { ...previous.caps } }; applyLiveDocumentToSiteState(next); await saveSiteState(next); return next; } function watchLiveSiteStatePage(options) { if (!options.isPage) return; if (document.documentElement.dataset[options.datasetFlag] === "1") return; document.documentElement.dataset[options.datasetFlag] = "1"; let debounceTimer; let lastSignature = ""; let isPersisting = false; let isPendingAfterPersist = false; const persistIfChanged = async () => { if (!options.isReady(document)) return; const signature = options.signature(document); if (signature === lastSignature) return; if (isPersisting) { isPendingAfterPersist = true; return; } isPersisting = true; try { const state = await refreshSiteStateFromPage(); lastSignature = signature; await options.onPersist?.(state); } finally { isPersisting = false; if (isPendingAfterPersist) { isPendingAfterPersist = false; persistIfChanged(); } } }; const schedule = () => { if (debounceTimer !== void 0) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { debounceTimer = void 0; persistIfChanged(); }, 250); }; (async () => { await options.waitForReady(); await persistIfChanged(); new MutationObserver(schedule).observe(document.documentElement, { childList: true, subtree: true, characterData: true }); if (!options.clickSelector) return; const clickSelector = options.clickSelector; document.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Element)) return; if (target.closest(clickSelector)) schedule(); }, { capture: true }); })(); } function watchBattlePassPage(onPersist) { watchLiveSiteStatePage({ isPage: location.pathname.includes("/battle-pass"), datasetFlag: "aoBpWatch", isReady: isBattlePassDocumentReady, signature: battlePassClaimSignature, waitForReady: waitForBattlePassDocument, ...onPersist && { onPersist }, clickSelector: ".bp-popup__claim-btn" }); } function watchControlCenterPage(onPersist) { watchLiveSiteStatePage({ isPage: location.pathname.includes("/control-center") && !location.pathname.includes("/battle-pass"), datasetFlag: "aoCcWatch", isReady: isControlCenterActivityReady, signature: controlCenterActivitySignature, waitForReady: waitForControlCenterDocument, ...onPersist && { onPersist } }); } function watchArpLogPage(onPersist) { watchLiveSiteStatePage({ isPage: location.pathname.includes("/arp-log"), datasetFlag: "aoArpWatch", isReady: isArpLogDocumentReady, signature: arpLogSignature, waitForReady: waitForArpLogDocument, ...onPersist && { onPersist } }); } async function applySteamFreeToPlayResolution(next) { await resolveSiteStateSteamFreeToPlay(next); const cap = steamQuestsCapFromRows(next.steamQuests?.quests ?? []); if (cap) next.caps.steamQuests = cap; } function emptySiteState() { return { updatedAt: new Date(0).toISOString(), caps: { ...DEFAULT_CAPS }, gameVault: [] }; } function isActivityAvailable(caps, key) { return caps[key] !== "capped"; } function isActivityPending(caps, key) { const status = caps[key]; if (status === "available") return true; if (status === "capped") return false; return [ "steamQuests", "dailyQuests", "steamCommunityEvent" ].includes(key); } function combinations(items, k) { if (k === 0) return [[]]; if (items.length < k) return []; const [first, ...rest] = items; if (first === void 0) return []; const withFirst = combinations(rest, k - 1).map((c) => [first, ...c]); const withoutFirst = combinations(rest, k); return [...withFirst, ...withoutFirst]; } function msUntilNextUtcMidnight(now = Date.now()) { const date = new Date(now); return Math.max(0, Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1) - now); } function msUntilNextSteamQuestWeek(now = Date.now()) { const date = new Date(now); const day = date.getUTCDay(); const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7; return Math.max(0, Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + daysUntilMonday) - now); } function isResetInWearWindow(delayMs, waitMs = 0, horizonMs = COOLDOWN_MS) { return delayMs > waitMs && delayMs <= waitMs + horizonMs; } var MS_PER_DAY$1 = 864e5; var UTC_DAILY_END_BUFFER_MS = 36e5; function wearWindowOverlapMs(availableFromMs, availableUntilMs, waitMs = 0, horizonMs = COOLDOWN_MS) { return Math.max(0, Math.min(availableUntilMs, waitMs + horizonMs) - Math.max(availableFromMs, waitMs)); } function canCompleteInWearWindow(availableFromMs, availableUntilMs, waitMs, durationMs, horizonMs = COOLDOWN_MS) { const overlap = wearWindowOverlapMs(availableFromMs, availableUntilMs, waitMs, horizonMs); if (durationMs <= 0) return overlap > 0; return overlap >= durationMs; } function canCompleteOutsideWearWindow(availableFromMs, availableUntilMs, waitMs, durationMs, horizonMs = COOLDOWN_MS, deadlineBufferMs = UTC_DAILY_END_BUFFER_MS) { const deadline = availableUntilMs - deadlineBufferMs; const lockStartMs = waitMs === 0 && availableFromMs <= 0 ? availableFromMs + durationMs : waitMs; if (Math.min(deadline, lockStartMs) - availableFromMs >= durationMs) return true; return deadline - Math.max(availableFromMs, waitMs + horizonMs) >= durationMs; } function completableUtcDayStarts(waitMs, durationMs, options) { const midnight = msUntilNextUtcMidnight(options.now ?? Date.now()); const horizonMs = options.horizonMs ?? 864e5; const starts = []; if (options.todayAvailable && canCompleteInWearWindow(0, midnight, waitMs, durationMs, horizonMs)) starts.push(0); for (let day = 0; day < 3; day += 1) { const dayStart = midnight + day * MS_PER_DAY$1; if (dayStart >= waitMs + horizonMs) break; if (canCompleteInWearWindow(dayStart, dayStart + MS_PER_DAY$1, waitMs, durationMs, horizonMs)) starts.push(dayStart); } return starts; } function isWeeklyForcedIntoLock(weekendMs, waitMs, horizonMs = COOLDOWN_MS) { return waitMs + horizonMs >= weekendMs; } function comboEquipWaitMs(combo, owned, settings, slotLocks) { if (isSameLoadout$1(combo, currentLoadout(owned))) return 0; const comboIds = new Set(combo.map((artifact) => artifact.instanceId)); let waitMs = 0; for (const position of [ 1, 2, 3 ]) { const equipped = owned.find((artifact) => artifact.equippedPosition === position); if (equipped && comboIds.has(equipped.instanceId)) continue; waitMs = Math.max(waitMs, showroomCooldownRemainingMs(settings, position, { ...slotLocks && { slotLocks }, ...typeof equipped?.slotLocked === "boolean" && { equippedSlotLocked: equipped.slotLocked } })); } return waitMs; } function pinHorizonMs(siteState, now = Date.now()) { const untilReset = msUntilNextUtcMidnight(now); const event = siteState.communityEvent; if (!event?.isLive || !canEarnCommunityEventArp(event)) return untilReset; if (breakDownCommunityEventPending(event).waitingCommunityArp <= 0) return untilReset; const eta = estimateNextCommunityUnlock(event, now); if (eta === void 0 || eta.etaMs > 864e5) return untilReset; return Math.min(untilReset, eta.etaMs); } function pinnedEquippedArtifacts(owned, settings, siteState, slotLocks) { const horizonMs = pinHorizonMs(siteState); return owned.filter((artifact) => { if (artifact.equippedPosition === void 0) return false; if (!isShowroomSlotLocked(artifact.equippedPosition, { ...slotLocks && { slotLocks }, ...typeof artifact.slotLocked === "boolean" && { equippedSlotLocked: artifact.slotLocked } })) return false; const remaining = showroomCooldownRemainingMs(settings, artifact.equippedPosition, { ...slotLocks && { slotLocks }, ...typeof artifact.slotLocked === "boolean" && { equippedSlotLocked: artifact.slotLocked } }); if (remaining > 0) return remaining >= horizonMs; return true; }); } function combinationsWithPinned(owned, size, pinned) { if (pinned.length >= size) return [pinned.slice(0, size)]; const pinnedIds = new Set(pinned.map((artifact) => artifact.instanceId)); return combinations(owned.filter((artifact) => !pinnedIds.has(artifact.instanceId)), size - pinned.length).map((extra) => [...pinned, ...extra]); } function activeSets(familyIds) { return ARTIFACT_SETS.filter((set) => !set.unconfirmed && set.memberIds.every((id) => familyIds.includes(id))); } function resolveOwnedList(context) { const { snapshot, settings } = context; if (settings.preferScraped && snapshot.artifacts.length > 0) return snapshot.artifacts; if (settings.manualArtifacts.length > 0) return settings.manualArtifacts.map((manual, index) => { const family = getArtifactById(manual.familyId); const owned = { instanceId: manual.instanceId ?? -(index + 1), familyId: manual.familyId, displayName: family ? displayNameFor(family, manual.tier) : manual.familyId, tier: manual.tier, category: family?.category ?? "Weapon", maxLevel: manual.tier >= ArtifactTier.Interstellar, perkDescription: "" }; const upgradeCost = fragmentCostToUpgradeFrom(manual.tier); if (upgradeCost !== void 0) owned.upgradeCost = upgradeCost; if (manual.equippedPosition !== void 0) owned.equippedPosition = manual.equippedPosition; return owned; }); return snapshot.artifacts; } function currentLoadout(owned) { return owned.filter((artifact) => artifact.equippedPosition !== void 0).toSorted((left, right) => (left.equippedPosition ?? 0) - (right.equippedPosition ?? 0)); } function isSameLoadout$1(left, right) { if (left.length !== right.length) return false; const rightIds = new Set(right.map((artifact) => artifact.instanceId)); return left.every((artifact) => rightIds.has(artifact.instanceId)); } function buildContext(snapshot, settings, siteState) { return { snapshot, settings, siteState: siteState ?? emptySiteState() }; } function emptyBonuses() { return { steamQuests: 0, watchTwitch: 0, dailyCalendar: 0, timeOnSite: 0, discordPoll: 0, marketDiscountPct: 0, allArpPct: 0, communityPlaytimePct: 0 }; } function applyEffect(bonuses, type, value) { switch (type) { case ArtifactEffectType.SteamQuests: bonuses.steamQuests += value; break; case ArtifactEffectType.WatchTwitch: bonuses.watchTwitch += value; break; case ArtifactEffectType.DailyCalendar: bonuses.dailyCalendar += value; break; case ArtifactEffectType.TimeOnSite: bonuses.timeOnSite += value; break; case ArtifactEffectType.DiscordPoll: bonuses.discordPoll += value; break; case ArtifactEffectType.MarketDiscountPct: bonuses.marketDiscountPct += Math.abs(value); break; case ArtifactEffectType.AllArpPct: bonuses.allArpPct += value; break; case ArtifactEffectType.CommunityPlaytimePct: bonuses.communityPlaytimePct += value; } } function applySetBonuses(bonuses, familyIds) { for (const set of activeSets(familyIds)) { const arpEffects = set.effects.filter((effect) => effect.unit !== "cosmetic"); for (const effect of arpEffects) applyEffect(bonuses, effect.type, effect.value); } } function collectBonuses(owned) { const bonuses = emptyBonuses(); for (const item of owned) { const family = getArtifactById(item.familyId); if (!family) continue; applyEffect(bonuses, family.effectType, getNumericEffect(family, item.tier)); } applySetBonuses(bonuses, owned.map((artifact) => artifact.familyId)); return bonuses; } function activityStatsForArtifacts(artifacts) { const bonuses = collectBonuses(artifacts); return { allArpPct: bonuses.allArpPct, steamQuestsFlat: bonuses.steamQuests, watchTwitchFlat: bonuses.watchTwitch, dailyCalendarFlat: bonuses.dailyCalendar, discordPollFlat: bonuses.discordPoll, timeOnSiteFlat: bonuses.timeOnSite }; } function setBreakdownParts(breakdown, key, base, categoryBonus = 0) { const value = base + categoryBonus; if (value === 0) return 0; breakdown[key] = { base, categoryBonus }; return value; } function addDailyCategory(breakdown, key, base, flatBonus, days, frequency) { return setBreakdownParts(breakdown, key, base * days * frequency, flatBonus * days * frequency); } function isAllArpLockWorthBattlePassBoost(best, allArp, readyBoosts) { if (!best || !allArp || allArp.allArpPct <= 0 || readyBoosts <= 0) return false; const extraOnBoost = readyBoosts * 40 * (allArp.allArpPct - best.allArpPct); if (extraOnBoost <= 0) return false; return extraOnBoost > best.weeklyArp - allArp.weeklyArp; } function scoreSteamQuestBases(breakdown, bonuses, freq, bases) { if (bases.length === 0) return 0; return setBreakdownParts(breakdown, "steamQuests", bases.reduce((sum, base) => sum + base, 0) * freq, bonuses.steamQuests * bases.length * freq); } function scoreDailyQuests(breakdown, freq, dayStartsMs, now = Date.now()) { if (dayStartsMs.length === 0) return 0; const B = BASE_ACTIVITY; let dailyBase = 0; let weekendBase = 0; for (const startMs of dayStartsMs) { const onDay = new Date(now + startMs); dailyBase += B.dailyQuestBase * freq; if (onDay.getUTCDay() === 0 || onDay.getUTCDay() === 6) weekendBase += B.weekendQuestBase * freq; } let flatSum = setBreakdownParts(breakdown, "dailyQuests", dailyBase); if (weekendBase > 0) flatSum += setBreakdownParts(breakdown, "weekendQuests", weekendBase); return flatSum; } function scoreSecondaryActivities(breakdown, bonuses, context, isEnabled, freq, waitMs) { const { siteState } = context; const caps = siteState.caps; const B = BASE_ACTIVITY; let flatSum = 0; if (isEnabled("discordPoll") && isActivityPending(caps, "discordPoll")) { const polls = B.discordPollsWhenPending * freq("discordPoll"); flatSum += setBreakdownParts(breakdown, "discordPoll", B.discordPollBase * polls, bonuses.discordPoll * polls); } if (isEnabled("dailyQuests")) { const questDays = completableUtcDayStarts(waitMs, 0, { todayAvailable: isActivityPending(caps, "dailyQuests") }); flatSum += scoreDailyQuests(breakdown, freq("dailyQuests"), questDays); } if (isEnabled("steamCommunityEvent")) { const eventArp = communityEventArpInSwapWindow(siteState, waitMs); if (eventArp > 0) flatSum += setBreakdownParts(breakdown, "steamCommunityEvent", eventArp * freq("steamCommunityEvent")); } const readyClaims = battlePassClaimableArp(siteState.battlePass); if (readyClaims > 0 && !shouldDeferBattlePassForContext(context)) { if (!hasAllArpEffect(currentLoadout(resolveOwnedList(context))) || bonuses.allArpPct > 0) flatSum += setBreakdownParts(breakdown, "battlePassClaims", readyClaims * 40); } return flatSum; } function communityEventArpInSwapWindow(siteState, waitMs = 0) { const event = siteState.communityEvent; if (!event?.isLive || !canEarnCommunityEventArp(event)) return 0; let arp = breakDownCommunityEventPending(event).waitingPersonalArp; for (const milestone of event.milestones) { if (milestone.isAwarded || milestone.arpReward <= 0 || !isPersonalHoursMet(milestone, event.personalHours) || isCommunityGateMet(milestone, event.communityHours)) continue; const target = milestone.communityHoursRequired; if (target === void 0) continue; const eta = estimateCommunityUnlockAt(event, target); if (eta !== void 0 && eta.etaMs >= waitMs && eta.etaMs <= waitMs + 864e5) arp += milestone.arpReward; } return arp; } var TWITCH_MS_PER_ARP$1 = 6e4; var TIME_ON_SITE_DURATION_MS$2 = BASE_ACTIVITY.timeOnSiteBasePerDay * 6e4; function twitchArpInWearWindow(siteState, twitchFlat, waitMs) { const midnight = msUntilNextUtcMidnight(); const todayRemaining = twitchWatchRemainingMs(siteState, twitchFlat) / 6e4; const fullDay = (siteState.watchTwitch?.capArp ?? BASE_ACTIVITY.watchTwitchBasePerDay) + twitchFlat; let twitchArp = 0; if (todayRemaining > 0 && canCompleteInWearWindow(0, midnight, waitMs, todayRemaining * TWITCH_MS_PER_ARP$1)) twitchArp += todayRemaining; const laterDays = completableUtcDayStarts(waitMs, fullDay * TWITCH_MS_PER_ARP$1, { todayAvailable: false }); for (const dayStart of laterDays) if (dayStart > 0) twitchArp += fullDay; return twitchArp; } function steamBasesInWearWindow(siteState, waitMs) { const mondayResetMs = msUntilNextSteamQuestWeek(); const steamBases = []; const remaining = scrapedRemainingSteamQuestRewards(siteState); if (remaining && remaining.length > 0 && isActivityPending(siteState.caps, "steamQuests") && isWeeklyForcedIntoLock(mondayResetMs, waitMs)) steamBases.push(...remaining); if (isResetInWearWindow(mondayResetMs, waitMs)) steamBases.push(...BASE_ACTIVITY.steamQuestBases); return steamBases; } function scoreWindowActivities(bonuses, context, waitMs) { const { settings, siteState } = context; const acts = settings.activities; const caps = siteState.caps; const B = BASE_ACTIVITY; const breakdown = {}; let flatSum = 0; const isEnabled = (key) => (acts[key]?.enabled ?? false) && (acts[key]?.frequency ?? 0) > 0; const freq = (key) => isEnabled(key) ? acts[key]?.frequency ?? 0 : 0; if (isEnabled("timeOnSite")) { const tosDays = completableUtcDayStarts(waitMs, TIME_ON_SITE_DURATION_MS$2, { todayAvailable: isActivityAvailable(caps, "timeOnSite") }); if (tosDays.length > 0) flatSum += addDailyCategory(breakdown, "timeOnSite", B.timeOnSiteBasePerDay, bonuses.timeOnSite, tosDays.length, freq("timeOnSite")); } if (isEnabled("watchTwitch")) { const twitchArp = twitchArpInWearWindow(siteState, bonuses.watchTwitch, waitMs); if (twitchArp > 0) flatSum += setBreakdownParts(breakdown, "watchTwitch", twitchArp); } if (isEnabled("steamQuests")) { const steamBases = steamBasesInWearWindow(siteState, waitMs); if (steamBases.length > 0) flatSum += scoreSteamQuestBases(breakdown, bonuses, freq("steamQuests"), steamBases); } if (isEnabled("dailyCalendar")) { const calendarDays = completableUtcDayStarts(waitMs, 0, { todayAvailable: false }); if (calendarDays.length > 0) flatSum += addDailyCategory(breakdown, "dailyCalendar", B.dailyCalendarBasePerDay, bonuses.dailyCalendar, calendarDays.length, freq("dailyCalendar")); } flatSum += scoreSecondaryActivities(breakdown, bonuses, context, isEnabled, freq, waitMs); return { flatSum, breakdown }; } function comboMarketDiscountPct(combo) { return combo?.marketDiscountPct ?? 0; } function projectedRedeemableArp(context, ...windows) { const current = context.siteState.arpLog?.redeemableArp; if (current === void 0) return; return current + Math.max(0, ...windows.map((combo) => combo?.weeklyArp ?? 0)); } function vaultListPrice(context, discountPct = 0) { if (context.settings.pendingVaultPurchaseArp > 0) return context.settings.pendingVaultPurchaseArp; return gameVaultCatalogPrice(context.siteState, discountPct); } function vaultPurchasePriceNow(context, discountPct = 0) { if (!isGameVaultCurrentlyOpen(context.siteState, discountPct)) return 0; const price = vaultListPrice(context, discountPct); if (price <= 0) return 0; if (!canAffordVaultPrice(context.siteState.arpLog?.redeemableArp, vaultPayArp(price, discountPct))) return 0; return price; } function scoreCombo(three, context, waitMsOverride) { const bonuses = collectBonuses(three); const owned = resolveOwnedList(context); const { flatSum, breakdown: rawBreakdown } = scoreWindowActivities(bonuses, context, waitMsOverride ?? comboEquipWaitMs(three, owned, context.settings, context.snapshot.slotLocks)); const multiplier = 1 + bonuses.allArpPct; const windowArp = flatSum * multiplier; const breakdown = {}; for (const [key, raw] of Object.entries(rawBreakdown)) { const preMultiplier = raw.base + raw.categoryBonus; const total = Math.round(preMultiplier * multiplier); const base = Math.round(raw.base); const categoryBonus = Math.round(raw.categoryBonus); breakdown[key] = { total, base, categoryBonus, allArpBonus: total - base - categoryBonus }; } const marketplaceSavingsArp = vaultPurchasePriceNow(context, bonuses.marketDiscountPct) * bonuses.marketDiscountPct; return { artifacts: three, weeklyArp: Math.round(windowArp), marketplaceSavingsArp: Math.round(marketplaceSavingsArp), totalScore: Math.round(windowArp + marketplaceSavingsArp), allArpPct: bonuses.allArpPct, steamQuestsFlat: bonuses.steamQuests, watchTwitchFlat: bonuses.watchTwitch, dailyCalendarFlat: bonuses.dailyCalendar, discordPollFlat: bonuses.discordPoll, marketDiscountPct: bonuses.marketDiscountPct, activeSetNames: activeSets(three.map((a) => a.familyId)).map((s) => s.name), breakdown }; } var BP_CLAIM_BUFFER_MS = 6e5; var deferBattlePassCache = new WeakMap(); var UPGRADE_PATH_MAX = 5; function monthlyUpgradeGain(artifact, toTier) { const family = getArtifactById(artifact.familyId); if (!family || family.effectUnit === "cosmetic") return 0; const delta = getNumericEffect(family, toTier) - getNumericEffect(family, artifact.tier); if (delta <= 0) return 0; if (family.effectType === ArtifactEffectType.AllArpPct) return Math.round(delta * MONTHLY_ARP_FOR_PCT); const uses = MONTHLY_CATEGORY_USES[family.effectType]; if (uses === void 0) return 0; return Math.round(delta * uses); } function withUpgradedArtifact(artifact, toTier) { const family = getArtifactById(artifact.familyId); const upgraded = { ...artifact, tier: toTier, displayName: family ? displayNameFor(family, toTier) : artifact.displayName }; const nextCost = fragmentCostToUpgradeFrom(toTier); if (nextCost === void 0) delete upgraded.upgradeCost; else upgraded.upgradeCost = nextCost; return upgraded; } function replaceOwned(owned, instanceId, replacement) { return owned.map((artifact) => artifact.instanceId === instanceId ? replacement : artifact); } function upgradeFocusRank(familyId, order) { const index = order.indexOf(familyId); return index === -1 ? order.length : index; } function nextUpgradeCandidate(owned, focusOrder) { const candidates = []; for (const artifact of owned) { if (artifact.tier >= ArtifactTier.Interstellar) continue; const family = getArtifactById(artifact.familyId); const toTier = artifact.tier + 1; if (family?.effects[toTier] === void 0) continue; const fragmentCost = artifact.upgradeCost ?? fragmentCostToUpgradeFrom(artifact.tier); if (fragmentCost === void 0) continue; const arpGain = monthlyUpgradeGain(artifact, toTier); if (arpGain <= 0) continue; candidates.push({ artifact, fromTier: artifact.tier, toTier, fragmentCost, arpGain, efficiency: arpGain / fragmentCost, isAffordable: false }); } return candidates.toSorted((left, right) => { const rankDelta = upgradeFocusRank(left.artifact.familyId, focusOrder) - upgradeFocusRank(right.artifact.familyId, focusOrder); if (rankDelta !== 0) return rankDelta; if (right.arpGain !== left.arpGain) return right.arpGain - left.arpGain; return left.fragmentCost - right.fragmentCost; })[0]; } function suggestUpgrades(owned, fragments) { const focusOrder = upgradeFocusOrder(new Set(owned.map((artifact) => artifact.familyId))); let remaining = fragments; let isSaving = false; let working = owned.map((artifact) => ({ ...artifact })); const path = []; while (path.length < UPGRADE_PATH_MAX) { const next = nextUpgradeCandidate(working, focusOrder); if (!next) break; const isAffordable = !isSaving && next.fragmentCost <= remaining; if (isAffordable) remaining -= next.fragmentCost; else isSaving = true; const ownedName = owned.find((artifact) => artifact.instanceId === next.artifact.instanceId)?.displayName ?? next.artifact.displayName; path.push({ ...next, artifact: { ...next.artifact, displayName: ownedName }, isAffordable }); working = replaceOwned(working, next.artifact.instanceId, withUpgradedArtifact(next.artifact, next.toTier)); } return path; } function findBestCombo(owned, context) { if (resolveDeferredAllArp(owned, context)) { const equipped = currentLoadout(owned); const frozen = findBestComboBy(owned, context, (combo) => combo.weeklyArp, (combo) => combo.allArpPct > 0 || isSameLoadout$1(combo.artifacts, equipped)); if (frozen) return frozen; } const best = findBestComboBy(owned, context, (combo) => combo.weeklyArp, () => true); const equipped = currentLoadout(owned); if (best && best.allArpPct > 0 && !isSameLoadout$1(best.artifacts, equipped)) { const waitMs = comboEquipWaitMs(best.artifacts, owned, context.settings, context.snapshot.slotLocks); if (waitMs > 0 && !isAllArpWorthTheLock(best.artifacts, owned, context, waitMs)) return findBestComboBy(owned, context, (combo) => combo.weeklyArp, (combo) => combo.allArpPct <= 0 || isSameLoadout$1(combo.artifacts, equipped)); } return best; } function isAllArpWorthTheLock(allArpArtifacts, owned, context, waitMs) { const allArpBonuses = collectBonuses(allArpArtifacts); const alternative = bestFlatBonusesForLock(owned, context, waitMs); if (!alternative) return true; return communityEventArpInSwapWindow(context.siteState, waitMs) * allArpBonuses.allArpPct + forcedDailyArpDelta(context.siteState, waitMs, allArpBonuses, alternative, utcDailyEndBufferMs(context.settings)) > 0; } function bestFlatBonusesForLock(owned, context, waitMs) { const size = Math.min(3, owned.length); const pinned = pinnedEquippedArtifacts(owned, context.settings, context.siteState, context.snapshot.slotLocks); let best; let bestArp = Number.NEGATIVE_INFINITY; const consider = (combo) => { const bonuses = collectBonuses(combo); if (bonuses.allArpPct > 0) return; const scored = scoreCombo(combo, context, waitMs).weeklyArp; if (!best || scored > bestArp) { best = bonuses; bestArp = scored; } }; for (const combo of combinationsWithPinned(owned, size, pinned)) consider(combo); const equipped = currentLoadout(owned); if (equipped.length > 0) consider(equipped); return best; } var MS_PER_DAY = 864e5; var TWITCH_MS_PER_ARP = 6e4; var TIME_ON_SITE_DURATION_MS$1 = BASE_ACTIVITY.timeOnSiteBasePerDay * 6e4; function utcDayBounds(dayStartMs, midnight) { if (dayStartMs <= 0) return { fromMs: 0, untilMs: midnight }; return { fromMs: dayStartMs, untilMs: dayStartMs + MS_PER_DAY }; } function isAutoClaimForcedIntoLock(dayStartMs, waitMs) { return dayStartMs > waitMs && dayStartMs <= waitMs + 864e5; } function isTimedDailyForcedIntoLock(dayStartMs, waitMs, durationMs, midnight, deadlineBufferMs) { const { fromMs, untilMs } = utcDayBounds(dayStartMs, midnight); return canCompleteInWearWindow(fromMs, untilMs, waitMs, durationMs) && !canCompleteOutsideWearWindow(fromMs, untilMs, waitMs, durationMs, 864e5, deadlineBufferMs); } function twitchDayArp(bonuses, siteState, isToday) { const cap = (siteState.watchTwitch?.capArp ?? BASE_ACTIVITY.watchTwitchBasePerDay) + bonuses.watchTwitch; const remaining = twitchWatchRemainingMs(siteState, bonuses.watchTwitch) / 6e4; return (isToday ? remaining : cap) * (1 + bonuses.allArpPct); } function twitchDayDurationMs(bonuses, siteState, isToday) { const cap = (siteState.watchTwitch?.capArp ?? BASE_ACTIVITY.watchTwitchBasePerDay) + bonuses.watchTwitch; const remaining = twitchWatchRemainingMs(siteState, bonuses.watchTwitch) / 6e4; return (isToday ? remaining : cap) * TWITCH_MS_PER_ARP; } function forcedDailyArpDelta(siteState, waitMs, allArp, flat, deadlineBufferMs) { const midnight = msUntilNextUtcMidnight(); let delta = 0; const twitchDays = []; if (twitchWatchRemainingMs(siteState, flat.watchTwitch) > 0) twitchDays.push(0); twitchDays.push(midnight, midnight + MS_PER_DAY); for (const dayStart of twitchDays) { if (dayStart > waitMs + 864e5) continue; const isToday = dayStart === 0; if (!isTimedDailyForcedIntoLock(dayStart, waitMs, twitchDayDurationMs(flat, siteState, isToday), midnight, deadlineBufferMs)) continue; delta += twitchDayArp(allArp, siteState, isToday) - twitchDayArp(flat, siteState, isToday); } const tosDays = [ 0, midnight, midnight + MS_PER_DAY ].filter((dayStart) => (dayStart > 0 || isActivityAvailable(siteState.caps, "timeOnSite")) && isTimedDailyForcedIntoLock(dayStart, waitMs, TIME_ON_SITE_DURATION_MS$1, midnight, deadlineBufferMs)); if (tosDays.length > 0) { const allArpTos = (BASE_ACTIVITY.timeOnSiteBasePerDay + allArp.timeOnSite) * (1 + allArp.allArpPct); const flatTos = (BASE_ACTIVITY.timeOnSiteBasePerDay + flat.timeOnSite) * (1 + flat.allArpPct); delta += tosDays.length * (allArpTos - flatTos); } const calendarDays = [midnight, midnight + MS_PER_DAY].filter((dayStart) => isAutoClaimForcedIntoLock(dayStart, waitMs)); if (calendarDays.length > 0) { const allArpCal = (BASE_ACTIVITY.dailyCalendarBasePerDay + allArp.dailyCalendar) * (1 + allArp.allArpPct); const flatCal = (BASE_ACTIVITY.dailyCalendarBasePerDay + flat.dailyCalendar) * (1 + flat.allArpPct); delta += calendarDays.length * (allArpCal - flatCal); } const questDays = [ 0, midnight, midnight + MS_PER_DAY ].filter((dayStart) => { const isTodayDue = dayStart === 0 && isActivityAvailable(siteState.caps, "dailyQuests"); if (dayStart === 0 && !isTodayDue) return false; return isTimedDailyForcedIntoLock(dayStart, waitMs, 0, midnight, deadlineBufferMs); }); for (const dayStart of questDays) { const onDay = new Date(Date.now() + dayStart); const weekend = onDay.getUTCDay() === 0 || onDay.getUTCDay() === 6 ? BASE_ACTIVITY.weekendQuestBase : 0; const base = BASE_ACTIVITY.dailyQuestBase + weekend; delta += base * (1 + allArp.allArpPct) - base * (1 + flat.allArpPct); } return delta; } function resolveDeferredAllArp(owned, context) { const event = context.siteState.communityEvent; if (!event?.isLive || !canEarnCommunityEventArp(event)) return; if (hasAllArpEffect(currentLoadout(owned))) return; const artifacts = unconstrainedAllArpCombo(owned); if (!artifacts) return; const waitMs = allArpEquipWaitMs(owned, context.settings, context.snapshot.slotLocks); if (waitMs === void 0 || waitMs <= 0) return; const waiting = waitingCommunityMilestones(event); const next = waiting[0]; if (next === void 0) return; const nextTarget = next.communityHoursRequired; const nextEta = nextTarget === void 0 ? void 0 : estimateCommunityUnlockAt(event, nextTarget); const later = nextEta === void 0 || nextEta.etaMs < waitMs ? waiting.slice(1) : waiting; const firstLater = later[0]; if (firstLater === void 0) return; const laterTarget = firstLater.communityHoursRequired; const laterEta = laterTarget === void 0 ? void 0 : estimateCommunityUnlockAt(event, laterTarget); const unlock = { arpReward: later.reduce((sum, milestone) => sum + milestone.arpReward, 0) }; if (laterTarget !== void 0) unlock.targetHours = laterTarget; if (laterEta !== void 0) unlock.etaMs = laterEta.etaMs; if (!isAllArpWorthTheLock(artifacts, owned, context, waitMs)) return; return { waitMs, artifacts, unlock }; } function resolveDeferredSteam(owned, context, best) { if (!isActivityPending(context.siteState.caps, "steamQuests")) return; const remaining = scrapedRemainingSteamQuestRewards(context.siteState); if (!remaining || remaining.length === 0) return; const steam = findBestSteamCombo(owned, context); if (!steam || steam.steamQuestsFlat <= 0) return; const equipped = currentLoadout(owned); if (isSameLoadout$1(steam.artifacts, equipped)) return; if (best && isSameLoadout$1(steam.artifacts, best.artifacts)) return; if (collectBonuses(equipped).steamQuests >= steam.steamQuestsFlat) return; const waitMs = comboEquipWaitMs(steam.artifacts, owned, context.settings, context.snapshot.slotLocks); if (isWeeklyForcedIntoLock(msUntilNextSteamQuestWeek(), waitMs)) return; return { waitMs, artifacts: steam.artifacts }; } function comboTieBreakDelta(scored, best, equipped) { if (scored.allArpPct !== best.allArpPct) return scored.allArpPct - best.allArpPct; const isScoredEquipped = isSameLoadout$1(scored.artifacts, equipped); if (isScoredEquipped === isSameLoadout$1(best.artifacts, equipped)) return 0; return isScoredEquipped ? 1 : -1; } function findBestComboBy(owned, context, primary, isEligible) { if (owned.length === 0) return; const size = Math.min(3, owned.length); const equipped = currentLoadout(owned); const pinned = pinnedEquippedArtifacts(owned, context.settings, context.siteState, context.snapshot.slotLocks); let best; let bestPrimary = Number.NEGATIVE_INFINITY; for (const combo of combinationsWithPinned(owned, size, pinned)) { const scored = scoreCombo(combo, context); if (!isEligible(scored)) continue; const score = primary(scored); if (!best || score > bestPrimary || score === bestPrimary && scored.totalScore > best.totalScore || score === bestPrimary && scored.totalScore === best.totalScore && comboTieBreakDelta(scored, best, equipped) > 0) { best = scored; bestPrimary = score; } } return best; } function findBestAllArpCombo(owned, context) { return findBestComboBy(owned, context, (combo) => combo.allArpPct, (combo) => combo.allArpPct > 0); } function findBestSteamCombo(owned, context) { return findBestComboBy(owned, context, (combo) => combo.steamQuestsFlat, (combo) => combo.steamQuestsFlat > 0); } function findBestMarketDiscountCombo(owned, context) { return findBestComboBy(owned, context, (combo) => combo.marketDiscountPct, (combo) => combo.marketDiscountPct > 0); } function hasMarketDiscount(combo) { if (!combo || combo.artifacts.length === 0) return false; return combo.marketDiscountPct > 0; } function isMonthlyMetaEligible(artifact) { const family = getArtifactById(artifact.familyId); if (!family || family.effectUnit === "cosmetic") return false; if (family.effectType === ArtifactEffectType.None) return false; if (family.effectType === ArtifactEffectType.AllArpPct && getNumericEffect(family, artifact.tier) < 0) return false; return true; } function bestOwnedOfFamily(owned, familyId, usedIds) { return owned.filter((artifact) => artifact.familyId === familyId && !usedIds.has(artifact.instanceId) && isMonthlyMetaEligible(artifact)).toSorted((left, right) => right.tier - left.tier)[0]; } function findMonthlyMetaCombo(owned, context) { const { standing, fillOrder } = monthlyMetaStandingFamilies(new Set(owned.map((artifact) => artifact.familyId))); const picked = []; const usedIds = new Set(); const tryAddFamily = (familyId) => { if (picked.length >= 3) return; const artifact = bestOwnedOfFamily(owned, familyId, usedIds); if (!artifact) return; picked.push(artifact); usedIds.add(artifact.instanceId); }; for (const familyId of standing) tryAddFamily(familyId); for (const familyId of fillOrder) tryAddFamily(familyId); if (picked.length === 0) return; return scoreCombo(picked, context); } function suggestDailySwap(best, current) { if (!current || current.artifacts.length < 3) return; const currentIds = new Set(current.artifacts.map((a) => a.instanceId)); const bestIds = new Set(best.artifacts.map((a) => a.instanceId)); const toUnequip = current.artifacts.find((a) => !bestIds.has(a.instanceId)); const toEquip = best.artifacts.find((a) => !currentIds.has(a.instanceId)); if (!toUnequip || !toEquip) return; return { unequip: toUnequip, equip: toEquip, reason: `Swap ${toUnequip.displayName} → ${toEquip.displayName} for +${best.totalScore - current.totalScore} estimated ARP in the next 24h swap window` }; } function hasAllArpEffect(artifacts) { return collectBonuses(artifacts).allArpPct > 0; } function canAssembleAllArp(owned) { const ids = new Set(owned.map((artifact) => artifact.familyId)); if (ids.has("herkow-plasma-chamber")) return true; return ARTIFACT_SETS.find((set) => set.id === "zorathian-renaissance")?.memberIds.every((id) => ids.has(id)) === true; } function hasInventoryAllArp(owned) { return canAssembleAllArp(owned) || hasAllArpEffect(owned); } function unconstrainedAllArpCombo(owned) { if (owned.length === 0) return; const size = Math.min(3, owned.length); let best; let bestPct = 0; for (const combo of combinations(owned, size)) { const pct = collectBonuses(combo).allArpPct; if (pct > bestPct) { bestPct = pct; best = combo; } } return bestPct > 0 ? best : void 0; } function allArpEquipWaitMs(owned, settings, slotLocks) { if (hasAllArpEffect(currentLoadout(owned))) return 0; const combo = unconstrainedAllArpCombo(owned); if (!combo) return; return comboEquipWaitMs(combo, owned, settings, slotLocks); } function shouldWaitForAllArpBeforeBattlePass(owned, settings, siteState, slotLocks) { if (!hasInventoryAllArp(owned)) return false; if (hasAllArpEffect(currentLoadout(owned))) return false; if (battlePassClaimableArp(siteState.battlePass) <= 0) return false; const waitMs = allArpEquipWaitMs(owned, settings, slotLocks); if (waitMs === void 0) return false; const bpLeft = battlePassRemainingMs(siteState.battlePass); if (bpLeft === void 0) return true; return waitMs + BP_CLAIM_BUFFER_MS < bpLeft; } function shouldDeferBattlePassForContext(context) { const cached = deferBattlePassCache.get(context); if (cached !== void 0) return cached; const shouldDefer = shouldWaitForAllArpBeforeBattlePass(resolveOwnedList(context), context.settings, context.siteState, context.snapshot.slotLocks); deferBattlePassCache.set(context, shouldDefer); return shouldDefer; } function appendBattlePassNotes(notes, owned, equipped, context) { const bp = context.siteState.battlePass; const readyArp = battlePassClaimableArp(bp); if (!bp || readyArp <= 0) return; const hasOwnedAllArp = hasInventoryAllArp(owned); const hasAllArpOn = hasAllArpEffect(equipped); if (hasOwnedAllArp && !hasAllArpOn) { if (shouldDeferBattlePassForContext(context)) return; notes.push(`Claim ${readyArp} Battle Pass ARP Boost(s) now — Battle Pass ends before All-ARP% can be equipped.`); return; } if (hasAllArpOn) { notes.push(`Claim ${readyArp} Battle Pass ARP Boost(s) now — All-ARP% is equipped.`); return; } notes.push(`You have ${readyArp} Battle Pass ARP Boost(s) ready to claim.`); } function appendCommunityEventNotes(notes, owned, equipped, context) { const event = context.siteState.communityEvent; if (!event?.isLive || !canEarnCommunityEventArp(event)) return; const breakdown = breakDownCommunityEventPending(event); if (breakdown.pendingCount <= 0 && nextLockedCommunityArpMilestone(event) === void 0) return; const summary = describeCommunityEventPendingNote(event, breakdown); const hasAllArpOwned = hasInventoryAllArp(owned); const hasAllArpOn = hasAllArpEffect(equipped); if (hasAllArpOwned && !hasAllArpOn && breakdown.waitingPersonalArp > 0) { notes.push(`${summary} — equip All-ARP% first.`); return; } if (hasAllArpOwned && !hasAllArpOn && communityEventArpInSwapWindow(context.siteState) > 0 && resolveDeferredAllArp(owned, context) === void 0) { notes.push(summary); return; } if (hasAllArpOwned && !hasAllArpOn && breakdown.waitingCommunityArp > 0) { const deferred = resolveDeferredAllArp(owned, context); if (deferred) { const hours = deferred.unlock.targetHours === void 0 ? "" : ` before ${deferred.unlock.targetHours.toLocaleString()}h`; notes.push(`${summary} — hold this loadout; All-ARP% in ${formatCommunityEta(deferred.waitMs)}${hours} (${formatCommunityEventArp(deferred.unlock.arpReward)}).`); return; } notes.push(`${summary} — consider All-ARP%.`); return; } notes.push(summary); } function describeCommunityEventPendingNote(event, breakdown) { if (breakdown.waitingPersonalArp > 0) return `${formatCommunityEventArp(breakdown.waitingPersonalArp)} unlocked by community`; if (breakdown.waitingCommunityArp > 0) return describeWaitingCommunityArpLine(event, breakdown.waitingCommunityArp); const nextLocked = nextLockedCommunityArpMilestone(event); if (nextLocked) return describeWaitingCommunityArpLine(event, nextLocked.arpReward); if (breakdown.imminentArp > 0) return `${formatCommunityEventArp(breakdown.imminentArp)} unlocked — not awarded yet`; return `${formatCommunityEventArp(event.pendingArp)} still open`; } function collectNotes(owned, equipped, best, context) { const notes = []; appendBattlePassNotes(notes, owned, equipped, context); appendCommunityEventNotes(notes, owned, equipped, context); if (isActivityPending(context.siteState.caps, "steamQuests") && equipped.length > 0) { const currentSteam = collectBonuses(equipped).steamQuests; const ownedSteam = Math.max(currentSteam, best?.steamQuestsFlat ?? 0, ...owned.map((artifact) => collectBonuses([artifact]).steamQuests)); if (best && best.steamQuestsFlat < currentSteam) notes.push(`Steam Quests still look unfinished — finish them before swapping away from your +${currentSteam} Steam Quests bonus (equip before starting quests).`); else if (currentSteam === 0 && ownedSteam > 0) notes.push("Equip a Steam Quests artifact before starting any quest — Control Center still shows 15/25; real ARP is on the ARP Log."); } return notes; } function earliestSlotUnlockMs(context, now = Date.now()) { const slotLocks = context.snapshot.slotLocks; const remaining = [ 1, 2, 3 ].map((position) => { const equipped = context.snapshot.artifacts.find((artifact) => artifact.equippedPosition === position); return showroomCooldownRemainingMs(context.settings, position, { now, ...slotLocks && { slotLocks }, ...typeof equipped?.slotLocked === "boolean" && { equippedSlotLocked: equipped.slotLocked } }); }); return now + Math.min(...remaining); } function dismissedVaultGuard(arpBest, cycleId) { return { best: arpBest, vaultDiscount: { cycleId, dismissed: true } }; } function suggestVaultDiscount(best, discountCombo, note, cycleId) { const vaultDiscount = { cycleId, note, dismissed: false }; if (discountCombo && !isSameLoadout$1(best.artifacts, discountCombo.artifacts)) return { best, marketDiscountLoadout: discountCombo, vaultDiscount }; if (hasMarketDiscount(best)) return { best, vaultDiscount }; return { best, vaultDiscount }; } function resolvePreOpenVaultDiscount(arpBest, current, discountCombo, context, cycleId, now) { const opensAt = gameVaultOpensAtMs(context.siteState); if (opensAt === void 0) return { best: arpBest }; const eta = formatCommunityEta(Math.max(0, opensAt - now)); const swapAt = earliestSlotUnlockMs(context, now); if (current !== void 0 && isSameLoadout$1(arpBest.artifacts, current.artifacts)) { if (!willMissDiscountEquipBeforeOpen(swapAt, context.siteState, now)) return { best: arpBest }; return { best: arpBest, vaultDiscount: { cycleId, dismissed: false, note: `Slots locked past Game Vault open (${eta}) — market-discount may not be equippable in time.` } }; } if (!willMissDiscountEquipBeforeOpen(swapAt + 864e5, context.siteState, now)) return { best: arpBest }; if (current && hasMarketDiscount(current)) return suggestVaultDiscount(current, discountCombo, `Keep market-discount equipped until Game Vault opens (${eta}) — swapping now would lock slots past open.`, cycleId); if (discountCombo) return suggestVaultDiscount(discountCombo, discountCombo, `Equip market-discount before Game Vault opens (${eta}) — a 24h ARP swap would still be locked at open.`, cycleId); return { best: arpBest }; } function resolveOpenVaultDiscount(arpBest, current, discountCombo, context, cycleId, now) { if (current !== void 0 && isSameLoadout$1(arpBest.artifacts, current.artifacts)) { if (!discountCombo) return { best: arpBest }; return { best: arpBest, marketDiscountLoadout: discountCombo, vaultDiscount: { cycleId, dismissed: false, note: "Game Vault has eligible claims — equip market-discount before buying (logout/relogin after)." } }; } if (current && hasMarketDiscount(current)) return suggestVaultDiscount(current, discountCombo, "Keep market-discount equipped — Game Vault stock can run out, and a 24h swap would miss the discount.", cycleId); const canEquipNow = earliestSlotUnlockMs(context, now) <= now; if (discountCombo && canEquipNow) return suggestVaultDiscount(discountCombo, discountCombo, "Equip market-discount before claiming Game Vault (eligible stock can run out). Logout/relogin after.", cycleId); return { best: arpBest, vaultDiscount: { cycleId, dismissed: false, note: "Slots locked — Game Vault stock may run out before you can equip market-discount." } }; } function resolveVaultDiscountBest(arpBest, current, discountCombo, context, now = Date.now()) { const cycleId = gameVaultCycleId(context.siteState); if (cycleId && context.settings.vaultDiscountDismissedCycle === cycleId) return dismissedVaultGuard(arpBest, cycleId); if (!arpBest || hasMarketDiscount(arpBest)) return { best: arpBest }; const discountPct = comboMarketDiscountPct(discountCombo); const projectedArp = projectedRedeemableArp(context, arpBest, current, discountCombo); if (hasPostedListPriceVaultGames(context.siteState) && !canAffordAnyVaultOffer(context.siteState, discountPct, projectedArp)) return { best: arpBest }; if (isGameVaultStockOpen(context.siteState)) return resolveOpenVaultDiscount(arpBest, current, discountCombo, context, cycleId ?? "open", now); const opensAt = gameVaultOpensAtMs(context.siteState); if (opensAt !== void 0 && opensAt > now) return resolvePreOpenVaultDiscount(arpBest, current, discountCombo, context, cycleId ?? context.siteState.gameVaultOpensAt ?? "upcoming", now); return { best: arpBest }; } function optimize(context) { const owned = resolveOwnedList(context); if (owned.length === 0) return { best: void 0, current: void 0, alternatives: [], upgrades: [], dailySwap: void 0, notes: ["No owned artifacts known yet — inventory could not be loaded automatically. Open the optimizer again in a moment, or expand Advanced / manual overrides."], hasAllArpOwned: false, hasAllArpEquipped: false }; const upgrades = suggestUpgrades(owned, context.settings.manualFragments ?? context.snapshot.fragments); const arpBest = findBestCombo(owned, context); const equipped = currentLoadout(owned); const current = equipped.length > 0 ? scoreCombo(equipped, context) : void 0; const allArpLoadout = findBestAllArpCombo(owned, context); const guarded = resolveVaultDiscountBest(arpBest, current, findBestMarketDiscountCombo(owned, context), context); const best = guarded.best; const monthlyMetaLoadout = findMonthlyMetaCombo(owned, context); const alternatives = []; if (owned.length >= 3) { const scored = combinationsWithPinned(owned, 3, pinnedEquippedArtifacts(owned, context.settings, context.siteState, context.snapshot.slotLocks)).map((combo) => scoreCombo(combo, context)).toSorted((left, right) => right.weeklyArp - left.weeklyArp); alternatives.push(...scored.slice(0, 5)); } const marketDiscountLoadout = guarded.marketDiscountLoadout; if (marketDiscountLoadout && alternatives.every((combo) => !isSameLoadout$1(combo.artifacts, marketDiscountLoadout.artifacts))) alternatives.push(marketDiscountLoadout); const deferredAllArp = resolveDeferredAllArp(owned, context); const deferredSteam = resolveDeferredSteam(owned, context, best); const shouldDeferBattlePassClaims = shouldDeferBattlePassForContext(context); const isDedicatedLockWorthIt = isAllArpLockWorthBattlePassBoost(best, allArpLoadout, battlePassClaimableArp(context.siteState.battlePass)); const notes = collectNotes(owned, equipped, best, context); const result = { best, current, alternatives, upgrades, dailySwap: best ? suggestDailySwap(best, current) : void 0, notes, hasAllArpOwned: hasInventoryAllArp(owned), hasAllArpEquipped: hasAllArpEffect(equipped), deferBattlePassClaims: shouldDeferBattlePassClaims }; if (isDedicatedLockWorthIt) result.worthDedicatedAllArpForBattlePass = true; if (context.snapshot.slotLocks) result.slotLocks = context.snapshot.slotLocks; if (allArpLoadout) result.allArpLoadout = allArpLoadout; if (deferredAllArp) result.deferredAllArp = deferredAllArp; if (deferredSteam) result.deferredSteam = deferredSteam; if (marketDiscountLoadout) result.marketDiscountLoadout = marketDiscountLoadout; if (monthlyMetaLoadout) result.monthlyMetaLoadout = monthlyMetaLoadout; if (guarded.vaultDiscount) result.vaultDiscount = guarded.vaultDiscount; return result; } function formatMs(ms) { const days = Math.floor(ms / 864e5); const hours = Math.floor(ms % 864e5 / 36e5); if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`; const mins = Math.floor(ms % 36e5 / 6e4); if (hours > 0 && mins > 0) return `${hours}h ${mins}m`; if (hours > 0) return `${hours}h`; if (mins > 0) return `${mins}m`; return "<1m"; } function msUntilUtcMidnight(now = new Date()) { const next = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1); return Math.max(0, next - now.getTime()); } function utcResetDeadlineLabel(now = new Date()) { return `${formatMs(msUntilUtcMidnight(now))} left`; } function sortArtifactsForDisplay(artifacts) { return artifacts.toSorted((left, right) => left.displayName.localeCompare(right.displayName, void 0, { sensitivity: "base" })); } function loadoutLabel(artifacts) { if (!artifacts || artifacts.length === 0) return "—"; return sortArtifactsForDisplay(artifacts).map((artifact) => artifact.displayName).join(" + "); } function loadoutSetNames(artifacts) { if (!artifacts || artifacts.length === 0) return []; return activeSets(artifacts.map((artifact) => artifact.familyId)).map((set) => set.name); } function comboLabel(result) { if (!result) return "—"; return loadoutLabel(result.artifacts); } function isSameLoadout(left, right) { if (!left || !right || left.length === 0 || right.length === 0) return false; const leftIds = new Set(left.map((artifact) => artifact.instanceId)); const rightIds = new Set(right.map((artifact) => artifact.instanceId)); return leftIds.size === rightIds.size && [...leftIds].every((id) => rightIds.has(id)); } function maxSlotCooldownMs(settings, current, slotLocks) { return Math.max(0, ...[ 1, 2, 3 ].map((position) => { const equippedSlotLocked = current?.artifacts.find((artifact) => artifact.equippedPosition === position)?.slotLocked; return showroomCooldownRemainingMs(settings, position, { ...slotLocks && { slotLocks }, ...typeof equippedSlotLocked === "boolean" && { equippedSlotLocked } }); })); } function formatLockedSlotParts(settings, lockedSlots, slotLocks) { return lockedSlots.map((position) => { const remaining = showroomCooldownRemainingMs(settings, position, { ...slotLocks && { slotLocks } }); const estimateTag = settings.slotCooldowns.find((slot) => slot.position === position)?.estimated === true ? ", estimated" : ""; if (remaining <= 0) return `slot ${position} (locked${estimateTag})`; return `slot ${position} (${formatMs(remaining)} left${estimateTag})`; }); } function hasAnySlotOnCooldown(current, slotLocks) { return [ 1, 2, 3 ].some((position) => isSlotLockedForEquip(current, position, slotLocks)); } function isSlotLockedForEquip(current, position, slotLocks) { const equipped = current?.artifacts.find((artifact) => artifact.equippedPosition === position); return isShowroomSlotLocked(position, { ...slotLocks && { slotLocks }, ...typeof equipped?.slotLocked === "boolean" && { equippedSlotLocked: equipped.slotLocked } }); } function loadoutMonthlyScore(artifacts) { const bonuses = collectBonuses(artifacts); return bonuses.allArpPct * MONTHLY_ARP_FOR_PCT + bonuses.steamQuests * (MONTHLY_CATEGORY_USES[ArtifactEffectType.SteamQuests] ?? 0) + bonuses.watchTwitch * (MONTHLY_CATEGORY_USES[ArtifactEffectType.WatchTwitch] ?? 0) + bonuses.dailyCalendar * (MONTHLY_CATEGORY_USES[ArtifactEffectType.DailyCalendar] ?? 0) + bonuses.timeOnSite * (MONTHLY_CATEGORY_USES[ArtifactEffectType.TimeOnSite] ?? 0) + bonuses.discordPoll * (MONTHLY_CATEGORY_USES[ArtifactEffectType.DiscordPoll] ?? 0) + bonuses.marketDiscountPct * 1e3 + bonuses.communityPlaytimePct * 200; } function marginalEquipScore(artifact, basis) { return loadoutMonthlyScore([...basis, artifact]) - loadoutMonthlyScore(basis); } function compareByName(left, right) { return left.displayName.localeCompare(right.displayName, void 0, { sensitivity: "base" }); } function sortByMarginalEquipPriority(pieces, basis) { return pieces.toSorted((left, right) => { const scoreDelta = marginalEquipScore(right, basis) - marginalEquipScore(left, basis); if (scoreDelta !== 0) return scoreDelta; return compareByName(left, right); }); } function pickImmediateEquips(kept, remaining, slotCount) { const fillCount = Math.min(slotCount, remaining.length); if (fillCount <= 0) return []; let bestSubset = []; let bestScore = Number.NEGATIVE_INFINITY; for (const subset of combinations(remaining, fillCount)) { const score = loadoutMonthlyScore([...kept, ...subset]); if (score < bestScore) continue; if (score > bestScore) { bestScore = score; bestSubset = subset; continue; } const bestKey = bestSubset.map((item) => item.displayName).join("|"); if (subset.map((item) => item.displayName).join("|").localeCompare(bestKey) < 0) bestSubset = subset; } const ordered = []; const pool = [...bestSubset]; const basis = [...kept]; while (pool.length > 0) { pool.sort((left, right) => { const scoreDelta = marginalEquipScore(right, basis) - marginalEquipScore(left, basis); if (scoreDelta !== 0) return scoreDelta; return compareByName(left, right); }); const next = pool.shift(); if (!next) break; ordered.push(next); basis.push(next); } return ordered; } function planLoadoutChanges(combo, current, settings, slotLocks) { const slots = [ 1, 2, 3 ]; const lockedSlots = slots.filter((position) => isSlotLockedForEquip(current, position, slotLocks)); const currentBySlot = new Map(); const equippedArtifacts = current?.artifacts ?? []; for (const artifact of equippedArtifacts) if (artifact.equippedPosition !== void 0) currentBySlot.set(artifact.equippedPosition, artifact); const comboIds = new Set(combo.map((artifact) => artifact.instanceId)); const placedIds = new Set(); const reservedSlots = new Set(); const keptSlots = new Set(); const kept = []; for (const position of slots) { const equipped = currentBySlot.get(position); if (equipped && comboIds.has(equipped.instanceId)) { placedIds.add(equipped.instanceId); reservedSlots.add(position); keptSlots.add(position); kept.push(equipped); continue; } if (lockedSlots.includes(position)) reservedSlots.add(position); } const remaining = combo.filter((artifact) => !placedIds.has(artifact.instanceId)); const freeSlots = slots.filter((position) => !reservedSlots.has(position) && !lockedSlots.includes(position)); const nowArtifacts = pickImmediateEquips(kept, remaining, freeSlots.length); const now = []; for (const artifact of nowArtifacts) { const position = freeSlots.shift(); if (position === void 0) break; const replaced = currentBySlot.get(position); now.push({ artifactId: artifact.instanceId, position, displayName: artifact.displayName, ...replaced && { replacedDisplayName: replaced.displayName } }); placedIds.add(artifact.instanceId); } const laterBasis = [...kept, ...nowArtifacts]; const later = sortByMarginalEquipPriority(combo.filter((artifact) => !placedIds.has(artifact.instanceId)), laterBasis).map((artifact) => ({ artifactId: artifact.instanceId, displayName: artifact.displayName })); const waitMs = Math.max(0, ...lockedSlots.filter((position) => later.length === 0 || !keptSlots.has(position)).map((position) => { const equippedSlotLocked = currentBySlot.get(position)?.slotLocked; return showroomCooldownRemainingMs(settings, position, { ...slotLocks && { slotLocks }, ...typeof equippedSlotLocked === "boolean" && { equippedSlotLocked } }); })); return { now, later, laterNames: later.map((item) => item.displayName), lockedSlots, waitMs }; } function artifactsAfterImmediateEquip(current, best, plan) { const bySlot = new Map(); const equipped = current?.artifacts ?? []; for (const artifact of equipped) if (artifact.equippedPosition !== void 0) bySlot.set(artifact.equippedPosition, artifact); for (const change of plan.now) { const incoming = best.artifacts.find((artifact) => artifact.instanceId === change.artifactId); if (!incoming) continue; bySlot.set(change.position, { ...incoming, equippedPosition: change.position }); } return bySlot.values().toArray(); } function escapeHtml(value) { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'"); } var MODAL_ID = "alienware-artifact-optimizer"; var INLINE_ID = "alienware-artifact-optimizer-inline"; var CC_PANEL_ID = "alienware-artifact-optimizer-cc"; var BP_CLAIM_BAR_ID = "alienware-artifact-optimizer-bp-claim"; var STYLE_ID$1 = "alienware-artifact-optimizer-styles"; var BACKDROP_ID = "alienware-artifact-optimizer-backdrop"; var DIALOG_ID = "alienware-artifact-optimizer-dialog"; var TOAST_ID = "alienware-artifact-optimizer-toast"; var ARTIFACT_TIP_ID = "ao-artifact-tip-float"; var TOAST_MS = 2200; var MODAL_LAYOUT = [ ["position", "fixed"], ["top", "50%"], ["left", "50%"], ["transform", "translate(-50%, -50%)"], ["z-index", "10001"], ["width", "min(560px, 94vw)"], ["max-height", "90vh"], ["overflow-y", "auto"] ]; var BACKDROP_LAYOUT = [ ["position", "fixed"], ["inset", "0"], ["background", "rgba(0, 0, 0, 0.85)"], ["z-index", "10000"] ]; function cssDeclarations(layout) { return layout.map(([property, value]) => `${property}: ${value};`).join("\n "); } function buildOptimizerCss() { return ` #${BACKDROP_ID} { display: none; ${cssDeclarations(BACKDROP_LAYOUT)} } #${MODAL_ID} { display: none; ${cssDeclarations(MODAL_LAYOUT)} background: transparent; } #${INLINE_ID}, #${CC_PANEL_ID} { display: block; margin: 16px 0; width: 100%; max-width: 100%; box-sizing: border-box; } body > #${INLINE_ID}, body > #${CC_PANEL_ID}, html > #${INLINE_ID}, html > #${CC_PANEL_ID} { margin: 88px auto 16px; padding: 0 16px; max-width: 1100px; } #${DIALOG_ID} { position: fixed; inset: 0; z-index: 10002; display: flex; align-items: center; justify-content: center; } #${DIALOG_ID}[hidden] { display: none !important; } #${DIALOG_ID} .ao-dialog-scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.85); } #${DIALOG_ID} .ao-dialog { position: relative; z-index: 1; width: min(420px, 92vw); background: #1a1a1a; color: #fff; border: 1px solid #00bc8c; border-radius: 8px; padding: 20px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.85); font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; line-height: 1.45; } #${DIALOG_ID} .ao-dialog-title { margin: 0 0 10px; color: #00bc8c; font-size: 1.1em; font-weight: bold; } #${DIALOG_ID} .ao-dialog-message { margin: 0 0 16px; color: #eee; white-space: pre-wrap; } #${DIALOG_ID} .ao-dialog-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; } #${DIALOG_ID} button { background: #00bc8c; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; } #${DIALOG_ID} button.ao-secondary { background: #555; } #${DIALOG_ID} button.ao-danger { background: #e74c3c; } #${TOAST_ID} { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 10003; max-width: min(420px, 92vw); background: #1a1a1a; color: #fff; border: 1px solid #00bc8c; border-radius: 8px; padding: 10px 16px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6); font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; } #${TOAST_ID}[hidden] { display: none !important; } #${ARTIFACT_TIP_ID} { position: fixed; z-index: 10004; max-width: min(280px, 92vw); background: #1a1a1a; color: #fff; border: 1px solid #00bc8c; border-radius: 8px; padding: 10px 12px; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 13px; line-height: 1.4; pointer-events: none; } #${ARTIFACT_TIP_ID}[hidden] { display: none !important; } #${ARTIFACT_TIP_ID} .ao-artifact-tip-name { color: #00bc8c; font-weight: 700; margin: 0 0 4px; } #${ARTIFACT_TIP_ID} .ao-artifact-tip-meta { color: #aaa; font-size: 0.92em; margin: 0 0 6px; } #${ARTIFACT_TIP_ID} .ao-artifact-tip-effect { color: #fff; font-weight: 600; } #${ARTIFACT_TIP_ID} .ao-artifact-tip-detail, #${ARTIFACT_TIP_ID} .ao-artifact-tip-set { color: #ccc; font-size: 0.92em; margin-top: 4px; } `; } function ensureOptimizerStyles() { let style = document.querySelector(`#${STYLE_ID$1}`); if (!style) { style = document.createElement("style"); style.id = STYLE_ID$1; (document.head || document.documentElement).append(style); } style.textContent = buildOptimizerCss(); } function applyOpaqueModalChrome(modal) { const paint = [ ...MODAL_LAYOUT, ["background", "transparent"], ["opacity", "1"] ]; for (const [property, value] of paint) modal.style.setProperty(property, value, "important"); } function applyOpaqueBackdropChrome(backdrop) { const paint = [ ...BACKDROP_LAYOUT, ["background-color", "rgba(0, 0, 0, 0.85)"], ["opacity", "1"] ]; for (const [property, value] of paint) backdrop.style.setProperty(property, value, "important"); } function buildPanelShadowCss(variant) { return ` ${variant === "modal" ? ` :host { display: none; ${cssDeclarations(MODAL_LAYOUT)} box-sizing: border-box; } ` : ` :host { display: block; margin: 0; width: 100%; max-width: 100%; box-sizing: border-box; } `} .ao-panel, .ao-panel * { text-decoration: none !important; text-decoration-line: none !important; -webkit-text-fill-color: unset !important; text-transform: none !important; letter-spacing: normal !important; text-shadow: none !important; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif !important; box-sizing: border-box; } .ao-panel { display: block; background: #1a1a1a; color: #fff; padding: ${variant === "modal" ? "20px" : "16px"}; border-radius: 8px; border: 1px solid ${variant === "modal" ? "#444" : "#00bc8c"}; box-shadow: ${variant === "modal" ? "0 12px 40px rgba(0, 0, 0, 0.85)" : "0 0 10px rgba(0, 188, 140, 0.25)"}; font-size: 14px; line-height: 1.4; width: 100%; } .ao-panel > * { display: block; width: 100%; } .ao-title { color: #fff !important; font-size: 1.4em !important; font-weight: bold !important; margin: 0 0 12px !important; } .ao-heading { color: #00bc8c !important; font-size: 1.05em !important; font-weight: bold !important; margin: 14px 0 8px !important; } .ao-heading:first-child { margin-top: 0 !important; } .ao-row { display: block; margin: 6px 0 6px 8px; color: #fff !important; line-height: 1.4; } .ao-panel .ao-artifact-tip { border-bottom: 1px dotted #00bc8c; cursor: help; } .ao-muted { color: #aaa !important; font-size: 0.9em !important; } .ao-credit { margin: 0 0 10px !important; } .ao-note { display: block; background: #2a2a2a; border-left: 3px solid #00bc8c; padding: 8px 10px; margin: 8px 0; color: #eee !important; } .ao-note > div + div { margin-top: 4px; } .ao-note-actions { margin-top: 8px; } .ao-status-details { margin: 8px 0 4px; } .ao-status-details summary { cursor: pointer; user-select: none; } .ao-status-details[open] summary { margin-bottom: 6px; } .ao-text-link { color: #00bc8c !important; text-decoration: underline !important; text-decoration-line: underline !important; cursor: pointer; } .ao-actions { display: flex !important; flex-wrap: wrap; gap: 8px; margin-top: 12px; width: 100%; } .ao-actions-sep { width: 1px; align-self: stretch; min-height: 28px; background: #555; margin: 0 4px; } .ao-todo-list { display: block; margin: 0 0 4px; padding: 0; list-style: none; width: 100%; } .ao-divider { display: block; border: 0; border-top: 1px solid #444; margin: 14px 0; width: 100%; } .ao-todo-item { display: flex; gap: 6px; margin: 6px 0; line-height: 1.45; color: #eee !important; align-items: flex-start; } .ao-todo-index { color: #00bc8c !important; font-weight: 600; flex: 0 0 auto; padding-top: 1px; } .ao-todo-item > .ao-upgrade-btn, .ao-todo-item > .ao-claim-btn, .ao-todo-item > .ao-twitch-btn { flex: 0 0 auto; padding: 4px 10px; font-size: 13px !important; } .ao-row .ao-upgrade-btn { margin-left: 8px; } .ao-todo-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; } .ao-todo-headline { display: block; font-weight: 600; } .ao-todo-loadout { display: block; color: #fff !important; margin: 2px 0 2px; } .ao-todo-reasons { display: block; margin: 4px 0 0; padding: 0 0 0 1.1em; list-style: disc; color: #ccc !important; } .ao-todo-reasons > li { display: list-item; margin: 2px 0; } .ao-todo-reason-text { display: block; } .ao-todo-reason-detail { display: block; margin-top: 1px; color: #aaa !important; font-size: 0.92em; } .ao-todo-muted { color: #aaa !important; } .ao-todo-warn { color: #f0c674 !important; } .ao-caution { display: block; margin: 0 0 10px; padding: 8px 10px; border: 1px solid #f0c674; border-radius: 6px; background: rgba(240, 198, 116, 0.12); color: #f0c674 !important; } .ao-caution .ao-todo-headline { font-weight: 700; } .ao-caution .ao-todo-reasons { color: #e6d5a3 !important; padding-left: 1.1em; } button { display: inline-block; width: auto; background: #00bc8c; color: #fff !important; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 14px !important; } button[hidden] { display: none !important; } button.ao-secondary { background: #555; } button.ao-loadout-preview { white-space: normal; text-align: left; max-width: 100%; } button.ao-danger { background: #e74c3c; } button:disabled { opacity: 0.5; cursor: not-allowed; } label.ao-toggle { display: block; margin: 4px 0 4px 8px; color: #fff !important; } input[type="number"], input[type="text"], select, textarea.ao-textarea { width: 90px; margin-left: 6px; padding: 2px 4px; background: #2a2a2a; color: #fff !important; border: 1px solid #555; border-radius: 3px; caret-color: #fff; font-size: 14px !important; } textarea.ao-textarea { display: block; width: calc(100% - 8px); min-height: 72px; margin: 6px 0 6px 8px; resize: vertical; } select { width: auto; min-width: 120px; } input[type="checkbox"] { margin-right: 6px; accent-color: #00bc8c; } .ao-notify { display: block; margin: 0 0 12px; padding: 10px 12px; background: #222; border: 1px solid #333; border-radius: 8px; } .ao-switch { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin: 0; cursor: pointer; color: #fff !important; } .ao-switch-copy { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; } .ao-switch-title { font-weight: 600; color: #fff !important; } .ao-switch-hint { color: #aaa !important; font-size: 0.88em !important; line-height: 1.4; } .ao-switch-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .ao-switch-track { position: relative; flex: 0 0 auto; width: 44px; height: 24px; border-radius: 999px; background: #3a3a3a; box-shadow: inset 0 0 0 1px #555; transition: background 0.16s ease, box-shadow 0.16s ease; } .ao-switch-knob { position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); transition: transform 0.16s ease; } .ao-switch-input:checked + .ao-switch-track { background: #00bc8c; box-shadow: inset 0 0 0 1px #00bc8c; } .ao-switch-input:checked + .ao-switch-track .ao-switch-knob { transform: translateX(20px); } .ao-switch-input:focus-visible + .ao-switch-track { outline: 2px solid #00bc8c; outline-offset: 3px; } .ao-notify-types { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #333; } .ao-notify-types[data-off] { opacity: 0.45; pointer-events: none; } .ao-switch-sm .ao-switch-title { font-weight: 500; font-size: 0.92em !important; } .ao-switch-sm .ao-switch-hint { font-size: 0.8em !important; } .ao-switch-sm .ao-switch-track { width: 36px; height: 20px; } .ao-switch-sm .ao-switch-knob { width: 16px; height: 16px; } .ao-switch-sm .ao-switch-input:checked + .ao-switch-track .ao-switch-knob { transform: translateX(16px); } details { display: block; width: 100%; } details.ao-advanced { margin-top: 14px; border-top: 1px solid #333; padding-top: 10px; } details.ao-advanced > summary { cursor: pointer; color: #00bc8c !important; font-weight: bold; list-style: none; } details.ao-advanced > summary::-webkit-details-marker { display: none; } details.ao-advanced > summary::before { content: '▸ '; } details.ao-advanced[open] > summary::before { content: '▾ '; } details > summary { color: #aaa !important; cursor: pointer; } .ao-hydrate { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; padding: 8px 10px; background: #222; border: 1px solid #00bc8c55; border-radius: 4px; color: #ccc !important; font-size: 0.92em !important; } .ao-spinner { width: 14px; height: 14px; border: 2px solid #00bc8c44; border-top-color: #00bc8c; border-radius: 50%; animation: ao-spin 0.7s linear infinite; flex: 0 0 auto; } .ao-skel { display: block; height: 12px; margin: 8px 0; border-radius: 4px; background: linear-gradient(90deg, #2a2a2a 25%, #333 37%, #2a2a2a 63%); background-size: 400% 100%; animation: ao-skel 1.2s ease-in-out infinite; } @keyframes ao-spin { to { transform: rotate(360deg); } } @keyframes ao-skel { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } } @media (prefers-reduced-motion: reduce) { .ao-spinner, .ao-skel { animation: none; } .ao-switch-track, .ao-switch-knob { transition: none; } } `; } function buildModalShadowCss() { return buildPanelShadowCss("modal"); } function buildInlineShadowCss() { return buildPanelShadowCss("inline"); } function formatSigned(value) { return value < 0 ? `−${Math.abs(value)}` : `+${value}`; } function formatPct(value) { const pct = value * 100; const abs = Math.abs(pct); const rounded = Number.isSafeInteger(abs) ? String(abs) : abs.toFixed(1); return `${pct < 0 ? "−" : "+"}${rounded}%`; } function describeNumericEffect(type, value) { switch (type) { case ArtifactEffectType.SteamQuests: return { effect: `${formatSigned(value)} Steam Quests ARP` }; case ArtifactEffectType.WatchTwitch: { const cap = BASE_ACTIVITY.watchTwitchBasePerDay + value; return { effect: `${formatSigned(value)} Watch Twitch ARP`, detail: `Raises the daily Twitch cap to ${cap} (1 ARP/min).` }; } case ArtifactEffectType.DailyCalendar: return { effect: `${formatSigned(value)} Daily Calendar ARP` }; case ArtifactEffectType.TimeOnSite: return { effect: `${formatSigned(value)} Time on Site ARP` }; case ArtifactEffectType.DiscordPoll: return { effect: `${formatSigned(value)} Discord Poll ARP` }; case ArtifactEffectType.MarketDiscountPct: return { effect: `${Math.round(Math.abs(value) * 100)}% Game Vault / marketplace discount` }; case ArtifactEffectType.AllArpPct: return { effect: `${formatPct(value)} All ARP`, detail: value > 0 ? "Multiplies ARP from activities and claims while equipped." : "Reduces All ARP while this is equipped." }; case ArtifactEffectType.CommunityPlaytimePct: return { effect: `${formatPct(value)} Community Event playtime` }; default: return { effect: "No ARP bonus" }; } } function describeDefinitionEffect(definition, tier) { const raw = definition.effects[tier]; if (definition.effectType === ArtifactEffectType.UsernameColor) return { effect: typeof raw === "string" && raw.length > 0 ? `Username color: ${raw}` : "Username color" }; if (definition.effectType === ArtifactEffectType.None) return { effect: "No ARP bonus" }; if (typeof raw !== "number") return { effect: "No ARP bonus" }; return describeNumericEffect(definition.effectType, raw); } function describeSetEffects(set) { return set.effects.map((effect) => { if (effect.unit === "cosmetic") return "username color"; return describeNumericEffect(effect.type, effect.value).effect; }).join(", "); } function artifactSetForFamily(familyId) { return ARTIFACT_SETS.find((set) => set.unconfirmed !== true && set.memberIds.includes(familyId)); } function artifactTipCopy(definition, tier, displayName) { const described = describeDefinitionEffect(definition, tier); const set = artifactSetForFamily(definition.id); const copy = { title: displayName, meta: `${definition.category} · ${TIER_LABELS[tier]}`, effect: described.effect }; if (described.detail) copy.detail = described.detail; if (set) copy.setBonus = `Set: ${set.name} — ${describeSetEffects(set)} when all 3 are equipped`; return copy; } function artifactTipSpan(copy, visibleName) { const detailAttribute = copy.detail ? ` data-tip-detail="${escapeHtml(copy.detail)}"` : ""; const bonusAttribute = copy.setBonus ? ` data-tip-set="${escapeHtml(copy.setBonus)}"` : ""; const aria = [ copy.title, copy.meta, copy.effect, copy.detail, copy.setBonus ].filter((part) => Boolean(part)).join(". "); return `${escapeHtml(visibleName)}`; } function nextArtifactNameMatch(text, entries) { let match; for (const entry of entries) { const index = text.indexOf(entry.name); if (index === -1) continue; if (!match || index < match.index || index === match.index && entry.name.length > match.entry.name.length) match = { index, entry }; } return match; } function wrapArtifactNames(text) { const entries = listArtifactNameEntries(); let remaining = text; let html = ""; while (remaining.length > 0) { const match = nextArtifactNameMatch(remaining, entries); if (!match) { html += escapeHtml(remaining); break; } html += escapeHtml(remaining.slice(0, match.index)); const name = remaining.slice(match.index, match.index + match.entry.name.length); html += artifactTipSpan(artifactTipCopy(match.entry.definition, match.entry.tier, name), name); remaining = remaining.slice(match.index + match.entry.name.length); } return html; } function ensureArtifactTipFloat() { ensureOptimizerStyles(); let tip = document.querySelector(`#${ARTIFACT_TIP_ID}`); if (tip) return tip; tip = document.createElement("div"); tip.id = ARTIFACT_TIP_ID; tip.setAttribute("role", "tooltip"); tip.hidden = true; document.body.append(tip); return tip; } function hideArtifactTip() { const tip = document.querySelector(`#${ARTIFACT_TIP_ID}`); if (!tip) return; tip.hidden = true; tip.replaceChildren(); } function renderTipFloat(copy) { const detail = copy.detail ? `