Module:GladeResourcesData

From Against the Storm Official Wiki

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

---@class GladeResourcesData
---
---Sample output of the first item from the JSON table:
---table#1 {
---    ["charges"] = 20,
---    ["description"] = "A very common plant, it thrives thanks to the magical rain. Requires a gathering camp with a <sprite name=grade1> recipe or better.",
---    ["displayName"] = "Reed Field (Small)",
---    ["extraProduction"] = table#2 {
---        table#3 {
---            ["amount"] = 1,
---            ["chance"] = 0.2,
---            ["name"] = "[Food Raw] Roots",
---        },
---        table#4 {
---            ["amount"] = 1,
---            ["chance"] = 0.2,
---            ["name"] = "[Mat Raw] Clay",
---        },
---    },
---    ["iconName"] = "Icon_Resource_Reeds",
---    ["id"] = "Moor Node Reed Deposit - Small",
---    ["label"] = "Gathering Node",
---    ["minGradeToCollect"] = "1",
---    ["production"] = table#5 {
---        ["amount"] = 1,
---        ["name"] = "[Mat Raw] Reeds",
---    },
---    ["type"] = 0,
---    ["xSize"] = 1,
---    ["ySize"] = 1,
---}
---
---@alias depositRecordTable table a full record for one deposit
local GladeResourcesData = {}



--region Dependencies

local JsonUtils = require("Module:JsonUtils")

--endregion



--region Private constants

local DATA_FILE1 = "Module:GladeResourcesData/Glade_Resources.json"
local DATA_FILE2 = "Module:GladeResourcesData/Ore.json"
local DATA_FILE3 = "Module:GladeResourcesData/Trees.json"

local SCHEMA = {
    CHARGES = "charges",
    ARRAY_OF_CHARGES = "mainCharges",

    DESCRIPTION = "description",
    NAME = "displayName",

    SUBTABLE_BONUSES = "extraProduction",
    BONUS_AMOUNT = "amount",
    BONUS_CHANCE = "chance",
    BONUS_GOOD_ID = "name",

    ICON = "iconName",
    NODE_ID = "id",
    CATEGORY = "label",
    GRADE_REQUIRED = "minGradeToCollect",

    SUBTABLE_PRODUCT = "production",
    PRODUCT_AMOUNT = "amount",
    PRODUCT_GOOD_ID = "name",

    TYPE_CODE = "type",
    SIZE_X = "xSize",
    SIZE_Y = "ySize",
}

local GRADE_STRING_TO_NUMBER = {
    ["1"] = 1,
    ["2"] = 2,
}

local ARG_TREE = "Tree"

--endregion



--region Localization string constants

local ERROR_DEPOSIT_RECORD_NIL = "Deposit record provided is nil. Check how it was last retrieved or extracted from a list"
local ERROR_DEPOSIT_RECORD_INVALID = "Deposit record provided does not contain expected data. Check how it was last retrieved or extracted from a list"

local ERROR_ID_INVALID = "Deposit's ID not found"
local ERROR_DEPOSIT_PRODUCT_RECORD_INVALID = "Deposit's product record is nil or empty"
local ERROR_DEPOSIT_BONUS_RECORD_INVALID = "Deposit's specified bonus resource record is nil or empty"

--endregion



--region Private member variables

local gladeResourcesData

--I did some time trials with first populating a lookup map for main-resource IDs to all the entries in the JSON data where that resource was the main product. It took about 3 times as long to create the lookup map before return my selection vs. just looping through and returning my selection directly:
--
--For 10,000 trials
--When creating a lookup map first:
--  Total time: 1.1892
--  Method average time: 0.00011892
--When searching directly--
--  Total time: 0.46102
--  Method average time: 4.6102e-5
--
--I don't believe that most use cases & page views of this module will be calling this module more than 3 times for more than 3 different main resources. This means that leveraging the caching of the module with this lookup map will NOT pay off, but will slow down the majority of page loads that use this module unnecessarily.
--I believe most use cases & page loads will call this module twice: (1) to get the deposits where there's a specified main resource and (2) to get the deposits where there's a specified bonus resource. That would require separate lookup maps for each search, and most use cases would only need the lookup map once.
--Therefore, I have decided not to use lookup maps for this module.

--endregion



--region Private methods

---@private
---Flips through the records loaded from JSON and removes any entries where IDs are duplicates. Overcomes any bugs in the developers' output methods that could result in duplicate entries.
---
---@param records table of deposit records
---@return table copy of deposit records, but without duplicates
local function deduplicateRecords(records)
    local newRecords = {}
    local seenIDs = {}

    for _, record in ipairs(records) do
        local id = record[SCHEMA.NODE_ID]
        if not seenIDs[id] then
            seenIDs[id] = true
            table.insert(newRecords, record)
        end
    end

    return newRecords
end

---@private
---Flips through the records loaded from JSON and removes any entries where *NAMES* are duplicates. Necessary for trees where there are multiple versions called the same thing even though they have different IDs.
---
---@param records table of deposit records
---@return table copy of deposit records, but without duplicates
local function deduplicateRecordsByName(records)
    local newRecords = {}
    local seenIDs = {}

    for _, record in ipairs(records) do
        local id = record[SCHEMA.NAME]
        if not seenIDs[id] then
            seenIDs[id] = true
            table.insert(newRecords, record)
        end
    end

    return newRecords
end


---@private
---Loads the data from the JSON if it hasn't already been loaded.
local function loadData()

    if not gladeResourcesData then

        --Start with the primary dataset
        local primaryRecords = JsonUtils.convertJSONToLuaTable(DATA_FILE1)
        gladeResourcesData = deduplicateRecords(primaryRecords)

        --Append the secondary dataset
        local recordsToAppend = JsonUtils.convertJSONToLuaTable(DATA_FILE2)
        for _, newRecord in ipairs(recordsToAppend) do
            table.insert(gladeResourcesData, newRecord)
        end

        --Append the third dataset, with a few substitutions
        local recordsToAppendAndSubstitute = deduplicateRecordsByName(JsonUtils.convertJSONToLuaTable(DATA_FILE3))
        for _, newRecord in ipairs(recordsToAppendAndSubstitute) do
            -- Currently it's "Resource" which isn't very helpful.
            if newRecord[SCHEMA.CATEGORY] == "Resource" then
                newRecord[SCHEMA.CATEGORY] = "Tree"
            end
            table.insert(gladeResourcesData, newRecord)
        end
    end
end



---@private
---Shorthand method to look at a specified deposit in the main data table for a specified product.
---
---@param goodID string the good ID
---@param index number the index within the main data table
---@return boolean true if the good is the product at that index
local function isPrimaryProductAtDepositIndex(goodID, index)
    return goodID == gladeResourcesData[index][SCHEMA.SUBTABLE_PRODUCT][SCHEMA.PRODUCT_GOOD_ID]
end



---@private
---Shorthand method to look at a specified deposit in the main data table for a specified bonus resource.
---
---@param goodID string the good ID
---@param index number the index within the main data table
---@return boolean true if the good is one of the bonus resources at that index
local function isBonusProductAtDepositIndex(goodID, index)
    for _, bonus in ipairs(gladeResourcesData[index][SCHEMA.SUBTABLE_BONUSES]) do
        if goodID == bonus[SCHEMA.BONUS_GOOD_ID] then
            return true
        end
    end
    return false
end



---@private
---Shorthand method to check whether the specified deposit in the main data table has a grade gatherable by a building with the specified grade.
---
---@param index number the index within the main data table
---@param maxGrade number the grade of a building
---@return boolean true if the building could gather it
local function isGradeAtDepositIndexLessThanOrEqualTo(index, maxGrade)

    return (tonumber(gladeResourcesData[index][SCHEMA.GRADE_REQUIRED]) or 0) <= maxGrade
end



---@private
---Standard checks for a valid reference and then existence of data to make methods that refer to depositRecordTables more concise.
---
---@param deposit depositRecordTable
---@return boolean true if the record exists and is not empty, otherwise throws an error.
local function checkDeposit(deposit)
    if not deposit then
        error(ERROR_DEPOSIT_RECORD_NIL)
    elseif not deposit[SCHEMA.NODE_ID] or not deposit[SCHEMA.SUBTABLE_PRODUCT] then
        error(ERROR_DEPOSIT_RECORD_INVALID)
    end
end



---@private
---Since the ID is used so much, this method will check whether the ID exists, is valid, and then return it if so.
---
---This method assumes checkDeposit has already validated the deposit record.
---
---@param deposit depositRecordTable
---@return string ID, or an error if it's invalid
local function getDepositID(deposit)
    local nodeID = deposit[SCHEMA.NODE_ID]
    if not nodeID or nodeID == "" then
        error(ERROR_ID_INVALID)
    else
        return nodeID
    end
end



---@private
---Adds up the charges in the specified array of charges.
---
---@param arrayOfNumbers table array of numbers
---@return number sum
local function sumArrayOfNumbers(arrayOfNumbers)
    local ret = 0
    for _, num in ipairs(arrayOfNumbers) do
        ret = ret + num
    end
    return ret
end



---@private
---Checks whether the product reference exist in the deposit and that it's not empty.
---
---This method assumes checkDeposit has already validated the deposit record.
---
---@param deposit depositRecordTable
---@return table of product and amount
local function getAndCheckDepositProductSubtable(deposit)
    local product = deposit[SCHEMA.SUBTABLE_PRODUCT]
    if not product then
        error(ERROR_DEPOSIT_PRODUCT_RECORD_INVALID .. ": " .. getDepositID(deposit))
    else
        return product
    end
end

--endregion



--region Public methods

---@public
---Gets all the deposits that have the specified good as the primary product.
---
---**Important:** Be sure to use one of the getDeposit query methods below rather than accessing the data directly. This is to protect your controller or view from changes made to the underlying data structure.
---
---@param goodID string the good
---@return table array of depositRecordTables, or {} if none found
function GladeResourcesData.getAllDepositsWherePrimaryResource(goodID, suppressTrees)

    loadData()

    local matchingDeposits = {}
    for i = 1,#gladeResourcesData do
        if isPrimaryProductAtDepositIndex(goodID, i) and (suppressTrees == false or gladeResourcesData[i][SCHEMA.CATEGORY] ~= ARG_TREE) then
            table.insert(matchingDeposits, gladeResourcesData[i])
        end
    end

    return matchingDeposits
end



---@public
---Gets all the deposits that have the specified good as the primary product and are at most the specified grade.
---
---**Important:** Be sure to use one of the getDeposit query methods below rather than accessing the data directly. This is to protect your controller or view from changes made to the underlying data structure.
---
---@param goodID string the good
---@param maxGrade number maximum grade of the deposit
---@return table array of depositRecordTables, or {} if none found
function GladeResourcesData.getAllDepositsWherePrimaryResourceAndGrade(goodID, maxGrade, suppressTrees)

    loadData()

    local matchingDeposits = {}
    for i = 1,#gladeResourcesData do
        if isPrimaryProductAtDepositIndex(goodID, i) and isGradeAtDepositIndexLessThanOrEqualTo(i, maxGrade) and (suppressTrees == false or gladeResourcesData[i][SCHEMA.CATEGORY] ~= ARG_TREE)  then
            table.insert(matchingDeposits, gladeResourcesData[i])
        end
    end
	mw.logObject(matchingDeposits)
    return matchingDeposits
end



---@public
---Gets all the deposits that have the specified good as a bonus product.
---
---**Important:** Be sure to use one of the getDeposit query methods below rather than accessing the data directly. This is to protect your controller or view from changes made to the underlying data structure.
---
---@param goodID string the good
---@return table array of depositRecordTables, or {} if none found
function GladeResourcesData.getAllDepositsWhereBonusResource(goodID, suppressTrees)

    loadData()

    local matchingDeposits = {}
    for i = 1,#gladeResourcesData do
        if isBonusProductAtDepositIndex(goodID, i) and (suppressTrees == false or gladeResourcesData[i][SCHEMA.CATEGORY] ~= ARG_TREE) then
            table.insert(matchingDeposits, gladeResourcesData[i])
        end
    end

    return matchingDeposits
end



---@public
---Gets all the deposits that have the specified good as either the primary product or bonus product.
---
---**Important:** Be sure to use one of the getDeposit query methods below rather than accessing the data directly. This is to protect your controller or view from changes made to the underlying data structure.
---
---@param goodID string the good
---@return table array of depositRecordTables, or {} if none found
function GladeResourcesData.getAllDepositsWherePrimaryOrBonusResource(goodID, suppressTrees)

    loadData()

    local matchingDeposits = {}
    for i = 1,#gladeResourcesData do
        if isPrimaryProductAtDepositIndex(goodID, i) or isBonusProductAtDepositIndex(goodID, i) and (suppressTrees == false or gladeResourcesData[i][SCHEMA.CATEGORY] ~= ARG_TREE) then
            table.insert(matchingDeposits, gladeResourcesData[i])
        end
    end

    return matchingDeposits
end

---@public
---Gets all the deposits that are trees
---
---**Important:** Be sure to use one of the getDeposit query methods below rather than accessing the data directly. This is to protect your controller or view from changes made to the underlying data structure.
---
---@return table array of depositRecordTables, or {} if none found
function GladeResourcesData.getAllDepositsWhereDepositIsTree()

    loadData()

    local matchingDeposits = {}
    for i = 1,#gladeResourcesData do
        if GladeResourcesData.getDepositCategory(gladeResourcesData[i]) == ARG_TREE then
            table.insert(matchingDeposits, gladeResourcesData[i])
        end
    end

    return matchingDeposits
end

--endregion



--region Public deposit query methods

---Takes a deposit record (from one of the getAll methods above) and returns the ID.
---
---**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return string the deposit ID
function GladeResourcesData.getDepositID(deposit)
    checkDeposit(deposit)
    return deposit[SCHEMA.NODE_ID]
end



---Takes a deposit record (from one of the getAll methods above) and unpacks the product (both the ID and the amount) from the record.
---
---**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return string the good ID
---@return number amount of product
function GladeResourcesData.getDepositPrimaryProduct(deposit)
    checkDeposit(deposit)
    local product = getAndCheckDepositProductSubtable(deposit)
    return product[SCHEMA.PRODUCT_GOOD_ID], product[SCHEMA.PRODUCT_AMOUNT]
end



---Takes a deposit record (from one of the getAll methods above) and gets the number of bonus resources in the deposit.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return number bonuses
function GladeResourcesData.getDepositNumBonusResources(deposit)
    checkDeposit(deposit)
    local bonuses = deposit[SCHEMA.SUBTABLE_BONUSES]
    if not bonuses then
        return 0
    else
        return #bonuses
    end
end



---Takes a deposit record (from one of the getAll methods above) and unpacks the bonus resource (the ID, the amount, and the probability) from the record.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
-----
---@param deposit depositRecordTable
---@param index number which bonus resource record to get from the deposit
---@return string the good ID
---@return number amount of the resource
---@return number the probability of getting the resource, as a decimal
function GladeResourcesData.getDepositBonusResourceAt(deposit, index)
    checkDeposit(deposit)
    local bonuses = deposit[SCHEMA.SUBTABLE_BONUSES]
    if not bonuses or #bonuses < index then
        return nil
    else
        local bonus = bonuses[index]
        if not bonus then
            error(ERROR_DEPOSIT_BONUS_RECORD_INVALID .. ": " .. getDepositID(deposit))
        end
        return bonus[SCHEMA.BONUS_GOOD_ID], bonus[SCHEMA.BONUS_AMOUNT], bonus[SCHEMA.BONUS_CHANCE]
    end
end



---Takes a deposit record (from one of the getAll methods above) and gets the category of the deposit.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return string category
function GladeResourcesData.getDepositCategory(deposit)
    checkDeposit(deposit)
    return deposit[SCHEMA.CATEGORY] or ""
end



---Takes a deposit record (from one of the getAll methods above) and gets the total charges the deposit has. This depends on the type (e.g., pond vs. mine), but this method takes care of that.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return number charges
function GladeResourcesData.getDepositCharges(deposit)
    checkDeposit(deposit)
    if deposit[SCHEMA.CHARGES] then
        return deposit[SCHEMA.CHARGES]
    elseif deposit[SCHEMA.ARRAY_OF_CHARGES] then
        return sumArrayOfNumbers(deposit[SCHEMA.ARRAY_OF_CHARGES])
    else
        return 0
    end
end



---Takes a deposit record (from one of the getAll methods above) and gets the description of the deposit.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return string description
function GladeResourcesData.getDepositDescription(deposit)
    checkDeposit(deposit)
    return deposit[SCHEMA.DESCRIPTION] or ""
end


---Takes a deposit record (from one of the getAll methods above) and gets the name of the deposit.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return string name
function GladeResourcesData.getDepositName(deposit)
    checkDeposit(deposit)
    return deposit[SCHEMA.NAME] or ""
end



---Takes a deposit record (from one of the getAll methods above) and gets the grade required to harvest from the deposit.
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return number grade required
function GladeResourcesData.getDepositRequiredGrade(deposit)
    checkDeposit(deposit)
    return GRADE_STRING_TO_NUMBER[deposit[SCHEMA.GRADE_REQUIRED]] or 0
end

--endregion

---Checks if a deposit is a tree
-----
-----**Important:** Use this method instead of accessing the data directly from the record.
---
---@param deposit depositRecordTable
---@return boolean if deposit is tree
function GladeResourcesData.isDepositTree(deposit)
    checkDeposit(deposit)
    if GladeResourcesData.getDepositCategory(deposit) == ARG_TREE then
    	return true
    end
    return false
end

--endregion

return GladeResourcesData