Added modular description system (limited scope to 170 fully automateable items)
This commit is contained in:
1 parent
411be3d8a7
commit
d98397ec26
27 files changed
+3807
-3
No files matched your search
+45
-2
@@ -639,8 +639,18 @@ function EID:getDescriptionObj(Type, Variant, SubType, entity, checkModifiers)
|
||||
description.Name = EID:getObjectName(Type, Variant, SubType)
|
||||
description.Entity = entity or nil
|
||||
|
||||
local tableEntry = EID:getDescriptionData(Type, Variant, SubType)
|
||||
description.Description = tableEntry and tableEntry[3] or EID:getXMLDescription(Type, Variant, SubType)
|
||||
local generatedModularDesc = false
|
||||
if EID.FullyAutomatedDescriptions[description.fullItemString] then
|
||||
local success, modularDescription = pcall(EID.GenerateDescription, nil, description)
|
||||
if success and modularDescription and modularDescription ~= "" then
|
||||
description.Description = modularDescription
|
||||
generatedModularDesc = true
|
||||
end
|
||||
end
|
||||
if not generatedModularDesc then
|
||||
local tableEntry = EID:getDescriptionData(Type, Variant, SubType)
|
||||
description.Description = tableEntry and tableEntry[3] or EID:getXMLDescription(Type, Variant, SubType)
|
||||
end
|
||||
|
||||
description.Transformation = EID:getTransformation(Type, Variant, SubType)
|
||||
|
||||
@@ -1625,6 +1635,39 @@ function EID:removeDescriptionModifier(modifierName)
|
||||
end
|
||||
end
|
||||
|
||||
---Adds Modular data modifier
|
||||
---@param modifierName string
|
||||
---@param condition fun(descObj: EID_DescObj): boolean
|
||||
---@param callback fun(descObj: EID_DescObj): EID_DescObj
|
||||
---@param position? integer
|
||||
function EID:addModularDataModifier(modifierName, condition, callback, position)
|
||||
position = position or #EID.ModularDataModifiers + 1
|
||||
for _, v in ipairs(EID.ModularDataModifiers) do
|
||||
if v["name"] == modifierName then
|
||||
v["condition"] = condition
|
||||
v["callback"] = callback
|
||||
return
|
||||
end
|
||||
end
|
||||
table.insert(EID.ModularDataModifiers, position, {
|
||||
name = modifierName,
|
||||
condition = condition,
|
||||
callback = callback
|
||||
})
|
||||
end
|
||||
|
||||
---Removes a Modular data modifier
|
||||
---@see EID.addModularDataModifier
|
||||
---@param modifierName string
|
||||
function EID:removeModularDataModifier(modifierName)
|
||||
for i, v in ipairs(EID.ModularDataModifiers) do
|
||||
if v["name"] == modifierName then
|
||||
table.remove(EID.ModularDataModifiers, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Interpolates between 2 KColors with a given fraction.
|
||||
---@param kColor1 KColor
|
||||
---@param kColor2 KColor
|
||||
|
||||
@@ -93,7 +93,9 @@ end
|
||||
|
||||
--- Attempts to load all language packs from a given game version
|
||||
function EID:LoadLanguagePacks(gameVersion)
|
||||
--transformation infos
|
||||
--modular item informations
|
||||
local _, _ = pcall(require, "descriptions." .. gameVersion .. ".item_data")
|
||||
--transformation informations
|
||||
local _, _ = pcall(require, "descriptions." .. gameVersion .. ".transformations")
|
||||
--load languages
|
||||
for _, languageCode in ipairs(EID.Languages) do
|
||||
@@ -108,6 +110,12 @@ function EID:LoadLanguagePacks(gameVersion)
|
||||
EID:WriteErrorMsg("Loading Languagepack '" .. languageCode .. "' (Names) failed: " ..
|
||||
tostring(err))
|
||||
end
|
||||
-- load modular descriptions
|
||||
wasSuccessful, err = pcall(require, "descriptions.modular." .. languageCode)
|
||||
if not wasSuccessful and not string.find(err, "not found") then
|
||||
EID:WriteErrorMsg("Loading Languagepack '" .. languageCode .. "' (modular) failed: " ..
|
||||
tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Special behavior functions that alter the data of modular descriptions
|
||||
local function HealthUpCondition(_, descObj)
|
||||
if not EID.Config["DynamicHealthUps"] then return descObj end
|
||||
|
||||
local closestPlayer = EID:ClosestPlayerTo(descObj.Entity)
|
||||
local playerType = closestPlayer:GetPlayerType()
|
||||
local heartType = EID.CharacterToHeartType[playerType] or "Red"
|
||||
return heartType ~= "Red"
|
||||
end
|
||||
|
||||
|
||||
-- Replaces the data entry defined as the key, with the one defined as the value. If the new data entry exists already, its value gets added to the old one
|
||||
local healthTypeReplacements = {
|
||||
Soul = { RedHeart = "SoulHeart", FullHealth = "", HealingRed = "", EmptyHeart = "SoulHeart", RottenHeart = "" },
|
||||
Black = { RedHeart = "BlackHeart", FullHealth = "", HealingRed = "", EmptyHeart = "BlackHeart", RottenHeart = "" },
|
||||
-- TODO: Potato Peeler and Abaddon do remove coin hearts in rep+. Maybe hide CoinHeart data, if max is reached?
|
||||
Coin = { RedHeart = "CoinHeart", HealingRed = "HealingCoin", EmptyHeart = "EmptyCoinHeart", SoulHeart = "", BlackHeart = "" },
|
||||
None = {
|
||||
RedHeart = "",
|
||||
CoinHeart = "",
|
||||
FullHealth = "",
|
||||
HealingRed = "",
|
||||
HealingCoin = "",
|
||||
SoulHeart = "",
|
||||
BlackHeart = "",
|
||||
BoneHeart = "",
|
||||
EternalHeart = "",
|
||||
GoldenHeart = "",
|
||||
RottenHeart = "",
|
||||
BrokenHeart = "",
|
||||
EmptyHeart = "",
|
||||
EmptyCoinHeart = "",
|
||||
},
|
||||
}
|
||||
|
||||
local function HealthUpCallback(itemDataTable, descObj)
|
||||
local closestPlayer = EID:ClosestPlayerTo(descObj.Entity)
|
||||
local playerType = closestPlayer:GetPlayerType()
|
||||
local heartType = EID.CharacterToHeartType[playerType] or "Red"
|
||||
if not healthTypeReplacements[heartType] then return itemDataTable end
|
||||
for oldDataName, newDataName in pairs(healthTypeReplacements[heartType]) do
|
||||
local oldValue = itemDataTable[oldDataName]
|
||||
itemDataTable[oldDataName] = nil -- remove old data entry
|
||||
if oldValue and newDataName ~= "" then
|
||||
local existingValue = type(itemDataTable[newDataName]) == "number" and tonumber(itemDataTable[newDataName]) or 0
|
||||
itemDataTable[newDataName] = oldValue + existingValue
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Highlight changes in text with player icon
|
||||
return itemDataTable
|
||||
end
|
||||
|
||||
EID:addModularDataModifier("HealthToX", HealthUpCondition, HealthUpCallback)
|
||||
@@ -0,0 +1,588 @@
|
||||
-- Table defining the construction bahavior of modular descriptions.
|
||||
-- A modular description is constructed by looking up the EID.ItemData entry for the given item id (for example: 5.100.123).
|
||||
-- Each item data coresponds to an entry in the "EID.ModuleBehaviors" table below, which defines how the description is constructed.
|
||||
-- If no BehaviorFunc is defined, the default constructor is used, which takes the "EID.descriptions[<CurrentLanguage>].ModularDescriptions" entry with the
|
||||
-- same name as the ModuleBehavior, and tries to use the provided data from the ItemData entry to fill the {value} placeholder in the description module text
|
||||
|
||||
---- Argument -----
|
||||
-- The key is the name of the module. It should be the exact same as the data entry in EID.ItemData and the entry of the translation text to be used (in EID.descriptions["en_us"].ModularDescriptions)
|
||||
-- Priority defines the order of which the modules are added to the description. Positive values get added before the "Additional Informations". Negative priority modules get added afterwards
|
||||
-- Arrow indicates if an up/down arrow should be shown at the front of a description for positive/negative values.
|
||||
-- InvertArrow inverts the Arrow direction, pointing up for negative values and down for positive.
|
||||
-- Icon indicates what icon should be shown before the modular description.
|
||||
-- IsMultiplier indicates if the modular description describes a multiplier (e.g. TearsMultiplier) and its value should be formatted with an "x" instead of "+/-".
|
||||
-- HideSign indicates if the sign (+/-/x) should be hidden for the modular description value
|
||||
-- BehaviorFunc can be used to define a custom behavior for building the modular description. its a function that takes 3 arguments and should return the altered description.
|
||||
EID.ModuleBehaviors = {
|
||||
["SingleUseInfo"] = { Priority = 11000, HideSign = true },
|
||||
-- Player Modules
|
||||
["TearsMultiplier"] = { Priority = 9990, Arrow = true, Icon = "{{Tears}}", IsMultiplier = true },
|
||||
["Tears"] = { Priority = 9980, Arrow = true, Icon = "{{Tears}}" },
|
||||
["TearDelayMultiplier"] = { Priority = 9970, Arrow = true, Icon = "{{Tears}}", IsMultiplier = true, InvertArrow = true },
|
||||
["TearDelay"] = { Priority = 9970, Arrow = true, Icon = "{{Tears}}" },
|
||||
["FireRateMultiplier"] = { Priority = 9960, Arrow = true, Icon = "{{Tears}}", IsMultiplier = true },
|
||||
["FireRate"] = { Priority = 9950, Arrow = true, Icon = "{{Tears}}" },
|
||||
["TearHeight"] = { Priority = 9940, Arrow = true },
|
||||
["TearSizeMultiplier"] = { Priority = 9930, Arrow = true, Icon = "{{Tearsize}}", IsMultiplier = true },
|
||||
["TearSize"] = { Priority = 9920, Arrow = true, Icon = "{{Tearsize}}" },
|
||||
["DamageMultiplier"] = { Priority = 9890, Arrow = true, Icon = "{{Damage}}", IsMultiplier = true },
|
||||
["Damage"] = { Priority = 9880, Arrow = true, Icon = "{{Damage}}" },
|
||||
["DamageTemp"] = { Priority = 9870, Arrow = true, Icon = "{{Timer}}" },
|
||||
["BombDamage"] = { Priority = 9860, Arrow = true, Icon = "{{Bomb}}", IsMultiplier = true },
|
||||
["Speed"] = { Priority = 9790, Arrow = true, Icon = "{{Speed}}" },
|
||||
["RangeMultiplier"] = { Priority = 9780, Arrow = true, Icon = "{{Range}}", IsMultiplier = true },
|
||||
["Range"] = { Priority = 9770, Arrow = true, Icon = "{{Range}}" },
|
||||
["ShotSpeedMultiplier"] = { Priority = 9690, Arrow = true, Icon = "{{Shotspeed}}", IsMultiplier = true },
|
||||
["ShotSpeed"] = { Priority = 9680, Arrow = true, Icon = "{{Shotspeed}}" },
|
||||
["Luck"] = { Priority = 9590, Arrow = true, Icon = "{{Luck}}" },
|
||||
["Life"] = { Priority = 9580, Arrow = true },
|
||||
["SizeUp"] = { Priority = 9570, Arrow = true },
|
||||
["SizeDown"] = { Priority = 9560, Arrow = true },
|
||||
["Flight"] = { Priority = 9550 },
|
||||
["Invincibility"] = { Priority = 9540 },
|
||||
["RandomStatUp"] = { Priority = 9530, HideSign = true },
|
||||
["RandomStatDown"] = { Priority = 9520, HideSign = true },
|
||||
["CantShoot"] = { Priority = 9510, HideSign = true },
|
||||
["PlayerContactDamage"] = { Priority = 9500, HideSign = true },
|
||||
|
||||
-- Health related
|
||||
["RedHeart"] = { Priority = 8990, Arrow = true, Icon = "{{Heart}}" },
|
||||
["CoinHeart"] = { Priority = 8990, Arrow = true, Icon = "{{CoinHeart}}" },
|
||||
["FullHealth"] = { Priority = 8980, Icon = "{{HealingRed}}", HideSign = true },
|
||||
["HealingRed"] = { Priority = 8970, Icon = "{{HealingRed}}", HideSign = true },
|
||||
["HealingCoin"] = { Priority = 8970, Icon = "{{HealingCoin}}", HideSign = true },
|
||||
["SoulHeart"] = { Priority = 8960, Icon = "{{SoulHeart}}" },
|
||||
["BlackHeart"] = { Priority = 8950, Icon = "{{BlackHeart}}" },
|
||||
["BoneHeart"] = { Priority = 8940, Icon = "{{EmptyBoneHeart}}" },
|
||||
["EternalHeart"] = { Priority = 8930, Icon = "{{EternalHeart}}" },
|
||||
["GoldenHeart"] = { Priority = 8920, Icon = "{{GoldenHeart}}" },
|
||||
["RottenHeart"] = { Priority = 8910, Icon = "{{RottenHeart}}" },
|
||||
["BrokenHeart"] = { Priority = 8910, Icon = "{{BrokenHeart}}" },
|
||||
["EmptyHeart"] = { Priority = 8900, Icon = "{{EmptyHeart}}" },
|
||||
["EmptyCoinHeart"] = { Priority = 8900, Icon = "{{EmptyCoinHeart}}" },
|
||||
|
||||
-- Pickups / Spawns
|
||||
["Coin"] = { Priority = 7990, Icon = "{{Coin}}" },
|
||||
["Bomb"] = { Priority = 7980, Icon = "{{Bomb}}" },
|
||||
["Key"] = { Priority = 7970, Icon = "{{Key}}" },
|
||||
["CoinBombKey"] = { Priority = 7960, },
|
||||
|
||||
-- Room chances
|
||||
["AngelDevilChance"] = { Priority = 6990, Icon = "{{AngelDevilChance}}" },
|
||||
["DevilChance"] = { Priority = 6980, Icon = "{{DevilChance}}" },
|
||||
["AngelChance"] = { Priority = 6970, Icon = "{{AngelChance}}" },
|
||||
["PlanetariumChance"] = { Priority = 6960, Icon = "{{PlanetariumChance}}" },
|
||||
|
||||
-- Familiars
|
||||
["FlyOrbital"] = { Priority = 5990 },
|
||||
["Orbital"] = { Priority = 5980 },
|
||||
["MimicMovement"] = { Priority = 5970, HideSign = true },
|
||||
["BlockProjectiles"] = { Priority = -5950, HideSign = true },
|
||||
["DamagePerTear"] = { Priority = -5960, HideSign = true },
|
||||
["DamagePerShot"] = { Priority = -5970, HideSign = true },
|
||||
["DamagePerSecond"] = { Priority = -5980, HideSign = true },
|
||||
["ContactDamagePerSecond"] = { Priority = -5990, HideSign = true },
|
||||
-- Miscellaneous
|
||||
["NoEffect"] = { Priority = 5000, HideSign = true },
|
||||
["DamageToAllEnemies"] = { Priority = 5010, HideSign = true },
|
||||
|
||||
-- Dynamic Spawns
|
||||
["Spawns"] = {
|
||||
Priority = 5500,
|
||||
HideSign = true,
|
||||
Icon = {
|
||||
RandomHeart = "{{UnknownHeart}}",
|
||||
RandomTrinket = "{{Trinket}}",
|
||||
RedHeart = "{{Heart}}",
|
||||
SoulHeart = "{{SoulHeart}}",
|
||||
BlackHeart = "{{BlackHeart}}",
|
||||
BoneHeart = "{{EmptyBoneHeart}}",
|
||||
EternalHeart = "{{EternalHeart}}",
|
||||
GoldenHeart = "{{GoldenHeart}}",
|
||||
EmptyHeart = "{{EmptyHeart}}",
|
||||
Bomb = "{{Bomb}}",
|
||||
Key = "{{Key}}",
|
||||
Coin = "{{Coin}}",
|
||||
RandomCoin = "{{Coin}}",
|
||||
Card = "{{Card}}",
|
||||
Pill = "{{Pill}}",
|
||||
Rune = "{{Rune}}",
|
||||
Battery = "{{Battery}}",
|
||||
},
|
||||
BehaviorFunc = function(moduleBehaviorEntry, itemDataTableEntry, description, indent)
|
||||
local moduleDescription = ""
|
||||
for subModuleID, subValue in pairs(itemDataTableEntry) do
|
||||
local subTextFragment = EID:getDescriptionEntry("ModularDescriptions", moduleBehaviorEntry.Name)[subModuleID]
|
||||
if not subTextFragment then
|
||||
print("[ERROR] Spawn translation not found!", subModuleID)
|
||||
end
|
||||
local subTextEntry = EID:FormatModuleDescription(moduleBehaviorEntry, subValue, subTextFragment)
|
||||
-- add icon of subentry to the beginning of the text
|
||||
local iconString = ""
|
||||
if moduleBehaviorEntry.Icon and type(moduleBehaviorEntry.Icon) == "table" and moduleBehaviorEntry.Icon[subModuleID] then
|
||||
iconString = moduleBehaviorEntry.Icon[subModuleID] .. " "
|
||||
end
|
||||
moduleDescription = moduleDescription .. iconString .. subTextEntry
|
||||
end
|
||||
|
||||
return EID:AddModularTextLineToDescription(description, moduleDescription, indent)
|
||||
end
|
||||
},
|
||||
["TearEffect"] = {
|
||||
Priority = 5510,
|
||||
BehaviorFunc = function(moduleBehaviorEntry, itemDataTableEntry, description)
|
||||
-- support single and multiple tear effects by turning single entries into a table
|
||||
if type(itemDataTableEntry) ~= "table" then
|
||||
itemDataTableEntry = {itemDataTableEntry}
|
||||
end
|
||||
for _, effect in ipairs(itemDataTableEntry) do
|
||||
local effectText = EID:getDescriptionEntry("ModularDescriptions", moduleBehaviorEntry.Name)[effect]
|
||||
description = EID:AddModularTextLineToDescription(description, effectText, indent)
|
||||
end
|
||||
return description
|
||||
end
|
||||
},
|
||||
["FullMapping"] = {
|
||||
Priority = 5520,
|
||||
BehaviorFunc = function(moduleBehaviorEntry, itemDataTableEntry, description, indent)
|
||||
local baseText = EID:getDescriptionEntry("ModularDescriptions", moduleBehaviorEntry.Name)["BaseDesc"]
|
||||
local valueText = EID:getDescriptionEntry("ModularDescriptions", moduleBehaviorEntry.Name)[itemDataTableEntry] or ""
|
||||
if not baseText then
|
||||
print("[ERROR] BaseDesc translation not found!", effect)
|
||||
else
|
||||
baseText = baseText:gsub("{exception}", valueText):gsub("%s+$", "")
|
||||
return EID:AddModularTextLineToDescription(description, baseText, indent)
|
||||
end
|
||||
return description
|
||||
end
|
||||
},
|
||||
["LuckChance"] = {
|
||||
Priority = -9950,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
-- build max luck information line
|
||||
local maxLuckText = EID:getDescriptionEntry("ModularDescriptions", "MaxLuck")
|
||||
local maxLuckPercent = EID:PrettyPrintValue((itemDataTableEntry.Maximum or 1) * 100)
|
||||
maxLuckText = maxLuckText:gsub("{max}", maxLuckPercent)
|
||||
local maxLuckVal = EID:CalculateChanceWithLuck(itemDataTableEntry, true)
|
||||
if EID.player and EID.player.Luck >= maxLuckVal then
|
||||
maxLuckText = "" -- dont show max luck info if player already has enough luck
|
||||
else
|
||||
maxLuckText = maxLuckText:gsub("{value}", maxLuckVal)
|
||||
end
|
||||
-- append max luck info after the affected line
|
||||
local _, chanceVarPos = string.find(description, "{VAR:LUCKCHANCE}", 1, true)
|
||||
local nextLineStart = string.find(description, "#", chanceVarPos or 1, true)
|
||||
-- add max Luck description line right after the affected line, then add the rest
|
||||
local newDesc = EID:AddModularTextLineToDescription(string.sub(description, 0 , nextLineStart), maxLuckText, indent)
|
||||
newDesc = newDesc .. string.sub(description, nextLineStart or 0)
|
||||
-- replace variable with calculated chance
|
||||
newDesc = newDesc:gsub("{VAR:LUCKCHANCE}", EID:CalculateChanceWithLuck(itemDataTableEntry, false))
|
||||
return newDesc
|
||||
end
|
||||
},
|
||||
["EffectList"] = {
|
||||
-- Generic list of Effects to be placed inside the description, without any title text
|
||||
Priority = -9940,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "EffectList", nil, indent)
|
||||
end
|
||||
},
|
||||
["RightEye"] = {
|
||||
Priority = 9500,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "RightEye", nil, indent)
|
||||
end
|
||||
},
|
||||
["LeftEye"] = {
|
||||
Priority = 9500,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "LeftEye", nil, indent)
|
||||
end
|
||||
},
|
||||
["RoomEffect"] = {
|
||||
Priority = -9930,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "RoomEffect", nil, indent)
|
||||
end
|
||||
},
|
||||
["TimedEffect"] = {
|
||||
Priority = -9920,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "TimedEffect", "Duration", indent)
|
||||
end
|
||||
},
|
||||
["OnUseEffect"] = {
|
||||
Priority = -9910,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "OnUseEffect", nil, indent)
|
||||
end
|
||||
},
|
||||
["HeldEffect"] = {
|
||||
Priority = -9900,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
indent = indent or 0
|
||||
return EID:ApplyModularNestedDescription(itemDataTableEntry, description, "HeldEffect", nil, indent)
|
||||
end
|
||||
},
|
||||
["ItemDescription"] = {
|
||||
-- Adds the description of another item
|
||||
Priority = -9980,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description, indent)
|
||||
-- support single and multiple tear effects by turning single entries into a table
|
||||
if type(itemDataTableEntry) ~= "table" then
|
||||
itemDataTableEntry = {itemDataTableEntry}
|
||||
end
|
||||
for _, itemID in ipairs(itemDataTableEntry) do
|
||||
local itemDesc = EID:GenerateDescription({fullItemString = itemID})
|
||||
if itemDesc and itemDesc ~= "" then
|
||||
local numReplaced = 0
|
||||
description, numReplaced = description:gsub("{VAR:ITEMDESCRIPTION}", itemDesc)
|
||||
if numReplaced == 0 then
|
||||
description = EID:AddModularTextLineToDescription(description, itemDesc, indent)
|
||||
end
|
||||
end
|
||||
end
|
||||
return description
|
||||
end
|
||||
},
|
||||
-- Variable-like entries that require special handling
|
||||
["Variables"] = {
|
||||
Priority = -9999,
|
||||
BehaviorFunc = function(_, itemDataTableEntry, description)
|
||||
-- Function to replace VAR markup with their specific content from the variables table
|
||||
description = description:gsub("{VAR:(.-)}", function(matchString)
|
||||
-- Try to capture main text and optional trailing numbers used to add multiple variables of the same type
|
||||
local variableName, number = matchString:match("^(.-)(%d*)$")
|
||||
-- if the variablename is a number, use generic handler
|
||||
local handlerName = variableName == "" and "FORMATTED" or variableName
|
||||
-- if a function for the given variable exists, use it and return its value
|
||||
local variableData = itemDataTableEntry[variableName] or
|
||||
itemDataTableEntry[tonumber(number)] or itemDataTableEntry[matchString]
|
||||
if EID.DynamicVariableHandlers[handlerName] and variableData then
|
||||
return EID.DynamicVariableHandlers[handlerName](variableData)
|
||||
else
|
||||
if not EID.DynamicVariableHandlers[handlerName] then
|
||||
print("[ERROR] No handler found for variable '"..tostring(handlerName).."' CurDescription:", description)
|
||||
elseif not variableData then
|
||||
print("[ERROR] No data-entry found for variable '"..tostring(variableName).."' CurDescription:", description)
|
||||
end
|
||||
end
|
||||
-- return nil if no function was found, to not replace the Variable string
|
||||
end)
|
||||
return description
|
||||
end
|
||||
},
|
||||
}
|
||||
|
||||
-- Metatable to automatically set the Name field of ModuleBehaviors when a new entry is added
|
||||
for k,v in pairs(EID.ModuleBehaviors) do v.Name = k end
|
||||
setmetatable(EID.ModuleBehaviors, { __newindex = function(_, key, value)
|
||||
if type(value) == "table" then rawset(value, "Name", key) end
|
||||
rawset(self, key, value) end
|
||||
})
|
||||
|
||||
EID.ModularDataModifiers = {}
|
||||
|
||||
-- Formats a number to a string, removing trailing zeros after the decimal point
|
||||
function EID:PrettyPrintValue(value)
|
||||
return string.format("%.2f", value):gsub("%.?0+$", "")
|
||||
end
|
||||
|
||||
-- Calculates a chance value based on the given luck and variable data
|
||||
function EID:CalculateChanceWithLuck(variableData, useMax)
|
||||
local luckValue = EID.player and EID.player.Luck or 0
|
||||
local top = variableData.Top or 1
|
||||
local bottom = variableData.Bottom or 1
|
||||
local multiplier = variableData.Multiplier or 1
|
||||
local maximum = variableData.Maximum or 1
|
||||
local luckFunc = variableData.Formula and EID.LuckFormulaPresets[variableData.Formula] or
|
||||
EID.LuckFormulaPresets.Default
|
||||
if useMax then
|
||||
local result = luckFunc.MaxFunc(top, bottom, multiplier, maximum)
|
||||
return math.floor(result)
|
||||
else
|
||||
local result = math.min(luckFunc.ChanceFunc(luckValue, top, bottom, multiplier), maximum)
|
||||
-- if the luck value exceeds the maximum, it can produce negative values. we handle it as 100%
|
||||
result = result >= 0 and result or maximum
|
||||
-- Colorize chance, if different than default
|
||||
result = EID:PrettyPrintValue(result * 100)
|
||||
return luckValue ~= 0 and "{{BlinkGreen}}".. result .."{{CR}}" or result
|
||||
end
|
||||
end
|
||||
|
||||
EID.DynamicVariableHandlers = {
|
||||
FORMATTED = function(variableData)
|
||||
if type(variableData) == "table" then return tostring(variableData[1]) .. "-" .. tostring(variableData[2]) -- interpret a table as a range of numbers
|
||||
elseif type(variableData) == "number" then return EID:PrettyPrintValue(variableData)
|
||||
else return tostring(variableData) end -- return string otherwise
|
||||
end,
|
||||
RANGE = function(variableData) return tostring(variableData[1]) .. "-" .. tostring(variableData[2]) end,
|
||||
TEXT = function(variableData) return variableData end,
|
||||
}
|
||||
|
||||
|
||||
-- Formulas to calculate effect trigger change changes based on the player luck.
|
||||
-- Each entry requires 2 functions. One calculates the chance based on the given luck. The second calculates the required luck to reach the maximum luck
|
||||
EID.LuckFormulaPresets = {
|
||||
-- default calculation using most common calculation method. Example for Apple! = 1/(15-Luck) (6.66% at 0 Luck) = top = 1, bottom = 15, multiplier = 1, maximum = 1
|
||||
Default = {
|
||||
-- source: Items listed in https://bindingofisaacrebirth.wiki.gg/wiki/Luck
|
||||
ChanceFunc = function(luckValue, top, bottom, multiplier)
|
||||
return top / (bottom - (luckValue * multiplier))
|
||||
end,
|
||||
-- created by solving top formula for Luck (L)
|
||||
-- T / (B - (L * M)) = MAX | do * (B - (L * M))
|
||||
-- T = MAX * (B - (L * M)) | do * 1/MAX
|
||||
-- T / MAX = B - L * M | do - B and * -1 and * 1/M
|
||||
-- (-T / MAX + B) / M = L
|
||||
MaxFunc = function(top, bottom, multiplier, maximum)
|
||||
return (-top / maximum + bottom) / multiplier
|
||||
end
|
||||
},
|
||||
Additive = {
|
||||
ChanceFunc = function(luckValue, top, bottom, multiplier)
|
||||
return top / bottom + (luckValue * multiplier)
|
||||
end,
|
||||
-- T/B + L*M = MAX | - T/B
|
||||
-- L*M = MAX - T/B | / M
|
||||
-- L = (MAX - T/B) / M
|
||||
MaxFunc = function(top, bottom, multiplier, maximum)
|
||||
return (maximum - top/bottom) / multiplier
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
-- Handles a modular description entry by calling its BehaviorFunc or the default generator
|
||||
function EID:GenerateTextFromModuleEntry(moduleBehaviorEntry, itemDataTableEntry, description, indent)
|
||||
if not moduleBehaviorEntry or not itemDataTableEntry then
|
||||
print("[ERROR] (EID:HandleModuleEntry) Missing ModuleBehavior entry or ItemData table entry!", moduleBehaviorEntry, itemDataTableEntry, description)
|
||||
return description
|
||||
end
|
||||
if type(moduleBehaviorEntry.BehaviorFunc) == "function" then
|
||||
local success, modifiedDescription = pcall(moduleBehaviorEntry.BehaviorFunc, moduleBehaviorEntry, itemDataTableEntry, description, indent)
|
||||
if not success then
|
||||
-- Make the error messages of BehaviorFuncs more verbose, by using pcall
|
||||
error("[ERROR] Error in BehaviorFunc of entry '" .. moduleBehaviorEntry.Name.."': ".. tostring(modifiedDescription))
|
||||
end
|
||||
return success and modifiedDescription or description
|
||||
end
|
||||
-- default behavior
|
||||
return EID:GenerateDefaultModuleDescription(moduleBehaviorEntry, itemDataTableEntry, description, indent)
|
||||
end
|
||||
|
||||
-- Applies a modular description for a nested modular entry (e.g. TimedEffect or RoomEffect)
|
||||
-- If the moduleName is given, it will try to replace {VAR:moduleName}
|
||||
-- If applyValueWithName is given, it will replace {value} in the module description with the value of the given name
|
||||
function EID:ApplyModularNestedDescription(itemDataTableEntry, description, moduleName, applyValueWithName, indent)
|
||||
-- Get base text fragment for the module
|
||||
local moduleText = EID:getDescriptionEntry("ModularDescriptions", moduleName)
|
||||
local textFragment = moduleText and (moduleText .. "#") or ""
|
||||
|
||||
-- replace value if a specific value name is given
|
||||
if applyValueWithName then
|
||||
local value = EID:PrettyPrintValue(itemDataTableEntry[applyValueWithName] or "UNDEFINED")
|
||||
textFragment = textFragment:gsub("{value}", value)
|
||||
if tonumber(value) then
|
||||
textFragment = EID:TryPluralizeString(textFragment, tonumber(value))
|
||||
end
|
||||
end
|
||||
-- add subdata entries
|
||||
local sortedmodules = EID:GetSortedModularDescriptionEntries(itemDataTableEntry, true)
|
||||
for _, moduleBehaviorEntry in ipairs(sortedmodules) do
|
||||
local moduleID = moduleBehaviorEntry.Name
|
||||
if EID:ValidateModuleEntry(moduleID, "located in a '"..moduleName.."' element") then
|
||||
if not (applyValueWithName and moduleID == applyValueWithName) then
|
||||
textFragment = EID:GenerateTextFromModuleEntry(EID.ModuleBehaviors[moduleID], itemDataTableEntry[moduleID], textFragment, (indent or 0) + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- remove leading/trailing # and multiple # in a row. Makes translation more readable in some places, if a # is explicitly added
|
||||
textFragment = textFragment:gsub("#$", ""):gsub("^#", ""):gsub("#+", "#")
|
||||
-- Escape % signs in text, to prevent pattern-matching errors
|
||||
textFragment = textFragment:gsub("%%", "%%%%")
|
||||
|
||||
-- Try replace inline variable
|
||||
local numReplaced = 0
|
||||
description, numReplaced = description:gsub("{VAR:"..string.upper(moduleName).."}", textFragment)
|
||||
if numReplaced == 0 then
|
||||
return EID:AddModularTextLineToDescription(description, textFragment, indent)
|
||||
end
|
||||
return description
|
||||
end
|
||||
|
||||
function EID:GetSortedModularDescriptionEntries(itemDataTable, combineTables, validateEntries, debugInfo)
|
||||
-- sort modules of selected Item by priority
|
||||
local sortedModulesPositivePrio = {}
|
||||
local sortedModulesNegativePrio = {}
|
||||
for moduleID, _ in pairs(itemDataTable) do
|
||||
if validateEntries and EID:ValidateModuleEntry(moduleID, "in "..debugInfo) or EID.ModuleBehaviors[moduleID] then
|
||||
if EID.ModuleBehaviors[moduleID].Priority > 0 then
|
||||
table.insert(sortedModulesPositivePrio, EID.ModuleBehaviors[moduleID])
|
||||
else
|
||||
table.insert(sortedModulesNegativePrio, EID.ModuleBehaviors[moduleID])
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(sortedModulesPositivePrio, function(a, b) return a.Priority > b.Priority end)
|
||||
table.sort(sortedModulesNegativePrio, function(a, b) return a.Priority > b.Priority end)
|
||||
if combineTables then
|
||||
return EID:ConcatTables(sortedModulesPositivePrio, sortedModulesNegativePrio)
|
||||
end
|
||||
return sortedModulesPositivePrio, sortedModulesNegativePrio
|
||||
end
|
||||
|
||||
---Apply Description Modifier to a given description object
|
||||
---@param itemDataTable table
|
||||
---@param descObj EID_DescObj
|
||||
---@return table
|
||||
function EID:ApplyModularDataModifier(itemDataTable, descObj)
|
||||
local dataCopy = EID:CopyTable(itemDataTable)
|
||||
for _,modifier in ipairs(EID.ModularDataModifiers) do
|
||||
local result = modifier.condition(dataCopy, descObj)
|
||||
if type(result) == "table" then
|
||||
for _,callback in ipairs(result) do
|
||||
dataCopy = callback(dataCopy, descObj)
|
||||
end
|
||||
elseif result then
|
||||
dataCopy = modifier.callback(dataCopy, descObj)
|
||||
end
|
||||
end
|
||||
return dataCopy
|
||||
end
|
||||
|
||||
-- Tries to generate a description for an item based on its modules defined in EID.ItemData
|
||||
-- Returns an empty string if no modules are defined for the item
|
||||
function EID:GenerateDescription(descriptionObj)
|
||||
local itemID = descriptionObj.fullItemString
|
||||
if not itemID then return nil end
|
||||
if EID.descriptions[EID:getLanguage()].DisableModularDescriptions then return nil end
|
||||
|
||||
local itemDataTable = EID.ItemData[itemID]
|
||||
-- return empty string if no ItemData are defined
|
||||
if not itemDataTable then
|
||||
return ""
|
||||
end
|
||||
|
||||
itemDataTable = EID:ApplyModularDataModifier(itemDataTable, descriptionObj)
|
||||
|
||||
local sortedModulesPositivePrio, sortedModulesNegativePrio = EID:GetSortedModularDescriptionEntries(itemDataTable, false, true, itemID)
|
||||
|
||||
local description = ""
|
||||
|
||||
-- Add module information with positive priority
|
||||
for _, moduleBehaviorEntry in ipairs(sortedModulesPositivePrio) do
|
||||
description = EID:GenerateTextFromModuleEntry(moduleBehaviorEntry, itemDataTable[moduleBehaviorEntry.Name], description, 0)
|
||||
end
|
||||
|
||||
-- add linebreak between additional information and negative priority modules
|
||||
if not string.find(description, "#$") then
|
||||
description = description .. "#"
|
||||
end
|
||||
|
||||
-- Add module information with negative priority
|
||||
for _, moduleBehaviorEntry in ipairs(sortedModulesNegativePrio) do
|
||||
description = EID:GenerateTextFromModuleEntry(moduleBehaviorEntry, itemDataTable[moduleBehaviorEntry.Name], description, 0)
|
||||
end
|
||||
-- remove leading/trailing # and multiple # in a row. Makes translation more readable in some places, if a # is explicitly added
|
||||
description = description:gsub("#$", ""):gsub("^#", ""):gsub("#+", "#")
|
||||
|
||||
return description
|
||||
end
|
||||
|
||||
-- Default modular description generator, used if no BehaviorFunc is defined
|
||||
function EID:GenerateDefaultModuleDescription(moduleBehaviorEntry, itemDataValue, description, indent)
|
||||
local moduleID = moduleBehaviorEntry.Name
|
||||
local textFragment = EID:getDescriptionEntry("ModularDescriptions", moduleID)
|
||||
local moduleDescription = EID:FormatModuleDescription(moduleBehaviorEntry, itemDataValue, textFragment)
|
||||
return EID:AddModularTextLineToDescription(description, moduleDescription, indent)
|
||||
end
|
||||
|
||||
function EID:AddModularTextLineToDescription(description, textToAdd, indent)
|
||||
if textToAdd and textToAdd ~= "" then
|
||||
if indent and indent > 0 then
|
||||
textToAdd = string.rep("{{IND}}", indent) .. textToAdd
|
||||
end
|
||||
if not description:find("#$") then
|
||||
description = description .."#".. textToAdd
|
||||
else
|
||||
description = description .. textToAdd
|
||||
end
|
||||
end
|
||||
return description
|
||||
end
|
||||
|
||||
-- Formats a modular description line based on the module behavior entry and the given value
|
||||
function EID:FormatModuleDescription(moduleBehaviorEntry, value, textFragment)
|
||||
local isMultiplier = moduleBehaviorEntry.IsMultiplier
|
||||
|
||||
if textFragment then
|
||||
-- if the value is a table, we handle it as a range of values
|
||||
if type(value) == "table" then
|
||||
local rangeValue = tostring(value[1]) .. "-" .. tostring(value[2])
|
||||
textFragment = textFragment:gsub("{value}", rangeValue)
|
||||
value = value[2] -- for pluralization, take the maximum value
|
||||
else
|
||||
-- If value is not of type number, we assume 1
|
||||
if type(value) == "number" then
|
||||
-- formated and without trailing zeros
|
||||
local formattedValue = EID:PrettyPrintValue(value)
|
||||
|
||||
-- Handle sign
|
||||
if not moduleBehaviorEntry.HideSign then
|
||||
local sign = isMultiplier and "x" or value > 0 and "+" or ""
|
||||
formattedValue = sign .. formattedValue
|
||||
end
|
||||
-- Put formatted value into text fragment
|
||||
textFragment = textFragment:gsub("{value}", formattedValue)
|
||||
else
|
||||
textFragment = textFragment:gsub("{value}", tostring(value))
|
||||
value = 1 -- for further evaluations
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- arrow up/down and icon decoration
|
||||
local decoration = ""
|
||||
if moduleBehaviorEntry.Arrow then
|
||||
if moduleBehaviorEntry.InvertArrow then
|
||||
if isMultiplier then
|
||||
decoration = value >= 1 and "↓ " or "↑ "
|
||||
else
|
||||
decoration = value >= 0 and "↓ " or "↑ "
|
||||
end
|
||||
else
|
||||
if isMultiplier then
|
||||
-- multipliers smaller 1 are a negative effect
|
||||
decoration = value >= 1 and "↑ " or "↓ "
|
||||
else
|
||||
decoration = value >= 0 and "↑ " or "↓ "
|
||||
end
|
||||
end
|
||||
end
|
||||
if moduleBehaviorEntry.Icon and type(moduleBehaviorEntry.Icon) ~= "table" then
|
||||
decoration = decoration .. moduleBehaviorEntry.Icon
|
||||
end
|
||||
|
||||
local text = (decoration or "") .. " " .. (textFragment or "") .. "#"
|
||||
-- try pluralize
|
||||
text = EID:TryPluralizeString(text, value)
|
||||
-- remove leading and double spaces
|
||||
text = text:gsub(" +", " "):gsub("^%s+", "")
|
||||
|
||||
return text
|
||||
end
|
||||
|
||||
-- DEBUG: Validate Module table entry existance
|
||||
function EID:ValidateModuleEntry(ModuleID, additionalInfo)
|
||||
if not EID.ModuleBehaviors[ModuleID] then
|
||||
print("[ERROR] Unknown item Modul '"..ModuleID.."'", additionalInfo)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
Reference in new issue
Block a user