Files
PathOfBuilding/Modules/StatDescriber.lua
2021-02-04 15:05:35 -05:00

238 lines
7.9 KiB
Lua

-- Path of Building
--
-- Module: Stat Describer
-- Manages stat description files, and provides stat descriptions
--
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local s_format = string.format
local scopes = { }
local function getScope(scopeName)
if not scopes[scopeName] then
local scope = LoadModule("Data/StatDescriptions/"..scopeName)
scope.name = scopeName
if scope.parent then
local parentScope = getScope(scope.parent)
scope.scopeList = copyTable(parentScope.scopeList, true)
else
scope.scopeList = { }
end
t_insert(scope.scopeList, 1, scope)
scopes[scopeName] = scope
return scope
else
return scopes[scopeName]
end
end
local function matchLimit(lang, val)
for _, desc in ipairs(lang) do
local match = true
for i, limit in ipairs(desc.limit) do
if limit[1] == "!" then
if val[i].min == limit[2] then
match = false
break
end
elseif (limit[2] ~= "#" and val[i].min > limit[2]) or (limit[1] ~= "#" and val[i].min < limit[1]) then
match = false
break
end
end
if match then
return desc
end
end
end
local function applySpecial(val, spec)
if spec.k == "negate" then
val[spec.v].max, val[spec.v].min = -val[spec.v].min, -val[spec.v].max
elseif spec.k == "divide_by_two_0dp" then
val[spec.v].min = val[spec.v].min / 2
val[spec.v].max = val[spec.v].max / 2
elseif spec.k == "divide_by_ten_0dp" then
val[spec.v].min = val[spec.v].min / 10
val[spec.v].max = val[spec.v].max / 10
elseif spec.k == "divide_by_fifteen_0dp" then
val[spec.v].min = val[spec.v].min / 15
val[spec.v].max = val[spec.v].max / 15
elseif spec.k == "divide_by_twelve" then
val[spec.v].min = round(val[spec.v].min / 12, 1)
val[spec.v].max = round(val[spec.v].max / 12, 1)
val[spec.v].fmt = "g"
elseif spec.k == "divide_by_one_hundred" then
val[spec.v].min = round(val[spec.v].min / 100, 1)
val[spec.v].max = round(val[spec.v].max / 100, 1)
val[spec.v].fmt = "g"
elseif spec.k == "divide_by_one_hundred_2dp" then
val[spec.v].min = round(val[spec.v].min / 100, 2)
val[spec.v].max = round(val[spec.v].max / 100, 2)
val[spec.v].fmt = "g"
elseif spec.k == "divide_by_one_hundred_and_negate" then
val[spec.v].min = -round(val[spec.v].min / 100, 1)
val[spec.v].max = -round(val[spec.v].max / 100, 1)
val[spec.v].fmt = "g"
elseif spec.k == "divide_by_twenty_then_double_0dp" then -- O_O
val[spec.v].min = round(val[spec.v].min / 20) * 2
val[spec.v].max = round(val[spec.v].max / 20) * 2
elseif spec.k == "per_minute_to_per_second" then
val[spec.v].min = round(val[spec.v].min / 60, 1)
val[spec.v].max = round(val[spec.v].max / 60, 1)
val[spec.v].fmt = "g"
elseif spec.k == "per_minute_to_per_second_0dp" then
val[spec.v].min = val[spec.v].min / 60
val[spec.v].max = val[spec.v].max / 60
elseif spec.k == "per_minute_to_per_second_1dp" then
val[spec.v].min = round(val[spec.v].min / 60, 1)
val[spec.v].max = round(val[spec.v].max / 60, 1)
val[spec.v].fmt = "g"
elseif spec.k == "per_minute_to_per_second_2dp" then
val[spec.v].min = round(val[spec.v].min / 60, 2)
val[spec.v].max = round(val[spec.v].max / 60, 2)
val[spec.v].fmt = "g"
elseif spec.k == "per_minute_to_per_second_2dp_if_required" then
val[spec.v].min = round(val[spec.v].min / 60, 2)
val[spec.v].max = round(val[spec.v].max / 60, 2)
val[spec.v].fmt = "g"
elseif spec.k == "milliseconds_to_seconds" then
val[spec.v].min = val[spec.v].min / 1000
val[spec.v].max = val[spec.v].max / 1000
val[spec.v].fmt = "g"
elseif spec.k == "milliseconds_to_seconds_0dp" then
val[spec.v].min = val[spec.v].min / 1000
val[spec.v].max = val[spec.v].max / 1000
elseif spec.k == "milliseconds_to_seconds_1dp" then
val[spec.v].min = round(val[spec.v].min / 1000, 1)
val[spec.v].max = round(val[spec.v].max / 1000, 1)
val[spec.v].fmt = "g"
elseif spec.k == "milliseconds_to_seconds_2dp" then
val[spec.v].min = round(val[spec.v].min / 1000, 2)
val[spec.v].max = round(val[spec.v].max / 1000, 2)
val[spec.v].fmt = "g"
elseif spec.k == "milliseconds_to_seconds_2dp_if_required" then
val[spec.v].min = round(val[spec.v].min / 1000, 2)
val[spec.v].max = round(val[spec.v].max / 1000, 2)
val[spec.v].fmt = "g"
elseif spec.k == "deciseconds_to_seconds" then
val[spec.v].min = val[spec.v].min / 10
val[spec.v].max = val[spec.v].max / 10
val[spec.v].fmt = ".2f"
elseif spec.k == "30%_of_value" then
val[spec.v].min = val[spec.v].min * 0.3
val[spec.v].max = val[spec.v].max * 0.3
elseif spec.k == "60%_of_value" then
val[spec.v].min = val[spec.v].min * 0.6
val[spec.v].max = val[spec.v].max * 0.6
elseif spec.k == "multiplicative_damage_modifier" then
val[spec.v].min = 100 + val[spec.v].min
val[spec.v].max = 100 + val[spec.v].max
elseif spec.k == "multiplicative_permyriad_damage_modifier" then
val[spec.v].min = 100 + round(val[spec.v].min / 100, 1)
val[spec.v].max = 100 + round(val[spec.v].max / 100, 1)
val[spec.v].fmt = "g"
elseif spec.k == "multiply_by_four" then
val[spec.v].min = val[spec.v].min * 4
val[spec.v].max = val[spec.v].max * 4
elseif spec.k == "times_twenty" then
val[spec.v].min = val[spec.v].min * 20
val[spec.v].max = val[spec.v].max * 20
elseif spec.k == "reminderstring" or spec.k == "canonical_line" or spec.k == "_stat" then
elseif spec.k then
ConPrintf("Unknown description function: %s", spec.k)
end
end
return function(stats, scopeName)
local rootScope = getScope(scopeName)
-- Figure out which descriptions we need, and identify them by the first stat that they describe
local describeStats = { }
for s, v in pairs(stats) do
if (type(v) == "number" and v ~= 0) or (type(v) == "table" and (v.min ~= 0 or v.max ~= 0)) then
for depth, scope in ipairs(rootScope.scopeList) do
if scope[s] then
local descriptor = scope[scope[s]]
if descriptor.lang then
describeStats[descriptor.stats[1]] = { depth = depth, order = scope[s], description = scope[scope[s]] }
end
break
end
end
end
end
-- Sort them by depth/order
local descOrdered = { }
for s, descriptor in pairs(describeStats) do
t_insert(descOrdered, descriptor)
end
table.sort(descOrdered, function(a, b) if a.depth ~= b.depth then return a.depth > b.depth else return a.order < b.order end end)
-- Describe the stats
local out = { }
for _, descriptor in ipairs(descOrdered) do
local val = { }
for i, s in ipairs(descriptor.description.stats) do
if stats[s] then
if type(stats[s]) == "number" then
val[i] = { min = stats[s], max = stats[s] }
else
val[i] = stats[s]
end
else
val[i] = { min = 0, max = 0 }
end
val[i].fmt = "d"
end
local desc = matchLimit(descriptor.description.lang["English"], val)
if desc then
for _, spec in ipairs(desc) do
applySpecial(val, spec)
end
local statDesc = desc.text:gsub("{(%d)}", function(n)
local v = val[tonumber(n)+1]
if v.min == v.max then
return s_format("%"..v.fmt, v.min)
else
return s_format("(%"..v.fmt.."-%"..v.fmt..")", v.min, v.max)
end
end):gsub("{}", function()
local v = val[1]
if v.min == v.max then
return s_format("%"..v.fmt, v.min)
else
return s_format("(%"..v.fmt.."-%"..v.fmt..")", v.min, v.max)
end
end):gsub("{:%+?d}", function()
local v = val[1]
if v.min == v.max then
return s_format("%"..v.fmt, v.min)
else
return s_format("(%"..v.fmt.."-%"..v.fmt..")", v.min, v.max)
end
end):gsub("{(%d):(%+?)d}", function(n, fmt)
local v = val[tonumber(n)+1]
if v.min == v.max then
return s_format("%"..fmt..v.fmt, v.min)
elseif fmt == "+" then
if v.max < 0 then
return s_format("-(%d-%d)", -v.min, -v.max)
else
return s_format("+(%d-%d)", v.min, v.max)
end
else
return s_format("(%"..fmt..v.fmt.."-%"..fmt..v.fmt..")", v.min, v.max)
end
end):gsub("%%%%","%%")
for line in (statDesc.."\\n"):gmatch("([^\\]+)\\n") do
t_insert(out, line)
end
end
end
return out
end