Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions spec/System/TestFoulborn_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
describe("TestFoulborn", function()
-- spell-checker: disable
-- Voll's Devotion has two foulborn-convertible mods in ModFoulbornMap.jsonc:
-- "30% reduced Power Charge Duration" -> "Gain a Power Charge every Second..."
-- "30% reduced Endurance Charge Duration" -> "Lose all Power Charges..."
local powerBase = "30% reduced Power Charge Duration"
local powerFoulborn = "Gain a Power Charge every Second if you haven't lost Power Charges Recently"

local function vollsDevotion()
return new("Item", [[
Rarity: Unique
Voll's Devotion
Agate Amulet
Implicits: 1
+(16-24) to Strength and Intelligence
+(20-30) to maximum Energy Shield
+(30-40) to maximum Life
+(15-20)% to all Elemental Resistances
30% reduced Endurance Charge Duration
30% reduced Power Charge Duration
Gain an Endurance Charge when you lose a Power Charge
]])
end

local function findByMutatedLine(item, replacement)
for _, modLine in ipairs(item.explicitModLines) do
if (modLine.mutatedLine == replacement) or (modLine.line == replacement) then
return modLine
end
end
end

local function findByLine(item, text)
for _, modLine in ipairs(item.explicitModLines) do
if modLine.line == text then
return modLine
end
end
end

it("matches convertible mods to their foulborn replacement", function()
local item = vollsDevotion()
local modLine = findByMutatedLine(item, powerFoulborn)
assert.is_not_nil(modLine)
assert.are.equals(powerBase, modLine.line)
-- present but inactive by default, so the item is not yet foulborn
assert.is_false(modLine.mutateActive)
assert.is_falsy(item.foulborn)
end)

it("does not mark non-convertible mods", function()
local item = vollsDevotion()
local plainMod = findByLine(item, "Gain an Endurance Charge when you lose a Power Charge")
assert.is_not_nil(plainMod)
assert.is_nil(plainMod.mutatedLine)
end)

it("converts the item to foulborn when a mod is activated", function()
local item = vollsDevotion()
findByMutatedLine(item, powerFoulborn).mutateActive = true
item:BuildAndParseRaw()

assert.is_true(item.foulborn)
assert.are.equals("Foulborn Voll's Devotion", item.title)

-- the original mod line is now shown as the foulborn text
local mutated = findByLine(item, powerFoulborn)
assert.is_not_nil(mutated)
assert.is_true(mutated.mutated)
assert.are.equals(powerBase, mutated.originalLine)
end)

it("round-trips foulborn state through BuildRaw", function()
local item = vollsDevotion()
findByMutatedLine(item, powerFoulborn).mutateActive = true
item:BuildAndParseRaw()

local raw = item:BuildRaw()
assert.is_truthy(raw:find("{mutateActive}", 1, true))
assert.is_truthy(raw:find("{originalLine:" .. powerBase .. "}", 1, true))

-- re-importing the built text preserves the foulborn conversion
local reimported = new("Item", raw)
assert.is_true(reimported.foulborn)
assert.are.equals("Foulborn Voll's Devotion", reimported.title)
end)

it("reverts to the base item when the mod is deactivated", function()
local item = vollsDevotion()
findByMutatedLine(item, powerFoulborn).mutateActive = true
item:BuildAndParseRaw()
assert.is_true(item.foulborn)

findByMutatedLine(item, powerFoulborn).mutateActive = false
item:BuildAndParseRaw()

assert.is_falsy(item.foulborn)
assert.are.equals("Voll's Devotion", item.title)
assert.is_not_nil(findByLine(item, powerBase))
end)

it("only converts one mod when several are convertible", function()
local item = vollsDevotion()
findByMutatedLine(item, powerFoulborn).mutateActive = true
item:BuildAndParseRaw()

-- the endurance charge mod stays in its base form and is still offered
local endurance = findByLine(item, "30% reduced Endurance Charge Duration")
assert.is_not_nil(endurance)
assert.is_falsy(endurance.mutateActive)
assert.is_not_nil(endurance.mutatedLine)
end)

it("leaves items without a foulborn mapping untouched", function()
-- same mod text, but on a rare: mutatedLines is only populated for uniques
local item = new("Item", "Rarity: Rare\nTest Subject\nAgate Amulet\n30% reduced Power Charge Duration")
assert.is_nil(item.mutatedLines)
assert.is_falsy(item.foulborn)
assert.is_nil(item.explicitModLines[1].mutatedLine)
end)
-- Meginord's Vise maps a single mod to a two-line foulborn replacement:
-- "+# to Strength" -> "+700 Strength Requirement\n(10-15)% chance to deal Triple Damage"
local viseFoulborn = "+700 Strength Requirement\n(10-15)% chance to deal Triple Damage"
local viseBase = "+50 to Strength"

local function meginordsVise()
return new("Item", [[
Rarity: Unique
Meginord's Vise
Steel Gauntlets
+50 to Strength
100% increased Knockback Distance
]])
end

local function linesAllByOriginal(item, originalLine)
local out = {}
for _, modLine in ipairs(item.explicitModLines) do
if modLine.originalLine == originalLine then
table.insert(out, modLine)
end
end
return out
end

local function hasMod(item, name)
for _, modLine in ipairs(item.explicitModLines) do
for _, mod in ipairs(modLine.modList or {}) do
if mod.name == name then
return true
end
end
end
return false
end

it("expands a single mod into two lines when a 1->2 foulborn mod is activated", function()
local item = meginordsVise()

-- one base mod carries the whole multi-line replacement
local base = findByMutatedLine(item, viseFoulborn)
assert.is_not_nil(base)
assert.are.equals(viseBase, base.line)
assert.is_false(base.mutateActive)
assert.is_falsy(item.foulborn)

base.mutateActive = true
item:BuildAndParseRaw()

assert.is_true(item.foulborn)
assert.are.equals("Foulborn Meginord's Vise", item.title)

-- the base line is gone, replaced by one modLine per stat line
assert.is_nil(findByLine(item, viseBase))
local mutated = linesAllByOriginal(item, viseBase)
assert.are.equals(2, #mutated)

local seen = {}
for _, modLine in ipairs(mutated) do
assert.is_true(modLine.mutated)
seen[modLine.line] = true
end
assert.is_true(seen["+700 Strength Requirement"])
assert.is_true(seen["(10-15)% chance to deal Triple Damage"])

-- both stat lines contribute their own parsed mod
assert.is_true(hasMod(item, "StrRequirement"))
assert.is_true(hasMod(item, "TripleDamageChance"))
end)

it("collapses the expanded lines back to the base mod when deactivated", function()
local item = meginordsVise()
findByMutatedLine(item, viseFoulborn).mutateActive = true
item:BuildAndParseRaw()
assert.are.equals(2, #linesAllByOriginal(item, viseBase))

linesAllByOriginal(item, viseBase)[1].mutateActive = false
item:BuildAndParseRaw()

assert.is_falsy(item.foulborn)
assert.are.equals("Meginord's Vise", item.title)

-- the two expanded lines collapse back into the single base mod
local restored = linesAllByOriginal(item, viseBase)
assert.are.equals(1, #restored)
assert.are.equals(viseBase, restored[1].line)
assert.is_falsy(restored[1].mutated)
assert.is_not_nil(findByLine(item, viseBase))
assert.is_false(hasMod(item, "TripleDamageChance"))
end)
end)
Loading
Loading