Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
 
minor rev // via Wikitext Extension for VSCode
Line 1: Line 1:
--[[ Module:AchievementSystem
-- Module:AchievementSystem
Implements the ICANNWiki Achievement System. It provides functions to:
-- Implements the ICANNWiki Achievement System. It provides functions to: 1) Load and cache achievement data from MediaWiki:AchievementData.json; 2) Retrieve achievement information for users; 3) Render achievement displays for Person templates; 4) Track pages using achievements for cache invalidation
- Load and cache achievement data from MediaWiki:AchievementData.json
- Retrieve achievement information for users
- Render achievement displays for Person templates
- Track pages using achievements for cache invalidation
]]


local Achievements = {}
local Achievements = {}

Revision as of 21:58, 28 March 2025

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

-- Module:AchievementSystem
-- Implements the ICANNWiki Achievement System. It provides functions to: 1) Load and cache achievement data from MediaWiki:AchievementData.json; 2) Retrieve achievement information for users; 3) Render achievement displays for Person templates; 4) Track pages using achievements for cache invalidation

local Achievements = {}
local json = require('Module:JSON')

-- Constants
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local CACHE_VERSION_KEY = 'achievement_cache_version'
local DEFAULT_CACHE_VERSION = 0

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

-- Safely attempts to get page content, returns nil on error or if page doesn't exist
local function safeGetPageContent(pageName)
    local success, page = pcall(function() return mw.title.new(pageName) end)
    
    if not success or not page or not page.exists then
        return nil
    end
    
    local content = page:getContent()
    if not content or content == '' then
        return nil
    end
    
    return content
end

-- Safely attempts to parse JSON, returns nil on error
local function safeParseJSON(jsonString)
    if not jsonString then return nil end
    
    local success, data = pcall(function() return json.decode(jsonString) end)
    if not success or not data then
        return nil
    end
    
    return data
end

-- Returns a default empty achievement data structure
local function getDefaultData()
    return { 
        schema_version = 1,
        last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
        achievement_types = {}, 
        user_achievements = {},
        cache_control = { version = DEFAULT_CACHE_VERSION }
    }
end

-- Loads achievement data from MediaWiki:AchievementData.json. Uses caching to avoid repeated loading within the same page render. Defaults empty structure on failure.
function Achievements.loadData()
    -- First, check the in-module request cache
    if dataCache then
        return dataCache
    end
    
    -- Try to load from mw.loadData cache (parser cache)
    local cacheSuccess, cachedData = pcall(function()
        return mw.loadData('Module:AchievementData')
    end)
    
    -- If cached data was loaded successfully, validate cache version
    if cacheSuccess and cachedData and cachedData.cache_control then
        local currentVersion = cachedData.cache_control.version or DEFAULT_CACHE_VERSION
        
        -- Use cached data if version hasn't changed
        if cacheVersion == nil then
            -- Initialize cacheVersion by checking the real data
            local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
            local freshData = safeParseJSON(content)
            
            if freshData and freshData.cache_control then
                cacheVersion = freshData.cache_control.version or DEFAULT_CACHE_VERSION
            else
                cacheVersion = DEFAULT_CACHE_VERSION
            end
        end
        
        -- If versions match, use the cached data
        if currentVersion == cacheVersion then
            dataCache = cachedData
            return dataCache
        end
    end
    
    -- Load data directly from the wiki page if cache is invalid
    local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
    local data = safeParseJSON(content)
    
    -- If something went wrong, use default empty data
    if not data then
        mw.log('Error loading achievement data - using default empty structure')
        data = getDefaultData()
    end
    
    -- Update cache variables
    dataCache = data
    cacheVersion = data.cache_control and data.cache_control.version or DEFAULT_CACHE_VERSION
    
    return data
end

--[[
Checks if a user has any achievements:
@param username string to check (with or without "User:" prefix)
@return boolean True if the user has any achievements, false otherwise
]]
function Achievements.hasAchievements(username)
    if not username or username == '' then return false end
    
    -- Normalize username (ensure it has User: prefix)
    if not username:match('^User:') then
        username = 'User:' .. username
    end
    
    local data = Achievements.loadData()
    local userAchievements = data.user_achievements[username]
    
    return userAchievements and #userAchievements > 0
end

--[[
Gets the highest tier achievement for a user:
@param username string The username to check (with or without "User:" prefix)
@return table|nil The achievement type data or nil if user has no achievements
]]
function Achievements.getHighestAchievement(username)
    if not username or username == '' then return nil end
    
    -- Normalize username (ensure it has User: prefix)
    if not username:match('^User:') then
        username = 'User:' .. username
    end
    
    local data = Achievements.loadData()
    local userAchievements = data.user_achievements[username]
    
    if not userAchievements or #userAchievements == 0 then return nil end
    
    -- Find achievement with lowest tier number (highest importance)
    local highestAchievement = nil
    local highestTier = 999
    
    for _, achievement in ipairs(userAchievements) do
        local achievementType = achievement.type
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
                highestAchievement = typeData
                highestTier = typeData.tier or 999
            end
        end
    end
    
    return highestAchievement
end

--[[
Gets the CSS class for the highest achievement to be applied to the template title:
@param username string The username to check
@return string The CSS class name or empty string if no achievement
]]
function Achievements.getTitleClass(username)
    local achievement = Achievements.getHighestAchievement(username)
    if not achievement then return '' end
    
    return 'achievement-' .. achievement.id
end

--[[
Gets all achievements for a user, formatted for display:
@param username string The username to check
@return table Array of achievement data objects for display
]]
function Achievements.getUserAchievements(username)
    if not username or username == '' then return {} end
    
    -- Normalize username (ensure it has User: prefix)
    if not username:match('^User:') then
        username = 'User:' .. username
    end
    
    local data = Achievements.loadData()
    local userAchievements = data.user_achievements[username] or {}
    local result = {}
    
    for _, achievement in ipairs(userAchievements) do
        local achievementType = achievement.type
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achievementType then
                table.insert(result, {
                    id = typeData.id,
                    name = typeData.name,
                    description = typeData.description,
                    icon = typeData.display.icon,
                    color = typeData.display.color,
                    background = typeData.display.background,
                    granted_date = achievement.granted_date
                })
            end
        end
    end
    
    -- Sort by tier
    table.sort(result, function(a, b)
        local tierA = 999
        local tierB = 999
        
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == a.id then tierA = typeData.tier or 999 end
            if typeData.id == b.id then tierB = typeData.tier or 999 end
        end
        
        return tierA < tierB
    end)
    
    return result
end

--[[
Renders HTML for an achievement box to display in templates:
@param username string The username to render achievements for
@return string HTML for the achievement box or empty string if no achievements
]]
function Achievements.renderAchievementBox(username)
    local achievements = Achievements.getUserAchievements(username)
    if #achievements == 0 then return '' end
    
    local html = '<div class="achievement-box">'
    html = html .. '<div class="achievement-box-title">Achievements</div>'
    html = html .. '<div class="achievement-badges">'
    
    for _, achievement in ipairs(achievements) do
        html = html .. string.format(
            '<div class="achievement-badge" style="color: %s; background-color: %s;" title="%s">%s %s</div>',
            achievement.color,
            achievement.background,
            mw.text.htmlEncode(achievement.description or ''),
            achievement.icon or '',
            mw.text.htmlEncode(achievement.name or '')
        )
    end
    
    html = html .. '</div></div>'
    return html
end

--[[
Tracks a page that displays achievements for cache purging
Note: This would ideally update the JSON with page references, but
      for now we rely on the cache version mechanism for invalidation
@param pageName string The page name to track
@return boolean Always returns true (for future expansion)
]]
function Achievements.trackPage(pageName)
    -- This would ideally be implemented to write back to the JSON
    -- For now, we just return true and rely on version-based cache invalidation
    return true
end

-- Return the module
return Achievements