Jump to content

Module:AchievementSystem: Difference between revisions

minor rev // via Wikitext Extension for VSCode
No edit summary
Line 1: Line 1:
-- Module:AchievementSystem
-- 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
-- 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 Achievements = {}
local json = require('Module:JSON')
 
-- 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
    }
    mw.log('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
-- Constants
Line 13: Line 35:
local dataCache = nil
local dataCache = nil
local cacheVersion = nil
local cacheVersion = 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 = DEFAULT_CACHE_VERSION }
}


-- Safely attempts to get page content, returns nil on error or if page doesn't exist
-- Safely attempts to get page content, returns nil on error or if page doesn't exist
local function safeGetPageContent(pageName)
local function safeGetPageContent(pageName)
     local success, page = pcall(function() return mw.title.new(pageName) end)
     local success, result = pcall(function()
        local page = mw.title.new(pageName)
        if 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)
      
      
     if not success or not page or not page.exists then
     if not success then
        mw.log('Error getting page content: ' .. (result or 'unknown error'))
         return nil
         return nil
     end
     end
      
      
     local content = page:getContent()
     return result
    if not content or content == '' then
        return nil
    end
   
    return content
end
end


Line 33: Line 72:
local function safeParseJSON(jsonString)
local function safeParseJSON(jsonString)
     if not jsonString then return nil end
     if not jsonString then return nil end
    if not json or not json.decode then return nil end
      
      
     local success, data = pcall(function() return json.decode(jsonString) end)
     local success, data = pcall(function() return json.decode(jsonString) end)
     if not success or not data then
     if not success or not data then
        mw.log('Error parsing JSON: ' .. (data or 'unknown error'))
         return nil
         return nil
     end
     end
Line 42: Line 83:
end
end


-- Returns a default empty achievement data structure
--[[
local function getDefaultData()
Loads achievement data from MediaWiki:AchievementData.json
    return {
Uses caching to avoid repeated loading within the same page render
        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.
@return table The achievement data structure or default empty structure on failure
]]
function Achievements.loadData()
function Achievements.loadData()
     -- First, check the in-module request cache
     local success, result = pcall(function()
    if dataCache then
        -- First, check the in-module request cache
        return dataCache
        if dataCache then
    end
            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
         -- Try to load from mw.loadData cache (parser cache)
         if cacheVersion == nil then
         local cacheSuccess, cachedData = pcall(function()
            -- Initialize cacheVersion by checking the real data
             return mw.loadData('Module:AchievementData')
            local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
         end)
             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 cached data was loaded successfully, use it
         if currentVersion == cacheVersion then
         if cacheSuccess and cachedData then
             dataCache = cachedData
             dataCache = cachedData
             return dataCache
             return dataCache
         end
         end
    end
       
   
        -- Load data directly from the wiki if cache is invalid
    -- Load data directly from the wiki page if cache is invalid
        local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
    local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
        local data = safeParseJSON(content)
    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 = DEFAULT_DATA
        end
       
        -- Update cache variables
        dataCache = data
       
        return data
    end)
      
      
    -- If something went wrong, use default empty data
     if not success then
     if not data then
         mw.log('Error in loadData: ' .. (result or 'unknown error'))
         mw.log('Error loading achievement data - using default empty structure')
         return DEFAULT_DATA
         data = getDefaultData()
     end
     end
      
      
    -- Update cache variables
     return result
    dataCache = data
    cacheVersion = data.cache_control and data.cache_control.version or DEFAULT_CACHE_VERSION
   
     return data
end
end


--[[
--[[
Checks if a user has any achievements:
Checks if a user has any achievements
@param username string to check (with or without "User:" prefix)
 
@param username string The username to check (with or without "User:" prefix)
@return boolean True if the user has any achievements, false otherwise
@return boolean True if the user has any achievements, false otherwise
]]
]]
function Achievements.hasAchievements(username)
function Achievements.hasAchievements(username)
     if not username or username == '' then return false end
     local success, result = pcall(function()
        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()
        if not data or not data.user_achievements then
            return false
        end
       
        local userAchievements = data.user_achievements[username]
       
        return userAchievements and #userAchievements > 0
    end)
      
      
    -- Normalize username (ensure it has User: prefix)
     if not success then
     if not username:match('^User:') then
         mw.log('Error in hasAchievements: ' .. (result or 'unknown error'))
         username = 'User:' .. username
        return false
     end
     end
      
      
    local data = Achievements.loadData()
     return result
    local userAchievements = data.user_achievements[username]
   
     return userAchievements and #userAchievements > 0
end
end


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


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


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


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


Line 254: Line 343:
Note: This would ideally update the JSON with page references, but
Note: This would ideally update the JSON with page references, but
       for now we rely on the cache version mechanism for invalidation
       for now we rely on the cache version mechanism for invalidation
@param pageName string The page name to track
@param pageName string The page name to track
@return boolean Always returns true (for future expansion)
@return boolean Always returns true (for future expansion)
]]
]]
function Achievements.trackPage(pageName)
function Achievements.trackPage(pageName)
     -- This would ideally be implemented to write back to the JSON
     -- This function is designed to be safe by default
    -- For now, we just return true and rely on version-based cache invalidation
     return true
     return true
end
end

Revision as of 22:21, 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 = {}

-- 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
    }
    mw.log('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'
local CACHE_VERSION_KEY = 'achievement_cache_version'
local DEFAULT_CACHE_VERSION = 0

-- Cache for achievement data (within request)
local dataCache = nil
local cacheVersion = 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 = DEFAULT_CACHE_VERSION }
}

-- Safely attempts to get page content, returns nil on error or if page doesn't exist
local function safeGetPageContent(pageName)
    local success, result = pcall(function()
        local page = mw.title.new(pageName)
        if 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)
    
    if not success then
        mw.log('Error getting page content: ' .. (result or 'unknown error'))
        return nil
    end
    
    return result
end

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

--[[
Loads achievement data from MediaWiki:AchievementData.json
Uses caching to avoid repeated loading within the same page render

@return table The achievement data structure or default empty structure on failure
]]
function Achievements.loadData()
    local success, result = pcall(function()
        -- 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, use it
        if cacheSuccess and cachedData then
            dataCache = cachedData
            return dataCache
        end
        
        -- Load data directly from the wiki 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 = DEFAULT_DATA
        end
        
        -- Update cache variables
        dataCache = data
        
        return data
    end)
    
    if not success then
        mw.log('Error in loadData: ' .. (result or 'unknown error'))
        return DEFAULT_DATA
    end
    
    return result
end

--[[
Checks if a user has any achievements

@param username string The username to check (with or without "User:" prefix)
@return boolean True if the user has any achievements, false otherwise
]]
function Achievements.hasAchievements(username)
    local success, result = pcall(function()
        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()
        if not data or not data.user_achievements then
            return false
        end
        
        local userAchievements = data.user_achievements[username]
        
        return userAchievements and #userAchievements > 0
    end)
    
    if not success then
        mw.log('Error in hasAchievements: ' .. (result or 'unknown error'))
        return false
    end
    
    return result
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)
    local success, result = pcall(function()
        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()
        if not data or not data.user_achievements then
            return nil
        end
        
        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 or {}) 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)
    
    if not success then
        mw.log('Error in getHighestAchievement: ' .. (result or 'unknown error'))
        return nil
    end
    
    return result
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 success, result = pcall(function()
        local achievement = Achievements.getHighestAchievement(username)
        if not achievement or not achievement.id then return '' end
        
        return 'achievement-' .. achievement.id
    end)
    
    if not success then
        mw.log('Error in getTitleClass: ' .. (result or 'unknown error'))
        return ''
    end
    
    return result
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)
    local success, result = pcall(function()
        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()
        if not data or not data.user_achievements then
            return {}
        end
        
        local userAchievements = data.user_achievements[username] or {}
        local results = {}
        
        for _, achievement in ipairs(userAchievements) do
            if achievement and achievement.type then
                local achievementType = achievement.type
                for _, typeData in ipairs(data.achievement_types or {}) do
                    if typeData.id == achievementType then
                        table.insert(results, {
                            id = typeData.id,
                            name = typeData.name,
                            description = typeData.description,
                            icon = typeData.display and typeData.display.icon or '',
                            color = typeData.display and typeData.display.color or '',
                            background = typeData.display and typeData.display.background or '',
                            granted_date = achievement.granted_date
                        })
                    end
                end
            end
        end
        
        -- Sort by tier
        table.sort(results, function(a, b)
            local tierA = 999
            local tierB = 999
            
            for _, typeData in ipairs(data.achievement_types or {}) 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 results
    end)
    
    if not success then
        mw.log('Error in getUserAchievements: ' .. (result or 'unknown error'))
        return {}
    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 success, result = pcall(function()
        local achievements = Achievements.getUserAchievements(username)
        if not achievements or #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 or '',
                achievement.background or '',
                htmlEncode(achievement.description or ''),
                achievement.icon or '',
                htmlEncode(achievement.name or '')
            )
        end
        
        html = html .. '</div></div>'
        return html
    end)
    
    if not success then
        mw.log('Error in renderAchievementBox: ' .. (result or 'unknown error'))
        return ''
    end
    
    return result
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 function is designed to be safe by default
    return true
end

-- Return the module
return Achievements