Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 1: Line 1:
-- Module:AchievementSystem
-- Module:AchievementSystem
-- Achievement system that loads data from MediaWiki:AchievementData.json,
-- Achievement system that loads data from MediaWiki:AchievementData.json.
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
-- This module only assigns CSS classes based on achievement IDs in the format:
-- This module only assigns CSS classes based on achievement IDs in the format:
-- .person-template .template-title.achievement-{id}::after {}
--   .person-template .template-title.achievement-{id}::after {}
--
--
-- The module does not use any styling information from the JSON data structure.
-- The module does not use any styling information from the JSON data structure.
Line 9: Line 9:
local Achievements = {}
local Achievements = {}


-- Debug configuration - set to true to enable console logging
-- Debug configuration
local DEBUG_MODE = true
local DEBUG_MODE = true
local function debugLog(message)
local function debugLog(message)
     if not DEBUG_MODE then return end
     if not DEBUG_MODE then return end
   
    -- Log to JavaScript console with structured data
     pcall(function()
     pcall(function()
         mw.logObject({
         mw.logObject({
Line 22: Line 20:
         })
         })
     end)
     end)
   
    -- Backup log to MediaWiki
     mw.log("ACHIEVEMENT-DEBUG: " .. message)
     mw.log("ACHIEVEMENT-DEBUG: " .. message)
end
end


-- Try to load JSON module with error handling
-- Attempt to load JSON handling
local json
local json
local jsonLoaded = pcall(function()
local jsonLoaded = pcall(function()
Line 33: Line 29:
end)
end)


-- If JSON module failed to load, create a minimal fallback
if not jsonLoaded or not json then
if not jsonLoaded or not json then
     json = { decode = function() return nil end }
     json = { decode = function() return nil end }
Line 39: Line 34:
end
end


-- Create a fallback htmlEncode if not available
-- Simple HTML encode fallback
local htmlEncode = function(str)
local function htmlEncode(str)
     if mw.text and mw.text.htmlEncode then
     if mw.text and mw.text.htmlEncode then
         return mw.text.htmlEncode(str or '')
         return mw.text.htmlEncode(str or '')
     else
     else
         return (str or ''):gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;'):gsub('"', '&quot;')
         return (str or '')
            :gsub('&', '&amp;')
            :gsub('<', '&lt;')
            :gsub('>', '&gt;')
            :gsub('"', '&quot;')
     end
     end
end
end


-- Constants
-- The page storing JSON data
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'


-- Test/debug configuration
-- Cache for achievement data (within one request)
local TEST_CONFIG = {
    enabled = true,                  -- Master toggle for test mode
    test_page_id = "18451",          -- Page ID used for testing
    test_user = "direct-test-user",  -- Test username
    debug_messages = true,          -- Show debug messages
    force_achievements = true,      -- Force achievements when JSON fails
   
    -- Type mapping for backward compatibility
    type_mapping = {
        ["jedi"] = "ach1",
        ["champion"] = "ach2",
        ["sponsor"] = "ach3",
        -- Include the new achievement types directly
        ["ach1"] = "ach1",
        ["ach2"] = "ach2",
        ["ach3"] = "ach3",
        ["title-test"] = "dev-role", -- Map old ID to new ID
        ["dev-role"] = "dev-role"
    },
   
    -- Default test achievements
    test_achievements = {
        "title-test",
        "ach1",
        "ach2",
        "ach3"
    }
}
 
-- Helper to check if a page ID is the test page
local function isTestPage(pageId)
    return TEST_CONFIG.enabled and tostring(pageId) == TEST_CONFIG.test_page_id
end
 
-- Cache for achievement data (within request)
local dataCache = nil
local dataCache = nil


-- Default data structure to use if loading fails
-- Default data if JSON load fails
local DEFAULT_DATA = {  
local DEFAULT_DATA = {
     schema_version = 1,
     schema_version = 1,
     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 }
     cache_control = { version = 0 }
}
}


--[[
--------------------------------------------------------------------------------
Loads achievement data from MediaWiki:AchievementData.json with caching
-- Internal: load achievement data from the page, or from cache
@return table The achievement data structure or default empty structure on failure
--------------------------------------------------------------------------------
]]
function Achievements.loadData()
function Achievements.loadData()
    -- Direct console log for better visibility
     mw.log("JSON-DEBUG: Starting to load achievement data")
     mw.log("JSON-DEBUG: Starting to load achievement data")
   
 
    -- Check if we can use the request-level cache
     if dataCache then
     if dataCache then
         mw.log("JSON-DEBUG: Using request-level cached data")
         mw.log("JSON-DEBUG: Using request-level cached data")
         return dataCache
         return dataCache
     end
     end
   
 
    -- Try to load data with error handling
     local success, data = pcall(function()
     local success, data = pcall(function()
         -- First try to load from parser cache
         -- First try to load from parser cache
Line 118: Line 78:
             return mw.loadData('Module:AchievementSystem')
             return mw.loadData('Module:AchievementSystem')
         end)
         end)
       
 
         if loadDataSuccess and cachedData then
         if loadDataSuccess and cachedData then
             mw.log("JSON-DEBUG: Using mw.loadData cached data")
             mw.log("JSON-DEBUG: Using mw.loadData cached data")
Line 125: Line 85:
             mw.log("JSON-DEBUG: mw.loadData failed or returned empty, using direct page load")
             mw.log("JSON-DEBUG: mw.loadData failed or returned empty, using direct page load")
         end
         end
       
 
        -- Fall back to direct page load
         local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
        local content = nil
         if not pageTitle or not pageTitle.exists then
         local pageSuccess, page = pcall(function() return mw.title.new(ACHIEVEMENT_DATA_PAGE) end)
             mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
       
         if not pageSuccess or not page then
             mw.log("JSON-DEBUG: Failed to create title object for " .. ACHIEVEMENT_DATA_PAGE)
             return DEFAULT_DATA
             return DEFAULT_DATA
         end
         end
       
 
        if not page.exists then
         local content = pageTitle:getContent()
            mw.log("JSON-DEBUG: Page " .. ACHIEVEMENT_DATA_PAGE .. " does not exist")
            return DEFAULT_DATA
        end
       
        -- Page exists, try to get content
         content = page:getContent()
       
         if not content or content == '' then
         if not content or content == '' then
             mw.log("JSON-DEBUG: Page content is empty")
             mw.log("JSON-DEBUG: Page content is empty")
             return DEFAULT_DATA
             return DEFAULT_DATA
         end
         end
       
 
        -- Log content statistics for debugging
         mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
         mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
        mw.log("JSON-DEBUG: First 100 chars: " .. content:sub(1, 100))
         local parseSuccess, parsedData = pcall(function()
        mw.log("JSON-DEBUG: Contains '18451': " .. (content:find('"18451"') and "true" or "false"))
             return json.decode(content)
       
        -- Parse JSON with detailed error handling
         local parseSuccess, parsedData = pcall(function()  
             return json.decode(content)  
         end)
         end)
       
 
         if not parseSuccess or not parsedData then
         if not parseSuccess or not parsedData then
             mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or "unknown error"))
             mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or 'unknown error'))
             return DEFAULT_DATA
             return DEFAULT_DATA
         end
         end
       
 
        -- Check structure exists
        mw.log("JSON-DEBUG: Parse successful, checking data structure")
       
        -- Verify key structures exist
        mw.log("JSON-DEBUG: Has achievement_types: " .. tostring(parsedData.achievement_types ~= nil))
        mw.log("JSON-DEBUG: Has user_achievements: " .. tostring(parsedData.user_achievements ~= nil))
       
        -- Verify our test page
        if parsedData.user_achievements then
            mw.log("JSON-DEBUG: Has data for 18451: " .. tostring(parsedData.user_achievements["18451"] ~= nil))
           
            -- If we have 18451 data, log how many achievements
            if parsedData.user_achievements["18451"] then
                mw.log("JSON-DEBUG: Number of achievements for 18451: " .. #parsedData.user_achievements["18451"])
                for i, achievement in ipairs(parsedData.user_achievements["18451"]) do
                    mw.log("JSON-DEBUG: Achievement " .. i .. " is type: " .. tostring(achievement.type))
                end
            end
        end
       
        -- Log successful load
         mw.log("JSON-DEBUG: Successfully loaded achievement data")
         mw.log("JSON-DEBUG: Successfully loaded achievement data")
       
         return parsedData
         return parsedData
     end)
     end)
   
 
    -- Handle errors
     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'))
         mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
         data = DEFAULT_DATA
         data = DEFAULT_DATA
     end
     end
   
 
    -- Update request cache so we don't need to reload within this page render
     dataCache = data
     dataCache = data
   
     return data
     return data
end
end


--[[
--------------------------------------------------------------------------------
Checks if a user has any achievements
-- Checks if a user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
    if not pageId or pageId == '' then
        return false
    end


@param pageId string|number The page ID to check
@return boolean True if the user has any achievements, false otherwise
]]
function Achievements.hasAchievements(pageId)
    if not pageId or pageId == '' then return false end
   
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.user_achievements then return false end
     if not data or not data.user_achievements then
      
        return false
    -- Convert to string for consistent lookup
     end
 
     local key = tostring(pageId)
     local key = tostring(pageId)
   
    -- Check for direct match
     if data.user_achievements[key] and #data.user_achievements[key] > 0 then
     if data.user_achievements[key] and #data.user_achievements[key] > 0 then
         return true
         return true
     end
     end
   
 
     -- Check for achievements under n-prefixed key (backward compatibility)
     -- Check for achievements under n-prefixed key (legacy)
     if key:match("^%d+$") and data.user_achievements["n" .. key] and #data.user_achievements["n" .. key] > 0 then
     if key:match("^%d+$") then
        return true
        local alt = "n" .. key
        if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
            return true
        end
     end
     end
   
 
    -- Special case for test page - force true for testing
    if isTestPage(pageId) then
        debugLog("Special case: Forcing true for test page " .. key)
        return true
    end
   
     return false
     return false
end
end


--[[
--------------------------------------------------------------------------------
Gets the actual name of an achievement for display purposes
-- Retrieves the display name for a given achievement type
 
--------------------------------------------------------------------------------
@param achievementType string The achievement type ID
@return string The display name for the achievement or a default value
]]
function Achievements.getAchievementName(achievementType)
function Achievements.getAchievementName(achievementType)
    -- Input validation with comprehensive logging
     if not achievementType or achievementType == '' then
     if not achievementType or achievementType == '' then  
         debugLog("Empty achievement type provided to getAchievementName")
         debugLog("Empty achievement type provided to getAchievementName")
        -- Direct console logging for better visibility
         mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
         mw.log("ACHIEVEMENT-NAME-ERROR: Empty achievement type provided")
         return 'Unknown'
         return 'Unknown'  
     end
     end


    -- Enhanced logging for tracing purposes
     debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
     debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
    -- Direct console logging for better visibility
 
    mw.log("ACHIEVEMENT-NAME-DEBUG: Looking up name for type: '" .. tostring(achievementType) .. "'")
   
    -- Data access with verification
     local data = Achievements.loadData()
     local data = Achievements.loadData()
      
     if not data or not data.achievement_types then
    -- Explicit data validation
         mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
    if not data then
         mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data available")
         return achievementType
         return achievementType
     end
     end
   
 
    if not data.achievement_types or type(data.achievement_types) ~= "table" then
        mw.log("ACHIEVEMENT-NAME-ERROR: Invalid achievement_types structure")
        return achievementType
    end
   
    -- Log achievement types count for debugging
    mw.log("ACHIEVEMENT-NAME-DEBUG: Found " .. #data.achievement_types .. " achievement types in JSON")
   
    -- Simple direct loop through achievement types to find a match with enhanced logging
     for i, typeData in ipairs(data.achievement_types) do
     for i, typeData in ipairs(data.achievement_types) do
         if not typeData.id then
         if typeData.id == achievementType then
            mw.log("ACHIEVEMENT-NAME-WARNING: Achievement at index " .. i .. " has no ID")
            if typeData.name and typeData.name ~= "" then
        else
                debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
            mw.log("ACHIEVEMENT-NAME-DEBUG: Checking type " .. i .. ": '" .. typeData.id .. "' against '" .. achievementType .. "'")
                return typeData.name
           
            else
            if typeData.id == achievementType then
                mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name")
                -- Return the name if available, otherwise fall back to ID
                return achievementType
                if typeData.name and typeData.name ~= "" then
                    debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                    mw.log("ACHIEVEMENT-NAME-SUCCESS: Found '" .. typeData.id .. "' with name: '" .. typeData.name .. "'")
                    return typeData.name
                else
                    debugLog("Achievement found but has no name, using ID as fallback")
                    mw.log("ACHIEVEMENT-NAME-WARNING: Achievement found but has no name")
                    return achievementType
                end
             end
             end
         end
         end
     end
     end
   
 
    -- If we get here, we couldn't find the achievement type
     mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type: '" .. achievementType .. "'")
    debugLog("No achievement found with type: " .. tostring(achievementType))
     return achievementType
     mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type: '" .. tostring(achievementType) .. "'")
   
    -- Return the ID as a fallback but with better logging
    mw.log("ACHIEVEMENT-NAME-FALLBACK: Using ID as fallback for: " .. achievementType)
   
     return achievementType -- Fall back to the ID as a last resort
end
end


--[[
--------------------------------------------------------------------------------
Gets the CSS class and name for the highest achievement to be applied to the template title
-- Finds the top-tier achievement for a user
 
-- Returns the CSS class and the achievement name
@param pageId string|number The page ID to check
--------------------------------------------------------------------------------
@return string, string The CSS class name and achievement name, or empty strings if no achievement
]]
function Achievements.getTitleClass(pageId)
function Achievements.getTitleClass(pageId)
     if not pageId or pageId == '' then  
     if not pageId or pageId == '' then
         debugLog("Empty page ID provided to getTitleClass")
         debugLog("Empty page ID provided to getTitleClass")
         return '', ''  
         return '', ''
     end
     end
   
 
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.user_achievements then
     if not data or not data.user_achievements then
         debugLog("No achievement data available")
         debugLog("No achievement data available in getTitleClass")
         return '', ''
         return '', ''
     end
     end
   
 
    -- Convert to string for consistent lookup
     local key = tostring(pageId)
     local key = tostring(pageId)
     debugLog("Looking up achievements for ID: " .. key)
     debugLog("Looking up achievements for ID: " .. key)
   
 
    -- Try with direct key first
     local userAchievements = data.user_achievements[key] or {}
     local userAchievements = data.user_achievements[key] or {}
   
    -- Try with n-prefix if not found (for backward compatibility)
     if #userAchievements == 0 and key:match("^%d+$") then
     if #userAchievements == 0 and key:match("^%d+$") then
         local nKey = "n" .. key
         local altKey = "n" .. key
        debugLog("Trying alternative key: " .. nKey)
         userAchievements = data.user_achievements[altKey] or {}
         userAchievements = data.user_achievements[nKey] or {}
     end
     end
   
 
    -- Special case for test page - get dev-role achievement from JSON properly
    if isTestPage(pageId) then
        debugLog("Getting dev-role achievement for test page")
       
        -- Look up the proper achievement name from JSON
        local achievementName = Achievements.getAchievementName("dev-role")
        debugLog("Retrieved achievement name: " .. achievementName)
       
        return "achievement-dev-role", achievementName
    end
   
     if #userAchievements == 0 then
     if #userAchievements == 0 then
         debugLog("No achievements found")
         debugLog("No achievements found for user " .. key)
         return '', ''
         return '', ''
     end
     end
   
 
     -- Find the highest tier (lowest number) achievement
     local highestTier = 999
     local highestAchievement = nil
     local highestAchievement = nil
    local highestTier = 999
 
   
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achievementType = achievement.type
         local achType = achievement.type
        debugLog("Found achievement type: " .. achievementType)
         for _, typeData in ipairs(data.achievement_types) do
       
             if typeData.id == achType then
         for _, typeData in ipairs(data.achievement_types or {}) do
                local tier = typeData.tier or 999
             if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
                if tier < highestTier then
                highestAchievement = typeData
                    highestTier = tier
                highestTier = typeData.tier or 999
                    highestAchievement = typeData
                 debugLog("New highest tier achievement: " .. typeData.id .. " (tier " .. (typeData.tier or "unknown") .. ")")
                 end
             end
             end
         end
         end
     end
     end
   
 
     if not highestAchievement or not highestAchievement.id then
     if not highestAchievement or not highestAchievement.id then
         debugLog("No valid achievement type found")
         debugLog("No valid top-tier achievement found for user " .. key)
         return '', ''
         return '', ''
     end
     end
   
 
     local className = 'achievement-' .. highestAchievement.id
     local cssClass = "achievement-" .. highestAchievement.id
     local achievementName = 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)
     debugLog("Using achievement class: " .. className .. " with name: " .. achievementName)
     return cssClass, displayName
     return className, achievementName
end
end


--[[
--------------------------------------------------------------------------------
Achievement box renderer - now shows real achievement names
-- Renders an achievement box showing the top-tier achievement
 
--------------------------------------------------------------------------------
@param pageId string|number The page ID to render achievements for
@return string HTML for the achievement box or empty string if no achievements
]]
function Achievements.renderAchievementBox(pageId)
function Achievements.renderAchievementBox(pageId)
    -- For test page, return a proper test achievement with name
     if not pageId or pageId == '' then
     if isTestPage(pageId) then
         return ''
        debugLog("Creating test achievement for test page")
       
        -- Look up the proper dev-role achievement name from JSON
        local achievementName = Achievements.getAchievementName("dev-role")
        mw.log("ACHIEVEMENT-BOX: Using achievement name from getAchievementName(): " .. achievementName)
       
         return '<div class="achievement-box-simple" data-achievement-type="dev-role" data-achievement-name="' ..
              htmlEncode(achievementName) .. '">' .. htmlEncode(achievementName) .. '</div>'
     end
     end
   
 
    -- Get achievements for other pages (if any)
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.user_achievements then return '' end
     if not data or not data.user_achievements then
      
        return ''
    -- Convert to string for consistent lookup
     end
 
     local key = tostring(pageId)
     local key = tostring(pageId)
     local userAchievements = {}
     local userAchievements = data.user_achievements[key]
   
     if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
    -- Try with direct key first
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        userAchievements = data.user_achievements[key]
     -- Try with n-prefix if not found (for backward compatibility)
    elseif key:match("^%d+$") and data.user_achievements["n" .. key] and #data.user_achievements["n" .. key] > 0 then
         userAchievements = data.user_achievements["n" .. key]
         userAchievements = data.user_achievements["n" .. key]
     else
     end
        -- No achievements found
 
    if not userAchievements or #userAchievements == 0 then
         return ''
         return ''
     end
     end
   
 
    -- Find highest tier achievement (same logic as getTitleClass)
    local highestAchievement = nil
     local highestTier = 999
     local highestTier = 999
      
     local topAch = nil
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achievementType = achievement.type
         local achType = achievement.type
       
         for _, typeData in ipairs(data.achievement_types) do
         for _, typeData in ipairs(data.achievement_types or {}) do
             if typeData.id == achType then
             if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
                local tier = typeData.tier or 999
                highestAchievement = typeData
                if tier < highestTier then
                 highestTier = typeData.tier or 999
                    highestTier = tier
                    topAch = typeData
                 end
             end
             end
         end
         end
     end
     end
   
 
    -- If we found a highest achievement, display it with its proper name
     if topAch then
     if highestAchievement then
         return string.format(
         return '<div class="achievement-box-simple" data-achievement-type="' ..
            '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
              highestAchievement.id .. '">' .. htmlEncode(highestAchievement.name) .. '</div>'
            topAch.id,
            htmlEncode(topAch.name or topAch.id or "")
        )
     end
     end
   
    -- Otherwise return empty string
     return ''
     return ''
end
end


--[[
--------------------------------------------------------------------------------
Tracks a page that displays achievements for cache purging
-- Marks a page for cache purging
@param pageId number|string The page ID to track
--------------------------------------------------------------------------------
@param pageName string The page name (for reference)
@return boolean Always returns true (for future expansion)
]]
function Achievements.trackPage(pageId, pageName)
function Achievements.trackPage(pageId, pageName)
    -- This function is designed to be safe by default
     return true
     return true
end
end


--[[
--------------------------------------------------------------------------------
Retrieves a specific achievement type for a user
-- Retrieves a specific achievement from a user if present
@param pageId string|number The page ID to check
--------------------------------------------------------------------------------
@param achievementType string The specific achievement type to look for
@return table|nil The achievement data if found, nil otherwise
]]
function Achievements.getSpecificAchievement(pageId, achievementType)
function Achievements.getSpecificAchievement(pageId, achievementType)
    -- Log detailed info about what we're checking
     debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
     debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. achievementType .. "' achievement for ID: " .. tostring(pageId))
            "' in page ID: " .. tostring(pageId))
   
 
     if not pageId or pageId == '' or not achievementType then  
     if not pageId or not achievementType or pageId == '' then
         debugLog("ACHIEVEMENT-DEBUG: Invalid inputs, pageId or achievementType missing")
         debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
         return nil  
         return nil
     end
     end
   
 
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.user_achievements then
     if not data or not data.user_achievements then
         debugLog("ACHIEVEMENT-DEBUG: No achievement data available")
         debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
         return nil
         return nil
     end
     end
   
 
    -- Log what data is available at each step
     local key = tostring(pageId)
     local key = tostring(pageId)
     debugLog("ACHIEVEMENT-DEBUG: Checking direct key: " .. key)
     local userAchievements = data.user_achievements[key] or {}
   
    if #userAchievements == 0 and key:match("^%d+$") then
    -- First check in direct key
         userAchievements = data.user_achievements["n" .. key] or {}
    if data.user_achievements[key] then
        debugLog("ACHIEVEMENT-DEBUG: Found data for key: " .. key .. " with " .. #data.user_achievements[key] .. " achievements")
         for i, achievement in ipairs(data.user_achievements[key]) do
            debugLog("ACHIEVEMENT-DEBUG: Key " .. key .. " achievement " .. i .. " is type: " .. tostring(achievement.type))
            if achievement.type == achievementType then
                debugLog("ACHIEVEMENT-DEBUG: MATCH FOUND in key: " .. key)
                return achievement
            end
        end
    else
        debugLog("ACHIEVEMENT-DEBUG: No data found for key: " .. key)
     end
     end
   
 
    -- Then check n-prefixed key
     for _, achievement in ipairs(userAchievements) do
     if key:match("^%d+$") then
        if achievement.type == achievementType then
        local nKey = "n" .. key
            return achievement
        debugLog("ACHIEVEMENT-DEBUG: Checking n-prefixed key: " .. nKey)
       
        if data.user_achievements[nKey] then
            debugLog("ACHIEVEMENT-DEBUG: Found data for key: " .. nKey .. " with " .. #data.user_achievements[nKey] .. " achievements")
            for i, achievement in ipairs(data.user_achievements[nKey]) do
                debugLog("ACHIEVEMENT-DEBUG: Key " .. nKey .. " achievement " .. i .. " is type: " .. tostring(achievement.type))
                if achievement.type == achievementType then
                    debugLog("ACHIEVEMENT-DEBUG: MATCH FOUND in key: " .. nKey)
                    return achievement
                end
            end
        else
            debugLog("ACHIEVEMENT-DEBUG: No data found for key: " .. nKey)
         end
         end
     end
     end
   
 
    -- Special test case for test page - always force achievements when enabled
    if isTestPage(pageId) and TEST_CONFIG.force_achievements then
        -- Add more direct console logging to ensure visibility
        mw.log("ACHIEVEMENT-CONSOLE: Testing for achievement type: " .. achievementType)
       
        -- Get mapped type using the central type mapping
        local mappedType = TEST_CONFIG.type_mapping[achievementType] or achievementType
        mw.log("ACHIEVEMENT-CONSOLE: Mapped type: " .. mappedType)
       
        -- Always try to get from JSON first, then fallback to force-injection
        local testPageId = TEST_CONFIG.test_page_id
        local data = Achievements.loadData()
        if data and data.user_achievements and data.user_achievements[testPageId] then
            -- Search for this achievement type in the JSON
            for _, achievement in ipairs(data.user_achievements[testPageId]) do
                if achievement.type == mappedType then
                    mw.log("ACHIEVEMENT-CONSOLE: Found achievement " .. mappedType .. " in JSON data")
                    return achievement
                end
            end
        end
       
        -- If not found in JSON, force-inject
        mw.log("ACHIEVEMENT-CONSOLE: Forcing " .. mappedType .. " achievement (not found in JSON)")
        return {
            type = mappedType,
            granted_date = os.date('!%Y-%m-%dT%H:%M:%SZ'),
            source = "test",
            forced = true
        }
    end
   
    debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
     return nil
     return nil
end
end


-- Return the module
return Achievements
return Achievements

Revision as of 01:30, 1 April 2025

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

-- Module:AchievementSystem
-- Achievement system that loads data from MediaWiki:AchievementData.json.
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
-- This module only assigns CSS classes based on achievement IDs in the format:
--   .person-template .template-title.achievement-{id}::after {}
--
-- The module does not use any styling information from the JSON data structure.

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

-- Attempt to load JSON handling
local json
local jsonLoaded = pcall(function()
    json = require('Module:JSON')
end)

if not jsonLoaded or not json then
    json = { decode = function() return nil end }
    debugLog('WARNING: Module:JSON not available, achievement features will be limited')
end

-- Simple HTML encode fallback
local function htmlEncode(str)
    if mw.text and mw.text.htmlEncode then
        return mw.text.htmlEncode(str or '')
    else
        return (str or '')
            :gsub('&', '&amp;')
            :gsub('<', '&lt;')
            :gsub('>', '&gt;')
            :gsub('"', '&quot;')
    end
end

-- The page storing JSON data
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'

-- Cache for achievement data (within one request)
local dataCache = nil

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

--------------------------------------------------------------------------------
-- Internal: load achievement data from the page, or from cache
--------------------------------------------------------------------------------
function Achievements.loadData()
    mw.log("JSON-DEBUG: Starting to load achievement data")

    if dataCache then
        mw.log("JSON-DEBUG: Using request-level cached data")
        return dataCache
    end

    local success, data = pcall(function()
        -- First try to load from parser cache
        local loadDataSuccess, cachedData = pcall(function()
            return mw.loadData('Module:AchievementSystem')
        end)

        if loadDataSuccess and cachedData then
            mw.log("JSON-DEBUG: Using mw.loadData cached data")
            return cachedData
        else
            mw.log("JSON-DEBUG: mw.loadData failed or returned empty, using direct page load")
        end

        local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
        if not pageTitle or not pageTitle.exists then
            mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
            return DEFAULT_DATA
        end

        local content = pageTitle:getContent()
        if not content or content == '' then
            mw.log("JSON-DEBUG: Page content is empty")
            return DEFAULT_DATA
        end

        mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
        local parseSuccess, parsedData = pcall(function()
            return json.decode(content)
        end)

        if not parseSuccess or not parsedData then
            mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or 'unknown error'))
            return DEFAULT_DATA
        end

        mw.log("JSON-DEBUG: Successfully loaded achievement data")
        return parsedData
    end)

    if not success or not data then
        mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
        data = DEFAULT_DATA
    end

    dataCache = data
    return data
end

--------------------------------------------------------------------------------
-- Checks if a user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
    if not pageId or pageId == '' then
        return false
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return false
    end

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

    -- Check for achievements under n-prefixed key (legacy)
    if key:match("^%d+$") then
        local alt = "n" .. key
        if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
            return true
        end
    end

    return false
end

--------------------------------------------------------------------------------
-- Retrieves the display name for a given achievement type
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
    if not achievementType or achievementType == '' then
        debugLog("Empty achievement type provided to getAchievementName")
        mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
        return 'Unknown'
    end

    debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")

    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

    for i, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            if typeData.name and typeData.name ~= "" then
                debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                return typeData.name
            else
                mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name")
                return achievementType
            end
        end
    end

    mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type: '" .. achievementType .. "'")
    return achievementType
end

--------------------------------------------------------------------------------
-- Finds the top-tier achievement for a user
-- Returns the CSS class and the achievement name
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
    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 '', ''
    end

    local key = tostring(pageId)
    debugLog("Looking up achievements for ID: " .. key)

    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        userAchievements = data.user_achievements[altKey] or {}
    end

    if #userAchievements == 0 then
        debugLog("No achievements found for user " .. key)
        return '', ''
    end

    local highestTier = 999
    local highestAchievement = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achType then
                local tier = typeData.tier or 999
                if tier < highestTier then
                    highestTier = tier
                    highestAchievement = typeData
                end
            end
        end
    end

    if not highestAchievement or not highestAchievement.id then
        debugLog("No valid top-tier achievement found for user " .. key)
        return '', ''
    end

    local cssClass = "achievement-" .. highestAchievement.id
    local displayName = highestAchievement.name or highestAchievement.id or "Award"
    debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
    return cssClass, displayName
end

--------------------------------------------------------------------------------
-- Renders an achievement box showing the top-tier achievement
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
    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 userAchievements = data.user_achievements[key]
    if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
        userAchievements = data.user_achievements["n" .. key]
    end

    if not userAchievements or #userAchievements == 0 then
        return ''
    end

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

    if topAch then
        return string.format(
            '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
            topAch.id,
            htmlEncode(topAch.name or topAch.id or "")
        )
    end
    return ''
end

--------------------------------------------------------------------------------
-- Marks a page for cache purging
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
    return true
end

--------------------------------------------------------------------------------
-- Retrieves a specific achievement from a user if present
--------------------------------------------------------------------------------
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
        debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
        return nil
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
        return nil
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements == 0 and key:match("^%d+$") then
        userAchievements = data.user_achievements["n" .. key] or {}
    end

    for _, achievement in ipairs(userAchievements) do
        if achievement.type == achievementType then
            return achievement
        end
    end

    return nil
end

return Achievements