Dungeon Boss Wiki
Register
Advertisement

Documentation for this module may be created at Module:Herobox/doc

-- <nowiki>
-- This module contains some helper functions for the design Template:Herobox 
--
local p = {}

local hvData = mw.loadData('Module:Hv/data')

libTraits = require('Module:LibTraits')

-- gets all traits or abilities of the given hero and calculates the highest ranked ability or 
-- trait, ignores the lower ranks.
-- arg1: given heroId (will be lower case'd)
-- arg2: flag to choose: either 'traits' or 'abilities'
-- debug console: =p.listFullyAscended({args={'lord zomm', 'traits'}})
--   output     > from the grave-iii/lord of the undead/slow/soulless scourge/mystical-iii
function p.listFullyAscended(frame)
    local heroId = frame.args[1] 
    local attrTraitsOrAbilities = frame.args[2]
    
    -- check args
    if heroId == nil then
        return "ERROR: arg1 missing, expected heroId"
    end
    heroId = string.lower(heroId)
    local heroData = hvData[heroId]
    if heroData == nil then
        return 'ERROR: arg1: unknown heroId ' .. heroId
    end
    if attrTraitsOrAbilities ~= "traits" and attrTraitsOrAbilities ~= "abilities" then
        return 'ERROR: arg2 missing or invalid, expected "traits" or "abilities"'
    end

    local rankToNumber = { ['i'] = 1, ['ii'] = 2, ['iii'] = 3, ['iv'] = 4, ['v'] = 5 }
    local rankToSign = { 'i', 'ii', 'iii', 'iv', 'v' }
    local highestRanks = {}  -- trait/ability with highest rank
    local byAscensions = {} -- trait/ability by lowest Ascension, needed for sort order
    
    local traitsOrAbilities = heroData[attrTraitsOrAbilities]
    local upgrades = heroData['upgrade'] or {}
    
    for abRanked, ascensionNo in pairs(traitsOrAbilities) do 
        local ab, rank = libTraits.splitRank(abRanked)
        mw.log(abRanked, '=>', ab, ' + ', rank, ascensionNo)
        if rank == "" then  -- it's a ability/trait without rank
            rank = 0
        else
            rank = rankToNumber[rank]
        end
        
        local currentRank = highestRanks[ab] or -1
        if rank > currentRank  then
            highestRanks[ab] = rank
        end
        -- a few epic traits (upgrades) might replace non-epic traits
        if upgrades[abRanked] ~= nil then
            highestRanks[ab] = nil  -- remove it because it will be superseded
        end
        
        -- save the lowest ascension for the trait/ability. Needed for correct sort order
        local currentAsc = byAscensions[ab] or 1000
        if ascensionNo < currentAsc  then
            byAscensions[ab] = ascensionNo
        end
    end    
    
    local sortIndex = {}  -- to list traits/abilities in order of the ascensions
    for ab, _ in pairs(highestRanks) do
        sortIndex[#sortIndex+1] = ab
    end
    -- sort by ascension
    table.sort(sortIndex, function (a, b)
        mw.log ('Sorter called', a, "<=>", b);          
        -- if true then return a<b; end -- this line ABORTs! for debug only, to see errors!
        local siA = byAscensions[a]
        local siB = byAscensions[b]
        if siA == siB then
            return a < b
        else
            return siA < siB
        end
    end)

    local result = {}
    for _, ab in ipairs(sortIndex) do
        local rank = highestRanks[ab]
        mw.log ("add rank: ", ab, '-' ,rank)
        if rank > 0 then 
            result[#result+1] = ab .. '-' .. (rankToSign[rank])
        else
            result[#result+1] = ab
        end
    end

    return table.concat(result, "/")
end

-- ---------------------------------------------
local abilitiesData = mw.loadData('Module:Abilities/data')
local sortOrderTypes = { ['Basic Attack'] = 10, ['Special Attack'] = 20, ['Trait'] = 30 }

-- helper to prepare the data structure 'v'. It's used to bring traits and abilities with ranks in the correct order, and to display the infos.
--   what (IN): a trait id or ability id with rank (e.g. 'heart stopper-ii'). It will be added to 'result' as key without the rank.
--   ascension (IN): 0 to 2, defines when the hero gets that trait/ability
--   result (OUT): table that saves the result, built by this function. It's a map with key='ascension id without rank' and value='v'. Example:
--  result = { 
--  ['heart stopper'] = {  -- <== this is a 'v'
--    hasRanks = true,
--    i = 0,
--    ii = 1,
--    iii = 2,
--    numOfRanks = 3,
--    sort = 0 
--  },
--  slow = { 
--    ascension = 0,
--    hasRanks = false,
--    numOfRanks = 1,
--    sort = 0 
--  }, 
--  ..., ... }
--   resultSortindex (OUT): returned list of keys of 'result'. This index should be sorted by
--     v.sort when finished to have the abilities and traits in correct order
function prepareAbilityData(what, ascension, result, resultSortindex)
    mw.log('prepareAbilityData what=', what)
    local abtr, rank = libTraits.splitRank(what)
    mw.log('prepareAbilityData -->', abtr, rank)
    local hasRanks = false
    local ability = abilitiesData[abtr]
    if ability == nil then
        mw.log('ERROR in prepareAbilityData: unknown trait or ability: ' ..  (abtr or "nil"))
        return
    end 
    if ability.ranks ~= 'N/A' then
        hasRanks = true
        if rank == '' then
            rank = 'i'
        end
    else
        -- key to get ascension for traits/abilities without rank
        rank = 'ascension' 
    end
    
    local v = result[abtr]
    local sortIdx = sortOrderTypes[ability.type] + ascension
    if v == nil then
        v = {}
        result[abtr] = v
        v.sort = sortIdx
        v.hasRanks = hasRanks
        v.numOfRanks = 1
        resultSortindex[#resultSortindex+1] = abtr
    else
        -- update order and number of ranks this hero has
        if sortIdx < v.sort then
            v.sort = sortIdx
        end    
        v.numOfRanks = v.numOfRanks + 1
    end
    v[rank] = ascension    
end    

-- helper for wiki markup
function headline(ability, note)
    local cost = ability['energy cost']
    if tonumber(cost) ~= nil then -- number?
        cost = cost .. " turns"
    end
    if (ability.targets or 0) == 5 then
         cost = cost .. ", AoE"
    end
    return ability.type .. ' (' .. cost .. ')' .. note
end

-- helper: hero: hv[hero];  epic: epics[hero]; asc = 0,1,2 or 3=epic, 4=skin
--   return image name for the hero's token, and description (1st Ascension, etc)
function getImageAndRequirement(hero, ability, epic, asc)
    local textReqCondition = { '1st Ascension', '2nd Ascension', 'Epic', 'Skin' }
    local filename
    local requirement = textReqCondition[asc] or "??"
    if 0 <= asc and asc <= 2 then
        filename = hero['token ' .. (asc+1)]
    elseif asc == 3 then
        if (epic ~= nil) then
            filename = (epic['name'] or "Blank Ability") .. '.png'
            requirement = requirement .. ' "' .. epic['name'] .. '"'
        else
            filename = 'Blank Ability.png'
            requirement = requirement .. " (missing epic info)"
        end
    elseif asc == 4 then
        local skinsData = mw.loadData('Module:Skins/data')
        local skinId = ability['skin']
        local skin = skinsData[skinId]
        if (skin ~= nil) then
            filename = skin.token 
            requirement = requirement .. ' "' .. skin.name .. '"'
        else
            filename = 'Blank Ability.png'
            requirement = requirement .. " (missing info for skin " .. skinId ..")"
        end
    end
    if filename == nil or filename == "" then
        filename = "Blank token.png"
    end
    return filename, requirement
end

-- create a tooltip because Tooltip template is gets not expanded when script is #invoked
function makeTooltip(which, id, textToHover) 
   return '<span class="' .. which .. '-tooltip" data-' .. which .. '=' .. '"' .. id .. '">' .. textToHover .. '</span>'
end

-- generated Wiki markup for the abilities and traits table of the Herobox
-- =p.abilitiesAndTraitsTable({ args={ 'Lord Zomm' }})
function p.abilitiesAndTraitsTable(frame)
    
    local args = (frame.args[1] ~= nil) and frame.args or frame:getParent().args
    local heroId = string.lower(args[1])

    local effectsData = mw.loadData('Module:Effects/data')
    local epicsData = mw.loadData('Module:Epics/data')
    local epic = epicsData[heroId]
    local hero = hvData[heroId]
    
    local result = {}
    local sortindex = {}

    -- prepare Basic attack, Abilities, and Traits -> result + sortindex
    prepareAbilityData(hero['basic attack'], 0, result, sortindex)
    local abtr = hero.abilities
    for ab, ascension in pairs(abtr) do
        prepareAbilityData(ab, ascension, result, sortindex)
    end
    abtr = hero.traits
    for trait, ascension in pairs(abtr) do
        prepareAbilityData(trait, ascension, result, sortindex)
    end
    -- add epic traits and traits of Ultimate skins, make them a dummy ascension ('3A' and '4A')
    local epicTrait = hero['epic trait']
    mw.log("epicTrait", epicTrait)
    if epicTrait ~= nil then
        prepareAbilityData(epicTrait, 3, result, sortindex)
    end
    local skinTrait = hero['skin trait']
    if skinTrait ~= nil then
        prepareAbilityData(skinTrait, 4, result, sortindex)
    end
    
    -- sort index by v.sort (=type and ascension)
    table.sort(sortindex, function(a,b) 
        if result[a].sort == result[b].sort then
            return a < b
        else
           return result[a].sort < result[b].sort
        end 
    end)
    
    -- create the wiki table
    local markup = {}
    local rankPlaceholderIdx = 0
    for _, k in pairs(sortindex) do 
        v = result[k]

        markup[#markup+1] = "<br>'''<span style='font-size:larger;'> " .. abilitiesData[k].name 
        markup[#markup+1] = ''  -- placeholder for ranks (I/II..)
        rankPlaceholderIdx = #markup
        markup[#markup+1] = "</span>'''"
        -- <nowiki>
        markup[#markup+1] = '[[File:' .. abilitiesData[k].image .. '|left|60px|link=]]\n'
        -- </nowiki>

        local ability = abilitiesData[k]        
        if v.hasRanks then -- trait/ability with ranks?
            local heroRanks = {}
            local abRanks = ability.ranks
            local note = ""
            if v.numOfRanks > 1 then
                note = ", upgrades with Ascensions"
            end
            mw.log(ability.name, note, ' known ability ranks: ' .. abRanks)

            markup[#markup+1] = '{| border="0" \n'
            markup[#markup+1] = '|-\n'
            markup[#markup+1] = "|'''" .. headline(ability, note).. "'''<br>\n"
            
            -- loop over all ranks of the trait/ability and display those which the hero has
            for rank in string.gmatch(abRanks, '([IV]*)') do 
                local rankId = string.lower(rank)
               
                if v[rankId] ~= nil then  -- has hero that rank?
                    heroRanks[#heroRanks+1] = rank
                    local asc = v[rankId] 
                    if asc > 0 then
                        local imageName, requirement = getImageAndRequirement(hero, ability, epic, asc)
                        markup[#markup+1] = "[[File:" .. imageName .. "|30px]] '''" .. requirement .. ": '''" .. "<br>\n"
                    end        
                    markup[#markup+1] =  "'''" .. rank .. ":''' " .. ability['description ' .. rankId] .. "<br>\n"
                    mw.log('::', rank .. ":",  ability['description ' .. rankId], (note or ""))
                end
            end
            markup[#markup+1] = '\n'
            -- add ranks after headline
            markup[rankPlaceholderIdx] = ' (' .. table.concat(heroRanks, '/') .. ')'
        else -- trait/ability without ranks (like 'Slow')
            local note = ""
            local asc = v.ascension
            if asc > 0 then
                local imageName, requirement = getImageAndRequirement(hero, ability, epic, asc)
                note = ", requires [[File:" .. imageName .. "|30px]] " .. requirement
            end
           
            markup[#markup+1] = '{| border="0" \n'
            markup[#markup+1] = '|-\n'
            markup[#markup+1] = "|'''" .. headline(ability, note).. "'''<br>\n"
            markup[#markup+1] =  ability.description
            markup[#markup+1] = '\n'
            mw.log(ability.name, note)
            mw.log('::', ability.description, "\n")
        end    
        
        -- add buff/debuff info if present
        if ability.effects ~= nil then
            markup[#markup+1] = "" -- placeholder to insert heading if there are effects
            local idxPlaceholder = #markup   -- save index
            for _, eid in ipairs(ability.effects) do
                fx = effectsData[eid] or { image = "Blank Ability.png", name = eid }
                markup[#markup+1] =  makeTooltip("effect", eid, '| [[File:' .. fx.image .. '|x25px|link=]] ' .. '[[' .. fx.name .. ']]' )
                markup[#markup+1] = ' '
            end
            if #markup > idxPlaceholder then -- effects present?
                markup[idxPlaceholder] = "<br>\n'''Effects '''"
                markup[#markup+1] = '\n'
            end
        end
        markup[#markup+1] = '|}\n'  -- table end
    end
    return table.concat(markup)
end

-- Utility function that concats hero.speed/<classicTags>/hero.families/hero.classes
--   only if type='classic' then Armored, Magical, Flying are added 
--   hero is loaded from hv/data
-- debug console: 
--   =p.herotags({args={'lord zomm'}})   -> output: Slow/Undead/Caster
--   =p.herotags({args={'solaris', tags ='classic'}})     -> output: Magical/Flying/Beast/Healer
function p.herotags(frame)
    local args = frame.args
    local heroId = string.lower(args[1])
	
    local hero = hvData[heroId]
    local tags = {}

    if args['tags'] == 'classic' then
    	mw.log("classic tags requested")
    	local classicTraits = { armored = true, magical = true, flying = true };
		for traitId, _ in pairs(hero.traits) do
			local traitId = libTraits.splitRank(traitId)
			if classicTraits[traitId] then 
				tags[#tags+1] = abilitiesData[traitId].name
			end
		end
	end

    mw.log('speed', hero.speed)
    if (hero.speed == 'Fast' or hero.speed == 'Slow') then
        tags[#tags+1] = hero.speed
    end
    
    for _, v in pairs(hero.families) do
        tags[#tags+1] = v
    end
    for _, v in pairs(hero.classes) do
        tags[#tags+1] = v
    end
    
    return table.concat(tags, "/")
end

-- Categorizes Abilities/Traits by given attribute (e.g. damage type). Returns unique categories
-- uses global var abilitiesData (Module:Abilities/data)
-- debug console: 
--   =p.categorize({args={'ability', 'Swing/Soul Shred/Spreading Panic/Double Chop', 'damage'}})
--   output: Physical
function p.categorize(frame)
    local args = frame.args
    local what = args[1]      -- reserved (unused) "ability" or "trait"; maybe hero in future
    local abilityList = string.lower(args[2])  -- parser list: "soul shred/../.."
    local attr = args[3]                       -- attribute "damage" or "effects", etc
        
    local cats = {}
    if what == 'ability' or what == 'trait' then
        for abRanked in string.gmatch(abilityList,'([^/]+)') do -- loop over parser list
            local abId, rank = libTraits.splitRank(abRanked)
            --mw.log('abId: ' .. abId)            
            local ability = abilitiesData[abId]
            if ability ~= nil then
                local v = ability[attr] -- or ("None")
                if v ~= nil then
                    if type(v) ~= 'table' then --it's a simple type 
                        cats[v] = true
                    else -- it's a table like ability.effects, categorize the values
                       for _, vv in pairs(v) do
                          cats[vv] = true                           
                       end
                    end
                end
            end
        end
    else 
        return 'Error: invalid keyword (arg1): word "ability" or "trait" expected'
    end
    local result = {}
    for k, _ in pairs(cats) do
        result[#result+1] = k
    end
    table.sort(result)
    return table.concat(result, "/")
end

-- ----------------------------------------------------
-- helper for p.resistances
local function fillDebuffResists(resistances, debuffs, resistValue)
   for _, debuff in ipairs(debuffs) do
        resistances[debuff] = resistValue;
    end
end

-- Calculates the resistances for Herobox/Stats
-- debug console: 
--   =p.resistances({args={'Solaris'}})
--   output: Immune/33%/0%/0%/0%/18%/18%/../0%/0%/Immune/../6%/Immune 
--           (values for resistances (damage types & debuffs))
function p.resistances(frame)
    local heroId = frame.args[1] 
    heroId = string.lower(heroId)
    local heroData = hvData[heroId]
    if heroData == nil then
        return 'ERROR: arg1: unknown heroId ' .. heroId
    end
    local damageTypes = { "fire", "nature", "water", "light", "dark", "spirit" }        
    local debuffResists = {
        ["fire"] = { "burned" }, 
        ["nature"] = { "poisoned", "wounded" },
        ["water"] = { "frozen", "chilled", "cryosleep", "frostbitten" },
        -- Hack: "Lightning" is own category but put it to Light so we can use hero.element
        ["light"] = { "lightning", "shocked", "paralysis" }, 
        ["dark"] = { "diseased", "banished" },
        ["spirit"] = {"feared", "stoned", "possessed", "marked"} 
    }
    local resist2Elem = { ["fire"] = "water"; ["water"] = "nature"; ["nature"] = "fire" }
    -- immunities must be a map to check for, used here also for mapping immunity -> debuff
    local immunities = {
        ["fire immune"] = "fire", ["water immune"] = "water", ["nature immune"] = "nature", ["light immune"] = "light", ["dark immune"] = "dark", ["spirit immune"] = "spirit", ["burn immune"] = "burned", ["poison immune"] = "poisoned", ["wound immune"] = "wounded", ["freeze immune"] = "frozen", ["chill immune"] = chilled, ["cryosleep immune"] = "cryosleep", ["frostbite immune"] = "frostbitten", ["lightning immune"] = "lightning", ["shock immune"] = "shocked", ["paralysis immune"] = "paralysis", ["disease immune"] = "diseased", ["banish immune"] = "banished", ["spirit immune"] = "spirit", ["fear immune"] = "feared", ["stone immune"] = "stoned", ["possess immune"] = "possessed", ["mark immune"] = "marked" 
    }
    local resistances = { } -- result 
    local element = string.lower(heroData.element)
    
    -- fill base resistances (primary, secondary & Spirit)
    -- and base debuff resistances depending on element resist.
    resistances[element] = 33;
    local debuffs = debuffResists[element]
    fillDebuffResists(resistances, debuffs, 33)

    local resist2 = resist2Elem[element]
    local debuffs = debuffResists[resist2]
    if resist2 ~= nil then
        resistances[resist2] = 18;
        fillDebuffResists(resistances, debuffs, 18)
    end
    resistances['spirit'] = 6;
    local debuffs = debuffResists['spirit']
    for _, debuff in ipairs(debuffs) do
        fillDebuffResists(resistances, debuffs, 6)
    end

    -- add immunities & resistances (like NubNub's Spirit resist)
    local abilitiesData = mw.loadData('Module:Abilities/data')
    local traits = heroData['traits']
    for traitRanked, _ in pairs(traits) do 
        local traitId, rank = libTraits.splitRank(traitRanked)
        local trait = abilitiesData[traitId]
        
        local effects = trait.effects
        if effects ~= nil then
            for _, fx in ipairs(effects) do
                mw.log("Is immunity?", fx)
                if immunities[fx] ~= nil then
                    resistances[immunities[fx]] = 'Immune'
                    mw.log('--> yes')
                end
            end
        end
        local values = trait.resistances 
        if values ~= nil then    -- it's a map, add percentage, cap at 75%
            for resist, percentage in pairs(values) do
                if resistances[resist] ~= 'Immune' then
                    resistances[resist] = math.min((resistances[resist] or 0) + percentage, 75)
                end
            end
        end
    end
    -- if construct make immune to all debuffs
    local isConstruct = false
    for _, family in ipairs(heroData.families) do
        mw.log(family)
        if family == "Construct" then
            isConstruct = true
        end
    end

    -- return result values in correct order   
    local result = {} 
    for _, damType in ipairs(damageTypes) do
        local v = resistances[damType] or 0
        if v ~= 'Immune' then v = v .. '%' end
        result[#result+1] = v 
        debuffs = debuffResists[damType]
        for _, debuff in ipairs(debuffs) do
            local v = resistances[debuff] or 0
            if isConstruct then v = 'Immune' end  -- override for constructs
            if v ~= 'Immune' then v = v .. '%' end
            result[#result+1] = v 
        end
    end    
    return table.concat(result, "/")
end

function createStatsMap() 
    return { ["Attack"]=0, ["Defense"]=0, ["Skill"]=0, ["Max Health"]=0, ["Crit Chance"]=0,
        ["Crit Multiplier"]=0, ["Dodge Chance"]=0, ["Damage Penetration"]=0, ["Damage Reduction"]=0,
        ["Chaos Damage Reduction"]=0 }
end

function epicStatBoosts(heroId, statsOrder)
    local epicsData = mw.loadData('Module:Epics/data')
    local epic = epicsData[heroId]
    if epic == nil then
        return { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }  -- no epic
    end
    local boosts = createStatsMap()
    local stat1, stat2, stat3 = epic['stats boosted'][1], epic['stats boosted'][2], epic['stats boosted'][3]
    boosts[stat1] = 30
    boosts[stat2] = (stat2 == 'Crit Chance') and 10 or 20 -- +10% for CRIT else +20%
    boosts[stat3] = (stat3 == 'Crit Chance') and 10 or 20

    local result = {}
    for _, stat in pairs(statsOrder) do
        mw.log("esb result:", _, stat, boosts[stat])
        result[#result+1] = boosts[stat]
    end

    return result
end

function passiveStatBoosts(heroId, statsOrder, skillFactor)
   
    local abilitiesData = mw.loadData('Module:Abilities/data')
    local boosts = createStatsMap()
    local hero = hvData[heroId]
    if hero == nil then
        return { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, {}  -- hero not found
    end
    local traitDone = {} -- map to filter Traits with ranks like Alrakis' Tempered Soul I-III
    local traitIDs = {} -- map to filter doubles like Slow (boosts ATK+HP)
    for traitAndRank, _ in pairs(hero.traits) do
        mw.log(traitAndRank)
        local traitId, rank = libTraits.splitRank(traitAndRank)
        local trait = abilitiesData[traitId]
        if (traitDone[trait] == nil) then
            traitDone[trait] = true
            local statBoost = trait["stat boost"]
            if statBoost ~= nil and statBoost.target.self then
                for statId, boostValue in pairs(statBoost.boosts) do
                    mw.log("-->", statId, boostValue, skillFactor)
                    local statBoost = math.floor(boostValue * skillFactor)
                    if (boostValue < 0) then -- ATK penality for fast!
                        statBoost = math.floor(boostValue)
                    end
                    boosts[statId] = (boosts[statId] or 0) + statBoost
                    traitIDs[traitId] = true 
                end
            end
        end -- traitDone
    end
    -- convert maps to lists
    local result = {}
    local resultTraits = {}
    for _, stat in pairs(statsOrder) do
        result[#result+1] = boosts[stat]
    end
    for traitId, _ in pairs(traitIDs) do
        resultTraits[#resultTraits+1] = traitId
    end

    return result, resultTraits
end

-- returns the stat boost values for traits (like Slow) and Epic for given Hero. 
--   Used by Template:Herobox
-- =p.statBoosts({args={'Lord Zomm'}})
-- Output: 50/0/0/0/0+lord of the undead/slow+20/30/0/0/10
function p.statBoosts(frame)
    local heroId = frame.args[1] 
    heroId = string.lower(heroId)

    local baseValues = { -1, -1, -1, -1,  5, 200, 0, 0, 0, 0 } 
    -- -1 = flag to indicate it's ATK, DEF, .. with is affected by Stars and Epic
    local statsOrder = { "Attack", "Defense", "Skill", "Max Health", "Crit Chance",
        "Crit Multiplier", "Dodge Chance", "Damage Penetration", "Damage Reduction",
        "Chaos Damage Reduction" } -- order of returned values

    local hero = hvData[heroId]
    local rarity = { ['Bronze'] = 0, ['Silver'] = 5, ['Gold'] = 15, ['Legendary'] = 30 }
    local heroRarity = hero.rarity 
    local rarityFactor = 1 + (rarity[heroRarity] / 100)
    local skillBonus = hero["max skill"] * rarityFactor * 2 
    local skillFactor = 1 + (skillBonus / 20000)
    local epicBoosts = epicStatBoosts(heroId, statsOrder)
    local passiveBoosts, traits = passiveStatBoosts(heroId, statsOrder, skillFactor)
    local baseStats = { hero["max attack"], hero["max defense"], hero["max skill"], hero["max health"], 5, 200, 0, 0, 0, 0 }
    local starBoost = { 100 * rarityFactor, 100 * rarityFactor, 100 * rarityFactor, 100 * rarityFactor, 0, 0, 0, 0, 0, 0 }
    local totalBoosts = {}
    for ii, epcBoost in ipairs(epicBoosts) do
        if (ii <= 4) then
            totalBoosts[#totalBoosts+1] = math.floor((100 + passiveBoosts[ii] + epcBoost) * rarityFactor * 2 + .5) - 100  -- the *2 is the 100% bonus of 6 stars 
            mw.log(baseStats[ii], passiveBoosts[ii], epcBoost, rarityFactor, 2, "=", totalBoosts[#totalBoosts])
        else
            totalBoosts[#totalBoosts+1] = baseStats[ii] + passiveBoosts[ii] + epcBoost
        end
    end
    -- fix CRIT Chance. It's +1% for ~81.6 SKL, max 50
    local critSkillBonus = math.floor(skillBonus / 81.6 + 0.5)
    mw.log("skillBonus", critSkillBonus)
    totalBoosts[5] = math.min(totalBoosts[5] + critSkillBonus, 50)
    starBoost[5] = critSkillBonus

    -- Base + Star Bonus + Trait Bonus + Epic Bonus + Total + Traits 
    return table.concat(baseStats, "/") .. "+"
        .. table.concat(starBoost, "/") .. "+"
        .. table.concat(passiveBoosts, "/") .. "+"
        .. table.concat(epicBoosts, "/") .. "+"
        .. table.concat(totalBoosts, "/") .. "+"
        .. table.concat(traits, "/") 
end

-- =p.ascensionCosts({args={'Lord Zomm'}})
function p.ascensionCosts(frame)
    local heroId = frame.args[1] 
    heroId = string.lower(heroId)
    local hero = hvData[heroId]
    if hero == nil then
        return -1  -- hero not found
    end
    local evosData = mw.loadData('Module:Evos/data')
    local results = { 0, 0}
    for idx, heroEvos in ipairs(hero.evos) do
        local score = 0
        for evo, amount in pairs(heroEvos) do
            evo = string.lower(evo)
            score = score + evosData[evo]["gem cost"] * amount
            --mw.log(idx, evo, amount, evosData[evo]["gem cost"], score)
        end
        results[idx] = score
        --mw.log("=>", score)
    end
    return table.concat(results, "/")
end

-- =p.combatStyle({args={'Lord Zomm'}})
-- calcualte the combat style of the given hero (1=basic attacks <==> 9=special attacks)
function p.combatStyle(frame)
    local heroId = frame.args[1] 
    heroId = string.lower(heroId)
    local hero = hvData[heroId]
    if hero == nil then
        return -1  -- hero not found
    end

    local skipRanks = {}
    local style = 1 -- let's start with basic attacks and adjust it when special attacks deal damage
    local abilitiesData = mw.loadData('Module:Abilities/data')
    local abilities = hero['abilities']
    local epicsData = mw.loadData('Module:Epics/data')
    for ab, _ in pairs(abilities) do
        local ability = abilitiesData[ab]
        if ability == nil then -- not found. Check if it has a rank (like Furnace')
            ab = libTraits.splitRank(ab)
            ability = abilitiesData[ab]
            if skipRanks[ab] ~= nil then 
                ability = nil  -- skip this. already processed. TODO: but might be higher rank
            else
                skipRanks[ab] = true
            end 
        end
        if ability ~= nil then
            if ability.damage ~= nil then
                mw.log(ab, ability.damage, ability.targets) -- log
                style = style + 2
                if (ability.targets or 0) > 1 then -- bonus for multi targets
                    style = style + 1
                end 
            end
        end
    end
    -- Check dual wielder like Grog-Gnog, Hagrim
    if (hero['basic attack'].targets or 1) > 1 then
        style = style - 1
    end
    -- if hero has an epic adjust style a bit to "basic attack"
    if epicsData[heroId] ~= nil then
        style = style - 1
        mw.log("epic") -- log
        if (epicsData[heroId].targets or 0) >= 4 then -- AoE attack?
            style = style - 1
        end
    end
    
    -- keep it in range 1 to 9
    style = math.max(math.min(style, 9), 1)
    
    return style
end


return p  -- </nowiki>
Advertisement