Module:Damage display
Jump to navigation
Jump to search

This module renders damage information in a format designed to replicate the in-game view.
Parameters
Parameter | Meaning |
---|---|
damage n
|
The damage string in the simple format
Expr → Term + Expr | Term For example, "2d8 + 1d6 + 4". The "prof" term is a special value for Proficiency bonus that is occasionally used. |
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 modifier
|
The modifier added to the damage. It may be a specific ability score such as Strength or Charisma or it may be a special value such as melee , ranged , finesse , or spell .
|
damage n save
|
The saving throw used to avoid or reduce this damage if applicable. |
damage n save dc
|
The DC of the saving throw for this damage instance. |
damage n save effect
|
The effect of a successful saving throw on this damage. Values can be negate or halve .
|
damage n per
|
Specify if the damage is dealt repeatedly for example per turn or per distance moved. |
damage n conditional
|
Specify that this damage requires some special conditions to apply. |
damage n delayed
|
Specify that this damage is delayed rather than applying immediately. |
damage n self
|
Specify if this damage is dealt to the user rather than the target. |
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 | damage 1 type = Piercing | damage 1 modifier = finesse | damage 2 = 1d6 | damage 2 type = Fire }} |
Damage: 4~14![]() ![]() 1d6 + 2 + Strength or Dexterity modifierPiercing + 1d6Fire |
Specified ability scores | {{#invoke: Damage display | main | damage 1 = 1d6 + 2 | damage 1 type = Piercing | damage 1 modifier = finesse | 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 | damage 1 type = Force | damage 1 modifier = spell | damage 2 = 1d10 | damage 2 type = Force | damage 2 modifier = spell | damage 3 = 1d10 | damage 3 type = Force | damage 3 modifier = spell | wis = 10 | int = 8 | cha = 17 | casting ability = cha }} | Damage: 12~39 |
Unspecified weapon | {{#invoke: Damage display | main | damage 1 = weapon | damage 2 = 1d6 | damage 2 type = Necrotic }} |
Damage: 1~6![]() Normal weapon damage + 1d6Necrotic |
Specified weapon | {{#invoke: Damage display | main | damage 1 = weapon | damage 2 = 1d6 | damage 2 type = Necrotic | weapon = Spear +1 }} |
Damage: 3~13![]() ![]() 1d6 + 1 + Strength or Dexterity modifierPiercing + 1d6Necrotic |
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 |
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 |
Proficiency bonus | {{#invoke: Damage display | main | damage 1 = 1d12 + 2 | damage 1 type = Slashing | damage 2 = prof | damage 2 type = Radiant }} |
Damage: 3~14![]() 1d12 + 2Slashing |
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 |
Saving throw | {{#invoke: Damage display | main | damage 1 = 8d6 | damage 1 type = Fire | damage 1 save = dex | damage 1 save dc = 16 | damage 1 save effect = halve }} |
Damage: 8~48![]() |
Miscellaneous properties | {{#invoke: Damage display | main | damage 1 = 1d6 | damage 1 type = Fire | damage 1 per = turn | damage 2 = 2d4 | damage 2 type = Acid | damage 2 delayed = yes | damage 3 = 1d6 | damage 3 type = Piercing | damage 3 self = yes | damage 4 = 1d10 | damage 4 type = Poison | damage 4 conditional = yes }} | Damage: 5~30 |
Healing | {{#invoke: Damage display | main | damage 1 = 1d6 | damage 1 type = Healing | damage 1 modifier = Wisdom | wis = 19 }} |
Healing: 5~10![]() 1d6 + 4Healing |
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]]",
}
-- Aliases for modifiers since they are not used consistently in every place
local modifier_aliases = {
spellcasting = "spell",
spellcaster = "spell",
casting = "spell",
caster = "spell",
str = "strength",
dex = "dexterity",
con = "constitution",
wis = "wisdom",
int = "intelligence",
cha = "charisma",
}
-- 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,
},
["healing"] = {
["dice"] = {},
["instances"] = {},
["min_roll"] = 0,
["max_roll"] = 0,
},
}
-- Given a modifier type such as "melee" or "caster", evaluate the specific
-- value based on provided ability scores
local function evaluate_modifier(modifier_type, args)
if not modifier_type then
return nil
end
local modifiers = {}
local evaluated_modifier = nil
for _, ability in ipairs({"str", "dex", "con", "wis", "int", "cha"}) do
modifiers[ability] = args[ability] and math.floor((args[ability] - 10)/2) or nil
end
-- Handle melee, ranged, and finesse special modifier values
if modifier_type == "melee" and modifiers["str"] then
evaluated_modifier = modifiers["str"]
elseif modifier_type == "ranged" and modifiers["dex"] then
evaluated_modifier = modifiers["dex"]
elseif modifier_type == "finesse" and modifiers["dex"] and modifiers["str"] then
if modifiers["dex"] > modifiers["str"] then
evaluated_modifier = modifiers["dex"]
else
evaluated_modifier = modifiers["str"]
end
end
-- Handle spell special modifier value
if modifier_type == "spell" and args["casting ability"] then
evaluated_modifier = modifiers[args["casting ability"]]
end
-- Handle prof bonus value
if modifier_type == "prof" and args["level"] then
evaluated_modifier = math.floor((tonumber(args["level"])+7)/4)
end
-- Handle case where modifier is a specific ability score
local modifier_abbr = string.sub(string.lower(modifier_type), 1, 3)
if modifiers[modifier_abbr] then
evaluated_modifier = modifiers[modifier_abbr]
end
return evaluated_modifier
end
-- 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
local function damage_format(frame, args, data, header)
local result = ""
-- Damage range preview
result = result .. "<b>" .. header .. ": " .. data.min_roll .. "~" .. data.max_roll .. "</b>"
-- 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
local value = damage["value"]
if value == "weapon" then
result = result .. "Normal weapon damage"
else
if damage["modifier"] and damage["modifier"] ~= "" then
-- Damage instance has an unevaluated modifier
local modifier = string.lower(damage["modifier"])
if unevaluated_modifiers[modifier] then
value = value .. " + " .. unevaluated_modifiers[modifier]
else
-- Copy the modifier field verbatim as a fallback
value = value .. " + " .. damage["modifier"]
end
end
if i > 1 then
result = result .. " + "
end
result = result .. frame:expandTemplate{
title = "DamageColor",
args = { damage["type"], value }
} .. frame:expandTemplate{
title = "DamageType",
args = { damage["type"] }
}
end
-- "Per" field is for effects that deal damage per turn, per meters moved, etc.
if damage["per"] then
result = result .. " per " .. damage["per"]
end
-- "Delayed" field is for damage that doesn't apply immediately such as Melf's Acid Arrow
if damage["delayed"] then
result = result .. " (delayed)"
end
-- "Conditional" field is for damage that requires specific conditions to apply
if damage["conditional"] then
result = result .. " (conditional)"
end
-- "Self" field is for when a damage instance is applied to the user rather than the target
if damage["self"] then
result = result .. " to self"
end
-- DC and save properties to append to the end of the damage line
if damage["save"] then
result = result .. " ("
local save_effect = "negate"
if damage["save effect"] == "half" or damage["save effect"] == "halve" then
save_effect = "halve"
end
result = result .. frame:expandTemplate{
title = "Saving throw",
args = {
damage["save"],
["dc"] = damage["save dc"],
}
}
result = result .. " to " .. save_effect .. ")"
end
result = result .. "</div>" -- End damage line div
end
result = result .. "</div>" -- End right flexbox element
result = result .. "</div>" -- End flexbox
return result
end
-- Parse a damage instance
-- Writes result to the global variable parsed_data
local function damage_parse(args, damage_instance)
local damage = damage_instance["value"]
local damage_modifier = string.lower(damage_instance["modifier"] or "")
local damage_type = damage_instance["type"]
-- Determine whether this instance is damage or healing
local kind = "damage"
if string.lower(damage_type) == "healing" then
kind = "healing"
end
-- Handle modifiers which do not strictly follow the expected values
if modifier_aliases[damage_modifier] then
damage_modifier = modifier_aliases[damage_modifier]
end
-- Evaluate the modifier if possible
local evaluated_modifier = evaluate_modifier(damage_modifier, args)
if evaluated_modifier or not damage_modifier then
damage_modifier = ""
end
local evaluated_damage = ""
local flat_bonus = 0
-- This will track terms that cannot be evaluated because of a lack of information
local unevaluated_terms = ""
-- Parse the damage string which (for now) assumes a very simple format of
-- Expr = Term + Expr | Term
-- Term = Dice | Integer | prof
-- Dice = Integer d Integer
for term in string.gmatch(damage, "[^+%s]+") do
if term == "prof" then
-- Special proficiency bonus value
local evaluated_prof = evaluate_modifier("prof", args)
if evaluated_prof then
flat_bonus = flat_bonus + evaluated_prof
else
unevaluated_terms = unevaluated_terms .. " + [[Proficiency bonus]]"
end
elseif string.find(term, "d") then
-- Dice term
local count = string.sub(term, 0, string.find(term, "d") - 1)
local dice = string.sub(term, string.find(term, "d") + 1, -1)
-- Track damage dice for when we render the dice image
table.insert(parsed_data[kind].dice, {
["value"] = "d" .. dice,
["count"] = count,
["type"] = damage_type
})
evaluated_damage = evaluated_damage .. " + " .. term
-- Track the low/high values for the overall damage range preview
parsed_data[kind].min_roll = parsed_data[kind].min_roll + count
parsed_data[kind].max_roll = parsed_data[kind].max_roll + count*dice
else
-- Constant term
flat_bonus = flat_bonus + tonumber(term)
end
end
-- If enough information is provided to assign a numerical value to the
-- modifier, combine it with other flat bonuses
if evaluated_modifier then
flat_bonus = flat_bonus + evaluated_modifier
end
parsed_data[kind].min_roll = parsed_data[kind].min_roll + flat_bonus
parsed_data[kind].max_roll = parsed_data[kind].max_roll + flat_bonus
-- Re-add the updated flat bonus to the damage string
if flat_bonus > 0 then
evaluated_damage = evaluated_damage .. " + " .. flat_bonus
elseif flat_bonus < 0 then
evaluated_damage = evaluated_damage .. " - " .. -flat_bonus
end
-- Re-add any special terms that could not be evaluated
if unevaluated_terms ~= "" then
evaluated_damage = evaluated_damage .. unevaluated_terms
end
-- Strip leading " + "
evaluated_damage = string.sub(evaluated_damage, 4, -1)
table.insert(parsed_data[kind].instances, {
["value"] = evaluated_damage,
["type"] = damage_type,
["modifier"] = damage_modifier,
["save"] = damage_instance["save"],
["save dc"] = damage_instance["save dc"],
["save effect"] = damage_instance["save effect"],
["per"] = damage_instance["per"],
["delayed"] = damage_instance["delayed"],
["conditional"] = damage_instance["conditional"],
["self"] = damage_instance["self"],
})
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]
for i, damage_field in ipairs({"damage", "extra_damage", "extra_damage_2"}) do
-- 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"
end
if weapon["finesse"] then
modifier = "finesse"
end
-- Modifier calculated this way only applies to main damage instance
if i > 1 then
modifier = nil
end
if weapon[damage_field] then
damage_parse(args, {
["value"] = weapon[damage_field],
["type"] = weapon[damage_field .. "_type"],
["modifier"] = modifier
})
end
end
else
table.insert(parsed_data["damage"].instances, { ["value"] = "weapon" })
end
end
function p.main(frame)
local args = getArgs(frame)
local i = 1
while args["damage " .. i] do
local damage = args["damage " .. i]
if damage == "weapon" then
weapon_parse(args)
else
damage_parse(args, {
["value"] = damage,
["type"] = args["damage " .. i .. " type"],
["modifier"] = args["damage " .. i .. " modifier"],
["save"] = args["damage " .. i .. " save"],
["save dc"] = args["damage " .. i .. " save dc"],
["save effect"] = args["damage " .. i .. " save effect"],
["per"] = args["damage " .. i .. " per"],
["delayed"] = args["damage " .. i .. " delayed"],
["conditional"] = args["damage " .. i .. " conditional"],
["self"] = args["damage " .. i .. " self"],
})
end
i = i + 1
end
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
return p