Module:Events
From Deepspace Lore
More actions
Documentation for this module may be created at Module:Events/doc
local p = {}
local getArgs = require('Module:Arguments').getArgs
-- ============================================================================
-- CONFIGURATION
-- ============================================================================
local ALL_CHARACTERS = {
"Xavier",
"Zayne",
"Rafayel",
"Sylus",
"Caleb"
}
local SERVER_TIMEZONES = {
asia = 8,
europe = 2,
america = -7
}
local RESET_HOUR = 5
local BUFFER_HOURS = 48
-- ============================================================================
-- UTILITY FUNCTIONS
-- ============================================================================
-- Split a delimited string, trim whitespace from each part
local function splitAndTrim( str, sep )
local result = {}
for part in str:gmatch( "[^" .. sep .. "]+" ) do
local trimmed = part:match( "^%s*(.-)%s*$" )
if trimmed ~= "" then
table.insert( result, trimmed )
end
end
return result
end
-- Resolve characters field: expands "All" to full roster
local function resolveCharacters( raw )
if not raw or raw == "" then return {} end
local trimmed = raw:match( "^%s*(.-)%s*$" )
if trimmed == "All" then return ALL_CHARACTERS end
return splitAndTrim( raw, ";" )
end
function p.formatDate( isoString )
if not isoString or isoString == "" then return "" end
local year = tonumber( isoString:sub( 1, 4 ) )
local month = tonumber( isoString:sub( 6, 7 ) )
local day = tonumber( isoString:sub( 9, 10 ) )
if not year or not month or not day then return "" end
local monthNames = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
}
return monthNames[month] .. " " .. day .. ", " .. year
end
function p.ordinal( n )
n = tonumber( n )
if n % 100 >= 11 and n % 100 <= 13 then return n .. "th" end
local suffixes = { [1] = "st", [2] = "nd", [3] = "rd" }
return n .. ( suffixes[n % 10] or "th" )
end
function p.formatDateOrdinal( isoString )
if not isoString or isoString == "" then return "" end
local year = tonumber( isoString:sub( 1, 4 ) )
local month = tonumber( isoString:sub( 6, 7 ) )
local day = tonumber( isoString:sub( 9, 10 ) )
if not year or not month or not day then return "" end
local monthNames = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
}
return monthNames[month] .. " " .. p.ordinal( day ) .. ", " .. year
end
-- ============================================================================
-- TIMESTAMP PARSING
-- ============================================================================
function p.parseTimestamp( isoString )
if not isoString or isoString == "" then return nil end
local year, month, day, hour, min, sec
if #isoString == 10 then
year = tonumber( isoString:sub( 1, 4 ) )
month = tonumber( isoString:sub( 6, 7 ) )
day = tonumber( isoString:sub( 9, 10 ) )
hour, min, sec = RESET_HOUR, 0, 0
else
year = tonumber( isoString:sub( 1, 4 ) )
month = tonumber( isoString:sub( 6, 7 ) )
day = tonumber( isoString:sub( 9, 10 ) )
hour = tonumber( isoString:sub( 12, 13 ) ) or 0
min = tonumber( isoString:sub( 15, 16 ) ) or 0
sec = tonumber( isoString:sub( 18, 19 ) ) or 0
end
if not year or not month or not day then return nil end
local baseTime = os.time( { year = year, month = month, day = day,
hour = hour, min = min, sec = sec } )
return {
asia = baseTime - ( SERVER_TIMEZONES.asia * 3600 ),
europe = baseTime - ( SERVER_TIMEZONES.europe * 3600 ),
america = baseTime - ( SERVER_TIMEZONES.america * 3600 )
}
end
-- ============================================================================
-- FILTERING & SORTING
-- ============================================================================
function p.filterCurrentEvents( events )
local now = os.time()
local buffer = BUFFER_HOURS * 3600
local valid = {}
for _, event in ipairs( events ) do
local endDate = event["end"]
if endDate and endDate ~= "" then
local year = tonumber( endDate:sub( 1, 4 ) )
local month = tonumber( endDate:sub( 6, 7 ) )
local day = tonumber( endDate:sub( 9, 10 ) )
if year and month and day then
local endTime = os.time( { year = year, month = month, day = day,
hour = 23, min = 59, sec = 59 } ) + buffer
if endTime > now then
table.insert( valid, event )
end
else
table.insert( valid, event )
end
else
table.insert( valid, event )
end
end
return { asia = valid, europe = valid, america = valid }
end
function p.sortEventsByStart( events, server, descending )
table.sort( events, function( a, b )
local aT = p.parseTimestamp( a.start )
local bT = p.parseTimestamp( b.start )
if not aT then return false end
if not bT then return true end
local aS = aT[server]
local bS = bT[server]
if not aS then return false end
if not bS then return true end
return descending and ( aS > bS ) or ( aS < bS )
end )
return events
end
-- ============================================================================
-- BUCKET DATA ACCESS
-- ============================================================================
local function queryEvents( filters )
local q = bucket("events")
.select( "name", "label", "type", "subtype", "image",
"start", "end", "characters", "reward_memory", "description" )
.orderBy( "start", "desc" )
if filters then
for _, condition in ipairs( filters ) do
q = q.where( condition )
end
end
return q.run()
end
-- ============================================================================
-- BUCKET WRITE
-- ============================================================================
function p.store( frame )
local args = frame.args
local characters = resolveCharacters( args.characters )
bucket("events").put({
name = args.name or "",
label = args.label or "",
type = args.type or "",
subtype = args.subtype or "",
image = args.image or "",
start = args.start or "",
["end"] = args["end"] or "",
characters = characters,
reward_memory = args.reward_memory or "",
description = args.description or ""
})
return ""
end
-- ============================================================================
-- CARD RENDERING
-- ============================================================================
function p.renderEventCard( event, servers )
local name = event.name or ""
local eventType = event.type or "story"
local image = event.image or ( name .. "_Banner.png" )
local label = event.label or name
local startTime = event.start or ""
local endTime = event["end"] or ""
local description = event.description or ""
local card = '<div class="event-card-container event-item" data-type="' .. eventType
.. '" data-servers="' .. ( servers or "asia,europe,america" ) .. '">\n'
card = card .. '<div class="event-card-header" style="z-index: 3;">\n'
card = card .. '<div class="event-duration"><b><span class="js-start-date"></span>'
.. ' - <span class="js-end-date"></span></b>\n'
card = card .. '</div><span class="event-countdown" data-start="' .. startTime
.. '" data-end="' .. endTime .. '"></span>\n'
card = card .. '</div>\n'
card = card .. '<div class="event-card-body">\n'
card = card .. '<div class="js-image-container" style="display:none;">[[File:'
.. image .. '|link=]]</div>\n'
card = card .. '<div style="z-index: 3;">\n'
card = card .. '<div class="event-title">[[' .. name .. '|' .. label .. ']]\n'
card = card .. '</div>\n'
card = card .. '<div class="event-description"><small>' .. description .. '</small>\n'
card = card .. '</div>\n'
card = card .. '</div>\n'
card = card .. '</div>\n'
card = card .. '</div>\n'
return card
end
-- ============================================================================
-- PUBLIC RENDER FUNCTIONS
-- ============================================================================
function p.renderCurrentEvents( frame )
local results = queryEvents()
if not results or #results == 0 then
return '<div class="notice">No events data available.</div>'
end
local filteredByServer = p.filterCurrentEvents( results )
local allEvents = {}
local eventServers = {}
for server, serverEvents in pairs( filteredByServer ) do
for _, event in ipairs( serverEvents ) do
local key = event.name
if not eventServers[key] then
eventServers[key] = {}
table.insert( allEvents, event )
end
table.insert( eventServers[key], server )
end
end
if #allEvents == 0 then
return '<div class="notice">No current or upcoming events.</div>'
end
table.sort( allEvents, function( a, b )
local aT = p.parseTimestamp( a["end"] )
local bT = p.parseTimestamp( b["end"] )
if not aT then return false end
if not bT then return true end
local aMin = math.min( aT.asia or math.huge, aT.europe or math.huge, aT.america or math.huge )
local bMin = math.min( bT.asia or math.huge, bT.europe or math.huge, bT.america or math.huge )
return aMin < bMin
end )
local output = '<div class="event-gallery-wrapper">\n<div class="event-gallery" data-server-filter="true">\n'
for _, event in ipairs( allEvents ) do
local servers = table.concat( eventServers[event.name], "," )
output = output .. p.renderEventCard( event, servers )
end
output = output .. '</div>\n</div>'
return output
end
function p.getEventsByType( frame )
local args = getArgs( frame )
local filterType = args[1] or args.type
if not filterType then
return '<div class="error">No event type provided</div>'
end
filterType = mw.text.trim( filterType )
local results = queryEvents( { { "type", filterType } } )
if not results or #results == 0 then
return '<div class="notice">No current ' .. mw.text.encode( filterType ) .. ' events found.</div>'
end
local filteredByServer = p.filterCurrentEvents( results )
local allEvents = {}
local eventServers = {}
for server, serverEvents in pairs( filteredByServer ) do
for _, event in ipairs( serverEvents ) do
local key = event.name
if not eventServers[key] then
eventServers[key] = {}
table.insert( allEvents, event )
end
table.insert( eventServers[key], server )
end
end
if #allEvents == 0 then
return '<div class="notice">No current ' .. mw.text.encode( filterType ) .. ' events found.</div>'
end
local output = '<div class="event-gallery-wrapper">\n<div class="event-gallery" data-server-filter="true">\n'
for _, event in ipairs( allEvents ) do
local servers = table.concat( eventServers[event.name], "," )
output = output .. p.renderEventCard( event, servers )
end
output = output .. '</div>\n</div>'
return output
end
function p.render10Days( frame )
local results = queryEvents( { { "name", "10 Days With You" }, { "type", "checkin" } } )
if not results or #results == 0 then
return '<div class="notice">No 10 Days With You events found.</div>'
end
p.sortEventsByStart( results, "america", false )
local output = '{| class="wikitable tendays" style="margin:auto"\n'
output = output .. '|-\n! Title !! Memory !! Duration\n'
for _, event in ipairs( results ) do
local banner = event.image or ( event.name .. "_Banner.png" )
local startStr = p.formatDateOrdinal( event.start )
local endStr = p.formatDateOrdinal( event["end"] )
local duration = startStr .. " - " .. endStr
local memory = event.reward_memory or ""
local memoryCell = ""
if memory ~= "" then
memoryCell = frame:expandTemplate{ title = "Memorybox", args = { memory = memory } }
else
memoryCell = "''No memory listed''"
end
local titleCell = "[[File:" .. banner .. "|300px]]"
if memory ~= "" then
titleCell = titleCell .. "<br>[[" .. memory .. "]]"
end
output = output .. "|-\n"
output = output .. "| style=\"width: 40%\" | " .. titleCell .. "\n"
output = output .. "| style=\"width: 25%\" | " .. memoryCell .. "\n"
output = output .. "| style=\"width: 35%\" | " .. duration .. "\n"
end
output = output .. "|}"
return output
end
function p.renderHeartfeltGift( frame )
local results = queryEvents( { { "name", "Heartfelt Gift" }, { "type", "checkin" } } )
if not results or #results == 0 then
return '<div class="notice">No Heartfelt Gift events found.</div>'
end
p.sortEventsByStart( results, "america", false )
local output = '{| class="wikitable tendays" style="margin:auto"\n'
output = output .. '|-\n! Title !! Memory !! Duration\n'
for _, event in ipairs( results ) do
local banner = event.image or ( event.name .. "_Banner.png" )
local startStr = p.formatDateOrdinal( event.start )
local endStr = p.formatDateOrdinal( event["end"] )
local duration = startStr .. " - " .. endStr
local memory = event.reward_memory or ""
local memoryCell = ""
if memory ~= "" then
memoryCell = frame:expandTemplate{ title = "Memorybox", args = { memory = memory } }
else
memoryCell = "''No memory listed''"
end
local titleCell = "[[File:" .. banner .. "|300px]]"
if memory ~= "" then
titleCell = titleCell .. "<br>[[" .. memory .. "]]"
end
output = output .. "|-\n"
output = output .. "| style=\"width: 40%\" | " .. titleCell .. "\n"
output = output .. "| style=\"width: 25%\" | " .. memoryCell .. "\n"
output = output .. "| style=\"width: 35%\" | " .. duration .. "\n"
end
output = output .. "|}"
return output
end
function p.list( frame )
local results = queryEvents()
if not results or #results == 0 then
return "No events found."
end
local out = {}
table.insert( out, '{| class="wikitable sortable"' )
table.insert( out, '! Name !! Type !! Subtype !! Start !! End !! Description' )
for _, row in ipairs( results ) do
local label = ( row.label and row.label ~= "" ) and row.label or row.name
local name = ( row.name and row.name ~= "" ) and row.name or ""
local link
if name ~= "" then
link = ( label ~= name ) and "[[" .. name .. "|" .. label .. "]]"
or "[[" .. name .. "]]"
else
link = label
end
table.insert( out, "|-" )
table.insert( out, string.format(
"| %s || %s || %s || %s || %s || %s",
link,
row.type or "",
row.subtype or "",
row.start or "",
row["end"] or "",
row.description or ""
))
end
table.insert( out, "|}" )
return table.concat( out, "\n" )
end
return p