Fire Emblem Heroes Wiki
Fire Emblem Heroes Wiki
Template-info.svg Documentation

Functions for rendering map images on the wiki.

Implements Template:Ally.
Implements Template:Enemy.
Implements Template:MapLayout and Template:RDMapLayout.
Like map, except this function always pulls initial units defined on the same page on all difficulties, and generates a tabber if there are multiple unit layouts, ignoring the init and initTab parameters. To be used as the mapImage argument for Template:Battle Infobox.
local cargo = mw.ext.cargo
local Util = require 'Module:Util'
local List = require 'Module:ListUtil'
local Hash = require 'Module:HashUtil'
local Tab = require 'Module:Tab'
local escq = require 'Module:EscQ'.main1
local mf = require 'Module:MF'.main1
local toboolean = require 'Module:Bool'.toboolean

local makeStartingSpotIcon = function (side, num, size)
	return ('[[File:%s Starting Spot %s.png|%s|link=|alt=]]'):format(side, num == '?' and 'Unknown' or tostring(num), size)
end

local makeGenericIcon = function (side, page, mov, wep)
	local iconDiv = mw.html.create('div'):css('position', 'relative'):css('display', 'inline-block')
	iconDiv:tag('div'):css('position', 'relative'):css('top', '0px'):css('left', '0px'):css('z-index', '1')
		:wikitext(('[[File:Map %s Highlight Base.png|60x60px|link=|alt=]]'):format(side))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '50%'):css('left', '50%'):css('z-index', '3')
		:css('transform', side == 'Ally' and 'translate(-50%, -50%)' or 'translate(-50%, -50%) scaleX(-1)')
		:wikitext(('[[File:%s_Mini_Unit_Idle.png|56x56px|link=]]'):format(page))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '0px'):css('left', '0px'):css('z-index', '3')
		:wikitext(('[[File:Map %s Highlight Top.png|60x60px|link=|alt=]]'):format(side))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '2px'):css('left', '41px'):css('z-index', '4')
		:wikitext(('[[File:Icon_Class_%s.png|16x16px|link=|alt=]]'):format(wep))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '38px'):css('left', '4px'):css('z-index', '4')
		:wikitext(('[[File:Icon_Move_%s.png|15x15px|link=|alt=]]'):format(mov))
	return iconDiv
end

local makeHeroIcon = function (side, page, mov, wep)
	local iconDiv = mw.html.create('div'):css('position', 'relative'):css('display', 'inline-block')
	iconDiv:tag('div'):css('position', 'relative'):css('top', '0px'):css('left', '0px'):css('z-index', '1')
		:wikitext(('[[File:Map %s Highlight Base.png|60x60px|link=|alt=]]'):format(side))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '4px'):css('left', '4px'):css('z-index', '3')--:css('transform', 'scaleX(1)')
		:wikitext(('[[File:%s_Face_FC.png|52x52px|link=]]'):format(mf(page)))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '0px'):css('left', '0px'):css('z-index', '3')
		:wikitext(('[[File:Map %s Highlight Top.png|60x60px|link=|alt=]]'):format(side))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '2px'):css('left', '41px'):css('z-index', '4')
		:wikitext(('[[File:Icon_Class_%s.png|16x16px|link=|alt=]]'):format(wep))
	iconDiv:tag('div'):css('position', 'absolute'):css('top', '38px'):css('left', '4px'):css('z-index', '4')
		:wikitext(('[[File:Icon_Move_%s.png|15x15px|link=|alt=]]'):format(mov))
	return iconDiv
end

-- also for Module:UnitData
local makeUnitIcon_noCargo = function (query, side)
	return (query.isGeneric and makeGenericIcon or makeHeroIcon)(side, query.page, query.MoveType, query.WeaponType)
end

local makeIcon = function (args, side)
	if args[1] and (tonumber(args[1]) or args[1] == '?') then
		return makeStartingSpotIcon(side, args[1], args.size or '60x60px')
	else
		local unit = args.generic or args.hero or args.unit or args[1]
		local query = cargo.query('Units', "MoveType,WeaponType,IF(Properties__full LIKE '%generic%','1','')=isGeneric", {
			where = ("'%s' IN (_pageName,IFNULL(CONCAT(Name,': ',Title),Name))"):format(escq(unit)),
			groupBy = '_pageName',
		})[1]
		if query then
			query.page = unit
			query.isGeneric = query.isGeneric ~= ''
			return makeUnitIcon_noCargo(query, side)
		end
	end
end

local ally = function (args)
	return makeIcon(args, 'Ally')
end

local enemy = function (args)
	return makeIcon(args, 'Enemy')
end



local getBaseMapInfo = function (filename, typ)
--	return mw.title.makeTitle('File', filename).file -- expensive parser function
	if typ == 'HO' then
		return {exists = true, width = 540, height = 684}
	elseif typ == 'RD' then
		return {exists = true, width = 540, height = 675}
	elseif typ == 'MS2' then
		return {exists = true, width = 540, height = 765}
	else
		return {exists = true, width = 540, height = 720}
	end
end

local makeBackdropDiv = function (filename, typ, width, height)
	return mw.html.create('div'):css('position', 'absolute'):css('top', '0px'):css('left', '0px'):css('z-index', '1')
		:wikitext(('[[File:%s%s.png|%dx%dpx|link=|alt=]]'):format(typ == 'RD' and 'Rival Domains ' or '', filename, width, height))
end

local makeBackgroundDiv = function (mapid, typ, width, height)
	local filename = ('Map %s.png'):format(mapid)
	local info = getBaseMapInfo(filename, typ)
	if info.exists then
		local div2 = mw.html.create('div'):css('position', 'absolute'):css('z-index', '2')
		local aspect = info.width / info.height
		if aspect > width / height + 1e-5 then
			local margin = (info.width / info.height * height - width) / 2
			div2:css('clip', ('rect(0px,%.5fpx,%dpx,%.5fpx)'):format(width + margin, height, margin)):css('top', '0px'):css('left', ('%.5fpx'):format(-margin))
			div2:wikitext(('[[File:%s|x%dpx|link=|alt=]]'):format(filename, height))
		elseif aspect < width / height - 1e-5 then
			local margin = (info.height / info.width * width - height) / 2
			div2:css('clip', ('rect(%.5fpx,%dpx,%.5fpx,0px)'):format(margin, width, height + margin)):css('top', ('%.5fpx'):format(-margin)):css('left', '0px')
			div2:wikitext(('[[File:%s|%dpx|link=|alt=]]'):format(filename, width))
		else
			div2:css('top', '0px'):css('left', '0px')
			div2:wikitext(('[[File:%s|%dx%dpx|link=|alt=]]'):format(filename, width, height))
		end
		return div2
	end
end

local makeTDScrollDiv = function ()
	return mw.html.create('div'):css('position', 'absolute'):css('top', '360px'):css('left', '0px'):css('z-index', '3')
		:wikitext('[[File:Tutorial_Scroll.png|360x120px|center|middle|link=#Text]]')
end

local makePos = function (x, y)
	return string.char(96 + x) .. tostring(y)
end

local parsePos = function (pos)
	local x, y = mw.ustring.match(pos, '^([a-z])(%d+)$')
	if y then
		return mw.ustring.codepoint(x) - 96, tonumber(y)
	end
end

local makeMapObjectDiv = function (pos, obj)
	if obj then
		local x, y = parsePos(pos)
		if x and y then
			return mw.html.create('div'):css('position', 'absolute'):css('z-index', '3')
				:css('bottom', ((y - 1) * 60) .. 'px'):css('left', ((x - 1) * 60) .. 'px')
				:wikitext(obj)
		end
	end
end



local makeMap = function (args)
	local MAP_HEIGHT = (args.type == 'RD' or args.type == 'MS2') and 10 or 8
	local MAP_WIDTH = (args.type == 'RD' or args.type == 'MS2') and 8 or 6
	local HEIGHT = MAP_HEIGHT * 60
	local WIDTH = MAP_WIDTH * 60

	local mapDiv = mw.html.create('div'):css('position', 'relative'):css('display', 'inline-block')
		:css('height', HEIGHT .. 'px'):css('width', WIDTH .. 'px')

	-- Backdrop (eg. Water, Lava)
	if args.backdrop then
		mapDiv:node(makeBackdropDiv(args.backdrop, args.type, WIDTH, HEIGHT))
	end

	-- Base Map
	if args.baseMap then
		mapDiv:node(makeBackgroundDiv(args.baseMap, args.type, WIDTH, HEIGHT))
	end

	-- Tactics Drills scroll
	if args.type == 'TD' then
		mapDiv:node(makeTDScrollDiv())
	end

	-- Starting positions
	if args.allyPos then
		local i = 1
		for pos in mw.text.gsplit(args.allyPos, '%s*,%s*') do
			mapDiv:node(makeMapObjectDiv(pos, makeStartingSpotIcon('Ally', i, '60x60px')))
			i = i + 1
		end
	end
	if args.enemyPos then
		local i = 1
		for pos in mw.text.gsplit(args.enemyPos, '%s*,%s*') do
			mapDiv:node(makeMapObjectDiv(pos, makeStartingSpotIcon('Enemy', i, '60x60px')))
			i = i + 1
		end
	end

	-- Map Objects
	for y = 1, MAP_HEIGHT do
		for x = 1, MAP_WIDTH do
			local pos = makePos(x, y)
			mapDiv:node(makeMapObjectDiv(pos, args[pos]))
		end
	end

	-- Initial units
	if args.init and args.initTab then
		local units = cargo.query(
			'MapUnits,Units',
			[[Pos,MoveType,WeaponType,Units._pageName=page,
			  IF(MapUnits.Properties__full LIKE '%is_ally%','Ally','Enemy')=Side,IF(Units.Properties__full LIKE '%generic%','1','')=isGeneric]], {
			join = 'MapUnits.Unit=Units.WikiName',
			where = ("MapUnits._pageName='%s' AND MapUnits.TabName='%s' AND Pos IS NOT NULL AND Spawn IS NULL"):format(escq(args.init), escq(args.initTab)),
			groupBy = 'Pos',
			limit = 100,
		})
		for _, v in ipairs(units) do
			v.isGeneric = v.isGeneric ~= ''
			mapDiv:node(makeMapObjectDiv(v.Pos, tostring(makeUnitIcon_noCargo(v, v.Side))))
		end
	end

	return tostring(mapDiv)
end

local makeRDmap = function (args)
	args.type = args.type or 'RD'
	return makeMap(args)
end

local initTabber = function (args)
	local MAP_HEIGHT = (args.type == 'RD' or args.type == 'MS2') and 10 or 8
	local MAP_WIDTH = (args.type == 'RD' or args.type == 'MS2') and 8 or 6
	local HEIGHT = MAP_HEIGHT * 60
	local WIDTH = MAP_WIDTH * 60

	local mapDiv = mw.html.create('div'):css('position', 'relative'):css('display', 'inline-block')
		:css('height', HEIGHT .. 'px'):css('width', WIDTH .. 'px')

	-- Backdrop (eg. Water, Lava)
	if args.backdrop then
		mapDiv:node(makeBackdropDiv(args.backdrop, args.type, WIDTH, HEIGHT))
	end

	-- Base Map
	if args.baseMap then
		mapDiv:node(makeBackgroundDiv(args.baseMap, args.type, WIDTH, HEIGHT))
	end

	-- Tactics Drills scroll
	if args.type == 'TD' then
		mapDiv:node(makeTDScrollDiv())
	end

	-- Starting positions
	if args.allyPos then
		local i = 1
		for pos in mw.text.gsplit(args.allyPos, '%s*,%s*') do
			mapDiv:node(makeMapObjectDiv(pos, makeStartingSpotIcon('Ally', i, '60x60px')))
			i = i + 1
		end
	end
	if args.enemyPos then
		local i = 1
		for pos in mw.text.gsplit(args.enemyPos, '%s*,%s*') do
			mapDiv:node(makeMapObjectDiv(pos, makeStartingSpotIcon('Enemy', i, '60x60px')))
			i = i + 1
		end
	end

	-- Map Objects
	for y = 1, MAP_HEIGHT do
		for x = 1, MAP_WIDTH do
			local pos = makePos(x, y)
			mapDiv:node(makeMapObjectDiv(pos, args[pos]))
		end
	end

	-- Initial units
	local units = cargo.query(
		'MapUnits,Units',
		[[Pos,MoveType,WeaponType,Units._pageName=page,TabName=tab,
		  IF(MapUnits.Properties__full LIKE '%is_ally%','Ally','Enemy')=Side,IF(Units.Properties__full LIKE '%generic%','1','')=isGeneric]], {
		join = 'MapUnits.Unit=Units.WikiName',
		where = ("MapUnits._pageName='%s' AND Pos IS NOT NULL AND Spawn IS NULL"):format(
			escq(args.page or mw.title.getCurrentTitle().fullText)),
		groupBy = 'tab,Pos',
		orderBy = 'tab,Pos',
		limit = 100,
	})
	for _, v in ipairs(units) do
		v.isGeneric = v.isGeneric ~= ''
	end
	if #units == 0 then
		return tostring(mapDiv)
	end

	local unitsByTab = List.group_by(units, function (v) return v.tab end)
	local unitLayouts = Hash.map(unitsByTab, function (vs)
		return table.concat(List.map(vs, function (v) return v.page .. '\0' .. v.Side .. '\0' .. v.Pos end), '\0')
	end)
	local tabs = {}
	for dif, layout in Hash.sorted_pairs(unitLayouts, function (_, _, k1, k2) return Util.difficultySort(k1, k2) end) do
		tabs[layout] = tabs[layout] or {}
		table.insert(tabs[layout], dif)
	end

	local tabberArgs = {}
	for dif, vs in Hash.sorted_pairs(unitsByTab, function (_, _, k1, k2) return Util.difficultySort(k1, k2) end) do
		local allDifs = Hash.remove(tabs, unitLayouts[dif])
		if allDifs then
			-- clone the entire html object every time except for the last tab
			local tabDiv = next(tabs) and mw.clone(mapDiv) or mapDiv
			for _, v in ipairs(vs) do
				tabDiv:node(makeMapObjectDiv(v.Pos, tostring(makeUnitIcon_noCargo(v, v.Side))))
			end
			tabberArgs[#tabberArgs + 1] = {table.concat(allDifs, '/'), tostring(tabDiv)}
		end
	end
	if #tabberArgs == 1 then
		return tabberArgs[1][2]
	end
	return Tab.tabber(tabberArgs)
end

local p = require 'Module:MakeMWModule'.makeMWModule {
	ally = ally,
	enemy = enemy,
	map = makeMap,
	RDmap = makeRDmap,
	initTabber = initTabber,
}
p.makeUnitIcon_noCargo = makeUnitIcon_noCargo
return p