local AIO = AIO or require("AIO") local L = require("MythicPlus.Mythic_Locale") MythicHandlers = AIO.AddHandlers("AIO_Mythic", {}) local function GetLocalizedText(player, category, key) local localeIndex = player:GetDbLocaleIndex() return L:Text(category, key, localeIndex) end function MythicHandlers.RequestLocaleData(player, category) if category and L[category] and type(L[category]) == "table" then AIO.Handle(player, "AIO_Mythic", "ReceiveLocaleData", category, L[category]) end end local AFFIX_EXCLUDE_CREATURES = { 29830, -- Living Mojo, Gundrak Boss Drakkari Colossus // would break its boss script 24849 -- Proto-Drake Rider, Utgarde Keep } local MYTHIC_LOOT_BRACKETS = { ["low_tier"] = {1, 2, 3}, ["mid_tier"] = {4, 5, 6, 7}, ["high_tier"] = {8, 9, 10, 11, 12}, ["endgame"] = {15, 16, 17, 18, 19, 20}, ["pets"] = {5, 6, 7, 8, 9}, ["all"] = "all" } local MythicRewardConfig = { pets = true, mounts = true, equipment = false, spells = false, } local VAULT_LOOT_BRACKETS = { ["vault_low"] = {1, 2, 3, 4, 5}, ["vault_mid"] = {6, 7, 8, 9, 10}, ["vault_high"] = {11, 12, 13, 14, 15}, ["vault_legendary"] = {16, 17, 18, 19, 20}, ["all"] = "all" } local VaultGenerationTracker = { lastProcessedDate = nil, eventId = nil } local VaultItemCache = {} local VAULT_GAMEOBJECT_ID = 900000 local CLASS_ARMOR_TYPES = { [1] = 4, -- Warrior -> Plate [2] = 4, -- Paladin -> Plate [3] = 3, -- Hunter -> Mail [4] = 2, -- Rogue -> Leather [5] = 1, -- Priest -> Cloth [6] = 4, -- Death Knight -> Plate [7] = 3, -- Shaman -> Mail [8] = 1, -- Mage -> Cloth [9] = 1, -- Warlock -> Cloth [11] = 2, -- Druid -> Leather } local WEAPON_PROFICIENCY = { [176] = {[3] = true, [4] = true, [1] = true}, -- Thrown [172] = {[6] = true, [3] = true, [2] = true, [7] = true, [1] = true}, -- Two-Handed Axes [43] = {[6] = true, [3] = true, [8] = true, [2] = true, [4] = true, [9] = true, [1] = true}, -- Swords [44] = {[6] = true, [3] = true, [2] = true, [7] = true, [1] = true}, -- Axes [136] = {[11] = true, [3] = true, [8] = true, [5] = true, [7] = true, [9] = true, [1] = true}, -- Staves [160] = {[6] = true, [11] = true, [2] = true, [7] = true, [1] = true}, -- Two-Handed Maces [46] = {[3] = true, [4] = true, [1] = true}, -- Guns [229] = {[6] = true, [3] = true, [2] = true, [1] = true}, -- Polearms [473] = {[11] = true, [3] = true, [4] = true, [7] = true, [1] = true}, -- Fist Weapons [54] = {[6] = true, [11] = true, [2] = true, [5] = true, [4] = true, [7] = true, [1] = true}, -- Maces [226] = {[3] = true, [4] = true, [1] = true}, -- Crossbows [45] = {[3] = true, [4] = true, [1] = true}, -- Bows [55] = {[6] = true, [3] = true, [2] = true, [1] = true}, -- Two-Handed Swords [228] = {[8] = true, [5] = true, [9] = true}, -- Wands [173] = {[11] = true, [3] = true, [8] = true, [5] = true, [4] = true, [7] = true, [9] = true, [1] = true}, -- Daggers } local WEAPON_SUBCLASS_TO_SKILL = { [0] = 44, [1] = 172, [2] = 45, [3] = 46, [4] = 54, [5] = 160, [6] = 229, [7] = 43, [8] = 55, [10] = 136, [13] = 473, [15] = 173, [16] = 176, [18] = 226, [19] = 228, } local BossNameCache = { ["enUS"] = {}, ["deDE"] = {}, ["esES"] = {}, ["esMX"] = {}, ["frFR"] = {}, ["koKR"] = {}, ["ruRU"] = {}, ["zhCN"] = {}, ["zhTW"] = {} } local function LoadBossNames() local bossEntries = {} local entriesStr = "" for mapId, data in pairs(MythicBosses) do if data.bosses then for _, entry in ipairs(data.bosses) do if not bossEntries[entry] then bossEntries[entry] = true if entriesStr ~= "" then entriesStr = entriesStr .. "," end entriesStr = entriesStr .. entry end end end end if entriesStr == "" then print("[Mythic+] No boss entries found to load names for") return end local query = WorldDBQuery("SELECT entry, name FROM creature_template WHERE entry IN (" .. entriesStr .. ")") local count = 0 if query then repeat local entry = query:GetUInt32(0) local name = query:GetString(1) BossNameCache["enUS"][entry] = name count = count + 1 until not query:NextRow() end local locales = {"deDE", "esES", "esMX", "frFR", "koKR", "ruRU", "zhCN", "zhTW"} for _, locale in ipairs(locales) do local localeQuery = WorldDBQuery( "SELECT entry, Name FROM creature_template_locale WHERE entry IN (" .. entriesStr .. ") AND locale = '" .. locale .. "'" ) if localeQuery then local localeCount = 0 repeat local entry = localeQuery:GetUInt32(0) local name = localeQuery:GetString(1) BossNameCache[locale][entry] = name localeCount = localeCount + 1 until not localeQuery:NextRow() end end end local function GetLocalizedBossNames(player, mapId) if not MythicBosses[mapId] or not MythicBosses[mapId].bosses then return {} end local localeIndex = player:GetDbLocaleIndex() local localeMap = { [0] = "enUS", -- Default English [1] = "koKR", -- Korean [2] = "frFR", -- French [3] = "deDE", -- German [4] = "zhCN", -- Chinese (China) [5] = "zhTW", -- Chinese (Taiwan) [6] = "esES", -- Spanish (Spain) [7] = "esMX", -- Spanish (Mexico) [8] = "ruRU" -- Russian } local locale = localeMap[localeIndex] or "enUS" local bossNames = {} for _, bossId in ipairs(MythicBosses[mapId].bosses) do local name = BossNameCache[locale][bossId] or BossNameCache["enUS"][bossId] or ("Boss #" .. bossId) table.insert(bossNames, name) end return bossNames end MythicBosses = { [574] = { -- Utgarde Keep bosses = {23953, 24200, 24201, 23954}, final = 23954, timer = 1500, enemies = 45}, [575] = { -- Utgarde Pinnacle bosses = {26668, 26687, 26693, 26861}, final = 26861, timer = 1500, enemies = 52}, [576] = { -- The Nexus bosses = {26731, 26763, 26794, 26723}, final = 26723, timer = 1500, enemies = 38}, [599] = { -- Halls of Stone bosses = {27977, 27975, 27978}, final = 27978, timer = 1500, enemies = 41}, [600] = { -- Drak'Tharon Keep bosses = {26630, 26631, 27483, 26632}, final = 26632, timer = 1500, enemies = 48}, [601] = { -- Azjol-Nerub bosses = {28684, 28921, 29120}, final = 29120, timer = 1500, enemies = 33}, [602] = { -- Halls of Lightning bosses = {28586, 28587, 28546, 28923}, final = 28923, timer = 1500, enemies = 42}, [604] = { -- Gundrak bosses = {29304, 29573, 29305, 29932, 29306}, final = 29306, timer = 1500, enemies = 55}, [608] = { -- The Violet Hold bosses = {31134}, final = 31134, timer = 1500, enemies = 0}, [619] = { -- Ahn'kahet: The Old Kingdom bosses = {29309, 29308, 29310, 30258, 29311}, final = 29311, timer = 1500, enemies = 46}, [578] = { -- The Oculus bosses = {27654, 27447, 27655, 27656}, final = 27656, timer = 1500, enemies = 35}, [595] = { -- The Culling of Stratholme bosses = {26529, 26530, 26532, 26533}, final = 26533, timer = 1500, enemies = 58}, [650] = { -- Trial of the Champion bosses = {35451}, final = 35451, timer = 1500, enemies = 0}, [632] = { -- Forge of Souls bosses = {36497, 36502}, final = 36502, timer = 1500, enemies = 32}, [658] = { -- Pit of Saron bosses = {36494, 36476, 36658}, final = 36658, timer = 1500, enemies = 44}, [668] = { -- Halls of Reflection bosses = {38112, 38113, 36954}, final = 36954, timer = 1500, enemies = 0} } LoadBossNames() local DungeonNames = { [574] = "Utgarde Keep", [575] = "Utgarde Pinnacle", [576] = "The Nexus", [578] = "The Oculus", [595] = "The Culling of Stratholme", [599] = "Halls of Stone", [600] = "Drak'Tharon Keep", [601] = "Azjol-Nerub", [602] = "Halls of Lightning", [604] = "Gundrak", [608] = "The Violet Hold", [619] = "Ahn'kahet: The Old Kingdom", [632] = "The Forge of Souls", [650] = "Trial of the Champion", [658] = "Pit of Saron", [668] = "Halls of Reflection" } local function GetLocalizedDungeonName(player, mapId) local nameKey = DungeonNames[mapId] if nameKey then return GetLocalizedText(player, "Dungeons", nameKey) else return nameKey or ("Unknown (" .. mapId .. ")") end end local mythicDungeonIds = { [574]=true, [575]=true, [576]=true, [578]=true, [595]=true, [599]=true, [600]=true, [601]=true, [602]=true, [604]=true, [608]=true, [619]=true, [632]=true, [650]=true, [658]=true, [668]=true } VaultLootTable = {} PlayerKeysCache = {} PlayerVaultCache = {} local PlayerRatingCache = {} local PlayerNamesCache = {} local WeeklyAffixesCache = nil local ActiveRunsCache = {} local LeaderboardCache = { topThree = {}, dungeonTop = {}, lastUpdate = 0 } local LEADERBOARD_CACHE_DURATION = 900 -- 15 minutes local function LoadPlayerCache(guid) if PlayerRatingCache[guid] then return end CharDBQueryAsync(string.format([[ SELECT total_points, total_runs, completed_runs, out_of_time_runs, too_many_death_runs, `574`, `575`, `576`, `578`, `595`, `599`, `600`, `601`, `602`, `604`, `608`, `619`, `632`, `650`, `658`, `668` FROM character_mythic_rating WHERE guid = %d ]], guid), function(result) if result then PlayerRatingCache[guid] = { total_points = result:GetDouble(0), total_runs = result:GetUInt32(1), completed_runs = result:GetUInt32(2), out_of_time_runs = result:GetUInt32(3), too_many_death_runs = result:GetUInt32(4), [574] = result:GetUInt32(5), [575] = result:GetUInt32(6), [576] = result:GetUInt32(7), [578] = result:GetUInt32(8), [595] = result:GetUInt32(9), [599] = result:GetUInt32(10), [600] = result:GetUInt32(11), [601] = result:GetUInt32(12), [602] = result:GetUInt32(13), [604] = result:GetUInt32(14), [608] = result:GetUInt32(15), [619] = result:GetUInt32(16), [632] = result:GetUInt32(17), [650] = result:GetUInt32(18), [658] = result:GetUInt32(19), [668] = result:GetUInt32(20) } else PlayerRatingCache[guid] = { total_points = 0, total_runs = 0, completed_runs = 0, out_of_time_runs = 0, too_many_death_runs = 0, [574] = 0, [575] = 0, [576] = 0, [578] = 0, [595] = 0, [599] = 0, [600] = 0, [601] = 0, [602] = 0, [604] = 0, [608] = 0, [619] = 0, [632] = 0, [650] = 0, [658] = 0, [668] = 0 } end end) if not PlayerKeysCache[guid] then local keyQuery = CharDBQuery(string.format("SELECT mapId, tier FROM character_mythic_keys WHERE guid = %d", guid)) if keyQuery then PlayerKeysCache[guid] = { mapId = keyQuery:GetUInt32(0), tier = keyQuery:GetUInt32(1) } else PlayerKeysCache[guid] = nil end end end local function SavePlayerRatingCache(guid) local cache = PlayerRatingCache[guid] if not cache then return end CharDBQuery(string.format([[ INSERT INTO character_mythic_rating (guid, total_runs, total_points, completed_runs, out_of_time_runs, too_many_death_runs, `574`, `575`, `576`, `578`, `595`, `599`, `600`, `601`, `602`, `604`, `608`, `619`, `632`, `650`, `658`, `668`, last_updated) VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, FROM_UNIXTIME(%d)) ON DUPLICATE KEY UPDATE total_runs = %d, total_points = %d, completed_runs = %d, out_of_time_runs = %d, too_many_death_runs = %d, `574` = %d, `575` = %d, `576` = %d, `578` = %d, `595` = %d, `599` = %d, `600` = %d, `601` = %d, `602` = %d, `604` = %d, `608` = %d, `619` = %d, `632` = %d, `650` = %d, `658` = %d, `668` = %d, last_updated = FROM_UNIXTIME(%d) ]], guid, cache.total_runs, cache.total_points, cache.completed_runs, cache.out_of_time_runs, cache.too_many_death_runs, cache[574], cache[575], cache[576], cache[578], cache[595], cache[599], cache[600], cache[601], cache[602], cache[604], cache[608], cache[619], cache[632], cache[650], cache[658], cache[668], os.time(), cache.total_runs, cache.total_points, cache.completed_runs, cache.out_of_time_runs, cache.too_many_death_runs, cache[574], cache[575], cache[576], cache[578], cache[595], cache[599], cache[600], cache[601], cache[602], cache[604], cache[608], cache[619], cache[632], cache[650], cache[658], cache[668], os.time())) end local function UpdateLeaderboardCache() local now = os.time() if now - LeaderboardCache.lastUpdate < LEADERBOARD_CACHE_DURATION then return end local top3Query = CharDBQuery([[ SELECT guid, total_points FROM character_mythic_rating ORDER BY total_points DESC LIMIT 3 ]]) LeaderboardCache.topThree = {} if top3Query then repeat local guid = top3Query:GetUInt32(0) local points = top3Query:GetDouble(1) local name = PlayerNamesCache[guid] if not name then local nameQ = CharDBQuery("SELECT name FROM characters WHERE guid = " .. guid) if nameQ then name = nameQ:GetString(0) PlayerNamesCache[guid] = name else name = "Unknown" end end table.insert(LeaderboardCache.topThree, { name = name, points = points }) until not top3Query:NextRow() end LeaderboardCache.dungeonTop = {} local dungeonIds = {574, 575, 576, 578, 595, 599, 600, 601, 602, 604, 608, 619, 632, 650, 658, 668} for _, dungeonId in ipairs(dungeonIds) do local scoreQuery = CharDBQuery("SELECT guid, `" .. dungeonId .. "` FROM character_mythic_rating ORDER BY `" .. dungeonId .. "` DESC LIMIT 1") if scoreQuery and scoreQuery:GetUInt32(1) > 0 then local guid = scoreQuery:GetUInt32(0) local score = scoreQuery:GetUInt32(1) local name = PlayerNamesCache[guid] if not name then local nameQ = CharDBQuery("SELECT name FROM characters WHERE guid = " .. guid) if nameQ then name = nameQ:GetString(0) PlayerNamesCache[guid] = name else name = "Unknown" end end local bossData = MythicBosses[dungeonId] local timerLimit = bossData and bossData.timer or 1500 local keyQuery = CharDBQuery(string.format([[ SELECT h.tier, h.member_1, h.member_2, h.member_3, h.member_4, h.member_5 FROM character_mythic_history h WHERE h.mapId = %d AND h.completed = 1 AND h.duration <= %d AND h.duration > 0 ORDER BY h.tier DESC LIMIT 1 ]], dungeonId, timerLimit)) local highestKey = 0 local keyHolderNames = {} if keyQuery then highestKey = keyQuery:GetUInt32(0) or 0 for i = 1, 5 do local memberGuid = keyQuery:GetUInt32(i) if memberGuid and memberGuid > 0 then local memberName = PlayerNamesCache[memberGuid] if not memberName then local nameQ = CharDBQuery("SELECT name FROM characters WHERE guid = " .. memberGuid) if nameQ then memberName = nameQ:GetString(0) PlayerNamesCache[memberGuid] = memberName else memberName = "Unknown" end end table.insert(keyHolderNames, memberName) end end end LeaderboardCache.dungeonTop[tostring(dungeonId)] = { name = name, score = score, highestKey = highestKey, keyHolderNames = keyHolderNames } end end LeaderboardCache.lastUpdate = now end local PEDESTAL_NPC_ENTRY = 900001 local WEEKLY_AFFIX_POOL = { { spell = 8599, name = "Enrage" }, { spell = {48441, 61301}, name = "Rejuvenating" }, { spell = 871, name = "Turtling" }, { spell = {57662, 57621, 58738, 8515}, name = "Shamanism" }, { spell = {43015, 43008, 43046, 57531, 12043}, name = "Magus" }, { spell = {48161, 48066, 6346, 48168, 15286}, name = "Priest Empowered" }, { spell = {47893, 50589}, name = "Demonism" }, { spell = 53201, name = "Falling Stars" } } local function GetCurrentMythicResetDate() local now = os.time() local t = os.date("*t", now) local daysToSubtract = (t.wday - 4) % 7 t.day = t.day - daysToSubtract t.hour = 6 t.min = 0 t.sec = 0 return os.date("%Y-%m-%d", os.time(t)) end local function LoadOrRollWeeklyAffixes() if WeeklyAffixesCache then return end local resetDate = GetCurrentMythicResetDate() local result = CharDBQuery(string.format("SELECT affix1, affix2, affix3 FROM character_mythic_weekly_affixes WHERE week_start = '%s'", resetDate)) if result then local names = { result:GetString(0), result:GetString(1), result:GetString(2) } WeeklyAffixesCache = {} for _, name in ipairs(names) do for _, affix in ipairs(WEEKLY_AFFIX_POOL) do if affix.name == name then table.insert(WeeklyAffixesCache, affix) break end end end else local shuffled = {} for i = 1, #WEEKLY_AFFIX_POOL do shuffled[i] = WEEKLY_AFFIX_POOL[i] end for i = #shuffled, 2, -1 do local j = math.random(i) shuffled[i], shuffled[j] = shuffled[j], shuffled[i] end local affix1, affix2, affix3 = shuffled[1], shuffled[2], shuffled[3] WeeklyAffixesCache = { affix1, affix2, affix3 } CharDBQuery(string.format([[ INSERT INTO character_mythic_weekly_affixes (week_start, affix1, affix2, affix3) VALUES ('%s', '%s', '%s', '%s') ]], resetDate, affix1.name, affix2.name, affix3.name)) end end LoadOrRollWeeklyAffixes() local penaltyPerDeath = 10 local fmt, floor = string.format, math.floor if MYTHIC_BOSS_KILL_TRACKER == nil then MYTHIC_BOSS_KILL_TRACKER = {} end if MYTHIC_FLAG_TABLE == nil then MYTHIC_FLAG_TABLE = {} end if MYTHIC_AFFIXES_TABLE == nil then MYTHIC_AFFIXES_TABLE = {} end if MYTHIC_LOOP_HANDLERS == nil then MYTHIC_LOOP_HANDLERS = {} end if MYTHIC_REWARD_CHANCE_TABLE == nil then MYTHIC_REWARD_CHANCE_TABLE = {} end if MYTHIC_ENEMY_FORCES_TRACKER == nil then MYTHIC_ENEMY_FORCES_TRACKER = {} end local MythicLootTable = {} local function LoadMythicLootTable() MythicLootTable = {} local q = WorldDBQuery("SELECT itemid, itemname, amount, type, faction, loot_bracket, chancePercent, additionalID, additionalType FROM world_mythic_loot") if q then repeat table.insert(MythicLootTable, { itemid = q:GetUInt32(0), itemname = q:GetString(1), amount = q:GetUInt32(2), type = q:GetString(3), faction = q:GetString(4), loot_bracket = q:GetString(5), chancePercent = q:GetFloat(6), additionalID = q:IsNull(7) and nil or q:GetUInt32(7), additionalType = q:IsNull(8) and nil or q:GetString(8), }) until not q:NextRow() end print("[Mythic+] Loaded " .. #MythicLootTable .. " mythic+ loot entries.") end local function LoadVaultLootTable() local q = WorldDBQuery("SELECT itemid, loot_bracket, chancePercent, faction FROM world_vault_loot") if q then repeat table.insert(VaultLootTable, { itemid = q:GetUInt32(0), loot_bracket = q:GetString(1), chancePercent = q:GetFloat(2), faction = q:GetString(3) }) until not q:NextRow() end print("[Mythic+] Loaded " .. #VaultLootTable .. " vault loot entries.") end LoadMythicLootTable() LoadVaultLootTable() local function CacheItemTemplate(itemId) if VaultItemCache[itemId] then return VaultItemCache[itemId] end local item = GetItemTemplate(itemId) if not item then VaultItemCache[itemId] = nil return nil end VaultItemCache[itemId] = { allowableClass = item:GetAllowableClass(), allowableRace = item:GetAllowableRace(), class = item:GetClass(), subClass = item:GetSubClass(), inventoryType = item:GetInventoryType(), name = item:GetName(), itemLevel = item:GetItemLevel() } return VaultItemCache[itemId] end local function HasClassFlag(allowableClass, playerClass) if allowableClass == 0 then return true end local classFlags = { [1] = 1, -- Warrior [2] = 2, -- Paladin [3] = 4, -- Hunter [4] = 8, -- Rogue [5] = 16, -- Priest [6] = 32, -- Death Knight [7] = 64, -- Shaman [8] = 128, -- Mage [9] = 256, -- Warlock [11] = 1024 -- Druid } local playerFlag = classFlags[playerClass] if not playerFlag then return false end return math.floor(allowableClass / playerFlag) % 2 == 1 end local function HasRaceFlag(allowableRace, playerRace) if allowableRace == 0 then return true end local raceFlags = { [1] = 1, -- Human [2] = 2, -- Orc [3] = 4, -- Dwarf [4] = 8, -- Night Elf [5] = 16, -- Undead [6] = 32, -- Tauren [7] = 64, -- Gnome [8] = 128, -- Troll [10] = 512, -- Blood Elf [11] = 1024 -- Draenei } local playerFlag = raceFlags[playerRace] if not playerFlag then return false end return math.floor(allowableRace / playerFlag) % 2 == 1 end local function CanPlayerUseItem(player, itemId) local itemData = CacheItemTemplate(itemId) if not itemData then return false end local playerClass = player:GetClass() local playerRace = player:GetRace() if not HasClassFlag(itemData.allowableClass, playerClass) then return false end if not HasRaceFlag(itemData.allowableRace, playerRace) then return false end if itemData.class == 4 then local playerArmorType = CLASS_ARMOR_TYPES[playerClass] if playerArmorType and itemData.subClass > 0 then if itemData.subClass > playerArmorType then return false end end end if itemData.class == 2 then local skillId = WEAPON_SUBCLASS_TO_SKILL[itemData.subClass] if skillId and WEAPON_PROFICIENCY[skillId] then if not WEAPON_PROFICIENCY[skillId][playerClass] then return false end end end return true end local function GetCurrentVaultWeek() local now = os.time() local t = os.date("*t", now) -- Find last Wednesday 8 AM local daysToSubtract = (t.wday + 3) % 7 if t.wday == 4 and t.hour < 8 then daysToSubtract = daysToSubtract + 7 end t.day = t.day - daysToSubtract t.hour = 8 t.min = 0 t.sec = 0 return os.date("%Y-%m-%d", os.time(t)) end function LoadPlayerVaultCache(guid) local currentWeek = GetCurrentVaultWeek() local query = CharDBQuery(string.format([[ SELECT highest_tier_1, highest_tier_2, highest_tier_3, successful_runs, item_1_id, item_2_id, item_3_id, items_generated, has_collected, can_collect, week_start FROM character_mythic_vault WHERE guid = %d AND week_start = '%s' ]], guid, currentWeek)) if not query or (query and query:GetUInt32(9) == 0) then query = CharDBQuery(string.format([[ SELECT highest_tier_1, highest_tier_2, highest_tier_3, successful_runs, item_1_id, item_2_id, item_3_id, items_generated, has_collected, can_collect, week_start FROM character_mythic_vault WHERE guid = %d AND can_collect = 1 AND has_collected = 0 ORDER BY week_start DESC LIMIT 1 ]], guid)) end if query then local weekStart = query:GetString(10) or currentWeek PlayerVaultCache[guid] = { week_start = weekStart, highest_tier_1 = query:IsNull(0) and nil or query:GetUInt32(0), highest_tier_2 = query:IsNull(1) and nil or query:GetUInt32(1), highest_tier_3 = query:IsNull(2) and nil or query:GetUInt32(2), successful_runs = query:GetUInt32(3), item_1_id = query:IsNull(4) and nil or query:GetUInt32(4), item_2_id = query:IsNull(5) and nil or query:GetUInt32(5), item_3_id = query:IsNull(6) and nil or query:GetUInt32(6), items_generated = query:GetUInt32(7) == 1, has_collected = query:GetUInt32(8) == 1, can_collect = query:GetUInt32(9) == 1 } else PlayerVaultCache[guid] = { week_start = currentWeek, highest_tier_1 = nil, highest_tier_2 = nil, highest_tier_3 = nil, successful_runs = 0, item_1_id = nil, item_2_id = nil, item_3_id = nil, items_generated = false, has_collected = false, can_collect = false } end end function SavePlayerVaultCache(guid) local cache = PlayerVaultCache[guid] if not cache then return end CharDBQuery(string.format([[ INSERT INTO character_mythic_vault (guid, week_start, highest_tier_1, highest_tier_2, highest_tier_3, successful_runs, item_1_id, item_2_id, item_3_id, items_generated, has_collected, can_collect) VALUES (%d, '%s', %s, %s, %s, %d, %s, %s, %s, %d, %d, %d) ON DUPLICATE KEY UPDATE highest_tier_1 = %s, highest_tier_2 = %s, highest_tier_3 = %s, successful_runs = %d, item_1_id = %s, item_2_id = %s, item_3_id = %s, items_generated = %d, has_collected = %d, can_collect = %d ]], guid, cache.week_start, cache.highest_tier_1 or "NULL", cache.highest_tier_2 or "NULL", cache.highest_tier_3 or "NULL", cache.successful_runs, cache.item_1_id or "NULL", cache.item_2_id or "NULL", cache.item_3_id or "NULL", cache.items_generated and 1 or 0, cache.has_collected and 1 or 0, cache.can_collect and 1 or 0, cache.highest_tier_1 or "NULL", cache.highest_tier_2 or "NULL", cache.highest_tier_3 or "NULL", cache.successful_runs, cache.item_1_id or "NULL", cache.item_2_id or "NULL", cache.item_3_id or "NULL", cache.items_generated and 1 or 0, cache.has_collected and 1 or 0, cache.can_collect and 1 or 0)) end local function GetEligibleVaultLoot(player, tier, faction) local eligible = {} for _, loot in ipairs(VaultLootTable) do if loot.faction ~= "N" and loot.faction ~= faction then goto continue end if not isVaultTierEligibleForBracket(loot.loot_bracket, tier) then goto continue end if not CanPlayerUseItem(player, loot.itemid) then goto continue end table.insert(eligible, loot) ::continue:: end return eligible end function GenerateVaultItemsForPlayer(player) local guid = player:GetGUIDLow() local cache = PlayerVaultCache[guid] if not cache or cache.items_generated or cache.successful_runs == 0 then return end if not player or not player:IsInWorld() then return end local faction = player:GetTeam() == 67 and "A" or (player:GetTeam() == 469 and "H" or "N") local tiers = {cache.highest_tier_1, cache.highest_tier_2, cache.highest_tier_3} local usedItems = {} for i, tier in ipairs(tiers) do if tier then local eligible = GetEligibleVaultLoot(player, tier, faction) if #eligible > 0 then local availableItems = {} for _, loot in ipairs(eligible) do if not usedItems[loot.itemid] then table.insert(availableItems, loot) end end if #availableItems == 0 then availableItems = eligible end local totalWeight = 0 for _, loot in ipairs(availableItems) do totalWeight = totalWeight + loot.chancePercent end if totalWeight > 0 then local roll = math.random() * totalWeight local currentWeight = 0 for _, loot in ipairs(availableItems) do currentWeight = currentWeight + loot.chancePercent if roll <= currentWeight then usedItems[loot.itemid] = true if i == 1 then cache.item_1_id = loot.itemid elseif i == 2 then cache.item_2_id = loot.itemid elseif i == 3 then cache.item_3_id = loot.itemid end break end end end end end end cache.items_generated = true cache.can_collect = (cache.item_1_id or cache.item_2_id or cache.item_3_id) and true or false SavePlayerVaultCache(guid) end local function UpdateVaultProgress(player, tier, wasSuccessful) local guid = player:GetGUIDLow() if not PlayerVaultCache[guid] then LoadPlayerVaultCache(guid) end local cache = PlayerVaultCache[guid] if wasSuccessful then cache.successful_runs = cache.successful_runs + 1 local tiers = {cache.highest_tier_1, cache.highest_tier_2, cache.highest_tier_3} table.insert(tiers, tier) table.sort(tiers, function(a, b) return (a or 0) > (b or 0) end) cache.highest_tier_1 = tiers[1] cache.highest_tier_2 = tiers[2] cache.highest_tier_3 = tiers[3] SavePlayerVaultCache(guid) end end function WeeklyVaultInteract(event, go, player) local guid = player:GetGUIDLow() if not PlayerVaultCache[guid] then LoadPlayerVaultCache(guid) end local cache = PlayerVaultCache[guid] if not cache.can_collect or cache.has_collected then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "No loot available in your vault this week.")) return end if not cache.items_generated then GenerateVaultItemsForPlayer(player) end local tiers = {cache.highest_tier_1, cache.highest_tier_2, cache.highest_tier_3} local items = {cache.item_1_id or 0, cache.item_2_id or 0, cache.item_3_id or 0} local itemLevels = {0, 0, 0} for i, itemId in ipairs(items) do if itemId and itemId > 0 then local itemData = CacheItemTemplate(itemId) itemLevels[i] = itemData and itemData.itemLevel or 0 end end AIO.Handle(player, "AIO_Mythic", "QueryItemData", items[1], items[2], items[3]) AIO.Handle(player, "AIO_Mythic", "ShowVaultGUI", items[1], items[2], items[3], tiers[1] or 0, tiers[2] or 0, tiers[3] or 0, itemLevels[1], itemLevels[2], itemLevels[3]) local playerGUID = player:GetGUID() local goX, goY, goZ = go:GetX(), go:GetY(), go:GetZ() local goMapId = go:GetMapId() local proximityEventId = CreateLuaEvent(function() local p = GetPlayerByGUID(playerGUID) if not p then return false end if p:GetMapId() ~= goMapId then AIO.Handle(p, "AIO_Mythic", "CloseVaultGUI") return false end local pX, pY, pZ = p:GetX(), p:GetY(), p:GetZ() local distance = math.sqrt((pX - goX)^2 + (pY - goY)^2 + (pZ - goZ)^2) if distance > 6 then AIO.Handle(p, "AIO_Mythic", "CloseVaultGUI") return false end return true end, 1000, 0) end function MythicHandlers.SelectVaultItem(player, itemIndex) local guid = player:GetGUIDLow() local cache = PlayerVaultCache[guid] if not cache or not cache.can_collect or cache.has_collected then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "No loot available to collect.")) return end local itemId = nil if itemIndex == 1 then itemId = cache.item_1_id elseif itemIndex == 2 then itemId = cache.item_2_id elseif itemIndex == 3 then itemId = cache.item_3_id end if itemId then player:AddItem(itemId, 1) cache.has_collected = true SavePlayerVaultCache(guid) local itemData = CacheItemTemplate(itemId) local itemName = itemData and itemData.name or "Unknown Item" AIO.Handle(player, "AIO_Mythic", "UpdateVaultStatus", false) end end function MythicHandlers.RequestVaultStatus(player) if not player or not player:IsInWorld() then return end local guid = player:GetGUIDLow() if not PlayerVaultCache[guid] then LoadPlayerVaultCache(guid) end local cache = PlayerVaultCache[guid] if cache.successful_runs > 0 and not cache.items_generated and not cache.has_collected then GenerateVaultItemsForPlayer(player) end local hasLoot = cache.can_collect and not cache.has_collected AIO.Handle(player, "AIO_Mythic", "UpdateVaultStatus", hasLoot) end local function ProcessWeeklyVaultGeneration() local currentWeek = GetCurrentVaultWeek() local query = CharDBQuery(string.format([[ SELECT DISTINCT guid FROM character_mythic_vault WHERE week_start = '%s' AND successful_runs > 0 AND NOT items_generated ]], currentWeek)) if query then repeat local guid = query:GetUInt32(0) local player = GetPlayerByGUID(guid) if player then GenerateVaultItemsForPlayer(player) else if not PlayerVaultCache[guid] then LoadPlayerVaultCache(guid) end local cache = PlayerVaultCache[guid] if cache then cache.can_collect = true SavePlayerVaultCache(guid) end end until not query:NextRow() end end local function ScheduleVaultGeneration() if VaultGenerationTracker.eventId then RemoveEventById(VaultGenerationTracker.eventId) end local now = os.time() local t = os.date("*t", now) local nextWednesday = os.time(t) local daysUntilWednesday = (11 - t.wday) % 7 if daysUntilWednesday == 0 and t.hour >= 8 then daysUntilWednesday = 7 end nextWednesday = nextWednesday + (daysUntilWednesday * 24 * 60 * 60) local wednesdayTable = os.date("*t", nextWednesday) wednesdayTable.hour = 8 wednesdayTable.min = 0 wednesdayTable.sec = 0 local targetTime = os.time(wednesdayTable) local delayMs = (targetTime - now) * 1000 VaultGenerationTracker.eventId = CreateLuaEvent(function() local currentDate = os.date("%Y-%m-%d") local currentTime = os.date("*t") if currentTime.wday == 4 and currentTime.hour >= 8 and VaultGenerationTracker.lastProcessedDate ~= currentDate then VaultGenerationTracker.lastProcessedDate = currentDate ProcessWeeklyVaultGeneration() print("[Mythic+] Weekly vault generation processed for " .. currentDate) end ScheduleVaultGeneration() end, delayMs, 1) end local function CheckAndProcessVaultOnStartup() local now = os.time() local t = os.date("*t", now) local currentDate = os.date("%Y-%m-%d") if t.wday == 4 and t.hour >= 8 and VaultGenerationTracker.lastProcessedDate ~= currentDate then VaultGenerationTracker.lastProcessedDate = currentDate ProcessWeeklyVaultGeneration() print("[Mythic+] Weekly vault generation processed on startup for " .. currentDate) end ScheduleVaultGeneration() end local function GetAffixSet(tier) local affixes = {} local baseAffixes = math.min(tier, 4) for i = 1, baseAffixes do local affix = WeeklyAffixesCache[i] if affix then if type(affix.spell) == "table" then for _, s in ipairs(affix.spell) do table.insert(affixes, s) end else table.insert(affixes, affix.spell) end end end if tier >= 5 then table.insert(affixes, 58549) end return affixes end local function GetAffixNameSet(tier) local names = {} for i = 1, tier do local affix = WeeklyAffixesCache[i] if affix then table.insert(names, affix.name) end end return table.concat(names, ", ") end local function LeaveDungeonMap(event, player) local mapId = player:GetMapId() if not mythicDungeonIds[mapId] then AIO.Handle(player, "AIO_Mythic", "KillMythicTimerGUI") end end local function GetRandomMythicMapId() local ids = {574, 575, 576, 578, 595, 599, 600, 601, 602, 604, 608, 619, 632, 650, 658, 668} return ids[math.random(1, #ids)] end local function PlayerHasAnyKeystone(player) return player:HasItem(900100) end local function IsRunActive(instanceId) return MYTHIC_FLAG_TABLE[instanceId] == true end local function HasValidKeyForCurrentDungeon(player) local guid = player:GetGUIDLow() local mapId = player:GetMapId() local keyData = PlayerKeysCache[guid] return keyData and keyData.mapId == mapId end local function CalculateMythicRating(tier, timeRemainingPercent) -- https://www.wowhead.com/guide/blizzard-mythic-plus-rating-score-in-game -- base rating: 37.5 + (tier * 7.5) + (affixes * 7.5) local baseRating = 37.5 + (tier * 7.5) local affixCount = math.min(tier, 3) local affixBonus = affixCount * 7.5 local totalBaseRating = baseRating + affixBonus local timeBonus = 0 if timeRemainingPercent > 0 then local bonusPercent = math.min(timeRemainingPercent, 40) / 40 timeBonus = bonusPercent * 7.5 else local penaltyPercent = math.min(math.abs(timeRemainingPercent), 40) / 40 timeBonus = -penaltyPercent * 15 end return math.max(0, totalBaseRating + timeBonus) end local function CalculateKeystoneUpgrade(timeRemainingPercent, completed) if not completed then return -1 end if timeRemainingPercent >= 40 then return 3 elseif timeRemainingPercent >= 20 then return 2 elseif timeRemainingPercent > 0 then return 1 else return -1 end end local function CalculateLootBonus(upgradeLevel) local chanceMultiplier = 1.0 local maxItems = 1 if upgradeLevel == 2 then chanceMultiplier = 1.25 maxItems = 1 elseif upgradeLevel >= 3 then chanceMultiplier = 1.5 maxItems = 2 end return chanceMultiplier, maxItems end local function RecalculateTotalPoints(guid) local cache = PlayerRatingCache[guid] if not cache then return end local dungeonColumns = {574, 575, 576, 578, 595, 599, 600, 601, 602, 604, 608, 619, 632, 650, 658, 668} local total = 0 for _, mapId in ipairs(dungeonColumns) do total = total + (cache[mapId] or 0) end local avg = total / #dungeonColumns cache.total_points = avg SavePlayerRatingCache(guid) end local function ApplyAuraToNearbyCreatures(player, affixes, tier) local seen = {} local map = player:GetMap() if not map then return end local MYTHIC_SCAN_RADIUS = 240 local count = 0 local creatures = player:GetCreaturesInRange(MYTHIC_SCAN_RADIUS, nil, 1, 1) for _, creature in ipairs(creatures) do local guid = creature:GetGUIDLow() local entry = creature:GetEntry() local faction = creature:GetFaction() local shouldExclude = false for _, excludeId in ipairs(AFFIX_EXCLUDE_CREATURES) do if entry == excludeId then shouldExclude = true break end end if not seen[guid] and creature:IsAlive() and creature:IsInWorld() and not creature:IsPlayer() and creature:IsElite() and not shouldExclude and faction ~= 2 and faction ~= 3 and faction ~= 4 and faction ~= 31 and faction ~= 35 and faction ~= 188 and faction ~= 1629 and faction ~= 114 and faction ~= 115 and faction ~= 1 then seen[guid] = true count = count + 1 for _, spellId in ipairs(affixes) do if spellId == 58549 then if tier >= 5 then local stacks = tier - 4 if not creature:HasAura(58549) then creature:AddAura(58549, creature) end local aura = creature:GetAura(58549) if aura then aura:SetStackAmount(stacks) end end else if not creature:HasAura(spellId) then creature:AddAura(spellId, creature) end end end end end end local function DowngradeKeystoneOnFail(player, tier) local guid = player:GetGUIDLow() if not PlayerHasAnyKeystone(player) then if tier > 1 then local newTier = tier - 1 local newMapId = GetRandomMythicMapId() PlayerKeysCache[guid] = {mapId = newMapId, tier = newTier} CharDBQuery(string.format("REPLACE INTO character_mythic_keys (guid, mapId, tier) VALUES (%d, %d, %d)", guid, newMapId, newTier)) player:AddItem(900100, 1) player:SendBroadcastMessage(string.format("[Mythic+] " .. GetLocalizedText(player, "UI", "Your keystone has been downgraded to Tier %d."), newTier)) CreateLuaEvent(function() local p = GetPlayerByGUID(guid) if p then MythicHandlers.RequestMapNameAndTier(p) end end, 500, 1) else player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "Your keystone was destroyed.")) end end end local function SetEndOfRunUnitFlags(player) if not player or not player:IsInWorld() then return end local map = player:GetMap() if not map then return end local instanceId = map:GetInstanceId() local UNIT_FLAG_NOT_SELECTABLE = 33554432 local UNIT_FLAG_NON_ATTACKABLE = 2 local UNIT_FLAG_IMMUNE_TO_PC = 256 local UNIT_FLAG_IMMUNE_TO_NPC = 512 local flagsToAdd = UNIT_FLAG_NOT_SELECTABLE + UNIT_FLAG_NON_ATTACKABLE + UNIT_FLAG_IMMUNE_TO_PC + UNIT_FLAG_IMMUNE_TO_NPC local function bitwise_or(a, b) local result, bitval = 0, 1 while a > 0 or b > 0 do if (a % 2 == 1) or (b % 2 == 1) then result = result + bitval end a = math.floor(a / 2) b = math.floor(b / 2) bitval = bitval * 2 end return result end local creatures = player:GetCreaturesInRange(2000, nil, 1, 1) for _, creature in ipairs(creatures) do local cMap = creature:GetMap() if cMap and cMap:GetInstanceId() == instanceId then local currentFlags = creature:GetUnitFlags() or 0 local newFlags = bitwise_or(currentFlags, flagsToAdd) if newFlags ~= currentFlags then creature:SetUnitFlags(newFlags) end end end end local function CheckHallsOfReflectionProgress(event, player) if player:GetMapId() ~= 668 then return end local instanceId = player:GetMap():GetInstanceId() if not IsRunActive(instanceId) then return end local tracker = MYTHIC_BOSS_KILL_TRACKER[instanceId] if not tracker then return end local lichKingStillRemaining = false for _, bossEntry in ipairs(tracker.remaining) do if bossEntry == 36954 then lichKingStillRemaining = true break end end if not lichKingStillRemaining then return end local x, y = player:GetX(), player:GetY() local completionPoint = {x = 5265.987, y = 1682.8047} local function getDistance2D(x1, y1, x2, y2) return math.sqrt((x1 - x2)^2 + (y1 - y2)^2) end local distance = getDistance2D(x, y, completionPoint.x, completionPoint.y) if distance <= 15 then local group = player:GetGroup() local members = group and group:GetMembers() or { player } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == 668 then member:KilledMonsterCredit(36954) end end for i, bossEntry in ipairs(tracker.remaining) do if bossEntry == 36954 then table.remove(tracker.remaining, i) local bossIndex = tracker.indexMap[36954] for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == 668 then AIO.Handle(member, "AIO_Mythic", "MarkBossKilled", 668, bossIndex) end end break end end CheckRunCompletion(instanceId, 668) end end local function StartAuraLoop(player, instanceId, mapId, affixes, interval, tier) local guid = player:GetGUIDLow() if MYTHIC_LOOP_HANDLERS[instanceId] then RemoveEventById(MYTHIC_LOOP_HANDLERS[instanceId]) end local eventId = CreateLuaEvent(function() local p = GetPlayerByGUID(guid) if not p then return end if not MYTHIC_FLAG_TABLE[instanceId] then return end if p:GetMapId() ~= mapId then if p:HasAura(8326) then return end local runData = ActiveRunsCache[instanceId] if runData and runData.run_id then local now = os.time() local duration = math.max(0, now - runData.start_time) CharDBQuery(string.format([[ UPDATE character_mythic_history SET completed = 2, end_time = FROM_UNIXTIME(%d), duration = %d WHERE run_id = %d ]], now, duration, runData.run_id)) p:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(p, "UI", "You left the dungeon. The run is over.")) local validPlayer = GetPlayerByGUID(guid) if validPlayer and validPlayer:IsInWorld() then SetEndOfRunUnitFlags(validPlayer) end DowngradeKeystoneOnFail(p, runData.tier) end AIO.Handle(p, "AIO_Mythic", "KillMythicTimerGUI") MYTHIC_FLAG_TABLE[instanceId] = nil MYTHIC_AFFIXES_TABLE[instanceId] = nil MYTHIC_LOOP_HANDLERS[instanceId] = nil MYTHIC_REWARD_CHANCE_TABLE[instanceId] = nil ActiveRunsCache[instanceId] = nil if eventId ~= nil then RemoveEventById(eventId) end return end local bossData = MythicBosses[mapId] if bossData then local runData = ActiveRunsCache[instanceId] if runData then local now = os.time() local elapsed = now - runData.start_time if elapsed >= (bossData.timer or 900) then local runData = ActiveRunsCache[instanceId] if runData and not runData.overtime_started then runData.overtime_started = true runData.overtime_start_time = now local group = p:GetGroup() local members = group and group:GetMembers() or { p } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then AIO.Handle(member, "AIO_Mythic", "StartOvertimeMode") DowngradeKeystoneOnFail(member, runData.tier) member:SendBroadcastMessage(string.format( "[Mythic+] " .. GetLocalizedText(member, "UI", "Time expired! Tier %d failed. You can continue in overtime for the chance of loot."), runData.tier )) end end CharDBQuery(string.format([[ UPDATE character_mythic_history SET completed = 3 WHERE run_id = %d ]], runData.run_id)) end end end end ApplyAuraToNearbyCreatures(p, affixes, tier) if mapId == 668 then CheckHallsOfReflectionProgress(nil, p) end end, interval, 0) MYTHIC_LOOP_HANDLERS[instanceId] = eventId end function Pedestal_OnGossipHello(_, player, creature) if not player:HasItem(900100) then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "You do not have a Mythic Keystone.")) player:GossipComplete() return end if not HasValidKeyForCurrentDungeon(player) then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "Your keystone does not appear to fit.")) player:GossipComplete() return end player:GossipClearMenu() local guid = player:GetGUIDLow() local keyData = PlayerKeysCache[guid] local tier = keyData and keyData.tier or 1 player:GossipMenuAddItem(5, GetLocalizedText(player, "UI", "Insert Keystone (Tier %d)"):format(tier), 0, 100, false, "", 0) player:GossipMenuAddItem(2, GetLocalizedText(player, "UI", "Step away"), 0, 999) player:GossipSendMenu(1, creature) end function Pedestal_OnGossipSelect(_, player, _, _, intid) if not HasValidKeyForCurrentDungeon(player) then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "This keystone is not for this dungeon.")) player:GossipComplete() return end if intid == 999 then player:GossipComplete() return end if intid == 100 then if not player:HasItem(900100) then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "You do not have a Mythic Keystone.")) player:GossipComplete() return end local map = player:GetMap() if not map or map:GetDifficulty() == 0 then player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "Keystones cannot be used in Normal mode dungeons.")) player:GossipComplete() return end local guid = player:GetGUIDLow() local keyData = PlayerKeysCache[guid] local tier = keyData and keyData.tier or 1 local now = os.time() local mapId = player:GetMapId() local instanceId = map:GetInstanceId() local group = player:GetGroup() local members = group and group:GetMembers() or {player} local today = os.date("%Y-%m-%d") local affixes = GetAffixSet(tier) local affixNames = {} for i = 1, math.min(tier, 4) do local affix = WeeklyAffixesCache[i] if affix then table.insert(affixNames, affix.name) end end local safeAffixNames = table.concat(affixNames, ", "):gsub("'", "''") for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then local mguid = member:GetGUIDLow() if not PlayerRatingCache[mguid] then LoadPlayerCache(mguid) end local cache = PlayerRatingCache[mguid] if cache then cache.total_runs = cache.total_runs + 1 SavePlayerRatingCache(mguid) end end end local guids = {} for i = 1, 5 do guids[i] = 0 end for i, member in ipairs(members) do if i > 5 then break end guids[i] = member:GetGUIDLow() end CharDBQuery(string.format([[ INSERT INTO character_mythic_history (member_1, member_2, member_3, member_4, member_5, date, mapId, instanceId, tier, completed, deaths, affixes) VALUES (%d, %d, %d, %d, %d, '%s', %d, %d, %d, 0, 0, '%s'); ]], guids[1], guids[2], guids[3], guids[4], guids[5], today, mapId, instanceId, tier, safeAffixNames)) local runIdQuery = CharDBQuery("SELECT LAST_INSERT_ID()") local runId = runIdQuery and runIdQuery:GetUInt32(0) or 0 ActiveRunsCache[instanceId] = { guid = guid, mapId = mapId, tier = tier, start_time = nil, deaths = 0, run_id = runId, members = {} } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then table.insert(ActiveRunsCache[instanceId].members, member:GetGUIDLow()) end end MYTHIC_FLAG_TABLE[instanceId] = false MYTHIC_AFFIXES_TABLE[instanceId] = affixes MYTHIC_REWARD_CHANCE_TABLE[instanceId] = tier <= 2 and 1.5 or tier <= 4 and 2.0 or 5.0 local cache = PlayerRatingCache[guid] local currentRating = cache and cache[mapId] or 0 local potentialGain = CalculateMythicRating(tier, 0) player:RemoveItem(900100, 1) PlayerKeysCache[guid] = nil CharDBQuery(string.format("DELETE FROM character_mythic_keys WHERE guid = %d", guid)) local map_x, map_y, map_z, map_o = GetMapEntrance(mapId) local memberGuids = {} for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then table.insert(memberGuids, member:GetGUID()) member:NearTeleport(map_x, map_y, map_z, map_o) member:SetRooted(true) end end local dungeoncreatures = player:GetCreaturesInRange(2000, nil, 0, 2) for _, creature in ipairs(dungeoncreatures) do creature:Respawn() end for _, guid in ipairs(memberGuids) do local member = GetPlayerByGUID(guid) if member and member:IsInWorld() and member:GetMapId() == mapId then AIO.Handle(member, "AIO_Mythic", "StartCountdown", 10) end end local starterGuid = player:GetGUID() CreateLuaEvent(function() local starter = GetPlayerByGUID(starterGuid) if not starter then return end for _, guid in ipairs(memberGuids) do local member = GetPlayerByGUID(guid) if member and member:IsInWorld() and member:GetMapId() == mapId then member:SetRooted(false) end end MYTHIC_FLAG_TABLE[instanceId] = true ApplyAuraToNearbyCreatures(starter, affixes, tier) StartAuraLoop(starter, instanceId, mapId, affixes, 6000, tier) StartBossScanLoop(starter, instanceId, mapId, tier) local bossData = MythicBosses[mapId] if bossData then local tracker = { remaining = {}, indexMap = {}, tier = tier } for idx, entry in ipairs(bossData.bosses) do tracker.remaining[#tracker.remaining + 1] = entry tracker.indexMap[entry] = idx end MYTHIC_BOSS_KILL_TRACKER[instanceId] = tracker local actualStartTime = os.time() if ActiveRunsCache[instanceId] then ActiveRunsCache[instanceId].start_time = actualStartTime CharDBQuery(string.format([[ UPDATE character_mythic_history SET start_time = FROM_UNIXTIME(%d) WHERE run_id = %d ]], actualStartTime, ActiveRunsCache[instanceId].run_id)) end for _, guid in ipairs(memberGuids) do local member = GetPlayerByGUID(guid) if member and member:IsInWorld() and member:GetMapId() == mapId then AIO.Handle(member, "AIO_Mythic", "StartMythicTimerGUI", mapId, tier, bossData.timer or 900, GetLocalizedBossNames(member, mapId), potentialGain, bossData.enemies or 50) end end end end, 10000, 1) player:GossipComplete() end end function StartBossScanLoop(player, instanceId, mapId, tier) local bosses = MythicBosses[mapId] if not bosses or not bosses.bosses then print("[Mythic+][Error] No boss data for mapId: " .. mapId) return end MYTHIC_BOSS_KILL_TRACKER[instanceId] = { remaining = {}, indexMap = {}, tier = tier } for idx, entry in ipairs(bosses.bosses) do table.insert(MYTHIC_BOSS_KILL_TRACKER[instanceId].remaining, entry) MYTHIC_BOSS_KILL_TRACKER[instanceId].indexMap[entry] = idx end end local function MythicBossKillCheck(event, player, killed) local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local instanceId = map:GetInstanceId() if not IsRunActive(instanceId) then return end local NO_CORPSE_REMOVE_IDS = { [26692] = true, -- 'Ymirjar Harpooner' in Utgarde Pinnacle // would not spawn harpoons otherwise [28585] = true, -- 'Slag' in Halls of Lightning // respawn would be too fast } local entry = killed:GetEntry() local tracker = MYTHIC_BOSS_KILL_TRACKER[instanceId] if not tracker then return end if not NO_CORPSE_REMOVE_IDS[entry] then killed:RemoveCorpse() end for i, bossEntry in ipairs(tracker.remaining) do if bossEntry == entry then table.remove(tracker.remaining, i) local bossIndex = tracker.indexMap[entry] local group = player:GetGroup() local members = group and group:GetMembers() or { player } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then AIO.Handle(member, "AIO_Mythic", "MarkBossKilled", mapId, bossIndex) end end break end end if #tracker.remaining == 0 then CheckRunCompletion(instanceId, mapId) end end if MYTHIC_ENEMY_FORCES_TRACKER == nil then MYTHIC_ENEMY_FORCES_TRACKER = {} end local function MythicEnemyKillCheck(event, player, killed) local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local instanceId = map:GetInstanceId() if not IsRunActive(instanceId) then return end local entry = killed:GetEntry() local bossData = MythicBosses[mapId] if not bossData then return end if not bossData.enemies or bossData.enemies == 0 then return end for _, bossEntry in ipairs(bossData.bosses) do if bossEntry == entry then return end end local faction = killed:GetFaction() if faction == 2 or faction == 3 or faction == 4 or faction == 31 or faction == 35 or faction == 188 or faction == 1629 or faction == 114 or faction == 115 or faction == 1 then return end if not MYTHIC_ENEMY_FORCES_TRACKER[instanceId] then MYTHIC_ENEMY_FORCES_TRACKER[instanceId] = { current = 0, required = bossData.enemies or 50, completed = false } end local tracker = MYTHIC_ENEMY_FORCES_TRACKER[instanceId] tracker.current = tracker.current + 1 local percentage = math.min((tracker.current / tracker.required) * 100, 100) if tracker.current >= tracker.required and not tracker.completed then tracker.completed = true end local group = player:GetGroup() local members = group and group:GetMembers() or { player } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then AIO.Handle(member, "AIO_Mythic", "UpdateEnemyForces", tracker.current, tracker.required, percentage, tracker.completed) end end CheckRunCompletion(instanceId, mapId) end function CheckRunCompletion(instanceId, mapId) local bossTracker = MYTHIC_BOSS_KILL_TRACKER[instanceId] local enemyTracker = MYTHIC_ENEMY_FORCES_TRACKER[instanceId] local bossData = MythicBosses[mapId] local bossesComplete = bossTracker and #bossTracker.remaining == 0 local enemyForcesComplete = true if bossData and bossData.enemies and bossData.enemies > 0 then enemyForcesComplete = enemyTracker and enemyTracker.completed end if bossesComplete and enemyForcesComplete then local runData = ActiveRunsCache[instanceId] if not runData then return end local player = GetPlayerByGUID(runData.guid) if not player then return end local group = player:GetGroup() local members = group and group:GetMembers() or { player } local now = os.time() local bossData = MythicBosses[mapId] local timerTotal = bossData and bossData.timer or 900 if not runData.start_time then return end local startTimeRaw = runData.start_time local deaths = runData.deaths or 0 local duration = math.max(0, now - startTimeRaw) local remainingTime = math.max(0, timerTotal - duration) local runId = runData.run_id local wasOvertime = runData.overtime_started or false if runId and runId > 0 then CharDBQuery(string.format([[ UPDATE character_mythic_history SET completed = 1, end_time = FROM_UNIXTIME(%d), duration = %d WHERE run_id = %d; ]], now, duration, runId)) end for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == mapId then if wasOvertime then AwardOvertimeLoot(member, bossTracker.tier) else AwardMythicPoints(member, bossTracker.tier, duration, deaths, remainingTime) end SetEndOfRunUnitFlags(member) end end MYTHIC_BOSS_KILL_TRACKER[instanceId] = nil MYTHIC_ENEMY_FORCES_TRACKER[instanceId] = nil MYTHIC_FLAG_TABLE[instanceId] = nil MYTHIC_AFFIXES_TABLE[instanceId] = nil MYTHIC_REWARD_CHANCE_TABLE[instanceId] = nil ActiveRunsCache[instanceId] = nil if MYTHIC_LOOP_HANDLERS[instanceId] then RemoveEventById(MYTHIC_LOOP_HANDLERS[instanceId]) MYTHIC_LOOP_HANDLERS[instanceId] = nil end end end local function MythicPlayerDeath(event, killer, killed) local map = killed:GetMap() if not map or map:GetDifficulty() == 0 then return end local instanceId = map:GetInstanceId() local runData = ActiveRunsCache[instanceId] if not runData then return end runData.deaths = runData.deaths + 1 local newDeaths = runData.deaths local tier = runData.tier CharDBQuery("UPDATE character_mythic_history SET deaths = " .. newDeaths .. " WHERE run_id = " .. runData.run_id) local penalty = newDeaths * penaltyPerDeath local group = killed:GetGroup() local members = group and group:GetMembers() or { killed } for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == map:GetMapId() then AIO.Handle(member, "AIO_Mythic", "UpdateMythicScore", penalty, newDeaths) end end local limit = (tier == 1) and 6 or 4 if newDeaths >= limit then local now = os.time() local startTime = runData.start_time if not startTime then return end local duration = math.max(0, now - startTime) local runId = runData.run_id CharDBQuery(string.format([[ UPDATE character_mythic_history SET completed = 2, end_time = FROM_UNIXTIME(%d), duration = %d WHERE run_id = %d ]], now, duration, runId)) for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == map:GetMapId() then local mguid = member:GetGUIDLow() local cache = PlayerRatingCache[mguid] if cache then cache.too_many_death_runs = cache.too_many_death_runs + 1 SavePlayerRatingCache(mguid) end AIO.Handle(member, "AIO_Mythic", "StopMythicTimerGUI", 0) DowngradeKeystoneOnFail(member, tier) end end MYTHIC_FLAG_TABLE[instanceId] = nil MYTHIC_AFFIXES_TABLE[instanceId] = nil MYTHIC_REWARD_CHANCE_TABLE[instanceId] = nil ActiveRunsCache[instanceId] = nil MYTHIC_ENEMY_FORCES_TRACKER[instanceId] = nil if MYTHIC_LOOP_HANDLERS[instanceId] then RemoveEventById(MYTHIC_LOOP_HANDLERS[instanceId]) MYTHIC_LOOP_HANDLERS[instanceId] = nil end end end local function CheckMalGanisEvade(event, creature) if creature:GetEntry() ~= 26533 then return end local map = creature:GetMap() if not map or map:GetMapId() ~= 595 then return end local instanceId = map:GetInstanceId() if not IsRunActive(instanceId) then return end if creature:IsInEvadeMode() then local players = creature:GetPlayersInRange(100) for _, player in ipairs(players) do if player:IsInWorld() and player:GetMapId() == 595 then player:KilledMonsterCredit(26533) local group = player:GetGroup() local members = group and group:GetMembers() or { player } local tracker = MYTHIC_BOSS_KILL_TRACKER[instanceId] if tracker then for i, bossEntry in ipairs(tracker.remaining) do if bossEntry == 26533 then table.remove(tracker.remaining, i) local bossIndex = tracker.indexMap[26533] for _, member in ipairs(members) do if member:IsInWorld() and member:GetMapId() == 595 then AIO.Handle(member, "AIO_Mythic", "MarkBossKilled", 595, bossIndex) end end break end end if #tracker.remaining == 0 then CheckRunCompletion(instanceId, mapId) end end end end end end local function isTierEligibleForBracket(bracket, tier) if MYTHIC_LOOT_BRACKETS[bracket] then local bracketData = MYTHIC_LOOT_BRACKETS[bracket] if bracketData == "all" then return true elseif type(bracketData) == "table" then for _, validTier in ipairs(bracketData) do if validTier == tier then return true end end return false end end if bracket == "all" then return true end local singleTier = tonumber(bracket) if singleTier then return tier == singleTier end local rangeStart, rangeEnd = bracket:match("^(%d+)-(%d+)$") if rangeStart and rangeEnd then rangeStart, rangeEnd = tonumber(rangeStart), tonumber(rangeEnd) return tier >= rangeStart and tier <= rangeEnd end local minTier = bracket:match("^(%d+)%+$") if minTier then minTier = tonumber(minTier) return tier >= minTier end local maxTier = bracket:match("^(%d+)-$") if maxTier then maxTier = tonumber(maxTier) return tier <= maxTier end return false end function isVaultTierEligibleForBracket(bracket, tier) if VAULT_LOOT_BRACKETS[bracket] then local bracketData = VAULT_LOOT_BRACKETS[bracket] if bracketData == "all" then return true elseif type(bracketData) == "table" then for _, validTier in ipairs(bracketData) do if validTier == tier then return true end end return false end end if bracket == "all" then return true end local singleTier = tonumber(bracket) if singleTier then return tier == singleTier end local rangeStart, rangeEnd = bracket:match("^(%d+)-(%d+)$") if rangeStart and rangeEnd then rangeStart, rangeEnd = tonumber(rangeStart), tonumber(rangeEnd) return tier >= rangeStart and tier <= rangeEnd end local minTier = bracket:match("^(%d+)%+$") if minTier then minTier = tonumber(minTier) return tier >= minTier end local maxTier = bracket:match("^(%d+)-$") if maxTier then maxTier = tonumber(maxTier) return tier <= maxTier end return false end local function TryRewardMythicLoot(player, tier, upgradeLevel) local faction = player:GetTeam() == 67 and "A" or (player:GetTeam() == 469 and "H" or "N") local eligible = {} local chanceMultiplier, maxItems = CalculateLootBonus(upgradeLevel) for _, loot in ipairs(MythicLootTable) do if loot.type == "pet" and not MythicRewardConfig.pets then goto continue end if loot.type == "mount" and not MythicRewardConfig.mounts then goto continue end if loot.type == "gear" and not MythicRewardConfig.equipment then goto continue end if loot.type == "spell" and not MythicRewardConfig.spells then goto continue end if not isTierEligibleForBracket(loot.loot_bracket, tier) then goto continue end if loot.faction ~= "N" and loot.faction ~= faction then goto continue end if loot.type == "gear" then if not CanPlayerUseItem(player, loot.itemid) then goto continue end end table.insert(eligible, loot) ::continue:: end if #eligible == 0 then return end local rewardsGiven = 0 local awardedItems = {} for attempt = 1, maxItems do if rewardsGiven >= maxItems then break end local availableItems = {} for _, loot in ipairs(eligible) do if not awardedItems[loot.itemid] then table.insert(availableItems, loot) end end if #availableItems == 0 then break end local reward = availableItems[math.random(1, #availableItems)] local adjustedChance = reward.chancePercent * chanceMultiplier if math.random() * 100 <= adjustedChance then if reward.type == "gear" or reward.type == "pet" or reward.type == "mount" then player:AddItem(reward.itemid, reward.amount) local itemData = CacheItemTemplate(reward.itemid) local itemName = itemData and itemData.name or "Unknown Item" player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "Reward:") .. " " .. itemName) elseif reward.type == "spell" then player:LearnSpell(reward.itemid) player:SendBroadcastMessage("[Mythic+] " .. GetLocalizedText(player, "UI", "Reward: Spell learned!")) end if reward.additionalID and reward.additionalType then if reward.additionalType == "item" then player:AddItem(reward.additionalID, 1) elseif reward.additionalType == "spell" then player:LearnSpell(reward.additionalID) elseif reward.additionalType == "skill" then player:AdvanceSkill(reward.additionalID, 1) end end awardedItems[reward.itemid] = true rewardsGiven = rewardsGiven + 1 end end if upgradeLevel >= 2 then local upgradeText = upgradeLevel == 2 and GetLocalizedText(player, "UI", "+2 Performance") or GetLocalizedText(player, "UI", "+3 Performance") player:SendBroadcastMessage("[Mythic+] " .. upgradeText .. " - " .. GetLocalizedText(player, "UI", "Enhanced loot chances applied!")) end end function AwardMythicPoints(player, tier, duration, deaths, remainingTime) local now = os.time() local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local instanceId = map:GetInstanceId() local guid = player:GetGUIDLow() local cache = PlayerRatingCache[guid] if not cache then cache = { total_points = 0, total_runs = 0, completed_runs = 0, out_of_time_runs = 0, too_many_death_runs = 0, [mapId] = 0 } PlayerRatingCache[guid] = cache end local previous = cache[mapId] or 0 local bossData = MythicBosses[mapId] local timerTotal = bossData and bossData.timer or 900 local timeRemainingPercent = (remainingTime / timerTotal) * 100 local gainedRating = CalculateMythicRating(tier, timeRemainingPercent) local deathPenalty = deaths * 10 local finalRating = math.max(0, gainedRating - deathPenalty) local newRating = math.max(previous, finalRating) cache[mapId] = newRating cache.completed_runs = cache.completed_runs + 1 cache.total_runs = cache.total_runs + 1 SavePlayerRatingCache(guid) RecalculateTotalPoints(guid) local upgradeLevel = CalculateKeystoneUpgrade(timeRemainingPercent, true) AIO.Handle(player, "AIO_Mythic", "StopMythicTimerGUI", remainingTime) AIO.Handle(player, "AIO_Mythic", "FinalizeMythicScore", deathPenalty, deaths, finalRating - gainedRating + deathPenalty) local dfmt = string.format("%02d:%02d", math.floor(duration/60), duration%60) local upgradeText = "" if upgradeLevel > 0 then upgradeText = string.format(" (Key +%d)", upgradeLevel) elseif upgradeLevel < 0 then upgradeText = " (Key -1)" end player:SendBroadcastMessage(string.format( "[Mythic+] %s\n%s", GetLocalizedText(player, "UI", "Tier %d completed in %s%s."):format(tier, dfmt, upgradeText), GetLocalizedText(player, "UI", "Rating: %d (+%d gained, -%d death penalty)"):format(newRating, gainedRating, deathPenalty) )) if not PlayerHasAnyKeystone(player) then local newTier = math.max(1, tier + upgradeLevel) local newMapId = GetRandomMythicMapId() PlayerKeysCache[guid] = {mapId = newMapId, tier = newTier} CharDBQuery(string.format("REPLACE INTO character_mythic_keys (guid, mapId, tier) VALUES (%d, %d, %d)", guid, newMapId, newTier)) player:AddItem(900100, 1) player:SendBroadcastMessage(string.format("[Mythic+] %s", GetLocalizedText(player, "UI", "You received a Tier %d Mythic Keystone!"):format(newTier))) CreateLuaEvent(function() local p = GetPlayerByGUID(guid) if p then MythicHandlers.RequestMapNameAndTier(p) end end, 500, 1) end TryRewardMythicLoot(player, tier, upgradeLevel) UpdateVaultProgress(player, tier, true) LeaderboardCache.lastUpdate = 0 end function AwardOvertimeLoot(player, tier) local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local guid = player:GetGUIDLow() local cache = PlayerRatingCache[guid] if cache then cache.completed_runs = cache.completed_runs + 1 SavePlayerRatingCache(guid) end AIO.Handle(player, "AIO_Mythic", "StopMythicTimerGUI", 0) player:SendBroadcastMessage(string.format( "[Mythic+] %s", GetLocalizedText(player, "UI", "Tier %d completed in overtime."):format(tier) )) TryRewardMythicLoot(player, tier, 0) LeaderboardCache.lastUpdate = 0 end function BindKeystoneToDungeon(event, player, item, count) local guid = player:GetGUIDLow() if item:GetEntry() == 900100 then local existingKey = PlayerKeysCache[guid] if not existingKey then local newMapId = GetRandomMythicMapId() local newTier = 1 PlayerKeysCache[guid] = {mapId = newMapId, tier = newTier} CharDBQuery(string.format("REPLACE INTO character_mythic_keys (guid, mapId, tier) VALUES (%d, %d, %d)", guid, newMapId, newTier)) end CreateLuaEvent(function() local p = GetPlayerByGUID(guid) if p then MythicHandlers.RequestMapNameAndTier(p) end end, 100, 1) end end function MythicHandlers.RequestMapNameAndTier(player) local guid = player:GetGUIDLow() local keyData = PlayerKeysCache[guid] if keyData then local mapName = GetLocalizedDungeonName(player, keyData.mapId) AIO.Handle(player, "AIO_Mythic", "ReceiveMapNameAndTier", mapName, keyData.tier) else AIO.Handle(player, "AIO_Mythic", "ReceiveMapNameAndTier", GetLocalizedText(player, "UI", "No Keystone"), 0) end end local function HeroicEndbossKeyReward(event, player, killed) local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local difficulty = map:GetDifficulty() if difficulty ~= 1 then return end local instanceId = map:GetInstanceId() if MYTHIC_FLAG_TABLE[instanceId] then return end local bossData = MythicBosses[mapId] if bossData and killed:GetEntry() == bossData.final then if not PlayerHasAnyKeystone(player) then player:AddItem(900100, 1) player:SendBroadcastMessage(string.format("[Mythic+] %s", GetLocalizedText(player, "UI", "You received a Mythic Keystone!"))) end end end local function OnPlayerLogin(event, player) local guid = player:GetGUIDLow() LoadPlayerCache(guid) LoadPlayerVaultCache(guid) PlayerNamesCache[guid] = player:GetName() CreateLuaEvent(function() local p = GetPlayerByGUID(guid) if p then MythicHandlers.RequestVaultStatus(p) end end, 2000, 1) end local function OnPlayerLogout(event, player) local guid = player:GetGUIDLow() if PlayerRatingCache[guid] then SavePlayerRatingCache(guid) end end RegisterCreatureGossipEvent(PEDESTAL_NPC_ENTRY, 1, Pedestal_OnGossipHello) RegisterCreatureGossipEvent(PEDESTAL_NPC_ENTRY, 2, Pedestal_OnGossipSelect) RegisterPlayerEvent(7, MythicBossKillCheck) RegisterPlayerEvent(7, HeroicEndbossKeyReward) RegisterPlayerEvent(7, MythicEnemyKillCheck) RegisterPlayerEvent(8, MythicPlayerDeath) RegisterPlayerEvent(53, BindKeystoneToDungeon) RegisterPlayerEvent(28, LeaveDungeonMap) RegisterPlayerEvent(3, OnPlayerLogin) RegisterPlayerEvent(4, OnPlayerLogout) RegisterCreatureEvent(26533, 1, CheckMalGanisEvade) RegisterGameObjectEvent(VAULT_GAMEOBJECT_ID, 14, WeeklyVaultInteract) CheckAndProcessVaultOnStartup() function MythicHandlers.RequestWeeklyAffixes(player) if WeeklyAffixesCache and #WeeklyAffixesCache >= 3 then local affix1 = WeeklyAffixesCache[1].name local affix2 = WeeklyAffixesCache[2].name or "-" local affix3 = WeeklyAffixesCache[3].name or "-" AIO.Handle(player, "AIO_Mythic", "ReceiveWeeklyAffixes", affix1, affix2, affix3) else AIO.Handle(player, "AIO_Mythic", "ReceiveWeeklyAffixes", "?", "?", "?") end end function MythicHandlers.RequestTotalPoints(player) local guid = player:GetGUIDLow() local cache = PlayerRatingCache[guid] if not cache then LoadPlayerCache(guid) AIO.Handle(player, "AIO_Mythic", "ReceiveTotalPoints", 0, {}) return end local dungeonScores = {} local dungeonIds = {574, 575, 576, 578, 595, 599, 600, 601, 602, 604, 608, 619, 632, 650, 658, 668} for _, mapId in ipairs(dungeonIds) do dungeonScores[tostring(mapId)] = cache[mapId] or 0 end AIO.Handle(player, "AIO_Mythic", "ReceiveTotalPoints", cache.total_points, dungeonScores) end function MythicHandlers.RequestLeaderboard(player) UpdateLeaderboardCache() AIO.Handle(player, "AIO_Mythic", "ReceiveLeaderboard", LeaderboardCache.topThree, LeaderboardCache.dungeonTop) end function MythicHandlers.RequestRunState(player) local map = player:GetMap() if not map then return end local mapId = map:GetMapId() local instanceId = map:GetInstanceId() if not IsRunActive(instanceId) then AIO.Handle(player, "AIO_Mythic", "ClearRunState") return end local runData = ActiveRunsCache[instanceId] if not runData then AIO.Handle(player, "AIO_Mythic", "ClearRunState") return end local bossData = MythicBosses[mapId] if not bossData then AIO.Handle(player, "AIO_Mythic", "ClearRunState") return end local now = os.time() local elapsed = now - runData.start_time local duration = bossData.timer or 900 local bossTracker = MYTHIC_BOSS_KILL_TRACKER[instanceId] local enemyTracker = MYTHIC_ENEMY_FORCES_TRACKER[instanceId] local killedBosses = {} if bossTracker then for idx, entry in ipairs(bossData.bosses) do local killed = true for _, remaining in ipairs(bossTracker.remaining) do if remaining == entry then killed = false break end end if killed then table.insert(killedBosses, idx) end end end local enemyForces = { current = 0, required = bossData.enemies or 0, percentage = 0, completed = false } if enemyTracker then enemyForces.current = enemyTracker.current enemyForces.required = enemyTracker.required enemyForces.percentage = math.min((enemyTracker.current / enemyTracker.required) * 100, 100) enemyForces.completed = enemyTracker.completed end local cache = PlayerRatingCache[player:GetGUIDLow()] local currentRating = cache and cache[mapId] or 0 local potentialGain = CalculateMythicRating(runData.tier, 0) AIO.Handle(player, "AIO_Mythic", "RestoreRunState", { active = true, mapId = mapId, tier = runData.tier, duration = duration, elapsed = elapsed, bossNames = GetLocalizedBossNames(player, mapId), killedBosses = killedBosses, potentialGain = potentialGain, penalty = runData.deaths * penaltyPerDeath, deaths = runData.deaths, maxDeaths = (runData.tier == 1) and 6 or 4, enemyForces = enemyForces, overtime = runData.overtime_started or false }) end