Module:Damage display: Difference between revisions
Jump to navigation
Jump to search
Improved modifier handling logic with support for "spell" special value |
m Added conditional damage property |
||
(38 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local getArgs = require("Module:Arguments").getArgs | local getArgs = require("Module:Arguments").getArgs | ||
local p = {} | 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 | -- Given a modifier type such as "melee" or "caster", evaluate the specific | ||
-- value based on provided ability scores | -- value based on provided ability scores | ||
local function | local function evaluate_modifier(modifier_type, args) | ||
if not modifier_type then | |||
return nil | |||
end | |||
local modifiers = {} | local modifiers = {} | ||
local evaluated_modifier = nil | local evaluated_modifier = nil | ||
Line 28: | Line 85: | ||
if modifier_type == "spell" and args["casting ability"] then | if modifier_type == "spell" and args["casting ability"] then | ||
evaluated_modifier = modifiers[args["casting ability"]] | 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 | end | ||
Line 67: | Line 129: | ||
width: %s; | width: %s; | ||
height: %s; | height: %s; | ||
margin-left: %s; | |||
margin-top: %s; | |||
margin-right: 10px; | margin-right: 10px; | ||
">]], | ">]], | ||
Line 76: | Line 138: | ||
top_padding .. "px" | top_padding .. "px" | ||
) | ) | ||
for i, dice in ipairs(damage_dice) do | for i, dice in ipairs(damage_dice) do | ||
if i > #dice_image_transform then | |||
break | |||
end | |||
element = element .. string.format( | element = element .. string.format( | ||
"<span style=\"z-index: %d; position: absolute; transform: %s\">", | "<span style=\"z-index: %d; position: absolute; transform: %s\">", | ||
n_dice - i, | n_dice - i, | ||
dice_image_transform[i] | |||
) | ) | ||
element = element .. string.format( | element = element .. string.format( | ||
Line 103: | Line 159: | ||
-- Format the damage values | -- Format the damage values | ||
local function damage_format( | local function damage_format(frame, args, data, header) | ||
local result = "" | |||
) | |||
local result = " | |||
-- Damage range preview | -- Damage range preview | ||
result = result .. "<b> | 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 | -- Flexbox that holds the dice images on the left and damage values on the right | ||
Line 132: | Line 172: | ||
-- Left div element containing the damage dice images | -- Left div element containing the damage dice images | ||
result = result .. damage_dice_format(frame, | 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 | -- Right div element containing the damage instance list | ||
result = result .. "<div>" | result = result .. "<div>" | ||
for i, damage in ipairs( | for i, damage in ipairs(data.instances) do | ||
-- | result = result .. "<div>" -- Begin damage line div | ||
local value = damage["value"] | local value = damage["value"] | ||
if | 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 | end | ||
result = result .. " | -- "Delayed" field is for damage that doesn't apply immediately such as Melf's Acid Arrow | ||
if | if damage["delayed"] then | ||
result = result .. " | 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 | end | ||
result = result | result = result .. "</div>" -- End damage line div | ||
end | end | ||
result = result .. "</div>" -- End right flexbox element | result = result .. "</div>" -- End right flexbox element | ||
result = result .. "</div>" -- End flexbox | result = result .. "</div>" -- End flexbox | ||
return result | 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 | end | ||
Line 165: | Line 412: | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
local i = 1 | local i = 1 | ||
while args["damage " .. i] do | while args["damage " .. i] do | ||
local damage = args["damage " .. i] | local damage = args["damage " .. i] | ||
if damage == "weapon" then | |||
if | 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 | end | ||
i = i + 1 | i = i + 1 | ||
end | end | ||
local | 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 | return p |
Latest revision as of 00:39, 29 January 2025

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