Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

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

-- Module:Difficulties
local p = {}

-- Helper to capitalize first letter of each word (after converting underscores to spaces)
local function title_case(str)
    if not str or str == '' then
        return str
    end
    local spaced_str = mw.ustring.gsub(str, '_', ' ')
    -- Use mw.ustring.gsub with a pattern to find words and capitalize their first letter
    -- %f[%a] is a frontier pattern matching a position where a non-alphabetic char is followed by an alphabetic one (or start of string)
    return mw.ustring.gsub(spaced_str, '%f[%a](%a+)', function(word)
        return mw.ustring.upper(mw.ustring.sub(word, 1, 1)) .. mw.ustring.sub(word, 2)
    end)
end

-- Simple ucfirst for first letter only, no underscore handling
local function ucfirst_first_char(str)
    if not str or str == '' then
        return str
    end
    return mw.ustring.upper(mw.ustring.sub(str, 1, 1))
        .. mw.ustring.sub(str, 2)
end


local function trim(s)
    return s and mw.text.trim(s) or nil
end

local function is_true(v)
    if v == nil then
        return false
    end
    local s = mw.text.trim(tostring(v)):lower()
    return s == 'true' or s == 'yes' or s == 'y' or s == '1'
end

local function split_list(s)
    local out = {}
    if not s then
        return out
    end
    for token in mw.ustring.gmatch(s, '([^,;|]+)') do
        local t = trim(token)
        if t and t ~= '' then
            table.insert(out, t)
        end
    end
    return out
end

local function normalize_hex(hex)
    if not hex then
        return nil
    end
    local s = trim(hex)
    if not s or s == '' then
        return nil
    end
    s = s:gsub('^#', '') -- Remove leading # if present
    s = mw.ustring.upper(s) -- Ensure it's uppercase for consistent matching
    -- Lua patterns do not support {6}. Use %x (hex digit) repeated.
    if mw.ustring.match(s, '^%x%x%x%x%x%x$') then
        return '#' .. s
    end
    -- Optional: support 3-digit shorthand like FFF
    if mw.ustring.match(s, '^%x%x%x$') then
        local r = s:sub(1, 1)
        local g = s:sub(2, 2)
        local b = s:sub(3, 3)
        return '#' .. r .. r .. g .. g .. b .. b
    end
    return nil
end

local function make_link(display_text, link_arg)
    if not display_text or display_text == '' then
        return ''
    end
    if not link_arg or link_arg == '' then
        return mw.text.nowiki(display_text)
    end
    local link = trim(link_arg)
    if not link or link == '' then
        return mw.text.nowiki(display_text)
    end
    if mw.ustring.match(link, '^%[%[.*%]%]$') then
        return link
    end
    if mw.ustring.match(link, '^[Hh][Tt][Tt][Pp][Ss]?://') or mw.ustring.match(link, '^//') then
        return '[' .. link .. ' ' .. display_text .. ']'
    end
    return '[[' .. link .. '|' .. display_text .. ']]'
end

local function collect_difficulty_names(args)
    local names, seen = {}, {}

    -- Helper to standardize name for internal use (lowercase snake_case)
    local function standardize_name_for_lookup(raw_name)
        if not raw_name then return nil end
        local cleaned = mw.ustring.gsub(raw_name, '%s+', '_') -- spaces to underscores
        return mw.ustring.lower(cleaned) -- to lowercase
    end

    -- 1) explicit list (preferred)
    for _, n in ipairs(split_list(args.difficulties or args.list)) do
        local key = standardize_name_for_lookup(n)
        if key and not seen[key] then
            table.insert(names, key)
            seen[key] = true
        end
    end

    -- 2) any foo_index discovered
    for k, _ in pairs(args) do
        local name_match = mw.ustring.match(k, '^([%a_][%w_]*)_index$')
        if name_match then
            local key = standardize_name_for_lookup(name_match)
            if key and not seen[key] then
                table.insert(names, key)
                seen[key] = true
            end
        end
    end

    -- 3) any other parameters that imply a difficulty (e.g., 'novice=true', 'Very_Hard_hex=F00')
    for k, _ in pairs(args) do
        local base_name_match = mw.ustring.match(k, '^([%a_][%w_]*)(?:_.*)?$')
        if base_name_match then
            local key = standardize_name_for_lookup(base_name_match)
            if key and key ~= 'difficulties' and key ~= 'list' and
               not mw.ustring.match(key, '^completions?_as_of$') and
               not mw.ustring.match(key, '^as_of(_date)?$')
            then
                if not seen[key] then
                    table.insert(names, key)
                    seen[key] = true
                end
            end
        end
    end

    return names
end

-- Robust argument getter for base_suffix style parameters (e.g., "very_hard_hex")
-- Checks common casing variations for the 'base' part of the parameter.
-- 'base_lower_snake' is the internal lowercase_snake_case name (e.g., "very_hard")
local function get_arg(args, base_lower_snake, suffix)
    if not suffix then
        return nil
    end

    -- Prioritize direct lowercase_snake_case key, then ucfirst first char
    local check_keys = {
        base_lower_snake .. '_' .. suffix,                          -- e.g., very_hard_hex
        ucfirst_first_char(base_lower_snake) .. '_' .. suffix,     -- e.g., Very_hard_hex
    }

    -- If the base name contains an underscore, also check the Title_Case version
    if mw.ustring.find(base_lower_snake, '_') then
        table.insert(check_keys, title_case(base_lower_snake) .. '_' .. suffix) -- e.g., Very_Hard_hex
    end

    for _, key in ipairs(check_keys) do
        if args[key] ~= nil then
            return args[key]
        end
    end
    return nil
end

-- Specific function to check for difficulty_name=true/yes/1 (without suffix)
local function get_arg_difficulty_boolean(args, base_lower_snake)
    return is_true(args[base_lower_snake]) or is_true(args[ucfirst_first_char(base_lower_snake)])
end


function p.main(frame)
    local parent = frame:getParent()
    local args = parent and parent.args or frame.args or {}
    local lang = mw.language.getContentLanguage()

    local output = mw.html.create('table')
    output:addClass('wikitable')
    output:addClass('wikitable--fluid')

    local header_row = output:tag('tr')
    header_row:tag('th'):wikitext('Icon')
    header_row:tag('th'):wikitext('Difficulty')
    header_row:tag('th'):wikitext('Stages')
    header_row:tag('th'):wikitext('Completion %')

    local as_of = trim(args.completions_as_of or args.as_of or args.as_of_date)
    local completions_header = 'Completions'
    if as_of and as_of ~= '' then
        completions_header = 'Completions (as of ' .. as_of .. ')'
    end
    header_row:tag('th'):wikitext(completions_header)

    header_row:tag('th'):wikitext('Rewards'):addClass('right')

    local names = collect_difficulty_names(args)

    local entries = {}
    for _, name in ipairs(names) do -- 'name' is now guaranteed lowercase snake_case
        local enabled = (get_arg(args, name, 'index') ~= nil)
            or (get_arg(args, name, 'display') ~= nil)
            or is_true(get_arg(args, name, 'enabled'))
            or get_arg_difficulty_boolean(args, name)

        if enabled then
            local e = {}
            e.name = name -- lowercase snake_case for internal lookup consistency
            e.index = tonumber(get_arg(args, name, 'index')) or math.huge

            -- FIX FOR DISPLAY CASING: This logic ensures correct fallback
            local explicit_display_value = get_arg(args, name, 'display')
            if explicit_display_value and trim(explicit_display_value) ~= '' then
                e.display = trim(explicit_display_value)
            else
                e.display = title_case(name) -- Fallback, now using title_case function directly
            end

            e.link_arg = trim(get_arg(args, name, 'link'))
            e.file_name = trim(get_arg(args, name, 'file_name'))

            -- FIX FOR HEX COLOR:
            -- Use the robust get_arg for all variations, then normalize.
            local raw_hex = get_arg(args, name, 'hex')
                or get_arg(args, name, 'color_hex')
                or get_arg(args, name, 'color')
            e.color_hex = normalize_hex(raw_hex) -- This should now work correctly with the fixed normalize_hex and get_arg

            e.unreleased = is_true(get_arg(args, name, 'unreleased')) -- Corrected to name_unreleased
                or is_true(args[name .. '_unreleased']) -- Backup check for consistency
                or is_true(args[ucfirst_first_char(name) .. '_unreleased']) -- Check for Ucfirst_unreleased

            e.completion_percent = trim(get_arg(args, name, 'completion'))
            e.total_completions = trim(
                get_arg(args, name, 'total_completions')
                    or get_arg(args, name, 'completions_total')
                    or get_arg(args, name, 'all_time_completions')
            )
            e.badge = trim(get_arg(args, name, 'badge'))
            e.start_arg = trim(get_arg(args, name, 'start'))
            e.end_arg = trim(get_arg(args, name, 'end'))

            table.insert(entries, e)
        end
    end

    table.sort(entries, function(a, b)
        if a.index == b.index then
            return a.display < b.display -- Secondary sort by display name
        end
        return a.index < b.index
    end)

    local inferred_next_start = 0
    local inclonlystr = ''

    for _, e in ipairs(entries) do
        local row = output:tag('tr')

        -- Icon
        local icon_cell = row:tag('td')
        if e.file_name and e.file_name ~= '' then
            icon_cell:wikitext(
                '[[File:' .. e.file_name .. '|48px|alt=Difficulty ' .. e.display .. ']]'
            )
            icon_cell:attr(
                'style',
                'border-radius:8px; overflow:hidden;'
            )
        else
            local bg = e.color_hex or '#888888' -- Fallback still exists, but e.color_hex should now be correct
            icon_cell:tag('div')
                :attr(
                    'style',
                    'width:48px; height:48px; background-color:' ..
                        bg ..
                        '; vertical-align:middle; border-radius:8px; display:inline-block;'
                )
        end

        -- Name (with optional custom link)
        local name_cell = row:tag('td')
        local name_wikitext = make_link(e.display, e.link_arg)
        if e.color_hex then
            name_cell:wikitext(name_wikitext):attr('style', 'color:' .. e.color_hex .. ';')
        else
            name_cell:wikitext(name_wikitext)
        end

        -- Stages
        local stage_cell = row:tag('td')
        local start_num = tonumber(e.start_arg)
        local end_num = tonumber(e.end_arg)

        if start_num == nil then
            start_num = inferred_next_start
        end -- Corrected: matched the 'if'

        if e.unreleased then
            stage_cell:wikitext("''Unreleased''")
        else
            local start_text = start_num ~= nil and tostring(start_num) or '??'
            local end_text = end_num ~= nil and tostring(end_num) or '??'
            stage_cell:wikitext(start_text .. ' - ' .. end_text)
        end

        if end_num ~= nil then
            inferred_next_start = end_num
        else
            inferred_next_start = start_num or inferred_next_start
        end

        -- Completion %
        local completionp_cell = row:tag('td')
        if e.completion_percent and e.completion_percent ~= '' then
            completionp_cell:wikitext(e.completion_percent .. '%')
        else
            completionp_cell:wikitext('??%')
        end

        -- Completions (all-time) with thousands separators
        local comps_cell = row:tag('td')
        if e.total_completions and e.total_completions ~= '' then
            local n = tonumber(e.total_completions)
            if n then
                comps_cell:wikitext(lang:formatNum(n))
            else
                comps_cell:wikitext(e.total_completions)
            end
        else
            comps_cell:wikitext('??')
        end

        -- Rewards (right-aligned, last column)
        local rewards_cell = row:tag('td'):addClass('right')
        if e.badge and e.badge ~= '' then
            rewards_cell:wikitext(
                '[[File:Icons Badge Small White.png|link=https://roblox.com/badges/' ..
                    e.badge ..
                    '/obbywikidifficultylink|alt=Badge|28px]]'
            )
            rewards_cell:attr('style', 'vertical-align:middle;')
        else
            rewards_cell:wikitext("''None''")
        end

        inclonlystr = inclonlystr .. '[[Category:' .. title_case(e.name) .. ']]'
    end

    return tostring(output) .. inclonlystr
end

return p