Module:Construction list

From the RuneScape Wiki, the wiki for all things RuneScape
Jump to navigation Jump to search
Module documentation
This documentation is transcluded from Module:Construction list/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Construction list/doc. [edit]
Module:Construction list's function main is invoked by Template:Construction list.
Function list
L 14 — members_icon
L 22 — normalize_and_key
L 41 — getLastElement
L 48 — plink_list
L 56 — format_plinked_list
L 67 — parse_recipe
L 127 — query_construction_recipes
L 176 — build_row
L 207 — build_header
L 223 — p.main
L 228 — p._main
require("strict")
require('Module:Mw.html extension')
local getArgs = require("Module:Arguments").getArgs
local plink = require('Module:Plink')._plink
local plinkp = require('Module:Plink')._plinkp
local scp = require('Module:Skill clickpic')._main
local addcommas = require('Module:Addcommas')._add

local p = {}

local SKILL_NAME = "Construction"
local BATCH_SIZE = 5000

local function members_icon(is_members)
	if is_members then
		return "[[File:P2P icon.png|21px|link=Members]]"
	end
	return "[[File:F2P icon.png|21px|link=Free-to-play]]"
end

--- Normalize nail materials (if condense is true) and return a dedup key + new materials list.
local function normalize_and_key(item, condense_nails)
	local materials = {}
	local mat_names = {}
	local nail_added = false
	for _, mat in ipairs(item.materials) do
		local mat_name = mat.name or mat.page or ""
		if condense_nails and not nail_added and mat_name:match('[Nn]ails$') then
			materials[#materials + 1] = { name = "Nails", page = "Nails", pic = "Steel nails", quantity = mat.quantity }
			mat_names[#mat_names + 1] = "Nails"
			nail_added = true
		elseif not (condense_nails and mat_name:match('[Nn]ails$')) then
			materials[#materials + 1] = mat
			mat_names[#mat_names + 1] = mat.name or ""
		end
	end
	local key = item.page_name .. "|" .. tostring(item.level or "") .. "|" .. table.concat(mat_names, ",")
	return key, materials
end

local function getLastElement(val)
	if type(val) == "table" then
		return val[#val]
	end
	return val
end

local function plink_list(items)
	local images = {}
	for i, entry in ipairs(items) do
		images[i] = plink(entry.name, { size = "30x30", pic = entry.pic })
	end
	return images
end

local function format_plinked_list(items)
	if not items then return "" end
	local images = plink_list(items)
	for i, entry in ipairs(items) do
		if entry.quantity and entry.quantity > 1 then
			images[i] = addcommas(entry.quantity) .. " × " .. images[i]
		end
	end
	return table.concat(images, "<br/>")
end

local function parse_recipe(row)
	local production_json = row["recipe.production_json"]
	if not production_json then return nil end

	local ok, production = pcall(mw.text.jsonDecode, production_json)
	if not ok or not production then return nil end

	local con_level, con_exp
	if production.skills then
		for _, skill in ipairs(production.skills) do
			if skill.name == SKILL_NAME then
				con_level = skill.level
				con_exp = skill.experience
				break
			end
		end
	end

	local fac_str = table.concat(row["recipe.uses_facility"], ",")

	local output_image
	if production.outputs and production.outputs[1] then
		output_image = production.outputs[1].image
	end

	local parsed_mats = {}
	if production.materials then
		for _, mat in ipairs(production.materials) do
			parsed_mats[#parsed_mats + 1] = {
				name = mat.name,
				pic = mat.image and mat.image:gsub('%.png$', ''),
				quantity = tonumber(mat.quantity),
			}
		end
	end

	local parsed_tools = {}
	local raw_tools = row["recipe.uses_tool"]
	if raw_tools then
		for _, t in ipairs(raw_tools) do
			parsed_tools[#parsed_tools + 1] = { name = t }
		end
	end

	return {
		page_name = row["recipe.page_name"],
		page_name_sub = row["recipe.page_name_sub"],
		image = getLastElement(row["infobox_scenery.image"])
			or output_image
			or ("File:"..row["recipe.page_name"]..".png"),
		members = row["recipe.is_members_only"] or getLastElement(row["infobox_scenery.is_members_only"]),
		level = con_level,
		experience = con_exp,
		materials = parsed_mats,
		tools = parsed_tools,
		facilities = fac_str,
		name = production.name or row["recipe.page_name"],
	}
end

local function query_construction_recipes(args)
	local all_data = {}
	local limit = tonumber(args.limit) or 5000
	local category = args.category
	local notcategory = args.notcategory

	for offset = 0, limit, BATCH_SIZE do
		local b = bucket("recipe")
			.select(
				"recipe.production_json",
				"infobox_scenery.image",
				"infobox_scenery.is_members_only",
				"infobox_scenery.scenery_location",
				"recipe.page_name",
				"recipe.page_name_sub",
				"recipe.uses_tool",
				"recipe.uses_facility",
                "recipe.is_members_only"
			)
			.join("infobox_scenery", "recipe.page_name", "infobox_scenery.page_name")
			.where("recipe.uses_skill", SKILL_NAME)
			.where(bucket.Not("Category:Flatpacks"))
			.where("infobox_scenery.scenery_location", "[[Player-owned house]]")
			.orderBy("recipe.page_name", "ASC")
			.limit(BATCH_SIZE)
			.offset(offset)

		if category then
			b.where("Category:" .. category)
		end

		if notcategory then
			b.where(bucket.Not("Category:" .. notcategory))
		end

		local rows = b.run()
		if not rows or #rows == 0 then break end

		for _, row in ipairs(rows) do
			local item = parse_recipe(row)
			if item then
				all_data[#all_data + 1] = item
			end
		end
	end

	return all_data
end

local function build_row(item)
	local pic = item.image:gsub('^File:', ''):gsub('%.[gp][in][fg]$', '')
	local image
	if item.image:match('%.gif$') then
		image = string.format(
			'<span class="pic-link">[[File:%s.gif|link=%s|35x35px]]</span>',
			pic, item.page_name, item.page_name
		)
	else
		image = plinkp(item.page_name, { size = "35x35", pic = pic })
	end

	local tools = format_plinked_list(item.tools)

	local tr = mw.html.create('tr')
	tr:td(image)
		:td(item.name_cell)
		:td(members_icon(item.members))
		:td(scp(SKILL_NAME, item.level))
		:td(item.experience or "?")
		:td(format_plinked_list(item.materials))
		:IF(tools == "")
			:na()
		:ELSE()
			:td(tools)
		:END()
		:td('[[' .. item.facilities .. ']]')

	return tr
end

local function build_header()
	local tbl = mw.html.create('table')
		:addClass('wikitable sortable sticky-header align-center-1 align-center-3')

	tbl:tr()
		:th({ 'Name', attr = {'colspan', '2'} })
		:th(members_icon('both'))
		:th('[[File:Construction-icon.png|21px|link=Construction]] Level')
		:th('XP')
		:th('Materials')
		:th('Tools')
		:th('Facility')

	return tbl
end

function p.main(frame)
	local args = getArgs(frame)
	return p._main(args)
end

function p._main(args)
	local data = query_construction_recipes(args)

	local condense_nails = args.condense_nails ~= "no"

	local seen = {}
	local page_count = {}
	local items = {}
	for _, item in ipairs(data) do
		local key, materials = normalize_and_key(item, condense_nails)
		item.materials = materials
		if not seen[key] then
			seen[key] = true
			page_count[item.page_name] = (page_count[item.page_name] or 0) + 1
			items[#items + 1] = item
		end
	end

	table.sort(items, function(a, b)
		local lvl_a = tonumber(a.level) or 0
		local lvl_b = tonumber(b.level) or 0
		if lvl_a ~= lvl_b then
			return lvl_a < lvl_b
		end
		return (a.name or ""):lower() < (b.name or ""):lower()
	end)

	local tbl = build_header()
	for _, item in ipairs(items) do
		local src = item.page_name_sub or item.name
		local title = mw.text.split(src, '#')[1]
		local subtitle = mw.text.split(src, '#')[2]
		if title == subtitle then subtitle = nil end
		item.name_cell = string.format("[[%s|%s]]", item.page_name, title)
		if subtitle and page_count[item.page_name] > 1 then
			item.name_cell = item.name_cell .. ' <small>(' .. subtitle:gsub('_', ' ') .. ')</small>'
		end
		tbl:node(build_row(item))
	end

	return tbl
end

return p