Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

This module uses info gathered from the API and stored in the data page to:

It is also invoked by other modules, such as Module:CosmeticMachine and Module:CosmeticCrates.


local getArgs = require('Module:Arguments').getArgs
local expand = require('Module:Utils').expand
local preprocess = require('Module:Utils').preprocess
local isAnimated = require('Module:Utils').isAnimated
local currentSeason = require("Module:CurrentSeason")

local cosmeticData = mw.loadData('Module:CosmeticInfo/Data')
if not cosmeticData then
	return "Error: Unable to load data"
end

local obtainabilityData = mw.loadData("Module:CosmeticInfo/Obtainability/Data")
local defaultChromaSetsData = mw.loadData("Module:CosmeticInfo/DefaultChromaSets/Data")
local rotatingWeaponForgeData = mw.loadData("Module:RotatingWeaponForge/Data")
local limitedWeaponForgeData = mw.loadData("Module:LimitedWeaponForge/Data")
local tradingData = require('Module:TradingData').tradableAssets

local today = os.date("%Y-%m-%d %H:%M:%S")

local chromaSets = mw.loadData("Module:CosmeticInfo/ChromaSets/Data")

local p = {}

local function getCosmetic(args)
	return cosmeticData["cosmetics"][args.name]
end

local function getSimpleField(field)
	return function(frame)
		local args = getArgs(frame)
		local cosmetic = getCosmetic(args)
		return cosmetic and cosmetic[field]
	end
end

local function makeSet(list)
	local t = {}
	for _, v in ipairs(list) do t[v] = true end
	return t
end

local function isACosmetic(rule)
	local collections = rule.collections and makeSet(rule.collections)
	return function(frame)
		local args = getArgs(frame)
		local cosmetic = getCosmetic(args)
		if not cosmetic then return false end

		if collections and not collections[p.getCollection({name=args.name})] then
			return false
		end

		if rule.type and p.getType({name=args.name}) ~= rule.type then
			return false
		end

		if rule.notType and p.getType({name=args.name}) == rule.notType then
			return false
		end

		return true
	end
end

local isACosmeticRules = {
	isCrateCosmetic = {
		collections = {
			"Elemental", "Standard Game", "Exclusive Game",
			"Exclusive Season", "Exclusive Variety", "Gate"
		},
		notType = "Arcane"
	},

	isArcaneGateCosmetic = {
		collections = { "Gate" },
		type = "Arcane"
	},

	isFishingCosmetic = {
		collections = { "Fishing" }
	},

	isWeaponSkin = {
		collections = { "Premium Weapon", "Limited Weapon" }
	}
}

for key, rule in pairs(isACosmeticRules) do
	p[key] = isACosmetic(rule)
end

local function formatGlyphs(description)
	local glyphs = {
		["<glyph:\"mcc:icon.tooltips.veteran\"> "] = "<br />[[File:Icon-Veteran.png|16px]] <span style=\"color: #52C8FF; font-weight: bold\">",
		["<glyph:\"mcc:icon.tooltips.squidtek\"> "] = "<br />[[File:Icon-Squidtek.png|16px]] <span style=\"color: #52C8FF; font-weight: bold\">",
		["<glyph:\"mcc:icon.info_blue\"> "] = "<br />[[File:Icon-Info-Blue.png|16px]] <span style=\"color: #55FF55; font-weight: bold\">",
	}
	
	local openSpan = false
	for key, val in pairs(glyphs) do
		if description:find(key) then
			description = description:gsub(key, (openSpan and val or "</span>" .. val))
			openSpan = true
		end
	end
	return openSpan and (description .. "</span>") or nil
end

local function formatArcaneBonus(description)
	if description:find("Arcane Bonus") then
		description = description:gsub("Arcane Bonus", "<br /><span style=\"color: #64FCFC\"><span class=\"white-text\">Arcane Bonus</span>")
		description = description:gsub("(Grants you)(.-)( while)", function(startText, middle, endText)
			return startText .. "<span class=\"white-text\">" .. middle .. "</span>" .. endText
		end)
		return description .. "</span>"
	end
	return description
end

local function stripDescription(description)
	description = description:gsub("<glyph:\".-\"> ?", "")
	description = description:gsub("Arcane Bonus.*", "")
	return mw.text.trim(description)
end

function p.getDescription(frame)
	local args = getArgs(frame)
	local cosmetic = getCosmetic(args)
	if not cosmetic then return "No data found" end
	
	local plain = args.plain == "true"
	if plain then
		return stripDescription(cosmetic.description)
	end
	
	return formatGlyphs(cosmetic.description) or formatArcaneBonus(cosmetic.description)
end

p.getCategory = getSimpleField("category")
p.getCollection = getSimpleField("collection")
p.getRarity = getSimpleField("rarity")
p.isColorable = getSimpleField("colorable")
p.getTrophiesAwarded = getSimpleField("trophies")
p.isBonusTrophies = getSimpleField("isBonusTrophies")
p.canBeDonated = getSimpleField("canBeDonated")

function p.getDonationLimit(frame)
	local args = getArgs(frame)
	local cosmetic = getCosmetic(args)
	return (cosmetic and cosmetic.royalReputation and cosmetic.royalReputation.donationLimit) or 0
end

function p.getReputationAmount(frame)
	local args = getArgs(frame)
	local cosmetic = getCosmetic(args)
	return (cosmetic and cosmetic.royalReputation and cosmetic.royalReputation.reputationAmount) or 0
end

p.getGlobalNumberOwned = getSimpleField("globalNumberOwned")
p.getObtainmentHint = getSimpleField("obtainmentHint")

function firstToUpper(str)
	return (str:gsub("^%l", string.upper))
end

function p.getType(frame)
	local args = getArgs(frame)
	local cosmetic = getCosmetic(args)
	return firstToUpper(cosmetic["type"]:lower())
end

function p.getFishingClimate(frame)
	local args = getArgs(frame)
	local cosmetic = getCosmetic(args)
	if not cosmetic or not p.isFishingCosmetic({name = args.name}) then
		return nil
	end

	local hint = cosmetic.obtainmentHint or ""
	local climate = hint:match("while fishing in the (.-) Climate")
	if climate then
		return firstToUpper(climate)
	end

	return nil
end

function p.getLastUpdatedDate()
	local lang = mw.language.getContentLanguage()
	return lang:formatDate( 'd F Y, h:i a', '@' .. cosmeticData['last_updated'], false ) .. ' UTC'
end

function p.lastUpdatedIcon()
	return string.format(
		'<sup><abbr title="Last updated: %s" tabindex="0">ⓘ</abbr></sup>',
		p.getLastUpdatedDate()
	)
end

function p.getDefaultChromaSet(frame)
	local args = getArgs(frame)
	return defaultChromaSetsData[args.name] or ""
end

local function isCurrentlyInLimitedForge(name)
	for _, rotation in ipairs(limitedWeaponForgeData) do
		if today >= rotation.start_date and today <= rotation.end_date then
			for _, skin in ipairs(rotation.skins) do
				if skin == name then return true end
			end
		end
	end
	return false
end

local function isCurrentlyInRotatingForge(name)
	local rotationToday = rotatingWeaponForgeData.days[rotatingWeaponForgeData.current_day] or {}
	for _, rotationSkin in ipairs(rotationToday) do
		if rotationSkin == name then return true end
	end
	return false
end

local function getOwnedNumber(raw)
	raw = tostring(raw or "0"):gsub(",", ""):gsub("%+", "")
	return tonumber(raw) or 0
end

local function sortCosmetics(list, order)
	table.sort(list, function(a, b)
		return order == "desc" and a.owned > b.owned or a.owned < b.owned
	end)
end

local function formatCosmeticList(cosmetics)
	local result = {}
	for _, c in ipairs(cosmetics) do
		table.insert(result, string.format("* %s (%s)", c.name, c.display or c.owned))
	end
	return table.concat(result, "\n")
end

function p.sortListByNumberOwned(frame)
	local args = getArgs(frame)
	local names = mw.text.split(args.names or "", ",%s*")
	local order = args.order or "asc"
	local unformatted = args.unformatted or ""

	local cosmetics = {}
	for _, name in ipairs(names) do
		local data = cosmeticData["cosmetics"][name]
		if data and data.globalNumberOwned then
			table.insert(cosmetics, {
				name = name,
				owned = getOwnedNumber(data.globalNumberOwned),
				display = data.globalNumberOwned
			})
		end
	end

	sortCosmetics(cosmetics, order)
	if unformatted ~= "" then
		return cosmetics
	else
		return formatCosmeticList(cosmetics)
	end
end

function p.getNumberOwnedListByFieldValue(frame)
	local args = getArgs(frame)
	local field = args.field
	local target = args.value
	local order = args.order or "asc"

	local filterValue = ({
		["true"] = true,
		["false"] = false
	})[target] or target

	local filtered = {}
	for name, data in pairs(cosmeticData["cosmetics"]) do
		if data[field] == filterValue and data.globalNumberOwned then
			table.insert(filtered, {
				name = name,
				owned = getOwnedNumber(data.globalNumberOwned),
				display = data.globalNumberOwned
			})
		end
	end

	sortCosmetics(filtered, order)
	return formatCosmeticList(filtered)
end

function p.tradeablesPage(frame)
	local function getGroup(cosmetic)
		local categories = {}
		
		local obtainability = obtainabilityData[cosmetic.name]
		local cType = p.getType({name = cosmetic.name})
		local collection = p.getCollection({name = cosmetic.name})
		
		if obtainability == "Grand Auction" then
			table.insert(categories, obtainability)
		elseif obtainability == "Returning" then
			table.insert(categories, "Returning in a future event")
		elseif obtainability == "Unobtainable" then
			table.insert(categories, "Unobtainable other than trading")
		elseif obtainability == "Limited Weapon Forge" then
			table.insert(categories, "Unobtainable once the [[Limited Weapon Forge]] rotates")
		elseif p.isWeaponSkin({ name = cosmetic.name }) then
			if isCurrentlyInRotatingForge(cosmetic.name) then
				table.insert(categories, "Currently available in the [[Rotating Weapon Forge]]")
			end
			if isCurrentlyInLimitedForge(cosmetic.name) then
				table.insert(categories, "Currently available in the [[Limited Weapon Forge]]")
			end
			
			if #categories == 0 then
				table.insert(categories, "Currently only obtainable from [[Premium Weapon Crate]]s")
			end
		elseif cType == "Collector" then
			table.insert(categories, cType)
		elseif obtainability ~= nil then
			table.insert(categories, string.format("Unobtainable once the current [[%s]] event ends", obtainability))
		else
			table.insert(categories, "Unobtainable once [[" .. currentSeason .. "|this season]] ends")
		end
		
		return categories
	end

	local function getOwnershipBand(owned)
		if owned == "10000+" or owned == "1000+" then
			return owned .. " owned"
		else
			return "Less than 1000 owned"
		end
	end
	
	local function groupCosmetics(cosmeticList)
		local grouped = {}
		for _, cosmetic in ipairs(cosmeticList) do
			local categories = getGroup(cosmetic)
			local band = getOwnershipBand(cosmetic.display)
			
			for _, category in ipairs(categories) do
				grouped[category] = grouped[category] or {}
				grouped[category][band] = grouped[category][band] or {}
				table.insert(grouped[category][band], cosmetic)
			end
		end
		return grouped
	end
	
	local function renderTableForGroup(group, title)
		local out = {'<h2>' .. title .. '</h2>'}
		local bandOrder = { "Less than 1000 owned", "1000+ owned", "10000+ owned" }
		
		local bandCount = 0
		for _, band in ipairs(bandOrder) do
			if group[band] then
				bandCount = bandCount + 1
			end
		end

		for _, band in ipairs(bandOrder) do
			local list = group[band]
			if list then
				sortCosmetics(list)
				if bandCount > 1 then
					table.insert(out, '<h3>' .. band .. '</h3>')
				end
				table.insert(out, '<div class="tradeable-display">')
				local count = 0
				for _, item in ipairs(list) do
					if count > 0 and count % 4 == 0 then
						--table.insert(out, '<div>')
					end

					local name, owned = item.name, item.display
					local filename = name .. '.png'
					local cell

					if band == "Less than 1000 owned" then
						cell = string.format('<div><center><div class="afix" style="width: 100px">[[File:%s|link=%s]]</div></center><div style="word-wrap:break-word; text-align:center"><small>[[%s]]</small><br />Owned: <b>%s</b></div></div>', filename, name, name, owned)
					else
						cell = string.format('<div><center><div class="afix" style="width: 100px">[[File:%s|link=%s]]</div></center><div style="word-wrap:break-word; text-align:center"><small>[[%s]]</small></div></div>', filename, name, name)
					end

					table.insert(out, cell)
					count = count + 1
				end
				table.insert(out, '</div>')
			end
		end

		return table.concat(out, '\n')
	end
	
	local tradeables = {}
	for name, _ in pairs(cosmeticData["cosmetics"]) do
		local cType = p.getType({ name = name })
		if cType == "Limited" or cType == "Premium" or cType == "Collector" then
			local data = cosmeticData["cosmetics"][name]
			if data and data.globalNumberOwned then
				table.insert(tradeables, {
					name = name,
					display = data.globalNumberOwned,
					owned = getOwnedNumber(data.globalNumberOwned)
				})
			end
		end
	end
	
	local grouped = groupCosmetics(tradeables)
	
	local output = {}
	local categoryOrder = {
		"Unobtainable other than trading",
		"Collector",
		"Grand Auction",
		"Currently available in the [[Rotating Weapon Forge]]",
		"Currently available in the [[Limited Weapon Forge]]",
		"Currently only obtainable from [[Premium Weapon Crate]]s",
		"Unobtainable once the [[Limited Weapon Forge]] rotates",
		"Returning in a future event"
	}
	
	for category, _ in pairs(grouped) do
		if category:match("^Unobtainable once the current %[") then
			table.insert(categoryOrder, category)
		end
	end
	
	table.insert(categoryOrder, "Unobtainable once [[" .. currentSeason .. "|this season]] ends")
	
	for _, category in ipairs(categoryOrder) do
		if grouped[category] then
			table.insert(output, renderTableForGroup(grouped[category], category))
		end
	end
	
	return table.concat(output, '\n\n')
end

function p.getInfoboxImage(frame)
	local args = getArgs(frame)
	
	local name = args.name
	local isWeaponSkin = p.isWeaponSkin(frame)
	local rarity = p.getRarity(frame)
	
	if isWeaponSkin == true and rarity == "Mythic" then
		return frame:callParserFunction('#tag:gallery', {
			name .. '.png|Normal\n' .. name .. ' (Evolved).png|Evolved'
		})
	elseif mw.title.new("File:" .. name .. ".png").exists then
		return name .. ".png"
	end
end

local function renderWeaponGrid(frame, list, useChromaTemplate, addSpacing)
	local out = {}
	
	if addSpacing then
		table.insert(out, '<div class="tradeable-display" style="margin-top: 1rem">')
	else
		table.insert(out, '<div class="tradeable-display">')
	end
	
	for _, item in ipairs(list) do
		local label
		
		if useChromaTemplate then
			label = expand(frame, "Chroma Set", item.label, "short=yes")
		else
			label = item.label
		end
		
		local spacing = addSpacing and ' style="margin-bottom:18px; width: 20% !important"' or ""
		
		table.insert(out, string.format(
			'<div%s><center><div class="afix" style="width: 100px">[[File:%s|link=%s]]</div></center><div style="text-align:center">%s</div></div>',
			spacing, item.file, item.link, label
		))
	end
	table.insert(out, '</div>')
	return table.concat(out, "\n")
end


function p.getChromasForWeapon(frame)
	local args = getArgs(frame)
	local name = args.name
	
	if not p.isWeaponSkin({name = name}) then
		return "Not a weapon skin"
	end
	
	local rarity = p.getRarity({name = name})
	local defaultChroma = defaultChromaSetsData[name]
	
	local chromas = {}
	for _, chromaName in pairs(chromaSets) do
		table.insert(chromas, chromaName)
	end
	table.sort(chromas)
	
	if rarity == "Mythic" then
		local normalList = {}
		local evolvedList = {}
		
		for _, chroma in ipairs(chromas) do
			local normalFile
			local evolvedFile
			
			if chroma == defaultChroma then
				normalFile = name .. ".png"
				evolvedFile = name .. " (Evolved).png"
			else
				normalFile = string.format("%s (%s).png", name, chroma)
				evolvedFile = string.format("%s (%s - Evolved).png", name, chroma)
			end
			
			table.insert(normalList, {
				file = normalFile,
				link = name,
				label = chroma
			})
		
			table.insert(evolvedList, {
				file = evolvedFile,
				link = name,
				label = chroma
			})
		end
		
		local wikitext = table.concat({
			"<tabber>",
			"|-|Normal=\n" .. renderWeaponGrid(frame, normalList, true, true),
			"|-|Evolved=\n" .. renderWeaponGrid(frame, evolvedList, true, true),
			"</tabber>"
		}, "\n")
		
		return preprocess(frame, wikitext)
	end
	
	local list = {}
	for _, chroma in ipairs(chromas) do
		local file
		if chroma == defaultChroma then
			file = name .. ".png"
		else
			file = string.format("%s (%s).png", name, chroma)
		end

		table.insert(list, {
			file = file,
			link = name,
			label = chroma
		})
	end

	return renderWeaponGrid(frame, list, true, true)
end

function p.getWeaponsForChromaSet(frame)
	local args = getArgs(frame)
	local chroma = args.chroma_set
	
	local groups = {}
	
	for name, data in pairs(cosmeticData.cosmetics) do
		if p.isWeaponSkin({name = name}) then
			local defaultChroma = defaultChromaSetsData[name]
			
			local file
			if chroma == defaultChroma then
				file = name .. ".png"
			else
				file = string.format("%s (%s).png", name, chroma)
			end
			
			local rarity = p.getRarity({name = name})
			
			groups[rarity] = groups[rarity] or {}
			table.insert(groups[rarity], {
				file = file,
				link = name,
				label = '[[' .. name .. ']]'
			})
		end
	end
	
	local rarityOrder = { "Mythic", "Legendary", "Epic", "Rare" }
	
	local out = {}
	
	for _, rarity in ipairs(rarityOrder) do
		table.insert(out, string.format("=== %s ===", expand(frame, "Rarity", rarity, "Text")))
		local list = groups[rarity]
		table.sort(list, function(a, b) return a.label < b.label end)
		
		table.insert(out, renderWeaponGrid(frame, list, false, false))
	end
	
	return table.concat(out, "\n")
end

return p