Module:Wares

local p = {} local h = {} local Data = mw.loadData("Module:Wares/Data")

local DataTable = require("Module:Data Table") local File = require("Module:File") local Franchise = require("Module:Franchise") local Term = require("Module:Term") local utilsArg = require("Module:UtilsArg") local utilsLayout = require("Module:UtilsLayout") local utilsMarkup = require("Module:UtilsMarkup") local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar")

local CATEGORY_INVALID_ARGS = require("Module:Constants/category/invalidArgs") local CLASS_TOOLTIP = require("Module:Constants/class/tooltip") local STR_PRICE_VARIES = "Varies" local VAR_ITEMS = "Module:Wares/items"

function h.warn(msg, ...) local utilsError = require("Module:UtilsError") msg = string.format(msg, ...) utilsError.warn(msg) end

function p.Main(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates["Wares"]) local categories = err and err.categoryText or "" if err and err.args and err.args.game then return "", categories end local items = utilsVar.get(VAR_ITEMS) or {} utilsVar.set(VAR_ITEMS) -- clear variable for the next table if #items == 0 then items, categories2 = h.parseShorthand(args.items, args.prices) categories = categories..categories2 end h.formatItems(args, items) h.store(args, items) local prices, categories2 = h.printWares(args, items) categories = categories..categories2 return prices, categories end

function p.Item(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates["Wares/Item"]) args.categories = err and err.categoryText or "" utilsVar.add(VAR_ITEMS, args) end

function h.parseShorthand(itemArgs, priceNames) local categories = "" local rows = DataTable.parseRows(itemArgs) local items = {} for i, row in ipairs(rows) do		local item, quantity = h.parseItemQuantity(row.cells[1]) if not item then h.warn("Empty  value at row %d. Row will be skipped.", i)			categories = categories.."" end local price, prices, description if not priceNames then price = row.cells[2] description = row.cells[3] if not price then h.warn("Item  is missing a price value. Row will be skipped.", item) categories = categories.."" end else prices = {} description = row.cells[2+#priceNames] for i in ipairs(priceNames) do				prices[i] = { price = row.cells[1+i] } end end table.insert(items, {			item = item,			quantity = quantity,			price = price,			prices = prices,			description = utilsString.nilIfEmpty(description),		}) end return items, categories end function h.parseItemQuantity(arg) if not arg or arg == "" then return nil, 0 end local quantity = string.match(arg, "^([%d,]+)") if quantity then string.gsub(quantity, ",", "") quantity = tonumber(quantity) else quantity = 1 end local item = string.gsub(arg, "^[%d,]+%s+", "") return item, quantity end

function h.formatItems(args, items) for i, item in ipairs(items) do		h.formatItemDisplay(args, item) item.restocks = item.restocks or h.getConfig("restocks", args, item) end end function h.formatItemDisplay(args, item) local containsLink = h.containsArticleLink(item.item) local formatArgs local info = "" local itemPage if not containsLink then itemPage, info = utilsMarkup.separateMarkup(item.item) local singular = Term.fetchTerm(itemPage, args.game) local plural = Term.fetchTerm(itemPage, args.game, { 			plural = true,			allowSingular = true,		}) -- Don't forget to update p.Schemas documentation when adding or changing format args formatArgs = { game = args.game, singular = singular or itemPage, plural = plural or itemPage, quantity = tostring(item.quantity or "") }	else formatArgs = {} end

local fileFormat = h.getConfig("fileFormat", args, item) local file = item.image if not file and not containsLink and fileFormat ~= "" then file = utilsString.interpolate(fileFormat, formatArgs) end local fileSize = h.getConfig("fileSize", args, item) local fileScale = h.getConfig("fileScale", args, item) local image = file and File.image(file, {		size = fileSize,		scale = fileScale,	})

local text = item.display if not text and not itemPage then text = item.item elseif not text and item.quantity and item.quantity > 1 then local quantityFormat = h.getConfig("quantityFormat", args, item) text = utilsString.interpolate(quantityFormat, formatArgs)..info elseif not text then local useDisplay = itemPage ~= formatArgs.singular and not string.find(itemPage, "%)$")		-- If the input refers to a synonym, we want to display that synonym if that's what appears in the shop, hence the display value		-- Unless the article uses parenthetical disambiguation, which are likely not part of the actual in-game item name		text = Term.link(itemPage, args.game, { display = useDisplay and itemPage or nil })		text = text..info	end	item.item = itemPage or item.item	item.displayText = text	item.displayAsset = file	item.displayThumb = image end function h.containsArticleLink(str)	str = string.gsub(str, "%[%[:?Category:[^%]]+%]%]", "")	local match = string.find(str, "%[%[[^:]")	return match ~= nil end

function h.store(args, items) for i, item in ipairs(items) do		local itemPrices = h.prices(args.prices, item) for j, item in ipairs(itemPrices) do			local stock = item.stock and utilsMarkup.separateMarkup(item.stock) mw.getCurrentFrame:expandTemplate({				title = "Wares/Store",				args = {					game = args.game,					item = utilsMarkup.separateMarkup(item.item),					quantity = item.quantity,					displayText = mw.text.killMarkers(item.displayText),					displayAsset = item.displayAsset,					price = item.price and utilsMarkup.separateMarkup(item.price),					priceRupees = item.priceRupees,					priceName = item.priceName,					stock = stock and tonumber(stock),					restockInfo = item.restocks,					description = item.description,					locations = item.locations and table.concat(item.locations, ","),					conditions = item.conditions and table.concat(item.conditions, ","),				}			}) end end end function h.prices(prices, item) local itemPrices = {} if not prices then itemPrices = {utilsTable.clone(item)} else for i, price in ipairs(prices) do			local itemPrice = utilsTable.merge({}, item, {				priceName = price,				price = item.prices[i] and item.prices[i].price			}) if price ~= "N/A" then table.insert(itemPrices, itemPrice) end end end for i, itemPrice in ipairs(itemPrices) do		local price = itemPrice.price price = price and utilsMarkup.separateMarkup(price) local priceNumber = price and price:gsub(",", "") local priceNumber = priceNumber and tonumber(priceNumber) if priceNumber then -- is number price = price.." Rupee" itemPrice.priceRupees = priceNumber end if price == STR_PRICE_VARIES then price = nil end itemPrice.price = price end return itemPrices end function h.printWares(args, items) local locations, categories = h.groupItemsBy("locations", items) locations = locations and utilsTable.sortBy(locations, "locations") -- listed in alphabetical order if locations and args.locationTabs then local content, categories2 = h.printTabs(args, locations, "locations", h.printConditionTabs) return content, categories..categories2 elseif locations then args.showLocationsColumn = true items = utilsTable.concat(unpack(locations)) end local content, categories2 = h.printConditionTabs(args, items) return content, categories..categories2 end

function h.printConditionTabs(args, items) local conditions, categories = h.groupItemsBy("conditions", items) conditions = conditions and utilsTable.sortBy(conditions, "index") -- so that the first conditions listed in source are the first displayed if conditions then local content, categories2 = h.printTabs(args, conditions, "conditions", h.printWaresTable) return content, categories..categories2 else local content, categories2 = h.printWaresTable(args, items) return content, categories..categories2 end end

function h.groupItemsBy(property, items) local categories = "" local hasProperty = #(utilsTable.filter(items, property)) > 0 local itemsByProperty = {} local itemGroups = {} local groupCount = 0 if not hasProperty then return nil, "" end for i, item in ipairs(items) do		if not item[property] then h.warn("Item  is missing the   parameter. If one item has a   parameter, then all items must have it.", item.item, property, property) categories = categories.."" end for i, propValue in ipairs(item[property] or {}) do			local itemsWithProperty = itemsByProperty[propValue] if not itemsWithProperty then groupCount = groupCount + 1 itemsWithProperty = { [property] = propValue, index = groupCount }			end table.insert(itemsWithProperty, utilsTable.merge({}, item, { [property] = propValue }))			itemsByProperty[propValue] = itemsWithProperty end end itemGroups = utilsTable.toArray(itemsByProperty) return itemGroups, categories end

function h.printTabs(args, itemGroups, groupingProperty, printWaresFn) local categories = ""

local tabData = {} for i, itemGroup in ipairs(itemGroups) do		local wares, categories2 = printWaresFn(args, itemGroup) categories = categories..categories2 table.insert(tabData, {			label = itemGroup[groupingProperty],			content = wares,		}) end local tabs = utilsLayout.tabs(tabData) return tabs, categories end

local notes = {} local noteCount = 0 function h.printWaresTable(args, items) local categories = "" local hasDescriptions = #(utilsTable.filter(items, "description")) > 0 local hasStock = #(utilsTable.filter(items, "stock")) > 0 local showQuantityColumn = args.showQuantityColumn local columns = {} if args.showLocationsColumn then table.insert(columns, "Location [Rowspan][Term]") end table.insert(columns, "Item") if showQuantityColumn then table.insert(columns, "Quantity") end if args.prices and #args.prices > 0 then table.insert(columns, string.format("Prices [Colspan:%d]", #args.prices)) columns = utilsTable.concat(columns, args.prices) else table.insert(columns, "Price") end if hasStock then table.insert(columns, "Stock") end if hasDescriptions then table.insert(columns, "Description [Transcript]") end local defaults = Data.items.defaults[Franchise.graphics(args.game)]

local rows = {} for i, item in ipairs(items) do		local rowCells = {} if args.showLocationsColumn then local locations = item.locations if locations then table.insert(rowCells, locations) else table.insert(rowCells, "N/A") end end local itemCell = h.itemCell(args, item) table.insert(rowCells, itemCell)

if showQuantityColumn then table.insert(rowCells, tostring(item.quantity or 1)) end

if args.prices and #args.prices > 0 then for j, priceName in ipairs(args.prices) do				local price = item.prices[j] and item.prices[j].price if price == nil then local itemName = utilsMarkup.separateMarkup(item.item) h.warn("Item  is missing the   parameter corresponding to the   price. If the item is not sold at this price, please set the parameter to  .", itemName, j, priceName) end table.insert(rowCells, h.price(args.game, price)) end else table.insert(rowCells, h.price(args.game, item.price)) if item.prices and #item.prices > 0 then h.warn("Item  has an invalid parameter   parameter. Rename the parameter to   or enable multiple prices using the   parameter.", item.item) categories = categories.."" end end if hasStock then local stock = item.stock or "Unlimited" local restockInfo = item.restocks if restockInfo then local noteId = notes[restockInfo] if not noteId then noteCount = noteCount + 1 noteId = "Wares "..noteCount notes[restockInfo] = noteId end local restockNote = mw.getCurrentFrame:expandTemplate({					title = "Table Note",					args = {						name = noteId,						[1] = notes[restockInfo] and restockInfo					}				}) stock = stock..restockNote end table.insert(rowCells, stock) end if hasDescriptions then table.insert(rowCells, item.description or "") end table.insert(rows, { cells = rowCells }) end local dataTable = DataTable.printTable(rows, {		game = args.game,		columns = columns,	}) if noteCount > 0 then dataTable = dataTable .. mw.getCurrentFrame:expandTemplate({ title = "Table Notes" }) end return dataTable, categories end

function h.itemCell(args, item) local html = mw.html.create("div") :addClass("zw-wares__item") if item.displayThumb then html:tag("div") :addClass("zw-wares__item-image") :wikitext(item.displayThumb) end html:tag("div") :addClass("zw-wares__item-text") :wikitext(item.displayText) return tostring(html) end

function h.price(game, price) local html = mw.html.create("div"):addClass("zw-wares__price") local sortValue if not price or price == "" then html:wikitext("") return { content = tostring(html), }	end local price, info = utilsMarkup.separateMarkup(price) if price == "N/A" then html:addClass("zw-wares__price--not-applicable") :tag("span") :addClass(CLASS_TOOLTIP) :attr("title", "Not Applicable") :wikitext("—") elseif price == STR_PRICE_VARIES then html:addClass('zw-wares__price--variable') :wikitext(STR_PRICE_VARIES) :wikitext(info) elseif string.find(price, ", ") then -- price is list of items local text = mw.getCurrentFrame:expandTemplate({			title = "Amounts",			args = {game, price}		}) html:wikitext(text..info) elseif not string.find(price, "^%d") then -- price is single non-currency item local text = mw.getCurrentFrame:expandTemplate({			title = "Icon List",			args = {game, price}		}) html:wikitext(text..info) elseif string.match(price, "^[%d,]+$") then -- price is just a number, so it is implicitly in Rupees local text = mw.getCurrentFrame:expandTemplate({			title = "Rupee",			args = {game, price}		}) local priceNumber = string.gsub(price, ",", "") sortValue = tonumber(priceNumber) html:wikitext(text..info) else local isCurrency for i, currency in ipairs(Data.currencies) do			if string.find(price, "^[%d,]+%s+"..currency.name.."$") then price = string.gsub(price, "%s+"..currency.name, "") local currencyTemplateArgs = {price} if currency.multigame then table.insert(currencyTemplateArgs, 1, game) end local text = mw.getCurrentFrame:expandTemplate({					title = currency.name,					args = currencyTemplateArgs,				}) local priceNumber = string.gsub(price, ",", "") sortValue = tonumber(priceNumber) html:wikitext(text..info) isCurrency = true break end end if not isCurrency then -- we asssume the price is a certain quantity of an item which does not have a dedicated currency template local text = mw.getCurrentFrame:expandTemplate({				title = "Amounts",				args = {game, price}			}) html:wikitext(text..info) end end return { content = tostring(html), sortValue = sortValue, } end

function h.getConfig(prop, args, item) if prop == "fileFormat" and args.fileType then return string.format("File:${game} ${singular} %s.png", args.fileType) end

local gameData = Data.items[args.game] local defaultProp = Data.items.defaults[Franchise.graphics(args.game)][prop] local gameProp = gameData and gameData[prop] local itemProp = gameData and gameData[item.item] and gameData[item.item][prop] return itemProp or gameProp or defaultProp end

function p.Schemas local interpolationVariablesDoc = "" .."\n* : The singular term for a given item in the given game." .."\n* : The plural term for a given item in the given game." .."\n* : The value of the   parameter for a given item in the Wares table." return { Data = { type = "record", required = true, properties = { {					name = "currencies", required = true, type = "array", desc = "A list of currencies which have dedicated templates for displaying values, for example Template:Rupee and Template:Mon. Template:Wares will use the corresponding template when a price is listed in the given currency.", items = { type = "record", properties = { {								name = "name", type = "string", required = true, desc = "The name of the currency.", },							{								name = "multigame", type = "boolean", required = true, desc = "Indicates whether the currency template has a game parameter that needs to be passed on to the currency template (e.g. ) or not (e.g.  )." },						},					},				},				{					name = "items", required = true, desc = " Determines how certain items are listed.  and   strings can have any of the following variables: "..interpolationVariablesDoc.." ", allOf = { {							type = "record", required = true, properties = { {									name = "defaults", required = true, type = "record", properties = { {											name = "2D", required = true, type = "record", desc = "Default configuration values for 2D games.", properties = { {													name = "fileScale", type = "number", },												{													name = "fileFormat", type = "string", },												{													name = "fileSize", type = "string", },												{													name = "quantityFormat", type = "string", },												{													name = "restocks", type = "string", },											},										},										{											name = "3D", required = true, type = "record", desc = "Default configuration values for 3D games.", properties = { {													name = "fileFormat", type = "string", },												{													name = "fileSize", type = "string", },												{													name = "quantityFormat", type = "string", },												{													name = "restocks", type = "string", },											},										},									},								},							},						},						{							type = "map", required = true, keyPlaceholder = "game", keys = { type = "string" }, values = { allOf = { {										type = "record", required = true, properties = { {												name = "fileScale", type = "number", desc = "Image thumbnails for the given game are sized based on their original size multiplied by this scaling factor. Ensures that sprites maintain their sizes relative to one another.", },											{												name = "fileSize", type = "string", desc = "Image size in pixels for items in the given game.", },											{												name = "fileFormat", type = "string", desc = "Default  value for all items in the given game.", },											{												name = "quantityFormat", type = "string", desc = "Default  value for all items in the given game.", },											{												name = "restocks", type = "string", desc = "Default  value for all items in the given game.", },										},									},									{										type = "any", required = true, keyPlaceholder = "item", keys = { type = "string", desc = "A wiki page describing an Item.", },										values = { type = "any", oneOf = { {													type = "string", },												{													type = "record", additionalProperties = true, properties = { {															name = "fileFormat", type = "string", desc = "A format string that determines what image should be shown in Wares tables for a particular item in a particular game.", },														{															name = "quantityFormat", type = "string", desc = "A format string that determines how a quantified item should be shown in Ware tables for a particular item in a particular game.", },														{															name = "restocks", type = "string", desc = "Information about when the item restocks. Shown as a in the   cell of the Wares table.", },													},												}											}										},									},								},							},						}					}				},			}		}	} end

p.Templates = { ["Wares"] = { description = "Lists items sold by merchants or in shops", format = "block", paramOrder = {"game", "prices", "locationTabs", "showQuantityColumn", "fileType", "..."}, params = { game = { required = true, desc = "A game code.", type = "string", enum = Franchise.enum, },			prices = { desc = "", type = "string", split = true, trim = true, nilIfEmpty = true, },			locationTabs = { desc = "", type = "boolean", default = false, trim = true, nilIfEmpty = true, },			showQuantityColumn = { desc = "", type = "boolean", default = false, trim = true, nilIfEmpty = true, },			fileType = { desc = "", type = "string", trim = true, nilIfEmpty = true, },			["..."] = {				name = "items", type = "content", trim = true, },		},	},	["Wares/Item"] = { repeatedGroup = { name = "prices", params = {"price"}, allowSingle = true, },		paramOrder = {"item", "price", "image", "display", "quantity", "stock", "restocks", "description", "locations", "conditions"}, params = { item = { desc = "", required = true, type = "wiki-page-name", trim = true, nilIfEmpty = true, },			price = { desc = "", required = true, type = "string", trim = true, nilIfEmpty = true, },			image = { desc = "", type = "string", trim = true, nilIfEmpty = true, },			display = { desc = "", type = "content", trim = true, nilIfEmpty = true, },			quantity = { desc = "", type = "number", default = 1, trim = true, nilIfEmpty = true, },			stock = { desc = "", type = "string", -- not a number because it may have footnotes attached to it				trim = true, nilIfEmpty = true, },			restocks = { desc = "", type = "content", trim = true, nilIfEmpty = true, },			description = { desc = "", type = "content", trim = true, nilIfEmpty = true, },			locations = { desc = "", type = "string", split = true, trim = true, nilIfEmpty = true, },			conditions = { desc = "", type = "string", split = true, trim = true, nilIfEmpty = true, },		}	}, }

return p