Module:Sandbox/Sorbasi

From Monumenta Wiki
Jump to navigation Jump to search

Intended for use by #invoke'ing inside Template:Item.

Automatic generation of standard parts of an Item page based on api data.

Defaults to the invoking page's Title as the item name, which can be overridden with the named parameter `source` in either the invoking template or the direct invocation.

Exposed functions are:

`ItemPage` - Attempts to do everything. Has the additional params skip_lore, skip_desc, manual_lore, manual_desc, manual_navbox, obtaining, usage, skins, trivia, see_also.

`InfoBox` - Populates and invokes Template:Item/InfoBox using json data looked up based on api name.

`ItemPageCategories` - Generates page categories for an item page. If passed the named parameter `test` outputs a comma separated list instead of category links. If passed the named parameters `source` or `source2` will override/merge item values. Will automatically add effects from the highest masterwork level of an item when it differs, if source2 is not supplied.

`ItemPageIntro` - Outputs the combined results of `getLoreStyled` and/or `getItemDesc`

`getLore` - Returns the item's lore, if any, unformatted.

`getLoreStyled` - Returns the result of Template:ItemLore with the item's lore, if any.

`getItemDesc` - Returns the result of Template:ItemDesc populate with the item's api data. Supports overrides via the named parameters `desc_name`/`desc_plural`/ `desc_proper`.

Depends on item stats converting data at [[./Tables]], and conditionally upon consumable effects data at [[./Effects]] and masterworking cost data at [[./Masterworking]]

Pulls data from itemized pages living under Monumenta_Wiki:API/Items. These are intended to be parsed and uploaded automatically by a bot from the official api json.


local p = {}

local DATA_BASE = 'Monumenta_Wiki:API/Items/'
local MAX_MASTERWORK = 4
local tables = mw.loadData('Module:Sandbox/Sorbasi/tables')

local data

local effects
local mwdata

function maybeLoadEffects()
	if not effects then
		effects = mw.loadData('Module:Sandbox/Sorbasi/Effects')
	end
end
function maybeLoadMasterworking()
	if not mwdata then
		mwdata = mw.loadData('Module:Sandbox/Sorbasi/Masterworking')
	end
end

function is_singleton_enchantment(v)
	if tables.enchantment_singletons[string.lower(v)] then
		return true
	end
	return false
end

function loadSourceByItemName(n)
	data = mw.loadJsonData(DATA_BASE .. n)	
end

function loadSourceByFrameContent(frame)
	local frameParent = frame:getParent()
	loadSourceByItemName(frame.args.source or frameParent.args.source or frameParent:getTitle())
end

function p.getLore(frame)
	local frameParent = frame:getParent()
	loadSourceByFrameContent(frame)
	return data.lore
end

function p.getLoreStyled(frame)
	local lore = p.getLore(frame)
	return lore and "<p>" .. frame:expandTemplate{ title = 'ItemLore', args = {lore} } .. "</p>" or ""
end

function p.getItemDesc(frame)
	local frameParent = frame:getParent()
	loadSourceByFrameContent(frame)
	local source2 = frame.args.source2 or frameParent.args.source2 or nil
	local nameOverride = frame.args.desc_name or frameParent.args.desc_name or data.name or nil
	local plural = frame.args.desc_plural or frameParent.args.desc_plural or nil
	local proper = frame.args.desc_proper or frameParent.args.desc_proper or nil
	local typeOverride = data.type
	local wand = false
	local typeSecond = nil
	local articleSecond = "a"
	
	if source2 then
		local dd = mw.loadJsonData(DATA_BASE .. source2)
		typeSecond = dd.type
		if string.sub(typeSecond, 1, 1) == "O" then articleSecond = "an" end
		if typeSecond == "Wand" then
			wand = true
			if dd.base_item == "Shield" then
				typeSecond = "Mainhand Shield"
			elseif string.find(dd.base_item, "Sword", 1, true) then
				typeSecond = "Mainhand Sword"
			elseif string.find(dd.base_item, "Axe", 1, true) then
				typeSecond = "Axe"
			end
		end
	end
	
	if typeOverride == "Wand" then
		wand = true
		if data.base_item == "Shield" then
			typeOverride = "Mainhand Shield"
		elseif string.find(data.base_item, "Sword", 1, true) then
			typeOverride = "Mainhand Sword"
		elseif string.find(data.base_item, "Axe", 1, true) then
			typeOverride = "Axe"
		end
	end
	--{{ItemDesc|item_type|tier|location|name|plural|proper_noun}}
	local desc = frame:expandTemplate{ title = 'ItemDesc', args = {typeOverride or nil, data.tier or nil, data.region or nil, nameOverride or nil, plural or nil, proper or "true"} }
	if wand and source2 and typeSecond ~= typeOverride then
		return desc .. " It can also function as ".. articleSecond .." [[" .. typeSecond .. "s|" .. string.lower(typeSecond) .. "]] and a [[Wands|wand]]."
	elseif source2 and typeSecond ~= typeOverride then
		return desc .. " It can also function as ".. articleSecond .." [[" .. typeSecond .. "s|" .. string.lower(typeSecond) .. "]]."
	elseif wand and typeOverride ~= "Wand" then
		return desc .. " It can also function as a [[Wands|wand]]."
	else
		return desc
	end
end

-- this uppercasing logic is premised on stackoverflow answers at https://stackoverflow.com/questions/2421695/first-character-uppercase-lua
function statToNiceName(s)
	local t = {}
	for ss in string.gmatch(s, "([^_ ]+)") do
		-- gsub actually returns an expanded value list, so parens pretend only the first value exists
		table.insert(t, (string.gsub(ss, "^%l", string.upper)))
	end
	return table.concat(t, " ")
end

function numToSigned(n)
	if n > 0 then return "+" .. tostring(n) end
	return tostring(n)
end

function numToSignedPercent(n)
	if n < 0 then
		return tostring(n * 100) .. "%"
	else
		return "+" .. tostring(n * 100) .. "%"
	end
end

function romanNumeral(n, frame)
	return frame:expandTemplate{ title = 'RomanNumeral', args = {n} }
end

-- this function is evidence of the damage writing lua has done to my mind
function ticksToHuman(x)
	if not x or x == 1 then
		return ""
	elseif x == -1 then
		return "∞"
	else
		local seconds = x / 20
		local minutes = math.floor(seconds / 60)
		local hours = tostring(math.floor(minutes / 60))
		seconds = string.format("%02d", seconds % 60)
		if hours ~= "0" then
			return hours .. ":" .. string.format("%02d", minutes) .. ":" .. seconds
		elseif minutes then
			return tostring(minutes) .. ":" .. seconds
		end
	end
end

function getName()
	return tostring(data.name) or tostring(data.base_item) or 'Unknown Item'
end

function getBaseItem()
	return tostring(data.base_item) or ''
end

--assuming this and tier and region lookups remain needed, should probably split into helper modules for readability and simplified maintenance
function locationNameFromShort(r)
	return tables.locationShortToLong[string.lower(r)] or r
end

function regionNameFromShort(r)
	return tables.regionShortToLong[string.lower(r)] or "Unknown Region"
end

function getRegion()
	return regionNameFromShort(tostring(data.region) or '')
end

function getTier()
	return tostring(data.tier) or 'Unknown'
end

--api gives dummy item groups for items that dont actually have one
function getLocation()
	return data.location and data.location ~= '' and not tables.regionLongToShort[data.location] and tostring(data.location) or nil
end

-- these are currently unsafe. need to validate for html insertion
function getTierStyled()
	local tier = getTier()
	return '<span class="tier_' .. string.lower(tier) .. '">' .. tier .. '</span>'
end

function getLocationStyled()
	local loc = getLocation()
	return '<span class="loc_' .. string.lower(loc) .. '">' .. locationNameFromShort(loc) .. '</span>'
end

function getSlotFromType(t)
	return tostring(tables.equipSlotFromType[t or data.type]) or 'Misc'
end

function getWearString(t)
	return tostring(tables.usageStringFromSlot[getSlotFromType(t or nil)]) or "When properly contemplated:"
end

function p.ItemPageIntro(frame)
	return p.getLoreStyled(frame) .. "<p>" .. p.getItemDesc(frame) .. "</p>"
end

function p.ItemPageCategories(frame)
	local frameParent = frame:getParent()
	if frameParent.args.catname then loadSourceByItemName(frameParent.args.catname)
	else loadSourceByFrameContent(frame) end
	local c = {}
	table.insert(c, "Items")

	local source2 = frame.args.source2 or frameParent.args.source2 or nil
	local source2_type = nil
	local source2_consumable = nil
	local source2_wand = nil
	-- this specifically breaks on its own for something like truest north where the base rank doesnt have a disambig but later ranks do
	if source2 == nil and data.masterwork and data.masterwork ~= MAX_MASTERWORK then source2 = data.name .. "-" .. tostring(MAX_MASTERWORK) end

	-- we accidentally shadowed the global effects table here but it doesnt matter since it's not directly used in this function
	local effects = nil
	local stats = nil

	if data.effects then
		effects = {}
		for k,v in pairs(data.effects) do effects[getEffectName(v)] = true end
	end
	if data.stats then
		stats = {}
		for k,v in pairs(data.stats) do stats[k] = true end
	end

	if source2 then
		local dd = mw.loadJsonData(DATA_BASE .. source2)
		source2_type = dd.type
		if source2_type == "Wand" then
			source2_wand = true
			if string.find(dd.base_item, "sword") then source2_type = "Mainhand Sword"
			elseif string.find(dd.base_item, "shield") then source2_type = "Mainhand Shield"
			elseif string.find(dd.base_item, "axe") then source2_type = "Axe"
			end
		end
		if dd.effects then
			source2_consumable = true
			if effects == nil then effects = {} end
			for k,v in pairs(dd.effects) do effects[getEffectName(v)] = true end
		end
		if dd.stats then
			if stats == nil then stats = {} end
			for k,v in pairs(dd.stats) do stats[k] = true end
		end
	end

	if data.type == "Consumable" or effects or source2_consumable then table.insert(c, "Consumable Items") end
	if effects then
		for k,v in pairs(effects) do
			table.insert(c, "Consumable Items with " .. k)
		end
	end
	if data.class_name then table.insert(c, tostring(data.class_name) .. " Charms") end
	if getLocation() then table.insert(c, locationNameFromShort(getLocation()) .. " Items") end
	if data.power then table.insert(c, tostring(data.power) .. " Power Charms") end
	if data.region then
		table.insert(c, getRegion() .. " Items")
		if data.tier then
			table.insert(c, getRegion() .. " " .. getTier() .. " Items")
		end
		local slot = getSlotFromType()
		local base = string.lower(tostring(data.base_item))
		local special = ""
		if data.type == "Wand" then
			if string.find(base, "sword") then special = "Sword"
			elseif string.find(base, "shield") then special = "Shield"
			elseif string.find(base, "axe") then special = "Axe"
			end
		end
		if effects and data.type ~= "Consumable" then
			table.insert(c, getRegion() .. " Consumable Items")
		end

		table.insert(c, getRegion() .. " " .. tostring(data.type) .. " Items")
		if data.type ~= "Wand" and source2_wand then
			table.insert(c, getRegion() .. " Wand Items")
		end
		if data.type ~= "Charm" and (special ~= "" or source2_type) then
			if special == "Axe" or source2_type == "Axe" then table.insert(c, getRegion() .. " Axe Items") end
			if special ~= "Axe" then table.insert(c, getRegion() .. " " .. slot .. " " .. special .. " Items") end
			if source2_type and source2_type ~= "Axe" then table.insert(c, getRegion() .. " " .. source2_type .. " Items") end
		end
	end

	if data.type ~= "Charm" then
		for k,v in pairs(stats) do
			local p = string.find(k, "_flat") or string.find(k, "_percent") or string.find(k, "_base")
			local name = string.sub(k, 1, (p or #k + 1) - 1)
			local op = string.sub(k, (p or #k) + 1)

			if op ~= "base" then
				table.insert(c, "Items with " .. statToNiceName(name))
			elseif name == "throw_rate" then
				table.insert(c, "Thrown Items")
			end
		end
	end

	if frame.args.test or frameParent.args.test then return table.concat(c, ",")
	else return "[[Category:" .. table.concat(c, "]][[Category:") .. "]]" end
end

function getEffectName(v)
	maybeLoadEffects()
	local effectFormat = effects.effects[string.lower(v.EffectType)] or {l=v.EffectType, t=""}
	local name = effectFormat.l
	return name
end

function getEffectDisplay(v, frame)
	maybeLoadEffects()
	local effectFormat = effects.effects[string.lower(v.EffectType)] or {l=v.EffectType, t=""}
	local nameLink = effectFormat.l
	local nameDisplay = nameLink
	local style = nil
	local strength = tonumber(v.EffectStrength)
	local strengthDisplay = ""
	local durationDisplay = ticksToHuman(v.EffectDuration)
	
	if durationDisplay then
		durationDisplay = " (" .. durationDisplay .. ")"
	end
	
	if effects.mirror[string.lower(v.EffectType)] then
		strength = strength * -1
		nameLink = effectFormat.l
		nameDisplay = effects.mirror[string.lower(v.EffectType)]
		style = "<span class=\"malus\" style=\"color:#b94e48;\">"
	else
		nameDisplay = effectFormat.l
	end
	
	if effectFormat.t == "single" then
		--strengthDisplay = ""
	elseif effectFormat.t == "percent" or (tonumber(v.EffectStrength) ~= math.floor(tonumber(v.EffectStrength)) and (effectFormat.t == "" or not effectFormat.t)) then
		strengthDisplay = numToSignedPercent(strength) .. " "
	else 
		--strengthDisplay = ""
		nameDisplay = nameDisplay .. " " .. romanNumeral(v.EffectStrength, frame)
	end
	return (style or "") .. strengthDisplay .. "[[" .. nameLink .. "|" .. nameDisplay .. "]]" .. durationDisplay .. (style and "</span>" or "")
end

function InfoBoxEffects(frame)
	if not data.effects then
		return nil
	end
	local result = {}
	for k,v in pairs(data.effects) do
		table.insert(result, getEffectDisplay(v, frame))
	end
	return #table and table.concat(result, "<br>") or nil;
end

function InfoBoxRegion()
	-- awkwardly complex preprocessing for the "region" block per ingame
	-- misleading name describes way more than the region 
	local result = {}
	if data.tier then
		table.insert(result, getRegion() .. ' : ' .. getTierStyled())
	end
	if tostring(data.type) == "Charm" then
		table.insert(result, "Charm Power : " .. string.rep("★", data.power) .. " " .. tostring(data.class_name) ) 
	end
	if data.masterwork then
		table.insert(result, "Masterwork : " .. mwstars(data.masterwork))
	end
	if getLocation() then
		table.insert(result, getLocationStyled())
	end
	return table.concat(result, "<br>\n")
end

-- key, value, positionOfOperandSplit
function DescribeAttribute(k, v, p)
	local name = string.sub(k, 1, p - 1)
	local operand = string.sub(k, p + 1)
	local nicename = statToNiceName(name)
	if operand == "percent" then
		return numToSigned(v) .. "% " .. nicename
	elseif operand == "base" then
		if k == "spell_power_base" then
			return tostring(v) .. "% " .. nicename
		else
			return tostring(v) .. " " .. nicename
		end
	else
		return numToSigned(v) .. " " .. nicename
	end
end

--unsmush into cursed duplicate code for items that have multiple valid slots
function SecondaryAttributes(name)
	local dd = mw.loadJsonData(DATA_BASE .. name)
	-- heckin truest north
	if dd.type == data.type then return "" end
	-- *inhales*
	local description = {}
	table.insert(description, getWearString(dd.type))
	local actuallyHasAttributes = false
	for k,v in pairs(dd.stats) do
		-- can this be a single find? first attempt to silently failed
		local is_attribute = string.find(k, "_flat") or string.find(k, "_percent") or string.find(k, "_base")
		if k == "armor" or k == "agility" then 
			is_attribute = #k + 1
		end
		
		if is_attribute ~= nil then
			actuallyHasAttributes = true
			table.insert(description, DescribeAttribute(k, v, is_attribute))
		end
	end
	return table.concat(description, '<br>')
end

--smush these into a single for loop
function InfoBoxStats(frame)
	local frameParent = frame:getParent()
	local describeEnchantments = {}
	local describeAttributes = {}
	local secondary = ""
	if frame.args.source2 or frameParent.args.source2 then
		secondary = SecondaryAttributes(frame.args.source2 or frameParent.args.source2)
	end
	-- why are lua tables so jank
	local actuallyHasAttributes = false
	table.insert(describeAttributes, getWearString())
	for k,v in pairs(data.stats) do
		-- can this be a single find? first attempt to silently failed
		local is_attribute = string.find(k, "_flat") or string.find(k, "_percent") or string.find(k, "_base")
		if k == "armor" or k == "agility" then 
			is_attribute = #k + 1
		end
		if is_attribute == nil then
			local n = statToNiceName(k)
			if is_singleton_enchantment(n) then
				table.insert(describeEnchantments, "[[" .. n .. "]]")
			else
				table.insert(describeEnchantments, "[[" .. n .. "|" .. n .. " " .. romanNumeral(v, frame) .. "]]")
			end
		else
			actuallyHasAttributes = true
			table.insert(describeAttributes, DescribeAttribute(k, v, is_attribute))	
		end
	end
	
	local attr = ""
	if actuallyHasAttributes and #secondary > 1 then
		attr = table.concat(describeAttributes, '<br>') .. "<br>" .. secondary
	elseif actuallyHasAttributes then
		attr = table.concat(describeAttributes, '<br>')
	elseif #secondary > 1 then
		attr = secondary
	end
	return table.concat(describeEnchantments, '<br>'), attr
end

function mwcost(frame, t, i)
	maybeLoadMasterworking()
	-- why was unpack only giving us nil values it had one job
	local location = mwdata.locations[string.lower(t)] or {}
	local ctype, mat, anim, abbrev = location[1], location[2], location[3], location[4]
	ctype = ctype or t
	local tier = (mwdata.costs[ctype] and mwdata.costs[ctype][i]) or {}
	local mats, har, pdia = tier[1], tier[2], tier[3]
	if frame.args.test2 then
		return tostring(mats) .. " " .. (mat or "nil") .. " + " ..  tostring(har) .. " HAR" .. ((pdia and " + " .. tostring(pdia) .. " P. Dia") or "") 
	end
	return tostring(mats) .. " " .. frame:expandTemplate{ title = 'Mini', args = {mat, anim or nil, abbrev or nil} } .. " + " .. tostring(har) .. " " .. frame:expandTemplate{ title = 'Mini', args = {"Hyperchromatic Archos Ring", nil, "HAR"} } .. (pdia and (" + " .. tostring(pdia) .. " " .. frame:expandTemplate{ title = 'Mini', args = {"Pulsating Diamond", nil, "P. Dia"} }) or "")
end

function mwstars(x)
	-- TODO remove explicit color when wiki stylesheets get values
	return "<span class=\"mwbright\" style=\"color:#EF9E23;\">" .. string.rep("★", x) .. "</span><span class=\"mwdull\" style=\"color:#7A7A7A;\">" .. string.rep("☆", MAX_MASTERWORK - x) .. "</span>"
end

--this can be made inline as seen elsewhere in the script i just didnt know how at the time
--use pcall to suppress load errors so we dont need to be explicitly told anything
--if this causes issues rework to have an explicit %start% arg
--https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#pcall
function pcalledLoadItem(name)
	return (mw.loadJsonData(DATA_BASE .. name))
end

-- send help the code keeps duplicating
function mwnumberformatter(k, v)
	local prefix = ""
	local suffix = ""
	local singleton = is_singleton_enchantment(k)
	local value = (is_singleton_enchantment(k) and v > 0 and "✓") or (v ~= 0 and tostring(v)) or "✗"
	
	if k == "armor" or k == "agility" or string.find(k, "_flat") or string.find(k, "_percent") then
		if v > 0 then prefix = "+"
		else prefix = "-" end
	end
	if k == "spell_power_base" or string.find(k, "_percent") then
		suffix = "%"
	end
	
	return prefix .. value .. suffix
end

function p.MWTable(frame)
	maybeLoadMasterworking()
	local frameParent = frame:getParent()
	loadSourceByFrameContent(frame)
	local source = frame.args.source or frameParent.args.source or frameParent:getTitle()
	local name = data.name
	local disambig = frame.args.mwdisambig or frameParent.args.mwdisambig or nil
	if disambig == nil and source ~= name .. "-" .. data.masterwork then
		disambig = string.sub(source, string.len(name .. "-" .. data.masterwork) + 1)
	end
	if disambig and string.sub(disambig, 1, 1) ~= " " then disambig = " " .. disambig end
	--tfw mandatory overrides
	--this set should only be needed for items with nonstandard cost progressions
	--like truest north or the miniquest things
	local costs = {
		frame.args.mwc0 or frameParent.args.mwc0 or nil,
		frame.args.mwc1 or frameParent.args.mwc1 or nil,
		frame.args.mwc2 or frameParent.args.mwc2 or nil,
		frame.args.mwc3 or frameParent.args.mwc3 or nil,
		frame.args.mwc4 or frameParent.args.mwc4 or nil,
		frame.args.mwc5 or frameParent.args.mwc5 or nil,
		frame.args.mwc6 or frameParent.args.mwc6 or nil,
		frame.args.mwc7 or frameParent.args.mwc7 or nil,
	}
	--ideally this set never gets used
	--but it does because redirects cant spot fix for multitools being wack
	local names = {
		frame.args.mwn0 or frameParent.args.mwn0 or nil,
		frame.args.mwn1 or frameParent.args.mwn1 or nil,
		frame.args.mwn2 or frameParent.args.mwn2 or nil,
		frame.args.mwn3 or frameParent.args.mwn3 or nil,
		frame.args.mwn4 or frameParent.args.mwn4 or nil,
		frame.args.mwn5 or frameParent.args.mwn5 or nil,
		frame.args.mwn6 or frameParent.args.mwn6 or nil,
		frame.args.mwn7 or frameParent.args.mwn7 or nil,
	}

	local result = {}
	-- mw level / .. stats .. / cost
	local table_width = 2
	local stat_cols = {}
	local dd = {}
	local base_mw = nil
	local location = ""

	if frame.args.test3 then
		local t = {}
		for i=0,MAX_MASTERWORK do t[i] = names[i+1] or (name .. "-" .. tostring(i) .. (disambig or "")) end
		return mw.text.jsonEncode(t, mw.text.JSON_PRETTY)
	end
	--awkward that lua array functions assume base index 1
	--because masterworks, like sane arrays, are base index 0
	for i=0,MAX_MASTERWORK do
		local success, d = pcall(pcalledLoadItem, names[i+1] or (name .. "-" .. tostring(i) .. (disambig or "")))
		dd[i] = success and d and d.stats or false
		if base_mw == nil and success then 
			base_mw = i 
			location = d.location or ""
		end
	end
	
	-- the line of confused debugging
	if frame.args.test4 then return mw.text.jsonEncode(dd, mw.text.JSON_PRETTY) end
	
	table.insert(result, "{| class=\"wikitable\"\n!Masterwork Level")
	--something to guarantee we're outputting each row's stats in the same order
	--originally was just the order of stats in the highest mw version's api entry
	--greenfrog asked for enchants before attributes

	--split bins to clump columns with
	local bin_ench = {}
	local bin_attr = {}
	if dd[MAX_MASTERWORK] then 
		for k,v in pairs(dd[MAX_MASTERWORK]) do
			if not mwdata.hidden[k] then
				if k == "armor" or k == "agility" or string.find(k, "_flat") or string.find(k, "_percent") or string.find(k, "_base") then
					bin_attr[#bin_attr + 1] = k
				else 
					bin_ench[#bin_ench + 1] = k
				end
			end
		end
		
		--awkward reversed pair naming and duplicate loops
		for v,k in pairs(bin_ench) do
			stat_cols[table_width] = k
			table_width = table_width + 1
			table.insert(result, "!"..(mwdata.headings[k] or statToNiceName(k)))
		end
		for v,k in pairs(bin_attr) do
			stat_cols[table_width] = k
			table_width = table_width + 1
			table.insert(result, "!"..(mwdata.headings[k] or statToNiceName(k)))
		end
		
		table.insert(result, "!Cost")
	end

	for i=0,MAX_MASTERWORK do
		table.insert(result, "|-\n|"..mwstars(i))
		if dd[i] then
			for k=2,table_width-1 do
				local v = dd[i][stat_cols[k]] or 0
				local dv = nil
				local dvp = ""
				
				if i ~= base_mw then
					dv = v - (dd[i - 1][stat_cols[k]] or 0)
					dv = dv > 0 and mwnumberformatter(stat_cols[k], dv) or nil
					if dv and string.sub(dv, 1, 1) ~= "+" and string.sub(dv, 1, 1) ~= "-" then dvp = "+" end
				end
				v = mwnumberformatter(stat_cols[k], v)
				
				if dv then 
					table.insert(result, "| " .. v .. " <span class=\"mwup\" style=\"color:#e6c100;\">(" .. dvp .. dv .. ")</span>")
				else 
					table.insert(result, "| " .. v) 
				end
			end
			if i == base_mw then table.insert(result, "|Base Masterwork Level") else
				table.insert(result, "|" .. (frame.args.test2 and (tostring(i) .. location) or "") .. (costs[i+1] or mwcost(frame, location, i) or "Unspecified Cost"))
			end
		end
	end
	table.insert(result, "|}")
	return table.concat(result, "\n")
end

-- hopefully this is an improvement from invoking per infobox input
function p.InfoBox(frame)
	loadSourceByFrameContent(frame)
	local enchantments, attributes = InfoBoxStats(frame)
	return frame:expandTemplate{ title = 'Item/InfoBox', args = {
		name = getName(),
		image = frame:getParent().args.image or "ItemTexture" .. getName() .. ".png",
		type = getBaseItem(),
		region = InfoBoxRegion(),
		effects = InfoBoxEffects(frame),
		enchantments = enchantments,
		attributes = attributes
	} }	
end

function p.ItemPage(frame)
	loadSourceByFrameContent(frame)
	local frameParent = frame:getParent()
	local result = {}
	local skip_lore = frame.args.skip_lore or frameParent.args.skip_lore or nil
	local skip_desc = frame.args.skip_desc or frameParent.args.skip_desc or nil
	local manual_lore = frame.args.manual_lore or frameParent.args.manual_lore or nil
	local manual_desc = frame.args.manual_desc or frameParent.args.manual_desc or nil
	local manual_navbox = frame.args.manual_navbox or frameParent.args.manual_navbox or nil
	local obtaining = frame.args.obtaining or frameParent.args.obtaining or nil
	local usage = frame.args.usage or frameParent.args.usage or nil
	local skins = frame.args.skins or frameParent.args.skins or nil
	local trivia = frame.args.trivia or frameParent.args.trivia or nil
	local see_also = frame.args.see_also or frameParent.args.see_also or nil
	local exalted = (string.find(frameParent:getTitle(), "EX ", 1, true) and data.region == "Ring" and true) or false
	
	table.insert(result, p.ItemPageCategories(frame))
	table.insert(result, p.InfoBox(frame))
	if not (skip_lore or skip_desc or manual_lore or manual_desc) then 
		table.insert(result, p.ItemPageIntro(frame))
	else
		if manual_lore then table.insert(result, "<p>"..manual_lore.."</p>")
		elseif not skip_lore then table.insert(result, p.getLoreStyled(frame)) end
		
		if manual_desc then table.insert(result, "<p>"..manual_desc.."</p>")
		elseif not skip_desc then table.insert(result, p.getItemDesc(frame)) end
	end
	
	table.insert(result, "== Obtainment Methods ==")
	if obtaining then table.insert(result, obtaining) 
	else 
		table.insert(result, "* Might drop in the related content.")
		table.insert(result, "* Can probably be purchased from an associated [[Rare Trader - "..locationNameFromShort(getLocation()).."|Rare Trader]] after first clear of the related content.")
	end
	
	if usage then
		table.insert(result, "== Usage == ")
		table.insert(result, usage)	
	end
	
	if data.masterwork and tonumber(data.masterwork) <= MAX_MASTERWORK then 
		table.insert(result, "== Masterworking Details ==")
		table.insert(result, "Like many other items in the Architect's Ring, this item can be [[Masterwork|masterworked]] to increase its power level further. The below table shows its stats at each Masterwork level, and what is needed to reach that level. Levels with no statistics shown are levels which cannot be obtained on the given item.")
		table.insert(result, p.MWTable(frame)) 
	end
	
	if trivia then 
		table.insert(result, "== Tips and Trivia ==")
		table.insert(result, trivia)
	end
	
	if skins then
		table.insert(result, "== Skins ==")
		table.insert(result, skins)	
	end
	
	if see_also or exalted then
		table.insert(result, "== See Also ==")
		table.insert(result, see_also)
		if exalted then
			table.insert(result, "* The [["..getName().."|base version]] of this item, sourced from the [[King's Valley]]")
		end
	end
	
	if manual_navbox then 
		table.insert(result, manual_navbox)
	elseif getLocation() then
		pcall(function () table.insert(result, frame:expandTemplate{title = (exalted and "ItemNavboxDngnExalted")  or ("ItemNavbox" .. getLocation()), args = {}}) end )
	end
	
	return table.concat(result, "\n")
end

return p