Module:Township

From Melvor Idle
Revision as of 23:36, 27 October 2022 by Gau Cho (talk | contribs)

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

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

-- 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)
	
	-- No skill? No recipes
	if skill == nil then
		return {}
	end
	
	-- the key name for each skill in the json file
	local skill_recipe_keys = {
		['melvorD:Woodcutting'] = {recipes='trees', productID='productId'}, -- lowercase "d"
		['melvorD:Fishing'] = {recipes='fish', productID='productId'}, -- lowercase "d"
		['melvorD:Cooking'] = {recipes='recipes', productID='productID'},
		['melvorD:Mining'] = {recipes='rockData', productID='productId'}, -- lowercase "d"
		['melvorD:Smithing'] = {recipes='recipes', productID='productID'},
		['melvorD:Farming'] = {recipes='recipes', productID='productId'}, -- lowercase "d"
		['melvorD:Summoning'] = {recipes='recipes', productID='productID'},
		['melvorD:Fletching'] = {recipes='recipes', productID='productID'},
		['melvorD:Crafting'] = {recipes='recipes', productID='productID'},
		['melvorD:Runecrafting'] = {recipes='recipes', productID='productID'},
		['melvorD: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 results = {}
	
	local SkillData = GameData.getSkillData(skill)
	local recipes = skill_recipe_keys[skill].recipes
	local productID = skill_recipe_keys[skill].productID
	
	if SkillData[recipes] ~= nil then
		for _, recipe in ipairs(SkillData[recipes]) do
			-- Special case for Herblore
			if skill == 'Herblore' then
				-- Iterate over the 4 potion tiers
				for _, potion in ipairs(recipe[productID]) do
					if itemid == potion.id then
						table.insert(results, Shared.clone(recipe))
					end
				end
			-- Base case
			else
				if itemid == recipe[productID] then
					table.insert(results, Shared.clone(recipe))
				end
			end
		end
	end
	
	return results
end

Data.Township = {}
-- Returns a list of all the Township resources
function Data.Township.Resources()
	
	-- Get a sorted list of all the resources
	local Township = GameData.getSkillData('melvorD:Township')
	local resources = GameData.sortByOrderTable(Township.resources, Township.resourceDisplayOrder[1].ids)
	resources = Shared.clone(resources)
	
	-- Append the icon reference to each resource, as well as the associated skill for the recipes
	-- From https://melvoridle.com/assets/data/melvorFull.json -> data.skillData -> Township.data.resources.media
	local resource_data = {
		['melvorF:GP'] = {_icon = {'Coins'}, _skill = nil},
		['melvorF:Food'] = {_icon = {'Raw Beef', type='item'}, _skill = 'melvorD:Cooking'},
		['melvorF:Wood'] = {_icon = {'Wood', type='resource'}, _skill = 'melvorD:Woodcutting'},
		['melvorF:Stone'] = {_icon = {'Stone', type='resource'}, _skill = 'melvorD:Mining'},
		['melvorF:Ore'] = {_icon = {'Iron Ore', type='rock'}, _skill = 'melvorD:Mining'},
		['melvorF:Coal'] = {_icon = {'Coal', type='resource'}, _skill = 'melvorD:Mining'},
		['melvorF:Bar'] = {_icon = {'Iron Bar', type='item'}, _skill = 'melvorD:Mining'},
		['melvorF:Herbs'] = {_icon = {'Garum Herb', type='item'}, _skill = 'melvorD:Farming'},
		['melvorF:Rune_Essence'] = {_icon = {'Rune Essence', type='item'}, _skill = 'melvorD:Mining'},
		['melvorF:Leather'] = {_icon = {'Leather', type='item'}, _skill = nil},
		['melvorF:Potions'] = {_icon = {'Potion', type='resource'}, _skill = 'melvorD:Herblore'},
		['melvorF:Planks'] = {_icon = {'Planks', type='resource'}, _skill = 'melvorD:Woodcutting'},
		['melvorF:Clothing'] = {_icon = {'Leather Body', type='item'}, _skill = 'melvorD:Crafting'}
	}
	for _, resource in ipairs(resources) do
		resource._skill = resource_data[resource.id]._skill
		resource._icon = resource_data[resource.id]._icon
		resource._icon.notext = true
		resource._icon.nolink = true
	end
	
	return resources
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 tradeable items
	-- See township.js -> TownshipResource.buildResourceItemConversions for the calculation of valid items
	local function matchNone(item)
		return false
	end
	local function matchFood(item)
		return item.type == 'Food' and (not string.match(item.id, '_Perfect')) and item.category ~= 'Farming' and (not item.ignoreCompletion)
	end
	local function matchLogs(item)
		return item.type == 'Logs'
	end
	local function matchOre(item)
		return item.type == 'Ore' and item.id ~= 'melvorTotH:Meteorite_Ore'
	end
	local function matchCoal(item)
		return item.id == 'melvorD:Coal_Ore'
	end
	local function matchBar(item)
		return item.type == 'Bar' and item.id ~= 'melvorTotH:Meteorite_Bar'
	end
	local function matchHerb(item)
		return item.type == 'Herb'
	end
	local function matchEssence(item)
		return item.id == 'melvorD:Rune_Essence' or item.id == 'melvorTotH:Pure_Essence'
	end
	local function matchLeather(item)
		return item.id == 'melvorD:Leather'
	end
	local function matchPotion(item)
		return item.type == 'Potion' and string.match(item.id, '_IV')
	end
	local function matchClothing(item)
		local valid_tiers = {'Leather', 'Hard Leather', 'Dragonhide', 'Elderwood', 'Revenant', 'Carrion'}
		for _, tier in ipairs(valid_tiers) do
			if item.tier == tier then
				return true
			end
		end
		return false
	end

	local resource_data = {
		['melvorF:GP'] = {traderMatches = matchNone},
		['melvorF:Food'] = {traderMatches = matchFood},
		['melvorF:Wood'] = {traderMatches = matchLogs},
		['melvorF:Stone'] = {traderMatches = matchOre},
		['melvorF:Ore'] = {traderMatches = matchOre},
		['melvorF:Coal'] = {traderMatches = matchCoal},
		['melvorF:Bar'] = {traderMatches = matchBar},
		['melvorF:Herbs'] = {traderMatches = matchHerb},
		['melvorF:Rune_Essence'] = {traderMatches = matchEssence},
		['melvorF:Leather'] = {traderMatches = matchLeather},
		['melvorF:Potions'] = {traderMatches = matchPotion},
		['melvorF:Planks'] = {traderMatches = matchLogs},
		['melvorF:Clothing'] = {traderMatches = matchClothing}
	}

	for _, resource in ipairs(resources) do
		resource.itemConversions = Shared.clone(GameData.getEntities('items', resource_data[resource.id].traderMatches))
	end
	
	-- Calculate the trader's conversion ratios
	-- See township.js TownshipResource.getBaseConvertToTownshipRatio and TownshipResource.getBaseConvertFromTownshipRatio for the conversion prices
	for _, resource in ipairs(resources) do
		if resource.id == 'melvorF:Food' then
			for _, item in ipairs(resource.itemConversions) do
				item.toTownship = math.max(math.floor(1000/(item.healsFor*10)), 2)
				item.fromTownship = item.healsFor*5*6
			end
		elseif resource.id == 'melvorF:Planks' then
			for _, item in ipairs(resource.itemConversions) do
				item.toTownship = math.max(math.floor(3000/math.max(item.sellsFor, 1)), 2)
				item.fromTownship = math.max(math.ceil(item.sellsFor/2)*6, 1);
			end
		elseif resource.id == 'melvorF:Rune_Essence' then
			for _, item in ipairs(resource.itemConversions) do
				item.toTownship = 5
				item.fromTownship = (item.sellsFor+1)*10*6
			end
		elseif resource.id == 'melvorF:Leather' then
			for _, item in ipairs(resource.itemConversions) do
				item.toTownship = 20
				item.fromTownship = 20*6
			end
		else
			for _, item in ipairs(resource.itemConversions) do
		        item.toTownship = math.max(math.floor(1000/math.max(item.sellsFor, 1)), 2)
		    	item.fromTownship = math.max(item.sellsFor*6, 1)
			end
		end
	end
	return resources
end

-- Builds the table of trader items
function Data.Township.getTraderTable(frame)
	-- Get the resources data with associated trader data
	local resources = Data.Township.Trader()
	
	-- Build the text
	local ret = {}
	for _, resource in ipairs(resources) do
		if #resource.itemConversions ~= 0 then -- Skips GP
			local ret_resource = {}
			
			-- Header
			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 =='melvorF:Food' then
				table.insert(ret_resource, '\r\n!Heals')
				table.insert(ret_resource, '\r\n!Heals/Resource')
			end
			
			-- Each item
			for _, item in ipairs(resource.itemConversions) do
				-- To indicate the skill level, we need to find the recipe of the item in the target skill
				-- Unfortunately Module:Items/SourceTables.getItemSources does not provide parseable data
				local required_level = nil
				local recipes = nil
				
				-- Get the skill based on the item.id or else use the resource's default skill
				local skill_overrides = {
					['melvorD:Raw_Magic_Fish'] = 'melvorD:Fishing',
					['melvorF:Apple'] = 'melvorD:Farming',
				}
				local skill = skill_overrides[item.id] or resource._skill
				
				-- Check for upgraded Crafting items and downgrade them so we can display the crafting level for the base item
				-- e.g. converts Black_Dhide_Body_U -> Black_Dhide_Body for the purposes of the lookup
				local lookup_id = item.id
				if string.match(item.id, '_U$') then
					lookup_id = string.sub(item.id, 1, #item.id - 2)
				end
				
				-- Find the recipe
				local recipes = Data.Item.FindRecipes(lookup_id, skill)
				if #recipes == 1 then
					required_level = recipes[1].level
				end
				
				-- Alright, now that we've found the required recipe and level, we can draw the item's row entry
				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"|'..'XXX')
				-- 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.toTownship .. '"|'..Icons.Icon({item.name, type='item', notext=true})..' '..Shared.formatnum(item.toTownship))
				-- Take From
				table.insert(ret_resource, '\r\n|style="text-align:center" data-sort-value="' .. item.fromTownship .. '"|'..Icons.Icon(resource._icon)..' '..Shared.formatnum(item.fromTownship))
				-- 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.fromTownship .. '"|'..Icons.GP(Shared.round(item.sellsFor/item.fromTownship, 2, 2)))
				if resource.id =='melvorF: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.fromTownship .. '"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..Shared.round(item.healsFor*10/item.fromTownship, 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