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 = string.find(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 hasParens = 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 = not hasParents 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.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.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 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  .", item.item, j, priceName) categories = categories.."" 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.match(price, "^[%d,]+$") or not string.find(price, ", ") and string.find(price, "^[%d,]+%s+Rupee$") then -- is number or Rupee price = string.gsub(price, "%s+Rupee", "") local text = mw.getCurrentFrame:expandTemplate({			title = "Rupee",			args = {game, price}		}) local priceNumber = string.gsub(price, ",", "") sortValue = tonumber(priceNumber) html:wikitext(text..info) elseif not string.find(price, ", ") and not string.find(price, "^%d") then -- assume it is a single, non-numbered item local text = mw.getCurrentFrame:expandTemplate({			title = "Icon List",			args = {game, price}		}) html:wikitext(text..info) else -- assume it is a list of item amounts local text = mw.getCurrentFrame:expandTemplate({			title = "Amounts",			args = {game, price}		}) html:wikitext(text..info) 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[args.game] local defaultProp = Data.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 = { desc = " 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 format 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 format 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