Mythic Plus Score Computation
Jump to navigation
Jump to search
There are a few reference implementations to calculate Mythic+ scores.[1]
Rule set
Key level Score
- + 25 points base score
- + 5 per level
- + 5 per weekly affix
- + 10 for season affix
This results in the following hardcoded table for the first 10 key levels:
local DungeonBaseScores = { 0, 40, 45, 55, 60, 65, 75, 80, 85, 100 }
After this we only add 5 points per key level.
Success score
- Not timed immediately results in a -5 penalty
- Up to 5 points for being up to 40% faster (linear scaling)
- Up to -5 points penalty for being up to 40% slower (linear scaling)
- If we are slower than 140% par time the key score is set to 0 and we don't get any rating at all
- The adjusted affix score is computed by taking the higher weekly affix score time 1.5 (150%) and the lower time 0.5 (50%)
- The dungeon score is the rounded sum of the unrounded addjusted affix scores (round(adjustedTyrannical + adjustedFortified))
- C_MythicPlus.GetSeasonBestAffixScoreInfoForMap() returns rounded values for the weekly affixes
- C_ChallengeMode.GetOverallDungeonScore() is computed from the unrounded values
Implementation
-- Data
local DungeonAbbreviations = {
[375] = "mots",
[376] = "nw",
[377] = "dos",
[378] = "hoa",
[379] = "pf",
[380] = "sd",
[381] = "soa",
[382] = "top",
[391] = "strt",
[392] = "gmbt"
};
local DungeonBaseScores = { 0, 40, 45, 55, 60, 65, 75, 80, 85, 100 };
local TimerConstants = {
---@type number bonus and malus are capped at 40% faster or slower than par time
Threshold = 0.4,
---@type number maximum bonus/malus points for being faster/slower than the par time
MaxModifier = 5,
---@type number maximum bonus/malus points for being faster/slower than the par time
DepletionPunishment = 5,
}
-- Math Functions
local function round(num)
return num >= 0 and math.floor(num + 0.5) or math.ceil(num - 0.5)
end
---@param num number
---@param precision number
---@param num boolean
local function formatNumber(num, precision, noPrefix)
if num == nil then
return "nil"
end
local formatString = "%." .. precision .. "f"
local absolutValueString = string.format(formatString, num)
if noPrefix then
return num < 0 and strsub(absolutValueString, 1) or absolutValueString
else
return (num > 0 and "+" or "") .. absolutValueString
end
end
local function padString(str, length)
local result = str .. ""
while strlen(result) < length do
result = " " .. result
end
return result
end
-- WoW Functions
local function computeTimeModifier(parTimePercentage)
---@type number if we took 130% time this is -30% (30% too slow)
local percentageOffset = (1 - parTimePercentage)
if percentageOffset > TimerConstants.Threshold then
-- bonus is capped at 40% faster than par time
return TimerConstants.MaxModifier;
elseif percentageOffset > 0 then
-- bonus is interpolated linear between 60% and 100% par time
return percentageOffset * TimerConstants.MaxModifier / TimerConstants.Threshold;
elseif percentageOffset == 0 then
-- redundant special case
return 0;
elseif percentageOffset > -TimerConstants.Threshold then
-- bonus is interpolated linear between 100% and 140% par time
return percentageOffset * TimerConstants.MaxModifier / TimerConstants.Threshold - TimerConstants.DepletionPunishment;
else
-- key is hard set to 0 points, `nil` indicates this
return nil;
end
end
local function computeScores(dungeonId, level, timeInSeconds)
local _, _, parTime, _, _ = C_ChallengeMode.GetMapUIInfo(dungeonId)
local baseScore = DungeonBaseScores[math.min(level, 10)] + max(0, level - 10) * 5;
local parTimeFraction = timeInSeconds / parTime;
local timeScore = computeTimeModifier(parTimeFraction);
formatNumber(parTimeFraction * 100, 2)
return {
baseScore = baseScore,
-- 0 if critically over timed
timeScore ~= nil and baseScore + timeScore or 0,
timeBonus = timeScore,
parTimePercentageString = padString(formatNumber(parTimeFraction * 100, 2, true), 7) .. "%"
}
end
local function computeKeyBaseScore(affixScoreData)
return affixScoreData.baseScore + affixScoreData.timeBonus;
end
local function computeAffixScoreSum(score1, score2)
return max(score1, score2) * 1.5 + min(score1, score2) * 0.5;
end
local totalScore = 0
local function buildKeyDataString(blizzardScores, affixScoreData)
local blizzardTyrannical = blizzardScores.Tyrannical.baseScore;
local computedTyrannical = computeKeyBaseScore(affixScoreData.Tyrannical);
local blizzardFortified = blizzardScores.Fortified.baseScore;
local computedFortified = computeKeyBaseScore(affixScoreData.Fortified);
local computedKeyScore = computeAffixScoreSum(computedTyrannical, computedFortified);
totalScore = totalScore + computedKeyScore;
return "Tyrannical " ..
" " .. padString(blizzardTyrannical, 3) .. " | " .. padString(round(computedTyrannical), 3) .. " = " ..
padString(affixScoreData.Tyrannical.baseScore, 3) .. padString(formatNumber(affixScoreData.Tyrannical.timeBonus, 2), 6) ..
affixScoreData.Tyrannical.parTimePercentageString ..
"\n" ..
"Fortified " .. " " .. padString(blizzardFortified, 3) .. " | " .. padString(round(computedFortified), 3) .. " = " ..
padString(affixScoreData.Fortified.baseScore, 3) .. padString(formatNumber(affixScoreData.Fortified.timeBonus, 2), 6) ..
affixScoreData.Fortified.parTimePercentageString ..
"\n" ..
"complete " ..
padString(blizzardScores.Complete, 3) .. " | " .. padString(round(computedKeyScore), 3)
end
local function computeTTEnhancement(dungeonId)
local computedAffixScoreData = {
Tyrannical = { baseScore = 0, timeScore = 0, timeBonus = 0, parTimePercentageString = " 0.00%" },
Fortified = { baseScore = 0, timeScore = 0, timeBonus = 0, parTimePercentageString = " 0.00%" },
}
local blizzardScores = {
Tyrannical = { baseScore = 0 },
Fortified = { baseScore = 0 },
Complete = 0
}
local blizzardAffixScoreData, blizzardTotalScore = C_MythicPlus.GetSeasonBestAffixScoreInfoForMap(dungeonId)
if (blizzardAffixScoreData ~= nil) then
for _, info in pairs(blizzardAffixScoreData) do
computedAffixScoreData[info.name] = computeScores(dungeonId, info.level, info.durationSec)
blizzardScores[info.name] = { baseScore = info.score }
end
end
if blizzardTotalScore ~= nil then
blizzardScores.Complete = blizzardTotalScore
end
return buildKeyDataString(blizzardScores, computedAffixScoreData)
end
local function printScoreTable()
for dungeonId, abbreviation in pairs(DungeonAbbreviations) do
print(abbreviation .. " - " .. C_ChallengeMode.GetMapUIInfo(dungeonId))
print(" Blizzard | Computed")
print(computeTTEnhancement(dungeonId))
end
local currentScore = C_ChallengeMode.GetOverallDungeonScore()
print("========================================")
print("Total " .. padString(currentScore, 3) .. " | " .. padString(round(totalScore), 3))
print("========================================")
end
printScoreTable()
References
- ^ Discord #wowuidev - Trinova - 2022.04.16