Module:Damage display
This module renders damage information in a format designed to replicate the in-game view.
Parameters
Parameter | Meaning |
---|---|
format
|
Format of the damage display, either list (default) or inline for a compact inline display.
|
damage n
|
The damage string in the simple format
Expr → Term + Expr | Term Example: |
damage n type
|
The type of the damage which may be any of the damage types in the game or one of the special values: weapon (for damage type that is inherited from the weapon), Physical (for an unspecified physical damage type), or Healing (for healing which is displayed separately from damage).
|
damage n info
|
Free-form field for adding additional information about a damage instance. For example, "per ray " for Scorching Ray damage, "if the target is a Fiend or Undead " for extra Divine Smite damage, or "delayed " for Melf's Acid Arrow damage.
|
damage n modifier
|
The modifier added to the damage. It may be a specific ability score such as |
str |
Strength ability score used for evaluating modifiers |
dex |
Dexterity ability score used for evaluating modifiers |
con |
Constitution ability score used for evaluating modifiers |
int |
Intelligence ability score used for evaluating modifiers |
wis |
Wisdom ability score used for evaluating modifiers |
cha |
Charisma ability score used for evaluating modifiers |
casting ability |
The ability score used for casting. Determines how to evaluate the spell special modifier value.
|
weapon |
Specify the weapon used in order to evaluate generic "Normal weapon damage" values. |
dice size |
Specify the size of the dice images. Setting it to 0 removes them entirely. |
level |
Specify the level which is needed to evaluate "Proficiency bonus" damage modifiers. |
Examples
Example | Markup | Renders as |
---|---|---|
Unspecified ability scores | {{#invoke: Damage display | main | damage 1 = 1d6 + 2 + finesse mod | damage 1 type = Piercing | damage 2 = 1d6 | damage 2 type = Fire }} |
Damage: 4~14 + modifiers![]() ![]() 1d6 + 2 + Strength or Dexterity modifier ![]() + 1d6 ![]() |
Specified ability scores | {{#invoke: Damage display | main | damage 1 = 1d6 + 2 + finesse mod | damage 1 type = Piercing | damage 2 = 1d6 | damage 2 type = Fire | damage 3 = 2d8 | damage 3 type = Radiant | str = 9 | dex = 17 }} | Damage: 9~33 |
Specified casting ability | {{#invoke: Damage display | main | damage 1 = 1d10 + spell + spell | damage 1 type = Force | damage 2 = 1d10 + spell + spell | damage 2 type = Force | damage 3 = 1d10 + spell + spell | damage 3 type = Force | wis = 10 | int = 8 | cha = 17 | casting ability = cha }} | Damage: 21~48 |
Unspecified weapon | {{#invoke: Damage display | main | damage 1 = weapon | damage 2 = 1d6 | damage 2 type = Necrotic }} |
Damage: 1~6![]() Normal weapon damage + 1d6 ![]() |
Specified weapon | {{#invoke: Damage display | main | damage 1 = weapon | damage 2 = 1d6 | damage 2 type = Necrotic | weapon = Spear +1 }} |
Damage: 3~13 + modifiers![]() ![]() 1d6 + 1 + Strength modifier ![]() + 1d6 ![]() |
Specified weapon and abilities | {{#invoke: Damage display | main | damage 1 = weapon | damage 2 = 1d6 | damage 2 type = Necrotic | weapon = Spear +1 | str = 17 | dex = 12 }} | Damage: 6~16 |
Healing | {{#invoke: Damage display | main | damage 1 = 1d6 + wis | damage 1 type = Healing | wis = 19 }} |
Healing: 5~10![]() 1d6 + 4 ![]() |
Proficiency bonus | {{#invoke: Damage display | main | damage 1 = 1d12 + 2 | damage 1 type = Slashing | damage 2 = prof | damage 2 type = Radiant }} |
Damage: 3~14 + modifiers![]() 1d12 + 2 ![]() |
Proficiency bonus w/ level | {{#invoke: Damage display | main | damage 1 = 1d12 + 2 | damage 1 type = Slashing | damage 2 = prof | damage 2 type = Radiant | level = 8 }} | Damage: 6~17 |
Info field | {{#invoke: Damage display | main | damage 1 = 2d4 | damage 1 type = Acid | damage 1 info = Delayed | damage 2 = 2d8 | damage 2 type = Radiant | damage 2 info = If the target is a Fiend or Undead | damage 3 = 2d6 | damage 3 type = Fire | damage 3 info = per ray | damage 4 = 1d6 | damage 4 type = Piercing | damage 4 info = to self }} | Damage: 7~42 |
Freeform damage input | {{#invoke: Damage display | main | damage 1 = (Sorcerer level)/2 | damage 1 type = Lightning | damage 2 = 5 + 2 x (Cleric level) | damage 2 type = Necrotic }} | Damage: 5 + modifiers |
Big dice | {{#invoke: Damage display | main | damage 1 = 1d12 | damage 1 type = Cold | damage 2 = 1d10 | damage 2 type = Lightning | damage 3 = 2d8 | damage 3 type = Psychic | damage 4 = 1d4 | damage 4 type = Force | damage 5 = 2d6 | damage 5 type = Bludgeoning | dice size = 45 }} |
Damage: 7~54![]() ![]() ![]() ![]() ![]() |
No dice | {{#invoke: Damage display | main | damage 1 = 1d12 + 2 | damage 1 type = Slashing | damage 2 = 1d6 | damage 2 type = Poison | dice size = 0 }} | Damage: 4~20 |
Inline output | This format can be used inline: {{#invoke: Damage display | main | format = inline | damage 1 = 1d12 + 2 | damage 1 type = Slashing | damage 2 = 1d6 | damage 2 type = Poison }}. It is simple and compact. |
This format can be used inline: 1d12 + 2 |
local getArgs = require("Module:Arguments").getArgs
local p = {}
-- Text to insert in place of modifiers whose value could not be evaluated
local unevaluated_modifiers = {
melee = "[[Strength#Strength_modifier_chart|Strength modifier]]",
ranged = "[[Dexterity#Dexterity_modifier_chart|Dexterity modifier]]",
finesse = "[[Finesse|Strength or Dexterity modifier]]",
spell = "[[Spells#Spellcasting_ability|Spellcasting modifier]]",
strength = "[[Strength#Strength_modifier_chart|Strength modifier]]",
dexterity = "[[Dexterity#Dexterity_modifier_chart|Dexterity modifier]]",
constitution = "[[Constitution#Constitution_modifier_chart|Constitution modifier]]",
wisdom = "[[Wisdom#Wisdom_modifier_chart|Wisdom modifier]]",
intelligence = "[[Intelligence#Intelligence_modifier_chart|Intelligence modifier]]",
charisma = "[[Charisma#Charisma_modifier_chart|Charisma modifier]]",
proficiency = "[[Proficiency bonus]]"
}
-- Aliases for modifiers since they are not used consistently in every place
local modifier_aliases = {
spellcasting = "spell",
spellcaster = "spell",
casting = "spell",
caster = "spell",
melee = "strength",
ranged = "dexterity",
str = "strength",
dex = "dexterity",
con = "constitution",
wis = "wisdom",
int = "intelligence",
cha = "charisma",
prof = "proficiency",
}
-- Special damage values, mostly a legacy of {{Damage info}}
local special_values = {
["half weapon"] = function(frame)
return "1/2 Normal weapon damage"
end,
["superiority die"] = function(frame)
return frame:expandTemplate{
title = "SmallIcon",
args = { "Superiority Die d8 Icon.png" },
} .. "[[Battlemaster#Superiority_dice|Superiority Die]]"
end,
["unarmed"] = function(frame)
return frame:expandTemplate{
title = "DamageColor",
args = { "Bludgeoning", "Unarmed" }
} .. frame:expandTemplate{
title = "DamageType",
args = { "Bludgeoning" }
}
end,
}
-- Translation and rotation to position each dice image in a way that replicates
-- the in-game damage preview
local dice_image_transform = {
[1] = "translate( 0%, 0%)",
[2] = "translate( 40%, -30%) rotate(20deg)",
[3] = "translate(-35%, -25%) rotate(40deg)",
[4] = "translate( 40%, -70%) rotate(25deg)",
[5] = "translate(-40%, -68%) rotate(40deg)",
}
-- These variables will be populated by the parser function
local parsed_data = {
damage = {
dice = {},
instances = {},
min_roll = 0,
max_roll = 0,
uneval_mods = false,
},
healing = {
dice = {},
instances = {},
min_roll = 0,
max_roll = 0,
uneval_mods = false,
},
}
-- Render the scattered damage dice to replicate how it looks in-game
local function damage_dice_format(frame, damage_dice, width)
local n_dice = #damage_dice
-- Determine width of overall element which is dependent on number of dice
local elem_width = width
if n_dice >= 2 then
elem_width = elem_width * 1.4
end
-- Determine padding which is dependent on number of dice
local left_padding = 0
if n_dice >= 3 then
left_padding = width * 0.4
end
local top_padding = 0
if n_dice >= 2 and n_dice < 4 then
top_padding = width * 0.3
elseif n_dice >= 4 then
top_padding = width * 0.7
end
local element = string.format([[<span style="
display: block;
position: relative;
width: %s;
height: %s;
margin-left: %s;
margin-top: %s;
margin-right: 10px;
">]],
elem_width .. "px",
width .. "px",
left_padding .. "px",
top_padding .. "px"
)
for i, dice in ipairs(damage_dice) do
if i > #dice_image_transform then
break
end
element = element .. string.format(
"<span style=\"z-index: %d; position: absolute; transform: %s\">",
n_dice - i,
dice_image_transform[i]
)
element = element .. string.format(
"[[File:%s %s.png|link= |x%s]]</span>",
dice["value"],
dice["type"],
width .. "px"
)
end
return element .. "</span>"
end
-- Format the damage values in a list format
local function damage_format(frame, args, data, header)
local result = "<b>" .. header .. ": "
-- Damage range preview
if data.min_roll and data.max_roll then
if data.min_roll ~= data.max_roll then
result = result .. data.min_roll .. "~" .. data.max_roll
elseif data.max_roll > 0 then
result = result .. data.max_roll
end
if data.uneval_mods and data.max_roll > 0 then
result = result .. " + modifiers"
end
end
result = result .. "</b>" -- End header
-- Flexbox that holds the dice images on the left and damage values on the right
result = result .. [[<div style="
display: flex;
align-items: center;
width: fit-content;
">]]
-- Left div element containing the damage dice images
local dice_size = tonumber(args["dice size"] or args["dice width"] or "30")
if dice_size > 0 and #data.dice > 0 then
result = result .. damage_dice_format(frame, data.dice, dice_size)
end
-- Right div element containing the damage instance list
result = result .. "<div>"
for i, damage in ipairs(data.instances) do
result = result .. "<div>" -- Begin damage line div
if i > 1 then
result = result .. " + "
end
local value = damage["value"]
if value == "weapon" then
result = result .. "Normal weapon damage"
elseif special_values[value] then
-- Handle some legacy special values
result = result .. special_values[value](frame)
else
result = result .. frame:expandTemplate{
title = "DamageColor",
args = { damage["type"], value }
} .. frame:expandTemplate{
title = "DamageType",
args = { damage["type"] }
}
end
-- Extra info field to add free-form context to the damage instance
if damage["info"] then
result = result .. " (" .. damage["info"] .. ")"
end
result = result .. "</div>" -- End damage line div
end
result = result .. "</div>" -- End right flexbox element
result = result .. "</div>" -- End flexbox
return result
end
-- Format the damage values in a compact inline format with less details
local function damage_format_inline(frame, args, data)
local result = ""
mw.log(data)
for _, kind in pairs(data) do
mw.log(kind)
for i, damage in ipairs(kind.instances) do
local value = damage["value"]
if i > 1 then
result = result .. " + "
end
if value == "weapon" then
result = result .. "Normal weapon damage"
elseif special_values[value] then
-- Handle some legacy special values
result = result .. special_values[value](frame)
else
result = result .. frame:expandTemplate{
title = "DamageColor",
args = { damage["type"], value }
} .. frame:expandTemplate{
title = "DamageType",
args = { damage["type"] }
}
end
end
end
return result
end
-- Parse and try to evaluate a term containing an ability score modifier
-- The first return value is the value of the modifier it can be evaluated
-- The second return value is the unevaluated modifier in a format ready to be rendered
local function parse_modifier(term, args)
-- Some basic parsing to match strings of the form "<ability> mod" or "<ability> modifier" or "<ability>"
local term = string.lower(term)
local words = {}
for word in string.gmatch(term, "[^ ]+") do
table.insert(words, word)
end
if #words >= 3 then
return nil
end
if words[2] and words[2] ~= "mod" and words[2] ~= "modifier" and words[2] ~= "bonus" then
return nil
end
local modifier_name = modifier_aliases[words[1]] or words[1]
-- Function to calculate a given ability score from the provided information
local calc_modifier = function(args, ability)
return function()
local ability_score = args[ability] or args[string.sub(ability, 1 ,3)]
return ability_score and math.floor((ability_score - 10)/2) or nil
end
end
-- Functions to calculate the modifier of the specific type
local modifiers = {
strength = calc_modifier(args, "strength"),
dexterity = calc_modifier(args, "dexterity"),
constitution = calc_modifier(args, "constitution"),
wisdom = calc_modifier(args, "wisdom"),
intelligence = calc_modifier(args, "intelligence"),
charisma = calc_modifier(args, "charisma"),
proficiency = function()
local level = tonumber(args.level)
if level then
return math.floor((level + 7)/4)
end
end,
finesse = function()
local str = calc_modifier(args, "strength")()
local dex = calc_modifier(args, "dexterity")()
if str and dex then
return math.max(str, dex)
end
end,
spell = function()
local casting_ability = args["casting ability"] or modifier_aliases[args["casting ability"]]
if casting_ability then
return calc_modifier(args, casting_ability)()
end
end
}
-- Return the modifier evaluated or the standardized modifier name if it could not be
if modifiers[modifier_name] then
local modifier = modifiers[modifier_name]()
if modifier then
return modifier, nil
else
return nil, unevaluated_modifiers[modifier_name]
end
end
end
-- Parse a damage term
-- The position of the return value indicates the type of term
-- The first return value is a dice term that should be displayed first
-- The second return value is an integer that should be summed with all other integer terms.
-- It should be displayed after the dice.
-- The third return value is an unevaluated modifier. It should be displayed last.
function parse_term(term, damage_type, args, data)
-- Strip whitespace
local term = string.match(term, "^%s*(.-)%s*$")
-- Try to parse as a dice (e.g. 6d8)
local d_idx = string.find(term, "d")
if d_idx then
local count = tonumber(string.sub(term, 0, string.find(term, "d") - 1))
local dice = tonumber(string.sub(term, string.find(term, "d") + 1, -1))
-- Treat missing first number as 1. E.g. "d8" is the same as "1d8"
if d_idx == 1 then
count = 1
end
if count and dice then
-- Track the dice type for displaying the dice icons
table.insert(data.dice, {
["value"] = "d" .. dice,
["count"] = count,
["type"] = damage_type
})
-- Track the low/high values for the overall damage range preview
data.min_roll = data.min_roll + count
data.max_roll = data.max_roll + count*dice
return term, nil, nil
end
end
-- Try to parse as a flat integer
local i = tonumber(term)
if i then
return nil, i, nil
end
-- Try to parse as a special value
local name, value = parse_modifier(term, args)
if name or value then
return nil, name, value
end
-- Catchall for any other term
return nil, nil, term
end
-- Parse a damage instance in the simple format of
-- Expr = Term + Expr | Term
-- Term = Dice | Integer | Modifier
-- Dice = Integer d Integer
-- Modifier = Ability mod | Ability modifier | Ability | prof | proficiency bonus
-- Ability = Strength | str | Dexterity | dex | ...
--
-- Writes result to the global variable parsed_data
local function damage_parse(args, damage_instance)
local damage = damage_instance["value"]
local damage_type = damage_instance["type"]
-- Determine whether this instance is damage or healing
local data = parsed_data.damage
if string.lower(damage_type or "") == "healing" then
data = parsed_data.healing
end
local parsed = ""
local unevaluated_terms = ""
local bonus = 0
for term in string.gmatch(damage, "[^+]+") do
local dice, value, modifier = parse_term(term, damage_type, args, data)
if dice then parsed = parsed .. " + " .. dice end
if value then bonus = bonus + value end
if modifier then unevaluated_terms = unevaluated_terms .. " + " .. modifier end
end
data.min_roll = data.min_roll + bonus
data.max_roll = data.max_roll + bonus
-- Re-add the updated flat bonus to the damage string
if bonus > 0 then
parsed = parsed .. " + " .. bonus
elseif bonus < 0 then
parsed = parsed .. " - " .. -bonus
end
-- Re-add the unevaluated modifiers to the end of the string
parsed = parsed .. unevaluated_terms
if unevaluated_terms ~= "" then
data.uneval_mods = true
end
-- Strip leading " + "
parsed = string.sub(parsed, 4, -1)
table.insert(data.instances, {
["value"] = parsed,
["type"] = damage_type,
["info"] = damage_instance["info"],
})
end
-- Parse the special "weapon" damage value which involves a cargo query into the
-- weapons table for the specific damage values
local function weapon_parse(args)
local weapon_name = args["weapon"]
if not weapon_name then
table.insert(parsed_data["damage"].instances, { ["value"] = "weapon" })
return
end
-- Fields stored in the weapons table. These are liable to change.
local fields = [[
name,
damage,
damage_type,
extra_damage,
extra_damage_type,
extra_damage_2,
extra_damage_2_type,
melee_or_ranged,
finesse
]]
local query = mw.ext.cargo.query('weapons', fields, {
where = "name=\"" .. weapon_name .. "\""
})
if #query > 0 then
weapon = query[1]
-- Apply the weapon modifier to the main weapon damage
-- TODO: Weapons with special modifiers like Sylvan Scimitar do not
-- have their modifier stored in the table correctly.
local modifier = "melee"
if weapon["melee_or_ranged"] == "ranged" then
modifier = "ranged"
elseif weapon["finesse"] == "1" then
modifier = "finesse"
end
weapon["damage"] = weapon["damage"] .. " + " .. modifier
for i, damage_field in ipairs({"damage", "extra_damage", "extra_damage_2"}) do
if weapon[damage_field] then
damage_parse(args, {
["value"] = weapon[damage_field],
["type"] = weapon[damage_field .. "_type"],
})
end
end
else
table.insert(parsed_data["damage"].instances, { ["value"] = "weapon" })
end
end
function p.main(frame)
local args = getArgs(frame)
-- Alias for damage 1. Omitting the 1 is acceptable.
args["damage 1"] = args["damage 1"] or args["damage"]
args["damage 1 type"] = args["damage 1 type"] or args["damage type"]
args["damage 1 info"] = args["damage 1 info"] or args["damage info"]
local i = 1
while args["damage " .. i] do
local damage = args["damage " .. i]
if damage == "weapon" then
weapon_parse(args)
else
-- Handle deprecated "damage modifier" field
if args["damage " .. i .. " modifier"] then
damage = damage .. " + " .. args["damage " .. i .. " modifier"]
end
damage_parse(args, {
["value"] = damage,
["type"] = args["damage " .. i .. " type"],
["info"] = args["damage " .. i .. " info"],
})
end
i = i + 1
end
if args["format"] == "inline" then
return damage_format_inline(frame, args, parsed_data)
else
local result = ""
-- Damage and healing instances are tracked and displayed separately
if #parsed_data.damage.instances > 0 then
result = result .. damage_format(frame, args, parsed_data.damage, "Damage")
end
if #parsed_data.healing.instances > 0 then
result = result .. damage_format(frame, args, parsed_data.healing, "Healing")
end
return result
end
end
return p