Module:Chem box

From Space Station 14 Wiki
Revision as of 06:53, 27 May 2025 by Aliser (talk | contribs) (added a-z sorting to fns generating reagents of group/all reagents)
Module documentation
View or edit this documentation (about module documentation)
Uses JSON data
This module uses JSON data pages:

Implements {{chem box}}.


local p = {} --p stands for package
local getArgs = require("Module:Arguments").getArgs
local yesNo = require("Module:Yesno")
local reagent_card = require('Module:Chemistry').reagent_card

local chem_data = mw.loadJsonData("Module:Chem box/chem data.json")

local current_frame = mw.getCurrentFrame()

-- ====================

local function numeric_table_length(t)
	local count = 0
	for _ in ipairs(t) do
		count = count + 1
	end
	return count
end

local function table_length(t)
	local count = 0
	for _ in pairs(t) do
		count = count + 1
	end
	return count
end

local function table_has_value(tab, val)
	for _, value in ipairs(tab) do
		if value == val then
			return true
		end
	end

	return false
end

local function assert_value_not_nil(value, error_message)
	if value == nil then
		if error_message == nil then
			error("value is nil")
		else
			error(error_message)
		end
	end
end

-- Makes the first letter uppercase.
-- Source: https://stackoverflow.com/a/2421746
local function capitalize(str)
	return (str:gsub("^%l", string.upper))
end

local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
	if value then
		return valueToReturnIfTrue
	else
		error(errorMessageOnFalse)
	end
end

local function find_first_numeric_table_item_matching_condition(table, condition)
	for i, item in ipairs(table) do
		if condition(item, i, table) then
			return item
		end
	end
end

-- table concat function because ofcourse fucking table.concat doesn't work
local function concat_numberic_table(tbl, sep)
	local temp_table = {}
	for i = 1, numeric_table_length(tbl) do
		table.insert(temp_table, tbl[i])
	end

	return table.concat(temp_table, sep)
end

local function ternary_strict(valueToCheck, valueIfTrue, valueIfFalse)
	if valueToCheck == true then
		return valueIfTrue
	else
		return valueIfFalse
	end
end

local function table_map(tbl, mapFn)
    local res = {}
    for k, v in pairs(tbl) do
        table.insert(res, mapFn(v, k, tbl))
    end
    return res
end

local function num_table_reduce(tbl, reduceFn, initial_value)
    local out = initial_value

    for i, v in ipairs(tbl) do
        out = reduceFn(out, v, i, tbl)
    end

    return out
end

local function table_filter(tbl, filterFn)
    local out = {}

    for k, v in pairs(tbl) do
        if filterFn(v, k, tbl) then out[k] = v end
    end

    return out
end


-- Sorts a table into a new table.
-- An alternative to cases such as when table.sort is not working,
-- like when trying to sort JSON tables (thanks fuckass lua).
local function table_to_sorted(tbl, sortfn)
    local keys = {}

    for key, _ in pairs(tbl) do
        table.insert(keys, key)
    end

    table.sort(keys, function(keyA, keyB) return sortfn(tbl[keyA], tbl[keyB]) end)

    local t2 = {}

    for _, key in ipairs(keys) do
        table.insert(t2, tbl[key])
    end

    return t2
end

-- ====================

-- Lookups reagent by ID or name.
--
-- Raises an error if no reagent was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_reagent(query, no_error)
	local query_lower = string.lower(query)

	for _, reagent in ipairs(chem_data) do
		assert_value_not_nil(reagent.id)
		assert_value_not_nil(reagent.name)

		if reagent.id == query then
			return reagent
		end
		if string.lower(reagent.name) == query_lower then
			return reagent
		end
	end

	return passthrough_assert_true(no_error, nil, "failed to lookup reagent: no match was found")
end

local function get_sort_reagent_az_final_comparator(map_of_reagent_name_or_id_to_order, a, b)
	return function (a, b)
		return map_of_reagent_name_or_id_to_order[string.lower(a.name or a.id)]
			< map_of_reagent_name_or_id_to_order[string.lower(b.name or b.id)]
	end 
end

-- Sorts an array of reagents in place by name (or ID if no name), a to z.
-- @param reagents Reagents to sort.
-- @param to_sorted Whether to create a new sorted table instead of sorting in place.
local function sort_reagents_az(reagents, to_sorted)
	local reagents_names_or_ids = table_map(reagents, function (reagent)
		return string.lower(reagent.name or reagent.id)
	end)
	table.sort(reagents_names_or_ids)

	local map_of_reagent_name_or_id_to_order = num_table_reduce(reagents_names_or_ids, function (accum, value, i)
		accum[value] = i

		return accum
	end, {})

	if to_sorted then
		return table_to_sorted(reagents, get_sort_reagent_az_final_comparator(map_of_reagent_name_or_id_to_order))
	else
		table.sort(reagents, get_sort_reagent_az_final_comparator(map_of_reagent_name_or_id_to_order))
	end
end

-- ====================

function p.generate_chem_box(frame)
	local args = getArgs(frame)

	local query = args[1]
	assert_value_not_nil(query, "failed to generate chem box: query was not provided")

	-- ===================

	local reagent = p.lookup_reagent(query)
	assert_value_not_nil(reagent.color)
	assert_value_not_nil(reagent.name)
	assert_value_not_nil(reagent.recipes)
	assert_value_not_nil(reagent.effectStrings)
	assert_value_not_nil(reagent.description)
	assert_value_not_nil(reagent.physicalDescription)
	assert_value_not_nil(reagent.textColorTheme)

	local recipes_container_el = mw.html.create("div")
	if numeric_table_length(reagent.recipes) == 0 then
		local no_recipes_text_el = mw.html.create("span"):css("color", "gray"):node("No recipes")

		recipes_container_el:node(no_recipes_text_el)
	else
		for _, recipe in ipairs(reagent.recipes) do
			assert_value_not_nil(recipe.id)
			assert_value_not_nil(recipe.reactants)
			assert_value_not_nil(recipe.products)

			local recipe_box_template_args = {}
			recipe_box_template_args.name = recipe.id

			-- a list of catalist reactants.
			-- contains "id" and "amount"
			local catalyst_reactants = {}

			for reactant_i, reactant in ipairs(recipe.reactants) do
				local reactant_name = capitalize(reactant.name)
				local reactant_amount = reactant.count
				local reactant_is_catalyst = reactant.isCatalyst or false

				local label = reactant_name
				local link = reactant.id

				if reactant_is_catalyst then
					table.insert(catalyst_reactants, {
						name = reactant_name,
						id = reactant_id,
						amount = reactant_amount,
					})
				end

				local component_el = current_frame:expandTemplate({
					title = "Recipe Component",
					args = {
						item = label,
						amount = reactant_amount,
						itemlink = link
						-- TODO
						-- image = ""
					},
				})

				recipe_box_template_args["component-" .. reactant_i] = component_el
			end

			local temperature_args = {}
			if recipe.minTemp ~= nil or recipe.maxTemp ~= nil then
				local temperature_string = ""
				if recipe.minTemp ~= nil then
					temperature_string = tostring(recipe.minTemp) .. "k "
				end
				temperature_string = temperature_string .. "<"
				if recipe.maxTemp ~= nil then
					temperature_string = temperature_string .. " " .. tostring(recipe.maxTemp) .. "k"
				end
				temperature_args = {temperature = temperature_string}
			end
			local required_mixer = "Beaker"
			if recipe.requiredMixerCategories ~= nil then
				required_mixer = recipe.requiredMixerCategories[1]
			end
			recipe_box_template_args.transformer = current_frame:expandTemplate({
				title = required_mixer,
				args = temperature_args
			})

			for product_i, product in ipairs(recipe.products) do
				local component_el = current_frame:expandTemplate({
					title = "Recipe Component",
					args = {
						item = capitalize(product.name),
						amount = product.count,
						-- TODO
						-- image = ""
					},
				})

				recipe_box_template_args["result-" .. product_i] = component_el
			end

			-- add catalysts as products, if any
			for catalyst_product_i, catalyst_product_entry in ipairs(catalyst_reactants) do
				local product_name = catalyst_product_entry.name
				local product_amount = catalyst_product_entry.amount

				local label = current_frame:expandTemplate({
					title = "Tooltip",
					args = {
						'<sup class="quickbox-catalyst-note">+</sup>',
						'<span class="quickbox-catalyst-note-tooltip">This product is a catalyst reactant, remaining after the reaction.</span>',
					},
				}) .. product_name

				local component_el = current_frame:expandTemplate({
					title = "Recipe Component",
					args = {
						item = label,
						amount = product_amount,
						-- TODO
						-- image = ""
					},
				})

				local index = numeric_table_length(recipe.products) + catalyst_product_i
				recipe_box_template_args["result-" .. index] = component_el
			end

			local recipes_el = current_frame:expandTemplate({
				title = "Recipe Box",
				args = recipe_box_template_args,
			})

			recipes_container_el:node(recipes_el)
		end
	end

	local effects = concat_numberic_table(reagent.effectStrings, "")

	return reagent_card {
		id = reagent.id,
		color = reagent.color,
		name = capitalize(reagent.name),
		recipes = tostring(recipes_container_el:allDone()),
		metabolisms = effects,
		desc = reagent.description,
		physicalDesc = reagent.physicalDescription,

	}
end

function p.generate_chem_boxes_for_all_reagents()
	local container_el = mw.html.create("div"):addClass("reagents-list")

	-- sort to a new obj cuz chem data is a json loaded table and is not enumerable by table.sort
	local reagents_sorted = sort_reagents_az(chem_data, true)

	for _, reagent in ipairs(reagents_sorted) do
		assert_value_not_nil(reagent.id)

		container_el:node(p.generate_chem_box({
			reagent.id,
		}))
	end

	return container_el:allDone()
end

function p.generate_chem_boxes_for_group(frame)
	local args = getArgs(frame)
	local query = args[1]
	assert_value_not_nil(query)
	
	-- ========
	
	local query_lc = string.lower(query)
	local container_el = mw.html.create("div"):addClass("reagents-list")

	local reagents = table_filter(chem_data, function (reagent)
		return reagent.group 
			and string.lower(reagent.group) == query_lc
	end)
	-- sort to a new obj cuz chem data is a json loaded table and is not enumerable by table.sort,
	-- even when filtered smh
	reagents = sort_reagents_az(reagents, true)

	for _, reagent in ipairs(reagents) do
		container_el:node(p.generate_chem_box({
			reagent.id,
		}))
	end

	return container_el:allDone()
end

return p