Module:Damage display: Difference between revisions

From bg3.wiki
Jump to navigation Jump to search
mNo edit summary
m Added conditional damage property
 
(20 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
-- These variables will be populated by the parser function
local damage_dice = {}      -- List of damage dice images to render
local parsed_data = {
local damage_instances = {} -- List of damage instances to render
    ["damage"] = {
local min_roll = 0         -- Value to show as the upper bound of damage
        ["dice"]      = {},
local max_roll = 0         -- Value to show as the upper bound of damage
        ["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 evaluate_modifer(modifier_type, args)
local function evaluate_modifier(modifier_type, args)
if not modifier_type then
if not modifier_type then
return nil
return nil
Line 37: 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 76: Line 129:
width:        %s;
width:        %s;
height:      %s;
height:      %s;
padding-left: %s;
margin-left: %s;
padding-top: %s;
margin-top:   %s;
margin-right: 10px;
margin-right: 10px;
">]],
">]],
Line 85: Line 138:
top_padding .. "px"
top_padding .. "px"
)
)
 
-- Translation and rotation to position each dice in a way that replicates
-- the in-game damage preview
local 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)",
}
for i, dice in ipairs(damage_dice) do
for i, dice in ipairs(damage_dice) do
if i > #image_transform then
if i > #dice_image_transform then
break
break
end
end
Line 102: Line 146:
"<span style=\"z-index: %d; position: absolute; transform: %s\">",
"<span style=\"z-index: %d; position: absolute; transform: %s\">",
n_dice - i,
n_dice - i,
image_transform[i]
dice_image_transform[i]
)
)
element = element .. string.format(
element = element .. string.format(
Line 115: Line 159:


-- Format the damage values
-- Format the damage values
local function damage_format(frame, args)
local function damage_format(frame, args, data, header)
-- Text to insert in place of modifiers whose value could not be evaluated
local result = ""
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]]",
}
-- Outermost div container for the entire element
local result = "<div class=\"bg3wiki-info-blob\" style=\"font-family: unset\">"
-- Damage range preview
-- Damage range preview
result = result .. "<b>Damage: " .. min_roll .. "~" .. max_roll .. "</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 145: Line 173:
-- Left div element containing the damage dice images
-- Left div element containing the damage dice images
local dice_size = tonumber(args["dice size"] or args["dice width"] or "30")
local dice_size = tonumber(args["dice size"] or args["dice width"] or "30")
if dice_size > 0 then
if dice_size > 0 and #data.dice  > 0 then
result = result .. damage_dice_format(frame, damage_dice, dice_size)
result = result .. damage_dice_format(frame, data.dice, dice_size)
end
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(damage_instances) do
for i, damage in ipairs(data.instances) do
result = result .. "<div>" -- Begin damage line div
local value = damage["value"]
local value = damage["value"]
if value == "weapon" then
if value == "weapon" then
result = result .. "<div>Normal weapon damage</div>"
result = result .. "Normal weapon damage"
else
else
if damage["modifier"] and damage["modifier"] ~= "" then
if damage["modifier"] and damage["modifier"] ~= "" then
Line 166: Line 196:
end
end
end
end
 
result = result .. "<div>"
if i > 1 then
if i > 1 then
result = result .. " + "
result = result .. " + "
Line 177: Line 206:
title = "DamageType",
title = "DamageType",
args = { damage["type"] }
args = { damage["type"] }
} .. "</div>"
}
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
end
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
result = result .. "</div>" -- End outermost div


return result
return result
Line 188: Line 254:


-- Parse a damage instance
-- Parse a damage instance
-- Writes result to the global variables damage_instances, damage_dice, min_roll, and max_roll
-- Writes result to the global variable parsed_data
local function damage_parse(args, damage, damage_type, damage_modifier)
local function damage_parse(args, damage_instance)
     local evaluated_modifier = evaluate_modifer(damage_modifier, args)
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
     if evaluated_modifier or not damage_modifier then
         damage_modifier = ""
         damage_modifier = ""
Line 197: Line 279:
     local flat_bonus = 0
     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
     -- Parse the damage string which (for now) assumes a very simple format of
     -- Expr = Term + Expr | Term
     -- Expr = Term + Expr | Term
     -- Term = Dice | Integer
     -- Term = Dice | Integer | prof
     -- Dice = Integer d Integer
     -- Dice = Integer d Integer
     for term in string.gmatch(damage, "[^+%s]+") do
     for term in string.gmatch(damage, "[^+%s]+") do
         if string.find(term, "d") then
    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
             -- Dice term
             local count = string.sub(term, 0, string.find(term, "d") - 1)
             local count = string.sub(term, 0, string.find(term, "d") - 1)
Line 208: Line 300:
              
              
             -- Track damage dice for when we render the dice image
             -- Track damage dice for when we render the dice image
             table.insert(damage_dice, {
             table.insert(parsed_data[kind].dice, {
                 ["value"] = "d" .. dice,
                 ["value"] = "d" .. dice,
                 ["count"] = count,
                 ["count"] = count,
Line 217: Line 309:


             -- Track the low/high values for the overall damage range preview
             -- Track the low/high values for the overall damage range preview
             min_roll = min_roll + count
             parsed_data[kind].min_roll = parsed_data[kind].min_roll + count
             max_roll = max_roll + count*dice
             parsed_data[kind].max_roll = parsed_data[kind].max_roll + count*dice
         else
         else
             -- Constant term
             -- Constant term
Line 231: Line 323:
     end
     end


     min_roll = min_roll + flat_bonus
     parsed_data[kind].min_roll = parsed_data[kind].min_roll + flat_bonus
     max_roll = max_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
     -- Re-add the updated flat bonus to the damage string
Line 239: Line 331:
     elseif flat_bonus < 0 then
     elseif flat_bonus < 0 then
         evaluated_damage = evaluated_damage .. " - " .. -flat_bonus
         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
     end


Line 244: Line 341:
     evaluated_damage = string.sub(evaluated_damage, 4, -1)
     evaluated_damage = string.sub(evaluated_damage, 4, -1)


     table.insert(damage_instances, {
     table.insert(parsed_data[kind].instances, {
         ["value"] = evaluated_damage,
         ["value"]   = evaluated_damage,
         ["type"] = damage_type,
         ["type"]     = damage_type,
         ["modifier"] = damage_modifier
         ["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
end
Line 256: Line 360:
local weapon_name = args["weapon"]
local weapon_name = args["weapon"]
if not weapon_name then
if not weapon_name then
table.insert(damage_instances, { ["value"] = "weapon" })
table.insert(parsed_data["damage"].instances, { ["value"] = "weapon" })
return
return
end
end
Line 293: Line 397:
if weapon[damage_field] then
if weapon[damage_field] then
damage_parse(args, weapon[damage_field], weapon[damage_field .. "_type"], modifier)
damage_parse(args, {
["value"]    = weapon[damage_field],
["type"]    = weapon[damage_field .. "_type"],
["modifier"] = modifier
})
end
end
end
end
else
else
table.insert(damage_instances, { ["value"] = "weapon" })
table.insert(parsed_data["damage"].instances, { ["value"] = "weapon" })
end
end
end
end
Line 307: Line 415:
while args["damage " .. i] do
while args["damage " .. i] do
local damage = args["damage " .. i]
local damage = args["damage " .. i]
local damage_type = args["damage " .. i .. " type"]
local damage_modifier = args["damage " .. i .. " modifier"]


if damage == "weapon" then
if damage == "weapon" then
weapon_parse(args)
weapon_parse(args)
else
else
damage_parse(args, damage, damage_type, damage_modifier)
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


return damage_format(frame, args)
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

Example of an in-game tooltip this module aims to replicate.

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

ExprTerm + Expr | Term
TermDice | Integer | "prof"
DiceInteger "d" Integer

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
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
1d6 + 5Damage TypesPiercing
+ 1d6Damage TypesFire
+ 2d8Damage TypesRadiant
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
1d10 + 3Damage TypesForce
+ 1d10 + 3Damage TypesForce
+ 1d10 + 3Damage TypesForce
Unspecified weapon
{{#invoke: Damage display | main
| damage 1          = weapon
| damage 2          = 1d6
| damage 2 type     = Necrotic
}}
Damage: 1~6
Normal weapon damage
+ 1d6Damage TypesNecrotic
Specified weapon
{{#invoke: Damage display | main
| damage 1          = weapon
| damage 2          = 1d6
| damage 2 type     = Necrotic

| weapon = Spear +1 
}}
Damage: 3~13
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
1d6 + 4Damage TypesPiercing
+ 1d6Damage TypesNecrotic
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
1d12Damage TypesCold
+ 1d10Damage TypesLightning
+ 2d8Damage TypesPsychic
+ 1d4Damage TypesForce
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
1d12 + 2Damage TypesSlashing
+ 1d6Damage TypesPoison
Proficiency bonus
{{#invoke: Damage display | main
| damage 1          = 1d12 + 2
| damage 1 type     = Slashing
| damage 2          = prof
| damage 2 type     = Radiant
}}
Damage: 3~14
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
1d12 + 2Damage TypesSlashing
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
8d6Damage TypesFire (DC 16  Dexterity saving throw to halve)
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
1d6Damage TypesFire per turn
+ 2d4Damage TypesAcid (delayed)
+ 1d6Damage TypesPiercing to self
+ 1d10Damage TypesPoison (conditional)
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