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: Difference between revisions

From Teriock
Content deleted Content added
// via Wikitext Extension for VSCode
 
// via Wikitext Extension for VSCode
Line 1: Line 1:
-- Module:Dice
-- Processes dice input strings and outputs HTML spans with roll links, templates, and data attributes

local p = {}
local p = {}
local html = mw.html


-- Trim whitespace from both ends
-- Helper function to trim whitespace
local function trim(s)
local function trim(s)
return s:match("^%s*(.-)%s*$")
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
end


-- Split the full roll string into parts and operators (+, -)
-- Helper function to escape special characters for URLs
local function urlEncode(str)
local function splitParts(s)
local parts = {}
return str:gsub("([^%w%-%.%_%~])", function(c)
local i = 1
return string.format("%%%02X", string.byte(c))
end)
while i <= #s do
local c = s:sub(i,i)
end
if c == "+" or c == "-" then

table.insert(parts, c)
-- Parse a single die expression and extract components
i = i + 1
local function parseDieExpression(expr)
local result = {
elseif c == " " then
original = expr,
i = i + 1
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
else
local j = s:find("[%+%-]", i)
-- Handle standalone modifiers like @p[memory]
if expr:match("^@p") then
if j then
result.hasProf = true
table.insert(parts, trim(s:sub(i, j-1)))
result.isStandaloneProf = true
i = j
end
else
table.insert(parts, trim(s:sub(i)))
break
end
end
end
end
return result
return parts
end
end


-- Capitalize first letter
-- Generate quick roll version (simplified for dice.run)
local function generateQuickRoll(diceExpr)
local function capitalize(s)
return (s:gsub("^%l", string.upper))
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
end


-- Parse an individual part into prefix, dice, and qualifiers
-- Generate formatted output based on type
local function generateFormattedOutput(diceExpr, diceType)
local function parsePart(part)
local s = trim(part)
if not diceType or diceType == "" or diceType == "none" then
local prefixCount, prefixFlag
-- Simple format for no type
local diceCount, diceFaces
return "[https://dice.run/#/d/" .. urlEncode(generateQuickRoll(diceExpr)) .. "]"
local qualifiers = {}

-- Extract qualifiers in [brackets]
local qualStr = s:match("^(.-)%[([^%]]+)%]$")
if qualStr then
s = trim(s:match("^(.-)%["))
for q in qualStr:gmatch("([^ ]+)") do
table.insert(qualifiers, q)
end
end
end

local parts = {}
-- Extract parenthesized prefix, e.g. (2@p) or (@f)
local segments = {}
local current = ""
local pref = s:match("^%(([%d]*@%a+)%)")
if pref then
local i = 1
local num, flag = pref:match("^(%d*)@(%a+)")
prefixCount = tonumber(num) or 1
-- Split expression into segments
while i <= #diceExpr do
prefixFlag = flag
local char = diceExpr:sub(i, i)
s = s:gsub("^%b()", "", 1)
else
if char == "+" or char == "-" then
-- Pure prefix without dice
if current ~= "" then
local pure = s:match("^@(%a+)$")
table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)})
if pure then
current = ""
end
prefixCount = 1
prefixFlag = pure
if char == "+" and #segments > 0 then
s = ""
table.insert(segments, {op = " + ", expr = ""})
elseif char == "-" then
table.insert(segments, {op = " - ", expr = ""})
end
else
current = current .. char
end
i = i + 1
end
end
end
if current ~= "" then

table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)})
-- Extract dice, e.g. 2d6 or d8
end
if s:match("^d%d+") then
diceCount = 1
for _, segment in ipairs(segments) do
diceFaces = s:match("^d(%d+)")
if segment.expr ~= "" then
elseif s:match("^%d+d%d+") then
local parsed = parseDieExpression(segment.expr)
local cnt, faces = s:match("^(%d+)d(%d+)")
local partText = ""
diceCount = tonumber(cnt)
diceFaces = faces
if parsed.count ~= "" and parsed.sides ~= "" then
end
local quickDie = parsed.count .. "d" .. parsed.sides

local displayDie = (parsed.count == "1" and "d" or parsed.count) .. parsed.sides
return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers
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
end


-- Main function to process dice input
-- Main entry for #invoke:Dice|d|<roll>|<type>
function p.d(frame)
function p.d(frame)
local diceInput = frame.args[1] or ""
local fullRoll = frame.args[1] or ""
local diceType = frame.args[2] or "none"
local typ = frame.args[2] or "none"
local parts = splitParts(fullRoll)
local quickParts = {}
if diceInput == "" then
local content = {}
return ""

for _, part in ipairs(parts) do
if part == "+" or part == "-" then
table.insert(quickParts, part)
table.insert(content, " " .. part .. " ")
else
local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = parsePart(part)
-- Build quick-roll (ignore flags)
if diceCount then
table.insert(quickParts, diceCount .. "d" .. diceFaces)
end

-- Add prefix template when type is not 'none'
if prefixFlag and typ ~= "none" and diceCount then
local tpl = "{{" .. string.upper(prefixFlag)
if prefixCount and prefixCount ~= 1 then
tpl = tpl .. "|" .. prefixCount
end
tpl = tpl .. "}}"
table.insert(content, tpl)
end

-- Build dice link
if diceCount then
local displayDice = (diceCount == 1) and ("d" .. diceFaces) or (diceCount .. "d" .. diceFaces)
local linkLabel = displayDice
-- Embed prefix in label when no type and no qualifiers
if prefixFlag and typ == "none" and #qualifiers == 0 then
linkLabel = (prefixCount or 1) .. string.upper(prefixFlag) .. displayDice
end
local link = "[https://dice.run/#/d/" .. diceCount .. "d" .. diceFaces .. " " .. linkLabel .. "]"
table.insert(content, link)
elseif prefixFlag and not diceCount and typ ~= "none" then
-- Pure prefix only for non-none type
table.insert(content, "{{" .. string.upper(prefixFlag) .. "}}")
end

-- Add qualifier labels
if qualifiers and #qualifiers > 0 then
for _, q in ipairs(qualifiers) do
table.insert(content, "{{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}")
end
end
end
end
end

local quickRoll = generateQuickRoll(diceInput)
-- Assemble quick-roll and content
local formattedOutput = generateFormattedOutput(diceInput, diceType)
local quickRoll = table.concat(quickParts, " ")
local inner = table.concat(content)
-- Handle special display cases

local displayText = formattedOutput
-- Build final span
if diceInput:match("%(2@p%)d4") and not (diceType and diceType ~= "" and diceType ~= "none") then
local span = html.create('span')
displayText = "[https://dice.run/#/d/" .. urlEncode(quickRoll) .. " 2Pd4]"
:addClass('dice')
end
:attr('data-full-roll', fullRoll)
:attr('data-quick-roll', quickRoll)
return '<span class="dice" data-full-roll="' .. diceInput .. '" data-quick-roll="' .. quickRoll .. '" data-type="' .. diceType .. '">' .. displayText .. '</span>'
:attr('data-type', typ)
:wikitext(inner)

return span:allDone()
end
end



Revision as of 23:15, 19 June 2025

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

-- Module:Dice
-- Processes dice input strings and outputs HTML spans with roll links, templates, and data attributes

local p = {}
local html = mw.html

-- Trim whitespace from both ends
local function trim(s)
  return (s:gsub("^%s*(.-)%s*$", "%1"))
end

-- Split the full roll string into parts and operators (+, -)
local function splitParts(s)
  local parts = {}
  local i = 1
  while i <= #s do
    local c = s:sub(i,i)
    if c == "+" or c == "-" then
      table.insert(parts, c)
      i = i + 1
    elseif c == " " then
      i = i + 1
    else
      local j = s:find("[%+%-]", i)
      if j then
        table.insert(parts, trim(s:sub(i, j-1)))
        i = j
      else
        table.insert(parts, trim(s:sub(i)))
        break
      end
    end
  end
  return parts
end

-- Capitalize first letter
local function capitalize(s)
  return (s:gsub("^%l", string.upper))
end

-- Parse an individual part into prefix, dice, and qualifiers
local function parsePart(part)
  local s = trim(part)
  local prefixCount, prefixFlag
  local diceCount, diceFaces
  local qualifiers = {}

  -- Extract qualifiers in [brackets]
  local qualStr = s:match("^(.-)%[([^%]]+)%]$")
  if qualStr then
    s = trim(s:match("^(.-)%["))
    for q in qualStr:gmatch("([^ ]+)") do
      table.insert(qualifiers, q)
    end
  end

  -- Extract parenthesized prefix, e.g. (2@p) or (@f)
  local pref = s:match("^%(([%d]*@%a+)%)")
  if pref then
    local num, flag = pref:match("^(%d*)@(%a+)")
    prefixCount = tonumber(num) or 1
    prefixFlag = flag
    s = s:gsub("^%b()", "", 1)
  else
    -- Pure prefix without dice
    local pure = s:match("^@(%a+)$")
    if pure then
      prefixCount = 1
      prefixFlag = pure
      s = ""
    end
  end

  -- Extract dice, e.g. 2d6 or d8
  if s:match("^d%d+") then
    diceCount = 1
    diceFaces = s:match("^d(%d+)")
  elseif s:match("^%d+d%d+") then
    local cnt, faces = s:match("^(%d+)d(%d+)")
    diceCount = tonumber(cnt)
    diceFaces = faces
  end

  return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers
end

-- Main entry for #invoke:Dice|d|<roll>|<type>
function p.d(frame)
  local fullRoll = frame.args[1] or ""
  local typ = frame.args[2] or "none"
  local parts = splitParts(fullRoll)
  local quickParts = {}
  local content = {}

  for _, part in ipairs(parts) do
    if part == "+" or part == "-" then
      table.insert(quickParts, part)
      table.insert(content, " " .. part .. " ")
    else
      local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = parsePart(part)
      -- Build quick-roll (ignore flags)
      if diceCount then
        table.insert(quickParts, diceCount .. "d" .. diceFaces)
      end

      -- Add prefix template when type is not 'none'
      if prefixFlag and typ ~= "none" and diceCount then
        local tpl = "{{" .. string.upper(prefixFlag)
        if prefixCount and prefixCount ~= 1 then
          tpl = tpl .. "|" .. prefixCount
        end
        tpl = tpl .. "}}"
        table.insert(content, tpl)
      end

      -- Build dice link
      if diceCount then
        local displayDice = (diceCount == 1) and ("d" .. diceFaces) or (diceCount .. "d" .. diceFaces)
        local linkLabel = displayDice
        -- Embed prefix in label when no type and no qualifiers
        if prefixFlag and typ == "none" and #qualifiers == 0 then
          linkLabel = (prefixCount or 1) .. string.upper(prefixFlag) .. displayDice
        end
        local link = "[https://dice.run/#/d/" .. diceCount .. "d" .. diceFaces .. " " .. linkLabel .. "]"
        table.insert(content, link)
      elseif prefixFlag and not diceCount and typ ~= "none" then
        -- Pure prefix only for non-none type
        table.insert(content, "{{" .. string.upper(prefixFlag) .. "}}")
      end

      -- Add qualifier labels
      if qualifiers and #qualifiers > 0 then
        for _, q in ipairs(qualifiers) do
          table.insert(content, "{{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}")
        end
      end
    end
  end

  -- Assemble quick-roll and content
  local quickRoll = table.concat(quickParts, " ")
  local inner = table.concat(content)

  -- Build final span
  local span = html.create('span')
    :addClass('dice')
    :attr('data-full-roll', fullRoll)
    :attr('data-quick-roll', quickRoll)
    :attr('data-type', typ)
    :wikitext(inner)

  return span:allDone()
end

return p