Jump to content

Module:GameData: Difference between revisions

From Bconomy Wiki
GameReferenceSync (talk | contribs)
Synchronize public game reference data
 
GameReferenceSync (talk | contribs)
Synchronize public game reference data
Line 68: Line 68:
     elseif type(value) == 'number' then kind = 'number' end
     elseif type(value) == 'number' then kind = 'number' end
     return format({value = value, format = kind})
     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)
    local entry = item(id)
    if not entry then return missing('item ' .. tostring(id)) end
    return '[[' .. mw.text.nowiki(entry.article) .. '|' .. mw.text.nowiki(entry.name) .. ']]'
end
end


Line 74: Line 96:
     local entry = item(a.id or a[1] or '')
     local entry = item(a.id or a[1] or '')
     if not entry then return missing('item ' .. (a.id or a[1] or '')) end
     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-item-infobox')
     local box = mw.html.create('table'):addClass('wikitable bc-item-infobox')
     box:tag('caption'):wikitext(mw.text.nowiki(entry.name))
     local caption = box:tag('caption'):wikitext(mw.text.nowiki(entry.name))
    if presentation.boostTier then
        local tier = presentation.boostTier
        caption:tag('span'):addClass('bc-item-tier'):wikitext(' (T' .. number(tier.tier) ..
            (tier.category and ' ' .. mw.text.nowiki(tier.category) or '') .. ')')
    end
     local function row(label, value)
     local function row(label, value)
         if value == nil then return end
         if not value or value == '' then return end
         local tr = box:tag('tr')
         local tr = box:tag('tr')
         tr:tag('th'):attr('scope', 'row'):wikitext(label)
         tr:tag('th'):attr('scope', 'row'):wikitext(label)
         tr:tag('td'):wikitext(value)
         tr:tag('td'):wikitext(value)
    end
    local function number(value)
        return value and mw.language.getContentLanguage():formatNum(value) or nil
     end
     end
     if entry.imageFile then
     if entry.imageFile then
Line 89: Line 114:
             :wikitext('[[File:' .. mw.text.nowiki(entry.imageFile) .. '|128px|alt=' .. mw.text.nowiki(entry.name) .. ']]')
             :wikitext('[[File:' .. mw.text.nowiki(entry.imageFile) .. '|128px|alt=' .. mw.text.nowiki(entry.name) .. ']]')
     end
     end
     if entry.desc then row('Description', mw.text.nowiki(entry.desc)) end
     row('Description', entry.desc and mw.text.nowiki(entry.desc))
    row('Item ID', tostring(entry.id))
     if entry.retired then row('Status', '[[:Category:Removed features|Removed from the current game]]') end
     if entry.retired then row('Status', 'Retired from the current game catalog') end
     row('Base worth', compact(entry.cost) .. ' BC per item')
     row('Base worth', number(entry.cost) .. ' BC')
     local attributes = {}
     local attributes = {}
     for _, value in ipairs(entry.attributes) do attributes[value] = true end
     for _, value in ipairs(entry.attributes) do attributes[value] = true end
    row('Account bound', attributes.accountBound and 'Yes' or 'No')
    row('Market listing', (attributes.accountBound or attributes.excludeFromMarket) and 'Unavailable' or 'Allowed on eligible profiles')
    row('Equippable', attributes.equippable and 'Yes' or 'No')
    row('Pet holdable', (attributes.petHoldable or entry.damageMultiplier) and 'Yes' or 'No')
    local labels = {petHoldable='Pet holdable', equippable='Equippable', excludeFromMarket='Excluded from Market',
        accountBound='Account bound', relayRestricted='Relay restricted', relayItem='Relay item'}
     local flags = {}
     local flags = {}
     for _, value in ipairs(entry.attributes) do table.insert(flags, mw.text.nowiki(labels[value] or value)) end
     if entry.useLimit then table.insert(flags, 'Usable') end
     if #flags > 0 then row('Attributes', table.concat(flags, ', ')) end
    if attributes.equippable then table.insert(flags, '[[Equipment|Equippable]]') end
     if entry.growTime then row('Base crop grow time', duration(entry.growTime)) end
    if attributes.petHoldable or entry.damageMultiplier then table.insert(flags, '[[Pets|Pet holdable]]') end
     if entry.cooldownTime then row('Cooldown', duration(entry.cooldownTime)) end
     if attributes.accountBound then table.insert(flags, 'Account bound') end
     row('Base damage', number(entry.damage))
    if attributes.relayItem then table.insert(flags, '[[Relay Network|Relay item]]') end
    row('Damage / worth', entry.damageMultiplier and number(entry.damageMultiplier) .. '×' or nil)
     if attributes.relayRestricted then table.insert(flags, 'Cannot be [[Refinery|refined]]') end
    row('Food tier', number(entry.foodTier))
     if attributes.excludeFromMarket and not attributes.accountBound then table.insert(flags, 'Cannot be listed on the [[Market]]') end
     row('Base pet energy', number(entry.petEnergy))
     row('Properties', table.concat(flags, ' · '))
    if entry.damage then
        row('Damage', compact(entry.damage) .. (entry.damageMultiplier and
            ' (' .. number(entry.damageMultiplier) .. '× base worth)' or ''))
     end
    local effects = {}
     local function boost(label, effect)
     local function boost(label, effect)
         if effect then row(label, number(effect.multiplier) .. '× for ' .. duration(effect.duration)) end
         if effect then table.insert(effects, number(effect.multiplier) .. '× ' .. label .. ' for ' .. duration(effect.duration)) end
     end
     end
     boost('Pet adventure boost', entry.petAdventureBoostEffect)
     boost('Pet Adventure Speed', entry.petAdventureBoostEffect)
     boost('Farm plot boost', entry.farmPlotBoostEffect)
     boost('Farm Plot Multiplier', entry.farmPlotBoostEffect)
     boost('Augment', entry.augmentProperties)
     boost('Tool Augment', entry.augmentProperties)
     if entry.effect then
     if entry.effect then
         local label = entry.effect.type == 'actionBoost' and (entry.effect.action .. ' action boost') or 'Rare loot boost'
         local actions = {work='Work', fish='Fish', hunt='Hunt', mine='Mine', explore='Explore'}
         boost(mw.text.nowiki(label), entry.effect)
        boost(entry.effect.type == 'actionBoost' and (actions[entry.effect.action] or mw.text.nowiki(entry.effect.action)) or 'Rare Loot', entry.effect)
    end
    row('When used', table.concat(effects, '<br />'))
    if entry.cooldownTime then row('Cooldown', duration(entry.cooldownTime)) end
    if entry.growTime then row('Farm', '[[Farm|Harvestable]] · ' .. duration(entry.growTime) .. ' base grow time') 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
            (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
     end
     for field, label in pairs({worthPerGainedFromBurn='Base BC burned per item',
     -- Older retained records may predate presentation metadata.
        worthPerGainedFromPetFeed='Base feeding worth per item', worthPerGainedFromHarvest='Base harvest worth per item'}) do
    if not presentation.lootSources then
        row(label, number(entry[field]))
        for _, source in ipairs(entry.lootSources) do table.insert(obtained, linkByName(source)) end
     end
     end
     if entry.useLimit then row('Use limit', number(entry.useLimit)) 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 = {}
     local recipe = {}
     for _, ingredient in ipairs(entry.recipe) do
     for _, ingredient in ipairs(entry.recipe) do
         table.insert(recipe, linkByName(ingredient[1]) .. ' × ' .. number(ingredient[2]))
         table.insert(recipe, linkByName(ingredient[1]) .. ' × ' .. compact(ingredient[2]))
     end
     end
     row('Crafting recipe', entry.uncraftable and 'Not craftable' or table.concat(recipe, '<br />'))
     if #recipe > 0 then row('Crafting recipe', table.concat(recipe, '<br />')) end
    local uses = {}
     box:tag('tr'):tag('td'):attr('colspan', 2):addClass('bc-item-technical')
    for _, name in ipairs(entry.usedToCraft) do table.insert(uses, linkByName(name)) end
        :wikitext('Base values before player bonuses. [[GameData:Reference.json|Game data]] · Item ID ' .. tostring(entry.id))
     if #uses > 0 then row('Used to craft', table.concat(uses, ', ')) end
    local sources = {}
    for _, source in ipairs(entry.lootSources) do table.insert(sources, mw.text.nowiki(source)) end
    if #sources > 0 then row('Loot pools', table.concat(sources, ', ')) end
    row('Source', '[[GameData:Reference.json|Synchronized game definitions]]; base properties before player bonuses')
     return tostring(box)
     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)) 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, '[[' .. 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))
            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
end



Revision as of 01:31, 4 November 2026

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 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 linkByName(name)
    for _, entry in pairs(data().items) do
        if entry.idName == name then
            return '[[' .. mw.text.nowiki(entry.article) .. '|' .. mw.text.nowiki(entry.name) .. ']]'
        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 kind = 'duration'
    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)
    local entry = item(id)
    if not entry then return missing('item ' .. tostring(id)) end
    return '[[' .. mw.text.nowiki(entry.article) .. '|' .. mw.text.nowiki(entry.name) .. ']]'
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-item-infobox')
    local caption = box:tag('caption'):wikitext(mw.text.nowiki(entry.name))
    if presentation.boostTier then
        local tier = presentation.boostTier
        caption:tag('span'):addClass('bc-item-tier'):wikitext(' (T' .. number(tier.tier) ..
            (tier.category and ' ' .. mw.text.nowiki(tier.category) or '') .. ')')
    end
    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 then
        box:tag('tr'):tag('td'):attr('colspan', 2):addClass('bc-item-picture')
            :wikitext('[[File:' .. mw.text.nowiki(entry.imageFile) .. '|128px|alt=' .. mw.text.nowiki(entry.name) .. ']]')
    end
    row('Description', entry.desc and mw.text.nowiki(entry.desc))
    if entry.retired then row('Status', '[[:Category:Removed features|Removed from the current game]]') end
    row('Base worth', compact(entry.cost) .. ' BC per item')
    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) .. '× base worth)' or ''))
    end
    local effects = {}
    local function boost(label, effect)
        if effect then table.insert(effects, number(effect.multiplier) .. '× ' .. label .. ' for ' .. duration(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('When used', table.concat(effects, '<br />'))
    if entry.cooldownTime then row('Cooldown', duration(entry.cooldownTime)) end
    if entry.growTime then row('Farm', '[[Farm|Harvestable]] · ' .. duration(entry.growTime) .. ' base grow time') 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
            (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('Crafting recipe', table.concat(recipe, '<br />')) end
    box:tag('tr'):tag('td'):attr('colspan', 2):addClass('bc-item-technical')
        :wikitext('Base values before player bonuses. [[GameData:Reference.json|Game data]] · Item ID ' .. tostring(entry.id))
    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)) 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, '[[' .. 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))
            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