Module:AchievementSystem
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('&', '&'):gsub('<', '<'):gsub('>', '>'):gsub('"', '"')
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
-- Simplified debug for tracing purposes
debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
local data = Achievements.loadData()
if not data or not data.achievement_types then
debugLog("No achievement data available")
return achievementType -- Fall back to the ID as a last resort
end
-- Simple direct loop through achievement types to find a match
for _, typeData in ipairs(data.achievement_types) do
if typeData.id == achievementType then
-- Return the name if available, otherwise fall back to ID
if typeData.name and typeData.name ~= "" then
debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
return typeData.name
else
debugLog("Achievement found but has no name, using ID as fallback")
return achievementType
end
end
end
-- If we get here, we couldn't find the achievement type
debugLog("No achievement found with type: " .. tostring(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 - get title-test achievement from JSON properly
if isTestPage(pageId) then
debugLog("Getting title-test achievement for test page")
-- Look up the proper achievement name from JSON
local achievementName = Achievements.getAchievementName("title-test")
debugLog("Retrieved achievement name: " .. 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