From Dark and Darker Wiki

damage on hit column added, all categories added
mostly copied weapons individual page functions to utility
 
(6 intermediate revisions by the same user not shown)
Line 94: Line 94:
local effect = ""
local effect = ""
if utility.utilitydesc then effect = utility.utilitydesc end
if utility.utilitydesc then effect = utility.utilitydesc end
local frame = mw.getCurrentFrame()
local expand = frame:expandTemplate({
    title = 'UtilityEffects',
    args = { string.gsub(utility.name, "%s+", "") }
})
if expand ~= "" then effect = effect.." "..expand
elseif effect == "" and expand ~= "" then effect = expand
end
return "<td>"..effect.."</td>"
return "<td>"..effect.."</td>"
end
end
Line 117: Line 125:
if interactiontimeformatted and interactiontimescaling then finalstring = format("<td>%s<br>%s Scaling</td>", interactiontimeformatted, interactiontimescaling) end
if interactiontimeformatted and interactiontimescaling then finalstring = format("<td>%s<br>%s Scaling</td>", interactiontimeformatted, interactiontimescaling) end
return finalstring
return finalstring
end
---Helper function: Collects move mult and prepare move mult for display
local function collect_multipliers(ability)
local move, prep_move = "",""
for _, attack_name in ipairs(ability.order) do
if ability[attack_name].movementmultiplier then
if move ~= "" then
move = move.."/"
prep_move = prep_move.."/"
end
move = move..(ability[attack_name].movementmultiplier or "")
prep_move = prep_move..(ability[attack_name].preparemovementmultiplier or "")
end
end
return move,prep_move
end
---Formats a table cell containing speed penalties for each action
local function action_move_speed_penalty(utility,is_header)
if is_header then return [=[<th style="width:10%">[[Weapons#Movement_Multiplier_Explanation|Action Movement Penalty]]</th>]=] end
-- The below was brought from the Weapons Module, it appears "Other" abilities were ignored
-- This seems to skip needed ones in utilities, maybe weapons also has some? this is a potential oversight
-- local order = {} --reconstruct order to exclude the other ability group
-- for _,ability in ipairs(utility.abilities.order) do
-- if ability ~= "Other" then order[#order+1] = ability end
-- end
local wt = ""
for i,ability_name in ipairs(utility.abilities.order) do
if i~=1 then wt = wt.."<br><br>" end
wt = wt.."<span style='color:#eee8; font-size:110%; font-weight:bold'>"
if ability_name=="Other" or ability_name=="Block" then wt = wt..ability_name.." Actions" else wt = wt..ability_name.." Attacks" end
wt = wt.."</span><br>"
local movement_multiplier,prepare_movement_multiplier = collect_multipliers(utility.abilities[ability_name])
if #prepare_movement_multiplier > 3 then
wt = wt.."<span style='color:#eee8'>Mid Attack:&nbsp;</span>x"..movement_multiplier
.."<br><span style='color:#eee8'>Otherwise:&nbsp;</span>x"..prepare_movement_multiplier
else
wt = wt.."<span style='color:#eee8'>Always:&nbsp;</span>x"..movement_multiplier
end
end
return "<td>"..wt.."</td>"
end
---Formats a table cell containing hitslow for each attack
local function slowdown_on_hit(utility,is_header)
if is_header then return [[<th style="width:8%">Slowdown On Hit</th>]] end
if not (utility.abilities.Primary or utility.abilities.Secondary or utility.abilities.Special) then return "<td></td>" end
local ability_type = "Primary"
-- if utility.slottype == "Off-Hand" then ability_type = "Secondary" end
local wt = format("<span style='color:#eee8'>%s Hitslow</span><br>", ability_type)
local duration = " for "
for i,attack in ipairs(utility.abilities[ability_type].order) do
if i~=1 then wt = wt.."/" end
wt = wt..(utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Move Speed Add"]
or utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Move Speed Bonus"] or "")
end
for i,attack in ipairs(utility.abilities[ability_type].order) do
if i~=1 then duration = duration.."/" end
duration = duration..(utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Duration"] or "")
end
return "<td>"..wt..duration.."s</td>"
end
---These stats get their own table cells, as such they are excluded from the stats cell to avoid needless redundancy
local exclude_stat = {["Physical Base Weapon Damage"]=true,["Move Speed"]=true}
---Marks up a stat block with the stat name and colored values and appropriate margins and alignment
local function inline_block(utility,stat)
if utility.stats[(stat):lower()] == nil then return "" end
return "<div style='display:inline-block;vertical-align:top;margin:0 15px 0 15px'><b style='color:#eee8'>"..stat.."</b><br>"..color_values(utility,utility.stats[(stat):lower()]).."</div>"
end
---Formats a table cell containing any stats not covered by the rest of the row
local function stats(utility,is_header)
if is_header then return [[<th style="width:12%">Stats</th>]] end
local wt = ""
for i,stat in ipairs(utility.stats.order) do
if not exclude_stat[stat] then
wt = wt..inline_block(utility,stat)
end
end
return "<td>"..wt.."</td>"
end
end


Line 129: Line 229:
---Determines which table data goes into the table row
---Determines which table data goes into the table row
local row_type = {
local row_type = {
["Drink"] = {name,move_speed,effect,action_time}, --action_move_speed_penalty
["Drink"] = {name, move_speed, effect, action_time, action_move_speed_penalty},
["Consumable"] = {name,move_speed,effect,interaction_time}, --action_move_speed_penalty
["Consumable"] = {name, move_speed, effect, interaction_time, action_move_speed_penalty},
["Installable"] = {name,class,move_speed,effect,interaction_time},
["Installable"] = {name,class,move_speed,effect,interaction_time},
["LightSource"] = {name,move_speed,damage_on_hit,effect}, --action_move_speed_penalty, slowdown_on_hit
["LightSource"] = {name, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
["Throwable"] = {name,class,move_speed,damage_on_hit,effect}, --action_move_speed_penalty, slowdown_on_hit
["Throwable"] = {name, class, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
["MusicalInstrument"] = {name,move_speed,damage_on_hit,effect}, --action_move_speed_penalty, slowdown_on_hit
["MusicalInstrument"] = {name, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
["Mining"] = {name,move_speed,damage_on_hit,effect}, --stats, action_move_speed_penalty, slowdown_on_hit
["Mining"] = {name, move_speed, damage_on_hit, stats, effect, action_move_speed_penalty, slowdown_on_hit},
["SoulHeart"] = {name,move_speed,effect},
["SoulHeart"] = {name, move_speed, effect},
["Food"] = {name,move_speed,effect},
["Food"] = {name, move_speed, effect},
["Tool"] = {name,move_speed,effect}, --stats, action_move_speed_penalty, slowdown_on_hit
["Tool"] = {name, move_speed, damage_on_hit, stats, effect, action_move_speed_penalty, slowdown_on_hit},
["Default"] = {name,move_speed,damage_on_hit,effect,action_time,action_move_speed_penalty}
["Default"] = {name, move_speed, damage_on_hit, stats, effect, action_time, action_move_speed_penalty}
}
}


Line 155: Line 255:
end
end


-- Draw the table based on type (Drink, Consumable, etc.)
function p.draw_table(frame)
function p.draw_table(frame)
local wt = {}
local wt = {}
Line 167: Line 268:
wt[#wt+1] = "</table>"
wt[#wt+1] = "</table>"


return concat(wt)
end
--[=[###########################################################################
                Functions for individual utility pages
#############################################################################]=]
function p_quest_information(wt, name)
if UD.Utility[name].questinformation == "" then return
else wt[#wt+1] = "<h2>Quest Information</h2><ul>"..UD.Utility[name].questinformation.."</ul>" end
end
local function p_projectile(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
if weapon.initialspeed ~= "" then
wt[#wt+1] = string.format("<h2>Projectile</h2>Initial Speed: %s<br>", weapon.initialspeed)
end
if weapon.piercecount ~= "" then
wt[#wt+1] = string.format("Pierce: %s<br>", weapon.piercecount)
end
end
local function p_damage_falloff(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
if weapon.damagefalloff == "" then return end
wt[#wt+1] = string.format("<h2>Damage Falloff</h2>%s has damage falloff depending on airtime of the projectile.<br><br>Projectile falloff damage chart:<br>X: Air time (seconds)<br>Y: %% Damage",weapon_name,weapon.initialspeed)
wt[#wt+1] = "<div style='width:500px'>"
wt[#wt+1] = mw.getCurrentFrame():callParserFunction({ name = "#widget:Chart",  args= { uniqueId = weapon_name, data = weapon.damagefalloff, color = 'orange' }})
wt[#wt+1] = "</div><br>Learn more at [[Damage_Calculation#Projectile_Falloff|Projectile Falloff]]"
end
local function p_hitbox(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
if not weapon.hitbox or not weapon.hitbox.height then return end
wt[#wt+1] = "<h2>Hitbox</h2>"
wt[#wt+1] = "Height: "..weapon.hitbox.height
wt[#wt+1] = "<br>Width: "..weapon.hitbox.width
wt[#wt+1] = "<br>Depth: "..weapon.hitbox.depth
local primary_attack = weapon.abilities.Primary and weapon.abilities.Primary["Attack 1"] or {}
local secondary_attack = weapon.abilities.Secondary and weapon.abilities.Secondary["Attack 1"] or {}
if #primary_attack == 0 and #secondary_attack == 0 then return end
-- If is not ranged, and has primary or secondary attacks
if not (weapon.types["Bow"] or weapon.types["Crossow"] or weapon.types["ThrowableStuff"])
and (primary_attack.impactzones or secondary_attack.impactzones) then
local impact_zones = primary_attack.impactzones or secondary_attack.impactzones
wt[#wt+1] = "<br><br>The weapon's hitbox doesn't necessarily determine a weapon's reach due to different arm extension during attack animations. Thus, shorter weapons can have better reach than longer weapons.<br>[[Impact Zones]] of "
wt[#wt+1] = weapon.name
wt[#wt+1] = ":<div style='display:flex;align-items:center'><div>[[File:"
wt[#wt+1] = weapon.name
wt[#wt+1] = " Hitbox.png|x300px]]</div><div><span style='color:green'>Green Zone deals "
wt[#wt+1] = impact_zones[1]
wt[#wt+1] = " damage</span>"
if impact_zones[2] then
wt[#wt+1] = "<br><br><span style='color:orange'>Orange Zone deals "
wt[#wt+1] = impact_zones[2]
wt[#wt+1] = " damage</span>"
end
if impact_zones[3] then
wt[#wt+1] = "<br><br><span style='color:red'>Red Zone deals "
wt[#wt+1] = impact_zones[3]
wt[#wt+1] = " damage</span>"
end
wt[#wt+1] = "</div></div><p><br>'''Disclaimer''': The impact zone's locations are an approximation. The real hitboxes may be slightly larger, closer together, and/or follow the weapon's shape more closely. Image is not able to be updated automatically. If it is outdated, please report it to the [https://discord.gg/KbsxgACkFS Wiki Discord].</p>"
end
if primary_attack.impactpower or secondary_attack.impactpower then
wt[#wt+1] = "<br>'''Impact Power''' of "
wt[#wt+1] = weapon.name
wt[#wt+1] = ": "
wt[#wt+1] = tostring(primary_attack.impactpower or secondary_attack.impactpower)
wt[#wt+1] = "<br>Impact power helps with breaking crates, barricades or any other breakables in game. The higher the impact power the faster something breaks.<br>Impact power also affects shield blocking. If the attacking weapon's impact power is higher than shield's impact power, the shield holder staggers, and vice versa."
end
end
local function list_to_string(l,separator)
local ret = ""
for i,v in ipairs(l) do
if i~=1 then ret = ret..separator end
ret = ret..v
end
return ret
end
local function combo_format(wt,attacks,type)
if type == "Other" or type == "Block" then return end
local first = ""
wt[#wt+1] = "<span style='color:#eee8'>"
wt[#wt+1] = type
wt[#wt+1] = " Attack: </span>"
for i,name in ipairs(attacks.order) do
if i ~= 1 then wt[#wt+1] = "/" else first = name end
wt[#wt+1] = attacks[name].damagetype
end
wt[#wt+1] = " dealing "
for i,name in ipairs(attacks.order) do
if i ~= 1 then wt[#wt+1] = "/" end
wt[#wt+1] = attacks[name].combodamage
end
wt[#wt+1] = " damage on each swing, with impact zone: "
wt[#wt+1] = list_to_string(attacks[first].impactzones, "/")
wt[#wt+1] = "<br>"
end
local function attack_animation_times(wt,weapon,ability_name,attack_name)
local attack = weapon.animationtimes[ability_name][attack_name]
if attack["Windup"]            then wt[#wt+1] = string.format("Windup: %s<br>",                          attack["Windup"]) end
if attack["Hit"]              then wt[#wt+1] = string.format("Hit: %s<br>",                              attack["Hit"]) end
if attack["Second Windup"]    then wt[#wt+1] = string.format("Second Windup: %s<br>",                    attack["Second Windup"]) end
if attack["Second Hit"]        then wt[#wt+1] = string.format("Second Hit: %s<br>",                      attack["Second Hit"]) end
if attack["Recover"]          then wt[#wt+1] = string.format("&nbsp;&#10551; Recover: %s<br>",          attack["Recover"]) end
if attack["Alternate Recover"] then wt[#wt+1] = string.format("&nbsp;&#10551; Alternate Recover: %s<br>", attack["Alternate Recover"]) end
if attack["Finish"]            then wt[#wt+1] = string.format("&nbsp;&#10551; Finish: %s<br>",            attack["Finish"]) end
end
local function animation_format(wt, weapon, ability)
if ability == "Other" or ability == "Block" then return end
for i, ability_name in ipairs(weapon.abilities[ability].order) do
wt[#wt+1] = ability.." Combo: "..ability_name.."<br><div style='display:inline-block;margin-left:20px'>"
attack_animation_times(wt,weapon,ability,ability_name)
wt[#wt+1] = "</div><br><br>"
end
end
local function p_combo(wt, page_name)
local weapon = UD.Utility[page_name]
if not (weapon.abilities.Primary or weapon.abilities.Secondary or weapon.abilities.Special) then return end
-- If is not ranged, and isn't shield, or at least isn't a Lantern Shield
if not (weapon.types.Bow or weapon.types.Crossow or weapon.types.ThrowableStuff)
and (not weapon.types.Shield or (weapon.name):match("Lantern Shield")) then
wt[#wt+1] = "<h2>Attack Animation Time</h2>"
for _,name in ipairs(weapon.abilities.order) do
combo_format(wt,weapon.abilities[name],name)
end
if weapon.abilities.Primary or weapon.abilities.Secondary or weapon.abilities.Special then
wt[#wt+1] = "<br><b>Attack Times</b><br><div style='display:inline-block;margin-left:20px'>"
for _,name in ipairs(weapon.abilities.order) do
animation_format(wt,weapon,name)
end
wt[#wt+1] = "</div>"
end
wt[#wt+1] = "Reference [[Attack Times]] for a glossary of the terms used above.<br>"
end
end
local function p_block(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
if not weapon.impactresistance or weapon.impactresistance == "" then return end
wt[#wt+1] = string.format("<h2>Block</h2>%s has a block ability with an impact resistance of <b>%s</b>.<br>To learn more about impact resistance see [[Impact Power]].", weapon.name, weapon.impactresistance)
end
local function p_action_move_slow_penalty(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
wt[#wt+1] = "<h2>Action Movement Slow</h2>When a player performs certain actions, such as attacking or defending themselves, they move slower."
wt[#wt+1] = "<div style='display:inline-block;margin-left:20px'>"
for _,ability in ipairs(weapon.abilities.order) do
for _,attack in ipairs(weapon.abilities[ability].order) do
-- There's a bug where Riposte values are recorded within "Other" abilities
-- Since ripose is covered in Special abilities, we will skip it here
if attack == "Riposte" then break end
local movespeedadd = weapon.abilities[ability][attack].effects
and weapon.abilities[ability][attack].effects.ItemActivateState
and weapon.abilities[ability][attack].effects.ItemActivateState.rarity.Global["Move Speed Add"]
or ""
local movement_multiplier = weapon.abilities[ability][attack].movementmultiplier or ""
local prepare_movement_multiplier = weapon.abilities[ability][attack].preparemovementmultiplier or ""
wt[#wt+1] = "<span style='color:#eee8; font-size:110%; font-weight:bold'>"
if ability == "Other" then wt[#wt+1] = attack else wt[#wt+1] = ability end
wt[#wt+1] = "</span><br>"
wt[#wt+1] = "<div style='display:inline-block;margin-left:20px'>"
if #prepare_movement_multiplier > 1 then
wt[#wt+1] = "<span style='color:#eee8'>Mid Attack:&nbsp;</span>x"
wt[#wt+1] = movement_multiplier
wt[#wt+1] = "<br><span style='color:#eee8'>Otherwise:&nbsp;</span>x"
wt[#wt+1] = prepare_movement_multiplier
elseif #movement_multiplier > 1 then
wt[#wt+1] = "<span style='color:#eee8'>Always:&nbsp;</span>x"
wt[#wt+1] = movement_multiplier
else
wt[#wt+1] = "<span style='color:#eee8'>Always:&nbsp;</span>"
wt[#wt+1] = movespeedadd
end
wt[#wt+1] = "</div>"
wt[#wt+1] = "<br><br>"
-- Everything except attacks within Other has invariant move mults
-- So after processessing the first attack, we break the loop
if ability ~= "Other" then break end
end
end
wt[#wt+1] = "</div>"
end
local function p_artifact(wt,weapon_name)
local weapon = UD.Utility[weapon_name]
if not weapon.isartifact then return end
wt[#wt+1] = string.format("<h2>Artifact</h2><p>%s has a powerful [[Artifacts|Artifact]] named [[%s]].</p>", weapon.name, weapon.artifactname)
end
-- Draw anything relevant to the item for its individual page (not infobox or crafts)
function p.page(f)
local page_name = f.args.name
local wt = {}
p_quest_information(wt, page_name)
p_projectile(wt,page_name)
p_damage_falloff(wt,page_name)
p_hitbox(wt,page_name)
p_combo(wt,page_name)
p_block(wt,page_name)
p_action_move_slow_penalty(wt,page_name)
p_artifact(wt,page_name)
return concat(wt)
return concat(wt)
end
end
Line 183: Line 513:
-- mw.log(p.draw_table({args={"Food"}}))
-- mw.log(p.draw_table({args={"Food"}}))
-- mw.log(p.draw_table({args={"Tool"}}))
-- mw.log(p.draw_table({args={"Tool"}}))
-- mw.log(p.page({args={name="Ale"}}))
-- mw.log(p.page({args={name="Francisca Axe"}}))

Latest revision as of 07:12, 12 November 2026

Overview

Functions for making Utility tables. Data comes from Data:Utility.json.

Functions

draw_table

Creates a table of utilities

Parameters

  • 1 - <Type>


draw_table examples

Drink


{{#invoke:Utility|draw_table|Drink}}


NameMovement SpeedEffectAction TimeAction Movement Penalty
-10While active, gain 4 | 5 | 6 | 7 | 8 | 9 | 10 Strength and become drunk for 15 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Drink this to gain 5 to All Attributes for 10 seconds. In return, you also gain an unhealing debuff. 3s
100% Scaling
Other Actions
Always: x75%
-10Recover 20 Magical Base Healing (50% Scaling) over 15 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Instantly restores 3 health and applies a festive effect. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain between 50 | 100 | 200 to 150 | 200 | 300 Luck. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain a shield that blocks 10 | 15 | 20 | 25 | 30 | 35 | 40 magical Damage for 24 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain 2 spell recovery every second for 20 | 40 | 60 | 80 | 100 | 120 | 140 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Recover 20 Magical Base Healing (50% Scaling) over 20 | 17.5 | 15 | 12.5 | 10 | 7.5 | 5 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain invisibility for 11 | 14 | 17 | 20 | 23 | 26 | 29 seconds. When you move, perform an action or take damage, the effect is removed. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain between 20 to 80 Luck. 3s
100% Scaling
Other Actions
Always: x75%
-10Gain a shield that blocks 10 | 15 | 20 | 25 | 30 | 35 | 40 physical damage for 24 seconds. 3s
100% Scaling
Other Actions
Always: x75%
-10Recover 20 | 30 | 40 | 50 | 60 | 70 | 80 Oxygen. 3s
100% Scaling
Other Actions
Always: x75%
-10Recover 100 Magical Base Healing (50% Scaling) over 100 seconds. 3s
100% Scaling
Other Actions
Always: x75%

Code


local p = {}
local UD = mw.loadJsonData("Data:Utility.json")
local concat = table.concat
local format = string.format

---Get the item's size in pixels
local function size(item)
	return tostring(item.invwidth*45).."x"..tostring(item.invheight*45).."px"
end

local function count(t)
	local c = 0
	for _ in pairs(t) do c = c + 1 end
	return c
end

---Marks up a list of values with color based on rarity
local function color_values(utility, values)
	if values == nil then return "" end
	if type(values)=="string" or type(values)=="number" then
		if count(utility.rarities) == 1 then return "<span class='cr"..utility.rarities[1].."'>"..values.."</span>" end
		return tostring(values)
	end

	local wt = ""
	local newline = ""
	for i, key in ipairs(utility.rarities) do
		wt = wt..newline.."<span class='cr"..key.."'>"..tostring(values[tonumber(key)] or "").."</span>"
		if i == 1 then newline = "<br>" end
	end
	return wt
end

---Returns a predetermined utility list
local function get_list(frame)
	if not (frame.args and frame.args[1]) then return {} end
	if not (UD[frame.args[1]] and UD[frame.args[1]]) then return {} end

	return UD[frame.args[1]]
end

---Formats a table cell for the iconbox
local function name(item, is_header)
	if is_header then return [[<th style="width:5%">Name</th>]] end

	local color = "2"
	local bold_link = "|<b>"..item.name
	if count(item.rarities) == 1 then
		color = item.rarities[1]
		bold_link = "|<b class=cr"..color..">"..item.name
	end

	local icon_amount = ""
	if item.maxammocount > 0 then
		icon_amount = "<span class='iconamount' style='pointer-events:none;color:#EEEA;font-size:16px'>"..item.maxammocount.."</span>"
	end

	return "<td><div class='iconbox' style='width:max-content;align-items:center'><div class='rarity"..color.." rounded relative'>[[File:"..item.name..".png|"..size(item).."|link="..item.name.."]]"
				..icon_amount
			.."</div><br>[["..item.name..bold_link.."</b>]]</div></td>"
end

---Formats a table cell of classes
local function class(utility,is_header)
	if is_header then return [[<th style="width:5%">Class</th>]] end
	
	local wt = {}
	wt[#wt+1] = "<td>"
	for _, class in ipairs(utility.classes) do
		wt[#wt+1] = class.."<br>"
	end
	wt[#wt+1] = "</td>"
	return concat(wt)
end

---Formats a table cell of move speeds
local function move_speed(utility,is_header)
	if is_header then return [[<th style="width:5%">Movement Speed</th>]] end

	return "<td>"..color_values(utility,utility.stats["move speed"]).."</td>"
end

---Formats a table cell of damages on hit
local function damage_on_hit(utility,is_header)
	if is_header then return [[<th style="width:5%">Damage on Hit</th>]] end

	return "<td>"..color_values(utility,utility.stats["physical base weapon damage"]).."</td>"
end

---Formats a table cell of effects
local function effect(utility,is_header)
	if is_header then return [[<th style="width:20%">Effect</th>]] end
	
	local effect = ""
	if utility.utilitydesc then effect = utility.utilitydesc end
	local frame = mw.getCurrentFrame() 
	local expand = frame:expandTemplate({
	    title = 'UtilityEffects',
	    args = { string.gsub(utility.name, "%s+", "") }
	})
	if expand ~= "" then effect = effect.." "..expand
	elseif effect == "" and expand ~= "" then effect = expand
	end
	return "<td>"..effect.."</td>"
end

---Formats a table cell of action times
local function action_time(utility,is_header)
	if is_header then return [[<th style="width:10%">Action Time</th>]] end
	
	local actiontime = utility.actiontime
	local actiontimescaling = utility.actiontimescaling
	local finalstring = "<td></td>"
	if actiontime and actiontimescaling then finalstring = format("<td>%ss<br>%s Scaling</td>", actiontime, actiontimescaling) end
	return finalstring
end

---Formats a table cell of interaction times (This is the little loading circle with the percent when interacting vs stuff like drinking a potion)
local function interaction_time(utility,is_header)
	if is_header then return [[<th style="width:10%">Interaction Time</th>]] end
	
	local interactiontimescaling = utility.interactiontimescaling
	local interactiontimeformatted = utility.interactiontimeformatted
	local finalstring = "<td></td>"
	if interactiontimeformatted and interactiontimescaling then finalstring = format("<td>%s<br>%s Scaling</td>", interactiontimeformatted, interactiontimescaling) end
	return finalstring
end

---Helper function: Collects move mult and prepare move mult for display
local function collect_multipliers(ability)
	local move, prep_move = "",""
	for _, attack_name in ipairs(ability.order) do
		if ability[attack_name].movementmultiplier then
			if move ~= "" then
				move = move.."/"
				prep_move = prep_move.."/"
			end
			move = move..(ability[attack_name].movementmultiplier or "")
			prep_move = prep_move..(ability[attack_name].preparemovementmultiplier or "")
		end
	end

	return move,prep_move
end

---Formats a table cell containing speed penalties for each action
local function action_move_speed_penalty(utility,is_header)
	if is_header then return [=[<th style="width:10%">[[Weapons#Movement_Multiplier_Explanation|Action Movement Penalty]]</th>]=] end

	-- The below was brought from the Weapons Module, it appears "Other" abilities were ignored
	-- This seems to skip needed ones in utilities, maybe weapons also has some? this is a potential oversight
	
	-- local order = {} --reconstruct order to exclude the other ability group
	-- for _,ability in ipairs(utility.abilities.order) do
	-- 	if ability ~= "Other" then order[#order+1] = ability end
	-- end

	local wt = ""
	for i,ability_name in ipairs(utility.abilities.order) do
		if i~=1 then wt = wt.."<br><br>" end

		wt = wt.."<span style='color:#eee8; font-size:110%; font-weight:bold'>"
		if ability_name=="Other" or ability_name=="Block" then wt = wt..ability_name.." Actions" else wt = wt..ability_name.." Attacks" end
		wt = wt.."</span><br>"

		local movement_multiplier,prepare_movement_multiplier = collect_multipliers(utility.abilities[ability_name])
		if #prepare_movement_multiplier > 3 then
			wt = wt.."<span style='color:#eee8'>Mid Attack:&nbsp;</span>x"..movement_multiplier
			.."<br><span style='color:#eee8'>Otherwise:&nbsp;</span>x"..prepare_movement_multiplier
		else
			wt = wt.."<span style='color:#eee8'>Always:&nbsp;</span>x"..movement_multiplier
		end
	end
	return "<td>"..wt.."</td>"
end

---Formats a table cell containing hitslow for each attack
local function slowdown_on_hit(utility,is_header)
	if is_header then return [[<th style="width:8%">Slowdown On Hit</th>]] end
	if not (utility.abilities.Primary or utility.abilities.Secondary or utility.abilities.Special) then return "<td></td>" end

	local ability_type = "Primary"
	-- if utility.slottype == "Off-Hand" then ability_type = "Secondary" end

	local wt = format("<span style='color:#eee8'>%s Hitslow</span><br>", ability_type)
	local duration = " for "
	for i,attack in ipairs(utility.abilities[ability_type].order) do
		if i~=1 then wt = wt.."/" end
		wt = wt..(utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Move Speed Add"]
				or utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Move Speed Bonus"] or "")
	end
	for i,attack in ipairs(utility.abilities[ability_type].order) do
		if i~=1 then duration = duration.."/" end
		duration = duration..(utility.abilities[ability_type][attack].effects.HitSlow.rarity.Global["Duration"] or "")
	end
	return "<td>"..wt..duration.."s</td>"
end

---These stats get their own table cells, as such they are excluded from the stats cell to avoid needless redundancy
local exclude_stat = {["Physical Base Weapon Damage"]=true,["Move Speed"]=true}

---Marks up a stat block with the stat name and colored values and appropriate margins and alignment
local function inline_block(utility,stat)
	if utility.stats[(stat):lower()] == nil then return "" end
	return "<div style='display:inline-block;vertical-align:top;margin:0 15px 0 15px'><b style='color:#eee8'>"..stat.."</b><br>"..color_values(utility,utility.stats[(stat):lower()]).."</div>"
end

---Formats a table cell containing any stats not covered by the rest of the row
local function stats(utility,is_header)
	if is_header then return [[<th style="width:12%">Stats</th>]] end

	local wt = ""
	for i,stat in ipairs(utility.stats.order) do
		if not exclude_stat[stat] then
			wt = wt..inline_block(utility,stat)
		end
	end
	return "<td>"..wt.."</td>"
end

local function determine_table_type(utility)
	if utility.args then
		return utility.args[1]
	elseif utility.utilitytype then
		return utility.utilitytype
	end
end

---Determines which table data goes into the table row
local row_type = {
	["Drink"] = {name, move_speed, effect, action_time, action_move_speed_penalty},
	["Consumable"] = {name, move_speed, effect, interaction_time, action_move_speed_penalty},
	["Installable"] = {name,class,move_speed,effect,interaction_time},
	["LightSource"] = {name, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
	["Throwable"] = {name, class, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
	["MusicalInstrument"] = {name, move_speed, damage_on_hit, effect, action_move_speed_penalty, slowdown_on_hit},
	["Mining"] = {name, move_speed, damage_on_hit, stats, effect, action_move_speed_penalty, slowdown_on_hit},
	["SoulHeart"] = {name, move_speed, effect},
	["Food"] = {name, move_speed, effect},
	["Tool"] = {name, move_speed, damage_on_hit, stats, effect, action_move_speed_penalty, slowdown_on_hit},
	["Default"] = {name, move_speed, damage_on_hit, stats, effect, action_time, action_move_speed_penalty}
}

---Return a table row of data cells containing utility information,
local function row(utility, is_header)
	is_header = is_header or false
	local wt = {}
	wt[#wt+1] = "<tr>"
	for _,content in ipairs(row_type[determine_table_type(utility)]) do
		wt[#wt+1] = content(utility,is_header)
	end
	wt[#wt+1] = "</tr>"

	return concat(wt)
end

-- Draw the table based on type (Drink, Consumable, etc.)
function p.draw_table(frame)
	local wt = {}
	local item_list = get_list(frame)
	if count(item_list) == 0 then return "<span style='font-size:24pt'>There are currently no items of this type.</span>" end

	wt[#wt+1] = "<table cellspacing='0' class='wikitable sortable jquery-tablesorter' style='text-align:center; vertical-align:middle;'>"
	wt[#wt+1] = row(frame, true)
	for _,item_name in ipairs(item_list) do
		wt[#wt+1] = row(UD.Utility[item_name])
	end
	wt[#wt+1] = "</table>"

	return concat(wt)
end

--[=[###########################################################################
                Functions for individual utility pages
#############################################################################]=]
function p_quest_information(wt, name)
	if UD.Utility[name].questinformation == "" then return
	else wt[#wt+1] = "<h2>Quest Information</h2><ul>"..UD.Utility[name].questinformation.."</ul>" end
end

local function p_projectile(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]
	if weapon.initialspeed ~= "" then 
		wt[#wt+1] = string.format("<h2>Projectile</h2>Initial Speed: %s<br>", weapon.initialspeed)
	end
	if weapon.piercecount ~= "" then
		wt[#wt+1] = string.format("Pierce: %s<br>", weapon.piercecount)
	end
end

local function p_damage_falloff(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]
	if weapon.damagefalloff == "" then return end

	wt[#wt+1] = string.format("<h2>Damage Falloff</h2>%s has damage falloff depending on airtime of the projectile.<br><br>Projectile falloff damage chart:<br>X: Air time (seconds)<br>Y: %% Damage",weapon_name,weapon.initialspeed)
	wt[#wt+1] = "<div style='width:500px'>"
	wt[#wt+1] = mw.getCurrentFrame():callParserFunction({ name = "#widget:Chart",  args= { uniqueId = weapon_name, data = weapon.damagefalloff, color = 'orange' }})
	wt[#wt+1] = "</div><br>Learn more at [[Damage_Calculation#Projectile_Falloff|Projectile Falloff]]"
end

local function p_hitbox(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]
	if not weapon.hitbox or not weapon.hitbox.height then return end

	wt[#wt+1] = "<h2>Hitbox</h2>"
	wt[#wt+1] = "Height: "..weapon.hitbox.height
	wt[#wt+1] = "<br>Width: "..weapon.hitbox.width
	wt[#wt+1] = "<br>Depth: "..weapon.hitbox.depth

	local primary_attack = weapon.abilities.Primary and weapon.abilities.Primary["Attack 1"] or {}
	local secondary_attack = weapon.abilities.Secondary and weapon.abilities.Secondary["Attack 1"] or {}
	if #primary_attack == 0 and #secondary_attack == 0 then return end

	-- If is not ranged, and has primary or secondary attacks
	if not (weapon.types["Bow"] or weapon.types["Crossow"] or weapon.types["ThrowableStuff"])
			and (primary_attack.impactzones or secondary_attack.impactzones) then
		local impact_zones = primary_attack.impactzones or secondary_attack.impactzones
		wt[#wt+1] = "<br><br>The weapon's hitbox doesn't necessarily determine a weapon's reach due to different arm extension during attack animations. Thus, shorter weapons can have better reach than longer weapons.<br>[[Impact Zones]] of "
		wt[#wt+1] = weapon.name
		wt[#wt+1] = ":<div style='display:flex;align-items:center'><div>[[File:"
		wt[#wt+1] = weapon.name
		wt[#wt+1] = " Hitbox.png|x300px]]</div><div><span style='color:green'>Green Zone deals "
		wt[#wt+1] = impact_zones[1]
		wt[#wt+1] = " damage</span>"
		if impact_zones[2] then
			wt[#wt+1] = "<br><br><span style='color:orange'>Orange Zone deals "
			wt[#wt+1] = impact_zones[2]
			wt[#wt+1] = " damage</span>"
		end
		if impact_zones[3] then
			wt[#wt+1] = "<br><br><span style='color:red'>Red Zone deals "
			wt[#wt+1] = impact_zones[3]
			wt[#wt+1] = " damage</span>"
		end
		wt[#wt+1] = "</div></div><p><br>'''Disclaimer''': The impact zone's locations are an approximation. The real hitboxes may be slightly larger, closer together, and/or follow the weapon's shape more closely. Image is not able to be updated automatically. If it is outdated, please report it to the [https://discord.gg/KbsxgACkFS Wiki Discord].</p>"
	end

	if primary_attack.impactpower or secondary_attack.impactpower then
		wt[#wt+1] = "<br>'''Impact Power''' of "
		wt[#wt+1] = weapon.name
		wt[#wt+1] = ": "
		wt[#wt+1] = tostring(primary_attack.impactpower or secondary_attack.impactpower)
		wt[#wt+1] = "<br>Impact power helps with breaking crates, barricades or any other breakables in game. The higher the impact power the faster something breaks.<br>Impact power also affects shield blocking. If the attacking weapon's impact power is higher than shield's impact power, the shield holder staggers, and vice versa."
	end
end

local function list_to_string(l,separator)
	local ret = ""
	for i,v in ipairs(l) do
		if i~=1 then ret = ret..separator end
		ret = ret..v
	end
	return ret
end

local function combo_format(wt,attacks,type)
	if type == "Other" or type == "Block" then return end
	local first = ""

	wt[#wt+1] = "<span style='color:#eee8'>"
	wt[#wt+1] = type
	wt[#wt+1] = " Attack: </span>"

	for i,name in ipairs(attacks.order) do
		if i ~= 1 then wt[#wt+1] = "/" else first = name end
		wt[#wt+1] = attacks[name].damagetype
	end

	wt[#wt+1] = " dealing "

	for i,name in ipairs(attacks.order) do
		if i ~= 1 then wt[#wt+1] = "/" end
		wt[#wt+1] = attacks[name].combodamage
	end

	wt[#wt+1] = " damage on each swing, with impact zone: "
	wt[#wt+1] = list_to_string(attacks[first].impactzones, "/")
	wt[#wt+1] = "<br>"
end

local function attack_animation_times(wt,weapon,ability_name,attack_name)
	local attack = weapon.animationtimes[ability_name][attack_name]
	if attack["Windup"]            then wt[#wt+1] = string.format("Windup: %s<br>",                           attack["Windup"]) end
	if attack["Hit"]               then wt[#wt+1] = string.format("Hit: %s<br>",                              attack["Hit"]) end
	if attack["Second Windup"]     then wt[#wt+1] = string.format("Second Windup: %s<br>",                    attack["Second Windup"]) end
	if attack["Second Hit"]        then wt[#wt+1] = string.format("Second Hit: %s<br>",                       attack["Second Hit"]) end
	if attack["Recover"]           then wt[#wt+1] = string.format("&nbsp;&#10551; Recover: %s<br>",           attack["Recover"]) end
	if attack["Alternate Recover"] then wt[#wt+1] = string.format("&nbsp;&#10551; Alternate Recover: %s<br>", attack["Alternate Recover"]) end
	if attack["Finish"]            then wt[#wt+1] = string.format("&nbsp;&#10551; Finish: %s<br>",            attack["Finish"]) end
end

local function animation_format(wt, weapon, ability)
	if ability == "Other" or ability == "Block" then return end

	for i, ability_name in ipairs(weapon.abilities[ability].order) do
		wt[#wt+1] = ability.." Combo: "..ability_name.."<br><div style='display:inline-block;margin-left:20px'>"
		attack_animation_times(wt,weapon,ability,ability_name)
		wt[#wt+1] = "</div><br><br>"
	end
end

local function p_combo(wt, page_name)
	local weapon = UD.Utility[page_name]
	if not (weapon.abilities.Primary or weapon.abilities.Secondary or weapon.abilities.Special) then return end

	-- If is not ranged, and isn't shield, or at least isn't a Lantern Shield
	if not (weapon.types.Bow or weapon.types.Crossow or weapon.types.ThrowableStuff)
		and (not weapon.types.Shield or (weapon.name):match("Lantern Shield")) then

		wt[#wt+1] = "<h2>Attack Animation Time</h2>"

		for _,name in ipairs(weapon.abilities.order) do
			combo_format(wt,weapon.abilities[name],name)
		end

		if weapon.abilities.Primary or weapon.abilities.Secondary or weapon.abilities.Special then
			wt[#wt+1] = "<br><b>Attack Times</b><br><div style='display:inline-block;margin-left:20px'>"
			for _,name in ipairs(weapon.abilities.order) do
				animation_format(wt,weapon,name)
			end
			wt[#wt+1] = "</div>"
		end
		wt[#wt+1] = "Reference [[Attack Times]] for a glossary of the terms used above.<br>"
	end
end

local function p_block(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]
	if not weapon.impactresistance or weapon.impactresistance == "" then return end

	wt[#wt+1] = string.format("<h2>Block</h2>%s has a block ability with an impact resistance of <b>%s</b>.<br>To learn more about impact resistance see [[Impact Power]].", weapon.name, weapon.impactresistance)
end

local function p_action_move_slow_penalty(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]

	wt[#wt+1] = "<h2>Action Movement Slow</h2>When a player performs certain actions, such as attacking or defending themselves, they move slower."
	wt[#wt+1] = "<div style='display:inline-block;margin-left:20px'>"
	for _,ability in ipairs(weapon.abilities.order) do
		for _,attack in ipairs(weapon.abilities[ability].order) do
			-- There's a bug where Riposte values are recorded within "Other" abilities
			-- Since ripose is covered in Special abilities, we will skip it here
			if attack == "Riposte" then break end

			local movespeedadd = weapon.abilities[ability][attack].effects
								and weapon.abilities[ability][attack].effects.ItemActivateState
								and weapon.abilities[ability][attack].effects.ItemActivateState.rarity.Global["Move Speed Add"]
								or ""
			local movement_multiplier = weapon.abilities[ability][attack].movementmultiplier or ""
			local prepare_movement_multiplier = weapon.abilities[ability][attack].preparemovementmultiplier or ""

			wt[#wt+1] = "<span style='color:#eee8; font-size:110%; font-weight:bold'>"
			if ability == "Other" then wt[#wt+1] = attack else wt[#wt+1] = ability end
			wt[#wt+1] = "</span><br>"
			wt[#wt+1] = "<div style='display:inline-block;margin-left:20px'>"
			if #prepare_movement_multiplier > 1 then
				wt[#wt+1] = "<span style='color:#eee8'>Mid Attack:&nbsp;</span>x"
				wt[#wt+1] = movement_multiplier
				wt[#wt+1] = "<br><span style='color:#eee8'>Otherwise:&nbsp;</span>x"
				wt[#wt+1] = prepare_movement_multiplier
			elseif #movement_multiplier > 1 then
				wt[#wt+1] = "<span style='color:#eee8'>Always:&nbsp;</span>x"
				wt[#wt+1] = movement_multiplier
			else
				wt[#wt+1] = "<span style='color:#eee8'>Always:&nbsp;</span>"
				wt[#wt+1] = movespeedadd
			end
			wt[#wt+1] = "</div>"
			wt[#wt+1] = "<br><br>"

			-- Everything except attacks within Other has invariant move mults
			-- So after processessing the first attack, we break the loop
			if ability ~= "Other" then break end
		end
	end
	wt[#wt+1] = "</div>"
end

local function p_artifact(wt,weapon_name)
	local weapon = UD.Utility[weapon_name]
	if not weapon.isartifact then return end

	wt[#wt+1] = string.format("<h2>Artifact</h2><p>%s has a powerful [[Artifacts|Artifact]] named [[%s]].</p>", weapon.name, weapon.artifactname)
end

-- Draw anything relevant to the item for its individual page (not infobox or crafts)
function p.page(f)
	local page_name = f.args.name

	local wt = {}
	p_quest_information(wt, page_name)
	p_projectile(wt,page_name)
	p_damage_falloff(wt,page_name)
	p_hitbox(wt,page_name)
	p_combo(wt,page_name)
	p_block(wt,page_name)
	p_action_move_slow_penalty(wt,page_name)
	p_artifact(wt,page_name)
	return concat(wt)
end

return p

-- Tests
-- mw.log(p.draw_table({args={"Drink"}}))
-- mw.log(p.draw_table({args={"Consumable"}}))
-- mw.log(p.draw_table({args={"Installable"}}))
-- mw.log(p.draw_table({args={"LightSource"}}))
-- mw.log(p.draw_table({args={"Throwable"}}))
-- mw.log(p.draw_table({args={"MusicalInstrument"}}))
-- mw.log(p.draw_table({args={"Mining"}}))
-- mw.log(p.draw_table({args={"SoulHeart"}}))
-- mw.log(p.draw_table({args={"Food"}}))
-- mw.log(p.draw_table({args={"Tool"}}))

-- mw.log(p.page({args={name="Ale"}}))
-- mw.log(p.page({args={name="Francisca Axe"}}))