Module:Goodbox: Difference between revisions

From Against the Storm Official Wiki
(Refactored to use the getter methods. Improved documentation; reordered methods; made private method for the inner table for better grouping)
(was calling the wrong species link template. fixed now; icons should be bigger)
 
(17 intermediate revisions by the same user not shown)
Line 1: Line 1:
---
---
--- Module to create an infobox for displaying on a good's wiki page.
--- Retrieves data for the specified resource and sends the data over to the view, another template.
---
--- Shows the facts from the data about the specified good in an easy-to-read
--- table, with a large icon, information, categories, and the flavor text.
---
--- This should be invoked from Template:Goodbox with the parameter of the
--- good whose infobox should be shown. See the template documentation for
--- more information about parameters and errors and to see examples.
---
---
--- @module Goodbox
--- @module Goodbox
Line 13: Line 6:




--region Dependencies


local GoodsData = require("Module:GoodsData")
local GoodsData = require("Module:GoodsData")
local SpeciesData = mw.loadData("Module:SpeciesData")
local ControllerUtilities = require("Module:ControllerUtilities")


local ViewTemplate = "Template:Goodbox/view"
--endregion




--region  Private constants
-- Template parameters
local ARG_GOOD_NAME = "name"
local ARG_GOOD_PURPOSE = "purpose"
local ARG_GOOD_SPECIES_PREF = "species_preference"
-- Infobox header labels
local TITLE_ID = "ID"
local TITLE_CATEGORY = "Storage Category"
local TITLE_EATABLE = "Eatable"
local TITLE_BURNABLE = "Burnable"
local TITLE_BURN_TIME = "Fuel burn time"
local TITLE_SELL_VALUE = "Value when sold"
local TITLE_BUY_VALUE = "Price when bought"


local PATTERN_FORMAT_TWO_DECIMALS = "%1.2f"
--region Private constants
local PATTERN_CAPTURE_FIRST_LETTER_IN_WORD = "(%a)([%w]*)"


local NBSP = " "
local ARG_NAME = "name"
local WIKIMARKUP_MED_ICON = "|32px|alt="


local CSS_INLINE_INFOBOX = {
local PARAM_TITLE = "title"
["float"] = "right",
local PARAM_SUBTITLE = "subtitle"
["clear"] = "right",
local PARAM_DESCRIPTION = "description"
["width"] = "256px",
local PARAM_ICON_FILENAME = "iconfilename"
["border"] = "1px solid #a2a9b1",
local PARAM_PREFERENCES = "preferences"
["border-spacing"] = "2px",
local PARAM_RECIPES = "recipes"
["border-collapse"] = "collapse",
local PARAM_BURN_TIME = "burntime"
["margin"] = "0.5em 0 0.5em 1em"
local PARAM_SELL_VALUE = "sellvalue"
}
local PARAM_SELL_VALUE_AFTER_PRESTIGE_PENALTY = "sellvaluep10"
local CSS_INLINE_INFOBOX_INNER_TABLE = {
local PARAM_BUY_VALUE = "buyvalue"
["border-collapse"] = "collapse"
local PARAM_ID = "id"
}
local CSS_INLINE_INFOBOX_TR_BOTTOM_DIVIDER = {
["border-bottom"] = "1px solid #a2a9b1"
}
local CSS_INLINE_INFOBOX_TR_TOP_DIVIDER = {
["border-top"] = "1px solid #a2a9b1"
}
-- Lookup table for storage category icons.
local ICON_FILENAMES_CATEGORIES = {
["Building Materials"] = "Icon_UI_CategoryBuilding.png",
["Consumable Items"] = "Icon_UI_CategoryConsumable.png",
["Crafting Resources"] = "Icon_UI_CategoryCrafting.png",
["Food"] = "Icon UI CategoryFood.png",
["Fuel & Exploration"] = "Icon_UI_CategoryFuel.png",
["Trade Goods"] = "Icon_UI_CategoryTrade.png"
}


--endregion
--endregion
Line 72: Line 42:
--region Private methods
--region Private methods


---
---renderPreferences takes the ID of a resource and looks up what species prefers it to satisfy one of their needs, calls external view templates to convert those species into usable content strings for display, and returns those strings concatenated together for display by the calling scope.
--- Builds using the provided wikiInfobox a subtable to lay out headers and
---@param id string the unique ID of the resource
--- fields.
---@return string a content string ready for display
---
local function renderPreferences(id)
---@param wikiInfobox table mw.html object into which we're building this
---@param goodName string the name of the good the infobox is about
---@param purpose string the parameter provided to the template
---@param speciesPref string the parameter provided to the template
function makeInnerTable(wikiInfobox, goodName, purpose, speciesPref)


-- Grab the data we'll use to populate this.
    local frame = mw.getCurrentFrame()
local goodID = GoodsData.getGoodID(goodName)
    local goodName = GoodsData.getGoodNameByID(id)
local goodCategory = GoodsData.getGoodCategory(goodName)
local goodIsEatable = GoodsData.isGoodEatable(goodName)
local goodIsBurnable = GoodsData.isGoodBurnable(goodName)
local goodBurnTime = GoodsData.getGoodBurnTime(goodName)
local goodSellValue, goodBuyValue = GoodsData.getGoodValue(goodName)


-- Start the inner table
    local concatenatedStrings = ""
local innerTable = wikiInfobox:tag("tr"):tag("td"):newline():tag("table"):css(CSS_INLINE_INFOBOX_INNER_TABLE)
    for speciesID, species in pairs(SpeciesData.species) do
innerTable:newline()
        for _, need in ipairs(species[SpeciesData.NEEDS]) do


-- Start with the things not provided by the data.
            if need[SpeciesData.NEED_NAME] == goodName then
if purpose and purpose ~= "" then
innerTable:tag("tr")
:tag("th"):wikitext(toTitleCase(ARG_GOOD_PURPOSE)):done()
:tag("td"):wikitext(purpose):done()
:done():newline()
end
if speciesPref and speciesPref ~= "" then
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_BOTTOM_DIVIDER)
:tag("th"):wikitext(toTitleCase(ARG_GOOD_SPECIES_PREF)):done()
:tag("td"):wikitext(speciesPref):done()
:done():newline()
end


-- Storage category (this is the same as the subtitle for now)
                local speciesString = frame:expandTemplate{
if goodCategory then
                    title = "Species_link/view",
local categoryIcon = "[[File:" .. ICON_FILENAMES_CATEGORIES[goodCategory] .. WIKIMARKUP_MED_ICON .. goodCategory .. "]]"
                    args = {
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_TOP_DIVIDER)
                        ["iconfilename"] = species[SpeciesData.ICON_FILENAME] .. ".png",
:tag("th"):wikitext(TITLE_CATEGORY):done()
                        ["name"] = speciesID,
:tag("td"):wikitext(categoryIcon .. NBSP .. goodCategory):done()
                        ["iconsize"] = "medium",
:done():newline()
                        ["display"] = "notext",
end
                    }, }
-- Since false is a valid value, have to directly check if it's nil.
if goodIsEatable ~= nil then
innerTable:tag("tr")
:tag("th"):wikitext(TITLE_EATABLE):done()
:tag("td"):wikitext(goodIsEatable and "Yes" or "No"):done()
:done():newline()
end
if goodIsBurnable ~= nil then
innerTable:tag("tr")
:tag("th"):wikitext(TITLE_BURNABLE):done()
:tag("td"):wikitext(goodIsBurnable and "Yes" or "No"):done()
:done():newline()
end
if goodIsBurnable and goodBurnTime then
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_BOTTOM_DIVIDER)
:tag("th"):wikitext(TITLE_BURN_TIME):done()
:tag("td"):wikitext(goodBurnTime):done()
:done():newline()
end
-- Trade values.
if goodSellValue then
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_TOP_DIVIDER)
:tag("th"):wikitext(TITLE_SELL_VALUE):done()
:tag("td"):wikitext(toDecimal(goodSellValue)):done()
:done():newline()
end
if goodBuyValue then
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_BOTTOM_DIVIDER)
:tag("th"):wikitext(TITLE_BUY_VALUE):done()
:tag("td"):wikitext(toDecimal(goodBuyValue)):done()
:done():newline()
end
-- End with the ID.
if goodID then
innerTable:tag("tr"):css(CSS_INLINE_INFOBOX_TR_TOP_DIVIDER)
:tag("th"):wikitext(TITLE_ID):done()
:tag("td"):wikitext("\'" .. goodID .. "\'"):done()
:done():newline()
end


-- Close the table.
                -- Separate multiple entries with a space
innerTable:done():newline()
                if #concatenatedStrings > 1 then
-- Close the table cell and row the inner table is in.
                    concatenatedStrings = concatenatedStrings .. " "
wikiInfobox:done():done():newline()
                end
end


                concatenatedStrings = concatenatedStrings .. speciesString
            end


        end
    end


---
    if #concatenatedStrings < 1 then
--- Shortcut string to reformat numbers with two decimals.
        return nil
---
    end
---@param value number whole or with a fractional part
---@return string reformatted to force two decimal places
function toDecimal(value)


return string.format(PATTERN_FORMAT_TWO_DECIMALS, value)
    return concatenatedStrings
end
end




---
--- Capitalizes the first character of each word to "Make It Title Case." Also
--- converts any underscores to spaces.
---
--- @param title string to capitalize
--- @return string capitalized with title case
function toTitleCase(title)


title = title:gsub("_", " ")
-- Building interface:
-- getCategory
-- getCityScore
-- getConstructionCosts (as [goodName] = stack size)
-- getConstructionTime
-- getDescription
-- getIcon
-- isMovable
-- getName
-- getNumberOfWorkplaces
-- getSize (as "X x Y")
-- getStorage
local function buildViewParameters(id)
 
    local frame = mw.getCurrentFrame()
 
    local viewParameters = {}
    viewParameters[PARAM_TITLE] = GoodsData.getName(id)
    viewParameters[PARAM_SUBTITLE] = GoodsData.getCategory(id)
    viewParameters[PARAM_DESCRIPTION] = ControllerUtilities.findAndReplaceSpriteTagsWithFiles(GoodsData.getDescription(id), frame)
    viewParameters[PARAM_ICON_FILENAME] = GoodsData.getIcon(id)
    viewParameters[PARAM_PREFERENCES] = renderPreferences(id)
    viewParameters[PARAM_RECIPES] = frame:expandTemplate{ title = "Recipe", args = { ["product"] = GoodsData.getName(id), ["display"] = "list", }, }
    local burnTime = GoodsData.getBurnTime(id) or 0
    local timeString = string.format( "%02d:%02d", math.floor(burnTime/60), math.fmod(burnTime, 60) )
    viewParameters[PARAM_BURN_TIME] = burnTime ~= 0 and timeString
    viewParameters[PARAM_SELL_VALUE] = GoodsData.getSellValue(id)
    viewParameters[PARAM_SELL_VALUE_AFTER_PRESTIGE_PENALTY] = GoodsData.getSellValueAfterPrestigePenalty(id)
    viewParameters[PARAM_BUY_VALUE] = GoodsData.getBuyValue(id)
    viewParameters[PARAM_ID] = id


local newTitle = title:gsub(PATTERN_CAPTURE_FIRST_LETTER_IN_WORD, function(firstLetter, rest)
    -- Augment the subtitle according to preferences. This is hard-code-y, but it's simple and won't break if the categories change, it'll just get skipped.
return firstLetter:upper() .. rest:lower()
    if viewParameters[PARAM_PREFERENCES] then
end)
        if viewParameters[PARAM_SUBTITLE] == "Food" then
            viewParameters[PARAM_SUBTITLE] = "Food (Complex Food)"
        else
            if viewParameters[PARAM_SUBTITLE] == "Consumable Items" then
                viewParameters[PARAM_SUBTITLE] = "Consumable Items (Service Goods)"
            end
        end
    end


return newTitle
    return viewParameters
end
end


--endregion




---findBoxData
--- Handles data only, does not interface with the view.
---@param name string the name of the resource about which to retrieve data
---@return table the essential data about the resource extracted
local function findBoxData(name)
    local id = GoodsData.getGoodID(name)
    if not id then
        error("No resource found with name: " .. name .. ".")
    end


--region Public methods
    local viewParameters = buildViewParameters(id)
    if not viewParameters then
        error("No resource found with name: " .. name .. ".")
    end


---
    return viewParameters
--- Creates an html html and wiki markup to display the data in an infobox.
end
---
--- @param frame table from template's calling context
--- @return string of html and wiki markup
function Goodbox.infobox(frame)


-- Every good must have a name.
--endregion
local goodName = frame.args[ARG_GOOD_NAME]
if not goodName or goodName == "" then
return "You must specify the name of the good. See [[Template:Goodbox]] for examples."
end


-- Use this method here to check to see whether the provided name is valid.
if not GoodsData.getGoodID(goodName) then
return "Infobox can't find the specified good: " .. goodName .. "."
end


-- Additional template parameters, may or may not be present
local purpose = frame.args[ARG_GOOD_PURPOSE]
local speciesPref = frame.args[ARG_GOOD_SPECIES_PREF]


-- Get the data.
--region Public methods
local goodDescription = GoodsData.getGoodDescription(goodName)
local goodCategory = GoodsData.getGoodCategory(goodName)
local goodIconFilename = GoodsData.getGoodIcon(goodName)


-- Make the top of the infobox that every item has...
---main
local wikiInfobox = mw.html.create("table"):css(CSS_INLINE_INFOBOX):newline()
--- For calling from Template:Goodbox. After handling the frame, forwards control to the primary method, findBoxData, then calls the view template with the compiled box data. Does not interface with any data modules.
-- with a title...
---
wikiInfobox:tag("tr")
---@param frame table the calling template's context
:tag("th"):wikitext(goodName):done()
---@return string wiki markup, constructed with the template view
:done():newline()
function Goodbox.main(frame)
-- and a subtitle, showing the category...
if goodCategory then
wikiInfobox:tag("tr")
:tag("td"):wikitext(goodCategory):done()
:done():newline()
end
-- and a large icon.
if goodIconFilename then
wikiInfobox:tag("tr")
:tag("td"):wikitext("[[File:" .. goodIconFilename .. "]]"):done()
:done():newline()
wikiInfobox:tag("tr"):css(CSS_INLINE_INFOBOX_TR_BOTTOM_DIVIDER)
:tag("td"):wikitext(goodName .. ", as seen in-game"):done()
:done():newline()
end


makeInnerTable(wikiInfobox, goodName, purpose, speciesPref)
    local name = frame.args[ARG_NAME]
    if not name then
        error("You must specify the name of the good or resource. Please see the template documentation for how to use the parameters.")
    end


-- Finish with the flavor text.
    local viewParameters = findBoxData(name)
if goodDescription then
wikiInfobox:tag("tr"):css(CSS_INLINE_INFOBOX_TR_TOP_DIVIDER)
:tag("td"):wikitext(goodDescription):done()
:done():newline()
end


return wikiInfobox
    return frame:expandTemplate{ title = ViewTemplate, args = viewParameters }
end
end



Latest revision as of 20:14, 23 October 2024

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

---
--- Retrieves data for the specified resource and sends the data over to the view, another template.
---
--- @module Goodbox
local Goodbox = {}



--region Dependencies

local GoodsData = require("Module:GoodsData")
local SpeciesData = mw.loadData("Module:SpeciesData")

local ControllerUtilities = require("Module:ControllerUtilities")

local ViewTemplate = "Template:Goodbox/view"

--endregion



--region Private constants

local ARG_NAME = "name"

local PARAM_TITLE = "title"
local PARAM_SUBTITLE = "subtitle"
local PARAM_DESCRIPTION = "description"
local PARAM_ICON_FILENAME = "iconfilename"
local PARAM_PREFERENCES = "preferences"
local PARAM_RECIPES = "recipes"
local PARAM_BURN_TIME = "burntime"
local PARAM_SELL_VALUE = "sellvalue"
local PARAM_SELL_VALUE_AFTER_PRESTIGE_PENALTY = "sellvaluep10"
local PARAM_BUY_VALUE = "buyvalue"
local PARAM_ID = "id"

--endregion



--region Private methods

---renderPreferences takes the ID of a resource and looks up what species prefers it to satisfy one of their needs, calls external view templates to convert those species into usable content strings for display, and returns those strings concatenated together for display by the calling scope.
---@param id string the unique ID of the resource
---@return string a content string ready for display
local function renderPreferences(id)

    local frame = mw.getCurrentFrame()
    local goodName = GoodsData.getGoodNameByID(id)

    local concatenatedStrings = ""
    for speciesID, species in pairs(SpeciesData.species) do
        for _, need in ipairs(species[SpeciesData.NEEDS]) do

            if need[SpeciesData.NEED_NAME] == goodName then

                local speciesString = frame:expandTemplate{
                    title = "Species_link/view",
                    args = {
                        ["iconfilename"] = species[SpeciesData.ICON_FILENAME] .. ".png",
                        ["name"] = speciesID,
                        ["iconsize"] = "medium",
                        ["display"] = "notext",
                    }, }

                -- Separate multiple entries with a space
                if #concatenatedStrings > 1 then
                    concatenatedStrings = concatenatedStrings .. " "
                end

                concatenatedStrings = concatenatedStrings .. speciesString
            end

        end
    end

    if #concatenatedStrings < 1 then
        return nil
    end

    return concatenatedStrings
end



-- Building interface:
-- getCategory
-- getCityScore
-- getConstructionCosts (as [goodName] = stack size)
-- getConstructionTime
-- getDescription
-- getIcon
-- isMovable
-- getName
-- getNumberOfWorkplaces
-- getSize (as "X x Y")
-- getStorage
local function buildViewParameters(id)

    local frame = mw.getCurrentFrame()

    local viewParameters = {}
    viewParameters[PARAM_TITLE] = GoodsData.getName(id)
    viewParameters[PARAM_SUBTITLE] = GoodsData.getCategory(id)
    viewParameters[PARAM_DESCRIPTION] = ControllerUtilities.findAndReplaceSpriteTagsWithFiles(GoodsData.getDescription(id), frame)
    viewParameters[PARAM_ICON_FILENAME] = GoodsData.getIcon(id)
    viewParameters[PARAM_PREFERENCES] = renderPreferences(id)
    viewParameters[PARAM_RECIPES] = frame:expandTemplate{ title = "Recipe", args = { ["product"] = GoodsData.getName(id), ["display"] = "list", }, }
    local burnTime = GoodsData.getBurnTime(id) or 0
    local timeString = string.format( "%02d:%02d", math.floor(burnTime/60), math.fmod(burnTime, 60) )
    viewParameters[PARAM_BURN_TIME] = burnTime ~= 0 and timeString
    viewParameters[PARAM_SELL_VALUE] = GoodsData.getSellValue(id)
    viewParameters[PARAM_SELL_VALUE_AFTER_PRESTIGE_PENALTY] = GoodsData.getSellValueAfterPrestigePenalty(id)
    viewParameters[PARAM_BUY_VALUE] = GoodsData.getBuyValue(id)
    viewParameters[PARAM_ID] = id

    -- Augment the subtitle according to preferences. This is hard-code-y, but it's simple and won't break if the categories change, it'll just get skipped.
    if viewParameters[PARAM_PREFERENCES] then
        if viewParameters[PARAM_SUBTITLE] == "Food" then
            viewParameters[PARAM_SUBTITLE] = "Food (Complex Food)"
        else
            if viewParameters[PARAM_SUBTITLE] == "Consumable Items" then
                viewParameters[PARAM_SUBTITLE] = "Consumable Items (Service Goods)"
            end
        end
    end

    return viewParameters
end



---findBoxData
--- Handles data only, does not interface with the view.
---@param name string the name of the resource about which to retrieve data
---@return table the essential data about the resource extracted
local function findBoxData(name)

    local id = GoodsData.getGoodID(name)
    if not id then
        error("No resource found with name: " .. name .. ".")
    end

    local viewParameters = buildViewParameters(id)
    if not viewParameters then
        error("No resource found with name: " .. name .. ".")
    end

    return viewParameters
end

--endregion



--region Public methods

---main
--- For calling from Template:Goodbox. After handling the frame, forwards control to the primary method, findBoxData, then calls the view template with the compiled box data. Does not interface with any data modules.
---
---@param frame table the calling template's context
---@return string wiki markup, constructed with the template view
function Goodbox.main(frame)

    local name = frame.args[ARG_NAME]
    if not name then
        error("You must specify the name of the good or resource. Please see the template documentation for how to use the parameters.")
    end

    local viewParameters = findBoxData(name)

    return frame:expandTemplate{ title = ViewTemplate, args = viewParameters }
end

--endregion

return Goodbox