Module:Database

From Cassette Beasts

Documentation for this module may be created at Module:Database/doc

local p = {}

local db = mw.loadData("Module:Database/data")

-- Private helpers

function table.empty(t)
	for _,_ in pairs(t) do
		return false
	end
	return true
end

function table.clone(t)
	local result = {}
	for k,v in pairs(t) do
		result[k] = v
	end
	return result
end

function table.values(t)
	local result = {}
	for _,v in pairs(t) do
		table.insert(result, v)
	end
	return result
end

function string.split(s, delim)
	local result = {}
	if s == "" then
		return result
	end
	local current = ""
	for i = 1, string.len(s) do
		if string.sub(s, i, i + string.len(delim) - 1) == delim then
			i = i + string.len(delim) - 1
			table.insert(result, current)
			current = ""
		else
			current = current .. string.sub(s, i, i)
		end
	end
	table.insert(result, current)
	return result
end

function species_sort_index(species)
	local i = species.bestiary_index
	if i == nil or i < 0 then
		return 99999
	else
		return i
	end
end

function cmp_i(a, b)
	if type(a) == "string" then
		a = string.lower(a)
	end
	if type(b) == "string" then
		b = string.lower(b)
	end
	return a < b
end

function cmp_species(a, b)
	local ai = species_sort_index(a)
	local bi = species_sort_index(b)
	if ai == bi then
		return string.lower(a.name) < string.lower(b.name)
	end
	return ai < bi
end

function cmp_moves(a, b)
	return string.lower(a.name) < string.lower(b.name)
end

function cmp_status_effects(a, b)
	return string.lower(a.name) < string.lower(b.name)
end

function get_species(id)
	if tonumber(id) ~= nil then
		id = tonumber(id)
	end
	if type(id) == "number" then
		return db.species.by_index[id] or db.species.unknown
	else
		return db.species.by_name[string.lower(id)] or db.species.unknown
	end
end

function describe_evolution_branch(evo)
	if evo.specialization ~= "" then
		return evo.specialization
	elseif evo.required_type_override ~= "" then
		return "[[" .. evo.required_type_override .. "]] Bootleg"
	elseif evo.required_move ~= "" then
		return "[[" ..evo.required_move .. "]] Sticker"
	elseif evo.min_hour == 18 and evo.max_hour == 6 then
		return "Night"
	elseif evo.min_hour == 6 and evo.max_hour == 18 then
		return "Day"
	else
		return "Default"
	end
end

function get_move(id)
	return db.moves.by_name[string.lower(id)] or db.moves.unknown
end

function get_type(id)
	return db.types.by_name[string.lower(id)] or db.types.typeless
end

function get_status_effect(id)
	return db.status_effects.by_name[string.lower(id)] or db.status_effects.unknown
end

local getters = {
	species = get_species,
	move = get_move,
	["type"] = get_type,
	status_effect = get_status_effect
}

function subscript(value, path, path_start)
	while value ~= nil and path[path_start] ~= nil do
		if type(value) ~= "table" then
			return nil
		end
		local key = path[path_start]
		if tonumber(key) ~= nil then
			value = value[tonumber(key)]
		else
			value = value[key]
		end
		path_start = path_start + 1
	end
	return value
end

function flatten_template_args(args)
	for k,v in pairs(args) do
		if type(v) == "table" and v.name then
			args[k] = v.name
		end
		args[k] = tostring(v)
	end
end

function textify(frame, value)
	if frame.args.sorted and type(value) == "table" then
		value = table.values(value)
		table.sort(value, cmp_i)
	elseif frame.args.sort_by and type(value) == "table" then
		value = table.values(value)
		table.sort(value, function(a, b)
			return cmp_i(a[frame.args.sort_by], b[frame.args.sort_by])
		end)
	end
	if frame.args["if_" .. tostring(value)] ~= nil then
		return frame.args["if_" .. tostring(value)]
	elseif type(value) == "number" and frame.args.if_number ~= nil then
		return string.format(frame.args.if_number, value)
	elseif type(value) == "table" and frame.args.if_empty ~= nil and table.empty(value) then
		return frame.args.if_empty
	elseif type(value) == "table" and (frame.args.delim or frame.args["foreach"]) then
		local delim = frame.args.delim or ""
		if delim == "," then delim = delim .. " " end
		local result = ""
		local i = 1
		local iter
		if value[1] then iter = ipairs else iter = pairs end
		for key,elem in iter(value) do
			if i > 1 then
				result = result .. delim
			end
			if frame.args.subscript then
				elem = subscript(elem, string.split(frame.args.subscript, "."), 1)
			end
			if frame.args["foreach"] then
				local args
				if type(elem) == "table" then
					args = table.clone(elem)
				else
					args = { elem, first = i == 1 }
				end
				if frame.args.key_param then
					local s = key
					if frame.args.key_param_format then
						s = string.format(frame.args.key_param_format, s)
					end
					args[frame.args.key_param] = s
				end
				if frame.args.pass_through then
					args[frame.args.pass_through] = frame.args[frame.args.pass_through]
				end
				flatten_template_args(args)
				elem = frame:expandTemplate{title = frame.args["foreach"], args = args}
			else
				elem = textify(frame, elem)
			end
			result = result .. elem
			i = i + 1
		end
		return result
	end
	if type(frame.args.format) == "string" then
		value = string.format(frame.args.format, value)
	end
	if type(value) == "table" and value.name ~= nil then
		value = value.name
	end

	if value == nil or value == false then
		return ""
	elseif type(value) == "string" or type(value) == "number" or type(value) == "boolean" then
		return tostring(value)
	end
	return "&lt;" .. tostring(value) .. "&gt;"
end

-- Public functions for use with #invoke

function p.get_species(frame)
	local species = get_species(frame.args[1])
	return textify(frame, subscript(species, frame.args, 2))
end

function p.get_prev_species(frame)
	local species = get_species(frame.args[1])
	if species.bestiary_index ~= nil then
		species = get_species(species.bestiary_index - 1)
	end
	return textify(frame, subscript(species, frame.args, 2))
end

function p.get_next_species(frame)
	local species = get_species(frame.args[1])
	if species.bestiary_index ~= nil then
		species = get_species(species.bestiary_index + 1)
	end
	return textify(frame, subscript(species, frame.args, 2))
end

function p.species_has_family(frame)
	local species = get_species(frame.args[1])
	local result = not table.empty(species.evolves_from) or not table.empty(species.evolves_to)
	return textify(frame, subscript(result, frame.args, 2))
end

function p.get_species_compatible_moves(frame)
	local result_set = {}
	local species = get_species(frame.args[1])
	local tags = species.moves.tags
	if frame.args.always_compatible then
		tags = species.moves.core_tags
	end
	for _,tag in ipairs(tags) do
		if db.moves.by_tag[tag] then
			for _,move in ipairs(db.moves.by_tag[tag]) do
				result_set[move.name] = move
			end
		end
	end
	local result = table.values(result_set)
	table.sort(result, cmp_moves)
	return textify(frame, result)
end

function p.get_tag_moves(frame)
	local result = {}
	local tag = frame.args[1]
	local for_bootleg = frame.args.for_bootleg
	if db.moves.by_tag[tag] then
		for _,move in ipairs(db.moves.by_tag[tag]) do
			if not for_bootleg or move.camouflage ~= tag then
				table.insert(result, move)
			end
		end
	end
	return textify(frame, result)
end

function p.get_species_move_learn_method(frame)
	local species = get_species(frame.args[1])
	local move = get_move(frame.args[2])
	for _,m in ipairs(species.moves.initial) do
		if move.name == m then
			return textify(frame, "initial")
		end
	end
	for i,m in ipairs(species.moves.upgrades) do
		if move.name == m then
			return textify(frame, i)
		end
	end
	if string.lower(species.elemental_type) ~= move.camouflage then
		local tags = {}
		for _,tag in ipairs(species.moves.tags) do
			tags[tag] = true
		end
		for _,tag in ipairs(move.tags) do
			if tags[tag] then
				return textify(frame, "compatible")
			end
		end
	end
	return textify(frame, "incompatible")
end

function p.get_species_list(frame)
	-- If frame.args includes either 'type' as a key, it returns only the
	-- species with that type. Otherwise it returns all species.
	-- Use this with foreach= and/or delim= in frame.args.
	local result = {}
	if frame.args["type"] then
		local t = get_type(frame.args["type"])
		result = table.values(db.species.by_type[string.lower(t.name)] or {})
		table.sort(result, cmp_species)
	else
		for i = db.species.min_index, db.species.max_index do
			if db.species.by_index[i] then
				table.insert(result, db.species.by_index[i])
			else
				table.insert(result, db.species.unknown)
			end
		end
		for _,s in ipairs(db.species.unnumbered) do
			table.insert(result, s)
		end
	end
	return textify(frame, result)
end

function p.describe_evolution_branch(frame)
	local from_species = get_species(frame.args[1])
	local to_species = get_species(frame.args[2])
	if from_species == db.species.unknown or to_species == db.species.unknown then
		return nil
	end
	for _,evo in ipairs(from_species.evolves_to) do
		if evo.evolved_form == to_species.name then
			return describe_evolution_branch(evo)
		end
	end
	return nil
end

function p.get_move(frame)
	local move = get_move(frame.args[1])
	return textify(frame, subscript(move, frame.args, 2))
end

function p.get_move_compatible_species(frame)
	local result_set = {}
	local move = get_move(frame.args[1])
	for _,tag in ipairs(move.tags) do
		if db.species.by_tag[tag] then
			for _,species in ipairs(db.species.by_tag[tag]) do
				if string.lower(species.elemental_type) ~= move.camouflage then
					result_set[species.name] = true
				end
			end
		end
	end
	local result = {}
	for species,_ in pairs(result_set) do
		table.insert(result, species)
	end
	table.sort(result)
	return textify(frame, result)
end

function p.get_move_type_tags(frame)
	local result = {}
	local move = get_move(frame.args[1])
	for _,tag in ipairs(move.tags) do
		if db.types.by_name[tag] and tag ~= move.camouflage then
			table.insert(result, db.types.by_name[tag].name)
		end
	end
	table.sort(result)
	return textify(frame, result)
end

function p.get_move_list(frame)
	-- If frame.args includes either 'type' as a key, it returns only the
	-- moves with that type. Otherwise it returns all moves.
	-- Use this with foreach= and/or delim= in frame.args.
	local result = {}
	if frame.args["type"] then
		local t = get_type(frame.args["type"])
		result = table.values(db.moves.by_type[string.lower(t.name)] or {})
	else
		result = table.values(db.moves.by_name)
	end
	table.sort(result, cmp_moves)
	return textify(frame, result)
end

function p.get_type(frame)
	local t = get_type(frame.args[1])
	local result = subscript(t, frame.args, 2)
	return textify(frame, result)
end

function p.get_type_list(frame)
	return textify(frame, db.types.ordered)
end

function p.get_reaction(frame)
	local atype = get_type(frame.args[1])
	local dtype = get_type(frame.args[2])
	local reaction = atype.inflicts[dtype.name]
	return textify(frame, subscript(reaction, frame.args, 3))
end

function p.get_status_effect(frame)
	local s = get_status_effect(frame.args[1])
	local result = subscript(s, frame.args, 2)
	return textify(frame, result)
end

function p.get_status_effect_list(frame)
	local list = table.values(db.status_effects.by_name)
	table.sort(list, cmp_status_effects)
	return textify(frame, list)
end

function p.get_link(frame)
	local table_name = frame.args[1]
	local getter = getters[table_name]
	if getter == nil then
		return frame:preprocess("[[INVALID]]")
	end
	local id = frame.args[2]
	local object = getter(id)
	for k,g in pairs(getters) do
		if k ~= table_name then
			local unknown = g("Unknown")
			local o = g(id)
			if o ~= unknown then
				return frame:preprocess("[[" .. object.name .. " (" .. table_name ..")|" .. object.name .. "]]")
			end
		end
	end
	return frame:preprocess("[[" .. object.name .. "]]")
end

function p.exists(frame)
	local getter = getters[frame.args[1]]
	local object = getter(frame.args[2])
	local unknown = getter("Unknown")
	return textify(frame, object ~= unknown)
end

return p