Module:SkillBuildInfobox

local p = {} local Util = require 'Module:Util' local List = require 'Module:ListUtil' local Hash = require 'Module:HashUtil' local escq = require 'Module:EscQ'.main1 local memoizer = require 'Module:Memoizer'.memoizer local stripWikitext = require('Module:StripWikitext').main1 local superimpose = require 'Module:Superimpose'._main local cargo = mw.ext.cargo

-- Function that returns the investment cost of a build for a specific hero given a set of skills function p._getBuildCost(hero, skills) --Get all the default skills for the hero this build is about local defaultSkills = List.to_set(List.map_self(cargo.query('UnitSkills,Units', 'skill', {   join = 'UnitSkills.WikiName=Units.WikiName',    where = ("Units._pageName='%s' AND IFNULL(Properties__full,'') NOT LIKE '%%enemy%%'"):format(escq(hero)),    limit = 50,  }), function (v) return v.skill end))

local nondefault = List.select(skills, function (s)   -- If the skill is flexible or positioning assist, then do nothing    -- We will consider both as low cost.    local l = mw.ustring.lower(s)    return l ~= '' and l ~= 'flexible' and l ~= 'positioning skill' and not defaultSkills[s]  end)

local skillCosts = List.map(nondefault, function (skill)   local costQuery = cargo.query('Skills', 'SkillBuildCost', { where = ("WikiName='%s'"):format(escq(skill)), limit = 1, })   return costQuery[1] and costQuery[1].SkillBuildCost or 'N/A'  end)

return List.find(skillCosts, 'N/A') and 'N/A' or   List.find(skillCosts, 'veryhigh') and 'veryhigh' or    List.find(skillCosts, 'high') and 'high' or    #nondefault >= 3 and 'medium' or    'low' end

function p.getBuildCost(frame) if not frame.expandTemplate then function frame:expandTemplate (...) return mw.getCurrentFrame:expandTemplate(...) end end

local args = frame.args local hero = args[1]

-- Putting all the skill arguments into one table to pass in -- weapon, assist, special, passivea, passiveb, passivec local skills = { frame:expandTemplate{title = 'SkillWikiName', args = {args[2] or ''}}, frame:expandTemplate{title = 'SkillWikiName', args = {args[3] or ''}}, frame:expandTemplate{title = 'SkillWikiName', args = {args[4] or ''}}, frame:expandTemplate{title = 'SkillWikiName', args = {args[5] or ''}}, frame:expandTemplate{title = 'SkillWikiName', args = {args[6] or ''}}, frame:expandTemplate{title = 'SkillWikiName', args = {args[7] or ''}}, }

return p._getBuildCost(hero, skills) end

local DEFAULT_ICONS = { weapon = "Icon_Skill_Weapon.png", assist = "Icon_Skill_Assist.png", special = "Icon_Skill_Special.png", a = "Empty_Passive_Icon.png", b = "Empty_Passive_Icon.png", c = "Empty_Passive_Icon.png", seal = "Empty_Passive_Icon.png", } local MISSING_ICONS = { weapon = "Icon_Skill_Weapon.png", assist = "Icon_Skill_Assist.png", special = "Icon_Skill_Special.png", a = "Passive_No_Image.png", b = "Passive_No_Image.png", c = "Passive_No_Image.png", seal = "Passive_No_Image.png", }

local memoizeSkill = (function 	local CATEGORY_QUERIES = {		weapon = "Scategory='weapon'",		assist = "Scategory='assist'",		special = "Scategory='special'",		a = "Scategory='passivea'",		b = "Scategory='passiveb'",		c = "Scategory='passivec'",		seal = "Scategory LIKE 'passive_' OR Scategory='sacredseal'",	}

return memoizer(function (skillName, category, refine)		if skillName == '-' or Util.isNilOrEmpty(skillName) then			return {ic = DEFAULT_ICONS[category], page = , name = 'Flexible', desc = , cost = 'low', ex = '0'}		elseif mw.ustring.lower(skillName) == 'flexible' or (mw.ustring.lower(skillName) == 'Positioning Skill' and category == 'assist') then			return {ic = DEFAULT_ICONS[category], page = , name = skillName, desc = , cost = 'low', ex = '0'}		else			return cargo.query("Skills", "CONCAT(Icon)=ic,_pageName=page,Name=name,Description=desc,WikiName=wikiname,SkillBuildCost=cost,RefinePath=refine,CanUseMove__full=mov,CanUseWeapon__full=wep,Exclusive=ex", { where = ("'%s' IN (Name,_pageName,WikiName) AND (%s) AND IFNULL(RefinePath,'-')='%s'"):format(escq(skillName), CATEGORY_QUERIES[category], refine or "-"), limit = 1, })[1] or {invalid = true, ic = MISSING_ICONS[category], cost = 'N/A', ex = '0'}		end	end, function (skillName, category, refine)		return (skillName or '-') .. ';' .. (category or '-') .. ';' .. (refine or '-')	end) end)

local Template_SkillPage_nocargo = function (frame, page, name, desc) if Util.isNilOrEmpty(name) then return Util.isNilOrEmpty(page) and '' or ('%s'):format(page) end if Util.isNilOrEmpty(page) then return name or '' end return ('%s'):format(page, Util.isNilOrEmpty(desc) and name or frame:expandTemplate {title = 'Hover', args = {name, stripWikitext(desc)}}) end

local infoboxHeaderField = function (image, text, tooltip, attr) return (' %s '):format(		attr or '', image, mw.getCurrentFrame:callParserFunction('#tip-text', text, tooltip)) end

-- Build Type Switch local getTypeTexts = function (typ) if typ == 'optimal' then return 'optimal', infoboxHeaderField('Dueling Sword.png', 'Optimal', 'Builds aiming to utilize the full potential of the unit in competitive environments') elseif typ == 'alternative' then return 'alternative', infoboxHeaderField('Build Icon Alternative.png', 'Alternative', 'Builds aiming for affordability or ease of use rather than optimal performance') elseif typ == 'creative' then return 'creative', infoboxHeaderField('Pen.png','Creative', 'Unconventional builds, or builds centering around specific tasks or themes') else return 'unlabelled', nil end end

-- Build Mode Switch local getModeTexts = function (mode) if mode == 'arena' then return 'arena', infoboxHeaderField('Dueling Crest.png', 'Arena', 'Builds recommended for general competitive arena use') elseif mode == 'defense' then return 'defense', infoboxHeaderField('Icon Arena Defense.png', 'Defense', 'Builds recommended for arena defense teams') elseif mode == 'assault' then return 'assault', infoboxHeaderField('Sacred Coin.png', 'Assault', 'Builds recommended for Assault game modes and Hero Battles, requiring specific counters to expected enemies') elseif mode == 'chain' then return 'chain', infoboxHeaderField('Stamina Potion.png', 'Chain', 'Builds recommended for endurance game modes, including Chain Challenges and Tempest Trials') elseif mode == 'ar-o' then return 'ar-o', infoboxHeaderField('Golden Throne.png', 'Aether Raids offense', 'Builds recommended for Aether Raids offense') elseif mode == 'ar-d' then return 'ar-d', infoboxHeaderField('Structure Fortress.png', 'Aether Raids defense', 'Builds recommended for Aether Raids defense') elseif mode == 'all' then return 'allpurpose', infoboxHeaderField('Build Icon Allpurpose.png', 'All-purpose', 'Builds that work well in most game modes') else return 'unlabelled', nil end end

-- Build Cost Switch local getCostTexts = function (cost) if cost == 'low' then return 'low', infoboxHeaderField('Universal Shard.png', 'Low investment', "Requires no skill inheritance from any 5★ rarity units, uses hero's base kit while filling empty skill slots (Typically 2 or less skills)") elseif cost == 'medium' then return 'medium', infoboxHeaderField('Universal Crystal.png', 'Medium investment', "Requires no skill inheritance from any 5★ rarity units, 3 or more skills need to be inherited") elseif cost == 'high' then return 'high', infoboxHeaderField('Hero Feather Darkened.png', 'High investment', "Requires skill inheritance from 5★ rarity units, but no units only available at 5★ rarity",			'data-build-cost="high"') elseif cost == 'very high' or cost == 'veryhigh' then return 'veryhigh', infoboxHeaderField('Orb.png', 'Very high investment', "Requires at least one skill from a unit only available at 5★ rarity",			'data-build-cost="very high"') else return 'undefined', nil end end

-- Build Emblem Switch local getEmblemTexts = function (emblem) if emblem == 'infantry' then return 'infantry', infoboxHeaderField('Icon Move Infantry.png', 'Infantry', 'This build reaches its full potential in infantry synergistic teams') elseif emblem == 'armored' or emblem == 'armor' then return 'armor', infoboxHeaderField('Icon Move Armored.png', 'Armored', 'This build reaches its full potential in armor synergistic teams') elseif emblem == 'horse' or emblem == 'cavalry' then return 'cavalry', infoboxHeaderField('Icon Move Cavalry.png', 'Cavalry', 'This build reaches its full potential in cavalry synergistic teams') elseif emblem == 'flying' or emblem == 'flier' then return 'flying', infoboxHeaderField('Icon Move Flying.png', 'Flying', 'This build reaches its full potential in flier synergistic teams') elseif emblem == 'bow' or emblem == 'bows' then return 'bow', infoboxHeaderField('Icon Class Bow.png', 'Bow', 'This build reaches its full potential in bow synergistic teams') elseif emblem == 'dagger' or emblem == 'daggers' then return 'dagger', infoboxHeaderField('Icon Class Dagger.png', 'Dagger', 'This build reaches its full potential in dagger synergistic teams') elseif emblem == 'tome' or emblem == 'magic' then return 'magic', infoboxHeaderField('Icon Class Magic.png', 'Magic', 'This build reaches its full potential in magic synergistic teams') elseif emblem == 'dragon' or emblem == 'dragons' then return 'dragon', infoboxHeaderField('Icon Class Dragonstone.png', 'Dragon', 'This build reaches its full potential in dragon synergistic teams') elseif emblem == 'beast' or emblem == 'beasts' then return 'beast', infoboxHeaderField('Icon Class Beast.png', 'Beast', 'This build reaches its full potential in beast synergistic teams') else return 'nonemblem', nil end end

local CANONICAL_REFINES = { ['+attack'] = 'atk', ['+atk'] = 'atk', ['attack'] = 'atk', ['atk'] = 'atk', ['+speed'] = 'spd', ['+spd'] = 'spd', ['speed'] = 'spd', ['spd'] = 'spd', ['+defense'] = 'def', ['+def'] = 'def', ['defense'] = 'def', ['def'] = 'def', ['+resistance'] = 'res', ['+res'] = 'res', ['resistance'] = 'res', ['res'] = 'res', ['skill1'] = 'skill1', ['skill 1'] = 'skill1', ['skill'] = 'skill1', ['skill2'] = 'skill2', ['skill 2'] = 'skill2', }

local REFINE_DISP = { atk = 'Atk', spd = 'Spd', def = 'Def', res = 'Res', skill1 = 'Skill', skill2 = 'Skill 2', }

--[[ port of Template:Skillbuild Infobox

todo: - use a nested table instead of flex container to display the skills - sanitize the HeroBuilds table a bit more - fix double store (it happens whenever the description text uses templates like SkillText that would reset parser state) ]] p._main = function (args, frame) local hero = args.hero or mw.title.getCurrentTitle.subpageText local weaponRefine = CANONICAL_REFINES[mw.ustring.lower(args.weaponRefine or '')] local weaponRefine2 = CANONICAL_REFINES[mw.ustring.lower(args.weaponRefine2 or '')]

local skillQueries = { weapon1 = memoizeSkill(args.weapon, 'weapon', weaponRefine), weapon2 = memoizeSkill(args.weapon2, 'weapon', weaponRefine2), assist1 = memoizeSkill(args.assist, 'assist'), assist2 = memoizeSkill(args.assist2, 'assist'), special1 = memoizeSkill(args.special, 'special'), special2 = memoizeSkill(args.special2, 'special'), a1 = memoizeSkill(args.passiveA, 'a'), a2 = memoizeSkill(args.passiveA2, 'a'), b1 = memoizeSkill(args.passiveB, 'b'), b2 = memoizeSkill(args.passiveB2, 'b'), c1 = memoizeSkill(args.passiveC, 'c'), c2 = memoizeSkill(args.passiveC2, 'c'), seal1 = memoizeSkill(args.seal, 'seal'), seal2 = memoizeSkill(args.seal2, 'seal'), }

local cost = args.costOverride or 'N/A' local invalid = Hash.any(skillQueries, function (v) return v.invalid end) local heroQuery = cargo.query("Units", "MoveType,WeaponType", {		where = ("_pageName='%s' AND IFNULL(Properties__full,'') NOT LIKE '%%enemy%%'"):format(escq(hero)),		limit = 1,	})[1] if heroQuery then local defaultSkills = List.to_set(List.map_self(cargo.query('UnitSkills,Units', 'skill', {			join = 'UnitSkills.WikiName=Units.WikiName',			where = ("Units._pageName='%s' AND IFNULL(Properties__full,'') NOT LIKE '%%enemy%%'"):format(escq(hero)),			limit = 50,		}), function (v) return v.skill end)) local nondefault = Hash.select(skillQueries, function (s)			return not s.invalid and s.wikiname and not defaultSkills[s.wikiname]		end) invalid = invalid or Hash.any(nondefault, function (s, k)			if s.ex ~= '0' then				if k == 'weapon1' or k == 'weapon2' then					-- refined weapon check					local refineQuery = cargo.query('WeaponUpgrades,Skills', 'Skills.WikiName=wikiname,Exclusive=ex', { join = 'WeaponUpgrades.BaseWeapon=Skills.WikiName', where = ("UpgradesInto='%s'"):format(escq(s.wikiname)), limit = 1, })[1]					return refineQuery and refineQuery.ex ~= '0' and not defaultSkills[refineQuery.wikiname]				else					return true				end			end			return false		end) if not args.costOverride then local skillCosts = Hash.values(Hash.map(Hash.select(nondefault, function (s, k)				return s.wikiname and not k:find('2$') and k ~= 'seal1'			end), function (s) return s.cost end)) cost = List.find(skillCosts, 'N/A') and 'N/A' or				List.find(skillCosts, 'veryhigh') and 'veryhigh' or				List.find(skillCosts, 'high') and 'high' or				Hash.size(nondefault) >= 3 and 'medium' or				'low' end invalid = invalid or Hash.any(skillQueries, function (s)			return s.mov and s.wep and not ( List.find((mw.text.split(s.mov, '%s*,%s*')), heroQuery.MoveType) and List.find((mw.text.split(s.wep, '%s*,%s*')), heroQuery.WeaponType))		end) end

local buildType, buildTypeText = getTypeTexts(mw.ustring.lower(args.type or '')) local buildMode, buildModeText = getModeTexts(mw.ustring.lower(args.mode or '')) local buildCost, buildCostText = getCostTexts(mw.ustring.lower(cost or '')) local buildEmblem, buildEmblemText = getEmblemTexts(mw.ustring.lower(args.emblem or ''))

-- Table Formatting local tbl = mw.html.create('table'):addClass('wikitable'):addClass('skillbuild') :attr('data-build-type', buildType) :attr('data-build-mode', buildMode) :attr('data-build-cost', buildCost) :attr('data-build-emblem', buildEmblem) local row = tbl:tag('tr') local cell = row:tag('td') local div = cell:tag('div'):css('float', 'right'):css('padding-top', '4px') if buildTypeText then div:tag('div'):css('display', 'inline-block'):css('margin-right', '8px'):wikitext(buildTypeText) end if buildModeText then div:tag('div'):css('display', 'inline-block'):css('margin-right', '8px'):wikitext(buildModeText) end if buildCostText then div:tag('div'):css('display', 'inline-block'):css('margin-right', '8px'):wikitext(buildCostText) end if buildEmblemText then div:tag('div'):css('display', 'inline-block'):css('margin-right', '8px'):wikitext(buildEmblemText) end

div = cell:tag('div'):css('font-size', '150%') div:wikitext(args.link and ('%s'):format(args.link, args.link) or args.name or 'Unnamed Build') if invalid then div:wikitext(' ') if mw.title.getCurrentTitle.namespace == 4 then -- Project namespace div:wikitext('') end end

row = tbl:tag('tr') cell = row:tag('td') div = cell:tag('div'):css('display', 'flex'):css('flex-wrap', 'wrap') local section = div:tag('div'):addClass('skillbuild-section') local section2 = div:tag('div'):addClass('skillbuild-section')

local skillFunc = function (cat, arg1, arg2) local skill1 = skillQueries[cat .. '1']		local skill2 = skillQueries[cat .. '2']		local icon, text if skill1.invalid then icon = (''):format(skill1.ic) text = tostring(mw.html.create('span'):css('color', 'red'):wikitext(arg1)) else icon = (''):format(not Util.isNilOrEmpty(skill1.ic) and skill1.ic or				(cat == 'weapon' and skill1.wep and not mw.ustring.find(skill1.wep, ',')) and ('Icon Class %s.png'):format(skill1.wep) or				DEFAULT_ICONS[cat], skill1.page) text = Template_SkillPage_nocargo(frame, skill1.page, skill1.name, skill1.desc) if REFINE_DISP[skill1.refine] then text = text .. (' (%s)'):format(REFINE_DISP[skill1.refine]) end end if not Util.isNilOrEmpty(arg2) then text = text .. ' / '			if skill2.invalid then text = text .. tostring(mw.html.create('span'):css('color', 'red'):wikitext(arg2)) else text = text .. Template_SkillPage_nocargo(frame, skill2.page, skill2.name, skill2.desc) if REFINE_DISP[skill2.refine] then text = text .. (' (%s)'):format(REFINE_DISP[skill2.refine]) end end end return icon, text end

-- Start Weapon local skillDiv = section:tag('div'):css('margin', '.5em 0') local skillIcon, skillText = skillFunc('weapon', args.weapon, args.weapon2) skillDiv:wikitext(skillIcon, ' ', skillText)

-- Start Assist skillDiv = section:tag('div'):css('margin', '.5em 0') skillIcon, skillText = skillFunc('assist', args.assist, args.assist2) skillDiv:wikitext(skillIcon, ' ', skillText)

-- Start Special skillDiv = section:tag('div'):css('margin', '.5em 0') skillIcon, skillText = skillFunc('special', args.special, args.special2) skillDiv:wikitext(skillIcon, ' ', skillText)

-- start IV	section:tag('div'):css('margin-left', '4px'):css('padding-top', '2px') :tag('b'):wikitext(frame:callParserFunction('#tip-text', 'Traits', "Recommended Asset/Flaw for the hero's stats")):done :wikitext(': '):wikitext((Util.isNilOrEmpty(args.ivs) or args.ivs == '-') and 'Flexible' or (args.ivs == 'Neutral' or args.ivs == 'neutral') and 'Normalized' or args.ivs)

local passiveFunc = function (kind, arg1, arg2) local skillDiv = section2:tag('div'):css('margin', '.5em 0') local skillIcon, skillText = skillFunc(kind, arg1, arg2, skillQueries[kind .. '1'], skillQueries[kind .. '2']) skillDiv:tag('div'):css('display', 'inline-block'):wikitext(superimpose {			c1 = skillIcon,			c2 = (''):format(kind == 'seal' and 'S' or mw.ustring.upper(kind)), x2 = 10, y2 = 10,		}) skillDiv:tag('div'):css('display', 'inline-block'):css('margin-left', '5px'):wikitext(' ', skillText) end

-- Start A	passiveFunc('a', args.passiveA, args.passiveA2)

-- Start B	passiveFunc('b', args.passiveB, args.passiveB2)

-- Start C	passiveFunc('c', args.passiveC, args.passiveC2)

-- Start S	passiveFunc('seal', args.seal, args.seal2)

row = tbl:tag('tr') cell = row:tag('td') if Util.isNilOrEmpty(args.description) then cell:tag('p'):wikitext('A description has not been written for this build yet.') else local desc = cell:tag('table'):addClass('skillbuild-description-container'):addClass('mw-collapsible'):addClass('mw-collapsed'):css('width', '100%') desc:tag('tr'):tag('th'):css('text-align', 'right'):css('color', 'black') :tag('div'):addClass('nomobile'):wikitext('Click to show/hide build description ') desc:tag('tr'):tag('td'):wikitext(args.description) end

local ret = tostring(tbl)

if Util.isNilOrEmpty(args['no cargo']) and mw.title.getCurrentTitle.namespace == 4 then mw.log('store') frame:expandTemplate {title = 'Skillbuilds', args = { hero = hero, name = args.name, cost = cost, type = args.type, mode = args.mode, emblem = args.emblem, ivs = args.ivs, weapon = args.weapon, weaponRefine = args.weaponRefine, weapon2 = args.weapon2, weaponRefine2 = args.weaponRefine2, assist = args.assist, assist2 = args.assist2, special = args.special, special2 = args.special2, passiveA = args.passiveA, passiveA2 = args.passiveA2, passiveB = args.passiveB, passiveB2 = args.passiveB2, passiveC = args.passiveC, passiveC2 = args.passiveC2, seal = args.seal, seal2 = args.seal2, description = args.description, }}		if cost == 'Low' or cost == 'low' or cost == 'Medium' or cost == 'medium' then ret = ret .. ''		end end

return ret end

p.main = function (frame) return p._main(require 'Module:Arguments'.getArgs(frame), frame) end

return p