Module:ArenaScoreTierList

local mw = mw local cargo = mw.ext.cargo local CargoUtil = require 'Module:CargoUtil' local Util = require 'Module:Util' local HeroUtil = require 'Module:HeroUtil' local List = require 'Module:ListUtil' local Hash = require 'Module:HashUtil' local FEHStatUtil = require 'Module:FEHStatUtil' local toboolean = require 'Module:Bool'.toboolean

local COLORS = {'Red', 'Blue', 'Green', 'Colorless'} local PTYPES = {'A', 'B', 'C'} local CUTOFF = 50

--Checks if it's possible for superboons to increase the total base stats --Preliminary calculations about upcoming merge updates: Because the new stats gained from the first merge are said to affect Arena matchmaking, --a neutral or boon/bane unit will gain 3 extra stats for matchmaking, a boon/superbane will gain 3, a superboon/superbane will gain 4, and a superboon/bane will gain 4 local hasHigh = function (growthRates) if List.find(growthRates, false) then return false end local sg = List.map(growthRates, function (x) return FEHStatUtil.getSupergrowth(5, x) end) return List.count(sg, 1) >= 1 end

local List = function (args, frame) local noDuel = toboolean(args.noDuel) local legendary = Hash.from_ipairs(cargo.query('LegendaryHero', '_pageName', {limit = 100}), function (v) return v._pageName, true end) local mythic = Hash.from_ipairs(cargo.query('MythicHero', '_pageName', {limit = 100}), function (v) return v._pageName, true end) local plusHeroes = List.to_set(List.select(List.map_self(mw.text.split(args.plus or , '%s*%$%s*'), function (x) return mw.text.trim(x) end), function (x) return x ~=  end)) Hash.merge_self(plusHeroes, legendary)

--weapon type stuff local weaponQuery1 = cargo.query('WeaponTypes', 'WikiName,Color,IconFile,Name', {groupBy = 'WikiName', orderBy = 'Sort'}) local weaponColorsInfo = Hash.from_ipairs(weaponQuery1, function (v) return v.WikiName, v.Color end)

local classMaxSP = Hash.map_self(List.group_by(cargo.query(   'Skills=s,Skills__CanUseMove=sm,Skills__CanUseWeapon=sw,MoveTypes=m,WeaponTypes=w',    'Scategory,m.WikiName=move,w.WikiName=wep,CONCAT(MAX(s.SP))=maxsp', {      join = 's._ID=sm._rowID,s._ID=sw._rowID,sm._value=m.WikiName,sw._value=w.WikiName',      where = "Exclusive=0",      groupBy = 'Scategory,move,wep',      limit = 1000,    }), function (v) return v.move end), function (vs)      return Hash.map_self(List.group_by(vs, function (v) return v.wep end), function (vs) return Hash.map_self(List.group_by(vs, function (v) return v.Scategory end), function (x)         return tonumber(x[1].maxsp) or 0        end) end)   end)

local maxSPSeals = Hash.map_self(List.group_by(cargo.query(   'SacredSealCosts=ssc,Skills=s,Skills__CanUseMove=sm,Skills__CanUseWeapon=sw,MoveTypes=m,WeaponTypes=w',    'm.WikiName=move,w.WikiName=wep,CONCAT(MAX(s.SP))=maxsp', {      join = 'ssc.Skill=s.Name,s._ID=sm._rowID,s._ID=sw._rowID,sm._value=m.WikiName,sw._value=w.WikiName',      where = "Exclusive=0",      groupBy = 'move,wep',      limit = 1000,    }), function (v) return v.move end), function (vs)      return Hash.map_self(List.group_by(vs, function (v) return v.wep end), function (x) return tonumber(x[1].maxsp) or 0 end)   end)

-- Create list of max inheritable sp for slots for each class and movement type. classList['Infantry']['Red Sword']['Special'] will return the highest possible SP cost of a inheritable special skill that can be used by infantry red swords. local moves = List.map_self(cargo.query('MoveTypes', 'WikiName', {groupBy = 'WikiName'}), function (v) return v.WikiName end) local weapons = List.map(weaponQuery1, function (v) return v.WikiName end) local classList = Hash.generate(moves, function (move)   return Hash.generate(weapons, function (wep) return Hash.zip(classMaxSP[move][wep], {weapon = 350, sacredseal = maxSPSeals[move][wep]}, function (x, y)       return math.max(x, y or 0) -- refined weapons have 350 SP, seals may come from non-seal-exclusive passives      end) end) end)

-- Create duel skills list, duelSkills['Infantry']['Red Sword'] exists if a skill for that combination exists, nil if not local duelSkills = Hash.map_self(List.group_by(cargo.query(   'Skills=S,Skills__CanUseMove=M,Skills__CanUseWeapon=W,Skills__Required=R,Skills=After',    'M._value=mov,W._value=wep,S.Name=name,S.SP=sp', {      join = 'S._ID=M._rowID,S._ID=W._rowID,S.WikiName=R._value,R._rowID=After._ID',      where = "S.Name REGEXP '^alpha: Duel (Infantry|Armored|Cavalry|Flying) digit:$' AND After._ID IS NULL",      groupBy = 'mov,wep',    }), function (v) return v.mov end), function (vs)      return Hash.from_ipairs(vs, function (x) return x.wep, {rank = tonumber(mw.ustring.match(x.name, '%d+$')), sp = tonumber(x.sp)} end)   end)

-- List of Heroes local heroQuery = List.map_self(cargo.query( 'Units', '_pageName', { where = "IFNULL(Properties__full,'') NOT LIKE '%enemy%'", groupBy = '_pageName', limit = 9000, }), function (v) return v._pageName end) local maxSPQuery = Hash.map_self(List.group_by(CargoUtil.full_query(   'UnitSkills,Skills,Units',    'Units._pageName=Hero,Scategory,CONCAT(MAX(Skills.SP))=MaxSP', {      join = 'UnitSkills.skill=Skills.WikiName,UnitSkills.WikiName=Units.WikiName',      where = "IFNULL(Units.Properties__full,'') NOT LIKE '%enemy%'",      groupBy = 'Units.WikiName,Scategory',    }), function (v) return v.Hero end), function (sps)      return Hash.from_ipairs(sps, function (row) return row.Scategory, tonumber(row.MaxSP) end)   end) local statsQuery = Hash.from_ipairs(cargo.query( 'Units=h,UnitStats=hg,LegendaryHero=l,DuoHero=d', h._pageName=_pageName,h.MoveType=MoveType,h.WeaponType=WeaponType,IFNULL(l.Duel, d.Duel)=duel,     Lv1HP5=HPBase,Lv1Atk5=AtkBase,Lv1Spd5=SpdBase,Lv1Def5=DefBase,Lv1Res5=ResBase,      HPGR3=HPGrowth,AtkGR3=AtkGrowth,SpdGR3=SpdGrowth,DefGR3=DefGrowth,ResGR3=ResGrowth   , { join = 'h.WikiName=hg.WikiName,h._pageName=l._pageName,h._pageName=d._pageName', groupBy = 'h.WikiName', where = "IFNULL(Properties__full,'') NOT LIKE '%enemy%'", limit = 9000, }), function (v) return v._pageName, v end) local avail = HeroUtil.getLowestRarities {current = true} local mergeQuery = Hash.from_ipairs(cargo.query( 'Distributions=d,Units=h', 'h._pageName=_pageName,FORMAT(SUM(Amount), 0)=amount', { join = 'd.Unit=h._pageName', groupBy = '_pageName', limit = 9000, }), function (v) return v._pageName, v end)

local heroPoints = Hash.generate(heroQuery, function (hero)   local statsInfo = statsQuery[hero]    if not statsInfo then return nil; end    local maxSPInfo = maxSPQuery[hero] or {}    local mergeInfo = mergeQuery[hero]    local limited = not avail[hero] or (#avail[hero].nonfocus == 0 and #avail[hero].focus == 0)    local useSE1D = false

local baseStats = List.map_self({statsInfo.HPBase, statsInfo.AtkBase, statsInfo.SpdBase, statsInfo.DefBase, statsInfo.ResBase}, function (x) return tonumber(x) or false end) local growths = List.map_self({statsInfo.HPGrowth, statsInfo.AtkGrowth, statsInfo.SpdGrowth, statsInfo.DefGrowth, statsInfo.ResGrowth}, function (x) return tonumber(x) or false end) local total = nil if not List.find(baseStats, false) and not List.find(growths, false) then total = 0 for i, g in ipairs(growths) do       total = total + baseStats[i] + FEHStatUtil.getGrowthValue(5, g)      end end

local moveType = statsInfo.MoveType local weaponType = statsInfo.WeaponType local cl = classList[moveType][weaponType] local mergeAmount = limited and mergeInfo and math.min(tonumber(mergeInfo.amount or 1) - 1, 10) or 10 local firstMergePlus = mergeAmount >= 1 and 3 or 0 local bstplus = hasHigh(growths) total = total and total + firstMergePlus + (bstplus and 1 or 0) or 0 -- Use Standard Effect 1: Duel if applicable if (not Util.isNilOrEmpty(statsInfo.duel)) and tonumber(statsInfo.duel) > total then useSE1D = true total = tonumber(statsInfo.duel) end

return { Hero = hero, MoveType = moveType, WeaponType = weaponType, MaxSP = Hash.zip(cl, maxSPInfo, function (x, y) return math.max(x, y or 0) end), TotalStats = total, Limited = limited, Merges = mergeAmount, useSE1D = useSE1D } end)

-- calculate score potentials, convert heroes into a list local scorePotential = Hash.values(Hash.map_self(heroPoints, function (v) local mergePoints = v['Merges'] * 2 local SP = v.MaxSP.weapon + v.MaxSP.assist + v.MaxSP.special + v.MaxSP.passivea + v.MaxSP.passiveb + v.MaxSP.passivec + v.MaxSP.sacredseal local statTotal = v['TotalStats'] local raw = mergePoints + math.floor(statTotal * 0.2) + (SP * 0.01) local useDuelTotal = false local useSE1D = v.useSE1D

-- Account for the Colored Dueling skills that change stat total, will consider if higher sp skills are used instead of those and then takes the larger of the two possible potentials local duel = not noDuel and duelSkills[v.MoveType] and duelSkills[v.MoveType][v.WeaponType] if duel then local newSP = SP + duel.sp - v.MaxSP.passivea -- unequip best slot A passive and equip duel passive local newStatLimit = duel.rank == 3 and 170 or (legendary[v.Hero] or mythic[v.Hero]) and 175 or 180 local newStatTotal = math.max(newStatLimit, statTotal) -- Treat stat total as 170 if less than 170 local newRaw = mergePoints + math.floor(newStatTotal * 0.2) + (newSP * 0.01) if newRaw > raw then SP = newSP statTotal = newStatTotal raw = newRaw useDuelTotal = true useSE1D = false end end

return { hero = v.Hero, move = v.MoveType, wep = v.WeaponType, bst = statTotal, merges = v.Merges, maxsp = SP, raw = raw, isDuel = useDuelTotal, useSE1D = useSE1D } end))

local scoreRows = Hash.map_self(List.group_by(scorePotential, function (v) local potential = math.floor(v.raw) return potential >= CUTOFF and (tostring(potential) .. (plusHeroes[v.hero] and '+' or '')) or ('< ' .. tostring(CUTOFF)) end), function (vs) --   table.sort(vs, function (v1, v2) return v1.raw > v2.raw or (v1.raw == v2.raw and v1.hero < v2.hero) end)    table.sort(vs, function (v1, v2) return v1.hero < v2.hero end)    return vs  end)

-- insert gaps local rawScores = List.map(scorePotential, function (v) return math.floor(v.raw) end) local minScore = math.min(unpack(rawScores)) local maxScore = math.max(unpack(rawScores)) for i = minScore, maxScore do   if not scoreRows[tostring(i)] then scoreRows[tostring(i)] = {} end end

-- Initialize the table local tbl = mw.html.create('table') :addClass('wikitable tierlist sortable') :css('text-align','center') :css('width', '100%') :css('max-width','1200px')

local BORDER_STYLE = 'solid 2px black'

-- Add table headers tbl:tag('th'):wikitext('Potential'):css('width', '2%'):css('border-bottom', BORDER_STYLE):css('border-right', BORDER_STYLE) for _, col in ipairs(COLORS) do   tbl:tag('th'):wikitext((''):format(col,col,col)):css('width', '24.5%'):addClass('unsortable'):css('border-bottom', BORDER_STYLE) end

-- reverse sort, except cutoff row always comes last local sortfn = function (v1, v2, k1, k2) if k1:find('^<') then return false; end if k2:find('^<') then return true; end return k1 > k2 end for potential, vs in Hash.sorted_pairs(scoreRows, sortfn) do   local cells = Hash.map_self(List.group_by(vs, function (v) return weaponColorsInfo[v.wep] end), function (vs)      return List.map_self(vs, function (v) --       return Util.getHeroIcon(v.hero, '40px') return frame:expandTemplate{title = 'Tooltip', args = { Util.getHeroIcon(v.hero, '40px'), ('Max stat total: %d%s Max merges: %d Max SP: %d Raw potential: %.1f'):format(v.bst, v.isDuel and ' (Duel)' or v.useSE1D and " (Hero Duel)" or "", v.merges, v.maxsp, v.raw), line = 'false', }}     end)    end)

local tr = tbl:tag('tr'):css('height', '43px') tr:tag('td'):wikitext( .. potential .. ):css('border-right', BORDER_STYLE) for _, col in ipairs(COLORS) do     tr:tag('td'):wikitext(table.concat(cells[col] or {}, '&#32;')) end end

return tostring(tbl) end

return require 'Module:MakeMWModule'.makeMWModule {List = List}