Jump to content
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: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, qualifiers, and constant
local function parsePart(part)
  local s = trim(part)
  local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = nil, nil, nil, nil, {}
  local constValue = nil

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

  -- Extract parenthesized prefix, e.g. (2@p), (@f), or (2 * @p)
  local rawPref = s:match("^%(([^)]+)%)")
  if rawPref then
    local num, flag = rawPref:match("^(%d+)%s*%*%s*@(%a+)$")
    if not num then num, flag = rawPref:match("^(%d*)@(%a+)$") end
    prefixCount = tonumber(num) or 1
    prefixFlag = flag
    s = s:gsub("^%b()", "", 1)
  else
    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%d+") then
    local cnt, faces = s:match("^(%d*)d(%d+)")
    diceCount = tonumber(cnt) or 1
    diceFaces = faces
  else
    -- If not dice, check for a bare numeric constant
    local num = s:match("^(%d+)$")
    if num then
      constValue = tonumber(num)
    end
  end

  return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers, constValue
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, content = {}, {}

  for _, part in ipairs(parts) do
    if part == "+" or part == "-" then
      -- operator
      table.insert(quickParts, part)
      table.insert(content, " " .. part .. " ")
    else
      -- parse dice, prefix, or constant
      local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers, constValue = parsePart(part)

      -- If it's truly plain (no dice/prefix/qualifiers), keep old behavior
      if not diceCount and not prefixFlag and #qualifiers == 0 and not constValue then
        table.insert(quickParts, part)
        table.insert(content, part)
      else
        -- Build quick-roll sequence (ignore flags)
        if diceCount then
          table.insert(quickParts, diceCount .. "d" .. diceFaces)
        elseif constValue then
          table.insert(quickParts, tostring(constValue))
        end

        -- Prefix template for typed rolls (only before dice, as before)
        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, frame:preprocess(tpl))
        end

        -- Build dice link or output constant/pure prefix
        if diceCount then
          local displayDice = diceCount .. "d" .. diceFaces
          local linkLabel = displayDice
          if prefixFlag then
            if typ == "none" and #qualifiers == 0 then
              local prefixLabel = string.upper(prefixFlag)
              if prefixCount and prefixCount > 1 then prefixLabel = prefixCount .. prefixLabel end
              linkLabel = prefixLabel .. "d" .. diceFaces
            else
              linkLabel = "d" .. diceFaces
            end
          end
          table.insert(content,
            string.format("[https://dice.run/#/d/%dd%s %s]", diceCount, diceFaces, linkLabel)
          )
        elseif constValue then
          -- numeric constant (e.g., 4[lethal])
          table.insert(content, tostring(constValue))
        elseif prefixFlag then
          -- pure prefix
          local tpl = "{{" .. string.upper(prefixFlag)
          if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end
          tpl = tpl .. "}}"
          table.insert(content, frame:preprocess(tpl))
        else
          -- fallback: unknown token but with qualifiers; keep original token text
          table.insert(content, part)
        end

        -- Qualifier labels
        for _, q in ipairs(qualifiers) do
          table.insert(content, frame:preprocess(" {{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}"))
        end
      end
    end
  end

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

  return html.create('span')
    :addClass('dice')
    :attr('data-full-roll', fullRoll)
    :attr('data-quick-roll', quickRoll)
    :attr('data-type', typ)
    :wikitext(inner)
    :tag("span")
    :addClass("metadata")
    :attr("data-full-roll", fullRoll)
    :attr("data-quick-roll", quickRoll)
    :attr("data-type", "roll")
    :attr("data-roll-type", typ)
    :done()
    :allDone()
end

return p