Module:AchievementSystem
Appearance
Documentation for this module may be created at Module:AchievementSystem/doc
-- Module:AchievementSystem
-- Achievement system that loads data from MediaWiki:AchievementData.json.
-- 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
local DEBUG_MODE = true
local function debugLog(message)
if not DEBUG_MODE then return end
pcall(function()
mw.logObject({
system = "achievement_simple",
message = message,
timestamp = os.date('%H:%M:%S')
})
end)
mw.log("ACHIEVEMENT-DEBUG: " .. message)
end
--------------------------------------------------------------------------------
-- JSON Handling
--------------------------------------------------------------------------------
local json
local jsonLoaded = pcall(function()
json = require('Module:JSON')
end)
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
-- Simple HTML encode fallback
local function htmlEncode(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
--------------------------------------------------------------------------------
-- Configuration, Default Data, and Cache
--------------------------------------------------------------------------------
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local dataCache = nil
local DEFAULT_DATA = {
schema_version = 1,
last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
achievement_types = {},
user_achievements = {},
cache_control = { version = 0 }
}
--------------------------------------------------------------------------------
-- (Optional) Testing config
-- Removed forced dev-role injection so it won't override normal JSON lookups.
--------------------------------------------------------------------------------
local TEST_CONFIG = {
enabled = true,
test_page_id = "18451",
test_user = "direct-test-user",
debug_messages = true,
force_achievements = false, -- Disabled forcing achievements
type_mapping = {
["jedi"] = "ach1",
["champion"] = "ach2",
["sponsor"] = "ach3",
["ach1"] = "ach1",
["ach2"] = "ach2",
["ach3"] = "ach3",
["title-test"] = "dev-role",
["dev-role"] = "dev-role"
},
test_achievements = {"title-test", "ach1", "ach2", "ach3"}
}
local function isTestPage(pageId)
return TEST_CONFIG.enabled and tostring(pageId) == TEST_CONFIG.test_page_id
end
--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
--------------------------------------------------------------------------------
function Achievements.loadData()
mw.log("JSON-DEBUG: Starting to load achievement data")
-- Use the request-level cache if we already loaded data once
if dataCache then
mw.log("JSON-DEBUG: Using request-level cached data")
return dataCache
end
local success, data = pcall(function()
-- Try parser cache first
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, proceeding to direct page load")
end
local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
if not pageTitle or not pageTitle.exists then
mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
return DEFAULT_DATA
end
local content = pageTitle:getContent()
if not content or content == '' then
mw.log("JSON-DEBUG: Page content is empty")
return DEFAULT_DATA
end
mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
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
mw.log("JSON-DEBUG: Successfully loaded achievement data")
return parsedData
end)
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
dataCache = data
return data
end
--------------------------------------------------------------------------------
-- Check if a page/user has any achievements
--------------------------------------------------------------------------------
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
local key = tostring(pageId)
if data.user_achievements[key] and #data.user_achievements[key] > 0 then
return true
end
-- Check for legacy "n123" style
if key:match("^%d+$") then
local alt = "n" .. key
if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
return true
end
end
-- We removed the forced "true" for test pages to avoid dev-role injection
return false
end
--------------------------------------------------------------------------------
-- Get a user-friendly name for a given achievement type
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
if not achievementType or achievementType == '' then
debugLog("Empty achievement type provided to getAchievementName")
mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
return 'Unknown'
end
debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
local data = Achievements.loadData()
if not data or not data.achievement_types then
mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
return achievementType
end
for i, typeData in ipairs(data.achievement_types) do
if typeData.id == achievementType then
if typeData.name and typeData.name ~= "" then
debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
return typeData.name
else
mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name; using ID")
return achievementType
end
end
end
mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type '" .. achievementType .. "'; using ID fallback")
return achievementType
end
--------------------------------------------------------------------------------
-- Find the top-tier achievement for the user (lowest tier number)
-- Return the CSS class and the readable achievement name
--------------------------------------------------------------------------------
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 in getTitleClass")
return '', ''
end
local key = tostring(pageId)
debugLog("Looking up achievements for ID: " .. key)
local userAchievements = data.user_achievements[key] or {}
if #userAchievements == 0 and key:match("^%d+$") then
local altKey = "n" .. key
userAchievements = data.user_achievements[altKey] or {}
end
if #userAchievements == 0 then
debugLog("No achievements found for user " .. key)
return '', ''
end
local highestTier = 999
local highestAchievement = nil
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
for _, typeData in ipairs(data.achievement_types) do
if typeData.id == achType then
local tier = typeData.tier or 999
if tier < highestTier then
highestTier = tier
highestAchievement = typeData
end
end
end
end
if not highestAchievement or not highestAchievement.id then
debugLog("No valid top-tier achievement found for user " .. key)
return '', ''
end
local cssClass = "achievement-" .. highestAchievement.id
local displayName = highestAchievement.name or highestAchievement.id or "Award"
debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
return cssClass, displayName
end
--------------------------------------------------------------------------------
-- Renders a simple "box" with the top-tier achievement for the user
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
if not pageId or pageId == '' then
return ''
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return ''
end
local key = tostring(pageId)
local userAchievements = data.user_achievements[key]
if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
userAchievements = data.user_achievements["n" .. key]
end
if not userAchievements or #userAchievements == 0 then
return ''
end
local highestTier = 999
local topAch = nil
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
for _, typeData in ipairs(data.achievement_types) do
if typeData.id == achType then
local tier = typeData.tier or 999
if tier < highestTier then
highestTier = tier
topAch = typeData
end
end
end
end
if topAch then
return string.format(
'<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
topAch.id,
htmlEncode(topAch.name or topAch.id or "")
)
end
return ''
end
--------------------------------------------------------------------------------
-- Simple pass-through to track pages (for future expansions)
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
return true
end
--------------------------------------------------------------------------------
-- Retrieve a specific achievement if present, by type
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
"' in page ID: " .. tostring(pageId))
if not pageId or not achievementType or pageId == '' then
debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
return nil
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
return nil
end
local key = tostring(pageId)
local userAchievements = data.user_achievements[key] or {}
if #userAchievements == 0 and key:match("^%d+$") then
userAchievements = data.user_achievements["n" .. key] or {}
end
for _, achievement in ipairs(userAchievements) do
if achievement.type == achievementType then
return achievement
end
end
-- Removed the forced injection code for test pages to avoid dev-role overrides
debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
return nil
end
return Achievements