Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 269: Line 269:


--[[
--[[
Gets the CSS class for the highest achievement to be applied to the template title
Gets the CSS class and name for the highest achievement to be applied to the template title


@param pageId string|number The page ID to check
@param pageId string|number The page ID to check
@return string The CSS class name or empty string if no achievement
@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
      
      
Line 283: Line 283:
     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")
         return ''
         return '', ''
     end
     end
      
      
Line 303: Line 303:
     if isTestPage(pageId) then
     if isTestPage(pageId) then
         mw.log("TITLE-DEBUG: Returning title-test achievement class for test page")
         mw.log("TITLE-DEBUG: Returning title-test achievement class for test page")
         return "achievement-title-test"
        -- Look up the name for title-test achievement
        local achievementName = Achievements.getAchievementName("title-test")
         return "achievement-title-test", achievementName
     end
     end
      
      
     if #userAchievements == 0 then
     if #userAchievements == 0 then
         debugLog("No achievements found")
         debugLog("No achievements found")
         return ''
         return '', ''
     end
     end
      
      
Line 330: Line 332:
     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 achievement type found")
         return ''
         return '', ''
     end
     end
      
      
     local className = 'achievement-' .. highestAchievement.id
     local className = 'achievement-' .. highestAchievement.id
     debugLog("Using achievement class: " .. className)
    local achievementName = highestAchievement.name or highestAchievement.id or "Award"
     return className
   
     debugLog("Using achievement class: " .. className .. " with name: " .. achievementName)
     return className, achievementName
end
end



Revision as of 16:21, 31 March 2025

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

-- Module:AchievementSystem
-- Simplified achievement system that loads data from MediaWiki:AchievementData.json,
-- retrieves achievement information for pages, and renders achievement displays
-- for templates with simplified error handling and display.
--
-- 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 - set to true to enable console logging
local DEBUG_MODE = true
local function debugLog(message)
    if not DEBUG_MODE then return end
    
    -- Log to JavaScript console with structured data
    pcall(function()
        mw.logObject({
            system = "achievement_simple",
            message = message,
            timestamp = os.date('%H:%M:%S')
        })
    end)
    
    -- Backup log to MediaWiki
    mw.log("ACHIEVEMENT-DEBUG: " .. message)
end

-- Try to load JSON module with error handling
local json
local jsonLoaded = pcall(function()
    json = require('Module:JSON')
end)

-- If JSON module failed to load, create a minimal fallback
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

-- Create a fallback htmlEncode if not available
local htmlEncode = function(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

-- Constants
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'

-- Test/debug configuration
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"] = "title-test"
    },
    
    -- 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

-- Default data structure to use if loading 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 }
}

--[[
Loads achievement data from MediaWiki:AchievementData.json with caching
@return table The achievement data structure or default empty structure on failure
]]
function Achievements.loadData()
    -- Direct console log for better visibility
    mw.log("JSON-DEBUG: Starting to load achievement data")
    
    -- Check if we can use the request-level cache
    if dataCache then
        mw.log("JSON-DEBUG: Using request-level cached data")
        return dataCache
    end
    
    -- Try to load data with error handling
    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
        
        -- Fall back to direct page load
        local content = nil
        local pageSuccess, page = pcall(function() return mw.title.new(ACHIEVEMENT_DATA_PAGE) end)
        
        if not pageSuccess or not page then
            mw.log("JSON-DEBUG: Failed to create title object for " .. ACHIEVEMENT_DATA_PAGE)
            return DEFAULT_DATA
        end
        
        if not page.exists then
            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
            mw.log("JSON-DEBUG: Page content is empty")
            return DEFAULT_DATA
        end
        
        -- Log content statistics for debugging
        mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
        mw.log("JSON-DEBUG: First 100 chars: " .. content:sub(1, 100))
        mw.log("JSON-DEBUG: Contains '18451': " .. (content:find('"18451"') and "true" or "false"))
        
        -- Parse JSON with detailed error handling
        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
        
        -- 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")
        
        return parsedData
    end)
    
    -- Handle errors
    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
    
    -- Update request cache so we don't need to reload within this page render
    dataCache = data
    
    return data
end

--[[
Checks if a user has any achievements

@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()
    if not data or not data.user_achievements then return false end
    
    -- Convert to string for consistent lookup
    local key = tostring(pageId)
    
    -- Check for direct match
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        return true
    end
    
    -- Check for achievements under n-prefixed key (backward compatibility)
    if key:match("^%d+$") and data.user_achievements["n" .. key] and #data.user_achievements["n" .. key] > 0 then
        return true
    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
end

--[[
Gets the actual name of an achievement for display purposes

@param achievementType string The achievement type ID
@return string The display name for the achievement or a default value
]]
function Achievements.getAchievementName(achievementType)
    if not achievementType or achievementType == '' then 
        debugLog("Empty achievement type provided to getAchievementName")
        return 'Unknown' 
    end
    
    local data = Achievements.loadData()
    if not data or not data.achievement_types then
        debugLog("No achievement data available for name lookup")
        return achievementType -- Fall back to the ID as a last resort
    end
    
    -- Loop through achievement types to find a match
    for _, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            debugLog("Found name for achievement type " .. achievementType .. ": " .. typeData.name)
            return typeData.name
        end
    end
    
    -- If we get here, we couldn't find the achievement type
    debugLog("No name found for achievement type: " .. achievementType)
    return achievementType -- Fall back to the ID as a last resort
end

--[[
Gets the CSS class and name for the highest achievement to be applied to the template title

@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)
    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")
        return '', ''
    end
    
    -- Convert to string for consistent lookup
    local key = tostring(pageId)
    debugLog("Looking up achievements for ID: " .. key)
    
    -- Try with direct key first
    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
        local nKey = "n" .. key
        debugLog("Trying alternative key: " .. nKey)
        userAchievements = data.user_achievements[nKey] or {}
    end
    
    -- Special case for test page - always return title-test achievement class
    if isTestPage(pageId) then
        mw.log("TITLE-DEBUG: Returning title-test achievement class for test page")
        -- Look up the name for title-test achievement
        local achievementName = Achievements.getAchievementName("title-test")
        return "achievement-title-test", achievementName
    end
    
    if #userAchievements == 0 then
        debugLog("No achievements found")
        return '', ''
    end
    
    -- Find the highest tier (lowest number) achievement
    local highestAchievement = nil
    local highestTier = 999
    
    for _, achievement in ipairs(userAchievements) do
        local achievementType = achievement.type
        debugLog("Found achievement type: " .. achievementType)
        
        for _, typeData in ipairs(data.achievement_types or {}) do
            if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
                highestAchievement = typeData
                highestTier = typeData.tier or 999
                debugLog("New highest tier achievement: " .. typeData.id .. " (tier " .. (typeData.tier or "unknown") .. ")")
            end
        end
    end
    
    if not highestAchievement or not highestAchievement.id then
        debugLog("No valid achievement type found")
        return '', ''
    end
    
    local className = 'achievement-' .. highestAchievement.id
    local achievementName = highestAchievement.name or highestAchievement.id or "Award"
    
    debugLog("Using achievement class: " .. className .. " with name: " .. achievementName)
    return className, achievementName
end

--[[
Achievement box renderer - now shows real achievement names

@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)
    -- For test page, return a proper test achievement with name
    if isTestPage(pageId) then
        debugLog("Creating test achievement for test page")
        -- Look up the name for title-test achievement
        local achievementName = Achievements.getAchievementName("title-test")
        return '<div class="achievement-box-simple" data-achievement-type="title-test">' .. 
               htmlEncode(achievementName) .. '</div>'
    end
    
    -- Get achievements for other pages (if any)
    local data = Achievements.loadData()
    if not data or not data.user_achievements then return '' end
    
    -- Convert to string for consistent lookup
    local key = tostring(pageId)
    local userAchievements = {}
    
    -- 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]
    else
        -- No achievements found
        return ''
    end
    
    -- Find highest tier achievement (same logic as getTitleClass)
    local highestAchievement = nil
    local highestTier = 999
    
    for _, achievement in ipairs(userAchievements) do
        local achievementType = achievement.type
        
        for _, typeData in ipairs(data.achievement_types or {}) do
            if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
                highestAchievement = typeData
                highestTier = typeData.tier or 999
            end
        end
    end
    
    -- If we found a highest achievement, display it with its proper name
    if highestAchievement then
        return '<div class="achievement-box-simple" data-achievement-type="' .. 
               highestAchievement.id .. '">' .. htmlEncode(highestAchievement.name) .. '</div>'
    end
    
    -- Otherwise return empty string
    return ''
end

--[[
Tracks a page that displays achievements 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)
    -- This function is designed to be safe by default
    return true
end

--[[
Retrieves a specific achievement type for a user
@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)
    -- Log detailed info about what we're checking
    debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. achievementType .. "' achievement for ID: " .. tostring(pageId))
    
    if not pageId or pageId == '' or not achievementType then 
        debugLog("ACHIEVEMENT-DEBUG: Invalid inputs, pageId or achievementType missing")
        return nil 
    end
    
    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("ACHIEVEMENT-DEBUG: No achievement data available")
        return nil
    end
    
    -- Log what data is available at each step
    local key = tostring(pageId)
    debugLog("ACHIEVEMENT-DEBUG: Checking direct key: " .. key)
    
    -- First check in direct key
    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
    
    -- Then check n-prefixed key
    if key:match("^%d+$") then
        local nKey = "n" .. key
        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
    
    -- 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
end

-- Return the module
return Achievements