Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
Revision as of 01:37, 25 April 2026 by WinterLampost (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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