Module:Buildables

From Dune: Awakening Community Wiki
Jump to navigation Jump to search

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

local p = {}

local common = require("Module:Common")

-- ---------- Translation ----------

-- resolves a piece name, checking the shared Buildables bundle first with fallback to the set-specific bundle
local function translatePiece(setSlug, id, lang)
    local key = id .. "-name"
    return common.translate("Buildables", key, lang, false, true)
        or common.translate("Buildables/" .. setSlug, key, lang)
end

-- ---------- Data ----------

-- loads dataset for the specified set
local function loadSet(setSlug)
    local ok, data = pcall(require, "Module:Buildables/data/" .. setSlug)
    if not ok then return nil, nil end

    local categories = {}
    for categorySlug in pairs(data) do
        categories[#categories + 1] = categorySlug
    end
    table.sort(categories)

    return data, categories
end

-- flattens a two-level table keyed by category into both a flat list sorted by display name, and a table keyed by id
local function flattenSet(data, categories, setSlug)
    local list = {}
    local byId = {}

    for _, categorySlug in ipairs(categories) do
        for _, piece in ipairs(data[categorySlug]) do
            piece.category = categorySlug
            piece.sortName = translatePiece(setSlug, piece.id):lower()
            list[#list + 1] = piece
            byId[piece.id] = piece
        end
    end

    table.sort(list, function(a, b) return a.sortName < b.sortName end)

    return list, byId
end

-- returns a sorted array of "Name (xN)" strings for a piece's building cost
local function formatCostItems(cost)
    local items = {}
    for material, quantity in pairs(cost) do
        items[#items + 1] = string.format("%s (x%d)", material, quantity)
    end

    table.sort(items)
    return items
end

-- resolves and validates the specified set
local function resolveSet(frame)
    local rawSet = common.trim(frame.args[1])
    if rawSet == "" then return nil, nil, nil, common.error("No set specified.") end

    local setSlug = common.slugify(rawSet)

    local data, categories = loadSet(setSlug)
    if not data then return nil, nil, nil, common.error("Unknown set: %s", rawSet) end

    return setSlug, data, categories
end

-- ---------- Rendering ----------

local function renderSourceCell(source)
    local bundleName, slug = common.parseSourceToken(source)
    if not bundleName then return common.error("Malformed source token: %s", source) end

    local key = slug .. "-name"
    return common.translate(bundleName, key, nil, true)
end

local function renderCostCell(cost)
    local items = formatCostItems(cost)

    -- manually generate a left-aligned bullet list
    local wrapper = mw.html.create("div")
        :css("display", "inline-block")
        :css("text-align", "left")

    local ul = wrapper:tag("ul")
    for _, item in ipairs(items) do
        ul:tag("li"):wikitext(item)
    end

    return wrapper
end

local function renderTable(list, setSlug, showSource, showCategory)
    local table_ = common.newSortableTable(#list, "Buildables", "label-title")
    local headerRow = table_:tag("tr")

    headerRow:tag("th"):addClass("unsortable"):wikitext(common.translate("Common", "label-icon"))
    headerRow:tag("th"):wikitext(common.translate("Common", "label-name"))

    if showCategory then
        headerRow:tag("th"):wikitext(common.translate("Common", "label-category"))
    end

    headerRow:tag("th"):wikitext(common.translate("Common", "label-health"))
    headerRow:tag("th"):addClass("unsortable"):wikitext(common.translate("Common", "label-cost"))

    if showSource then
        headerRow:tag("th"):wikitext(common.translate("Common", "label-source"))
    end

    if #list == 0 then
        local row = table_:tag("tr")
        row:tag("td"):attr("colspan", 99):css("font-style", "italic"):wikitext("No matching pieces.")
        return table_
    end

    for _, piece in ipairs(list) do
        local displayName = translatePiece(setSlug, piece.id)

        local row = table_:tag("tr")

        row:tag("td"):css("min-width", "128px"):wikitext(string.format("[[File:%s|128px]]", piece.image))
        row:tag("td"):css("white-space", "nowrap"):css("font-weight", "bold"):wikitext(displayName)

        if showCategory then
            local displayCategory = common.translate("Buildables", "category-" .. piece.category)
            row:tag("td"):css("white-space", "nowrap"):wikitext(displayCategory)
        end

        row:tag("td"):css("white-space", "nowrap"):wikitext(piece.health)
        row:tag("td"):css("white-space", "nowrap"):node(renderCostCell(piece.cost))

        if showSource then
            row:tag("td"):css("white-space", "nowrap"):wikitext(renderSourceCell(piece.source))
        end
    end

    return table_
end

local function renderInfobox(setSlug, piece)
    local displayName = translatePiece(setSlug, piece.id)
    local englishName = translatePiece(setSlug, piece.id, "en")

    local displayCategory = common.translate("Buildables", "category-" .. piece.category)
    local englishCategory = common.translate("Buildables", "category-" .. piece.category, "en")

    local displaySet = common.translate("Buildables/" .. setSlug, "set-name")
    local englishSet = common.translate("Buildables/" .. setSlug, "set-name", "en")

    local out = {}
    out[#out + 1] = "{{BuildableInfobox"

    out[#out + 1] = "|title=" .. englishName
    out[#out + 1] = "|display_title=" .. displayName
    out[#out + 1] = "|set=" .. englishSet
    out[#out + 1] = "|display_set=" .. displaySet
    out[#out + 1] = "|category=" .. englishCategory
    out[#out + 1] = "|display_category=" .. displayCategory
    out[#out + 1] = "|health=" .. piece.health
    out[#out + 1] = "|cost=" .. table.concat(formatCostItems(piece.cost), ",")

    if piece.source then
        out[#out + 1] = "|source=" .. renderSourceCell(piece.source)
    end

    out[#out + 1] = "}}"
    return table.concat(out, "\n")
end

-- ---------- Interface ----------

function p.list(frame)
    local setSlug, data, categories, err = resolveSet(frame)
    if err then return err end

    local rawCategory = common.trim(frame.args.category)
    local catFilter = rawCategory ~= "" and common.slugify(rawCategory) or ""

    if catFilter ~= "" and not common.listContains(categories, catFilter) then
        return common.error("Unknown category: %s", rawCategory)
    end

    local rawSource = common.trim(frame.args.source)
    local sourceFilter = rawSource ~= "" and common.slugify(rawSource) or ""

    local list
    if catFilter ~= "" then
        list = flattenSet(data, { catFilter }, setSlug)
    else
        list = flattenSet(data, categories, setSlug)
    end

    local filtered = {}
    for _, piece in ipairs(list) do
        local sourceMatch = true

        if sourceFilter ~= "" then
            local _, slug = common.parseSourceToken(piece.source)
            sourceMatch = slug == sourceFilter
        end

        if sourceMatch then filtered[#filtered + 1] = piece end
    end

    local showSource = sourceFilter == ""
    local showCategory = catFilter == ""
    return tostring(renderTable(filtered, setSlug, showSource, showCategory))
end

function p.get(frame)
    local setSlug, data, categories, err = resolveSet(frame)
    if err then return err end

    local rawName = common.trim(frame.args[2])
    if rawName == "" then return common.error("No piece specified.") end

    local idSlug = common.slugify(rawName)
    local _, byId = flattenSet(data, categories, setSlug)

    local piece = byId[idSlug]
    if not piece then return common.error("Unknown piece: %s", rawName) end

    return frame:preprocess(renderInfobox(setSlug, piece))
end

return p