More actions
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 | 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; | ||
margin-left: %s; | |||
margin-top: %s; | |||
margin-right: 10px; | margin-right: 10px; | ||
">]], | ">]], | ||
Line 79: | 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 106: | 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 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, | 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 168: | 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 | |||
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 |