Module:Damage display

From bg3.wiki
Revision as of 08:43, 28 January 2025 by NtCarlson (talk | contribs)
Jump to navigation Jump to search
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: 0~0
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: 0~0
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: 0~0
Unspecified weapon
{{#invoke: Damage display | main
| damage 1          = weapon
| damage 2          = 1d6
| damage 2 type     = Necrotic
}}
Damage: 0~0
Specified weapon
{{#invoke: Damage display | main
| damage 1          = weapon
| damage 2          = 1d6
| damage 2 type     = Necrotic

| weapon = Spear +1 
}}
Damage: 0~0
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: 0~0
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: 0~0
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: 0~0
Proficiency bonus
{{#invoke: Damage display | main
| damage 1          = 1d12 + 2
| damage 1 type     = Slashing
| damage 2          = prof
| damage 2 type     = Radiant
}}
Damage: 0~0
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: 0~0
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: 0~0
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: 0~0
Healing
{{#invoke: Damage display | main
| damage 1             = 1d6
| damage 1 type        = Healing
| damage 1 modifier    = Wisdom

| wis = 19
}}
Damage: 0~0

local getArgs = require("Module:Arguments").getArgs
local p = {}

-- Given a modifier type such as "melee" or "caster", evaluate the specific
-- value based on provided ability scores
local function evaluate_modifer(modifier_type, args)
	local modifiers = {}
	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

	local evaluated_modifier = nil
	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

	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;
		padding-left: %s;
		padding-top:  %s;
		margin-right: 10px;
	">]],
		elem_width .. "px",
		width .. "px",
		left_padding .. "px",
		top_padding .. "px"
	)
	
	-- Translation and movement 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
		element = element .. string.format(
			"<span style=\"z-index: %d; position: absolute; transform: %s\">",
			n_dice - i,
			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,
	damage_instances, -- List of damage instances to render
	damage_dice, -- List of dice to render
	min_roll, -- Value to show as the lower bound of damage
	max_roll, -- Value to show as the upper bound of damage
	dice_size -- Size of dice images
)
	-- Text to insert in place of modifiers whose value could not be evaluated
	local unevaluated_modifiers = {
		melee = "[[Strength|Strength modifier]]",
		ranged = "[[Dexterity|Dexterity modifier]]",
		finesse = "[[Finesse|Strength or Dexterity modifier]]"
	}
	
	local result = "<b>Damage: " .. min_roll .. "~" .. max_roll .. "</b>"

	-- Flexbox that holds the dice images on the left and damage values on the right
	local result = result .. "<div style=\"display: flex; align-items: center\">"
	
	result = result .. damage_dice_format(frame, damage_dice, dice_size)

	for i, damage in ipairs(damage_instances) do
		-- Damage instance has an unevaluated modifier
		local value = damage["value"]
		if unevaluated_modifiers[damage["modifier"]] then
			value = value .. " + " .. unevaluated_modifiers[damage["modifier"]]
		end
	
		result = result .. "<div>"
		if i > 1 then
			result = result .. " + "
		end
		result = result .. frame:expandTemplate{
			title = "DamageColor",
			args = { damage["type"], value }
		} .. frame:expandTemplate{
			title = "DamageType",
			args = { damage["type"] }
		} .. "<div>"
	end
	result = result .. "</div>"
	return result
end

function p.main(frame)
	local args = getArgs(frame)
	-- Deal with the awkwardly named table fields
	local damage_instances = {
		[1] = args["damage"],
		[2] = args["extra damage"],
		[3] = args["extra damage 2"],
		[4] = args["extra damage 3"],
	}

	local damage_types = {
		[1] = args["damage type"],
		[2] = args["extra damage type"],
		[3] = args["extra damage type 2"],
		[4] = args["extra damage type 3"],
	}

	local damage_modifiers = {
		[1] = args["damage modifier"],
		[2] = args["extra damage modifier"],
		[3] = args["extra damage modifier 2"],
		[4] = args["extra damage modifier 3"],
	}

	local low_roll = 0
	local high_roll = 0
	local damage_dice = {}
	local damage_instances_processed = {}
	for i, damage_instance in ipairs(damage_instances) do
		local damage_type = damage_types[i]
		local damage_modifier = damage_modifiers[i]
		local evaluated_modifier = evaluate_modifer(damage_modifier, args)
		if evaluated_modifier or not damage_modifier then
			damage_modifier = ""
		end
		local flat_bonus = 0
		local evaluated_sum = ""
		-- Parse the damage string which (for now) assumes a very simple format of
		-- Expr = Term + Expr | Term
		-- Term = Dice | Integer
		-- Dice = Integer d Integer
		for term in string.gmatch(damage_instance, "[^+%s]+") do
			if 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(damage_dice, {
					["value"] = "d" .. dice,
					["count"] = count,
					["type"] = damage_type
				})

				evaluated_sum = evaluated_sum .. " + " .. term

				-- Track the low/high values for the overall damage range preview
				low_roll = low_roll + count
				high_roll = high_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
	
		low_roll = low_roll + flat_bonus
		high_roll = high_roll + flat_bonus
		
		-- Re-add the updated flat bonus to the damage string
		if flat_bonus > 0 then
			evaluated_sum = evaluated_sum .. " + " .. flat_bonus
		elseif flat_bonus < 0 then
			evaluated_sum = evaluated_sum .. " - " .. -flat_bonus
		end

		-- Strip leading " + "
		evaluated_sum = string.sub(evaluated_sum, 4, -1)
		
		table.insert(damage_instances_processed, {
			["value"]	 = evaluated_sum,
			["type"]	 = damage_type,
			["modifier"] = damage_modifier
		})
	end

	local dice_width = args["dice width"] or 30
	return damage_format(
		frame,
		damage_instances_processed,
		damage_dice,
		low_roll,
		high_roll,
		dice_width
	)
end

return p