Files
moonwell-core/lua_scripts/MythicPlus/Mythic_Server.lua
2025-08-30 19:38:04 +04:00

2354 lines
84 KiB
Lua

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