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

Module:Dice

From Teriock
Revision as of 23:11, 19 June 2025 by Gpe (talk | contribs) (// via Wikitext Extension for VSCode)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:Dice/doc

local p = {}

-- Helper function to trim whitespace
local function trim(s)
    return s:match("^%s*(.-)%s*$")
end

-- Helper function to escape special characters for URLs
local function urlEncode(str)
    return str:gsub("([^%w%-%.%_%~])", function(c)
        return string.format("%%%02X", string.byte(c))
    end)
end

-- Parse a single die expression and extract components
local function parseDieExpression(expr)
    local result = {
        original = expr,
        count = "",
        sides = "",
        modifiers = "",
        damageTypes = {},
        hasEdge = false,
        hasProf = false
    }
    
    -- Remove whitespace
    expr = trim(expr)
    
    -- Extract damage types in brackets
    local damageTypePattern = "%[([^%]]+)%]"
    for damageType in expr:gmatch(damageTypePattern) do
        table.insert(result.damageTypes, trim(damageType))
    end
    expr = expr:gsub(damageTypePattern, "")
    
    -- Check for edge (@f) and proficiency (@p) modifiers
    if expr:match("@f") then
        result.hasEdge = true
        expr = expr:gsub("%(?@f%)?", "1")
    end
    
    if expr:match("@p") then
        result.hasProf = true
        expr = expr:gsub("%(?(%d*)@p%)?", function(num)
            return num ~= "" and num or "1"
        end)
    end
    
    -- Parse basic die notation (XdY with optional modifiers)
    local count, sides, modifiers = expr:match("^(%d*)d(%d+)(.*)$")
    if count and sides then
        result.count = count ~= "" and count or "1"
        result.sides = sides
        result.modifiers = trim(modifiers or "")
    else
        -- Handle standalone modifiers like @p[memory]
        if expr:match("^@p") then
            result.hasProf = true
            result.isStandaloneProf = true
        end
    end
    
    return result
end

-- Generate quick roll version (simplified for dice.run)
local function generateQuickRoll(diceExpr)
    local parts = {}
    
    -- Split by + and - while preserving operators
    local segments = {}
    local current = ""
    local i = 1
    while i <= #diceExpr do
        local char = diceExpr:sub(i, i)
        if char == "+" or char == "-" then
            if current ~= "" then
                table.insert(segments, {op = (#segments == 0 and "" or "+"), expr = trim(current)})
                current = ""
            end
            table.insert(segments, {op = char, expr = ""})
        else
            current = current .. char
        end
        i = i + 1
    end
    if current ~= "" then
        table.insert(segments, {op = (#segments == 0 and "" or "+"), expr = trim(current)})
    end
    
    for _, segment in ipairs(segments) do
        if segment.expr ~= "" then
            local parsed = parseDieExpression(segment.expr)
            if parsed.count ~= "" and parsed.sides ~= "" then
                local quickDie = parsed.count .. "d" .. parsed.sides
                table.insert(parts, segment.op .. quickDie)
            elseif parsed.isStandaloneProf then
                -- Skip standalone proficiency modifiers in quick roll
            else
                -- Handle numeric constants
                local num = segment.expr:match("^(%d+)")
                if num then
                    table.insert(parts, segment.op .. num)
                end
            end
        end
    end
    
    local result = table.concat(parts, " ")
    return trim(result:gsub("^%+", ""))
end

-- Generate formatted output based on type
local function generateFormattedOutput(diceExpr, diceType)
    if not diceType or diceType == "" or diceType == "none" then
        -- Simple format for no type
        return "[https://dice.run/#/d/" .. urlEncode(generateQuickRoll(diceExpr)) .. "]"
    end
    
    local parts = {}
    local segments = {}
    local current = ""
    local i = 1
    
    -- Split expression into segments
    while i <= #diceExpr do
        local char = diceExpr:sub(i, i)
        if char == "+" or char == "-" then
            if current ~= "" then
                table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)})
                current = ""
            end
            if char == "+" and #segments > 0 then
                table.insert(segments, {op = " + ", expr = ""})
            elseif char == "-" then
                table.insert(segments, {op = " - ", expr = ""})
            end
        else
            current = current .. char
        end
        i = i + 1
    end
    if current ~= "" then
        table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)})
    end
    
    for _, segment in ipairs(segments) do
        if segment.expr ~= "" then
            local parsed = parseDieExpression(segment.expr)
            local partText = ""
            
            if parsed.count ~= "" and parsed.sides ~= "" then
                local quickDie = parsed.count .. "d" .. parsed.sides
                local displayDie = (parsed.count == "1" and "d" or parsed.count) .. parsed.sides
                
                if parsed.hasEdge then
                    partText = "{{F}}[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. displayDie .. "]"
                elseif parsed.hasProf then
                    local profNum = parsed.count ~= "1" and parsed.count or ""
                    partText = "{{P|" .. profNum .. "}}[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. displayDie .. "]"
                else
                    partText = "[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. quickDie .. "]"
                end
                
                -- Add damage type templates
                for _, damageType in ipairs(parsed.damageTypes) do
                    local typeWords = {}
                    for word in damageType:gmatch("%S+") do
                        table.insert(typeWords, word:gsub("^%l", string.upper))
                    end
                    partText = partText .. " {{L|" .. (diceType:gsub("^%l", string.upper)) .. "|" .. table.concat(typeWords, " ") .. "}}"
                end
                
            elseif parsed.isStandaloneProf then
                partText = "{{P}}"
                for _, damageType in ipairs(parsed.damageTypes) do
                    local typeWords = {}
                    for word in damageType:gmatch("%S+") do
                        table.insert(typeWords, word:gsub("^%l", string.upper))
                    end
                    partText = partText .. " {{L|" .. (diceType:gsub("^%l", string.upper)) .. "|" .. table.concat(typeWords, " ") .. "}}"
                end
            else
                -- Handle numeric constants
                local num = segment.expr:match("^(%d+)")
                if num then
                    partText = num
                end
            end
            
            if partText ~= "" then
                table.insert(parts, segment.op .. partText)
            end
        elseif segment.op ~= "" then
            -- Add standalone operators
            table.insert(parts, segment.op)
        end
    end
    
    local result = table.concat(parts, "")
    return trim(result:gsub("^ %+ ", ""))
end

-- Main function to process dice input
function p.d(frame)
    local diceInput = frame.args[1] or ""
    local diceType = frame.args[2] or "none"
    
    if diceInput == "" then
        return ""
    end
    
    local quickRoll = generateQuickRoll(diceInput)
    local formattedOutput = generateFormattedOutput(diceInput, diceType)
    
    -- Handle special display cases
    local displayText = formattedOutput
    if diceInput:match("%(2@p%)d4") and not (diceType and diceType ~= "" and diceType ~= "none") then
        displayText = "[https://dice.run/#/d/" .. urlEncode(quickRoll) .. " 2Pd4]"
    end
    
    return '<span class="dice" data-full-roll="' .. diceInput .. '" data-quick-roll="' .. quickRoll .. '" data-type="' .. diceType .. '">' .. displayText .. '</span>'
end

return p