Jump to content

Module:AchievementSystem

Revision as of 03:42, 30 March 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)

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.

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'

-- 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()
    -- Check if we can use the request-level cache
    if dataCache then
        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
            debugLog("Using cached achievement data")
            return cachedData
        end
        
        -- Fall back to direct page load
        local content = nil
        local pageSuccess, page = pcall(function() return mw.title.new(ACHIEVEMENT_DATA_PAGE) end)
        
        if pageSuccess and page and page.exists then
            content = page:getContent()
        end
        
        if not content or content == '' then
            debugLog("Failed to load achievement data from page")
            return DEFAULT_DATA
        end
        
        -- Parse JSON
        local parsedData = json.decode(content)
        if not parsedData then
            debugLog("Failed to parse achievement data JSON")
            return DEFAULT_DATA
        end
        
        -- Log successful load
        debugLog("Successfully loaded achievement data")
        
        return parsedData
    end)
    
    -- Handle errors
    if not success or not data then
        debugLog('Error loading achievement data: ' .. 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 18451 - force true for testing
    if key == "18451" then
        debugLog("Special case: Forcing true for page ID 18451")
        return true
    end
    
    return false
end

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

@param pageId string|number The page ID to check
@return string The CSS class name or empty string 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 page ID 18451
    if key == "18451" and #userAchievements == 0 then
        debugLog("Special override for page ID 18451")
        return "achievement-jedi"
    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
    debugLog("Using achievement class: " .. className)
    return className
end

--[[
Ultra simplified achievement box renderer - just shows "Test" directly

@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 page ID 18451, return a simple test achievement
    if tostring(pageId) == "18451" then
        debugLog("Creating direct Test achievement for page ID 18451")
        return '<div style="padding: 5px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px;">Test</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)
    
    -- Check if user has achievements (direct key or n-prefix)
    local hasAchievements = (data.user_achievements[key] and #data.user_achievements[key] > 0) or
                          (key:match("^%d+$") and data.user_achievements["n" .. key] and #data.user_achievements["n" .. key] > 0)
    
    -- If user has achievements, show the simplified box
    if hasAchievements then
        return '<div style="padding: 5px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px;">Test</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 page ID 18451 - always force all three buttons
    if tostring(pageId) == "18451" then
        -- Add more direct console logging to ensure visibility
        mw.log("ACHIEVEMENT-CONSOLE: Testing for achievement type: " .. achievementType)
        
        if achievementType == "jedi" then
            mw.log("ACHIEVEMENT-CONSOLE: Forcing jedi achievement")
            return {
                type = "jedi",
                granted_date = os.date('!%Y-%m-%dT%H:%M:%SZ'),
                source = "test",
                forced = true
            }
        elseif achievementType == "champion" then
            mw.log("ACHIEVEMENT-CONSOLE: Forcing champion achievement")
            return {
                type = "champion",
                granted_date = os.date('!%Y-%m-%dT%H:%M:%SZ'),
                source = "test",
                forced = true
            }
        elseif achievementType == "sponsor" then
            mw.log("ACHIEVEMENT-CONSOLE: Forcing sponsor achievement")
            return {
                type = "sponsor",
                granted_date = os.date('!%Y-%m-%dT%H:%M:%SZ'),
                source = "test",
                forced = true
            }
        end
    end
    
    debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
    return nil
end

-- Return the module
return Achievements