Module:Township

From Melvor Idle
Revision as of 03:21, 26 October 2022 by Auron956 (talk | contribs) (User:Gau Cho)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

local Shared = require('Module:Shared')
local Icons = require('Module:Icons')

-- Data Module
local DataDemo = require('Module:GauTest/DataDemo')
local DataFull = require('Module:GauTest/DataFull')
local DataTotH = require('Module:GauTest/DataTotH')

local Namespaces = {
	melvorD = DataDemo,
	melvorF = DataFull,
	melvorTotH = DataTotH
}

local Data = {}


-- For a table that is indexed but uses a key, use this function to find the correct element
function Data.tableMatch(tabl, property, value)
	for _, elem in ipairs(tabl) do
		if elem[property] == value then
			return elem
		end
	end
	return nil
end

-- For a table that is indexed but uses a key, use this function to find the correct element, when there are several duplicates elements
function Data.tableMatches(tabl, property, value)
	local matches = {}
	for _, elem in ipairs(tabl) do
		if elem[property] == value then
			table.insert(matches, elem)
		end
	end
	return matches
end

-- Separates the namespace and id of a string
-- e.g. 'melvorD:Coal_Ore' will return {namespace='melvorD', id='Coal_Ore'}
-- e.g. 'Coal_Ore' will return {namespace=nil, id='Coal_Ore'}
function Data.splitID(text)
	local split = Shared.splitString(text, ':')
	local target_namespace = nil
	local target_id = nil
	if #split == 2 then
		return {namespace=split[1], id=split[2]}
	elseif #split == 1 then
		return {id=split[1]}
	else
		return nil
	end
end

-- Returns the namespace name (eventually we should use an icon?)
function Data.PLACEHOLDER_NAMESPACE_ICON(namespace)
	local namespaces = {
		melvorD = 'Demo',
		melvorF = 'Full',
		melvorTotH = 'TotH'
	}
	return namespaces[namespace]
end

Data.Item = {}

-- Get all the items with a property equal to value
function Data.Item.Match(property, value)
	local items = {}
	for namespace, data in pairs(Namespaces) do
		for k, item in pairs(data.data.items) do
			if item[property] == value then
				local itemcopy = Shared.clone(item)
				itemcopy._namespace = namespace
				table.insert(items, itemcopy)
			end
		end
	end
	return items
end

-- Get item by id
function Data.Item.ByID(id)
	local target = Data.splitID(id)
	for namespace, data in pairs(Namespaces) do
		if target.namespace == nil or namespace == target.namespace then
			for k, item in pairs(data.data.items) do
				if item.id == target.id then
					local itemcopy = Shared.clone(item)
					itemcopy._namespace = namespace
					return itemcopy
				end
			end
		end
	end
	return nil
end

-- Returns the recipe for the item of a desired skill.
function Data.Item.FindRecipes(itemid, skill)
	-- the key name for each skill in the json file
	local skill_recipe_keys = {
		Woodcutting = {recipes='trees', productID='productId'}, -- lowercase "d"
		Fishing = {recipes='fish', productID='productId'}, -- lowercase "d"
		Cooking = {recipes='recipes', productID='productID'},
		Mining = {recipes='rockData', productID='productId'}, -- lowercase "d"
		Smithing = {recipes='recipes', productID='productID'},
		Farming = {recipes='recipes', productID='productId'}, -- lowercase "d"
		Summoning = {recipes='recipes', productID='productID'},
		Fletching = {recipes='recipes', productID='productID'},
		Crafting = {recipes='recipes', productID='productID'},
		Runecrafting = {recipes='recipes', productID='productID'},
		Herblore = {recipes='recipes', productID='potionIDs'} -- Special case potions I-IV
		--[[ Excluded skills:
			Attack, Strength, Defence, Magic, Ranged, Prayer, Slayer
			Thieving, Agility, Astrology, Firemaking, Township (not items)]]
		}

	local item = Data.splitID(itemid)
	local results = {}
	
	-- Find the recipe at data.data.skillData -> SKILL.data.KEY
	for namespace, data in pairs(Namespaces) do
		-- We match multiple entries because there's a bug - melvorF -> skillData Farming has two duplicate entries in the array with differing info
		local Skill_matches = Data.tableMatches(data.data.skillData, 'skillID', 'melvorD:'..skill)
		for _, Skill in ipairs(Skill_matches) do
			local key = skill_recipe_keys[skill]
			if Skill ~= nil and Skill.data ~= nil and Skill.data[key.recipes] ~= nil then
				for _, recipe in ipairs(Skill.data[key.recipes]) do
					-- Check if id matches
					if skill == 'Herblore' then
						-- Iterate over the 4 potion tiers
						for _, potion in ipairs(recipe[key.productID]) do
							-- Same as below
							local recipe_id = Data.splitID(potion)
							if item.id == recipe_id.id then
								local recipecopy = Shared.clone(recipe)
								recipecopy._namespace = namespace
								table.insert(results, recipecopy)
							end
						end
					else
						-- Same as above
						local recipe_id = Data.splitID(recipe[key.productID])
						if item.id == recipe_id.id then
							local recipecopy = Shared.clone(recipe)
							recipecopy._namespace = namespace
							table.insert(results, recipecopy)
						end
					end
				end
			end
		end
	end
	return results
end

Data.Township = {}
-- Returns a list of all the Township resources
function Data.Township.Resources()
	-- Input for Module:Icons.Icon to generate the correct resource image
	local resource_icons = {
		GP = {coins, notext=true, nolink=true},
		Food = {'Raw Beef', type='item', notext=true, nolink=true},
		Wood = {'Wood', type='resource', notext=true, nolink=true},
		Stone = {'Stone', type='resource', notext=true, nolink=true},
		Ore = {'Iron Ore', type='rock', notext=true, nolink=true},
		Coal = {'Coal', type='resource', notext=true, nolink=true},
		Bar = {'Iron Bar', type='item', notext=true, nolink=true},
		Herbs = {'Garum Herb', type='item', notext=true, nolink=true},
		Rune_Essence = {'Rune Essence', type='item', notext=true, nolink=true},
		Leather = {'Leather', type='item', notext=true, nolink=true},
		Potions = {'Potion', type='resource', notext=true, nolink=true},
		Planks = {'Planks', type='resource', notext=true, nolink=true},
		Clothing = {'Leather Body', type='item', notext=true, nolink=true}
	}

	-- Save all the resources to a table
	local resources = {}
	for namespace, data in pairs(Namespaces) do
		local Township = Data.tableMatch(data.data.skillData, 'skillID', 'melvorD:Township')
		if Township ~= nil and Township.data.resources ~= nil then
			for _, resource in ipairs(Township.data.resources) do
				local resourcecopy = Shared.clone(resource)
				resourcecopy._namespace = namespace
				resourcecopy._icon = resource_icons[resourcecopy.id]
				table.insert(resources, resourcecopy)
			end
		end
	end

	-- Sort the resources according to the canonical sort order
	-- We get the sort order from data.skillData -> Township.data.resourceDisplayOrder
	local resources_sorted = {}
	for namespace, data in pairs(Namespaces) do
		local Township = Data.tableMatch(data.data.skillData, 'skillID', 'melvorD:Township')
		if Township ~= nil and Township.data.resourceDisplayOrder ~= nil then
			for _, resourceOrder in ipairs(Township.data.resourceDisplayOrder[1].ids) do
				resourceOrder_split = Data.splitID(resourceOrder)
				local target_resource = Data.tableMatch(resources, 'id', resourceOrder_split.id)
				table.insert(resources_sorted, target_resource)
			end
		end
	end
	
	return resources_sorted
end

-- Returns a list of all the Township resources along with the Trader's trade ratios
function Data.Township.Trader()
	-- Get the list of resources
	local resources = Data.Township.Resources()
	
	-- Get the list of convertable items, and calculate the exchange rate
	-- Also inserts the resource's assocaited skill
	-- See TownshipResource.buildResourceItemConversions for the list of valid items
	for _, resource in ipairs(resources) do
		resource.itemConversions = {}
		if resource.id == 'GP' then
			-- No conversions
		elseif resource.id == 'Food' then
			resource._skill = 'Cooking'
			for _, food in ipairs(Data.Item.Match('type', 'Food')) do
				if (not string.match(food.id, '_Perfect')) and food.category ~= 'Farming' and (not food.ignoreCompletion) then
					table.insert(resource.itemConversions, food)
				end
			end
		elseif resource.id == 'Wood' or resource.id == 'Planks' then
			resource._skill = 'Woodcutting'
			resource.itemConversions = Data.Item.Match('type', 'Logs')
		elseif resource.id == 'Stone' or resource.id == 'Ore' then
			resource._skill = 'Mining'
			for _, ore in ipairs(Data.Item.Match('type', 'Ore')) do
				if not string.match(ore.id, 'Meteorite_Ore') then
					table.insert(resource.itemConversions, ore)
				end
			end
		elseif resource.id == 'Coal' then
			resource._skill = 'Mining'
			local coal = 'melvorD:Coal_Ore'
			table.insert(resource.itemConversions, Data.Item.ByID(coal))
		elseif resource.id == 'Bar' then
			resource._skill = 'Mining'
			for _, bar in ipairs(Data.Item.Match('type', 'Ore')) do
				if not string.match(bar.id, 'Meteorite_Bar') then
					table.insert(resource.itemConversions, bar)
				end
			end
		elseif resource.id == 'Herbs' then
			resource._skill = 'Farming'
			resource.itemConversions = Data.Item.Match('type', 'Herb')
		elseif resource.id == 'Rune_Essence' then
			resource._skill = 'Mining'
			local ressence = 'melvorD:Rune_Essence'
			local pessence = 'melvorTotH:Pure_Essence'
			table.insert(resource.itemConversions, Data.Item.ByID(ressence))
			table.insert(resource.itemConversions, Data.Item.ByID(pessence))
		elseif resource.id == 'Leather' then
			resource._skill = 'Crafting' -- Placeholder - actually no related skill to obtain
			local leather = 'Leather'
			table.insert(resource.itemConversions, Data.Item.ByID(leather))
		elseif resource.id == 'Potions' then
			resource._skill = 'Herblore'
			for _, potion in ipairs(Data.Item.Match('type', 'Potion')) do
				if string.match(potion.id, '_IV') then
					table.insert(resource.itemConversions, potion)
				end
			end
		elseif resource.id == 'Clothing' then
			resource._skill = 'Crafting'
			local matches = {}
			table.insert(matches, Data.Item.Match('tier', 'Leather'))
			table.insert(matches, Data.Item.Match('tier', 'Hard Leather'))
			table.insert(matches, Data.Item.Match('tier', 'Dragonhide'))
			table.insert(matches, Data.Item.Match('tier', 'Elderwood'))
			table.insert(matches, Data.Item.Match('tier', 'Revenant'))
			table.insert(matches, Data.Item.Match('tier', 'Carrion'))
			for _, match in ipairs(matches) do
				for _, v in ipairs(match) do
					table.insert(resource.itemConversions, v)
				end
			end
		end
	end
	
	-- Calculate the conversion ratios
	-- See TownshipResource.getBaseConvertToTownshipRatio and TownshipResource.getBaseConvertFromTownshipRatio for the conversion prices
	for _, resource in ipairs(resources) do
		if resource.id == 'Food' then
			for _, item in ipairs(resource.itemConversions) do
				item.to = math.max(math.floor(1000/(item.healsFor*10)), 2)
				item.from = item.healsFor*5*6
			end
		elseif resource.id == 'Planks' then
			for _, item in ipairs(resource.itemConversions) do
				item.to = math.max(math.floor(3000/math.max(item.sellsFor, 1)), 2)
				item.from = math.max(math.ceil(item.sellsFor/2)*6, 1);
			end
		elseif resource.id == 'Rune_Essence' then
			for _, item in ipairs(resource.itemConversions) do
				item.to = 5
				item.from = (item.sellsFor+1)*10*6
			end
		elseif resource.id == 'Leather' then
			for _, item in ipairs(resource.itemConversions) do
				item.to = 20
				item.from = 20*6
			end
		else
			for _, item in ipairs(resource.itemConversions) do
		        item.to = math.max(math.floor(1000/math.max(item.sellsFor, 1)), 2)
		    	item.from = math.max(item.sellsFor * 6, 1)
			end
		end
	end
	return resources
end

-- Builds the table of trader items
function Data.Township.getTraderTable(frame)
	local resources = Data.Township.Trader()
	
	local ret = {}
	for _, resource in ipairs(resources) do
		if #resource.itemConversions ~= 0 then -- Skips GP
			local ret_resource = {}
			table.insert(ret_resource, '\r\n==='..resource.name..'===')
			table.insert(ret_resource, '\r\n{| class="wikitable sortable stickyHeader"')
			table.insert(ret_resource, '\r\n|- class="headerRow-0"')
			table.insert(ret_resource, '\r\n!Item')
			table.insert(ret_resource, '\r\n!Name')
			table.insert(ret_resource, '\r\n!DLC')
			table.insert(ret_resource, '\r\n!Level')
			table.insert(ret_resource, '\r\n!Give To')
			table.insert(ret_resource, '\r\n!Take From')
			table.insert(ret_resource, '\r\n!Value')
			table.insert(ret_resource, '\r\n!Value/Resource')
			if resource.id =='Food' then
				table.insert(ret_resource, '\r\n!Heals')
				table.insert(ret_resource, '\r\n!Heals/Resource')
			end
			
			for _, item in ipairs(resource.itemConversions) do
				-- Find the recipe to get the required level
				local required_level = nil
				local recipes = nil
				local skill = resource._skill
				local lookup_id = item.id
				-- A few special skill overrides
				if item.id == 'Raw_Magic_Fish' then
					skill = 'Fishing'
				elseif item.id == 'Apple' then
					skill = 'Farming'
				elseif string.match(item.id, '_U$') then
					-- Upgraded Crafting item. Display the level for the base item
					-- Converts Black_Dhide_Body_U -> Black_Dhide_Body for the purposes of the lookup
					lookup_id = string.sub(item.id, 1, #item.id - 2)
				end
				local recipes = Data.Item.FindRecipes(lookup_id, skill)
				if #recipes == 1 then
					required_level = recipes[1].level
				end
				table.insert(ret_resource, '\r\n|-')
				-- Icon
				table.insert(ret_resource, '\r\n|style="text-align:center"|'..Icons.Icon({item.name, type='item', size='50', notext=true}))
				-- Name
				table.insert(ret_resource, '\r\n|style="text-align:left"|'..Icons.Icon({item.name, type='item', noicon=true}))
				-- DLC
				table.insert(ret_resource, '\r\n|style="text-align:center"|'..Data.PLACEHOLDER_NAMESPACE_ICON(item._namespace))
				-- Level
				if required_level == nil then
					-- Recipe not found, or multiple recipes found
					table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="0"|N/A')
				else
					table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. required_level .. '"|'..Icons.Icon({skill, type="skill", notext=true})..' '..required_level)
				end
				-- Give To
				table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.to .. '"|'..Icons.Icon({item.name, type='item', notext=true})..' '..Shared.formatnum(item.to))
				-- Take From
				table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.from .. '"|'..Icons.Icon(resource._icon)..' '..Shared.formatnum(item.from))
				-- Value
				table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.sellsFor .. '"|'..Icons.GP(item.sellsFor))
				-- Value/Resource
				table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.sellsFor/item.from .. '"|'..Icons.GP(Shared.round(item.sellsFor/item.from, 2, 2)))
				if resource.id =='Food' then
					-- Heals
					table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.healsFor*10 .. '"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..Shared.formatnum(item.healsFor*10))
					-- Heals/Resource
					table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.healsFor*10/item.from .. '"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..Shared.round(item.healsFor*10/item.from, 2, 2))
				end
			end
			
			table.insert(ret_resource, '\r\n|}')
			
			table.insert(ret, table.concat(ret_resource))
		end
	end
	return table.concat(ret)
end

local p = {}

p.getTraderTable = Data.Township.getTraderTable

return p