Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

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