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