Module:UnitData

local Util = require 'Module:Util' local List = require 'Module:ListUtil' local Hash = require 'Module:HashUtil' local FEHStatUtil = require 'Module:FEHStatUtil' local Tab = require 'Module:Tab' local MapLayout = require 'Module:MapLayout' local parseArgs = require 'Module:ObjectArg'.parse local memoizer = require 'Module:Memoizer'.memoizer local superimposeDiv = require 'Module:Superimpose'.div local stripWikitext = require('Module:StripWikitext').main1 local escq = require 'Module:EscQ'.main1 local toboolean = require 'Module:Bool'.toboolean local mf = Util.mf local cargo = mw.ext.cargo

local STATS = {'HP', 'Atk', 'Spd', 'Def', 'Res'} local SKILL_CATEGORIES = {'weapon', 'assist', 'special', 'a', 'b', 'c', 'seal'} local DIFFICULTY_ORDER = Util.getDifficulties local EMPTY_STAT_MODIFIERS = {0, 0, 0, 0, 0} local EMPTY_STRING = "-" local EMPTY_TEXT_LONG = "—" local UNKNOWN_TEXT = "?" local ORDERING_MIXIN = List.ordering_mixin

local memoizeTable = function (f) local mt = {__metatable = 'memoizeTable'} local done = false mt.__index = function (_, k)		done = true mt.__index = f return mt.__index[k] end mt.__pairs = function (_) if not done then mt.__index = f end return pairs(mt.__index) end return setmetatable({}, mt) end local MOVE_TYPES = memoizeTable(function 	return Hash.from_ipairs(cargo.query("MoveTypes", "IconFile,Name,_pageName,WikiName,Sort", {groupBy = 'WikiName', limit = 100}), function (row) row.Sort = tonumber(row.Sort) return row.WikiName, row end) end) local WEAPON_TYPES = memoizeTable(function 	return Hash.from_ipairs(cargo.query("WeaponTypes", "IconFile,Name,_pageName,WikiName,Classes__full=Classes,Type,Sort", {groupBy = 'WikiName', limit = 100}), function (row) row.Sort = tonumber(row.Sort) row.Classes = mw.text.split(row.Classes, '%s*,%s*') return row.WikiName, row end) end)

local memoizeUnits = memoizer(function (name)	local result = cargo.query( 'Units,UnitStats', Units._pageName=page,Units.WikiName=wikiname,Name=name,MoveType,WeaponType,Properties,		 IF(Properties__full LIKE '%enemy%','1',)=isEnemy,IF(Properties__full LIKE '%generic%','1',)=isGeneric,		  Lv1HP5,Lv1Atk5,Lv1Spd5,Lv1Def5,Lv1Res5,HPGR3,AtkGR3,SpdGR3,DefGR3,ResGR3, { join = 'Units.WikiName=UnitStats.WikiName', where = ("'%s' IN (Units._pageName,IFNULL(CONCAT(Name,': ',Title),Name))"):format(escq(name)), limit = 2, -- ally + enemy })	for _, v in ipairs(result) do		v.isEnemy = v.isEnemy ~= 		v.isGeneric = v.isGeneric ~= 	end	return result end)

local memoizeUnitsWithWikiName = function memoizeUnits = memoizer(function (name)		local result = cargo.query( 'Units,UnitStats', Units._pageName=page,Units.WikiName=wikiname,Name=name,MoveType,WeaponType,Properties,			 IF(Properties__full LIKE '%enemy%','1',)=isEnemy,IF(Properties__full LIKE '%generic%','1',)=isGeneric,			  Lv1HP5,Lv1Atk5,Lv1Spd5,Lv1Def5,Lv1Res5,HPGR3,AtkGR3,SpdGR3,DefGR3,ResGR3, { join = 'Units.WikiName=UnitStats.WikiName', where = ("Units.WikiName='%s'"):format(escq(name)), limit = 2, -- ally + enemy })		for _, v in ipairs(result) do			v.isEnemy = v.isEnemy ~= 			v.isGeneric = v.isGeneric ~= 		end		return result	end) end

-- {default skill at 1*, ..., default skill at 5*} local memoizeUnitWeapons = memoizer(function (wikiname)	local result = cargo.query('Units,UnitSkills,Skills', "skill,defaultRarity,skillPos", { join = 'Units.WikiName=UnitSkills.WikiName,UnitSkills.skill=Skills.WikiName', where = ("Units.WikiName='%s' AND Scategory='weapon'"):format(escq(wikiname)), limit = 10, })	for _, v in ipairs(result) do		v.defaultRarity = tonumber(v.defaultRarity)		v.skillPos = tonumber(v.skillPos)	end

result = List.select(result, function (v) return v.defaultRarity ~= nil end) table.sort(result, function (x, y) return x.defaultRarity < y.defaultRarity end) return List.generate(5, function (rarity)		local s = select(2, List.rfind_if(result, function (v) return v.defaultRarity <= rarity end))		return s and s.skill or false	end) end)

local disableSkillDesc = false

local memoizeSkill = (function 	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 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 Util.isNilOrEmpty(skillName) then			return {ic = MISSING_ICONS[category], name = UNKNOWN_TEXT, StatModifiers = EMPTY_STAT_MODIFIERS}		elseif skillName == EMPTY_STRING then			return {ic = DEFAULT_ICONS[category], name = EMPTY_TEXT_LONG, StatModifiers = EMPTY_STAT_MODIFIERS}		end

local query = cargo.query("Skills", "CONCAT(Icon)=ic,_pageName=page,Name=name,Description=desc,WikiName=wikiname,StatModifiers,Cooldown=cd,Next=nextSkill,PromotionRarity=prarity,PromotionTier=ptier", {			where = ("'%s' IN (Name,_pageName,WikiName) AND (%s) AND IFNULL(RefinePath,'-')='%s'"):format(escq(skillName), CATEGORY_QUERIES[category], refine or "-"),			limit = 1,		})[1] if not query then return {invalid = true, ic = MISSING_ICONS[category], name = skillName, StatModifiers = EMPTY_STAT_MODIFIERS} end

if disableSkillDesc then query.desc = nil end if query.ic == '' then query.ic = DEFAULT_ICONS[category] end query.cd = tonumber(query.cd) query.prarity = tonumber(query.prarity) query.ptier = tonumber(query.ptier) query.StatModifiers = List.map_self(mw.text.split(query.StatModifiers, ','), function (x) return tonumber(x) end) for i = 1, 5 do			query.StatModifiers[i] = query.StatModifiers[i] or 0 end return query end, function (skillName, category, refine) return (skillName or '') .. ';' .. (category or '') .. ';' .. (refine or '') end) end)

local objectArgStruct = function (t) if t then local assocs = {} for k, v in Hash.sorted_pairs(t) do			assocs[#assocs + 1] = ('%s=%s'):format(k, v)		end return ('{%s}'):format(table.concat(assocs, ';')) end end

local Template_Hover_nocargo = function (text, title) if Util.isNilOrEmpty(title) then return text end return tostring(mw.html.create('span'):attr('title', mw.text.decode(title)) -- mw.html automatically HTML-escapes attributes		:css('border-bottom', '0'):css('text-decoration', 'underline dotted'):css('cursor', 'help')		:wikitext(text)) end

local Template_SkillPage_nocargo = function (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 Template_Hover_nocargo(name, stripWikitext(desc))) end

local getMoveIcon = function (wikiname) local mov = MOVE_TYPES[wikiname] if mov then return (''):format(mov.IconFile, mov.Name, mov._pageName) elseif not Util.isNilOrEmpty(wikiname) then return "" else return '' end end

local getWeaponIcon = function (wikiname) local wep = WEAPON_TYPES[wikiname] if wep then return (''):format(wep.IconFile, wep.Name, wep._pageName) elseif not Util.isNilOrEmpty(wikiname) then return "" else return '' end end

local makeStatRow = function (title, text) local tr = mw.html.create("tr") tr:tag("th"):attr("scope", "row"):css("text-align", "left"):wikitext(title) tr:tag("td"):wikitext(text) return tr end

local makeSkillRow = function (icon, text, pkind) local tr = mw.html.create('tr'):css('height', '25%') if pkind then tr:tag("td"):css("width", "15%"):node(superimposeDiv((""):format(icon), {(''):format(pkind), 12, 10})) else tr:tag("td"):css("width", "15%"):wikitext((""):format(icon)) end tr:tag("td"):css("width", "85%"):wikitext(text) return tr end

local statTextFunc = function (stat, modifier) if not stat or not tonumber(stat) then return UNKNOWN_TEXT else stat = math.min(99, math.max(0, tonumber(stat))) local finalStat = math.min(99, math.max(0, stat + modifier)) if finalStat ~= stat then return Template_Hover_nocargo(finalStat, "Without skills: " .. stat) else return stat end end end

local rarityFunc = function (text) if not text then return "" else return "" end end

local lvFunc = function (text, displayed) if not text then return UNKNOWN_TEXT elseif (tonumber(text) or 0) > tonumber(displayed) then return Template_Hover_nocargo(text, "Displayed as " .. displayed .. "+ in‐game.") else return text end end

local slotFunc = function (text) if not text then return Template_Hover_nocargo(UNKNOWN_TEXT, "It is unknown what this unit's slot order is.") elseif text == EMPTY_STRING then return "" else return Template_Hover_nocargo("#" .. text, "This unit is in slot " .. text .. ".") end end

local cooldownFunc = function (cdcount, initcd) if cdcount then local text = Template_Hover_nocargo(cdcount, 'This unit has a Special cooldown count of ' .. cdcount .. '.') if initcd then text = Template_Hover_nocargo(initcd, 'This unit has an initial Special cooldown of ' .. initcd .. '.') .. ' / ' .. text end return text end return "" end

local blessingFunc = function (text) if text and text ~= EMPTY_STRING then local escQBlessing = escq(text) local blessingQuery = cargo.query( "Items", "Name,ImageFile", { where="_pageName LIKE '".. escQBlessing .. "%' OR Name LIKE '".. escQBlessing .. "%'", limit=1 } )[1] if blessingQuery then return Template_Hover_nocargo("",				"This unit has the " .. mw.ustring.lower(blessingQuery["Name"] or text) .. " conferred.") end end return "" end

local accessoryFunc = function (text) if text and text ~= EMPTY_STRING then local accessoryQuery = cargo.query( "Accessories", "_pageName,Name,Type", { where="'"..escq(text).."' IN (_pageName,Name)", limit=1 } )[1] if accessoryQuery then return "" end end return "" end

local skillFunc = function (query) local txt = Template_SkillPage_nocargo(query.page, query.name, query.desc) if query.invalid then txt = ("%s"):format(txt) end return query.ic, txt end

local getUnitQuery = function (unitInfo) local unitQueries = memoizeUnits(unitInfo.unit) return unitQueries[#unitQueries <= 1 and 1 or List.find_if(unitQueries, function (query)		if query.isEnemy then			return (not unitInfo.properties.is_ally or unitInfo.properties.use_enemy_stats) and not unitInfo.properties.use_ally_stats		else			return (unitInfo.properties.is_ally or unitInfo.properties.use_ally_stats) and not unitInfo.properties.use_enemy_stats		end	end)] end

local skillPromotion = function (unitInfo, cat, mptier) if Util.isNilOrEmpty(unitInfo[cat]) or unitInfo[cat] == EMPTY_STRING then return unitInfo[cat] end local skill = memoizeSkill(unitInfo[cat], cat) if skill.invalid then return unitInfo[cat] end

if cat == 'weapon' then local unitQuery = getUnitQuery(unitInfo) local weapons = memoizeUnitWeapons(unitQuery.wikiname) if unitQuery.isEnemy then if weapons[unitInfo.rarity] then return weapons[unitInfo.rarity] end elseif mptier and skill.ptier and skill.ptier <= mptier then local oldRarity = List.find(weapons, skill.wikiname) -- prevent skill demotion? if oldRarity and oldRarity <= unitInfo.rarity and weapons[unitInfo.rarity] then return weapons[unitInfo.rarity] end end end

if mptier then while skill.nextSkill ~= '' and skill.prarity and skill.prarity <= unitInfo.rarity and skill.ptier and skill.ptier <= mptier do			local skill2 = memoizeSkill(skill.nextSkill, cat) if skill2.invalid then break end skill = skill2 end end return skill.wikiname end

local translateAIInfo = function (info) if info == 'activeall' then return {turn = 1} elseif info == 'passivelinked' then return {group = 1} elseif info == 'passivesingle' then return {} else return info end end

local parseGlobalAIInfo = function (str) if Util.isNilOrEmpty(str) then return {turn = 1} end local info, err = parseArgs(str) if err then return nil, require 'Module:Error'.error(err) else return translateAIInfo(info) end end

local makeAITable = function (infos, globalai) local tbl = mw.html.create('table'):addClass('wikitable'):css('text-align', 'center')

local row = tbl:tag('tr') row:tag('th'):wikitext('Group') row:tag('th'):wikitext('Index') row:tag('th'):wikitext('Unit') row:tag('th'):wikitext('Start turn') row:tag('th'):wikitext('Notes')

local getTurnTxt = function (unitInfo, inGroup) local hasTurn = type(unitInfo.ai.turn) == 'number' if inGroup then local hasDelay = type(unitInfo.ai.delay) == 'number' if hasTurn and hasDelay then return 'Turn ' .. unitInfo.ai.turn .. ' or ' .. unitInfo.ai.delay .. ' turn(s) after group is engaged' elseif hasTurn and not hasDelay then return 'Turn ' .. unitInfo.ai.turn .. ' or after group is engaged' elseif hasDelay then return unitInfo.ai.delay .. ' turn(s) after group is engaged' else return 'After group is engaged' end elseif hasTurn then return 'Turn ' .. unitInfo.ai.turn else return 'None' end end

local fillUnit = function (unitInfo, inGroup) local unitQuery = getUnitQuery(unitInfo) row:tag('td'):wikitext(type(unitInfo.slot) == 'number' and ('#' .. unitInfo.slot) or UNKNOWN_TEXT) row:tag('td'):wikitext(unitInfo.random and 'Random' or			unitQuery and ('%s'):format(unitQuery.page, unitQuery.name) or unitInfo.unit) row:tag('td'):wikitext(getTurnTxt(unitInfo, inGroup))

local notes = table.concat(List.compact {			unitInfo.ai.break_walls and 'Attacks breakable terrain' or nil,			unitInfo.ai.tether and 'Returns to starting position if unit has no actions to make' or nil,		}, ' ') row:tag('td'):wikitext(notes == '' and EMPTY_TEXT_LONG or notes) end

local unitsByAIGroup = List.group_by(List.select(infos, function (v) return not v.properties.is_ally end), function (v)		return v.ai and tonumber(v.ai.group) or math.huge	end) for group, units in Hash.sorted_pairs(unitsByAIGroup) do		table.sort(units, function (x, y) return x._index < y._index end) row = tbl:tag('tr') if group < math.huge then row:tag('td'):attr('rowspan', #units):wikitext(group) for i, unitInfo in ipairs(units) do				if i > 1 then row = tbl:tag('tr') end fillUnit(unitInfo, true) end else for _, unitInfo in ipairs(units) do				row = tbl:tag('tr') row:tag('td'):wikitext(EMPTY_TEXT_LONG) fillUnit(unitInfo, false) end end end

return (' AI settings %s See AI for a detailed description of the enemy movement settings. '):format(tostring(tbl)) end

local parseUnitData = function (unitInfo, opts) local unitQuery = getUnitQuery(unitInfo) local unitImage = "" local unitPageLink = "" local moveTypeIcon = "" local weaponTypeIcon = ""

if unitInfo.random then unitPageLink = " Random" if not unitInfo.random.moves then for wikiname in Hash.sorted_pairs(MOVE_TYPES, function (v1, v2) return v1.Sort < v2.Sort end) do moveTypeIcon = moveTypeIcon .. getMoveIcon(wikiname) end else moveTypeIcon = getMoveIcon(unitInfo.random.moves) end local weaponList = unitInfo.random.weapons local excludeStaff = toboolean(unitInfo.random.staff) == false for wikiname, v in Hash.sorted_pairs(WEAPON_TYPES, function (v1, v2) return v1.Sort < v2.Sort end) do			if (not weaponList or List.find(v.Classes, weaponList) or v.Type == weaponList or wikiname == weaponList) and not (v.Type == 'Staff' and excludeStaff) then weaponTypeIcon = weaponTypeIcon .. getWeaponIcon(wikiname) end end elseif unitQuery then local cc = opts.ccArgs if cc then cc.level = tonumber(cc.level) cc.rarity = tonumber(cc.rarity) cc.hpfactor = tonumber(cc.hpfactor) cc.ptier = tonumber(cc.ptier)

if unitInfo.level and unitInfo.rarity then local newLv = math.max(unitInfo.level, cc.level or 1) local newRarity = math.max(unitInfo.rarity, cc.rarity or 1) local baseStats = List.map(STATS, function (stat) return tonumber(unitQuery['Lv1' .. stat .. '5']) or 0 end) local growthRates = List.map(STATS, function (stat) return tonumber(unitQuery[stat .. 'GR3']) or 0 end)

if unitInfo.rarity < newRarity then local rarityBonuses = FEHStatUtil.getRarityBonuses(baseStats) List.map_self(unitInfo.stats, function (statVal, i)						return statVal and statVal + rarityBonuses[newRarity][i] - rarityBonuses[unitInfo.rarity][i]					end) end

List.map_self(unitInfo.stats, function (statVal, i)					return statVal and statVal + math.floor((						FEHStatUtil.getMasterGrowthRate(newRarity, growthRates[i]) * (newLv - 1) -						FEHStatUtil.getMasterGrowthRate(unitInfo.rarity, growthRates[i]) * (unitInfo.level - 1)) / 100)				end)

unitInfo.rarity = newRarity unitInfo.level = newLv unitInfo._lv = newLv end

for _, cat in ipairs(SKILL_CATEGORIES) do				if not (cat == 'weapon' and unitInfo.refine) then unitInfo[cat] = skillPromotion(unitInfo, cat, cc.ptier) end end

if unitInfo.stats[1] and cc.hpfactor then unitInfo.stats[1] = math.max(1, math.floor(unitInfo.stats[1] * cc.hpfactor)) end end

local unitProperties = mw.text.split(unitQuery.Properties, '%s*,%s*') unitPageLink = "" .. unitQuery.name .. "" unitImage = (' '):format(			unitInfo.isEnemy and 'style="transform:scaleX(-1);' or '',			mf(unitQuery.page), unitQuery.isGeneric and 'Mini_Unit_Idle' or 'Face_FC', unitQuery.page)		moveTypeIcon = getMoveIcon(unitQuery.MoveType)		weaponTypeIcon = getWeaponIcon(unitQuery.WeaponType)	elseif not Util.isNilOrEmpty(unitInfo.unit) then		unitPageLink = " ".. unitInfo.unit .. ""	end

local skillQueries = Hash.generate(SKILL_CATEGORIES, function (cat)		return memoizeSkill(unitInfo[cat], cat, cat == 'weapon' and unitInfo.refine or nil)	end) local hasInvalid = Hash.any(skillQueries, function (v) return v.invalid end) local statModifiers = List.map(List.transpose(List.map(Hash.values(skillQueries), function (t)		return t.StatModifiers	end)), function (stats) return List.sum(stats) end)

local cooldownCount = nil if skillQueries.special.wikiname then if unitInfo.max_cooldown then cooldownCount = unitInfo.max_cooldown else cooldownCount = 0 for _, v in pairs(skillQueries) do				if v.wikiname and v.cd then cooldownCount = cooldownCount + v.cd				end end cooldownCount = math.max(1, cooldownCount) end end

-- obtain icons and texts local hpText = statTextFunc(unitInfo.stats[1], statModifiers[1]) local atkText = statTextFunc(unitInfo.stats[2], statModifiers[2]) local spdText = statTextFunc(unitInfo.stats[3], statModifiers[3]) local defText = statTextFunc(unitInfo.stats[4], statModifiers[4]) local resText = statTextFunc(unitInfo.stats[5], statModifiers[5])

local rarityText = rarityFunc(unitInfo.rarity) local lvText = lvFunc(unitInfo._lv, unitInfo.displaylevel or 40) local slotText = slotFunc(unitInfo.slot) local cooldownText = cooldownFunc(cooldownCount, unitInfo.cooldown) local blessingText = blessingFunc(unitInfo.blessing) local accessoryText = accessoryFunc(unitInfo.accessory)

local weaponIcon, weaponText = skillFunc(skillQueries["weapon"]) local assistIcon, assistText = skillFunc(skillQueries["assist"]) local specialIcon, specialText = skillFunc(skillQueries["special"]) local passiveAIcon, passiveAText = skillFunc(skillQueries["a"]) local passiveBIcon, passiveBText = skillFunc(skillQueries["b"]) local passiveCIcon, passiveCText = skillFunc(skillQueries["c"]) local passiveSIcon, passiveSText = skillFunc(skillQueries["seal"])

if cooldownText ~= '' then specialText = specialText .. ' (' .. cooldownText .. ')' end

-- generate sub-tables local statTable = mw.html.create("table"):css("width", "100%"):css("height", "100%"):css("text-align", "right") statTable:node(makeStatRow("LV.", lvText)) statTable:node(makeStatRow("HP", hpText)) statTable:node(makeStatRow("Atk", atkText)) statTable:node(makeStatRow("Spd", spdText)) statTable:node(makeStatRow("Def", defText)) statTable:node(makeStatRow("Res", resText))

--       | ++       |        | ++---++ 	if unitInfo.random then local randomTable = mw.html.create("table"):css("width", "100%"):css("height", "100%") randomTable:tag('tr'):tag('td'):attr('colspan', 2):wikitext('This unit is randomly generated.') randomTable:tag('tr'):tag('td'):css('width', '30%'):wikitext("Move types"):done :tag('td'):css('width', '70%'):wikitext("Weapon types") randomTable:tag('tr'):tag('td'):wikitext(moveTypeIcon):done :tag('td'):wikitext(weaponTypeIcon)
 * rarity | stats | random |
 * slot |       |        |
 * unit |       |        |

local uTbl = mw.html.create("table"):css("text-align","center"):css("width","100%"):css("height", "100%"):css("table-layout", "fixed")

local row = uTbl:tag("tr") row:tag("td"):css("width", "28%"):wikitext(rarityText) row:tag("td"):attr("rowspan", 3):css("width", "16%"):css("height", "100%"):css("padding-right", "2em"):node(statTable) row:tag("td"):attr("rowspan", 3):css("width", "56%"):node(randomTable) uTbl:tag("tr"):tag("td"):wikitext(slotText) uTbl:tag("tr"):tag("td"):wikitext(unitImage .. unitPageLink) -- hero icon and name

return uTbl, hasInvalid end

local skills1Table = mw.html.create("table"):css("width", "100%"):css("height", "100%"):css("table-layout", "fixed") skills1Table:node(makeSkillRow(weaponIcon, weaponText)) skills1Table:node(makeSkillRow(assistIcon, assistText)) skills1Table:node(makeSkillRow(specialIcon, specialText)) skills1Table:tag("tr"):css("height", "25%"):tag("td"):attr("colspan", 2) :wikitext(mw.text.trim(blessingText .. " " .. accessoryText))

local passivesTable = mw.html.create("table"):css("width", "100%"):css("height", "100%") passivesTable:node(makeSkillRow(passiveAIcon, passiveAText, 'A')) passivesTable:node(makeSkillRow(passiveBIcon, passiveBText, 'B')) passivesTable:node(makeSkillRow(passiveCIcon, passiveCText, 'C')) passivesTable:node(makeSkillRow(passiveSIcon, passiveSText, 'S'))

--       |          | +---+       |        |          | +-+-+       |        |          | +-+-+---++--+ 	-- generate html local uTbl = mw.html.create("table"):css("text-align","center"):css("width","100%"):css("height", "100%"):css("table-layout", "fixed")
 * rarity  | stats | skills | passives |
 * slot   |       |        |          |
 * unit   |       |        |          |
 * mov | wep |      |        |          |

local row = uTbl:tag("tr") row:tag("td"):attr("colspan", 2):css("width", "28%"):wikitext(rarityText) row:tag("td"):attr("rowspan", 4):css("width", "16%"):css("height", "100%"):css("padding-right", "2em"):node(statTable) row:tag("td"):attr("rowspan", 4):css("width", "28%"):node(skills1Table) row:tag("td"):attr("rowspan", 4):css("width", "28%"):node(passivesTable)

row = uTbl:tag("tr") row:tag("td"):attr("colspan", 2):wikitext(slotText)

row = uTbl:tag("tr") row:tag("td"):attr("colspan", 2):wikitext(unitImage .. unitPageLink) -- hero icon and name

row = uTbl:tag("tr") row:tag("td"):wikitext(weaponTypeIcon) -- weapon type icon row:tag("td"):wikitext(moveTypeIcon) -- move icon

if toboolean(opts['no cargo']) or opts.ccArgs then return uTbl, hasInvalid end

local propertiesClone = Hash.clone(unitInfo.properties) propertiesClone.use_ally_stats = nil -- unnecessary as wikiname is used to identify unit propertiesClone.use_enemy_stats = nil -- unnecessary as wikiname is used to identify unit

return uTbl, hasInvalid, { tabname = opts.tabname, difficulty = opts.difficulty, name = unitQuery and unitQuery.page, unit = unitQuery and unitQuery.wikiname, pos = unitInfo.pos, rarity = unitInfo.rarity, slot = unitInfo.slot, cooldown = unitInfo.cooldown, blessing = unitInfo.blessing, level = unitInfo._lv, hp = unitInfo.stats[1], atk = unitInfo.stats[2], spd = unitInfo.stats[3], def = unitInfo.stats[4], res = unitInfo.stats[5], weapon = skillQueries.weapon.wikiname, assist = skillQueries.assist.wikiname, special = skillQueries.special.wikiname, a = skillQueries.a.wikiname, b = skillQueries.b.wikiname, c = skillQueries.c.wikiname, seal = skillQueries.seal.wikiname, accessory = unitInfo.accessory, ai = objectArgStruct(unitInfo.ai), -- unless there is a good reason to query their subfields individually, these will be passed as objectargs spawn = objectArgStruct(unitInfo.spawn), properties = table.concat(Hash.keys(propertiesClone), ','), } end

local parseTabData = function (unitObj, opts) local infos, err = unitObj if type(infos) == 'string' then infos, err = parseArgs(unitObj == '' and '[]' or unitObj) if err then return require 'Module:Error'.error(err) end end

-- sanitize input if opts.ccArgs and not next(opts.ccArgs) then opts.ccArgs = nil end for i, unitInfo in ipairs(infos) do		unitInfo._index = i		unitInfo._lv = unitInfo.level unitInfo.level = tonumber(unitInfo.level) unitInfo.rarity = tonumber(unitInfo.rarity) if not unitInfo.stats then unitInfo.stats = List.map(STATS, function (stat) return unitInfo[mw.ustring.lower(stat)] or false end) end List.map_self(unitInfo.stats, function (v) return tonumber(v) or false end) if not unitInfo.properties then unitInfo.properties = {} elseif type(unitInfo.properties) == 'string' then unitInfo.properties = List.to_set(mw.text.split(unitInfo.properties, ',')) end unitInfo.ai = unitInfo.ai or opts.globalai or {turn = 1} if unitInfo.spawn and Util.isNilOrEmpty(opts.mapImage) then return require 'Module:Error'.error('A reinforcement unit has been specified but the mapImage parameter is missing.') end end local hasInvalid = false

-- workaround for lua not having true hash tables local unitsBySpawn = Hash.from_ipairs(infos, function (u)		local spawn = u.spawn or {count = -1}		return setmetatable({ u.properties.is_ally and 0 or 1, spawn.cond or '', tonumber(spawn.turn) or -1, spawn.target or '', tonumber(spawn.kills) or -1, tonumber(spawn.remain) or -1, tonumber(spawn.count) or 1, }, ORDERING_MIXIN), u	end) local unitsBySpawnFlatten = {} for spawn, unitInfo in Hash.sorted_pairs(unitsBySpawn) do		if #unitsBySpawnFlatten == 0 or spawn ~= unitsBySpawnFlatten[#unitsBySpawnFlatten][1] then table.insert(unitsBySpawnFlatten, {spawn, {}}) end table.insert(unitsBySpawnFlatten[#unitsBySpawnFlatten][2], unitInfo) end local allyStartIdx = List.find_if(unitsBySpawnFlatten, function (v) return v[1][1] == 0 end) local enemyStartIdx = List.find_if(unitsBySpawnFlatten, function (v) return v[1][1] == 1 end) local enemyReinfStartIdx = List.find_if(unitsBySpawnFlatten, function (v) return v[1][1] == 1 and v[1][7] ~= -1 end)

local ns = mw.title.getCurrentTitle.namespace

local tables = List.map(unitsBySpawnFlatten, function (allSpawns, i)		local spawn, infosPart = unpack(allSpawns)		table.sort(infosPart, function (x, y) return x._index < y._index end)

local entry = mw.html.create('div')

if i == allyStartIdx then entry:tag('p'):tag('b'):wikitext('Ally units') elseif i == enemyStartIdx then entry:tag('p'):tag('b'):wikitext('Enemy units') elseif i == enemyReinfStartIdx then entry:tag('p'):tag('b'):wikitext('Enemy reinforcements') end

if spawn[7] ~= -1 then -- *.spawn.count local reinfmap = entry:tag('table'):addClass('wikitable'):addClass('default'):addClass('mw-collapsed'):addClass('mw-collapsible') local txtnode = reinfmap:tag('tr'):tag('th') txtnode:wikitext('Reinforcement') if spawn[2] ~= '' then -- *.spawn.cond txtnode:wikitext(spawn[2]) else if spawn[7] ~= 1 then -- *.spawn.count txtnode:wikitext((' (×%d)'):format(spawn[7])) -- *.spawn.count end txtnode:wikitext(': ') if spawn[3] ~= -1 then -- *.spawn.turn txtnode:wikitext('Turn ', spawn[3]) -- *.spawn.turn if spawn[4] ~= '' then -- *.spawn.target txtnode:wikitext(' or later, ') end end if spawn[4] ~= '' then -- *.spawn.target local target_count = spawn[6] ~= -1 and spawn[6] or spawn[5] -- *.spawn.remain or *.spawn.kills txtnode:wikitext(('%d %s unit%s %s'):format(target_count, spawn[4], target_count > 1 and 's' or '', spawn[6] ~= -1 and 'remaining' or 'killed')) end end

local mapArgs = {} for _, unitInfo in ipairs(infosPart) do				if unitInfo.pos then mapArgs[unitInfo.pos] = tostring(MapLayout.makeUnitIcon_noCargo(getUnitQuery(unitInfo), spawn[1] == 0 and 'Ally' or 'Enemy')) end end reinfmap:tag('tr'):tag('td'):node(superimposeDiv(opts.mapImage or '', {MapLayout._map(mapArgs), 0, 0})) end

local tbl = entry:tag("table"):addClass("wikitable"):addClass("unitdata-unit-table") :css("text-align", "center"):css('border-spacing', '1px') tbl:tag("tr"):tag("th"):attr("scope", "colgroup"):wikitext(spawn[1] == 0 and 'Ally data' or 'Enemy data')

for _, unitInfo in ipairs(infosPart) do			local unitRow, invalid, cargoArgs = parseUnitData(unitInfo, opts) tbl:tag('tr'):css("height", "18em"):tag('td'):node(unitRow) hasInvalid = hasInvalid or invalid

-- Store in Cargo if cargoArgs and ns == 0 then mw.getCurrentFrame:expandTemplate {title = "MapUnitDefinition", args = cargoArgs} end end

return tostring(entry) end)

if not toboolean(opts['no ai']) then table.insert(tables, 1, opts.globalaierror or makeAITable(infos, opts.globalai)) end if opts.ccArgs then if Util.isNilOrEmpty(opts.mapImage) then return require 'Module:Error'.error('An infobox for the derived map is to be generated but the mapImage parameter is missing.') end local initMapArgs = {allyPos = opts.allyPos} for _, unitInfo in ipairs(infos) do			if unitInfo.pos and (unitInfo.spawn and (unitInfo.spawn.count or 1) or -1) == -1 then initMapArgs[unitInfo.pos] = tostring(MapLayout.makeUnitIcon_noCargo(getUnitQuery(unitInfo), unitInfo.properties.is_ally and 'Ally' or 'Enemy')) end end table.insert(tables, 1, mw.getCurrentFrame:expandTemplate {title = 'Derived Map Infobox', args = {			battle = opts.battle,			baseMap = opts.mapPage,			baseTab = opts.tabnameOrig,			level = opts.ccArgs.level,			rarity = opts.ccArgs.rarity,			hpfactor = opts.ccArgs.hpfactor,			mapImage = tostring(superimposeDiv(opts.mapImage, {MapLayout._map(initMapArgs), 0, 0})),		}}) end if ns == 0 then if List.any(infos, function (v) return not v.random and List.any(SKILL_CATEGORIES, function (cat) return not v[cat] end) end) then tables[#tables + 1] = '' end if hasInvalid then tables[#tables + 1] = '' end end return table.concat(tables, '\n') end

local p = {}

p.makeTable = function (frame) local globalai, globalaierror = parseGlobalAIInfo(frame.args.globalai) local opts = { globalai = globalai, globalaierror = globalaierror, mapImage = frame.args.mapImage, tabname = frame.args.tabName, difficulty = frame.args.difficulty, battle = frame.args.battle, ['no cargo'] = frame.args['no cargo'], ['no ai'] = frame.args['no ai'], ccArgs = { hpfactor = tonumber(frame.args.hpfactor), level = tonumber(frame.args.level), rarity = tonumber(frame.args.rarity), ptier = tonumber(frame.args.ptier), },	}	return parseTabData(frame.args[1], opts) end

local findDifficulty = function (tabName) return select(2, List.find_if(DIFFICULTY_ORDER, function (difficulty) return mw.ustring.find(tabName, difficulty, 1, true) end)) end

local NOEMPTY_UNIT_FIELDS = {'pos', 'rarity', 'slot', 'cooldown', 'blessing', 'level', 'hp', 'atk', 'spd', 'def', 'res', 'accessory', 'ai', 'spawn'}

p.main = function (frame) local allUnits = Hash.clone(frame.args) local derived = Hash.remove(allUnits, 'derived') if derived then derived = mw.loadData 'Module:UnitData/data'.derived_settings[derived] end local globalai, globalaierror = parseGlobalAIInfo(Hash.remove(allUnits, 'globalai')) local opts = { globalai = globalai, globalaierror = globalaierror, battle = Hash.remove(allUnits, 'battle'), mapImage = Hash.remove(allUnits, 'mapImage'), ['no cargo'] = Hash.remove(allUnits, 'no cargo'), ['no ai'] = Hash.remove(allUnits, 'no ai'), }

local tabsStr = Hash.remove(allUnits, 'derivedTabs') if tabsStr then local tabMap, err = parseArgs(tabsStr) if err then return require 'Module:Error'.error(err) end for from, to in pairs(tabMap) do			allUnits[to] = {from = from} end

disableSkillDesc = true memoizeUnitsWithWikiName opts.mapPage = Hash.remove(allUnits, 'derivedMap') or mw.title.getCurrentTitle.fullText opts.allyPos = Hash.remove(allUnits, 'allyPos') local baseUnits = cargo.query(			'MapUnits',			TabName,Unit=unit,Pos=pos,Rarity=rarity,Slot=slot,Cooldown=cooldown,Blessing=blessing,Level=level,			 HP=hp,Atk=atk,Spd=spd,Def=def,Res=res,Weapon=weapon,Assist=assist,Special=special,PassiveA=a,PassiveB=b,PassiveC=c,Seal=seal,			  Accessory=accessory,AI=ai,Spawn=spawn,Properties__full=properties, {			where = ("_pageName='%s' AND Unit IS NOT NULL"):format(escq(opts.mapPage)),			orderBy = 'Slot,Unit',			limit = 100,		}) for _, unitInfo in ipairs(baseUnits) do			if tabMap[unitInfo.TabName] then for _, k in ipairs(NOEMPTY_UNIT_FIELDS) do					if unitInfo[k] == '' then unitInfo[k] = nil end end for _, cat in ipairs(SKILL_CATEGORIES) do					unitInfo[cat] = unitInfo[cat] == '' and '-' or unitInfo[cat] end if unitInfo.ai then unitInfo.ai = parseArgs(unitInfo.ai) end if unitInfo.spawn then unitInfo.spawn = parseArgs(unitInfo.spawn) end unitInfo.properties = List.to_set(mw.text.split(unitInfo.properties, '%s*,%s*')) for k, v in pairs(unitInfo) do					if type(v) ~= 'table' then unitInfo[k] = tonumber(v) or v					end end table.insert(allUnits[tabMap[unitInfo.TabName]], unitInfo) end end end

local tabberArgs = {} for tabName, unitList in Hash.sorted_pairs(allUnits, function (_, _, k1, k2) return Util.difficultySort(k1, k2) end) do		opts.tabnameOrig = unitList.from opts.tabname = tabName opts.difficulty = findDifficulty(tabName) opts.ccArgs = derived and derived[opts.difficulty] if opts.ccArgs then -- remove metatable opts.ccArgs = Hash.clone(opts.ccArgs) end tabberArgs[#tabberArgs + 1] = {tabName, '\n' .. tostring(parseTabData(unitList, opts))} end

return Tab.tabber(tabberArgs):css( "overflow-x", "auto" ) end

-- for doc page p.derivedSettingsList = function local DERIVED_SETTINGS = mw.loadData 'Module:UnitData/data'.derived_settings local deriveds = {} for k, v in Hash.sorted_pairs(DERIVED_SETTINGS) do		deriveds[#deriveds + 1] = ("* : %s"):format(k,			table.concat(List.select(DIFFICULTY_ORDER, function (dif) return v[dif] end), ', ')) end return table.concat(deriveds, '\n') end

return p