Module:Page/Memento

From The Alchemist Code Wiki
Jump to: navigation, search

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

--<nowiki>
local p = {}
local h = {}

bit32 = require('bit32')
local cargo = require('Module:CargoUtil')
local util_table = require('Module:TableUtil')
local util_vars = require('Module:VarsUtil')
-- local model = require('Module:Data').model
local render_item_icon = require('Module:Render/Item')._icon2
local render_gear_icon = require('Module:Render/Gear')._icon2
local render_memento_icon = require('Module:Render/Memento')._icon2
local render_unit_icon = require('Module:Render/Unit')._icon2
local enums = require('Module:Data/Enums')
local yesno = require('Module:Yesno')
enums.eCardType = {
	[0] = "None",
	[1] = "Equipment",
	[2] = "Enhance_exp",
	[3] = "Enhance_trust"
}
local typeMap = {
	[1] = 'Equipment',
	[2] = 'Enhance EXP',
	[3] = 'Enhance Trust',
}

h.flags = {
	NONE = 0x0,
	TRIGGER = 0x1,
	BUFF = 0x2,
	TRIGGER_BUFF = 0x3,
}

local function setdefault(dict, key, value)
	dict[key] = dict[key] or value
	return dict[key]
end

function h.quote( s )
	if not s then return end
	return ('%q'):format(s)
end

function h.makeInfobox(args)
	local root = mw.html.create('div')
	root:addClass('infobox')
	root:addClass(args.class)
	local heading = root:tag('div')
	heading:addClass('heading')
	local icon = heading:tag('div')
	icon:addClass('infobox-icon')
	icon:node(args.icon)
	local name = heading:tag('div')
	name:addClass('name')
	name:wikitext(args.name)
	local wrapper = root:tag('div')
	wrapper:addClass('wrapper')
	local dl = wrapper:tag('dl')

	local function printRow(t, label, content)
		if content == nil then return end
		local dt = t:tag('dt')
		dt:wikitext(label)
		local dd = t:tag('dd')
		dd:wikitext(content)
	end
	
	for i, label in ipairs(args.labels) do
		printRow(dl, label, args.rows[label])
	end

	return tostring(root)
end

local cats = {}

function p.getData(iname)
	local datapage = cargo.query{
		tables = 'ConceptCard',
		fields = '_pageName',
		where = 'iname = "'..iname..'"',
		groupBy = '_pageName, iname',
	}[1]
	local pagename = datapage and datapage._pageName or 'Data:Game/MasterParam/ConceptCard/'..iname
	local frame = mw.getCurrentFrame()
	local title = mw.title.new(pagename)
	local content = title:getContent()
	local args = {}
	local params = content:match('^%s*%{%{#invoke:Data%|insert%|(.-)%}%}%s*$')
	for param in mw.text.gsplit(params, '|', true) do
		local k, v = param:match('([^=]+)=(.+)')
		local obj = mw.text.jsonDecode(frame:preprocess(v))
		if k == 'gl' or k == 'jp' then obj.server = k end
		args[k] = obj
	end
	return args
end

local maxLevels = {1, 25, 30, 35, 40}

function h.getVCR(data)
	local t = {}
	local rows = cargo.query{
		tables = {
			'ConceptCard=CC',
			'ConceptCard__trust_item_names=CC_IT_names', 'Item=IT', 'ItemLoc=ITL', 'Pages=ITP',
			'ConceptCard__trust_item_counts=CC_IT_counts',
			'ConceptCard__trust_artifact_names=CC_AF_names', 'Artifact=AF', 'ArtifactLoc=AFL', 'Pages=AFP',
			'ConceptCard__trust_artifact_counts=CC_AF_counts',
		},
		join = {
			'CC._ID=CC_IT_counts._rowID', 'CC._ID=CC_IT_names._rowID', 'CC_IT_names._value=IT.iname', 'IT.iname=ITL.iname', 'IT.iname=ITP.iname',
			'CC._ID=CC_AF_counts._rowID', 'CC._ID=CC_AF_names._rowID', 'CC_AF_names._value=AF.iname', 'AF.iname=AFL.iname', 'AF.iname=AFP.iname',
		},
		where = {
			'CC.iname="'..data.iname..'"',
			'CC.server="gl"',
			'COALESCE(IT.server,CC.server)=CC.server',
			'COALESCE(ITL.lang,"english")="english"',
			'COALESCE(CC_IT_names._position,0)=COALESCE(CC_IT_counts._position,0)',
			'COALESCE(AF.server,CC.server)=CC.server',
			'COALESCE(AFL.lang,"english")="english"',
			'COALESCE(CC_AF_names._position,0)=COALESCE(CC_AF_counts._position,0)',
		},
		fields = {
			'ITP._pageName=IT_pageName',
			'CC_IT_names._position=CC_IT_idx',
			'IT.iname=IT_iname',
			'IT.icon=IT_icon',
			'ITL.name=IT_name',
			'IT.rare=IT_rare',
			'IT.type=IT_type',
			'CC_IT_counts._value=IT_count',
			'CC_AF_names._position=CC_AF_idx',
			'AFP._pageName=AF_pageName',
			'AF.iname=AF_iname',
			'AF.icon=AF_icon',
			'AFL.name=AF_name',
			'AF.rini=AF_rare',
			'AF.type=AF_type',
			'CC_AF_counts._value=AF_count',
		}
	}
	local items = {}
	local gears = {}
	for i, row in ipairs(rows) do
		local idx = tonumber(row.CC_IT_idx)
		if idx then
			items[idx] = items[idx] or render_item_icon{data={
					_pageName = row.IT_pageName,
					iname = row.IT_iname,
					icon = row.IT_icon,
					name = row.IT_name,
					rare = tonumber(row.IT_rare),
					type = tonumber(row.IT_type),
				}, name='none', count=(tonumber(row.IT_count) or 0) > 1 and tonumber(row.IT_count)}
		end
		local idx = tonumber(row.CC_AF_idx)
		if idx then
			items[idx] = items[idx] or render_gear_icon{data={
				_pageName = row.AF_pageName,
				iname = row.AF_iname,
				icon = row.AF_icon,
				name = row.AF_name,
				rare = tonumber(row.AF_rare),
				type = tonumber(row.AF_type),
			}, name='none', count=(tonumber(row.AF_count) or 0) > 1 and tonumber(row.AF_count)}
		end
	end
	local t = {}
	t[#t+1] = table.concat(items)
	t[#t+1] = table.concat(gears)

	return #t > 0 and table.concat(t) or nil
end
p.x = h.getVCR

function h.getUnitReward(data)
	local iname = data.first_get_unit or ''
	if iname == '' then return nil end
	local row = cargo.query{
		tables = {'Unit=UN', 'UnitLoc=UNL', 'Pages=P'},
		join = {'UN.iname = UNL.iname', 'UN.iname = P.iname'},
		fields = {
			'UN.iname = iname',
			'UNL.name = name',
			'P._pageName = _pageName',
			'UN.rare = rare',
			'UN.img = icon',
		},
		where = {
			'UN.iname = "'..iname..'"',
			'UN.server = "gl"',
			'UNL.lang = "english"',
		},
	}[1]
	return render_unit_icon{data=row, size='small'}
end

function h.minQuery(args)
	if type(args.fields) == 'string' and args.fields == '*' then
		local subModel = mw.loadData('Module:Data/Model/'..args.tables)
		local t = {'_pageName', 'server'}
		for k,v in pairs(subModel) do
			t[#t+1] = k
		end
		args.fields = t
	end
	local row = cargo.query(args)[1]
	if not row then return end
	local obj = {}
	for k,v in pairs(row) do
		if type(v) == 'table' then
			if #v > 0 then obj[k] = v end
		elseif v ~= '' then
			obj[k] = v
		end
	end
	return obj
end

function h.queryBuff(iname)
	if not iname then return end
	return h.minQuery{tables='Buff',fields='*', where='iname="'..iname..'" AND server="gl"'}
end

function h.printEffects(root, args)
	if not args then return end
	if not args.effects then return end

	local stat = nil
	local card_skills = {}
	local abilities = {}

	for i, eff in ipairs(args.effects) do
		if eff.statusup_skill then
			stat = eff
		end
		if eff.card_skill then
			card_skills[#card_skills+1] = eff
		end
		if eff.abil_iname then
			abilities[#abilities+1] = eff
		end
	end
	
	local maxLevel = maxLevels[args.rare+1]
	if stat then
		root:tag('h2'):wikitext('Stats')
		h.printStatsTable(root, stat.statusup_skill, maxLevel)
	end

	if #card_skills > 0 then
		root:tag('h2'):wikitext('Group skills')
		for i, eff in ipairs(card_skills) do
			local skill = cargo.query{
				tables = {'Skill', 'SkillLoc'},
				join = 'Skill.iname = SkillLoc.iname',
				fields = {'Skill._pageName', 'name'},
				where = {
					'Skill.iname = "'..eff.card_skill..'"',
					'Skill.server = "gl"',
					'SkillLoc.lang = "english"',
				},
			}[1]
			local skillname = skill and skill.name or '???'
			root:tag('h3'):wikitext(skillname)
			h.printStatsTable(root, eff.card_skill, maxLevel)

			if eff.add_card_skill_buff_awake then -- limit break buff
				root:tag('h4'):wikitext('Limit Break')
				local buff_details = cargo.query{
					tables = 'BuffDetail',
					fields = {
						'idx',
						'calc',
						'tktag',
						'type',
						'vini',
						'vmax',
						'vone',
					},
					where = {
						'BuffDetail.server = "gl"',
						'BuffDetail.buff_iname = "'..eff.add_card_skill_buff_awake..'"',
					},
					orderBy = 'idx',
				}
				if #buff_details == 0 then
					root:tag('div'):wikitext('Missing buff ' .. eff.add_card_skill_buff_awake .. ' from DB')
					return
				end
				-- Show limit break
				local tbl = root:tag('table'):addClass('wikitable')
				local tr = tbl:tag('tr')
				tr:tag('th'):wikitext('Type')
				for lb=1,5 do tr:tag('th'):wikitext('LB'..lb) end
				for i, buff in ipairs(buff_details) do
					tr = tbl:tag('tr')
					if tonumber(buff.type or 0) > 0 then
						local statName = enums.statNameFromType(buff.type, buff.tktag)
						tr:tag('td'):wikitext(statName)
						local min, max = buff.vini, buff.vmax
						local per = (max - min) / 4
						for lb=1,5 do
							local value = math.floor((lb - 1) * per + min)
							tr:tag('td'):wikitext(string.format('%+d', value))
						end
					end
				end
			end

			if eff.add_card_skill_buff_lvmax then
				root:tag('h4'):wikitext('Max Limit Break')
				local buffdetails = cargo.query{
					tables = 'BuffDetail',
					fields = {
						'idx',
						'calc',
						'tktag',
						'type',
						'vini',
						'vmax',
						'vone',
					},
					where = {
						'BuffDetail.server = "gl"',
						'BuffDetail.buff_iname = "'..eff.add_card_skill_buff_lvmax..'"',
					},
					orderBy = 'idx',
				}
				if #buffdetails == 0 then
					root:tag('div'):wikitext('Missing buff '..eff.add_card_skill_buff_lvmax..' from DB')
				else
					local tbl = root:tag('table'):addClass('wikitable')
					local tr = tbl:tag('tr')
					tr:tag('th'):wikitext('Type')
					for lb=5,5 do tr:tag('th'):wikitext('LB'..lb) end
					for i, buff in ipairs(buffdetails) do
						tr = tbl:tag('tr')
						if tonumber(buff.type or 0) > 0 then
							local statName = enums.statNameFromType(buff.type, buff.tktag)
							tr:tag('td'):wikitext(statName)
							local min, max = buff.vini, buff.vmax
							local per = (max - min) / 4
							for lb=5,5 do
								local value = math.floor((lb - 1) * per + min)
								tr:tag('td'):wikitext(string.format('%+d', value))
							end
						end
					end
				end
			end

			if eff.cnds_iname then
				h.printEffect(root, eff)
			end
		end
	end
	if #abilities > 0 then
		root:tag('h2'):wikitext('Vision abilities')

		for _, eff in ipairs(abilities) do
			h.printAbility(root, eff)
			if eff.cnds_iname then
				h.printEffect(root, eff)
			end
		end
	end
end

function h.printAbility(root, eff)
	if not eff then return end
	if not eff.abil_iname then return end
	if not eff.abil_iname_lvmax then return end

	local ability = {}
	local ids = {
		eff.abil_iname,
		eff.abil_iname_lvmax,
	}

	local t = {}
	local rows = cargo.query{
		tables = {
			'Ability = AB',
			'AbilityLoc = ABL',
			'Skill = SK',
			'SkillLoc = SKL',
		},
		join = {
			'AB.iname = ABL.iname',
			'AB.skl1 = SK.iname',
			'SK.iname = SKL.iname',
		},
		fields = {
			'AB.iname = iname', 
			'AB.type_detail = type_detail',
			'AB.slot = slot',
			'AB.skl1 = skl1',
			'ABL.name = ab_name',

			'SKL.name = sk_name',
			'SKL.expr = sk_expr',
			'SK.atk_type = sk_atk_type',
			'SK.atk_det = sk_atk_det',
			'SK.elem = sk_elem',
			'SK.eff_type = sk_eff_type',
			'SK.eff_val_ini = sk_eff_val_ini',
			'SK.eff_val_max = sk_eff_val_max',
			'SK.count = sk_count',
			'SK.cost = sk_cost',
			'SK.eff_h = sk_eff_h',
			'SK.sran = sk_sran',
			'SK.rangemin = sk_rangemin',
			'SK.rangemax = sk_rangemax',
			'SK.ssco = sk_ssco',
			'SK.scope = sk_scope',
		},
		where = {
			'AB.iname IN ('..util_table.concat(ids, ',', h.quote)..')',
			'AB.server="gl"',
			'ABL.lang = "english"',
			'SK.server IS NULL OR SK.server = "gl" AND SKL.lang = "english"',
		},
	}
	for i, row in ipairs(rows) do
		t[row.iname] = t[row.iname] or {
			name = row.ab_name,
			type_detail = row.type_detail,
			slot = tonumber(row.slot),
		}
		if row.skl1 then
			t[row.iname].skl1 = t[row.iname].skl1 or {
				name = row.sk_name,
				expr = row.sk_expr,
				atk_type = tonumber(row.sk_atk_type),
				atk_det = tonumber(row.sk_atk_det),
				elem = tonumber(row.sk_elem),
				eff_type = tonumber(row.sk_eff_type),
				eff_val_ini = tonumber(row.sk_eff_val_ini),
				eff_val_max = tonumber(row.sk_eff_val_max),
				count = tonumber(row.sk_count),
				cost = tonumber(row.sk_cost),
				eff_h = tonumber(row.sk_eff_h),
				sran = tonumber(row.sk_sran),
				rangemin = tonumber(row.sk_rangemin),
				rangemax = tonumber(row.sk_rangemax),
				ssco = tonumber(row.sk_ssco),
				scope = tonumber(row.sk_scope),
			}
		end
	end
	ability.base = t[eff.abil_iname]
	ability.max = t[eff.abil_iname_lvmax]

	local baseName = ability.base.name
	local maxName = ability.max.name
	root:tag('h3'):wikitext(baseName)
	if maxName ~= baseName then
		error('Expected max ability name to be `'..baseName..'` but was `'..maxName..'`.')
	end

	for _, key in ipairs{'base', 'max'} do
		root:tag('h4'):wikitext('Ability '..key)
		local ab = ability[key]
		if ab.type_detail then
			local abType = enums.EAbilityTypeDetail[ab.type_detail]
			if abType and abType ~= 'VisionAbility' then
				error('Expected ability type "VisionAbility", but was "'..tostring(abType)..'".')
			end
		end
		local ul = root:tag('ul')
		if ab.slot then
			local slot = enums.EAbilitySlot[ab.slot]
			ul:tag('li'):wikitext('Slot: [[File:MasterAbilityIcon.png|20px|'..slot..'|link=]] '..slot)
		end
		local skill = ab.skl1
		if skill then
			if (skill.atk_type or 0) > 0 then
				local atk_type = enums.AttackTypes[skill.atk_type]
				if atk_type == 'MagAttack' then
					atk_type = 'Magical'
				elseif atk_type == 'PhyAttack' then
					atk_type = 'Physical'
				end
				ul:tag('li'):wikitext('DMG Type: [[File:Attack Class '..atk_type..'.png|20px|'..atk_type..'|link=]] '..atk_type)
			end
			if (skill.atk_det or 0) > 0 then
				local atk_det = enums.attackDetailNameFromKey(skill.atk_det)
				if atk_det == 'Blow' then
					atk_det = 'Strike'
				end
				ul:tag('li'):wikitext('ATK Type: [[File:Attack Type '..atk_det..'.png|20px|'..atk_det..'|link=]] '..atk_det)
			end
			if (skill.elem or 0) > 0 then
				local elem = enums.elemFromKey(skill.elem)
				ul:tag('li'):wikitext('Element: [[File:'..elem..'Element.png|20px|link=]] '..elem)
			end

			-- Explanation
			root:tag('p'):wikitext(skill.expr)

			-- Skill effect
			if (skill.eff_type or 0) > 0 and skill.eff_val_ini and skill.eff_val_max then
				local tbl = root:tag('table'):addClass('wikitable')
				tbl:tag('caption'):wikitext('Skill Effect')
				local tr = tbl:tag('tr')
				tr:tag('th'):wikitext('Type')
				tr:tag('th'):wikitext('Min')
				tr:tag('th'):wikitext('Max')
				tr = tbl:tag('tr')
				tr:tag('td'):wikitext(enums.SkillEffectTypes[skill.eff_type])
				tr:tag('td'):wikitext((100+skill.eff_val_ini)..'%')
				tr:tag('td'):wikitext((100+skill.eff_val_max)..'%')
			end

			-- Scaling
			-- Jewel cost
			local ul = root:tag('ul')
			if (skill.count or 0) > 0 then
				ul:tag('li'):wikitext('Charges: ' .. skill.count)
			end
			if (skill.cost or 0) > 0 then
				ul:tag('li'):wikitext('Jewel Cost: ' .. skill.cost)
			end
			if (skill.eff_h or 0) > 0 then
				ul:tag('li'):wikitext('Height: ' .. skill.eff_h)
			end
			if skill.sran then
				ul:tag('li'):wikitext('Select Range: ' .. enums.ESelectType[skill.sran])
				ul:tag('li'):wikitext('Range: ' .. (skill.rangemin or 0) .. '-'.. (skill.rangemax or 0))
			end
			if skill.ssco then
				ul:tag('li'):wikitext('Select Scope: ' .. enums.ESelectType[tonumber(skill.ssco)])
				ul:tag('li'):wikitext('Scope: ' .. (skill.scope or 0))
			end
		end
	end
end

function h.printEffect(root, args)
	-- Stats
	if args.statusup_skill then
		h.printStatsTable(root, args.statusup_skill, args.rare)
	end

	local inames = {}
	-- Condition groups
	for iname, flag in pairs(h.getTriggerGroups(args.cnds_iname) or {}) do
		inames[iname] = inames[iname] or {}
		inames[iname].hasConditions = true
	end
	
	-- Buff groups
	for iname, flag in pairs(h.getBuffGroups(args.add_card_skill_buff_awake) or {}) do
		inames[iname] = inames[iname] or {}
		inames[iname].hasBuff = true
	end

	local icons = {
		['Trigger + Buff'] = {Unit={},Job={}},
		['Trigger'] = {Unit={},Job={}},
		['Buff'] = {Unit={},Job={}},
		[''] = {Unit={},Job={}},
	}

	local ids = util_table.concat(util_table.getKeys(inames), ',', h.quote)
	if ids == '' then return end
	local rows = cargo.query{
		tables = {
			'Pages',
			'Unit', 'UnitLoc',
			'Job', 'JobLoc',
		},
		join = {
			'Pages.iname = Unit.iname',
			'Unit.iname = UnitLoc.iname',
			'Pages.iname = Job.iname',
			'Job.iname = JobLoc.iname',
		},
		fields = {
			'Pages.iname = iname',
			'Pages._pageName = _pageName',
			'Pages.type = entityType',
			'COALESCE(Job.ac2d, Job.mdl, Unit.img) = icon',
			'COALESCE(JobLoc.name, UnitLoc.name) = name',
		},
		where = {
			'Pages.iname IN ('..ids..')',
			'Job.server IS NULL OR Job.server = "gl" AND JobLoc.lang = "english"',
			'Unit.server IS NULL OR Unit.server = "gl" AND UnitLoc.lang = "english"',
		},
	}
	for i, row in ipairs(rows) do
		local thing = inames[row.iname]
		local triggerbuff = {}
		if thing.hasConditions then
			triggerbuff[#triggerbuff+1] = 'Trigger'
		end
		if thing.hasBuff then
			triggerbuff[#triggerbuff+1] = 'Buff'
		end
		triggerbuff = table.concat(triggerbuff, ' + ')
		local tp = row.entityType
		local render_icon = require('Module:Render/'..tp)._icon2
		local t = icons[triggerbuff][tp]
		t[row.iname] = {
			iname = row.iname,
			name = row.name,
			icon = render_icon({data=row, size='small'}),
		}
	end

	if args.add_card_skill_buff_awake then
		for _, triggerbuff in ipairs({'Trigger + Buff', 'Trigger', 'Buff', ''}) do
			for _, tp in ipairs({'Unit', 'Job'}) do
				local objs = {}
				for iname, obj in pairs(icons[triggerbuff][tp]) do
					objs[#objs+1] = obj
				end
				if next(objs) then
					local heading = root:tag('h4')
					heading:wikitext(tp, ' group')
					if triggerbuff ~= '' then
						heading:wikitext(' (', triggerbuff:lower(), ')')
					end
					table.sort(objs, function(a, b)
						return a.name < b.name
					end)
					for i, obj in ipairs(objs) do
						root:wikitext(obj.icon)
					end
				end
			end
		end
	elseif args.abil_iname then
		local objs = {}
		if next(icons.Trigger.Unit) and next(icons.Trigger.Job) then
			local unit_inames = util_table.getKeys(icons.Trigger.Unit)
			local job_inames = util_table.getKeys(icons.Trigger.Job)
			local un_ids = util_table.concat(unit_inames, ',', h.quote)
			local jb_ids = util_table.concat(job_inames, ',', h.quote)

			for _, row in ipairs(cargo.query{
				tables = {
					'Unit',
					'Unit__jobsets',
					'JobSet',
				},
				fields = {
					'Unit.iname = un_iname',
					'JobSet.job = jb_iname',
				},
				where = {
					'Unit.server = "gl"',
					'JobSet.server = "gl"',
					'Unit.iname IN ('..un_ids..')',
					'JobSet.job IN ('..jb_ids..')',
				},
				join = {
					'Unit._ID = Unit__jobsets._rowID',
					'Unit__jobsets._value = JobSet.iname',
				},
			}) do
				objs[#objs+1] = {
					unit=icons.Trigger.Unit[row.un_iname],
					job=icons.Trigger.Job[row.jb_iname],
				}
			end
			local heading = root:tag('h4')
			heading:wikitext('Unit and job restriction')
			table.sort(objs, function(a, b)
			   if a.unit.name ~= b.unit.name then
			   	return a.unit.name < b.unit.name
			   end
				return a.job.name < b.job.name
			end)
			local ul = root:tag('ul')
			for _, obj in ipairs(objs) do
				local li = ul:tag('li')
				li:wikitext(obj.unit.icon, ' + ', obj.job.icon)
			end
		elseif next(icons.Trigger.Unit) then
			local heading = root:tag('h4')
			heading:wikitext('Unit restriction')
			for iname, obj in pairs(icons.Trigger.Unit) do
				objs[#objs+1] = obj
			end
			table.sort(objs, function(a,b) return a.name < b.name end)
			for _, obj in ipairs(objs) do
				root:wikitext(obj.icon)
			end
		elseif next(icons.Trigger.Job) then
			local heading = root:tag('h4')
			heading:wikitext('Job restriction')
			for iname, obj in pairs(icons.Trigger.Job) do
				objs[#objs+1] = obj
			end
			table.sort(objs, function(a,b) return a.name < b.name end)
			for _, obj in ipairs(objs) do
				root:wikitext(obj.icon)
			end
		end
	end
end

function h.getTriggerGroups(cnds_iname)
	-- Condition groups
	if not cnds_iname then return end

	local rows = cargo.query{
		tables = {
			'ConceptCardConditions = CCC',
			'ConceptCardConditions__birth_id = CCC__UN2',
			'JobGroup = JG1',
			'JobGroup__jobs = JG1__JB1',
			'Job = JB1',
			'UnitGroup = UG1',
			'UnitGroup__units = UG1__UN1',
			'Unit = UN1',
			'Unit = UN2',
			'Unit = UN3',
		},
		join = {
			'CCC.job_group = JG1.iname',
			'JG1._ID = JG1__JB1._rowID',
			'JG1__JB1._value = JB1.iname',
			'CCC.un_group = UG1.iname',
			'UG1._ID = UG1__UN1._rowID',
			'UG1__UN1._value = UN1.iname',
			'CCC._ID = CCC__UN2._rowID',
			'CCC__UN2._value = UN2.birth_id',
			'CCC.sex = UN3.sex',
		},
		fields = {
			'COALESCE(JB1.origin, JB1.iname) = jb_iname1',
			'UN1.iname = un_iname1',
			'UN2.iname = un_iname2',
			'UN3.iname = un_iname3',
		},
		where = {
			'CCC.iname = "'..cnds_iname..'"',
			'CCC.server = "gl"',
			'JG1.server IS NULL OR JB1.server IS NULL OR JG1.server = "gl" AND JB1.server = "gl"',
			'UG1.server IS NULL OR UN1.server IS NULL OR UG1.server = "gl" AND UN1.server = "gl" AND UN1.ai = "AI_PLAYER" AND (UN1.notsmn IS NULL OR UN1.hero = 1)',
			'UN2.server IS NULL OR UN2.server = "gl" AND UN2.ai = "AI_PLAYER" AND (UN2.notsmn IS NULL OR UN2.hero = 1)',
			'UN3.server IS NULL OR UN3.server = "gl" AND UN3.ai = "AI_PLAYER" AND (UN3.notsmn IS NULL OR UN3.hero = 1)',
		},
	}
	local t = {}
	for _, row in ipairs(rows) do
		for k, v in pairs(row) do
			t[v] = h.flags.TRIGGER
		end
	end
	return t
end

function h.getBuffGroups(buff_iname)
	if not buff_iname then return end
	local rows = cargo.query{
		tables = {
			'Buff = BF', -- buff
			'UnitGroup = UG0', -- buff.un_group
			'UnitGroup__units = UG0__UN0',
			'Unit = UN0', -- buff.un_group.units
			'Buff__custom_targets = BF__CT',
			'CustomTarget = CT', -- buff.custom_targets
			'CustomTarget__unit_groups = CT__UG1',
			'UnitGroup = UG1', -- buff.custom_targets.unit_groups
			'UnitGroup__units = UG1__UN1',
			'Unit = UN1', -- buff.custom_targets.unit_groups.units
			'CustomTarget__units = CT__UN2',
			'Unit = UN2', -- buff.custom_targets.units
			'Unit = UN3', -- buff.custom_targets.birth_id
			'Unit = UN4', -- buff.custom_targets.sex
			'CustomTarget__job_groups = CT__JG1',
			'JobGroup = JG1', -- buff.custom_targets.job_groups
			'JobGroup__jobs = JG1__JB1',
			'Job = JB1', -- buff.custom_targets.job_groups.jobs
			'CustomTarget__jobs = CT__JB2',
			'Job = JB2', -- buff.custom_targets.jobs
		},
		join = {
			'BF.un_group = UG0.iname',
			'BF._ID = BF__CT._rowID', 'BF__CT._value = CT.iname',
			'UG0._ID = UG0__UN0._rowID', 'UG0__UN0._value = UN0.iname',
			'CT._ID = CT__UG1._rowID', 'CT__UG1._value = UG1.iname',
			'UG1._ID = UG1__UN1._rowID', 'UG1__UN1._value = UN1.iname',
			'CT._ID = CT__UN2._rowID', 'CT__UN2._value = UN2.iname',
			'CT.birth_id = UN3.birth_id',
			'CT.sex = UN4.sex',
			'CT._ID = CT__JG1._rowID', 'CT__JG1._value = JG1.iname',
			'JG1._ID = JG1__JB1._rowID', 'JG1__JB1._value = JB1.iname',
			'CT._ID = CT__JB2._rowID', 'CT__JB2._value = JB2.iname',
		},
		fields = {
			'UN0.iname = un_iname0',
			'UN1.iname = un_iname1',
			'UN2.iname = un_iname2',
			'UN3.iname = un_iname3',
			'UN4.iname = un_iname4',
			'COALESCE(JB1.origin, JB1.iname) = jb_iname1',
			'COALESCE(JB2.origin, JB2.iname) = jb_iname2',
		},
		where = {
			'BF.iname = "'..buff_iname..'"',
			'BF.server = "gl"',
			'CT.server = "gl"',
			'UG0.server IS NULL OR UN0.server IS NULL OR UG0.server = "gl" AND UN0.server = "gl" AND UN0.ai = "AI_PLAYER" AND (UN0.notsmn IS NULL OR UN0.hero = 1)',
			'UG1.server IS NULL OR UN1.server IS NULL OR UG1.server = "gl" AND UN1.server = "gl" AND UN1.ai = "AI_PLAYER" AND (UN1.notsmn IS NULL OR UN1.hero = 1)',
			'UN2.server IS NULL OR UN2.server = "gl" AND UN2.ai = "AI_PLAYER" AND (UN2.notsmn IS NULL OR UN2.hero = 1)',
			'UN3.server IS NULL OR UN3.server = "gl" AND UN3.ai = "AI_PLAYER" AND (UN3.notsmn IS NULL OR UN3.hero = 1)',
			'UN4.server IS NULL OR UN4.server = "gl" AND UN4.ai = "AI_PLAYER" AND (UN4.notsmn IS NULL OR UN4.hero = 1)',
			'JG1.server IS NULL OR JB1.server IS NULL OR JG1.server = "gl" AND JB1.server = "gl"',
			'JB2.server IS NULL OR JB2.server = "gl"',
		},
	}
	local t = {}
	for _, row in ipairs(rows) do
		for k, v in pairs(row) do
			t[v] = h.flags.BUFF
		end
	end
	return t
end

function h.printStatsTable(root, skill_iname, maxLevel)
	if not skill_iname then return end
	local rows = cargo.query{
		tables = {
			'Skill = SK',
			'BuffDetail = TBD',
			'BuffDetail = SBD',
		},
		join = {
			'SK.t_buff = TBD.buff_iname',
			'SK.s_buff = SBD.buff_iname',
		},
		fields = {
			'TBD.idx = t_idx',
			'TBD.calc = t_calc',
			'TBD.tktag = t_tktag',
			'TBD.type = t_type',
			'TBD.vini = t_vini',
			'TBD.vmax = t_vmax',
			'TBD.vone = t_vone',
			'SBD.idx = s_idx',
			'SBD.calc = s_calc',
			'SBD.tktag = s_tktag',
			'SBD.type = s_type',
			'SBD.vini = s_vini',
			'SBD.vmax = s_vmax',
			'SBD.vone = s_vone',
		},
		where = {
			'SK.iname="'..skill_iname..'"',
			'SK.server="gl"',
			'COALESCE(TBD.server,"gl")="gl"',
			'COALESCE(SBD.server,"gl")="gl"',
		},
	}
	if #rows == 0 then return end
	
	local t_buff_details = {}
	local s_buff_details = {}
	for i, row in ipairs(rows) do
		local idx = tonumber(row.t_idx)
		if idx then
			t_buff_details[idx] = {
				idx = row.t_idx,
				calc = row.t_calc,
				tktag = row.t_tktag,
				type = row.t_type,
				vini = row.t_vini,
				vmax = row.t_vmax,
				vone = row.t_vone,
			}
		end
		local idx = tonumber(row.s_idx)
		if idx then
			s_buff_details[idx] = {
				idx = row.s_idx,
				calc = row.s_calc,
				tktag = row.s_tktag,
				type = row.s_type,
				vini = row.s_vini,
				vmax = row.s_vmax,
				vone = row.s_vone,
			}
		end
	end
	local stats = {
	}
	for i, buff in ipairs(t_buff_details) do
		if tonumber(buff.type or 0) > 0 then
			local tp = tostring(buff.type)
			local values = {}
			local min, max = buff.vini, buff.vmax
			local per = (max - min) / (maxLevel - 1)
			for level=1,maxLevel do
				values[level] = math.floor((level - 1) * per + min)
			end
			stats[tp] = values
		end
	end

	-- Render
	root:tag('h4'):wikitext('Simple stats')
	local simple = root:tag('table'):addClass('wikitable')
	local tr = simple:tag('tr')
	tr:tag('th'):wikitext('Type')
	tr:tag('th'):wikitext('Lvl 1')
	tr:tag('th'):wikitext('Lvl '..(maxLevel-10))
	tr:tag('th'):wikitext('Lvl '..maxLevel)
	for i, buff in ipairs(t_buff_details) do
		if tonumber(buff.type or 0) > 0 then
			local tr = simple:tag('tr')
			local tp = tostring(buff.type)
			local values = stats[tp]
			local statName = enums.statNameFromType(buff.type, buff.tktag)
			tr:tag('td'):wikitext(statName)
			tr:tag('td'):wikitext(string.format('%+d', values[1]))
			tr:tag('td'):wikitext(string.format('%+d', values[maxLevel-10]))
			tr:tag('td'):wikitext(string.format('%+d', values[maxLevel]))
		end
	end
	
	--[==[ Scrollable table?
		<div style="display: table">
		<div style="max-height: 240px; overflow-y: scroll; box-sizing: border-box; padding: 4px; border: 1px solid #131313">
		-- Long table here
		</div>
		</div>
	--]==]

	root:tag('h4'):wikitext('Detailed stats')
	local detailed = root:tag('div'):css{
		display = 'flex',
		['flex-wrap'] = 'wrap',
	}
	for col = 1, maxLevel/10 do
		local flexitem = detailed:tag('div'):css{
			flex = 0,
			background = col % 2 == 0 and 'orange' or 'salmon', -- yum ^_^
		}
		local wikitable = flexitem:tag('table'):addClass('wikitable')
		local tr = wikitable:tag('tr')
		tr:tag('th'):wikitext('Level')
		for i, buff in ipairs(t_buff_details) do
			if tonumber(buff.type or 0) > 0 then
				local statName = enums.statNameFromType(buff.type, buff.tktag)
				tr:tag('th'):wikitext(statName)
			end
		end
		for row = 1, 10 do
			local level = (col - 1) * 10 + row
			local tr = wikitable:tag('tr')
			tr:tag('td'):wikitext(level)
			for i, buff in ipairs(t_buff_details) do
				if tonumber(buff.type or 0) > 0 then
					local tp = tostring(buff.type)
					tr:tag('td'):wikitext(string.format('%+d', stats[tp][level]))
				end
			end
		end
	end
end

function p.renderPage(iname)
	local card_rows = cargo.query{
		tables = {
			'ConceptCard = CC',
			'ConceptCardLoc = CCLoc',
			'ConceptCardEffect = CCE',
		},
		join = {
			'CC.iname = CCLoc.iname',
			'CC.iname = CCE.cc_iname',
		},
		fields = {
			'CC.iname = iname',
			-- Graphics
			'CC.icon = icon',
			-- Type
			'CC.type = type', -- eCardType
			'CC.rare = rare',
			'CC.lvcap = lvcap', -- Default defined by rare
			'CC.birth_id = birth_id', -- The origin of the memento
			'CC.coin_item = coin_item', -- Not sure what this is used for, maybe related to the coin shop
			
			-- Value
			'CC.sell = sell', -- Integer",
			'CC.not_sale = not_sale', -- Boolean",
			'CC.en_cost = en_cost', -- Integer",
			'CC.en_exp = en_exp', -- Integer",
			'CC.en_trust = en_trust', -- Integer",
			
			-- Rewards
			'CC.trust_artifact_names = trust_artifact_names', -- {"String", list='|', dataRef="MasterParam/Artifact"},
			'CC.trust_artifact_counts = trust_artifact_counts', -- {"Integer", list='|'},
			'CC.trust_item_names = trust_item_names', -- {"String", list='|', dataRef="MasterParam/Item"},
			'CC.trust_item_counts = trust_item_counts', -- {"Integer", list='|'},
			
			-- References
			'CC.first_get_unit = first_get_unit', -- {"String", dataRef="MasterParam/Unit"}, -- Unit acquired when first obtaining memento
			
			-- Leader Skill
			'CC.concept_card_groups = concept_card_groups', -- {"String", list='|', dataRef="MasterParam/ConceptCardGroup"},
			'CC.leader_skill = leader_skill', -- {"String", dataRef="MasterParam/Skill"},
			-- Labels
			'CCLoc.name = name',
			'CCLoc.flavor = flavor',
		  
			'CCE.abil_iname = abil_iname',
			'CCE.abil_iname_lvmax = abil_iname_lvmax',
			'CCE.add_card_skill_buff_awake = add_card_skill_buff_awake',
			'CCE.add_card_skill_buff_lvmax = add_card_skill_buff_lvmax',
			'CCE.card_skill = card_skill',
			'CCE.cnds_iname = cnds_iname',
			'CCE.is_decrease_eff = is_decrease_eff',
			'CCE.skin = skin',
			'CCE.statusup_skill = statusup_skill',
		},
		where = {
			'CC.iname = "'..iname..'"',
			'CC.server = "gl"',
			'CCLoc.lang = "english"',
			'COALESCE(CCE.server, "gl") = "gl"',
		},
	}
	mw.logObject(card_rows)
	
	local data = nil
	for i, row in ipairs(card_rows) do
		data = data or {
			iname = row.iname,
			icon = row.icon,
			type = row.type,
			rare = row.rare,
			lvcap = row.lvcap,
			birth_id = row.birth_id,
			coin_item = row.coin_item,
			sell = row.sell,
			not_sale = row.not_sale,
			en_cost = row.en_cost,
			en_exp = row.en_exp,
			en_trust = row.en_trust,
			trust_artifact_names = row.trust_artifact_names,
			trust_artifact_counts = row.trust_artifact_counts,
			trust_item_names = row.trust_item_names,
			trust_item_counts = row.trust_item_counts,
			first_get_unit = row.first_get_unit,
			concept_card_groups = row.concept_card_groups,
			leader_skill = row.leader_skill,
			name = row.name,
			flavor = row.flavor,
			effects = {},
		}
		data.effects[i] = {
			abil_iname = row.abil_iname,
			abil_iname_lvmax = row.abil_iname_lvmax,
			add_card_skill_buff_awake = row.add_card_skill_buff_awake,
			add_card_skill_buff_lvmax = row.add_card_skill_buff_lvmax,
			card_skill = row.card_skill,
			cnds_iname = row.cnds_iname,
			is_decrease_eff = row.is_decrease_eff,
			skin = row.skin,
			statusup_skill = row.statusup_skill,
		}
	end
	if not data then
		return '`'..iname..'` not found in ConceptCard table.'
	end
	local effects = data.effects
	cats[#cats+1] = 'Memento'

	local name = data.name
	local icon = mw.html.create('div')
	icon:addClass('item-icon')
	local img = icon:tag('div')
	img:addClass('img x-'..data.rare..' y-4')
	img:css{
		['width'] = '57px',
		['height'] = '57px',
		['padding'] = '3.5px',
	}
	img:wikitext('[[File:Game,ConceptCardIcon,',data.icon,'.png|57px]]')
	-- Memento extra data
	local extra = mw.loadData("Module:Data/Extra/Memento")[iname] or {}
	local releaseDates
	for _, region in ipairs{'jp', 'gl'} do
		local date = extra[region..'Date']
		if date then
			releaseDates = releaseDates or mw.html.create('ul')
			releaseDates:tag('li'):wikitext(region:upper(), ': ', date)
		end
	end
	
	local root = mw.html.create()
	-- Info Box
	local infobox = h.makeInfobox{
		name = name,
		icon = icon,
		labels = {
			'Type',
			'Rank',
			'Max level',
			'Enhancer cost',
			'Enhancer EXP',
			'Enhancer trust',
			'Vision Clear Reward',
			'Gives unit',
			'Source',
			'Release dates',
			'Global only',
			'External links',
		},
		rows = {
			['Type'] = typeMap[tonumber(data.type)],
			['Rank'] = (data.rare + 1)..'★',
			['Max level'] = maxLevels[data.rare + 1],
			['Enhancer cost'] = data.en_cost, -- zeni required to upgrade a memento with this memento
			['Enhancer EXP'] = data.en_exp, -- EXP given by this memento
			['Enhancer trust'] = data.en_trust,
			['Vision Clear Reward'] = h.getVCR(data),
			['Gives unit'] = h.getUnitReward(data),
			['Source'] = extra.source or 'Unreleased',
			['Release dates'] = releaseDates and tostring(releaseDates) or nil,
			['Global only'] = extra.glOnly and 'Yes' or nil,
			['External links'] = '[http://www.alchemistcodedb.com/card/'..iname:gsub('_','-'):lower()..' AlchemistCodeDB]',
		}
	}
	root:node(infobox)
	if tonumber(data.type) and typeMap[tonumber(data.type)] then
		cats[#cats+1] = typeMap[tonumber(data.type)] .. ' memento'
	end

	-- Body
	-- Card Image + Flavor
	root:tag('div'):addClass('responsive-img')
		:css{
			['margin'] = 'auto',
			['display'] = 'flex',
			['flex-direction'] = 'column',
			['min-width'] = '300px',
		}
		:tag('div')
			:wikitext('[[File:Game,ConceptCard,', data.icon, '.png|512px]]')
			:done()
		:tag('div'):addClass('flavor')
			:css{
				['max-width'] = '1024px',
				['padding'] = '8px',
			}
			:wikitext(data.flavor)
			:done()
			
	h.printLeaderSkill(root, data.leader_skill)

	-- TODO: Stats + Effects
	-- h.printEffects2(root, data.iname)
	h.printEffects(root, {
		rare = data.rare,
		lvmax = data.lvmax,
		effects = effects,
	})

	-- Category
	for i, cat in ipairs(cats) do
		root:wikitext('[[Category:', cat, ']]')
	end
	-- TODO Implement http://cdn.alchemistcodedb.com/assets/big-card-frames-b34ac394d6035fb657d491b956e2c4764836b51b9361fccc4b31e722a27df510.png
	
	return tostring(root)
end

local function renderTransclude(iname, args)
	local type = mw.text.trim(args[1])
	args[1] = iname
	if type == 'Icon' then
		args.data = cargo.query{
			tables = {'ConceptCard=CC', 'ConceptCardLoc=CCLoc', 'Pages'},
			join = {
				'CC.iname = CCLoc.iname',
				'CC.iname = Pages.iname',
			},
			fields = {
				'Pages._pageName = _pageName',
				'CC.iname = iname',
				'CC.server = server',
				'CC.rare = rare',
				'CC.icon = icon',
				'CCLoc.name = name',
			},
			where = {
				('"%s" IN (CC.iname, Pages._pageName)'):format(iname),
				('CC.server = "%s"'):format(server),
				('CCLoc.lang = "%s"'):format(lang),
				'Pages._pageName IS NULL OR Pages.type = "Memento"',
			},
		}[1]
		return render_memento_icon(args)
	end
	return (cargo.query{
			tables = 'ConceptCardLoc',
			fields = 'name',
			where = 'iname = "'..iname..'" AND lang = "english"',
		}[1] or {}).name
end

function p._main(iname, args)
	if not args or args.isPage then
		return p.renderPage(iname)
	else
		return renderTransclude(iname, args)
	end
end

function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {parentFirst = true})
	return p.renderPage(args[1])
end

function h.printLeaderSkill(root, sk_iname)
	if sk_iname == nil then return end
	local skill
	local rows = cargo.query{
		tables = {
			'Skill = SK',
			'SkillLoc = SK_LOC',
			'Buff = BF',
			'BuffDetail = BD',
			'Buff__custom_targets = BF_CT',
			'CustomTarget = CT',
			'CustomTarget__concept_card_groups = CT_CCG',
			'ConceptCardGroup = CCG',
			'ConceptCardGroup__cards = CCG_CC',
			'ConceptCard = CC',
			'Pages = CC_PAGES',
		},
		join = {
			'SK.iname = SK_LOC.iname',
			'SK.t_buff = BF.iname',
			'BF.iname = BD.buff_iname',
			'BF._ID = BF_CT._rowID, BF_CT._value = CT.iname',
			'CT._ID = CT_CCG._rowID, CT_CCG._value = CCG.iname',
			'CCG._ID = CCG_CC._rowID, CCG_CC._value = CC.iname',
			'CC.iname = CC_PAGES.iname',
		},
		fields = {
			'SK.iname = iname',
			'SK.type = type',
			'SK.eff_type = eff_type',
			'SK.target = target',
			'SK.cond = cond',
			'SK.timing = timing',
			'SK_LOC.name = name',
			'SK_LOC.expr = expr',

			'BF.iname = t_buff_iname',

			'BD.idx = BD_idx',
			'BD.calc = calc',
			'BD.tktag = tktag',
			'BD.type = type',
			'BD.vini = vini',
			'BD.vmax = vmax',
			'BD.vone = vone',

			'BF_CT._position = BF_CT_idx',
			'CT.birth_id = birth_id', -- Integer
			'CT.sex = sex', -- Integer
			'CT.dark = dark', -- Integer
			'CT.fire = fire', -- Integer
			'CT.shine = shine', -- Integer
			'CT.thunder = thunder', -- Integer
			'CT.water = water', -- Integer
			'CT.wind = wind', -- Integer
			-- 'CT.concept_card_groups = concept_card_groups', -- List of String
			'CT.job_groups = job_groups', -- List of String
			'CT.unit_groups = unit_groups', -- List of String
			'CT.units = units', -- List of String

			'CT_CCG._position = CT_CCG_idx',
			'CCG.iname = CCG_iname',
			
			'CCG_CC._position = CCG_CC_idx',
			'CC.iname = CC_iname',
			'CC.icon = CC_icon',
			'CC.type = CC_type',
			'CC.rare = CC_rare',
			'CC_PAGES._pageName = CC_PAGENAME',
		},
		where = {
			'SK.iname = "'..sk_iname..'"',
			'SK.server = "gl"',
			'SK_LOC.lang = "english"',
			'COALESCE(SK.t_buff, "") = "" OR BF.server = SK.server AND BD.server = SK.server',
			'COALESCE(BF.custom_targets__full, "") = "" OR CT.server = SK.server',

			'CCG.server = "gl"',
			'CC.server = "gl"',
		},
		orderBy = 'idx',
	}

	if #rows == 0 then return end
	for _, row in ipairs(rows) do
		skill = skill or {
			iname = row.iname,
			name = row.name,
			expr = row.expr,
			type = enums.ESkillType[tonumber(row.type)],
			eff_type = enums.SkillEffectTypes[tonumber(row.eff_type)],
			target = enums.ESkillTarget[tonumber(row.target)],
			cond = enums.ESkillCondition[tonumber(row.cond)],
			timing = enums.ESkillTiming[tonumber(row.timing)],
			t_buff = {
				iname = row.t_buff_iname,
				custom_targets = {},
				details = {},
			},
		}
		setdefault(skill.t_buff.details, tonumber(row.BD_idx), {
			calc = row.calc,
			tktag = row.tktag,
			type = tonumber(row.type),
			vini = tonumber(row.vini),
			vmax = tonumber(row.vmax),
			vone = tonumber(row.vone),
		})
		local custom_target = setdefault(skill.t_buff.custom_targets, tonumber(row.BF_CT_idx), {
			birth_id = tonumber(row.birth_id),
			sex = tonumber(row.sex),
			dark = tonumber(row.dark),
			fire = tonumber(row.fire),
			shine = tonumber(row.shine),
			thunder = tonumber(row.thunder),
			water = tonumber(row.water),
			wind = tonumber(row.wind),
			concept_card_groups = {},
			job_groups = row.job_groups,
			unit_groups = row.unit_groups,
			units = row.units,
		})
		local concept_card_group = setdefault(custom_target.concept_card_groups, tonumber(row.CT_CCG_idx), {
			iname = row.CCG_iname,
			concept_cards = {},
		})
		local concept_card = setdefault(concept_card_group.concept_cards, tonumber(row.CCG_CC_idx), {
			iname = row.CC_iname,
			icon = row.CC_icon,
			type = row.CC_type,
			rare = row.CC_rare,
			PAGENAME = row.CC_PAGENAME,
		})
	end
	root:newline():tag('h2'):wikitext("Leader Skill")
	root:newline():tag('h3'):wikitext(skill.name)
	root:newline():tag('p'):wikitext(skill.expr)

	if #skill.t_buff.details > 0 then
		local tbl = root:newline():tag('table'):addClass('wikitable')
		tbl:newline():tag('caption'):wikitext('Target Buff ('..skill.target..') ('..skill.cond..')')
		tbl:newline():tag('tr'):tag('th'):attr('colspan', 3):wikitext('Stats')
		tbl:newline():tag('tr')
			:tag('th'):wikitext('Type'):done()
			:tag('th'):wikitext('Min'):done()
			:tag('th'):wikitext('Max'):done()
		for i, buff in ipairs(skill.t_buff.details) do
			local tr = tbl:newline():tag('tr')
			local fmt = yesno(buff.calc) and '%+d%%' or '%+d'
			local statName = enums.statNameFromType(buff.type, buff.tktag)
			tr:tag('th'):wikitext(statName)
			tr:tag('td'):wikitext(string.format(fmt, tonumber(buff.vini)))
			tr:tag('td'):wikitext(string.format(fmt, tonumber(buff.vmax)))
			if buff.tktag then
				cats[#cats+1] = statName
			end
		end
	end
	for _, custom_target in ipairs(skill.t_buff.custom_targets or {}) do
		for _, cc_group in ipairs(custom_target.concept_card_groups or {}) do
			local cards = cc_group.concept_cards
			root:newline():tag('h4'):wikitext('Memento Group')
			local ul = root:newline():tag('ul')
			for _, card in ipairs(cards) do
				-- local icon = ''
				if card.PAGENAME then
					local icon = string.format(
						'[[File:Game,ConceptCardIcon,%s.png|32px|link=%s]] [[%s]]',
						card.icon, card.PAGENAME, card.PAGENAME)
					ul:newline():tag('li'):wikitext(icon)
				end
			end
		end
	end
end

function p.test(frame)
	util_vars.setVar('cargo query count', 0)
	-- local root = mw.html.create()
	-- h.printLeaderSkill(root, 'SK_LS_TS_ENVYRIA_CLOE_01')
	-- return
	local x
	-- x = p.renderPage('TS_SAGA_BIRGITTA_01')
	x = p.renderPage('TS_GL_SIBLINGS_01')
	mw.log(util_vars.getVar('cargo query count'))
	--return x
end

p.h = h -- for testing purposes >_<

return p
--</nowiki>