Jump to content

Module:GameData

From Bconomy Wiki
Revision as of 15:16, 4 November 2026 by GameReferenceSync (talk | contribs)

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

local p = {}
local snapshot

local function data()
    if not snapshot then snapshot = mw.loadJsonData('GameData:Reference.json') end
    return snapshot
end

local function missing(key)
    return '<strong class="error">Missing game reference: ' .. mw.text.nowiki(key) ..
        '</strong>[[Category:Pages with missing game references]]'
end

local function duration(value)
    local seconds = value / 1000
    local units = {{86400, 'day'}, {3600, 'hour'}, {60, 'minute'}, {1, 'second'}}
    for _, unit in ipairs(units) do
        if seconds >= unit[1] then
            local amount = math.floor(seconds / unit[1] * 100 + 0.5) / 100
            return mw.language.getContentLanguage():formatNum(amount) .. ' ' .. unit[2] .. (amount == 1 and '' or 's')
        end
    end
    return tostring(value) .. ' milliseconds'
end

local function itemDuration(value)
    local parts = {}
    local remaining = value
    for _, unit in ipairs({{86400000, 'd'}, {3600000, 'h'}, {60000, 'm'}, {1000, 's'}, {1, 'ms'}}) do
        local amount = math.floor(remaining / unit[1])
        if amount > 0 then
            table.insert(parts, tostring(amount) .. unit[2])
            remaining = remaining - amount * unit[1]
        end
    end
    return #parts > 0 and table.concat(parts) or '0s'
end

local function format(record)
    if record.format == 'markdown' then return record.wikitext or missing('markdown') end
    if record.format == 'duration' then return duration(tonumber(record.value)) end
    if record.format == 'number' then return mw.language.getContentLanguage():formatNum(record.value) end
    return mw.text.nowiki(tostring(record.value))
end

local function args(frame)
    return frame:getParent() and frame:getParent().args or frame.args
end

function p.value(frame)
    local a = args(frame)
    local key = mw.text.trim(a[1] or '')
    local record = data().values[key]
    if not record then return missing(key) end
    return format(record)
end

local function item(id)
    if not tostring(id):match('^%d+$') then return nil end
    return data().items['item' .. tostring(tonumber(id))]
end

local function iconFile(filename)
    if not filename then return '' end
    return '<span class="bc-item-inline-icon">[[File:' .. mw.text.nowiki(filename) .. '|20x20px|middle|link=|alt=]]</span>'
end

local function icon(emoji)
    local record = (data().icons or {})[emoji]
    return record and iconFile(record.imageFile) or ''
end

local function boostType(entry)
    local tier = (entry.presentation or {}).boostTier
    if not tier then return nil end
    local category = tier.category == 'Tool' and 'Tool Augment' or (tier.category or '') .. ' Boost'
    return 'T' .. mw.language.getContentLanguage():formatNum(tier.tier) .. ' ' .. category
end

local function itemType(entry)
    local boost = boostType(entry)
    if boost then return boost end
    if entry.effect then return 'Rare Loot Boost' end
    if entry.damage then return 'Weapon' end
    for _, attribute in ipairs(entry.attributes) do
        if attribute == 'equippable' then return 'Equipment' end
    end
    if (entry.presentation or {}).loot and entry.presentation.loot[1] then return 'Loot Container' end
    if entry.growTime then return 'Crop' end
    for _, attribute in ipairs(entry.attributes) do
        if attribute == 'relayItem' then return 'Relay Material' end
    end
    for _, source in ipairs(entry.lootSources) do
        if source == 'rare' then return 'Rare Item' end
    end
    if entry.usedToCraft[1] then return 'Crafting Ingredient' end
    if entry.useLimit then return 'Usable Item' end
    if entry.petEnergy then return 'Pet Food' end
    return 'Material'
end

local function entryLink(entry, detailed)
    local kind = detailed and boostType(entry)
    local suffix = kind and '<span class="bc-item-link-tier"> (' .. mw.text.nowiki(kind) .. ')</span>' or ''
    return iconFile(entry.imageFile) .. '[[' .. mw.text.nowiki(entry.article) .. '|' .. mw.text.nowiki(entry.name) .. ']]' .. suffix
end

local function linkByName(name, detailed)
    for _, entry in pairs(data().items) do
        if entry.idName == name then
            return entryLink(entry, detailed)
        end
    end
    return mw.text.nowiki(name)
end

function p.itemValue(frame)
    local a = args(frame)
    local entry = item(a.id or a[1] or '')
    if not entry then return missing('item ' .. (a.id or a[1] or '')) end
    local field = a.field or a[2] or ''
    local value = entry[field]
    if value == nil or type(value) == 'table' then return missing(field) end
    local kind = 'text'
    if field == 'growTime' or field == 'cooldownTime' then return itemDuration(value)
    elseif type(value) == 'number' then kind = 'number' end
    return format({value = value, format = kind})
end

local function number(value)
    return mw.language.getContentLanguage():formatNum(value)
end

local function compact(value)
    local units = {{1e12, 'T'}, {1e9, 'B'}, {1e6, 'M'}, {1e3, 'K'}}
    for _, unit in ipairs(units) do
        if value >= unit[1] then
            local amount = math.floor(value / unit[1] * 100 + 0.5) / 100
            return tostring(mw.html.create('abbr'):attr('title', number(value))
                :wikitext(number(amount) .. unit[2]))
        end
    end
    return number(value)
end

local function itemLink(id, detailed)
    local entry = item(id)
    if not entry then return missing('item ' .. tostring(id)) end
    return entryLink(entry, detailed)
end

function p.infobox(frame)
    local a = args(frame)
    local entry = item(a.id or a[1] or '')
    if not entry then return missing('item ' .. (a.id or a[1] or '')) end
    local presentation = entry.presentation or {}
    local box = mw.html.create('table'):addClass('wikitable bc-infobox bc-item-infobox')
    box:tag('caption'):wikitext(mw.text.nowiki(entry.name))
    local function row(label, value)
        if not value or value == '' then return end
        local tr = box:tag('tr')
        tr:tag('th'):attr('scope', 'row'):wikitext(label)
        tr:tag('td'):wikitext(value)
    end
    if entry.imageFile or entry.desc then
        local picture = box:tag('tr'):tag('td'):attr('colspan', 2):addClass('bc-item-picture')
        if entry.imageFile then picture:wikitext('[[File:' .. mw.text.nowiki(entry.imageFile) .. '|128px|alt=' .. mw.text.nowiki(entry.name) .. ']]') end
        if entry.desc then picture:tag('blockquote'):addClass('bc-item-flavor'):wikitext(mw.text.nowiki(entry.desc)) end
    end
    row('Type', itemType(entry))
    if entry.retired then row('Status', '[[:Category:Removed features|Removed from the current game]]') end
    row('Worth', compact(entry.cost) .. ' BC')
    local attributes = {}
    for _, value in ipairs(entry.attributes) do attributes[value] = true end
    local flags = {}
    if entry.useLimit then table.insert(flags, 'Usable') end
    if attributes.equippable then table.insert(flags, '[[Equipment|Equippable]]') end
    if attributes.petHoldable or entry.damageMultiplier then table.insert(flags, '[[Pets|Pet holdable]]') end
    if attributes.accountBound then table.insert(flags, 'Account bound') end
    if attributes.relayItem then table.insert(flags, '[[Relay Network|Relay item]]') end
    if attributes.relayRestricted then table.insert(flags, 'Cannot be [[Refinery|refined]]') end
    if attributes.excludeFromMarket and not attributes.accountBound then table.insert(flags, 'Cannot be listed on the [[Market]]') end
    row('Properties', table.concat(flags, ' · '))
    if entry.damage then
        row('Damage', compact(entry.damage) .. (entry.damageMultiplier and
            ' (' .. number(entry.damageMultiplier) .. '× worth)' or ''))
    end
    local effects = {}
    local function boost(label, effect)
        if effect then table.insert(effects, number(effect.multiplier) .. '× ' .. label .. ' for ' .. itemDuration(effect.duration)) end
    end
    boost('Pet Adventure Speed', entry.petAdventureBoostEffect)
    boost('Farm Plot Multiplier', entry.farmPlotBoostEffect)
    boost('Tool Augment', entry.augmentProperties)
    if entry.effect then
        local actions = {work='Work', fish='Fish', hunt='Hunt', mine='Mine', explore='Explore'}
        boost(entry.effect.type == 'actionBoost' and (actions[entry.effect.action] or mw.text.nowiki(entry.effect.action)) or 'Rare Loot', entry.effect)
    end
    row('Effect', table.concat(effects, '<br />'))
    if entry.cooldownTime then row('Cooldown', itemDuration(entry.cooldownTime)) end
    if entry.growTime then row('Grow time', itemDuration(entry.growTime)) end
    if entry.petEnergy then
        row('Pet food', compact(entry.petEnergy) .. ' energy per item' ..
            (entry.foodTier and ' · Food tier ' .. number(entry.foodTier) or ''))
    end
    local obtained = {}
    for _, source in ipairs(presentation.lootSources or {}) do
        local article = ({fish='Fishing', hunt='Hunting', mine='Mining', explore='Explore'})[source.source]
        local target = source.itemId and itemLink(source.itemId) or
            (source.emoji and icon(source.emoji) or '') ..
            (article and '[[' .. article .. '|' .. mw.text.nowiki(source.label) .. ']]' or mw.text.nowiki(source.label))
        local label = source.itemId and (source.source == 'rare' and 'Rare Loot / ' .. target or target .. ' Loot') or target
        table.insert(obtained, label .. ' (' .. string.format('%.2f', math.max(0.01, source.percent)) .. '%)')
    end
    -- Older retained records may predate presentation metadata.
    if not presentation.lootSources then
        for _, source in ipairs(entry.lootSources) do table.insert(obtained, linkByName(source)) end
    end
    if entry.worthPerGainedFromBurn then table.insert(obtained, '[[Burn]]: 1 per ' .. compact(entry.worthPerGainedFromBurn) .. ' BC burned on average, before perks') end
    if entry.worthPerGainedFromPetFeed then table.insert(obtained, '[[Pets|Pet feeding]]: 1 per ' .. compact(entry.worthPerGainedFromPetFeed) .. ' BC of food fed on average, before perks') end
    if entry.worthPerGainedFromHarvest then table.insert(obtained, '[[Farm]]: 1 per ' .. compact(entry.worthPerGainedFromHarvest) .. ' BC of unboosted crops harvested on average, before perks') end
    row('Obtained from', table.concat(obtained, '<br />'))
    local recipe = {}
    for _, ingredient in ipairs(entry.recipe) do
        table.insert(recipe, linkByName(ingredient[1]) .. ' × ' .. compact(ingredient[2]))
    end
    if #recipe > 0 then row('Recipe', table.concat(recipe, '<br />')) end
    row('Internal ID', '<code>' .. tostring(entry.id) .. '</code>')
    return tostring(box)
end

function p.itemSections(frame)
    local a = args(frame)
    local entry = item(a.id or a[1] or '')
    if not entry then return missing('item ' .. (a.id or a[1] or '')) end
    local output = {}
    local presentation = entry.presentation or {}
    local usages = presentation.uses or {}
    local categories = {tools='Tools', farm='Farm', pets='Pets', relay='Relay Network'}
    local groups = {}
    local crafting = {}
    for _, name in ipairs(entry.usedToCraft) do table.insert(crafting, linkByName(name, true)) end
    table.insert(groups, {title='Used to craft', entries=crafting})
    for _, group in ipairs({{'upgrades', 'Used to upgrade'}, {'purchases', 'Used to purchase'}, {'research', 'Used to research'}}) do
        local lines = {}
        for _, usage in ipairs(usages[group[1]] or {}) do
            local suffix = ''
            if usage.threshold then
                local prefix = ({bay='#', count='#', tier='Tier ', level='Lv. '})[usage.thresholdKind] or ''
                suffix = ' (' .. prefix .. number(usage.threshold) .. '+)'
            end
            table.insert(lines, (usage.emoji and icon(usage.emoji) or '') .. '[[' .. categories[usage.category] .. '|' .. mw.text.nowiki(usage.label) .. ']]' .. suffix)
        end
        table.insert(groups, {title=group[2], entries=lines})
    end
    for _, group in ipairs(groups) do
        if #group.entries > 0 then
            if #output == 0 then table.insert(output, '\n== Item uses ==\n') end
            table.insert(output, '\n=== ' .. group.title .. ' ===\n* ' .. table.concat(group.entries, '\n* ') .. '\n')
        end
    end
    if presentation.loot and presentation.loot[1] then
        table.insert(output, '\n== Loot ==\nThe following chances apply to each draw from this item’s loot pool.\n')
        local loot = mw.html.create('table'):addClass('wikitable bc-item-loot')
        local header = loot:tag('tr')
        for _, title in ipairs({'Item', 'Chance', 'Odds'}) do header:tag('th'):attr('scope', 'col'):wikitext(title) end
        for _, reward in ipairs(presentation.loot) do
            local tr = loot:tag('tr')
            tr:tag('td'):wikitext(itemLink(reward.itemId, true))
            tr:tag('td'):wikitext(string.format('%.2f', reward.probability * 100) .. '%')
            tr:tag('td'):wikitext(mw.text.nowiki(reward.odds))
        end
        table.insert(output, tostring(loot) .. '\n')
    end
    return table.concat(output)
end

function p.referenceTable(frame)
    local key = args(frame)[1] or ''
    local source = data().tables[key]
    if not source then return missing(key) end
    local output = mw.html.create('table'):addClass('wikitable')
    local head = output:tag('tr')
    for _, column in ipairs(source.columns) do head:tag('th'):attr('scope', 'col'):wikitext(mw.text.nowiki(column)) end
    for _, record in ipairs(source.rows) do
        local tr = output:tag('tr')
        for _, value in ipairs(record) do
            tr:tag('td'):wikitext(format({value=value, format=type(value) == 'number' and 'number' or 'text'}))
        end
    end
    return tostring(output)
end

return p