Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Damage display: Difference between revisions

From bg3.wiki
mNo edit summary
m Added conditional damage property
 
(37 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 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 31: 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 70: 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 79: 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 > #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,
image_transform[i]
dice_image_transform[i]
)
)
element = element .. string.format(
element = element .. string.format(
Line 106: Line 159:


-- Format the damage values
-- Format the damage values
local function damage_format(
local function damage_format(frame, args, data, header)
frame,
local result = ""
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]]"
}
-- 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 135: Line 172:


-- Left div element containing the damage dice images
-- Left div element containing the damage dice images
result = result .. damage_dice_format(frame, damage_dice, dice_size)
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(damage_instances) do
for i, damage in ipairs(data.instances) do
-- Damage instance has an unevaluated modifier
result = result .. "<div>" -- Begin damage line div
local value = damage["value"]
local value = damage["value"]
if unevaluated_modifiers[damage["modifier"]] then
if value == "weapon" then
value = value .. " + " .. unevaluated_modifiers[damage["modifier"]]
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 .. "<div>"
-- "Delayed" field is for damage that doesn't apply immediately such as Melf's Acid Arrow
if i > 1 then
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 .. frame:expandTemplate{
result = result .. "</div>" -- End damage line div
title = "DamageColor",
args = { damage["type"], value }
} .. frame:expandTemplate{
title = "DamageType",
args = { damage["type"] }
} .. "</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
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 168: Line 412:
local args = getArgs(frame)
local args = getArgs(frame)


local low_roll = 0
local high_roll = 0
local damage_dice = {}
local damage_instances = {}
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]
local damage_type = args["damage " .. i .. " type"]
local damage_modifier = args["damage " .. i .. " modifier"]


local evaluated_modifier = evaluate_modifer(damage_modifier, args)
if damage == "weapon" then
if evaluated_modifier or not damage_modifier then
weapon_parse(args)
damage_modifier = ""
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
local flat_bonus = 0
local evaluated_damage = ""
-- 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, "[^+%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_damage = evaluated_damage .. " + " .. 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_damage = evaluated_damage .. " + " .. flat_bonus
elseif flat_bonus < 0 then
evaluated_damage = evaluated_damage .. " - " .. -flat_bonus
end
-- Strip leading " + "
evaluated_damage = string.sub(evaluated_damage, 4, -1)
table.insert(damage_instances, {
["value"] = evaluated_damage,
["type"] = damage_type,
["modifier"] = damage_modifier
})
i = i + 1
i = i + 1
end
end


local dice_width = args["dice width"] or 30
local result = ""
return damage_format(
-- Damage and healing instances are tracked and displayed separately
frame,
if #parsed_data.damage.instances > 0 then
damage_instances,
result = result .. damage_format(frame, args, parsed_data.damage, "Damage")
damage_dice,
end
low_roll,
if #parsed_data.healing.instances > 0 then
high_roll,
result = result .. damage_format(frame, args, parsed_data.healing, "Healing")
dice_width
end
)
return result
end
end


return p
return p