Module:Sandbox/SMWTable

From Final Fantasy XIV Online Wiki
Jump to navigation Jump to search
Documentation for Module:Sandbox/SMWTable does not exist yet [create] (How does this work?)
local Icon = require("Module:Icon")
local yesno = require("Module:Yesno")

---@class SMWTableColumn A column that displays property values of Semantic Mediawiki entities.
---@field label string Header label for the column
---@field printouts {[string]: string} Map of property names to printout options used for the property; pagename property is keyed by empty string
---@field render fun(entity: table): string Function that renders the column's value for each queried entity

---@class SMWTable A table of Semantic Mediawiki entities, composed of multiple columns.
---@field selection string SMW query selection string
---@field printouts {[string]: string} Map of property names to printout options used for the property; pagename property is keyed by empty string
---@field columnLabels string[] Array of column header labels
---@field columnRenderers (fun(entity: table): string)[] Array of column render functions in the order they will appear
---@field options table Additional SMW query options for e.g. sorting etc
local SMWTable = {}
SMWTable.__index = SMWTable

---@param selection string Query selection for this table
---@return SMWTable
function SMWTable.new(selection, options)
	---@type SMWTable
	local instance = {
		-- from constructor args
		selection = selection,
		options = options or {},
		-- populated by :column()
		printouts = {},
		columnLabels = {},
		columnRenderers = {},
	}
	setmetatable(instance, SMWTable)
	return instance
end

-- Methods

---@param column SMWTableColumn Column definition
---@return self
function SMWTable:column(column)
	-- apply printouts required for column
	for propertyName, formatString in pairs(column.printouts) do
		local existingFormat = self.printouts[propertyName]
		if existingFormat then
			if existingFormat ~= formatString then
				error("Two columns requested different formats for property " .. propertyName .. ": \"" .. existingFormat .. "\", \"" .. formatString .. "\"")
			end
		else
			self.printouts[propertyName] = formatString
		end
	end

	table.insert(self.columnLabels, column.label)
	table.insert(self.columnRenderers, column.render)

	return self
end

---@param self SMWTable
---@return string
function SMWTable:__tostring()
	local query = {self.selection}
	for propertyName, formatString in pairs(self.printouts) do
		table.insert(query, "?" .. propertyName .. formatString)
	end

	for key, value in pairs(self.options) do
		query[key] = value
	end

	-- Execute query
	local results = mw.smw.ask(query)
	if not results or #results == 0 then
		return "No results"
	end

	-- Render table

	local out = "{| class=\"table\"\n"
	-- Render table header
	for _, label in ipairs(self.columnLabels) do
		out = out .. "! " .. label .. "\n"
	end

	-- render rows
	for _, item in ipairs(results) do
		out = out .. "|-\n"
		for _, render in ipairs(self.columnRenderers) do
			out = out .. "|" .. tostring(render(item)) .. "\n"
		end
	end

	out = out .. "|}"

	return out
end

-- Static methods - column helpers

---@param options string | {property: string, label?: string}
---@return SMWTableColumn
function SMWTable.textColumn(options)
	if type(options) == "string" then
		options = {property = options}
	end
	return {
		label = options.label or options.property,
		printouts = {
			[options.property] = "",
		},
		render = function (item)
			local value = item[options.property]
			if type(value) == "table" then
				return table.concat(value, ", ")
			end
			return tostring(item[options.property])
		end
	}
end

---@param options {name?: string, iconSize?: ("small" | "mid" | "big"), allowWrap?: boolean?}
---@return SMWTableColumn
function SMWTable.nameAndIconColumn(options)
	options = options or {}
	return {
		label = "Name",
		printouts = {
			[""] = "=Pagename#",
			["Has context"] = "",
			["Has game icon"] = "#",
			["Has canonical name"] = "",
			["Has dye channels"] = "",
			["Has game icon frame type"] = "",
		},
		render = function(item)
			return Icon.gameIconWithLabel({
				type = item['Has context'],
				frameType = item['Has game icon frame type'],
				icon = item['Has game icon'],
				link = item['Pagename'],
				size = options.iconSize or "big",
				dyeCount = item['Has dye channels'],
				text = item['Has canonical name'] or item['Pagename'],
				allowWrap = options.allowWrap or false
			})
		end
	}
end

local classJobAbbreviations = {
	-- DoH/DoL
	alchemist = "ALC",
	armorer = "ARM",
	blacksmith = "BSM",
	botanist = "BTN",
	carpenter = "CRP",
	culinarian = "CUL",
	fisher = "FSH",
	goldsmith = "GSM",
	leatherworker = "LTW",
	miner = "MIN",
	weaver = "WVR",
	-- Combat classes
	marauder = "MRD",
	gladiator = "GLA",
	pugilist = "PGL",
	lancer = "LNC",
	archer = "ARC",
	conjurer = "CNJ",
	thaumaturge = "THM",
	arcanist = "ACN",
	rogue = "ROG",
	-- Combat jobs
	["dark knight"] = "DRK",
	["red mage"] = "RDM",
	["black mage"] = "BLM",
	["white mage"] = "WHM",
	["blue mage"] = "BLM",
	astrologian = "AST",
	bard = "BRD",
	dragoon = "DRG",
	monk = "MNK",
	machinist = "MCH",
	ninja = "NIN",
	samurai = "SAM",
	scholar = "SCH",
	summoner = "SMN",
	paladin = "PLD",
	warrior = "WAR",
	gunbreaker = "GNB",
	dancer = "DNC",
	reaper = "RPR",
	sage = "SGE",
	viper = "VPR",
	pictomancer = "PCT",
}

function SMWTable.classJobRequirementColumn()
	return {
		label = "Requirement",
		printouts = {
			["Is for class"] = "#",
			["Is for job"] = "#",
		},
		render = function(item)
			local out = ""

			local classes = item["Is for class"] or {}
			if type(classes) == "string" then
				classes = {classes}
			end
			local jobs = item["Is for job"] or {}
			if type(jobs) == "string" then
				jobs = {jobs}
			end

			for i, className in ipairs(classes) do
				if i ~= 1 then
					out = out .. " "
				end
				out = out .. "[[" .. className .. "|" .. classJobAbbreviations[className:lower()] .. "]]"
			end

			out = out .. " "

			for i, jobName in ipairs(jobs) do
				if i ~= 1 then
					out = out .. " "
				end
				out = out .. "[[" .. jobName .. "|" .. classJobAbbreviations[jobName:lower()] .. "]]"
			end

			return out
		end
	}
end

function SMWTable.weaponDamageColumn()
	return {
		label = "Damage (Type)",
		printouts = {
			["Is for job"] = "#",
			["Has weapon physical damage"] = "",
			["Has weapon magic damage"] = "",
			["Has hq weapon physical damage"] = "",
			["Has hq weapon magic damage"] = "",
		},
		render = function(item)
			-- only render damage if this weapon is for a job
			local job = item["Is for job"]
			if not job or job == "" then return "" end

			
			job = job:lower()
			local damageType = "magic"
			if job == "warrior"
			or job == "paladin"
			or job == "monk"
			or job == "dragoon"
			or job == "bard"
			or job == "ninja"
			or job == "dark knight"
			or job == "machinist"
			or job == "samurai"
			or job == "gunbreaker"
			or job == "dancer"
			or job == "reaper"
			or job == "viper"
			then
				damageType = "physical"
			end

			local out = tostring(item["Has weapon " .. damageType .. " damage"])
			local hqDamage = item["Has hq weapon " .. damageType .. " damage"]
			if hqDamage and hqDamage ~= "" then
				out = out .. "&nbsp;<sup style=\"font-size: 0.7em; font-weight: bold;\">" .. mw.getCurrentFrame():expandTemplate({title = "HQ"}) .. item["Has hq weapon physical damage"] .. "</sup>"
			end
			if damageType == "physical" then
				out = out .. "[[File:Physical Damage.png|24px|link=Damage Types#Physical]]"
			else
				out = out .. "[[File:Magical Damage.png|24px|link=Damage Types#Magical]]"
			end
			return out
		end
	}
end

function SMWTable.materiaSlotsColumn()
	return {
		label = "Materia slots",
		printouts = {
			["Has materia slots"] = "",
			["Has advanced melding"] = "",
		},
		render = function(item)
			local out = item["Has materia slots"]
			if yesno(item["Has advanced melding"]) then
				out = out .. " <span style=\"color:red\" title=\"This item can undergo advanced melding\">(5)</span>"
			end
			return out
		end
	}
end

local attributes = {
	"Strength",
	"Dexterity",
	"Intelligence",
	"Mind",
	"Vitality",
	"Critical Hit",
	"Determination",
	"Direct Hit Rate",
	"Skill Speed",
	"Spell Speed",
	"Tenacity",
	"Piety",
}

local attributePrintouts = {}
for _, v in ipairs(attributes) do
	attributePrintouts["Has " .. v:lower() .. " bonus"] = ""
	attributePrintouts["Has hq " .. v:lower() .. " bonus"] = ""
end
attributePrintouts["Has customizable substats"] = ""
attributePrintouts["Has random substats"] = ""

function SMWTable.attributeBonusesColumn()
	return {
		label = "Stats and Attributes",
		printouts = attributePrintouts,
		render = function(item)
			local out = ""

			for _, attributeName in ipairs(attributes) do
				local bonus = item["Has " .. attributeName:lower() .. " bonus"]
				if bonus and bonus ~= "" then
					out = out .. "[[" .. attributeName .. "]]&nbsp;+" .. bonus
					local hqBonus = item["Has hq " .. attributeName:lower() .. " bonus"]
					if hqBonus and hqBonus ~= "" then
						out = out .. "<sup style=\"font-size: 0.7em; font-weight: bold;\">" .. mw.getCurrentFrame():expandTemplate({title = "HQ"}) .. item["Has hq " .. attributeName:lower() .. " bonus"] .. "</sup>"
					end
					out = out .. "&emsp;"
				end
			end

			if yesno(item["Has customizable substats"]) then
				out = out .. "[[File:Item settings icon.png|24px|link=]] ''Customizable [[Attributes#Secondary_Attributes|secondary attributes]]''"
			elseif yesno(item["Has random substats"]) then
				out = out .. "''Random [[Attributes#Secondary_Attributes|secondary attributes]]''"
			end

			return out
		end
	}
end



-- testing
function SMWTable.main(frame)

	return SMWTable.new("[[Has item type::Gunbreaker's Arm]]", {
		sort = "Has level requirement,Has item level,Has canonical name",
		limit = 999,
	})
		:column(SMWTable.nameAndIconColumn({label = "Item"}))
		:column(SMWTable.textColumn({property = "Has level requirement", label = "Level"}))
		:column(SMWTable.textColumn({property = "Has item level", label = "Item level"}))
		:column(SMWTable.classJobRequirementColumn())
		:column(SMWTable.weaponDamageColumn())
		:column(SMWTable.materiaSlotsColumn())
		:column(SMWTable.attributeBonusesColumn())

	end



return SMWTable

--[===[ Test in console with:

= p.new("[[Has item type::Gunbreaker's Arm]][[Has level requirement::80]]", {sort = "Has level requirement,Has item level,Has canonical name"})
	:column(p.nameAndIconColumn())
	:column(p.textColumn({property = "Has level requirement", label = "Level"}))
	:column(p.textColumn({property = "Has item level", label = "Item level"}))
	:column(p.classJobRequirementColumn())
	:column(p.weaponDamageColumn())
	:column(p.materiaSlotsColumn())
	:column(p.attributeBonusesColumn())

]===]