Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 249: Line 249:
     end
     end


     -- Direct console log for better visibility of what we're looking up
     -- ULTRA VERBOSE DEBUG OUTPUT TO TRACE ISSUE
     mw.log("NAME-DEBUG: Looking up name for achievement type: " .. achievementType)
    mw.log("NAME-DEBUG: ======= START getAchievementName =======")
    mw.log("NAME-DEBUG: Looking up achievement type: '" .. tostring(achievementType) .. "'")
   
    -- Check if we're doing a direct match or some other comparison
     mw.log("NAME-DEBUG: Type of achievementType is: " .. type(achievementType))
    mw.log("NAME-DEBUG: Raw value: [" .. tostring(achievementType) .. "]")
      
      
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.achievement_types then
     if not data then
         mw.log("NAME-DEBUG: No achievement data available for name lookup")
        mw.log("NAME-DEBUG: CRITICAL ERROR: data is nil")
        return achievementType -- Fall back to the ID as a last resort
    end
   
    mw.log("NAME-DEBUG: Data loaded successfully, type: " .. type(data))
   
    if not data.achievement_types then
         mw.log("NAME-DEBUG: ERROR: No achievement_types in data")
         return achievementType -- Fall back to the ID as a last resort
         return achievementType -- Fall back to the ID as a last resort
     end
     end
      
      
     -- Log the loaded data to see what's available
     mw.log("NAME-DEBUG: achievement_types found, count: " .. #data.achievement_types)
    mw.log("NAME-DEBUG: Achievement types found: " .. #data.achievement_types)
   
    -- Dump full JSON for examination
    mw.log("NAME-DEBUG: === FULL DATA DUMP ===")
    pcall(function()
        -- Try to stringify the whole data structure
        mw.log("NAME-DEBUG: " .. mw.text.jsonEncode(data.achievement_types))
    end)
      
      
     -- Print all available achievements to diagnose
     -- Print every available achievement with all their fields
    mw.log("NAME-DEBUG: === SCANNING ALL ACHIEVEMENTS ===")
     for i, typeData in ipairs(data.achievement_types) do
     for i, typeData in ipairs(data.achievement_types) do
         mw.log("NAME-DEBUG: Achievement #" .. i .. ": id='" .. tostring(typeData.id) ..
        -- Try to print everything about this achievement
              "', name='" .. tostring(typeData.name) .. "'")
        local fields = ""
        for k, v in pairs(typeData) do
            fields = fields .. k .. "='" .. tostring(v) .. "', "
        end
       
         mw.log("NAME-DEBUG: Achievement #" .. i .. ": " .. fields)
       
        -- Specific check for our target
        if tostring(typeData.id) == tostring(achievementType) then
            mw.log("NAME-DEBUG: POTENTIAL MATCH BY ID - Full details: " .. fields)
        end
     end
     end
      
      
     -- Loop through achievement types to find a match
     -- Loop through achievement types to find a match - with extra checks
     for _, typeData in ipairs(data.achievement_types) do
     for i, typeData in ipairs(data.achievement_types) do
         if typeData.id == achievementType then
         -- Debug exact equality check
             mw.log("NAME-DEBUG: MATCH FOUND! Achievement type '" .. achievementType ..  
        local idMatch = (typeData.id == achievementType)
                   "' has name '" .. tostring(typeData.name) .. "'")
        local idToString = (tostring(typeData.id) == tostring(achievementType))
       
        mw.log("NAME-DEBUG: Comparing - Item " .. i .. ": id='" .. tostring(typeData.id) ..
              "' to '" .. tostring(achievementType) .. "' - Direct match: " .. tostring(idMatch) ..
              ", String match: " .. tostring(idToString))
       
        if idMatch then
             mw.log("NAME-DEBUG: !!! MATCH FOUND !!! - type #" .. i)
            mw.log("NAME-DEBUG: Match details - id: " .. tostring(typeData.id) ..  
                   ", name: " .. tostring(typeData.name) ..  
                  ", description: " .. tostring(typeData.description))
              
              
             -- Make absolutely sure we're returning the name, not the ID
             -- Make absolutely sure we're returning the name, not the ID
             if typeData.name and typeData.name ~= "" then
             if typeData.name and typeData.name ~= "" then
                mw.log("NAME-DEBUG: Returning name: '" .. typeData.name .. "'")
                 return typeData.name
                 return typeData.name
             else
             else
                 mw.log("NAME-DEBUG: Achievement found but name is empty, using ID as fallback")
                 mw.log("NAME-DEBUG: Name is empty, using ID as fallback: '" .. achievementType .. "'")
                 return achievementType
                 return achievementType
             end
             end
Line 284: Line 324:
      
      
     -- If we get here, we couldn't find the achievement type
     -- If we get here, we couldn't find the achievement type
     mw.log("NAME-DEBUG: No match found for achievement type: " .. achievementType)
     mw.log("NAME-DEBUG: NO MATCH FOUND for type: '" .. achievementType .. "'")
    mw.log("NAME-DEBUG: Returning ID as fallback: '" .. achievementType .. "'")
    mw.log("NAME-DEBUG: ======= END getAchievementName =======")
   
     return achievementType -- Fall back to the ID as a last resort
     return achievementType -- Fall back to the ID as a last resort
end
end
Line 324: Line 367:
         mw.log("TITLE-DEBUG: Returning title-test achievement class for test page")
         mw.log("TITLE-DEBUG: Returning title-test achievement class for test page")
          
          
         -- Explicitly look up the achievement name for title-test in JSON
         -- DIRECT OVERRIDE: Use the Developer name from JSON without lookups
        local data = Achievements.loadData()
         local achievementName = "Developer"
         local achievementName = nil
        mw.log("TITLE-DEBUG: FORCING title-test achievement name to: " .. achievementName)
       
        if data and data.achievement_types then
            for _, typeData in ipairs(data.achievement_types) do
                if typeData.id == "title-test" and typeData.name then
                    achievementName = typeData.name
                    mw.log("TITLE-DEBUG: Found title-test achievement with name: " .. achievementName)
                    break
                end
            end
        end
       
        -- Fallback if name not found in JSON
        if not achievementName or achievementName == "" then
            achievementName = "Title Test"
            mw.log("TITLE-DEBUG: Using fallback name for title-test: " .. achievementName)
        end
          
          
         return "achievement-title-test", achievementName
         return "achievement-title-test", achievementName

Revision as of 16:39, 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

    -- ULTRA VERBOSE DEBUG OUTPUT TO TRACE ISSUE
    mw.log("NAME-DEBUG: ======= START getAchievementName =======")
    mw.log("NAME-DEBUG: Looking up achievement type: '" .. tostring(achievementType) .. "'")
    
    -- Check if we're doing a direct match or some other comparison
    mw.log("NAME-DEBUG: Type of achievementType is: " .. type(achievementType))
    mw.log("NAME-DEBUG: Raw value: [" .. tostring(achievementType) .. "]")
    
    local data = Achievements.loadData()
    if not data then
        mw.log("NAME-DEBUG: CRITICAL ERROR: data is nil")
        return achievementType -- Fall back to the ID as a last resort
    end
    
    mw.log("NAME-DEBUG: Data loaded successfully, type: " .. type(data))
    
    if not data.achievement_types then
        mw.log("NAME-DEBUG: ERROR: No achievement_types in data")
        return achievementType -- Fall back to the ID as a last resort
    end
    
    mw.log("NAME-DEBUG: achievement_types found, count: " .. #data.achievement_types)
    
    -- Dump full JSON for examination
    mw.log("NAME-DEBUG: === FULL DATA DUMP ===")
    pcall(function() 
        -- Try to stringify the whole data structure
        mw.log("NAME-DEBUG: " .. mw.text.jsonEncode(data.achievement_types)) 
    end)
    
    -- Print every available achievement with all their fields
    mw.log("NAME-DEBUG: === SCANNING ALL ACHIEVEMENTS ===")
    for i, typeData in ipairs(data.achievement_types) do
        -- Try to print everything about this achievement
        local fields = ""
        for k, v in pairs(typeData) do
            fields = fields .. k .. "='" .. tostring(v) .. "', "
        end
        
        mw.log("NAME-DEBUG: Achievement #" .. i .. ": " .. fields)
        
        -- Specific check for our target
        if tostring(typeData.id) == tostring(achievementType) then
            mw.log("NAME-DEBUG: POTENTIAL MATCH BY ID - Full details: " .. fields)
        end
    end
    
    -- Loop through achievement types to find a match - with extra checks
    for i, typeData in ipairs(data.achievement_types) do
        -- Debug exact equality check
        local idMatch = (typeData.id == achievementType)
        local idToString = (tostring(typeData.id) == tostring(achievementType))
        
        mw.log("NAME-DEBUG: Comparing - Item " .. i .. ": id='" .. tostring(typeData.id) .. 
               "' to '" .. tostring(achievementType) .. "' - Direct match: " .. tostring(idMatch) .. 
               ", String match: " .. tostring(idToString))
        
        if idMatch then
            mw.log("NAME-DEBUG: !!! MATCH FOUND !!! - type #" .. i)
            mw.log("NAME-DEBUG: Match details - id: " .. tostring(typeData.id) .. 
                  ", name: " .. tostring(typeData.name) .. 
                  ", description: " .. tostring(typeData.description))
            
            -- Make absolutely sure we're returning the name, not the ID
            if typeData.name and typeData.name ~= "" then
                mw.log("NAME-DEBUG: Returning name: '" .. typeData.name .. "'")
                return typeData.name
            else
                mw.log("NAME-DEBUG: Name is empty, using ID as fallback: '" .. achievementType .. "'")
                return achievementType
            end
        end
    end
    
    -- If we get here, we couldn't find the achievement type
    mw.log("NAME-DEBUG: NO MATCH FOUND for type: '" .. achievementType .. "'")
    mw.log("NAME-DEBUG: Returning ID as fallback: '" .. achievementType .. "'")
    mw.log("NAME-DEBUG: ======= END getAchievementName =======")
    
    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")
        
        -- DIRECT OVERRIDE: Use the Developer name from JSON without lookups
        local achievementName = "Developer"
        mw.log("TITLE-DEBUG: FORCING title-test achievement name to: " .. achievementName)
        
        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 proper title-test achievement name from JSON
        local achievementName = Achievements.getAchievementName("title-test")
        mw.log("ACHIEVEMENT-BOX: Using achievement name from getAchievementName(): " .. achievementName)
        
        return '<div class="achievement-box-simple" data-achievement-type="title-test" data-achievement-name="' .. 
               htmlEncode(achievementName) .. '">' .. 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