Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(45 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:AchievementSystem
--[[
-- Achievement system that loads data from MediaWiki:AchievementData.json.
* Name: AchievementSystem
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
* Author: Mark W. Datysgeld
-- This module only assigns CSS classes based on achievement IDs in the format:
* Description: Comprehensive achievement system that manages user badges and titles throughout ICANNWiki, loading data from MediaWiki JSON files and providing rendering functions for Person templates
--   .person-template .template-title.achievement-{id}::after {}
* Notes: Loads from MediaWiki:AchievementData.json (user assignments) and MediaWiki:AchievementList.json (type definitions). CSS styling defined in Templates.css using achievement-{id} format. Includes caching and fallback mechanisms for robust JSON handling
--
]]
-- The module does not use any styling information from the JSON data structure.
 
---@class UserAchievement
---@field type string
---@field date? string


local Achievements = {}
local Achievements = {}
-- Debug configuration
local DEBUG_MODE = true
local function debugLog(message)
    if not DEBUG_MODE then return end
    pcall(function()
        mw.logObject({
            system = "achievement_simple",
            message = message,
            timestamp = os.date('%H:%M:%S')
        })
    end)
    mw.log("ACHIEVEMENT-DEBUG: " .. message)
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- JSON Handling
-- JSON Handling
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local json
-- Helper function to ensure we get an array
local jsonLoaded = pcall(function()
local function ensureArray(value)
     json = require('Module:JSON')
    if type(value) ~= "table" then
end)
        return {}
    end
   
    -- Check if it's an array-like table
    local isArray = true
    local count = 0
    for _ in pairs(value) do
        count = count + 1
    end
   
    -- If it has no numeric indices or is empty, return empty array
    if count == 0 then
        return {}
    end
   
    -- If it's a single string, wrap it in an array
    if count == 1 and type(value[1]) == "string" then
        return {value[1]}
    end
   
    -- If it has a single non-array value, try to convert it to an array
     if count == 1 and next(value) and type(next(value)) ~= "number" then
        local k, v = next(value)
        if type(v) == "string" then
            return {v}
        end
    end
   
    -- Return the original table if it seems to be an array
    return value
end


if not jsonLoaded or not json then
-- Use MediaWiki's built-in JSON functions directly
     json = { decode = function() return nil end }
local function jsonDecode(jsonString)
     debugLog('WARNING: Module:JSON not available, achievement features will be limited')
    if not jsonString then return nil end
      
    if mw.text and mw.text.jsonDecode then
        local success, result = pcall(function()
            -- Use WITHOUT PRESERVE_KEYS flag to ensure proper array handling
            return mw.text.jsonDecode(jsonString)
        end)
       
        if success and result then
            return result
        end
    end
      
    return nil
end
end


Line 52: Line 84:
-- Configuration, Default Data, and Cache
-- Configuration, Default Data, and Cache
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local ACHIEVEMENT_LIST_PAGE = 'MediaWiki:AchievementList.json'
local dataCache = nil
local dataCache = nil
local typesCache = nil


local DEFAULT_DATA = {
local DEFAULT_DATA = {
     schema_version = 1,
     schema_version = 2,
     last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
     last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
     achievement_types = {},
     achievement_types = {},
     user_achievements = {},
     user_achievements = {},
    cache_control = { version = 0 }
}
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (Optional) Testing config
-- Load achievement types from the JSON page
-- Removed forced dev-role injection so it won't override normal JSON lookups.
-- @param frame - The Scribunto frame object for preprocessing
-- @return Array of achievement type definitions
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local TEST_CONFIG = {
function Achievements.loadTypes(frame)
    enabled = true,
    -- Use the request-level cache if we already loaded data once
    test_page_id = "18451",
    if typesCache then
    test_user = "direct-test-user",
        return typesCache
    debug_messages = true,
    end
    force_achievements = false, -- Disabled forcing achievements
 
    type_mapping = {
    local success, types = pcall(function()
        ["jedi"]      = "ach1",
        -- Get the JSON content using frame:preprocess if available
        ["champion"= "ach2",
        local jsonText
        ["sponsor"]  = "ach3",
        if frame and type(frame) == "table" and frame.preprocess then
         ["ach1"]      = "ach1",
            -- Make sure frame is valid and has preprocess method
        ["ach2"]      = "ach2",
            local preprocessSuccess, preprocessResult = pcall(function()
        ["ach3"]      = "ach3",
                return frame:preprocess('{{MediaWiki:AchievementList.json}}')
        ["title-test"] = "dev-role",
            end)
         ["dev-role"]  = "dev-role"
           
     },
            if preprocessSuccess and preprocessResult then
    test_achievements = {"title-test", "ach1", "ach2", "ach3"}
                jsonText = preprocessResult
}
            end
        end
       
        -- If we couldn't get JSON from frame:preprocess, fall back to direct content loading
        if not jsonText then
            -- Try using mw.loadJsonData first (preferred method)
            if mw.loadJsonData then
                local loadJsonSuccess, jsonData = pcall(function()
                    return mw.loadJsonData(ACHIEVEMENT_LIST_PAGE)
                end)
               
                if loadJsonSuccess and jsonData and type(jsonData) == 'table' and jsonData.achievement_types then
                    return jsonData.achievement_types
                end
            end
           
            -- Direct content loading approach as fallback
            local pageTitle = mw.title.new(ACHIEVEMENT_LIST_PAGE)
            if pageTitle and pageTitle.exists then
                -- Get raw content from the wiki page
                local contentSuccess, content = pcall(function()
                    return pageTitle:getContent()
                end)
               
                if contentSuccess and content and content ~= "" then
                    -- Remove any BOM or leading whitespace that might cause issues
                    content = content:gsub("^%s+", "")
                    if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
                        content = content:sub(4)
                    end
                   
                    jsonText = content
                   
                    -- Try different JSON decode approaches
                    if jsonText and mw.text and mw.text.jsonDecode then
                        -- First try WITHOUT PRESERVE_KEYS flag (standard approach)
                        local jsonDecodeSuccess, jsonData = pcall(function()
                            return mw.text.jsonDecode(jsonText)
                        end)
                       
                        if jsonDecodeSuccess and jsonData and jsonData.achievement_types then
                            return jsonData.achievement_types
                        end
                       
                        -- If that failed, try with JSON_TRY_FIXING flag
                        jsonDecodeSuccess, jsonData = pcall(function()
                            return mw.text.jsonDecode(jsonText, mw.text.JSON_TRY_FIXING)
                        end)
                       
                        if jsonDecodeSuccess and jsonData and jsonData.achievement_types then
                            return jsonData.achievement_types
                        end
                    end
                end
            end
           
            -- If we couldn't load from AchievementList.json, fall back to AchievementData.json
            local data = Achievements.loadData(frame)
            if data and data.achievement_types then
                return data.achievement_types
            end
         else
            -- We have jsonText from frame:preprocess, try to decode it
            if jsonText and mw.text and mw.text.jsonDecode then
                -- First try WITHOUT PRESERVE_KEYS flag (standard approach)
                local jsonDecodeSuccess, jsonData = pcall(function()
                    return mw.text.jsonDecode(jsonText)
                end)
               
                if jsonDecodeSuccess and jsonData and jsonData.achievement_types then
                    return jsonData.achievement_types
                end
               
                -- If that failed, try with JSON_TRY_FIXING flag
                jsonDecodeSuccess, jsonData = pcall(function()
                    return mw.text.jsonDecode(jsonText, mw.text.JSON_TRY_FIXING)
                end)
               
                if jsonDecodeSuccess and jsonData and jsonData.achievement_types then
                    return jsonData.achievement_types
                end
            end
           
            -- If we couldn't decode the JSON, fall back to AchievementData.json
            local data = Achievements.loadData(frame)
            if data and data.achievement_types then
                return data.achievement_types
            end
        end
       
         -- As an absolute last resort, return an empty array
        return {}
    end)
 
     if not success or not types then
        -- If there was an error, fall back to AchievementData.json
        local data = Achievements.loadData(frame)
        if data and data.achievement_types then
            typesCache = data.achievement_types
            return typesCache
        end
        types = {}
    end


local function isTestPage(pageId)
    typesCache = types
     return TEST_CONFIG.enabled and tostring(pageId) == TEST_CONFIG.test_page_id
     return types
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
-- Load achievement data from the JSON page
-- @param frame - The Scribunto frame object for preprocessing
-- @return Table containing the full achievement data
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.loadData()
function Achievements.loadData(frame)
    mw.log("JSON-DEBUG: Starting to load achievement data")
 
     -- Use the request-level cache if we already loaded data once
     -- Use the request-level cache if we already loaded data once
     if dataCache then
     if dataCache then
        mw.log("JSON-DEBUG: Using request-level cached data")
         return dataCache
         return dataCache
     end
     end


     local success, data = pcall(function()
     local success, data = pcall(function()
         -- Try parser cache first
         -- Get the JSON content using frame:preprocess if available
         local loadDataSuccess, cachedData = pcall(function()
        local jsonText
            return mw.loadData('Module:AchievementSystem')
         if frame and type(frame) == "table" and frame.preprocess then
        end)
            -- Make sure frame is valid and has preprocess method
 
            local preprocessSuccess, preprocessResult = pcall(function()
        if loadDataSuccess and cachedData then
                return frame:preprocess('{{MediaWiki:AchievementData.json}}')
            mw.log("JSON-DEBUG: Using mw.loadData cached data")
            end)
             return cachedData
           
        else
            if preprocessSuccess and preprocessResult then
            mw.log("JSON-DEBUG: mw.loadData failed or returned empty, proceeding to direct page load")
                jsonText = preprocessResult
             end
         end
         end
 
       
         local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
         -- If we couldn't get JSON from frame:preprocess, fall back to direct content loading
        if not pageTitle or not pageTitle.exists then
        if not jsonText then
             mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
            -- Try using mw.loadJsonData first (preferred method)
             return DEFAULT_DATA
            if mw.loadJsonData then
                local loadJsonSuccess, jsonData = pcall(function()
                    return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
                end)
               
                if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
                    return jsonData
                end
            end
           
            -- Direct content loading approach as fallback
            local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
            if not pageTitle or not pageTitle.exists then
                return DEFAULT_DATA
            end
           
            -- Get raw content from the wiki page
             local contentSuccess, content = pcall(function()
                return pageTitle:getContent()
            end)
           
            if contentSuccess and content and content ~= "" then
                -- Remove any BOM or leading whitespace that might cause issues
                content = content:gsub("^%s+", "")
                if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
                    content = content:sub(4)
                end
               
                jsonText = content
             else
                return DEFAULT_DATA
            end
         end
         end
 
       
         local content = pageTitle:getContent()
         -- Try different JSON decode approaches
         if not content or content == '' then
         if jsonText and mw.text and mw.text.jsonDecode then
             mw.log("JSON-DEBUG: Page content is empty")
             -- First try WITHOUT PRESERVE_KEYS flag (standard approach)
             return DEFAULT_DATA
             local jsonDecodeSuccess, jsonData = pcall(function()
        end
                return mw.text.jsonDecode(jsonText)
 
            end)
        mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
           
        local parseSuccess, parsedData = pcall(function()
            if jsonDecodeSuccess and jsonData then
            return json.decode(content)
                return jsonData
        end)
            end
 
           
        if not parseSuccess or not parsedData then
             -- If that failed, try with JSON_TRY_FIXING flag
             mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or 'unknown error'))
            jsonDecodeSuccess, jsonData = pcall(function()
             return DEFAULT_DATA
                return mw.text.jsonDecode(jsonText, mw.text.JSON_TRY_FIXING)
            end)
              
            if jsonDecodeSuccess and jsonData then
                return jsonData
            end
         end
         end
 
         -- As an absolute last resort, use local default data
         mw.log("JSON-DEBUG: Successfully loaded achievement data")
         return DEFAULT_DATA
         return parsedData
     end)
     end)


     if not success or not data then
     if not success or not data then
        mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
         data = DEFAULT_DATA
         data = DEFAULT_DATA
     end
     end
Line 149: Line 318:
     dataCache = data
     dataCache = data
     return data
     return data
end
--------------------------------------------------------------------------------
-- Get user achievements
-- @param pageId - The page ID to get achievements for
-- @return Array of achievement objects for the specified page
--------------------------------------------------------------------------------
local userAchievementsCache = {}
---@return UserAchievement[]
function Achievements.getUserAchievements(pageId)
    if not pageId or pageId == '' then
        return {}
    end
   
    -- Check cache first
    local cacheKey = tostring(pageId)
    if userAchievementsCache[cacheKey] then
        return userAchievementsCache[cacheKey]
    end
    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return {}
    end
    local key = cacheKey
    local userEntry = data.user_achievements[key]
   
    -- If found with string key, return achievements
    if userEntry and userEntry.achievements then
        local achievements = ensureArray(userEntry.achievements)
        userAchievementsCache[cacheKey] = achievements
        return achievements
    end
   
    -- Try numeric key as fallback
    local numKey = tonumber(key)
    if numKey then
        userEntry = data.user_achievements[numKey]
        if userEntry and userEntry.achievements then
            local achievements = ensureArray(userEntry.achievements)
            userAchievementsCache[cacheKey] = achievements
            return achievements
        end
    end
   
    -- Cache empty result to avoid repeated lookups
    userAchievementsCache[cacheKey] = {}
    return {}
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Check if a page/user has any achievements
-- Check if a page/user has any achievements
-- @param pageId - The page ID to check
-- @return Boolean indicating if the page has any achievements
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
function Achievements.hasAchievements(pageId)
Line 159: Line 380:
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
     if not data or not data.user_achievements then
    return #userAchievements > 0
         return false
end
 
--------------------------------------------------------------------------------
-- Get all badge-type achievements for a user
-- @param pageId - The page ID to check
-- @param frame - The Scribunto frame object for preprocessing
-- @return Array of badge achievement objects
--------------------------------------------------------------------------------
function Achievements.getBadgeAchievements(pageId, frame)
     if not pageId or pageId == '' then
         return {}
     end
     end


     local key = tostring(pageId)
     local userAchievements = Achievements.getUserAchievements(pageId)
     if data.user_achievements[key] and #data.user_achievements[key] > 0 then
     if #userAchievements == 0 then
         return true
         return {}
     end
     end


     -- Check for legacy "n123" style
    local types = Achievements.loadTypes(frame)
     if key:match("^%d+$") then
   
         local alt = "n" .. key
     -- Build a lookup table for achievement types for efficient access
         if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
    local typeDefinitions = {}
             return true
     for _, typeData in ipairs(types) do
        if typeData.id and typeData.type then
            typeDefinitions[typeData.id] = typeData
        end
    end
   
    local badgeAchievements = {}
    -- Filter user achievements to only include badge types
    for _, achievementTbl in ipairs(userAchievements) do
         local achType = achievementTbl['type']
         if achType and typeDefinitions[achType] and typeDefinitions[achType]['type'] == "badge" then
                local newAchievement = {
                    type = achType,
                    date = achievementTbl['date'] or '',
                    name = typeDefinitions[achType].name or achType,
                    category = typeDefinitions[achType].category
                }
             table.insert(badgeAchievements, newAchievement)
         end
         end
     end
     end


    -- We removed the forced "true" for test pages to avoid dev-role injection
     return badgeAchievements
     return false
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Get a user-friendly name for a given achievement type
-- Get a user-friendly name for a given achievement type
-- @param achievementType - The achievement type ID
-- @param frame - The Scribunto frame object for preprocessing
-- @return String containing the user-friendly name
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
function Achievements.getAchievementName(achievementType, frame)
     if not achievementType or achievementType == '' then
     if not achievementType or achievementType == '' then
        debugLog("Empty achievement type provided to getAchievementName")
        mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
         return 'Unknown'
         return 'Unknown'
     end
     end


    debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
     local types = Achievements.loadTypes(frame)
 
     local data = Achievements.loadData()
    if not data or not data.achievement_types then
        mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
        return achievementType
    end
      
      
    -- Display available achievement types in the log
    mw.log("ACHIEVEMENT-NAME-DEBUG: Searching achievement types for: " .. achievementType)
    mw.log("ACHIEVEMENT-NAME-DEBUG: Available achievement types:")
    for i, typeData in ipairs(data.achievement_types) do
        mw.log("  " .. i .. ": id=" .. (typeData.id or "nil") ..
              ", name=" .. (typeData.name or "nil") ..
              ", tier=" .. (typeData.tier or "nil"))
    end
     -- Try to match achievement ID
     -- Try to match achievement ID
     for i, typeData in ipairs(data.achievement_types) do
     for _, typeData in ipairs(types) do
         if typeData.id == achievementType then
         if typeData.id == achievementType then
             if typeData.name and typeData.name ~= "" then
             if typeData.name and typeData.name ~= "" then
                mw.log("ACHIEVEMENT-NAME-SUCCESS: Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                 return typeData.name
                 return typeData.name
             else
             else
                mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name; using ID")
                 return achievementType
                 return achievementType
             end
             end
Line 221: Line 452:
     end
     end


    -- Special case for dev-role lookup - always show details
    if achievementType == "dev-role" then
        mw.log("ACHIEVEMENT-NAME-WARNING: Could not find dev-role in achievement_types!")
        mw.log("ACHIEVEMENT-NAME-DEBUG: Raw achievement_types:")
       
        -- Try to log the raw JSON for debugging
        pcall(function()
            if json and json.encode then
                mw.log(json.encode(data.achievement_types))
            end
        end)
    end
    mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type '" .. achievementType .. "'; using ID fallback")
     return achievementType
     return achievementType
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Find the top-tier achievement for the user (lowest tier number)
-- Find the top-tier Title achievement for the user (lowest tier number)
-- Return the CSS class and the readable achievement name
-- Return the CSS class and the readable achievement name
-- @param pageId - The page ID to get the title achievement for
-- @param frame - The Scribunto frame object for preprocessing
-- @return CSS class, display name
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
function Achievements.getTitleClass(pageId, frame)
     if not pageId or pageId == '' then
     if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getTitleClass")
        return '', ''
    end
    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("No achievement data available in getTitleClass")
         return '', ''
         return '', ''
     end
     end


     local key = tostring(pageId)
     local userAchievements = Achievements.getUserAchievements(pageId)
    debugLog("Looking up achievements for ID: " .. key)
     if #userAchievements == 0 then
   
    -- Debug logging to show available achievements in JSON
    if data.user_achievements then
        local availableIds = {}
        for k, _ in pairs(data.user_achievements) do
            table.insert(availableIds, k)
        end
        debugLog("Available user IDs in JSON: " .. table.concat(availableIds, ", "))
    end
 
    -- Try to fetch achievements for this pageId
    local userAchievements = data.user_achievements[key] or {}
   
    -- If no achievements found under normal ID, try alternative format
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
        userAchievements = data.user_achievements[altKey] or {}
    end
 
    -- Special handling for test page ID 18451
     if key == "18451" then
        -- Verify if the dev-role achievement exists
        local devRoleFound = false
        for _, ach in ipairs(userAchievements) do
            if ach.type == "dev-role" then
                devRoleFound = true
                debugLog("FOUND DEV-ROLE for page 18451!")
                break
            end
        end
       
        -- If no achievements found for test page, this is concerning
        if #userAchievements == 0 then
            debugLog("WARNING: Test page ID 18451 has no achievements!")
        end
       
        -- If achievements exist but dev-role is missing, also concerning
        if #userAchievements > 0 and not devRoleFound then
            debugLog("WARNING: Test page ID 18451 has achievements but missing dev-role!")
        end
    end
 
    -- Log found achievements for debugging
    if #userAchievements > 0 then
        debugLog("Found " .. #userAchievements .. " achievements for user " .. key)
        for i, ach in ipairs(userAchievements) do
            debugLog("  Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        end
    else
        debugLog("No achievements found for user " .. key)
         return '', ''
         return '', ''
     end
     end


    local types = Achievements.loadTypes(frame)
     local highestTier = 999
     local highestTier = 999
     local highestAchievement = nil
     local highestAchievement = nil


     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement["type"]
        debugLog("Processing achievement type: " .. (achType or "nil"))
          
          
         for _, typeData in ipairs(data.achievement_types) do
         for _, typeData in ipairs(types) do
             if typeData.id == achType then
             if typeData.id == achType then
                 local tier = typeData.tier or 999
                 local tier = typeData.tier or 999
                debugLog("  Found type '" .. typeData.id .. "' with tier " .. tier .. " and name '" .. (typeData.name or "nil") .. "'")
                 if tier < highestTier then
                 if tier < highestTier then
                     highestTier = tier
                     highestTier = tier
                     highestAchievement = typeData
                     highestAchievement = typeData
                    debugLog("  New highest tier achievement: " .. typeData.id)
                 end
                 end
             end
             end
Line 331: Line 491:


     if not highestAchievement or not highestAchievement.id then
     if not highestAchievement or not highestAchievement.id then
        debugLog("No valid top-tier achievement found for user " .. key)
         return '', ''
         return '', ''
     end
     end
Line 337: Line 496:
     local cssClass = "achievement-" .. highestAchievement.id
     local cssClass = "achievement-" .. highestAchievement.id
     local displayName = highestAchievement.name or highestAchievement.id or "Award"
     local displayName = highestAchievement.name or highestAchievement.id or "Award"
   
    debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
      
      
     return cssClass, displayName
     return cssClass, displayName
Line 344: Line 501:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Renders a simple "box" with the top-tier achievement for the user
-- Renders a box with the top-tier achievement for the user
-- @param pageId - The page ID to render the achievement box for
-- @param frame - The Scribunto frame object for preprocessing
-- @return HTML string containing the achievement box
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
function Achievements.renderAchievementBox(pageId, frame)
     if not pageId or pageId == '' then
     if not pageId or pageId == '' then
         return ''
         return ''
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
     if not data or not data.user_achievements then
     if #userAchievements == 0 then
         return ''
         return ''
     end
     end
 
   
     local key = tostring(pageId)
     local types = Achievements.loadTypes(frame)
     local userAchievements = data.user_achievements[key]
   
     if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
    -- Build a lookup table for achievement type definitions
        userAchievements = data.user_achievements["n" .. key]
     local typeDefinitions = {}
    end
     for _, typeData in ipairs(types) do
 
        if typeData.id and typeData.name then
    if not userAchievements or #userAchievements == 0 then
            typeDefinitions[typeData.id] = {
         return ''
                name = typeData.name,
                tier = typeData.tier or 999
            }
         end
     end
     end


    -- Look for the highest-tier Title achievement (lowest tier number)
     local highestTier = 999
     local highestTier = 999
     local topAch = nil
     local topAchType = nil


     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
         for _, typeData in ipairs(data.achievement_types) do
         if typeDefinitions[achType] and typeDefinitions[achType].tier < highestTier then
            if typeData.id == achType then
            highestTier = typeDefinitions[achType].tier
                local tier = typeData.tier or 999
            topAchType = achType
                if tier < highestTier then
                    highestTier = tier
                    topAch = typeData
                end
            end
         end
         end
     end
     end


     if topAch then
    -- If we found an achievement, render it
     if topAchType and typeDefinitions[topAchType] then
        local achName = typeDefinitions[topAchType].name or topAchType
       
         return string.format(
         return string.format(
             '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
             '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
             topAch.id,
             topAchType,
             htmlEncode(topAch.name or topAch.id or "")
             htmlEncode(achName)
         )
         )
     end
     end
Line 394: Line 556:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Simple pass-through to track pages (for future expansions)
-- Get page name for a given page ID
-- @param pageId - The page ID to get the name for
-- @return String containing the page name
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
function Achievements.getPageName(pageId)
     return true
    if not pageId or pageId == '' then
        return ''
    end
   
    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return ''
    end
   
    local key = tostring(pageId)
    local userEntry = data.user_achievements[key]
   
    -- Check if entry exists with string key
    if userEntry and userEntry.page_name then
        return userEntry.page_name
    end
   
    -- Try numeric key as fallback
    local numKey = tonumber(key)
    if numKey then
        userEntry = data.user_achievements[numKey]
        if userEntry and userEntry.page_name then
            return userEntry.page_name
        end
    end
   
     return ''
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Retrieve a specific achievement if present, by type
-- Retrieve a specific achievement if present, by type
-- @param pageId - The page ID to get the achievement for
-- @param achievementType - The achievement type ID to look for
-- @return Achievement object or nil if not found
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
function Achievements.getSpecificAchievement(pageId, achievementType)
    debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
            "' in page ID: " .. tostring(pageId))
     if not pageId or not achievementType or pageId == '' then
     if not pageId or not achievementType or pageId == '' then
        debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
         return nil
         return nil
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
     if not data or not data.user_achievements then
      
         debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
    -- Direct lookup for the requested achievement type
    for _, achievementTbl in ipairs(userAchievements) do
        if achievementTbl["type"] == achievementType then
local def = Achievements.getAchievementDefinition(achievementType)
            return {
                type    = achievementTbl.type,
                date    = achievementTbl.date or '',
                name    = def and def.name or achievementType,
                category = def and def.category
            }
         end
    end
 
    return nil
end
 
--------------------------------------------------------------------------------
-- Get achievement definition directly from JSON data
-- @param achievementType - The achievement type ID to get the definition for
-- @param frame - The Scribunto frame object for preprocessing
-- @return Achievement type definition or nil if not found
--------------------------------------------------------------------------------
function Achievements.getAchievementDefinition(achievementType, frame)
    if not achievementType or achievementType == '' then
         return nil
         return nil
     end
     end
    local key = tostring(pageId)
    debugLog("DIRECT LOOKUP: Checking for '" .. achievementType .. "' in page ID '" .. key .. "'")
      
      
    -- List ALL achievement IDs in the JSON
     local types = Achievements.loadTypes(frame)
     local availableIds = {}
   
    for userId, userAchs in pairs(data.user_achievements) do
    -- Direct lookup in achievement_types array
        table.insert(availableIds, userId)
    for _, typeData in ipairs(types) do
       
        if typeData.id == achievementType then
        -- List all achievement types for this user
             return typeData
        local typesForUser = {}
        for _, ach in ipairs(userAchs) do
             table.insert(typesForUser, tostring(ach.type))
         end
         end
       
        debugLog("User ID '" .. userId .. "' has achievement types: " .. table.concat(typesForUser, ", "))
     end
     end
    debugLog("ALL User IDs in JSON: " .. table.concat(availableIds, ", "))
      
      
     -- Get user achievements
     return nil
     local userAchievements = data.user_achievements[key] or {}
end
 
--------------------------------------------------------------------------------
-- Find and return title achievement for the user if one exists
-- This specifically looks for achievements with type="title"
-- Return the CSS class, readable achievement name, and achievement ID (or empty strings if none found)
-- @param pageId - The page ID to get the title achievement for
-- @param frame - The Scribunto frame object for preprocessing
-- @return achievementId, displayName, achievementId
--------------------------------------------------------------------------------
function Achievements.getTitleAchievement(pageId, frame)
    if not pageId or pageId == '' then
        return nil
    end
 
     local userAchievements = Achievements.getUserAchievements(pageId)
    if #userAchievements == 0 then
        return nil
    end
 
    local types = Achievements.loadTypes(frame)
      
      
     -- If no achievements found under normal ID, try alternative format
     -- Build a table of achievement definitions for quick lookup
     if #userAchievements == 0 and key:match("^%d+$") then
     local typeDefinitions = {}
        local altKey = "n" .. key
    for _, typeData in ipairs(types) do
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
         typeDefinitions[typeData.id] = typeData
         userAchievements = data.user_achievements[altKey] or {}
     end
     end
    -- Find title achievements only
    local highestTier = 999
    local titleAchievement = nil
      
      
     -- Log the achievements we found
     for _, achievement in ipairs(userAchievements) do
    if #userAchievements > 0 then
        local achType = achievement["type"]
        debugLog("Found " .. #userAchievements .. " achievements for user " .. key)
        if achType then
        for i, ach in ipairs(userAchievements) do
            local typeData = typeDefinitions[achType]
            debugLog(" Achievement " .. i .. ": type=" .. (ach.type or "nil"))
            if typeData and typeData["type"] == "title" then
                local tier = typeData.tier or 999
                if tier < highestTier then
                    highestTier = tier
                    titleAchievement = typeData
                end
            end
         end
         end
    end
    return titleAchievement
end
-- Renders a title block with achievement integration
function Achievements.renderTitleBlockWithAchievement(args, titleClass, titleText, achievementClass, achievementId, achievementName)
    titleClass = titleClass or "template-title"
   
    -- Only add achievement attributes if they exist
    if achievementClass and achievementClass ~= "" and achievementId and achievementId ~= "" then
        return string.format(
            '|-\n! colspan="2" class="%s %s" data-achievement-id="%s" data-achievement-name="%s" | %s',
            titleClass, achievementClass, achievementId, achievementName, titleText
        )
     else
     else
         debugLog("No achievements found for user " .. key)
         -- Clean row with no achievement data
        return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
     end
     end
end


    -- Special handling for test page ID 18451
--------------------------------------------------------------------------------
    if key == "18451" and achievementType == "dev-role" then
-- Generate wikitext category links for a given list of achievements
        -- Extra logging to debug this specific case
-- @param achievements - An array of user achievement objects
        debugLog("CRITICAL CHECK: Looking for dev-role in test page 18451!")
-- @param frame - The Scribunto frame object
       
-- @return A string of wikitext category links, e.g., "[[Category:Cat1]][[Category:Cat2]]"
        -- First check if the user exists
--------------------------------------------------------------------------------
        if data.user_achievements["18451"] then
function Achievements.getCategoryLinks(achievements, frame)
            debugLog("User ID 18451 exists in JSON with " ..
    if not achievements or #achievements == 0 then
                    #data.user_achievements["18451"] .. " achievements")
        return ""
                   
    end
            -- Dump all achievements for this user
 
            for i, ach in ipairs(data.user_achievements["18451"]) do
    local types = Achievements.loadTypes(frame)
                debugLog(" Achievement " .. i .. ": type=" .. (ach.type or "nil"))
    local typeDefinitions = {}
            end
    for _, typeData in ipairs(types) do
        else
         typeDefinitions[typeData.id] = typeData
            debugLog("WARNING: User ID 18451 doesn't exist in JSON!")
         end
     end
     end


     -- Find achievement by type
     local categoryLinks = {}
     for _, achievement in ipairs(userAchievements) do
    local foundCategories = {} -- To prevent duplicate categories
         debugLog("Checking achievement type: " .. tostring(achievement.type) ..
 
                " against requested: " .. tostring(achievementType))
     for _, ach in ipairs(achievements) do
               
         local achType = ach['type']
         if achievement.type == achievementType then
        local definition = typeDefinitions[achType]
             debugLog("FOUND ACHIEVEMENT: " .. achievementType .. " for user " .. key)
       
             return achievement
         if definition and definition.category and definition.category ~= "" and not foundCategories[definition.category] then
             table.insert(categoryLinks, "[[Category:" .. definition.category .. "]]")
             foundCategories[definition.category] = true
         end
         end
     end
     end


     debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
     return table.concat(categoryLinks)
    return nil
end
end


return Achievements
return Achievements