diff --git a/spec/System/TestConfigTab_spec.lua b/spec/System/TestConfigTab_spec.lua new file mode 100644 index 0000000000..b699447ae0 --- /dev/null +++ b/spec/System/TestConfigTab_spec.lua @@ -0,0 +1,375 @@ +describe("TestConfig", function() + before_each(function() + newBuild() + runCallback("OnFrame") + end) + + describe("ConfigTab", function() + describe("NewConfigSet", function() + it("Creates a new config set with specified ID", function() + local configSetName = "New Config Set" + local configId = 2 + build.configTab:NewConfigSet(configId, configSetName) + + assert.are.equals(configSetName, build.configTab.configSets[configId].title) + assert.are.equals(configId, build.configTab.configSets[configId].id) + end) + + it("Assigns auto-incremented ID when not specified", function() + local configSetName = "Auto ID Config Set" + local configSet1 = build.configTab:NewConfigSet() + local configSet2 = build.configTab:NewConfigSet(nil, configSetName) + + assert.are.equals(2, configSet1.id) + assert.are.equals(3, configSet2.id) + assert.are.equals(configSetName, build.configTab.configSets[configSet2.id].title) + end) + + it("Adds set to order list", function() + local newTitle = "New Config" + + build.configTab:NewConfigSet(nil, newTitle) + + assert.are.equals(2, #build.configTab.configSetOrderList) + assert.are.equals(2, build.configTab.configSetOrderList[2]) + end) + + it("Updates the mod flag", function() + local newTitle = "New Config" + build.configTab.modFlag = false + + build.configTab:NewConfigSet(nil, newTitle) + + assert.is_true(build.configTab.modFlag) + end) + end) + + describe("CopyConfigSet", function() + it("Copies a config set with a new name", function() + local newTitle = "Copied Config" + local newConfigSet = build.configTab:CopyConfigSet(1, newTitle) + + + assert.is_not.same(build.configTab.configSets[1], newConfigSet) + assert.are.equals(newTitle, newConfigSet.title) + assert.are.same(build.configTab.configSets[1].input, newConfigSet.input) + assert.are.same(build.configTab.configSets[1].placeholder, newConfigSet.placeholder) + end) + + it("Copies all input and placeholder values", function() + local newTitle = "Copied Config" + + build.configTab.configSets[1].input.testValue = 42 + build.configTab.configSets[1].placeholder.testValue = 99 + + local newConfigSet = build.configTab:CopyConfigSet(1, newTitle) + + assert.are.equals(42, newConfigSet.input.testValue) + assert.are.equals(99, newConfigSet.placeholder.testValue) + end) + + it("Returns copied config set with next ID", function() + local newTitle = "Copied Config" + + local newConfigSet = build.configTab:CopyConfigSet(1, newTitle) + + assert.are.equals(2, newConfigSet.id) + end) + + it("Adds copied config set to order list", function() + local newTitle = "Copied Config" + + build.configTab:CopyConfigSet(1, newTitle) + + assert.are.equals(2, #build.configTab.configSetOrderList) + assert.are.equals(2, build.configTab.configSetOrderList[2]) + end) + + it("Updates the mod flag", function() + local newTitle = "Copied Config" + build.configTab.modFlag = false + + build.configTab:CopyConfigSet(1, newTitle) + + assert.is_true(build.configTab.modFlag) + end) + end) + + describe("RenameConfigSet", function() + it("Renames a config set", function() + local newTitle = "Renamed Config" + build.configTab:RenameConfigSet(1, newTitle) + + assert.are.equals(newTitle, build.configTab.configSets[1].title) + end) + + it("Does not rename non-existent config set", function() + build.configTab:RenameConfigSet(999, "Non-existent") + + assert.is_nil(build.configTab.configSets[999]) + end) + + it("Updates the mod flag", function() + local newTitle = "Renamed Config" + build.configTab.modFlag = false + + build.configTab:RenameConfigSet(1, newTitle) + + assert.is_true(build.configTab.modFlag) + end) + end) + + describe("DeleteConfigSet", function() + it("Deletes a config set and its order entry", function() + local configSetName = "Config To Delete" + build.configTab:NewConfigSet(2, configSetName) + + build.configTab:DeleteConfigSet(2, 2) + + assert.is_nil(build.configTab.configSets[2]) + assert.are.equals(1, #build.configTab.configSetOrderList) + end) + + it("allows deletion of the last config set", function() + local lastConfig = 1 + + assert.are.equals(1, #build.configTab.configSetOrderList) + build.configTab:DeleteConfigSet(lastConfig, 1) + + assert.are.equals(0, #build.configTab.configSetOrderList) + assert.is_nil(build.configTab.configSets[lastConfig]) + end) + end) + + describe("SetActiveConfigSet", function() + it("Switches to a valid config set", function() + local configSetName = "New Config" + build.configTab:NewConfigSet(2, configSetName) + + build.configTab:SetActiveConfigSet(2) + + assert.are.equals(2, build.configTab.activeConfigSetId) + assert.are.same(build.configTab.configSets[2].input, build.configTab.input) + assert.are.same(build.configTab.configSets[2].placeholder, build.configTab.placeholder) + end) + + it("Defaults to first config set if invalid ID provided", function() + build.configTab:SetActiveConfigSet(999) + + assert.are.equals(1, build.configTab.activeConfigSetId) + end) + + it("Does not trigger rebuild when init is true", function() + local configSetName = "New Config" + build.configTab:NewConfigSet(2, configSetName) + + build.modFlag = false + + build.configTab:SetActiveConfigSet(2, true) + + assert.are.equals(2, build.configTab.activeConfigSetId) + assert.is_false(build.modFlag) + end) + end) + end) + + describe("ConfigSetService", function() + local configSetService + + before_each(function() + configSetService = new("ConfigSetService", build.configTab) + end) + + describe("NewConfigSet", function() + it("Creates a new config set via service", function() + local configSetName = "Service New Config" + configSetService:NewConfigSet(configSetName) + + assert.are.equals(2, #build.configTab.configSetOrderList) + assert.are.equals(configSetName, build.configTab.configSets[2].title) + end) + + it("Sets newly created config as active", function() + configSetService:NewConfigSet("New From Service") + + assert.are.equals(2, build.configTab.activeConfigSetId) + end) + + it("Adds an undo state", function() + local newTitle = "New Config" + + local undoCountBefore = #build.configTab.undo + configSetService:NewConfigSet(newTitle) + + assert.are.equals(#build.configTab.undo, undoCountBefore + 1) + end) + end) + + describe("CopyConfigSet", function() + it("Copies a config set via service", function() + local configSetName = "Copied via Service" + configSetService:CopyConfigSet(1, configSetName) + local configId = build.configTab.configSetOrderList[2] + + assert.are.equals(configSetName, build.configTab.configSets[configId].title) + end) + + it("Sets copied config as active via service", function() + configSetService:NewConfigSet("Config") + configSetService:CopyConfigSet(1, "Service Copy") + local configId = build.configTab.configSetOrderList[3] + + assert.are.equals(configId, build.configTab.activeConfigSetId) + end) + + it("Adds an undo state when copying a config set", function() + local undoCountBefore = #build.configTab.undo + configSetService:CopyConfigSet(1, "Copied Config") + + assert.are.equals(#build.configTab.undo, undoCountBefore + 1) + end) + end) + + describe("RenameConfigSet", function() + it("Renames a config set via service", function() + local configSetName = "Original Name" + configSetService:NewConfigSet(configSetName) + local configId = build.configTab.configSetOrderList[2] + + configSetService:RenameConfigSet(configId, "New Name") + + assert.are.equals("New Name", build.configTab.configSets[configId].title) + end) + + it("Does not rename non-existent config set", function() + configSetService:RenameConfigSet(10, "Non-existent") + + assert.is_nil(build.configTab.configSets[10]) + end) + + it("Adds an undo state when renaming", function() + local configSetName = "Config To Rename" + configSetService:NewConfigSet(configSetName) + local configId = build.configTab.configSetOrderList[2] + + local undoCountBefore = #build.configTab.undo + configSetService:RenameConfigSet(configId, "Renamed Config") + + assert.are.equals(#build.configTab.undo, undoCountBefore + 1) + end) + end) + + describe("DeleteConfigSet", function() + it("Deletes a config set via service", function() + configSetService:NewConfigSet("Service Delete Me") + + configSetService:DeleteConfigSet(2, 2) + + assert.is_nil(build.configTab.configSets[2]) + assert.is_nil(build.configTab.configSetOrderList[2]) + end) + + + it("Switches active config set when deleting current", function() + configSetService:NewConfigSet("Service Delete Me") + local configToKeep = 1 + local configToDelete = 2 + + assert.are.equals(configToDelete, build.configTab.activeConfigSetId) + + configSetService:DeleteConfigSet(configToDelete, 2) + + assert.are.equals(configToKeep, build.configTab.activeConfigSetId) + end) + + it("Does not delete last config set via service", function() + configSetService:NewConfigSet("Last One") + + configSetService:DeleteConfigSet(1, 1) + + assert.are.equals(1, #build.configTab.configSetOrderList) + end) + + it("Adds an undo state when deleting", function() + local undoCountBefore = #build.configTab.undo + local undoCountAfterNew = #build.configTab.undo + configSetService:NewConfigSet("Undo Delete Test") + + configSetService:DeleteConfigSet(2, 2) + + assert.are.equals(#build.configTab.undo, undoCountBefore + 2) + end) + end) + + describe("Integration", function() + it("Completes a config set lifecycle", function() + local config1Name = "First Config" + local config2Name = "Second Config" + + configSetService:NewConfigSet(config1Name) + assert.are.equals("First Config", build.configTab.configSets[2].title) + + configSetService:CopyConfigSet(2, config2Name) + assert.are.equals("Second Config", build.configTab.configSets[3].title) + end) + end) + end) + + describe("ConfigStateManagement", function() + local configSetService + + before_each(function() + configSetService = new("ConfigSetService", build.configTab) + end) + + describe("Input and placeholder persistence", function() + it("Preserves input values across config set switches", function() + local testValue = 123 + + configSetService:NewConfigSet("Set 1") + configSetService:NewConfigSet("Set 2") + + build.configTab.configSets[1].input.testVar = testValue + build.configTab:SetActiveConfigSet(1) + + assert.are.equals(testValue, build.configTab.configSets[1].input.testVar) + + build.configTab:SetActiveConfigSet(3) + build.configTab.configSets[3].input.testVar = testValue + + build.configTab:SetActiveConfigSet(1) + assert.are.equals(testValue, build.configTab.configSets[1].input.testVar) + assert.are.equals(testValue, build.configTab.configSets[3].input.testVar) + end) + + it("Preserves placeholder values across config set switches", function() + local placeholderValue = 456 + + configSetService:NewConfigSet("Set 1") + configSetService:NewConfigSet("Set 2") + configSetService:NewConfigSet("Set 3") + + build.configTab.configSets[3].placeholder.testVar = placeholderValue + + build.configTab:SetActiveConfigSet(3) + assert.are.equals(placeholderValue, build.configTab.configSets[3].placeholder.testVar) + end) + end) + + describe("Default values", function() + it("Initializes new config sets with default placeholder states", function() + assert.is_not_nil(build.configTab.configSets[1]) + assert.is_not_nil(build.configTab.configSets[1].input) + assert.is_not_nil(build.configTab.configSets[1].placeholder) + end) + + it("Copies placeholder states when copying config sets", function() + build.configTab.configSets[1].placeholder.testPlaceholder = "test" + + local newConfigSet = build.configTab:CopyConfigSet(1, "Copy Test") + + assert.are.equals("test", newConfigSet.placeholder.testPlaceholder) + end) + end) + end) +end) diff --git a/spec/System/TestItemsTab_spec.lua b/spec/System/TestItemsTab_spec.lua new file mode 100644 index 0000000000..b18423402e --- /dev/null +++ b/spec/System/TestItemsTab_spec.lua @@ -0,0 +1,414 @@ +describe("TestItemsTab", function() + before_each(function() + newBuild() + + build.itemsTab:CreateDisplayItemFromRaw([[Dialla's Malefaction + Sage's Robe + Energy Shield: 95 + EnergyShieldBasePercentile: 0 + Variant: Pre 3.19.0 + Variant: Current + Selected Variant: 2 + Sage's Robe + Quality: 20 + Sockets: R-G-B-B-B-B + LevelReq: 37 + Implicits: 0 + Gems can be Socketed in this Item ignoring Socket Colour + {variant:1}Gems Socketed in Red Sockets have +1 to Level + {variant:2}Gems Socketed in Red Sockets have +2 to Level + {variant:1}Gems Socketed in Green Sockets have +10% to Quality + {variant:2}Gems Socketed in Green Sockets have +30% to Quality + {variant:1}Gems Socketed in Blue Sockets gain 25% increased Experience + {variant:2}Gems Socketed in Blue Sockets gain 100% increased Experience + Has no Attribute Requirements]]) + build.itemsTab:AddDisplayItem(true) + + build.itemsTab:CreateDisplayItemFromRaw("New Item\nKarui Chopper") + build.itemsTab:AddDisplayItem(true) + + runCallback("OnFrame") + end) + + describe("ItemsTab", function() + describe("NewItemSet", function() + it("Creates a new item set with specified ID", function() + local itemSetName = "New Item Set" + local setId = 2 + build.itemsTab:NewItemSet(setId, itemSetName) + + assert.are.equals(itemSetName, build.itemsTab.itemSets[setId].title) + assert.are.equals(setId, build.itemsTab.itemSets[setId].id) + end) + + it("Assigns auto-incremented ID when not specified", function() + local itemSetName = "Auto ID Item Set" + local setId1 = build.itemsTab:NewItemSet() + local setId2 = build.itemsTab:NewItemSet(nil, itemSetName) + + assert.are.equals(2, setId1.id) + assert.are.equals(3, setId2.id) + assert.are.equals(itemSetName, build.itemsTab.itemSets[setId2.id].title) + end) + + it("Adds set to order list", function() + local newTitle = "New Item" + + build.itemsTab:NewItemSet(nil, newTitle) + + assert.are.equals(2, #build.itemsTab.itemSetOrderList) + assert.are.equals(2, build.itemsTab.itemSetOrderList[2]) + end) + + it("Sets mod flag when creating new item set", function() + local newTitle = "New Item" + build.itemsTab.modFlag = false + + build.itemsTab.modFlag = false + + build.itemsTab:NewItemSet(nil, newTitle) + + assert.is_true(build.itemsTab.modFlag) + end) + end) + + describe("CopyItemSet", function() + it("Copies an item set with a new name", function() + local newTitle = "Copied Item Set" + local newItemSet = build.itemsTab:CopyItemSet(1, newTitle) + + assert.is_not.same(build.itemsTab.itemSets[1], newItemSet) + assert.are.equals(newTitle, newItemSet.title) + end) + + it("Returns copied item set with next ID", function() + local newTitle = "Copied Item" + + local newItemSet = build.itemsTab:CopyItemSet(1, newTitle) + + assert.are.equals(2, newItemSet.id) + end) + + it("Adds copied item set to order list", function() + local newTitle = "Copied Item" + + build.itemsTab:CopyItemSet(1, newTitle) + + assert.are.equals(2, #build.itemsTab.itemSetOrderList) + assert.are.equals(2, build.itemsTab.itemSetOrderList[2]) + end) + + it("Updates the mod flag", function() + local newTitle = "Copied Item" + build.itemsTab.modFlag = false + + build.itemsTab:CopyItemSet(1, newTitle) + + assert.is_true(build.itemsTab.modFlag) + end) + end) + + describe("RenameItemSet", function() + it("Renames an item set", function() + local newTitle = "Renamed Item Set" + build.itemsTab:RenameItemSet(1, newTitle) + + assert.are.equals(newTitle, build.itemsTab.itemSets[1].title) + end) + + it("Updates the mod flag", function() + local newTitle = "Renamed Item" + build.itemsTab.modFlag = false + + build.itemsTab:RenameItemSet(1, newTitle) + + assert.is_true(build.itemsTab.modFlag) + end) + + it("Does not error on invalid item set ID", function() + build.itemsTab.modFlag = false + + build.itemsTab:RenameItemSet(999, "Should Not Error") + + assert.is_false(build.itemsTab.modFlag) + end) + end) + + describe("DeleteItemSet", function() + it("Deletes an item set and removes from order list", function() + local itemSetName = "Item Set To Delete" + build.itemsTab:NewItemSet(2, itemSetName) + + build.itemsTab:DeleteItemSet(2, 2) + + assert.is_nil(build.itemsTab.itemSets[2]) + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + end) + + it("Updates the mod flag", function() + build.itemsTab:NewItemSet(2, "ToDelete") + build.itemsTab.modFlag = false + + build.itemsTab:DeleteItemSet(2, 2) + + assert.is_true(build.itemsTab.modFlag) + end) + + it("allows deletion of the last item set", function() + local lastConfig = 1 + + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + build.itemsTab:DeleteItemSet(lastConfig, 1) + + assert.are.equals(0, #build.itemsTab.itemSetOrderList) + assert.is_nil(build.itemsTab.itemSets[lastConfig]) + end) + end) + end) + + describe("SetActiveItemSet", function() + it("Switches to a valid item set", function() + local itemSetName = "New Item Set" + build.itemsTab:NewItemSet(2, itemSetName) + + build.itemsTab:SetActiveItemSet(2) + + assert.are.equals(2, build.itemsTab.activeItemSetId) + assert.are.same(build.itemsTab.itemSets[2], build.itemsTab.activeItemSet) + end) + + it("Defaults to first item set if invalid ID provided", function() + build.itemsTab:SetActiveItemSet(999) + + assert.are.equals(1, build.itemsTab.activeItemSetId) + end) + + it("Sets buildFlag", function() + local itemSetName = "New Item Set" + build.itemsTab:NewItemSet(2, itemSetName) + + build.buildFlag = false + + build.itemsTab:SetActiveItemSet(2, true) + + assert.are.equals(2, build.itemsTab.activeItemSetId) + assert.is_true(build.buildFlag) + end) + end) + + describe("ItemSetService", function() + local itemSetService + before_each(function() + itemSetService = new("ItemSetService", build.itemsTab) + end) + + describe("NewItemSet", function() + it("Creates a new item set via service", function() + local itemSetName = "Service New Item" + itemSetService:NewItemSet(itemSetName) + + assert.are.equals(2, #build.itemsTab.itemSetOrderList) + assert.are.equals(itemSetName, build.itemsTab.itemSets[2].title) + end) + + it("Sets newly created item set as active", function() + itemSetService:NewItemSet("New From Service") + + assert.are.equals(2, build.itemsTab.activeItemSetId) + end) + + it("Adds an undo state", function() + local newTitle = "New Item" + + local undoCountBefore = #build.itemsTab.undo + itemSetService:NewItemSet(newTitle) + + assert.are.equals(#build.itemsTab.undo, undoCountBefore + 1) + end) + end) + + describe("CopyItemSet", function() + it("Copies a item set via service", function() + local itemName = "Copied via Service" + itemSetService:CopyItemSet(1, itemName) + local setId = build.itemsTab.itemSetOrderList[2] + + assert.are.equals(itemName, build.itemsTab.itemSets[setId].title) + end) + + it("Sets copied item set as active via service", function() + itemSetService:NewItemSet("Item") + itemSetService:CopyItemSet(1, "Service Copy") + local setId = build.itemsTab.itemSetOrderList[3] + + assert.are.equals(setId, build.itemsTab.activeItemSetId) + end) + + it("Adds an undo state when copying a item set", function() + local undoCountBefore = #build.itemsTab.undo + itemSetService:CopyItemSet(1, "Copied Item") + + assert.are.equals(#build.itemsTab.undo, undoCountBefore + 1) + end) + end) + + describe("RenameItemSet", function() + it("Renames a item set via service", function() + local itemName = "Original Name" + itemSetService:NewItemSet(itemName) + local setId = build.itemsTab.itemSetOrderList[2] + + itemSetService:RenameItemSet(setId, "New Name") + + assert.are.equals("New Name", build.itemsTab.itemSets[setId].title) + end) + + it("Does not rename non-existent item set", function() + itemSetService:RenameItemSet(10, "Non-existent") + + assert.is_nil(build.itemsTab.itemSets[10]) + end) + + it("Adds an undo state when renaming", function() + local itemSetName = "Item To Rename" + itemSetService:NewItemSet(itemSetName) + local setId = build.itemsTab.itemSetOrderList[2] + + local undoCountBefore = #build.itemsTab.undo + itemSetService:RenameItemSet(setId, "Renamed Item") + + assert.are.equals(#build.itemsTab.undo, undoCountBefore + 1) + end) + end) + + describe("DeleteItemSet", function() + it("Deletes a item set via service", function() + itemSetService:NewItemSet("Service Delete Me") + + itemSetService:DeleteItemSet(2, 2) + + assert.is_nil(build.itemsTab.itemSets[2]) + assert.is_nil(build.itemsTab.itemSetOrderList[2]) + end) + + + it("Switches active item set when deleting current", function() + itemSetService:NewItemSet("Service Delete Me") + local itemToKeep = 1 + local itemToDelete = 2 + + assert.are.equals(itemToDelete, build.itemsTab.activeItemSetId) + + itemSetService:DeleteItemSet(itemToDelete, 2) + + assert.are.equals(itemToKeep, build.itemsTab.activeItemSetId) + end) + + it("Does not delete last item set via service", function() + itemSetService:NewItemSet("Last One") + + itemSetService:DeleteItemSet(1, 1) + + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + end) + + it("Adds an undo state when deleting", function() + local undoCountBefore = #build.itemsTab.undo + local undoCountAfterNew = #build.itemsTab.undo + itemSetService:NewItemSet("Undo Delete Test") + + itemSetService:DeleteItemSet(2, 2) + + assert.are.equals(#build.itemsTab.undo, undoCountBefore + 2) + end) + end) + + describe("Integration", function() + it("Completes a item set lifecycle", function() + local item1Name = "First Item" + local item2Name = "Second Item" + + itemSetService:NewItemSet(item1Name) + assert.are.equals("First Item", build.itemsTab.itemSets[2].title) + + itemSetService:CopyItemSet(2, item2Name) + assert.are.equals("Second Item", build.itemsTab.itemSets[3].title) + end) + end) + end) + + describe("ItemStateManagement", function() + local itemSetService + + before_each(function() + itemSetService = new("ItemSetService", build.itemsTab) + end) + + describe("Item set persistence across switches", function() + it("Preserves item assignments when switching between item sets", function() + local itemSetName1 = "Set 1" + local itemSetName2 = "Set 2" + + itemSetService:NewItemSet(itemSetName1) + itemSetService:NewItemSet(itemSetName2) + + local setId1 = 2 + local setId2 = 3 + + build.itemsTab:SetActiveItemSet(setId1) + build.itemsTab:EquipItemInSet(build.itemsTab.items[1], setId1) + + build.itemsTab:SetActiveItemSet(setId2) + build.itemsTab:EquipItemInSet(build.itemsTab.items[2], setId2) + + build.itemsTab:SetActiveItemSet(setId1) + assert.are.equals(1, build.itemsTab.activeItemSet["Body Armour"].selItemId) + + build.itemsTab:SetActiveItemSet(setId2) + assert.are.equals(2, build.itemsTab.activeItemSet["Weapon 1"].selItemId) + end) + + it("Preserves weapon set flags when switching between item sets", function() + itemSetService:NewItemSet("First Set") + itemSetService:NewItemSet("Set with Weapon Swap") + + build.itemsTab.activeItemSet.useSecondWeaponSet = true + + build.itemsTab:SetActiveItemSet(2) + assert.is_not.truthy(build.itemsTab.activeItemSet.useSecondWeaponSet) + + build.itemsTab:SetActiveItemSet(3) + assert.is_true(build.itemsTab.activeItemSet.useSecondWeaponSet) + end) + + it("Preserves active slot flags when switching between item sets", function() + itemSetService:NewItemSet("First Set") + itemSetService:NewItemSet("Second Set") + + build.itemsTab.slots["Weapon 1"].active = true + build.itemsTab.activeItemSet["Weapon 1"].active = true + + build.itemsTab:SetActiveItemSet(2) + assert.is_not.truthy(build.itemsTab.activeItemSet["Weapon 1"].active) + + build.itemsTab:SetActiveItemSet(3) + assert.is_true(build.itemsTab.activeItemSet["Weapon 1"].active) + end) + end) + + describe("Default values", function() + it("Initializes new item sets with default placeholder states", function() + assert.is_not_nil(build.itemsTab.itemSets[1]) + assert.is.same(build.itemsTab.itemSets[1]["Weapon 1"], { selItemId = 0 }) + end) + + it("Copies placeholder states when copying item sets", function() + build.itemsTab.itemSets[1]["Weapon 1"].selItemId = 1 + + local newItemSet = build.itemsTab:CopyItemSet(1, "Copy Test") + + assert.are.equals(1, newItemSet["Weapon 1"].selItemId) + end) + end) + end) +end) diff --git a/spec/System/TestLoadouts_spec.lua b/spec/System/TestLoadouts_spec.lua new file mode 100644 index 0000000000..3f1264f980 --- /dev/null +++ b/spec/System/TestLoadouts_spec.lua @@ -0,0 +1,687 @@ +local t_insert = table.insert +local t_remove = table.remove + +describe("TestLoadouts", function() + before_each(function() + newBuild() + + build.itemsTab:CreateDisplayItemFromRaw([[Dialla's Malefaction + Sage's Robe + Energy Shield: 95 + EnergyShieldBasePercentile: 0 + Variant: Pre 3.19.0 + Variant: Current + Selected Variant: 2 + Sage's Robe + Quality: 20 + Sockets: R-G-B-B-B-B + LevelReq: 37 + Implicits: 0 + Gems can be Socketed in this Item ignoring Socket Colour + {variant:1}Gems Socketed in Red Sockets have +1 to Level + {variant:2}Gems Socketed in Red Sockets have +2 to Level + {variant:1}Gems Socketed in Green Sockets have +10% to Quality + {variant:2}Gems Socketed in Green Sockets have +30% to Quality + {variant:1}Gems Socketed in Blue Sockets gain 25% increased Experience + {variant:2}Gems Socketed in Blue Sockets gain 100% increased Experience + Has no Attribute Requirements]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + end) + + teardown(function() + -- newBuild() takes care of resetting everything in setup() + end) + + describe("Build", function() + describe("NewLoadout", function() + it("Creates a new loadout with the correct name", function() + local loadoutName = "Loadout Name" + build:NewLoadout(loadoutName) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_true(build.modFlag) + end) + end) + + describe("CopyLoadout", function() + it("Copies a loadout with a new name", function() + local loadoutName = "Loadout Name" + local newSpec, newItemSet, newSkillSet, newConfigSet = build:CopyLoadout("Default", loadoutName) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + -- First index is the "Loadout: " header, second index is the start of the loadouts + assert.is_not.same(build.controls.buildLoadouts.list[2], build.controls.buildLoadouts.list[3]) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_not.same(newSpec, build.treeTab.specList[1]) + assert.is_not.same(newItemSet, build.itemsTab.itemSets[1]) + assert.is_not.same(newSkillSet, build.skillsTab.skillSets[1]) + assert.is_not.same(newConfigSet, build.configTab.configSets[1]) + assert.is_same(loadoutName, newSpec.title) + assert.is_same(loadoutName, newItemSet.title) + assert.is_same(loadoutName, newSkillSet.title) + assert.is_same(loadoutName, newConfigSet.title) + assert.is_true(build.modFlag) + end) + end) + + describe("DeleteLoadout", function() + it("Deletes a loadout and sets the next to the requested loadout by name", function() + local loadoutNameToDelete = "Delete Me" + build:NewLoadout(loadoutNameToDelete) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + local loadoutToDelete = build:GetLoadoutByName(loadoutNameToDelete) + local nextLoadout = build.controls.buildLoadouts.list[2] -- Default loadout + + build:DeleteLoadout(loadoutNameToDelete, nextLoadout) + build:SyncLoadouts() + + assert.is_nil(build.treeTab.specList[loadoutToDelete.specId]) + assert.is_nil(build.skillsTab.skillSets[loadoutToDelete.skillSetId]) + assert.is_nil(build.itemsTab.itemSets[loadoutToDelete.itemSetId]) + assert.is_nil(build.configTab.configSets[loadoutToDelete.configSetId]) + + assert.is_nil(build.itemsTab.itemSetOrderList[loadoutToDelete.itemSetId]) + assert.is_nil(build.skillsTab.skillSetOrderList[loadoutToDelete.skillSetId]) + assert.is_nil(build.configTab.configSetOrderList[loadoutToDelete.configSetId]) + + assert.is_same(1, build.treeTab.activeSpec) + assert.is_same(1, build.itemsTab.activeItemSetId) + assert.is_same(1, build.skillsTab.activeSkillSetId) + assert.is_same(1, build.configTab.activeConfigSetId) + assert.is_true(build.modFlag) + end) + + it("Deleting the first loadout with one other loadout decrements the index for the remaining sets", + function() + local loadoutNameToDelete = "Default" + local nextLoadout = "Do not delete" + build:NewLoadout(nextLoadout) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + + build:DeleteLoadout(loadoutNameToDelete, nextLoadout) + build:SyncLoadouts() + + assert.are.equals(1, #build.treeTab.specList) + assert.are.equals(2, build.skillsTab.activeSkillSetId) + assert.are.equals(2, build.itemsTab.activeItemSetId) + assert.are.equals(2, build.configTab.activeConfigSetId) + + assert.are.equals(nextLoadout, build.treeTab.specList[1].title) + assert.are.equals(nextLoadout, build.skillsTab.skillSets[build.skillsTab.activeSkillSetId].title) + assert.are.equals(nextLoadout, build.itemsTab.itemSets[build.itemsTab.activeItemSetId].title) + assert.are.equals(nextLoadout, build.configTab.configSets[build.configTab.activeConfigSetId].title) + + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + assert.are.equals(1, #build.configTab.configSetOrderList) + + assert.are.equals(2, build.itemsTab.itemSetOrderList[1]) + assert.are.equals(2, build.skillsTab.skillSetOrderList[1]) + assert.are.equals(2, build.configTab.configSetOrderList[1]) + + assert.is_true(build.modFlag) + end) + + it( + "Deleting the first loadout with multiple other loadouts does not change the index for the remaining sets", + function() + local loadoutNameToDelete = "Default" + local nextLoadout1 = "Do not delete 1" + local nextLoadout2 = "Do not delete 2" + build:NewLoadout(nextLoadout1) + build:NewLoadout(nextLoadout2) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(8, #build.controls.buildLoadouts.list) + local loadoutToDelete = build:GetLoadoutByName(loadoutNameToDelete) + + build:DeleteLoadout(loadoutNameToDelete, nextLoadout1) + build:SyncLoadouts() + + assert.are.equals(2, #build.treeTab.specList) + assert.are.equals(3, #build.skillsTab.skillSets) + assert.are.equals(3, #build.itemsTab.itemSets) + assert.are.equals(3, #build.configTab.configSets) + + assert.are.equals(nextLoadout1, build.treeTab.specList[1].title) + assert.are.equals(nextLoadout1, build.skillsTab.skillSets[2].title) + assert.are.equals(nextLoadout1, build.itemsTab.itemSets[2].title) + assert.are.equals(nextLoadout1, build.configTab.configSets[2].title) + + assert.are.equals(nextLoadout2, build.treeTab.specList[2].title) + assert.are.equals(nextLoadout2, build.skillsTab.skillSets[3].title) + assert.are.equals(nextLoadout2, build.itemsTab.itemSets[3].title) + assert.are.equals(nextLoadout2, build.configTab.configSets[3].title) + + assert.is_same(1, build.treeTab.activeSpec) + assert.is_same(2, build.itemsTab.activeItemSetId) + assert.is_same(2, build.skillsTab.activeSkillSetId) + assert.is_same(2, build.configTab.activeConfigSetId) + assert.is_true(build.modFlag) + end) + end) + + describe("RenameLoadout", function() + it("renames a loadout and calls the callback", function() + local oldName = "Old Loadout" + local newName = "New Loadout" + build:NewLoadout(oldName) + build:SyncLoadouts() + + build:RenameLoadout(oldName, newName) + build:SyncLoadouts() + -- Verify the new name appears in the loadout list + assert.is_same(newName, build.controls.buildLoadouts.list[3]) + -- Verify titles updated on spec, itemSet, skillSet, and configSet + local loadout = build:GetLoadoutByName(newName) + assert.is_same(newName, build.treeTab.specList[loadout.specId].title) + assert.is_same(newName, build.itemsTab.itemSets[loadout.itemSetId].title) + assert.is_same(newName, build.skillsTab.skillSets[loadout.skillSetId].title) + assert.is_same(newName, build.configTab.configSets[loadout.configSetId].title) + -- Verify mod flags set on all tabs + assert.is_true(build.treeTab.modFlag) + assert.is_true(build.itemsTab.modFlag) + assert.is_true(build.skillsTab.modFlag) + assert.is_true(build.configTab.modFlag) + -- Old name should no longer exist + assert.is_nil(build:GetLoadoutByName(oldName)) + assert.is_true(build.modFlag) + end) + end) + + describe("ReorderLoadout", function() + local function assertActiveLoadoutByName(expectedName) + local activeSpec = build.treeTab.specList[build.treeTab.activeSpec] + local activeItemSet = build.itemsTab.itemSets[build.itemsTab.activeItemSetId] + local activeSkillSet = build.skillsTab.skillSets[build.skillsTab.activeSkillSetId] + local activeConfigSet = build.configTab.configSets[build.configTab.activeConfigSetId] + assert.is_not_nil(activeSpec) + assert.is_same(expectedName, activeSpec.title) + assert.is_not_nil(activeItemSet) + assert.is_same(expectedName, activeItemSet.title) + assert.is_not_nil(activeSkillSet) + assert.is_same(expectedName, activeSkillSet.title) + assert.is_not_nil(activeConfigSet) + assert.is_same(expectedName, activeConfigSet.title) + end + + it("does not reorder loadouts when oldIndex is the same as newIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout A")) + + local spec = build.treeTab.specList[2] + t_remove(build.treeTab.specList, 2) + t_insert(build.treeTab.specList, 2, spec) + build:ReorderLoadout(2, 2) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout A", build.treeTab.specList[2].title) + assertActiveLoadoutByName("Loadout A") + assert.is_false(build.modFlag) + end) + + it("reorders loadouts when oldIndex is less than newIndex and activeSpec is at oldIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout A")) + + local spec = build.treeTab.specList[2] + t_remove(build.treeTab.specList, 2) + t_insert(build.treeTab.specList, 4, spec) + build:ReorderLoadout(2, 4) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout A", build.treeTab.specList[4].title) + assertActiveLoadoutByName("Loadout A") + assert.is_true(build.modFlag) + end) + + it("reorders loadouts when oldIndex is less than newIndex and activeSpec is before oldIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout C")) + + local spec = build.treeTab.specList[2] + t_remove(build.treeTab.specList, 2) + t_insert(build.treeTab.specList, 3, spec) + build:ReorderLoadout(2, 3) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout A", build.treeTab.specList[3].title) + assertActiveLoadoutByName("Loadout C") + assert.is_true(build.modFlag) + end) + + it("reorders loadouts when oldIndex is less than newIndex and activeSpec is after oldIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout B")) + + local spec = build.treeTab.specList[2] + t_remove(build.treeTab.specList, 2) + t_insert(build.treeTab.specList, 4, spec) + build:ReorderLoadout(2, 4) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout A", build.treeTab.specList[4].title) + assertActiveLoadoutByName("Loadout B") + assert.is_true(build.modFlag) + end) + + it("reorders loadouts when oldIndex is greater than newIndex and activeSpec is at oldIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout C")) + + local spec = build.treeTab.specList[4] + t_remove(build.treeTab.specList, 4) + t_insert(build.treeTab.specList, 2, spec) + build:ReorderLoadout(4, 2) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout C", build.treeTab.specList[2].title) + assertActiveLoadoutByName("Loadout C") + assert.is_true(build.modFlag) + end) + + it("reorders loadouts when oldIndex is greater than newIndex and activeSpec is before newIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout B")) + + local spec = build.treeTab.specList[4] + t_remove(build.treeTab.specList, 4) + t_insert(build.treeTab.specList, 2, spec) + build:ReorderLoadout(4, 2) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout C", build.treeTab.specList[2].title) + assertActiveLoadoutByName("Loadout B") + assert.is_true(build.modFlag) + end) + + it("reorders loadouts when oldIndex is greater than newIndex and activeSpec is after newIndex", function() + build:NewLoadout("Loadout A") + build:NewLoadout("Loadout B") + build:NewLoadout("Loadout C") + build.modFlag = false + + build:SetActiveLoadout(build:GetLoadoutByName("Loadout A")) + + local spec = build.treeTab.specList[4] + t_remove(build.treeTab.specList, 4) + t_insert(build.treeTab.specList, 3, spec) + build:ReorderLoadout(4, 3) + + assert.is_same(4, #build.treeTab.specList) + assert.is_same("Loadout C", build.treeTab.specList[3].title) + assertActiveLoadoutByName("Loadout A") + assert.is_true(build.modFlag) + end) + end) + end) + + describe("BuildSetService", function() + local buildSetService + before_each(function() + buildSetService = new("BuildSetService", build) + end) + + describe("NewLoadout", function() + it("creates a new loadout", function() + local loadoutName = "Loadout Name" + buildSetService:NewLoadout(loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_true(build.modFlag) + end) + end) + + describe("CopyLoadout", function() + it("copies an existing loadout and selects it", function() + local loadoutName = "Loadout Name" + buildSetService:CopyLoadout("Default", loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.are.equals(3, build.controls.buildLoadouts.selIndex) + assert.is_true(build.modFlag) + end) + end) + + describe("RenameLoadout", function() + it("renames an existing loadout", function() + local oldname = "New Loadout" + local newName = "Renamed Loadout" + buildSetService:NewLoadout(oldname) + buildSetService:RenameLoadout(oldname, newName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.is_same({ "Default", newName }, + { build.controls.buildLoadouts.list[2], build.controls.buildLoadouts.list[3] }) + assert.is_true(build.modFlag) + end) + end) + + describe("DeleteLoadout", function() + it("deletes the current loadout", function() + local loadoutNameToDelete = "Delete Me" + + buildSetService:NewLoadout(loadoutNameToDelete) + build:SetActiveLoadout(build:GetLoadoutByName(loadoutNameToDelete)) + local specIdToDelete = build:GetLoadoutByName(loadoutNameToDelete).specId + buildSetService:DeleteLoadout(2, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + assert.are.equals(6, #build.controls.buildLoadouts.list) + -- Default loadout return when only one loadout remains + assert.is_same({ itemSetId = 1, skillSetId = 1, configSetId = 1 }, + build:GetLoadoutByName(loadoutNameToDelete)) + end) + + it("deletes the loadout before the current", function() + local loadoutNameToDelete = "Default" + local currentLoadout = "Do not delete" + + buildSetService:NewLoadout(currentLoadout) + build:SetActiveLoadout(build:GetLoadoutByName(currentLoadout)) + local specIdToDelete = build:GetLoadoutByName(loadoutNameToDelete).specId + buildSetService:DeleteLoadout(1, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + assert.are.equals(6, #build.controls.buildLoadouts.list) + -- Default loadout return when only one loadout remains + assert.is_same({ itemSetId = 2, skillSetId = 2, configSetId = 2 }, + build:GetLoadoutByName(loadoutNameToDelete)) + assert.are.equals(2, build.controls.buildLoadouts.selIndex) + assert.are.equals("Do not delete", build.controls.buildLoadouts:GetSelValue()) + end) + + it("deletes the loadout after the current", function() + local loadoutNameToDelete = "Delete Me" + local currentLoadout = "Do not delete" + + buildSetService:NewLoadout(currentLoadout) + buildSetService:NewLoadout(loadoutNameToDelete) + build:SetActiveLoadout(build:GetLoadoutByName(currentLoadout)) + local specIdToDelete = build:GetLoadoutByName(loadoutNameToDelete).specId + buildSetService:DeleteLoadout(3, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.is_nil(build:GetLoadoutByName(loadoutNameToDelete)) + assert.are.equals(3, build.controls.buildLoadouts.selIndex) + assert.are.equals(currentLoadout, build.controls.buildLoadouts:GetSelValue()) + end) + end) + + describe("CustomLoadout", function() + it("creates a new loadout with default values (all -1)", function() + local loadoutName = "Custom Loadout" + buildSetService:CustomLoadout(-1, -1, -1, -1, loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_true(build.modFlag) + end) + + it("creates a new loadout by copying existing spec, item, skill, and config sets", function() + local loadoutName = "Custom Loadout from Existing" + local loadoutToCopy = build:GetLoadoutByName("Default") + buildSetService:CustomLoadout(loadoutToCopy.specId, loadoutToCopy.itemSetId, loadoutToCopy.skillSetId, + loadoutToCopy.configSetId, loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_true(build.modFlag) + end) + + it("creates new sets when id is -1, copies existing when id is valid", function() + -- Create initial custom loadout with all new sets + buildSetService:CustomLoadout(-1, -1, -1, -1, "All New Sets") + assert.are.equals(2, #build.treeTab.specList) + assert.are.equals(2, #build.skillsTab.skillSets) + assert.are.equals(2, #build.itemsTab.itemSets) + assert.are.equals(2, #build.configTab.configSets) + + -- Create second loadout mixing new and existing sets + local existingItemSet = build.itemsTab.itemSets[1] + local existingSkillSet = build.skillsTab.skillSets[1] + local existingConfigSet = build.configTab.configSets[1] + + local mixedLoadoutName = "Mixed Sets" + buildSetService:CustomLoadout(-1, existingItemSet.id, existingSkillSet.id, existingConfigSet.id, + mixedLoadoutName) + assert.are.equals(3, #build.treeTab.specList) + assert.are.equals(3, #build.skillsTab.skillSets) + assert.are.equals(3, #build.itemsTab.itemSets) + assert.are.equals(3, #build.configTab.configSets) + + local mixedLoadout = build:GetLoadoutByName(mixedLoadoutName) + local newSpec = build.treeTab.specList[mixedLoadout.specId] + local newItemSet = build.itemsTab.itemSets[build.itemsTab.itemSetOrderList[mixedLoadout.itemSetId]] + local newSkillSet = build.skillsTab.skillSets + [build.skillsTab.skillSetOrderList[mixedLoadout.skillSetId]] + local newConfigSet = build.configTab.configSets + [build.configTab.configSetOrderList[mixedLoadout.configSetId]] + assert.is_not.same(build.itemsTab.itemSetOrderList[mixedLoadout.itemSetId], existingItemSet.id) + assert.is_not.same(build.skillsTab.skillSetOrderList[mixedLoadout.skillSetId], existingSkillSet.id) + assert.is_not.same(build.configTab.configSetOrderList[mixedLoadout.configSetId], existingConfigSet.id) + assert.is_same(mixedLoadoutName, newSpec.title) + assert.is_same(mixedLoadoutName, newItemSet.title) + assert.is_same(mixedLoadoutName, newSkillSet.title) + assert.is_same(mixedLoadoutName, newConfigSet.title) + assert.is_true(build.modFlag) + end) + + it("sets the newly created loadout as selected", function() + local loadoutName = "Should Be Selected" + buildSetService:CustomLoadout(-1, -1, -1, -1, loadoutName) + assert.are.equals(3, build.controls.buildLoadouts.selIndex) + assert.are.equals(loadoutName, build.controls.buildLoadouts:GetSelValue()) + local loadout = build:GetLoadoutByName(loadoutName) + assert.is_not_nil(build.treeTab.specList[loadout.specId]) + assert.is_not_nil(build.itemsTab.itemSets[loadout.itemSetId]) + assert.is_not_nil(build.skillsTab.skillSets[loadout.skillSetId]) + assert.is_not_nil(build.configTab.configSets[loadout.configSetId]) + assert.is_true(build.modFlag) + end) + + it("handles copy with partial existing sets", function() + -- Create first custom loadout + buildSetService:CustomLoadout(-1, -1, -1, -1, "Custom 1") + assert.are.equals(7, #build.controls.buildLoadouts.list) + + -- Get the first loadout and copy only spec and item sets + local custom1 = build:GetLoadoutByName("Custom 1") + local existingItemSet = build.itemsTab.itemSets[1] + + buildSetService:CustomLoadout(custom1.specId, existingItemSet.id, -1, -1, "Custom 1 Modified") + assert.are.equals(8, #build.controls.buildLoadouts.list) + + -- Verify the loadout was created with new sets + local modifiedCustom = build:GetLoadoutByName("Custom 1 Modified") + assert.is_not_nil(modifiedCustom) + assert.is_true(build.modFlag) + end) + + it("works with copy of existing loadout combined with new sets", function() + local copyLoadout = build:GetLoadoutByName("Default") + buildSetService:CustomLoadout(copyLoadout.specId, -1, -1, -1, "Custom from Default") + assert.are.equals(7, #build.controls.buildLoadouts.list) + + -- Verify new sets were created + local customFromDefault = build:GetLoadoutByName("Custom from Default") + assert.is_not.same(customFromDefault.itemSetId, copyLoadout.itemSetId) + assert.is_not.same(customFromDefault.skillSetId, copyLoadout.skillSetId) + assert.is_not.same(customFromDefault.configSetId, copyLoadout.configSetId) + assert.is_true(build.modFlag) + end) + end) + + describe("Integration", function() + it("completes a loadout lifecycle", function() + -- 2 New Loadouts, Copy 2, Delete one of each + local newLoadout1 = "New Loadout 1" + local newLoadout2 = "New Loadout 2" + local copyLoadout1 = "Copy Loadout 1" + local copyLoadout2 = "Copy Loadout 2" + local deleteLoadout1 = newLoadout1 + local deleteLoadout2 = copyLoadout2 + local currentLoadout = newLoadout2 + + buildSetService:NewLoadout(newLoadout1) + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(newLoadout1, build.controls.buildLoadouts.list[3]) + + buildSetService:NewLoadout(newLoadout2) + assert.are.equals(8, #build.controls.buildLoadouts.list) + assert.are.equals(newLoadout2, build.controls.buildLoadouts.list[4]) + + build:SetActiveLoadout(build:GetLoadoutByName(currentLoadout)) + assert.are.equals(4, build.controls.buildLoadouts.selIndex) + + buildSetService:CopyLoadout(newLoadout1, copyLoadout1) + + assert.are.equals(9, #build.controls.buildLoadouts.list) + assert.are.equals(copyLoadout1, build.controls.buildLoadouts.list[5]) + assert.are.equals(5, build.controls.buildLoadouts.selIndex) + assert.is_true(build.modFlag) + + buildSetService:CopyLoadout(newLoadout2, copyLoadout2) + + assert.are.equals(10, #build.controls.buildLoadouts.list) + assert.are.equals(copyLoadout2, build.controls.buildLoadouts.list[6]) + assert.are.equals(6, build.controls.buildLoadouts.selIndex) + assert.is_true(build.modFlag) + + build:SetActiveLoadout(build:GetLoadoutByName(currentLoadout)) + assert.are.equals(4, build.controls.buildLoadouts.selIndex) + + local specIdToDelete = build:GetLoadoutByName(deleteLoadout1).specId + buildSetService:DeleteLoadout(2, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + + assert.are.equals(9, #build.controls.buildLoadouts.list) + assert.is_nil(build:GetLoadoutByName(deleteLoadout1)) + assert.are.equals(3, build.controls.buildLoadouts.selIndex) + assert.are.equals(currentLoadout, build.controls.buildLoadouts:GetSelValue()) + + local specIdToDelete = build:GetLoadoutByName(deleteLoadout2).specId + buildSetService:DeleteLoadout(4, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + + assert.are.equals(8, #build.controls.buildLoadouts.list) + assert.is_nil(build:GetLoadoutByName(deleteLoadout2)) + assert.are.equals(3, build.controls.buildLoadouts.selIndex) + assert.are.equals(currentLoadout, build.controls.buildLoadouts:GetSelValue()) + end) + + it("deletes all but the last loadout then copies it", function() + local loadoutNames = { "Loadout 1", "Loadout 2", "Loadout 3", "Loadout 4" } + for _, name in ipairs(loadoutNames) do + buildSetService:NewLoadout(name) + end + assert.are.equals(10, #build.controls.buildLoadouts.list) + + build:SetActiveLoadout(build:GetLoadoutByName(loadoutNames[3])) + + for i = 1, 4 do + local loadoutToDelete = build:GetLoadoutByName(build.controls.buildLoadouts.list[2]) + local specIdToDelete = loadoutToDelete.specId + buildSetService:DeleteLoadout(1, build.treeTab.specList, build.treeTab.specList[specIdToDelete]) + assert.are.equals(10 - i, #build.controls.buildLoadouts.list) + end + + assert.is_not_nil(build:GetLoadoutByName(loadoutNames[4])) + assert.are.equals(2, build.controls.buildLoadouts.selIndex) + assert.are.equals(loadoutNames[4], build.controls.buildLoadouts:GetSelValue()) + + for i = 1, 3 do + local copyLoadoutName = loadoutNames[i] .. " Copy" + buildSetService:CopyLoadout(loadoutNames[4], copyLoadoutName) + assert.are.equals(6 + i, #build.controls.buildLoadouts.list) + assert.are.equals(copyLoadoutName, build.controls.buildLoadouts.list[2 + i]) + end + end) + + it("New Loadout, delete config, rename default, create custom", function() + local newLoadoutName = "New Loadout" + buildSetService:NewLoadout(newLoadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + local newLoadout = build:GetLoadoutByName(newLoadoutName) + build.configTab:DeleteConfigSet(newLoadout.configSetId, 2) + build.configTab.configSets[1].title = "Config 1" + + local customLoadoutName = "Custom Loadout" + buildSetService:CustomLoadout(-1, -1, -1, 1, customLoadoutName) + assert.are.equals(6, #build.controls.buildLoadouts.list) + assert.are.equals(customLoadoutName, build.controls.buildLoadouts.list[2]) + assert.are.equals(customLoadoutName, build.controls.buildLoadouts:GetSelValue()) + local customLoadout = build:GetLoadoutByName(customLoadoutName) + assert.are.equals(2, customLoadout.configSetId) + assert.are.equals(3, customLoadout.itemSetId) + assert.are.equals(3, customLoadout.skillSetId) + assert.are.equals(3, customLoadout.specId) + assert.is_true(build.modFlag) + end) + + it("does not leave the config set in a broken state when deleting the last configset along with the loadout", + function() + local loadoutName = "Loadout To Delete" + buildSetService:NewLoadout(loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + build.configTab:DeleteConfigSet(build.configTab.configSetOrderList[1], 1) + assert.are.equals(1, #build.configTab.configSetOrderList) + local loadout = build:GetLoadoutByName(loadoutName) + buildSetService:DeleteLoadout(2, build.treeTab.specList, build.treeTab.specList[loadout.specId]) + assert.are.equals(1, #build.configTab.configSetOrderList) + assert.are.equals(6, #build.controls.buildLoadouts.list) + assert.are.equals("Default", build.controls.buildLoadouts.list[2]) + end) + + it("does not leave the item set in a broken state when deleting the last item set along with the loadout", + function() + local loadoutName = "Loadout To Delete" + buildSetService:NewLoadout(loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + build.itemsTab:DeleteItemSet(build.itemsTab.itemSetOrderList[1], 1) + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + local loadout = build:GetLoadoutByName(loadoutName) + buildSetService:DeleteLoadout(2, build.treeTab.specList, build.treeTab.specList[loadout.specId]) + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + assert.are.equals(6, #build.controls.buildLoadouts.list) + assert.are.equals("Default", build.controls.buildLoadouts.list[2]) + end) + + it("does not leave the skill set in a broken state when deleting the last skill set along with the loadout", + function() + local loadoutName = "Loadout To Delete" + buildSetService:NewLoadout(loadoutName) + assert.are.equals(7, #build.controls.buildLoadouts.list) + build.skillsTab:DeleteSkillSet(build.skillsTab.skillSetOrderList[1], 1) + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + local loadout = build:GetLoadoutByName(loadoutName) + buildSetService:DeleteLoadout(2, build.treeTab.specList, build.treeTab.specList[loadout.specId]) + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + assert.are.equals(6, #build.controls.buildLoadouts.list) + assert.are.equals("Default", build.controls.buildLoadouts.list[2]) + end) + end) + end) +end) diff --git a/spec/System/TestSkillsTab_spec.lua b/spec/System/TestSkillsTab_spec.lua new file mode 100644 index 0000000000..596eacaf3e --- /dev/null +++ b/spec/System/TestSkillsTab_spec.lua @@ -0,0 +1,375 @@ +describe("TestSkillsTab", function() + before_each(function() + newBuild() + runCallback("OnFrame") + end) + + describe("SkillsTab", function() + describe("NewSkillSet", function() + it("Creates a new skill set with specified ID", function() + local skillSetName = "New Skill Set" + local skillSetId = 2 + build.skillsTab:NewSkillSet(skillSetId, skillSetName) + + assert.are.equals(skillSetName, build.skillsTab.skillSets[skillSetId].title) + assert.are.equals(skillSetId, build.skillsTab.skillSets[skillSetId].id) + end) + + it("Assigns auto-incremented ID when not specified", function() + local skillSetName = "Auto ID Skill Set" + local skillSet1 = build.skillsTab:NewSkillSet() + local skillSet2 = build.skillsTab:NewSkillSet(nil, skillSetName) + + assert.are.equals(2, skillSet1.id) + assert.are.equals(3, skillSet2.id) + assert.are.equals(skillSetName, build.skillsTab.skillSets[skillSet2.id].title) + end) + + it("Adds set to order list", function() + local newTitle = "New Skill" + + build.skillsTab:NewSkillSet(nil, newTitle) + + assert.are.equals(2, #build.skillsTab.skillSetOrderList) + assert.are.equals(2, build.skillsTab.skillSetOrderList[2]) + end) + + it("Updates the mod flag", function() + local newTitle = "New Skill" + build.skillsTab.modFlag = false + + build.skillsTab:NewSkillSet(nil, newTitle) + + assert.is_true(build.skillsTab.modFlag) + end) + end) + + describe("CopySkillSet", function() + it("Copies a skill set with a new name", function() + local newTitle = "Copied Skill" + local newSkillSet = build.skillsTab:CopySkillSet(1, newTitle) + + assert.is_not.same(build.skillsTab.skillSets[1], newSkillSet) + assert.are.equals(newTitle, newSkillSet.title) + assert.are.same(newSkillSet.socketGroupList, build.skillsTab.skillSets[1].socketGroupList) + end) + + it("Deep copies all socket groups and gems", function() + local newTitle = "Copied Skill" + + local testGem = { nameSpec = "TestGem", level = 20 } + build.skillsTab.skillSets[1].socketGroupList = { { gemList = { testGem } } } + + local newSkillSet = build.skillsTab:CopySkillSet(1, newTitle) + + assert.are.same("TestGem", build.skillsTab.skillSets[1].socketGroupList[1].gemList[1].nameSpec) + assert.are.same(20, build.skillsTab.skillSets[1].socketGroupList[1].gemList[1].level) + build.skillsTab.skillSets[1].socketGroupList[1].gemList[1].nameSpec = "Modified" + + assert.are.equals("TestGem", newSkillSet.socketGroupList[1].gemList[1].nameSpec) + assert.are.equals(20, newSkillSet.socketGroupList[1].gemList[1].level) + end) + + it("Returns copied skill set with next ID", function() + local newTitle = "Copied Skill" + + local newSkillSet = build.skillsTab:CopySkillSet(1, newTitle) + + assert.are.equals(2, newSkillSet.id) + end) + + it("Adds copied skill set to order list", function() + local newTitle = "Copied Skill" + + build.skillsTab:CopySkillSet(1, newTitle) + + assert.are.equals(2, #build.skillsTab.skillSetOrderList) + assert.are.equals(2, build.skillsTab.skillSetOrderList[2]) + end) + + it("Updates the mod flag", function() + local newTitle = "Copied Skill" + build.skillsTab.modFlag = false + + build.skillsTab:CopySkillSet(1, newTitle) + + assert.is_true(build.skillsTab.modFlag) + end) + end) + + describe("RenameSkillSet", function() + it("Renames a skill set", function() + local newTitle = "Renamed Skill" + build.skillsTab:RenameSkillSet(1, newTitle) + + assert.are.equals(newTitle, build.skillsTab.skillSets[1].title) + end) + + it("Does not rename non-existent skill set", function() + build.skillsTab:RenameSkillSet(999, "Non-existent") + + assert.is_nil(build.skillsTab.skillSets[999]) + end) + + it("Updates the mod flag", function() + local newTitle = "Renamed Skill" + build.skillsTab.modFlag = false + + build.skillsTab:RenameSkillSet(1, newTitle) + + assert.is_true(build.skillsTab.modFlag) + end) + end) + + describe("DeleteSkillSet", function() + it("Deletes a skill set and its order entry", function() + local skillSetName = "Skill To Delete" + build.skillsTab:NewSkillSet(2, skillSetName) + + build.skillsTab:DeleteSkillSet(2, 2) + + assert.is_nil(build.skillsTab.skillSets[2]) + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + end) + + it("allows deletion of the last skill set", function() + local lastSkill = 1 + + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + build.skillsTab:DeleteSkillSet(lastSkill, 1) + + assert.are.equals(0, #build.skillsTab.skillSetOrderList) + assert.is_nil(build.skillsTab.skillSets[lastSkill]) + end) + end) + + describe("SetActiveSkillSet", function() + it("Switches to a valid skill set", function() + local skillSetName = "New Skill" + build.skillsTab:NewSkillSet(2, skillSetName) + + build.skillsTab:SetActiveSkillSet(2) + + assert.are.equals(2, build.skillsTab.activeSkillSetId) + assert.are.same(build.skillsTab.skillSets[2].socketGroupList, build.skillsTab.socketGroupList) + end) + + it("Defaults to first skill set if invalid ID provided", function() + build.skillsTab:SetActiveSkillSet(999) + + assert.are.equals(1, build.skillsTab.activeSkillSetId) + end) + end) + end) + + describe("SkillsSetService", function() + local skillsSetService + + before_each(function() + skillsSetService = new("SkillsSetService", build.skillsTab) + end) + + describe("NewSkillSet", function() + it("Creates a new skill set via service", function() + local skillSetName = "Service New Skill" + skillsSetService:NewSkillSet(skillSetName) + + assert.are.equals(2, #build.skillsTab.skillSetOrderList) + assert.are.equals(skillSetName, build.skillsTab.skillSets[2].title) + end) + + it("Sets newly created skill as active", function() + skillsSetService:NewSkillSet("New From Service") + + assert.are.equals(2, build.skillsTab.activeSkillSetId) + end) + + it("Adds an undo state", function() + local newTitle = "New Skill" + + local undoCountBefore = #build.skillsTab.undo + skillsSetService:NewSkillSet(newTitle) + + assert.are.equals(#build.skillsTab.undo, undoCountBefore + 1) + end) + end) + + describe("CopySkillSet", function() + it("Copies a skill set via service", function() + local skillSetName = "Copied via Service" + skillsSetService:CopySkillSet(1, skillSetName) + local skillSetId = build.skillsTab.skillSetOrderList[2] + + assert.are.equals(skillSetName, build.skillsTab.skillSets[skillSetId].title) + end) + + it("Sets copied skill set as active via service", function() + skillsSetService:NewSkillSet("Skill") + skillsSetService:CopySkillSet(1, "Service Copy") + local skillSetId = build.skillsTab.skillSetOrderList[3] + + assert.are.equals(skillSetId, build.skillsTab.activeSkillSetId) + end) + + it("Adds an undo state when copying a skill set", function() + local undoCountBefore = #build.skillsTab.undo + skillsSetService:CopySkillSet(1, "Copied Skill") + + assert.are.equals(#build.skillsTab.undo, undoCountBefore + 1) + end) + end) + + describe("RenameSkillSet", function() + it("Renames a skill set via service", function() + local skillSetName = "Original Name" + skillsSetService:NewSkillSet(skillSetName) + local skillSetId = build.skillsTab.skillSetOrderList[2] + + skillsSetService:RenameSkillSet(skillSetId, "New Name") + + assert.are.equals("New Name", build.skillsTab.skillSets[skillSetId].title) + end) + + it("Does not rename non-existent skill set", function() + skillsSetService:RenameSkillSet(10, "Non-existent") + + assert.is_nil(build.skillsTab.skillSets[10]) + end) + + it("Adds an undo state when renaming", function() + local skillSetName = "Skill To Rename" + skillsSetService:NewSkillSet(skillSetName) + local skillSetId = build.skillsTab.skillSetOrderList[2] + + local undoCountBefore = #build.skillsTab.undo + skillsSetService:RenameSkillSet(skillSetId, "Renamed Skill") + + assert.are.equals(#build.skillsTab.undo, undoCountBefore + 1) + end) + end) + + describe("DeleteSkillSet", function() + it("Deletes a skill set via service", function() + skillsSetService:NewSkillSet("Service Delete Me") + + skillsSetService:DeleteSkillSet(2, 2) + + assert.is_nil(build.skillsTab.skillSets[2]) + assert.is_nil(build.skillsTab.skillSetOrderList[2]) + end) + + it("Switches active skill set when deleting current", function() + skillsSetService:NewSkillSet("Service Delete Me") + local skillToKeep = 1 + local skillToDelete = 2 + + assert.are.equals(skillToDelete, build.skillsTab.activeSkillSetId) + + skillsSetService:DeleteSkillSet(skillToDelete, 2) + + assert.are.equals(skillToKeep, build.skillsTab.activeSkillSetId) + end) + + it("Does not delete last skill set via service", function() + skillsSetService:NewSkillSet("Last One") + + skillsSetService:DeleteSkillSet(1, 1) + + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + end) + + it("Adds an undo state when deleting", function() + local undoCountBefore = #build.skillsTab.undo + local undoCountAfterNew = #build.skillsTab.undo + skillsSetService:NewSkillSet("Undo Delete Test") + + skillsSetService:DeleteSkillSet(2, 2) + + assert.are.equals(#build.skillsTab.undo, undoCountBefore + 2) + end) + end) + + + + describe("Integration", function() + it("Completes a skill set lifecycle", function() + local skill1Name = "First Skill" + local skill2Name = "Second Skill" + + skillsSetService:NewSkillSet(skill1Name) + assert.are.equals("First Skill", build.skillsTab.skillSets[2].title) + + skillsSetService:CopySkillSet(2, skill2Name) + assert.are.equals("Second Skill", build.skillsTab.skillSets[3].title) + end) + end) + end) + + describe("SkillSetStateManagement", function() + local skillsSetService + + before_each(function() + skillsSetService = new("SkillsSetService", build.skillsTab) + end) + + describe("Socket group persistence", function() + it("Preserves socket groups across skill set switches", function() + skillsSetService:NewSkillSet("Set 1") + skillsSetService:NewSkillSet("Set 2") + + local testSocketGroup = { + label = "Test Group", + enabled = true, + gemList = { + { nameSpec = "TestGem", level = 20, quality = 0, qualityId = "Default", enabled = true, count = 1 } + } + } + + build.skillsTab.skillSets[1].socketGroupList[1] = testSocketGroup + build.skillsTab:SetActiveSkillSet(1) + + assert.are.same(testSocketGroup, build.skillsTab.skillSets[1].socketGroupList[1]) + + build.skillsTab:SetActiveSkillSet(3) + build.skillsTab.skillSets[3].socketGroupList[1] = { label = "Different" } + + build.skillsTab:SetActiveSkillSet(1) + assert.are.same(testSocketGroup, build.skillsTab.skillSets[1].socketGroupList[1]) + end) + + it("Preserves nested gem data across skill set switches", function() + skillsSetService:NewSkillSet("Set 1") + skillsSetService:NewSkillSet("Set 2") + skillsSetService:NewSkillSet("Set 3") + + local testGem = { nameSpec = "TestGem", level = 20, quality = 20 } + build.skillsTab.skillSets[3].socketGroupList = { { gemList = { testGem } } } + + build.skillsTab:SetActiveSkillSet(3) + assert.are.same(testGem, build.skillsTab.skillSets[3].socketGroupList[1].gemList[1]) + end) + end) + + describe("Default values", function() + it("Initializes new skill sets with default structure", function() + assert.is_not_nil(build.skillsTab.skillSets[1]) + assert.is_not_nil(build.skillsTab.skillSets[1].socketGroupList) + assert.is_not_nil(build.skillsTab.skillSets[1].id) + end) + + it("Copies socket group structure when copying skill sets", function() + local testSocketGroup = { + label = "Original", + enabled = false, + gemList = {} + } + build.skillsTab.skillSets[1].socketGroupList[1] = testSocketGroup + + local newSkillSet = build.skillsTab:CopySkillSet(1, "Copy Test") + + assert.are.equals("Original", newSkillSet.socketGroupList[1].label) + assert.is_false(newSkillSet.socketGroupList[1].enabled) + end) + end) + end) +end) diff --git a/src/Classes/BuildSetListControl.lua b/src/Classes/BuildSetListControl.lua new file mode 100644 index 0000000000..557914b632 --- /dev/null +++ b/src/Classes/BuildSetListControl.lua @@ -0,0 +1,231 @@ +-- Path of Building +-- +-- Class: Build Set List +-- Build set list control. +-- +local t_insert = table.insert +local t_remove = table.remove + +local BuildSetListClass = newClass("BuildSetListControl", "ListControl", function(self, anchor, rect, buildMode) + self.ListControl(anchor, rect, 16, "VERTICAL", true, buildMode.treeTab.specList) + self.buildMode = buildMode + self.buildSetService = new("BuildSetService", buildMode) + self.controls.new = new("ButtonControl", { "BOTTOMLEFT", self, "TOP" }, { -190, -4, 60, 18 }, "New", + function() + self:NewLoadout() + end) + self.controls.rename = new("ButtonControl", { "LEFT", self.controls.new, "RIGHT" }, { 5, 0, 60, 18 }, "Rename", + function() + self:RenameLoadout(self.selValue) + end) + self.controls.rename.enabled = function() + return self.selValue ~= nil + end + self.controls.copy = new("ButtonControl", { "LEFT", self.controls.rename, "RIGHT" }, { 5, 0, 60, 18 }, "Copy", + function() + local loadoutNameToCopy = self.selValue.title or "Default" + self:CopyLoadout(loadoutNameToCopy) + end) + self.controls.copy.enabled = function() + return self.selValue ~= nil + end + self.controls.delete = new("ButtonControl", { "LEFT", self.controls.copy, "RIGHT" }, { 5, 0, 60, 18 }, "Delete", + function() + self:DeleteLoadout(self.selIndex, self.selValue) + end) + self.controls.delete.enabled = function() + return self.selValue ~= nil and #self.list > 1 + end + self.controls.custom = new("ButtonControl", { "LEFT", self.controls.delete, "RIGHT" }, { 5, 0, 120, 18 }, + "New/Copy Custom", + function() + if self.selValue == nil then + self:CustomLoadout({ + specId = 0, + itemSetId = 0, + skillSetId = 0, + configSetId = 0, + }) + else + local loadoutNameToCopy = self.selValue.title or "Default" + local build = buildMode:GetLoadoutByName(loadoutNameToCopy) + self:CustomLoadout(build) + end + end) +end) + +function BuildSetListClass:RenameLoadout(spec) + local controls = {} + local specName = spec.title or "Default" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, specName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + local newTitle = controls.edit.buf + self.buildSetService:RenameLoadout(specName, newTitle) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, specName and "Rename Loadout" or "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:CopyLoadout(loadoutName) + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, loadoutName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.buildSetService:CopyLoadout(loadoutName, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, loadoutName and "Rename" or "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:CustomLoadout(build) + local function getList(setList, orderList, activeId) + local newSetList = { "^7New" } + local activeIndex = 1 + for index, setId in ipairs(orderList) do + local set = setList[setId] + t_insert(newSetList, set.title or "Default") + if setId == activeId then + activeIndex = index + 1 + end + end + return newSetList, activeIndex + end + local controls = {} + local buildName = build.specId > 0 and self.buildMode.treeTab.specList[build.specId].title or "New Loadout" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, buildName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + + local treeList = self.buildMode.treeTab:GetSpecList() + t_insert(treeList, 1, "^7New") + controls.treeDropDown = new("DropDownControl", nil, { 0, 90, 350, 20 }, treeList, function(index) + + end) + controls.treeDropDown:SetSel(build.specId + 1) + controls.treeLabel = new("LabelControl", { "BOTTOMLEFT", controls.treeDropDown, "TOPLEFT" }, { 4, -4, 0, 16 }, + "^7Copy from Tree:") + + local skillList, activeSkillIndex = getList(self.buildMode.skillsTab.skillSets, + self.buildMode.skillsTab.skillSetOrderList, build.skillSetId) + controls.skillDropDown = new("DropDownControl", nil, { 0, 140, 350, 20 }, skillList, function(index) + + end) + controls.skillDropDown:SetSel(activeSkillIndex) + controls.skillLabel = new("LabelControl", { "BOTTOMLEFT", controls.skillDropDown, "TOPLEFT" }, { 4, -4, 0, 16 }, + "^7Copy from Skill Set:") + + local itemList, activeItemIndex = getList(self.buildMode.itemsTab.itemSets, self.buildMode.itemsTab.itemSetOrderList, + build.itemSetId) + controls.itemDropDown = new("DropDownControl", nil, { 0, 190, 350, 20 }, itemList, function(index) + + end) + controls.itemDropDown:SetSel(activeItemIndex) + controls.itemLabel = new("LabelControl", { "BOTTOMLEFT", controls.itemDropDown, "TOPLEFT" }, { 4, -4, 0, 16 }, + "^7Copy from Item Set:") + + local configList, activeConfigIndex = getList(self.buildMode.configTab.configSets, + self.buildMode.configTab.configSetOrderList, build.configSetId) + controls.configDropDown = new("DropDownControl", nil, { 0, 240, 350, 20 }, configList, function(index) + + end) + controls.configDropDown:SetSel(activeConfigIndex) + controls.configLabel = new("LabelControl", { "BOTTOMLEFT", controls.configDropDown, "TOPLEFT" }, { 4, -4, 0, 16 }, + "^7Copy from Config Set:") + + controls.save = new("ButtonControl", nil, { -45, 270, 80, 20 }, "Save", function() + local treeIndex = controls.treeDropDown.selIndex + local itemIndex = controls.itemDropDown.selIndex + local skillIndex = controls.skillDropDown.selIndex + local configIndex = controls.configDropDown.selIndex + + local newSpecId = treeIndex == 1 and -1 or (treeIndex > 1 and treeIndex - 1) + local newItemSetId = itemIndex == 1 and -1 or + (itemIndex > 1 and self.buildMode.itemsTab.itemSetOrderList[itemIndex - 1] or 0) + local newSkillSetId = skillIndex == 1 and -1 or + (skillIndex > 1 and self.buildMode.skillsTab.skillSetOrderList[skillIndex - 1] or 0) + local newConfigSetId = configIndex == 1 and -1 or + (configIndex > 1 and self.buildMode.configTab.configSetOrderList[configIndex - 1] or 0) + print("Before CustomLoadout: ", newSpecId, newItemSetId, newSkillSetId, newConfigSetId) + self.buildSetService:CustomLoadout(newSpecId, newItemSetId, newSkillSetId, newConfigSetId, + controls.edit.buf) + print("After CustomLoadout: ", newSpecId, newItemSetId, newSkillSetId, newConfigSetId) + main:ClosePopup() + print("After ClosePopup: ", newSpecId, newItemSetId, newSkillSetId, newConfigSetId) + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 270, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 300, "Create Custom Loadout", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:NewLoadout() + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Loadout", nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.buildSetService:NewLoadout(controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:GetRowValue(column, index, spec) + if column == 1 then + local used = spec:CountAllocNodes() + return (spec.treeVersion ~= latestTreeVersion and ("[" .. treeVersions[spec.treeVersion].display .. "] ") or "") + .. (spec.title or "Default") + .. + " (" .. + (spec.curAscendClassName ~= "None" and spec.curAscendClassName or spec.curClassName) .. + ", " .. used .. " points)" + .. (index == self.buildMode.treeTab.activeSpec and " ^9(Current)" or "") + end +end + +function BuildSetListClass:OnSelClick(index, spec, doubleClick) + if doubleClick and index ~= self.buildMode.treeTab.activeSpec then + self.buildMode.controls.buildLoadouts:SetSel(index + 1) + end +end + +function BuildSetListClass:DeleteLoadout(index, spec) + if #self.list > 1 then + main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '" .. (spec.title or "Default") .. "'?", + "Delete", function() + self.buildSetService:DeleteLoadout(index, self.list, spec) + self.selIndex = nil + self.selValue = nil + end) + end +end + +function BuildSetListClass:OnSelKeyDown(index, spec, key) + if key == "F2" then + self:RenameLoadout(spec) + end +end + +function BuildSetListClass:OnOrderChange(oldIndex, newIndex) + self.buildSetService:ReorderLoadout(oldIndex, newIndex) +end diff --git a/src/Classes/BuildSetService.lua b/src/Classes/BuildSetService.lua new file mode 100644 index 0000000000..41543ce3f5 --- /dev/null +++ b/src/Classes/BuildSetService.lua @@ -0,0 +1,43 @@ +-- Path of Building +-- +-- Module: BuildSetService +-- Build set service for managing loadouts. + +-- Utility functions +local m_max = math.max +local t_insert = table.insert + + +local BuildSetServiceClass = newClass("BuildSetService", function(self, buildMode) + self.buildMode = buildMode +end) + +function BuildSetServiceClass:NewLoadout(name) + self.buildMode:NewLoadout(name) + self.buildMode:SyncLoadouts() + self.buildMode.controls.buildLoadouts:SetSel(1) +end + +function BuildSetServiceClass:CopyLoadout(copyLoadoutName, newName) + self.buildMode:CopyLoadout(copyLoadoutName, newName) +end + +function BuildSetServiceClass:RenameLoadout(oldName, newName) + self.buildMode:RenameLoadout(oldName, newName) + self.buildMode:SyncLoadouts() +end + +function BuildSetServiceClass:DeleteLoadout(index, list, spec) + local nextLoadoutIndex = index == self.buildMode.treeTab.activeSpec and (index > 1 and index - 1 or index + 1) or + self.buildMode.treeTab.activeSpec + local nextLoadout = list[nextLoadoutIndex] + self.buildMode:DeleteLoadout(spec.title or "Default", nextLoadout.title or "Default") +end + +function BuildSetServiceClass:CustomLoadout(specId, itemSetId, skillSetId, configSetId, name) + self.buildMode:CustomLoadout(specId, itemSetId, skillSetId, configSetId, name) +end + +function BuildSetServiceClass:ReorderLoadout(oldIndex, newIndex) + self.buildMode:ReorderLoadout(oldIndex, newIndex) +end diff --git a/src/Classes/ConfigSetListControl.lua b/src/Classes/ConfigSetListControl.lua index c38e809d76..5afd991f67 100644 --- a/src/Classes/ConfigSetListControl.lua +++ b/src/Classes/ConfigSetListControl.lua @@ -3,75 +3,95 @@ -- Class: Config Set List -- Config Set list control -- -local t_insert = table.insert -local t_remove = table.remove -local m_max = math.max local ConfigSetListClass = newClass("ConfigSetListControl", "ListControl", function(self, anchor, rect, configTab) self.ListControl(anchor, rect, 16, "VERTICAL", true, configTab.configSetOrderList) self.configTab = configTab - self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local configSet = configTab.configSets[self.selValue] - local newConfigSet = copyTable(configSet) - newConfigSet.id = 1 - while configTab.configSets[newConfigSet.id] do - newConfigSet.id = newConfigSet.id + 1 - end - configTab.configSets[newConfigSet.id] = newConfigSet - self:RenameSet(newConfigSet, true) + self.configSetService = new("ConfigSetService", configTab) + self.controls.copy = new("ButtonControl", { "BOTTOMLEFT", self, "TOP" }, { 2, -4, 60, 18 }, "Copy", function() + self:CopyConfigSet(self.selValue) end) self.controls.copy.enabled = function() return self.selValue ~= nil end - self.controls.delete = new("ButtonControl", {"LEFT",self.controls.copy,"RIGHT"}, {4, 0, 60, 18}, "Delete", function() - self:OnSelDelete(self.selIndex, self.selValue) - end) + self.controls.delete = new("ButtonControl", { "LEFT", self.controls.copy, "RIGHT" }, { 4, 0, 60, 18 }, "Delete", + function() + self:OnSelDelete(self.selIndex, self.selValue) + end) self.controls.delete.enabled = function() return self.selValue ~= nil and #self.list > 1 end - self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function() - self:RenameSet(configTab.configSets[self.selValue]) + self.controls.rename = new("ButtonControl", { "BOTTOMRIGHT", self, "TOP" }, { -2, -4, 60, 18 }, "Rename", function() + self:RenameConfigSet(self.selValue) end) self.controls.rename.enabled = function() return self.selValue ~= nil end - self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() - self:RenameSet(configTab:NewConfigSet(), true) - end) + self.controls.new = new("ButtonControl", { "RIGHT", self.controls.rename, "LEFT" }, { -4, 0, 60, 18 }, "New", + function() + self:CreateConfigSet() + end) end) -function ConfigSetListClass:RenameSet(configSet, addOnName) - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this config set:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, configSet.title, nil, nil, 100, function(buf) +function ConfigSetListClass:CreateConfigSet() + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for new config set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Config Set", nil, nil, 100, function(buf) controls.save.enabled = buf:match("%S") end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - configSet.title = controls.edit.buf - self.configTab.modFlag = true - if addOnName then - t_insert(self.list, configSet.id) - self.selIndex = #self.list - self.selValue = configSet.id - end - self.configTab:AddUndoState() - self.configTab.build:SyncLoadouts() + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.configSetService:NewConfigSet(controls.edit.buf) main:ClosePopup() end) controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - if addOnName then - self.configTab.configSets[configSet.id] = nil - end + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(370, 100, configSet.title and "Rename" or "Set Name", controls, "save", "edit", "cancel") + main:OpenPopup(370, 100, "Create Config Set", controls, "save", "edit", "cancel") +end + +function ConfigSetListClass:CopyConfigSet(selValue) + local configSet = self.configTab.configSets[selValue] + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this config set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, configSet.title, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.configSetService:CopyConfigSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Copy Config Set", controls, "save", "edit", "cancel") +end + +function ConfigSetListClass:RenameConfigSet(selValue) + local configSet = self.configTab.configSets[selValue] + local controls = {} + local specName = configSet.title or "Default" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this config set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, specName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.configSetService:RenameConfigSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, specName and "Rename Config Set" or "Set Name", controls, "save", "edit", "cancel") end function ConfigSetListClass:GetRowValue(column, index, configSetId) local configSet = self.configTab.configSets[configSetId] if column == 1 then - return (configSet.title or "Default") .. (configSetId == self.configTab.activeConfigSetId and " ^9(Current)" or "") + return (configSet.title or "Default") .. + (configSetId == self.configTab.activeConfigSetId and " ^9(Current)" or "") end end @@ -89,22 +109,18 @@ end function ConfigSetListClass:OnSelDelete(index, configSetId) local configSet = self.configTab.configSets[configSetId] if #self.list > 1 then - main:OpenConfirmPopup("Delete Config Set", "Are you sure you want to delete '"..(configSet.title or "Default").."'?", "Delete", function() - t_remove(self.list, index) - self.configTab.configSets[configSetId] = nil - self.selIndex = nil - self.selValue = nil - if configSetId == self.configTab.activeConfigSetId then - self.configTab:SetActiveConfigSet(self.list[m_max(1, index - 1)]) - end - self.configTab:AddUndoState() - self.configTab.build:SyncLoadouts() - end) + main:OpenConfirmPopup("Delete Config Set", + "Are you sure you want to delete '" .. (configSet.title or "Default") .. "'?", "Delete", function() + self.configSetService:DeleteConfigSet(configSetId, index) + + self.selIndex = nil + self.selValue = nil + end) end end function ConfigSetListClass:OnSelKeyDown(index, configSetId, key) if key == "F2" then - self:RenameSet(self.configTab.configSets[configSetId]) + self:RenameConfigSet(configSetId) end end diff --git a/src/Classes/ConfigSetService.lua b/src/Classes/ConfigSetService.lua new file mode 100644 index 0000000000..f1eb3f8906 --- /dev/null +++ b/src/Classes/ConfigSetService.lua @@ -0,0 +1,42 @@ +-- Path of Building +-- +-- Module: ConfigSetService +-- Config set service for managing config sets. +-- + +local m_max = math.max + +local ConfigSetServiceClass = newClass("ConfigSetService", function(self, configTab) + self.configTab = configTab +end) + +function ConfigSetServiceClass:NewConfigSet(name) + local configSet = self.configTab:NewConfigSet(nil, name) + self.configTab:SetActiveConfigSet(configSet.id, false, true) + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() +end + +function ConfigSetServiceClass:CopyConfigSet(configSetId, name) + local configSet = self.configTab:CopyConfigSet(configSetId, name) + self.configTab:SetActiveConfigSet(configSet.id, false, true) + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() +end + +function ConfigSetServiceClass:RenameConfigSet(configSetId, newName) + self.configTab:RenameConfigSet(configSetId, newName) + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() +end + +function ConfigSetServiceClass:DeleteConfigSet(configSetId, orderListIndex) + if #self.configTab.configSetOrderList > 1 then + self.configTab:DeleteConfigSet(configSetId, orderListIndex) + if configSetId == self.configTab.activeConfigSetId then + self.configTab:SetActiveConfigSet(self.configTab.configSetOrderList[m_max(1, orderListIndex - 1)], false, true) + end + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() + end +end diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index da28d4e3f6..1c5412d0bb 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -4,6 +4,8 @@ -- Configuration tab for the current build. -- local t_insert = table.insert +local t_remove = table.remove +local t_maxn = table.maxn local m_min = math.min local m_max = math.max local m_floor = math.floor @@ -25,7 +27,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont -- Initialise config sets self.configSets = { } self.configSetOrderList = { 1 } - self:NewConfigSet(1) + self:CreateConfigSet(1) self:SetActiveConfigSet(1, true) self.enemyLevel = 1 @@ -670,17 +672,17 @@ function ConfigTabClass:Load(xml, fileName) -- Catch special case of empty Config if xml.empty then - self:NewConfigSet(1, "Default") + self:CreateConfigSet(1, "Default") end for index, node in ipairs(xml) do if node.elem ~= "ConfigSet" then if not self.configSets[1] then - self:NewConfigSet(1, "Default") + self:CreateConfigSet(1, "Default") end setInputAndPlaceholder(node, 1) else local configSetId = tonumber(node.attrib.id) - self:NewConfigSet(configSetId, node.attrib.title or "Default") + self:CreateConfigSet(configSetId, node.attrib.title or "Default") self.configSetOrderList[index] = configSetId for _, child in ipairs(node) do setInputAndPlaceholder(child, configSetId) @@ -955,13 +957,10 @@ function ConfigTabClass:OpenConfigSetManagePopup() end -- Creates a new config set -function ConfigTabClass:NewConfigSet(configSetId, title) - local configSet = { id = configSetId, title = title, input = { }, placeholder = { } } +function ConfigTabClass:CreateConfigSet(configSetId, title) + local configSet = { id = configSetId, title = title, input = {}, placeholder = {} } if not configSetId then - configSet.id = 1 - while self.configSets[configSet.id] do - configSet.id = configSet.id + 1 - end + configSet.id = #self.configSets + 1 end -- there are default values for input and placeholder that every new config set needs to have for _, varData in ipairs(varList) do @@ -977,12 +976,49 @@ function ConfigTabClass:NewConfigSet(configSetId, title) return configSet end +-- Creates a new config set, adds it to the order list and sets the modFlag +function ConfigTabClass:NewConfigSet(configSetId, title) + local configSet = self:CreateConfigSet(configSetId, title) + t_insert(self.configSetOrderList, configSet.id) + self.modFlag = true + return configSet +end + +function ConfigTabClass:CopyConfigSet(configSetId, newConfigSetName) + local configSet = self.configSets[configSetId] + local newConfigSet = copyTable(configSet) + newConfigSet.id = #self.configSets + 1 + newConfigSet.title = newConfigSetName or configSet.title .. " (Copy)" + t_insert(self.configSets, newConfigSet) + t_insert(self.configSetOrderList, newConfigSet.id) + self.modFlag = true + return newConfigSet +end + +function ConfigTabClass:RenameConfigSet(configSetId, newTitle) + local configSet = self.configSets[configSetId] + + if not configSet then + return + end + + configSet.title = newTitle + self.modFlag = true +end + +-- Deletes a config set +function ConfigTabClass:DeleteConfigSet(configSetId, orderListIndex) + t_remove(self.configSetOrderList, orderListIndex) + self.configSets[configSetId] = nil + self.modFlag = true +end + -- Changes the active config set -function ConfigTabClass:SetActiveConfigSet(configSetId, init) +function ConfigTabClass:SetActiveConfigSet(configSetId, init, deferSync) -- Initialize config sets if needed if not self.configSetOrderList[1] then self.configSetOrderList[1] = 1 - self:NewConfigSet(1) + self:CreateConfigSet(1) end if not configSetId then @@ -1002,5 +1038,7 @@ function ConfigTabClass:SetActiveConfigSet(configSetId, init) self:BuildModList() end self.build.buildFlag = true - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end diff --git a/src/Classes/ItemSetListControl.lua b/src/Classes/ItemSetListControl.lua index 04d87e8537..872ef637fc 100644 --- a/src/Classes/ItemSetListControl.lua +++ b/src/Classes/ItemSetListControl.lua @@ -4,21 +4,13 @@ -- Item set list control. -- local t_insert = table.insert -local t_remove = table.remove -local m_max = math.max -local s_format = string.format local ItemSetListClass = newClass("ItemSetListControl", "ListControl", function(self, anchor, rect, itemsTab) self.ListControl(anchor, rect, 16, "VERTICAL", true, itemsTab.itemSetOrderList) self.itemsTab = itemsTab + self.itemSetService = new("ItemSetService", itemsTab) self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local newSet = copyTable(itemsTab.itemSets[self.selValue]) - newSet.id = 1 - while itemsTab.itemSets[newSet.id] do - newSet.id = newSet.id + 1 - end - itemsTab.itemSets[newSet.id] = newSet - self:RenameSet(newSet, true) + self:CopyItemSet(self.selValue) end) self.controls.copy.enabled = function() return self.selValue ~= nil @@ -30,43 +22,68 @@ local ItemSetListClass = newClass("ItemSetListControl", "ListControl", function( return self.selValue ~= nil and #self.list > 1 end self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function() - self:RenameSet(itemsTab.itemSets[self.selValue]) + self:RenameItemSet(self.selValue) end) self.controls.rename.enabled = function() return self.selValue ~= nil end self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() - local newSet = itemsTab:NewItemSet() - self:RenameSet(newSet, true) + self:CreateItemSet() end) end) -function ItemSetListClass:RenameSet(itemSet, addOnName) - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this item set:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, itemSet.title, nil, nil, 100, function(buf) +function ItemSetListClass:CreateItemSet() + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for new item set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Item Set", nil, nil, 100, function(buf) controls.save.enabled = buf:match("%S") end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - itemSet.title = controls.edit.buf - self.itemsTab.modFlag = true - if addOnName then - t_insert(self.list, itemSet.id) - self.selIndex = #self.list - self.selValue = itemSet.id - end - self.itemsTab:AddUndoState() - self.itemsTab.build:SyncLoadouts() + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.itemSetService:NewItemSet(controls.edit.buf) main:ClosePopup() end) controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - if addOnName then - self.itemsTab.itemSets[itemSet.id] = nil - end + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(370, 100, itemSet.title and "Rename" or "Set Name", controls, "save", "edit", "cancel") + main:OpenPopup(370, 100, "Create Item Set", controls, "save", "edit", "cancel") +end + +function ItemSetListClass:CopyItemSet(selValue) + local itemSet = self.itemsTab.itemSets[selValue] + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this item set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, itemSet.title, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.itemSetService:CopyItemSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Copy Item Set", controls, "save", "edit", "cancel") +end + +function ItemSetListClass:RenameItemSet(selValue) + local itemSet = self.itemsTab.itemSets[selValue] + local controls = {} + local setName = itemSet.title or "Default" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this item set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, setName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.itemSetService:RenameItemSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, setName and "Rename Item Set" or "Set Name", controls, "save", "edit", "cancel") end function ItemSetListClass:GetRowValue(column, index, itemSetId) @@ -119,23 +136,18 @@ end function ItemSetListClass:OnSelDelete(index, itemSetId) local itemSet = self.itemsTab.itemSets[itemSetId] if #self.list > 1 then - main:OpenConfirmPopup("Delete Item Set", "Are you sure you want to delete '"..(itemSet.title or "Default").."'?\nThis will not delete any items used by the set.", "Delete", function() - t_remove(self.list, index) - self.itemsTab.itemSets[itemSetId] = nil - self.selIndex = nil - self.selValue = nil - if itemSetId == self.itemsTab.activeItemSetId then - self.itemsTab:SetActiveItemSet(self.list[m_max(1, index - 1)]) - end - self.itemsTab:AddUndoState() - self.itemsTab.build:SyncLoadouts() - end) + main:OpenConfirmPopup("Delete Item Set", + "Are you sure you want to delete '" .. + (itemSet.title or "Default") .. "'?\nThis will not delete any items used by the set.", "Delete", function() + self.itemSetService:DeleteItemSet(itemSetId, index) + self.selIndex = nil + self.selValue = nil + end) end end function ItemSetListClass:OnSelKeyDown(index, itemSetId, key) - local itemSet = self.itemsTab.itemSets[itemSetId] if key == "F2" then - self:RenameSet(itemSet) + self:RenameItemSet(itemSetId) end end diff --git a/src/Classes/ItemSetService.lua b/src/Classes/ItemSetService.lua new file mode 100644 index 0000000000..57176b2c49 --- /dev/null +++ b/src/Classes/ItemSetService.lua @@ -0,0 +1,46 @@ +-- Path of Building +-- +-- Module: ItemSetService +-- Item set service for managing item sets. +-- + +local m_max = math.max + +local ItemSetServiceClass = newClass("ItemSetService", function(self, itemsTab) + self.itemsTab = itemsTab +end) + +function ItemSetServiceClass:NewItemSet(name) + local itemSet = self.itemsTab:NewItemSet(nil, name) + self.itemsTab:SetActiveItemSet(itemSet.id, true) + self.itemsTab:AddUndoState() + self.itemsTab.build:SyncLoadouts() + self.itemsTab.build.buildFlag = true +end + +function ItemSetServiceClass:CopyItemSet(itemSetId, name) + local itemSet = self.itemsTab:CopyItemSet(itemSetId, name) + self.itemsTab:SetActiveItemSet(itemSet.id, true) + self.itemsTab:AddUndoState() + self.itemsTab.build:SyncLoadouts() + self.itemsTab.build.buildFlag = true +end + +function ItemSetServiceClass:RenameItemSet(itemSetId, newName) + self.itemsTab:RenameItemSet(itemSetId, newName) + self.itemsTab:AddUndoState() + self.itemsTab.build:SyncLoadouts() + self.itemsTab.build.buildFlag = true +end + +function ItemSetServiceClass:DeleteItemSet(itemSetId, orderListIndex) + if #self.itemsTab.itemSetOrderList > 1 then + self.itemsTab:DeleteItemSet(itemSetId, orderListIndex) + if itemSetId == self.itemsTab.activeItemSetId then + self.itemsTab:SetActiveItemSet(self.itemsTab.itemSetOrderList[m_max(1, orderListIndex - 1)], true) + end + self.itemsTab:AddUndoState() + self.itemsTab.build:SyncLoadouts() + self.itemsTab.build.buildFlag = true + end +end diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index aaa060c8c7..e55f12df91 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -994,7 +994,7 @@ holding Shift will put it in the second.]]) -- Initialise item sets self.itemSets = { } self.itemSetOrderList = { 1 } - self:NewItemSet(1) + self:CreateItemSet(1, "Default") self:SetActiveItemSet(1) self:PopulateSlots() @@ -1066,8 +1066,7 @@ function ItemsTabClass:Load(xml, dbFileName) end end elseif node.elem == "ItemSet" then - local itemSet = self:NewItemSet(tonumber(node.attrib.id)) - itemSet.title = node.attrib.title + local itemSet = self:CreateItemSet(tonumber(node.attrib.id), node.attrib.title or "Default") itemSet.useSecondWeaponSet = node.attrib.useSecondWeaponSet == "true" for _, child in ipairs(node) do if child.elem == "Slot" then @@ -1095,7 +1094,7 @@ function ItemsTabClass:Load(xml, dbFileName) end end if not self.itemSetOrderList[1] then - self.activeItemSet = self:NewItemSet(1) + self.activeItemSet = self:CreateItemSet(1, "Default") self.activeItemSet.useSecondWeaponSet = xml.attrib.useSecondWeaponSet == "true" self.itemSetOrderList[1] = 1 end @@ -1334,8 +1333,8 @@ function ItemsTabClass:Draw(viewPort, inputEvents) end -- Creates a new item set -function ItemsTabClass:NewItemSet(itemSetId) - local itemSet = { id = itemSetId } +function ItemsTabClass:CreateItemSet(itemSetId, name) + local itemSet = { id = itemSetId, title = name } if not itemSetId then itemSet.id = 1 while self.itemSets[itemSet.id] do @@ -1351,9 +1350,48 @@ function ItemsTabClass:NewItemSet(itemSetId) return itemSet end +function ItemsTabClass:NewItemSet(itemSetId, name) + local itemSet = self:CreateItemSet(itemSetId, name) + t_insert(self.itemSetOrderList, itemSet.id) + self.modFlag = true + return itemSet +end + +function ItemsTabClass:CopyItemSet(sourceItemSetId, newItemSetName) + local newSet = copyTable(self.itemSets[sourceItemSetId]) + newSet.id = #self.itemSets + 1 + newSet.title = newItemSetName or newSet.title .. " (Copy)" + t_insert(self.itemSetOrderList, newSet.id) + self.modFlag = true + self.itemSets[newSet.id] = newSet + return newSet +end + +-- Renames an item set +function ItemsTabClass:RenameItemSet(itemSetId, newTitle) + local itemSet = self.itemSets[itemSetId] + + if not itemSet then + return + end + + itemSet.title = newTitle + self.modFlag = true +end + +-- Deletes an item set +function ItemsTabClass:DeleteItemSet(itemSetId, orderListIndex) + t_remove(self.itemSetOrderList, orderListIndex) + self.itemSets[itemSetId] = nil + self.modFlag = true +end + -- Changes the active item set -function ItemsTabClass:SetActiveItemSet(itemSetId) +function ItemsTabClass:SetActiveItemSet(itemSetId, deferSync) local prevSet = self.activeItemSet + if not self.itemSetOrderList[1] then + self:NewItemSet(1, "Default") + end if not self.itemSets[itemSetId] then itemSetId = self.itemSetOrderList[1] end @@ -1377,7 +1415,9 @@ function ItemsTabClass:SetActiveItemSet(itemSetId) end self.build.buildFlag = true self:PopulateSlots() - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end -- Equips the given item in the given item set diff --git a/src/Classes/PassiveSpecListControl.lua b/src/Classes/PassiveSpecListControl.lua index f227764d8f..28fce0a6d1 100644 --- a/src/Classes/PassiveSpecListControl.lua +++ b/src/Classes/PassiveSpecListControl.lua @@ -11,12 +11,8 @@ local PassiveSpecListClass = newClass("PassiveSpecListControl", "ListControl", f self.ListControl(anchor, rect, 16, "VERTICAL", true, treeTab.specList) self.treeTab = treeTab self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local newSpec = new("PassiveSpec", treeTab.build, self.selValue.treeVersion) - newSpec.title = self.selValue.title - newSpec.jewels = copyTable(self.selValue.jewels) - newSpec:RestoreUndoState(self.selValue:CreateUndoState()) - newSpec:BuildClusterJewelGraphs() - self:RenameSpec(newSpec, "Copy Tree", true) + local newSpec = treeTab:CopyTree(self.selIndex) + self:RenameSpec(newSpec, "Copy Tree") end) self.controls.copy.enabled = function() return self.selValue ~= nil @@ -43,7 +39,7 @@ local PassiveSpecListClass = newClass("PassiveSpecListControl", "ListControl", f self:UpdateItemsTabPassiveTreeDropdown() end) -function PassiveSpecListClass:RenameSpec(spec, title, addOnName) +function PassiveSpecListClass:RenameSpec(spec, popupTitle, addOnName) local controls = { } controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this passive tree:") controls.edit = new("EditControl", nil, {0, 40, 350, 20}, spec.title, nil, nil, 100, function(buf) @@ -66,7 +62,7 @@ function PassiveSpecListClass:RenameSpec(spec, title, addOnName) main:ClosePopup() end) -- main:OpenPopup(370, 100, spec.title and "Rename" or "Set Name", controls, "save", "edit") - main:OpenPopup(370, 100, title, controls, "save", "edit") + main:OpenPopup(370, 100, popupTitle, controls, "save", "edit") end function PassiveSpecListClass:GetRowValue(column, index, spec) diff --git a/src/Classes/SkillSetListControl.lua b/src/Classes/SkillSetListControl.lua index c8208c476d..043af1fa86 100644 --- a/src/Classes/SkillSetListControl.lua +++ b/src/Classes/SkillSetListControl.lua @@ -5,77 +5,92 @@ -- local t_insert = table.insert local t_remove = table.remove +local t_maxn = table.maxn local m_max = math.max local s_format = string.format local SkillSetListClass = newClass("SkillSetListControl", "ListControl", function(self, anchor, rect, skillsTab) self.ListControl(anchor, rect, 16, "VERTICAL", true, skillsTab.skillSetOrderList) self.skillsTab = skillsTab - self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local skillSet = skillsTab.skillSets[self.selValue] - local newSkillSet = copyTable(skillSet, true) - newSkillSet.socketGroupList = { } - for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do - local newGroup = copyTable(socketGroup, true) - newGroup.gemList = { } - for gemIndex, gem in pairs(socketGroup.gemList) do - newGroup.gemList[gemIndex] = copyTable(gem, true) - end - t_insert(newSkillSet.socketGroupList, newGroup) - end - newSkillSet.id = 1 - while skillsTab.skillSets[newSkillSet.id] do - newSkillSet.id = newSkillSet.id + 1 - end - skillsTab.skillSets[newSkillSet.id] = newSkillSet - self:RenameSet(newSkillSet, true) + self.skillsSetService = new("SkillsSetService", skillsTab) + self.controls.copy = new("ButtonControl", { "BOTTOMLEFT", self, "TOP" }, { 2, -4, 60, 18 }, "Copy", function() + self:CopySkillSet(self.selValue) end) self.controls.copy.enabled = function() return self.selValue ~= nil end - self.controls.delete = new("ButtonControl", {"LEFT",self.controls.copy,"RIGHT"}, {4, 0, 60, 18}, "Delete", function() - self:OnSelDelete(self.selIndex, self.selValue) - end) + self.controls.delete = new("ButtonControl", { "LEFT", self.controls.copy, "RIGHT" }, { 4, 0, 60, 18 }, "Delete", + function() + self:OnSelDelete(self.selIndex, self.selValue) + end) self.controls.delete.enabled = function() return self.selValue ~= nil and #self.list > 1 end - self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function() - self:RenameSet(skillsTab.skillSets[self.selValue]) + self.controls.rename = new("ButtonControl", { "BOTTOMRIGHT", self, "TOP" }, { -2, -4, 60, 18 }, "Rename", function() + self:RenameSkillSet(self.selValue) end) self.controls.rename.enabled = function() return self.selValue ~= nil end - self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() - self:RenameSet(skillsTab:NewSkillSet(), true) - end) + self.controls.new = new("ButtonControl", { "RIGHT", self.controls.rename, "LEFT" }, { -4, 0, 60, 18 }, "New", + function() + self:CreateSkillSet() + end) end) -function SkillSetListClass:RenameSet(skillSet, addOnName) - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this skill set:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, skillSet.title, nil, nil, 100, function(buf) +function SkillSetListClass:CreateSkillSet() + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for new skill set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Skill Set", nil, nil, 100, function(buf) controls.save.enabled = buf:match("%S") end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - skillSet.title = controls.edit.buf - self.skillsTab.modFlag = true - if addOnName then - t_insert(self.list, skillSet.id) - self.selIndex = #self.list - self.selValue = skillSet.id - end - self.skillsTab:AddUndoState() - self.skillsTab.build:SyncLoadouts() + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.skillsSetService:NewSkillSet(controls.edit.buf) main:ClosePopup() end) controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - if addOnName then - self.skillsTab.skillSets[skillSet.id] = nil - end + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() main:ClosePopup() end) - main:OpenPopup(370, 100, skillSet.title and "Rename" or "Set Name", controls, "save", "edit", "cancel") + main:OpenPopup(370, 100, "Create Skill Set", controls, "save", "edit", "cancel") +end + +function SkillSetListClass:CopySkillSet(selValue) + local skillSet = self.skillsTab.skillSets[selValue] + local controls = {} + local skillSetName = skillSet.title or "Default" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this skill set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, skillSetName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.skillsSetService:CopySkillSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Copy Skill Set", controls, "save", "edit", "cancel") +end + +function SkillSetListClass:RenameSkillSet(selValue) + local skillSet = self.skillsTab.skillSets[selValue] + local controls = {} + local skillSetName = skillSet.title or "Default" + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this skill set:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, skillSetName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.skillsSetService:RenameSkillSet(selValue, controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, skillSetName and "Rename Skill Set" or "Set Name", controls, "save", "edit", "cancel") end function SkillSetListClass:GetRowValue(column, index, skillSetId) @@ -99,22 +114,18 @@ end function SkillSetListClass:OnSelDelete(index, skillSetId) local skillSet = self.skillsTab.skillSets[skillSetId] if #self.list > 1 then - main:OpenConfirmPopup("Delete Item Set", "Are you sure you want to delete '"..(skillSet.title or "Default").."'?", "Delete", function() - t_remove(self.list, index) - self.skillsTab.skillSets[skillSetId] = nil - self.selIndex = nil - self.selValue = nil - if skillSetId == self.skillsTab.activeSkillSetId then - self.skillsTab:SetActiveSkillSet(self.list[m_max(1, index - 1)]) - end - self.skillsTab:AddUndoState() - self.skillsTab.build:SyncLoadouts() - end) + main:OpenConfirmPopup("Delete Skill Set", + "Are you sure you want to delete '" .. (skillSet.title or "Default") .. "'?", "Delete", function() + self.skillsSetService:DeleteSkillSet(skillSetId, index) + + self.selIndex = nil + self.selValue = nil + end) end end function SkillSetListClass:OnSelKeyDown(index, skillSetId, key) if key == "F2" then - self:RenameSet(self.skillsTab.skillSets[skillSetId]) + self:RenameSkillSet(skillSetId) end end diff --git a/src/Classes/SkillsSetService.lua b/src/Classes/SkillsSetService.lua new file mode 100644 index 0000000000..151fde1b66 --- /dev/null +++ b/src/Classes/SkillsSetService.lua @@ -0,0 +1,42 @@ +-- Path of Building +-- +-- Module: SkillsSetService +-- Skill set service for managing skill sets. +-- + +local m_max = math.max + +local SkillsSetServiceClass = newClass("SkillsSetService", function(self, skillsTab) + self.skillsTab = skillsTab +end) + +function SkillsSetServiceClass:NewSkillSet(name) + local skillSet = self.skillsTab:NewSkillSet(nil, name) + self.skillsTab:SetActiveSkillSet(skillSet.id, true) + self.skillsTab:AddUndoState() + self.skillsTab.build:SyncLoadouts() +end + +function SkillsSetServiceClass:CopySkillSet(skillSetId, name) + local skillSet = self.skillsTab:CopySkillSet(skillSetId, name) + self.skillsTab:SetActiveSkillSet(skillSet.id, true) + self.skillsTab:AddUndoState() + self.skillsTab.build:SyncLoadouts() +end + +function SkillsSetServiceClass:RenameSkillSet(skillSetId, newName) + self.skillsTab:RenameSkillSet(skillSetId, newName) + self.skillsTab:AddUndoState() + self.skillsTab.build:SyncLoadouts() +end + +function SkillsSetServiceClass:DeleteSkillSet(skillSetId, orderListIndex) + if #self.skillsTab.skillSetOrderList > 1 then + self.skillsTab:DeleteSkillSet(skillSetId, orderListIndex) + if skillSetId == self.skillsTab.activeSkillSetId then + self.skillsTab:SetActiveSkillSet(self.skillsTab.skillSetOrderList[m_max(1, orderListIndex - 1)], true) + end + self.skillsTab:AddUndoState() + self.skillsTab.build:SyncLoadouts() + end +end diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index a7dfedcc09..97f7734428 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -263,7 +263,7 @@ will automatically apply to the skill.]] -- Initialise skill sets self.skillSets = { } self.skillSetOrderList = { 1 } - self:NewSkillSet(1) + self:CreateSkillSet(1) self:SetActiveSkillSet(1) -- Skill gem slots @@ -419,13 +419,13 @@ function SkillsTabClass:Load(xml, fileName) -- Old format, initialize skill sets if needed if not self.skillSetOrderList[1] then self.skillSetOrderList[1] = 1 - self:NewSkillSet(1) + self:CreateSkillSet(1) end self:LoadSkill(node, 1) end if node.elem == "SkillSet" then - local skillSet = self:NewSkillSet(tonumber(node.attrib.id)) + local skillSet = self:CreateSkillSet(tonumber(node.attrib.id)) skillSet.title = node.attrib.title t_insert(self.skillSetOrderList, skillSet.id) for _, subNode in ipairs(node) do @@ -1321,25 +1321,67 @@ function SkillsTabClass:OpenSkillSetManagePopup() }) end --- Creates a new skill set -function SkillsTabClass:NewSkillSet(skillSetId) - local skillSet = { id = skillSetId, socketGroupList = {} } +-- Creates a new skill set without adding to order list +function SkillsTabClass:CreateSkillSet(skillSetId, title) + local skillSet = { id = skillSetId, title = title, socketGroupList = {} } if not skillSetId then - skillSet.id = 1 - while self.skillSets[skillSet.id] do - skillSet.id = skillSet.id + 1 - end + skillSet.id = #self.skillSets + 1 end self.skillSets[skillSet.id] = skillSet return skillSet end +-- Creates a new skill set with title, adds to order list and sets modFlag +function SkillsTabClass:NewSkillSet(skillSetId, title) + local skillSet = self:CreateSkillSet(skillSetId, title) + t_insert(self.skillSetOrderList, skillSet.id) + self.modFlag = true + return skillSet +end + +function SkillsTabClass:CopySkillSet(sourceSkillSetId, newSkillSetName) + local skillSet = self.skillSets[sourceSkillSetId] + local newSkillSet = copyTable(skillSet, true) + newSkillSet.title = newSkillSetName or skillSet.title .. " (Copy)" + newSkillSet.socketGroupList = {} + for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do + local newGroup = copyTable(socketGroup, true) + newGroup.gemList = {} + for gemIndex, gem in pairs(socketGroup.gemList) do + newGroup.gemList[gemIndex] = copyTable(gem, true) + end + t_insert(newSkillSet.socketGroupList, newGroup) + end + newSkillSet.id = #self.skillSets + 1 + self.skillSets[newSkillSet.id] = newSkillSet + t_insert(self.skillSetOrderList, newSkillSet.id) + self.modFlag = true + return newSkillSet +end + +function SkillsTabClass:RenameSkillSet(skillSetId, newTitle) + local skillSet = self.skillSets[skillSetId] + + if not skillSet then + return + end + + skillSet.title = newTitle + self.modFlag = true +end + +function SkillsTabClass:DeleteSkillSet(skillSetId, orderListIndex) + t_remove(self.skillSetOrderList, orderListIndex) + self.skillSets[skillSetId] = nil + self.modFlag = true +end + -- Changes the active skill set -function SkillsTabClass:SetActiveSkillSet(skillSetId) +function SkillsTabClass:SetActiveSkillSet(skillSetId, deferSync) -- Initialize skill sets if needed if not self.skillSetOrderList[1] then self.skillSetOrderList[1] = 1 - self:NewSkillSet(1) + self:CreateSkillSet(1) end if not skillSetId then @@ -1357,5 +1399,7 @@ function SkillsTabClass:SetActiveSkillSet(skillSetId) -- set the loadout option to the dummy option since it is now dirty self:SetDisplayGroup(self.socketGroupList[1]) - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index f596fb4daf..b21c8c435b 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -10,6 +10,7 @@ local t_insert = table.insert local t_remove = table.remove local t_sort = table.sort local t_concat = table.concat +local t_maxn = table.maxn local m_max = math.max local m_min = math.min local m_floor = math.floor @@ -535,7 +536,7 @@ function TreeTabClass:Save(xml) end end -function TreeTabClass:SetActiveSpec(specId) +function TreeTabClass:SetActiveSpec(specId, deferSync) local prevSpec = self.build.spec self.activeSpec = m_min(specId, #self.specList) local curSpec = self.specList[self.activeSpec] @@ -570,7 +571,9 @@ function TreeTabClass:SetActiveSpec(specId) if self.controls.versionSelect then self.controls.versionSelect:SelByValue(curSpec.treeVersion, 'value') end - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end function TreeTabClass:SetCompareSpec(specId) @@ -641,6 +644,17 @@ function TreeTabClass:OpenSpecManagePopup() }) end +function TreeTabClass:CopyTree(sourceSpecId, newSpecName) + local newSpec = new("PassiveSpec", self.build, self.specList[sourceSpecId].treeVersion) + newSpec.title = newSpecName or self.specList[sourceSpecId].title .. " (Copy)" + newSpec.jewels = copyTable(self.specList[sourceSpecId].jewels) + newSpec:RestoreUndoState(self.specList[sourceSpecId]:CreateUndoState()) + newSpec:BuildClusterJewelGraphs() + newSpec.id = #self.specList + 1 + t_insert(self.specList, newSpec) + return newSpec +end + function TreeTabClass:OpenVersionConvertPopup(version, ignoreTreeSubType) local controls = { } controls.warningLabel = new("LabelControl", nil, {0, 20, 0, 16}, "^7Warning: some or all of the passives may be de-allocated due to changes in the tree.\n\n" .. diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 7173c2fb4b..21fdda7474 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -7,6 +7,7 @@ local pairs = pairs local ipairs = ipairs local next = next local t_insert = table.insert +local t_remove = table.remove local t_sort = table.sort local m_min = math.min local m_max = math.max @@ -299,103 +300,12 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildLoadouts:SetSel(1) return end - if value == "^7^7New Loadout" then - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) - controls.save.enabled = buf:match("%S") - end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - local loadout = controls.edit.buf - - local newSpec = new("PassiveSpec", self, latestTreeVersion) - newSpec.title = loadout - t_insert(self.treeTab.specList, newSpec) - - local itemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) - t_insert(self.itemsTab.itemSetOrderList, itemSet.id) - itemSet.title = loadout - - local skillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) - t_insert(self.skillsTab.skillSetOrderList, skillSet.id) - skillSet.title = loadout - - local configSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) - t_insert(self.configTab.configSetOrderList, configSet.id) - configSet.title = loadout - - self:SyncLoadouts() - self.modFlag = true - main:ClosePopup() - end) - controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") - - self.controls.buildLoadouts:SetSel(1) - return - end - - -- item, skill, and config sets have identical structure - -- return id as soon as it's found - local function findSetId(setOrderList, value, sets, setSpecialLinks) - for _, setOrder in ipairs(setOrderList) do - if value == (sets[setOrder].title or "Default") then - return setOrder - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil - end - - -- trees have a different structure with id/name pairs - -- return id as soon as it's found - local function findNamedSetId(treeList, value, setSpecialLinks) - for id, spec in ipairs(treeList) do - if value == spec then - return id - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil + if value == "^7^7Manage" then + self:OpenBuildSetManagePopup() end - local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 - local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 - local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 - - local newSpecId = findNamedSetId(self.treeTab:GetSpecList(), value, self.treeListSpecialLinks) - local newItemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, value, self.itemsTab.itemSets, self.itemListSpecialLinks) - local newSkillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, value, self.skillsTab.skillSets, self.skillListSpecialLinks) - local newConfigId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, value, self.configTab.configSets, self.configListSpecialLinks) - - -- if exact match nor special grouping cannot find setIds, bail - if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then - return - end - - if newSpecId ~= self.treeTab.activeSpec then - self.treeTab:SetActiveSpec(newSpecId) - end - if newItemId ~= self.itemsTab.activeItemSetId then - self.itemsTab:SetActiveItemSet(newItemId) - end - if newSkillId ~= self.skillsTab.activeSkillSetId then - self.skillsTab:SetActiveSkillSet(newSkillId) - end - if newConfigId ~= self.configTab.activeConfigSetId then - self.configTab:SetActiveConfigSet(newConfigId) - end + local loadout = self:GetLoadoutByName(value) + self:SetActiveLoadout(loadout) self.controls.buildLoadouts:SelByValue(value) end) @@ -818,10 +728,9 @@ function buildMode:SyncLoadouts() end end end - -- giving the options unique formatting so it can not match with user-created sets t_insert(filteredList, "^7^7-----") - t_insert(filteredList, "^7^7New Loadout") + t_insert(filteredList, "^7^7Manage") t_insert(filteredList, "^7^7Sync") t_insert(filteredList, "^7^7Help >>") @@ -857,6 +766,230 @@ function buildMode:SyncLoadouts() return treeList, itemList, skillList, configList end +function buildMode:NewLoadout(loadoutName) + local newSpec = new("PassiveSpec", self, latestTreeVersion) + local newItemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1, loadoutName) + local newSkillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1, loadoutName) + local newConfigSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1, loadoutName) + + t_insert(self.treeTab.specList, newSpec) + newSpec.title = loadoutName + + self.modFlag = true +end + +function buildMode:CopyLoadout(copyLoadoutName, loadoutName) + local loadout = self:GetLoadoutByName(copyLoadoutName) + if not loadout then return end + + local newSpec = self.treeTab:CopyTree(loadout.specId, loadoutName) + local newItemSet = self.itemsTab:CopyItemSet(loadout.itemSetId, loadoutName) + local newSkillSet = self.skillsTab:CopySkillSet(loadout.skillSetId, loadoutName) + local newConfigSet = self.configTab:CopyConfigSet(loadout.configSetId, loadoutName) + + local copyLoadout = self:GetLoadoutByName(loadoutName) + self:SetActiveLoadout(copyLoadout) + + self.modFlag = true + return newSpec, newItemSet, newSkillSet, newConfigSet +end + +function buildMode:CustomLoadout(specId, itemSetId, skillSetId, configSetId, name) + local newSpec + if specId == -1 then + newSpec = new("PassiveSpec", self, latestTreeVersion) + newSpec.id = #self.treeTab.specList + 1 + t_insert(self.treeTab.specList, newSpec) + else + newSpec = self.treeTab:CopyTree(specId, name) + end + newSpec.title = name + + local newItemSet + if itemSetId == -1 then + newItemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1, name) + else + newItemSet = self.itemsTab:CopyItemSet(itemSetId, name) + end + + local newSkillSet + if skillSetId == -1 then + newSkillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1, name) + else + newSkillSet = self.skillsTab:CopySkillSet(skillSetId, name) + end + newSkillSet.title = name + + local newConfigSet + if configSetId == -1 then + newConfigSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1, name) + else + newConfigSet = self.configTab:CopyConfigSet(configSetId, name) + end + + local customLoadout = self:GetLoadoutByName(name) + self:SetActiveLoadout(customLoadout) + + self.modFlag = true + return newSpec, newItemSet, newSkillSet, newConfigSet +end + +function buildMode:DeleteLoadout(loadoutName, nextLoadoutName) + local function reverseLookup(setOrderList, value) + for id, set in ipairs(setOrderList) do + if set == value then + return id + end + end + return nil + end + + local loadout = self:GetLoadoutByName(loadoutName) + if loadout.specId then + t_remove(self.treeTab.specList, loadout.specId) + end + if loadout.itemSetId then + local index = reverseLookup(self.itemsTab.itemSetOrderList, loadout.itemSetId) + self.itemsTab:DeleteItemSet(loadout.itemSetId, index) + end + if loadout.skillSetId then + local index = reverseLookup(self.skillsTab.skillSetOrderList, loadout.skillSetId) + self.skillsTab:DeleteSkillSet(loadout.skillSetId, index) + end + if loadout.configSetId then + local index = reverseLookup(self.configTab.configSetOrderList, loadout.configSetId) + self.configTab:DeleteConfigSet(loadout.configSetId, index) + end + self.modFlag = true + + local nextLoadout = self:GetLoadoutByName(nextLoadoutName) + self:SetActiveLoadout(nextLoadout) +end + +function buildMode:RenameLoadout(oldName, newName) + local loadout = self:GetLoadoutByName(oldName) + if loadout.specId then + self.treeTab.specList[loadout.specId].title = newName + self.treeTab.modFlag = true + end + if loadout.itemSetId then + self.itemsTab:RenameItemSet(loadout.itemSetId, newName) + end + if loadout.skillSetId then + self.skillsTab:RenameSkillSet(loadout.skillSetId, newName) + end + if loadout.configSetId then + self.configTab:RenameConfigSet(loadout.configSetId, newName) + end + self.modFlag = true +end + +function buildMode:GetLoadoutByName(loadoutName) + -- item, skill, and config sets have identical structure + -- return id as soon as it's found + local function findSetId(setOrderList, value, sets, setSpecialLinks) + for _, setOrder in ipairs(setOrderList) do + if value == (sets[setOrder].title or "Default") then + return setOrder + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + -- trees have a different structure with id/name pairs + -- return id as soon as it's found + local function findNamedSetId(treeList, value, setSpecialLinks) + for id, spec in ipairs(treeList) do + if value == spec then + return id + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 + local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 + local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 + + local specId = findNamedSetId(self.treeTab:GetSpecList(), loadoutName, self.treeListSpecialLinks) + local itemId = oneItem and self.itemsTab.itemSetOrderList[1] or + findSetId(self.itemsTab.itemSetOrderList, loadoutName, self.itemsTab.itemSets, self.itemListSpecialLinks) + local skillId = oneSkill and self.skillsTab.skillSetOrderList[1] or + findSetId(self.skillsTab.skillSetOrderList, loadoutName, self.skillsTab.skillSets, self.skillListSpecialLinks) + local configId = oneConfig and self.configTab.configSetOrderList[1] or + findSetId(self.configTab.configSetOrderList, loadoutName, self.configTab.configSets, self.configListSpecialLinks) + + if not specId and not itemId and not skillId and not configId then + return nil + end + + return { + specId = specId, + itemSetId = itemId, + skillSetId = skillId, + configSetId = configId + } +end + +function buildMode:SetActiveLoadout(loadout) + if not loadout then + return + end + + local newSpecId, newItemId, newSkillId, newConfigId = loadout.specId, loadout.itemSetId, loadout.skillSetId, + loadout.configSetId + if newSpecId == nil then + return + end + + if newSpecId ~= self.treeTab.activeSpec then + self.treeTab:SetActiveSpec(newSpecId, true) + end + self.itemsTab:SetActiveItemSet(newItemId, true) + self.skillsTab:SetActiveSkillSet(newSkillId, true) + self.configTab:SetActiveConfigSet(newConfigId, false, true) + self:SyncLoadouts() + + for i, loadoutName in ipairs(self.controls.buildLoadouts.list) do + if loadoutName == (self.treeTab.specList[newSpecId].title) then + self.controls.buildLoadouts:SetSel(i) + break + end + end +end + +function buildMode:ReorderLoadout(oldIndex, newIndex) + if oldIndex == newIndex then + return + end + + self.modFlag = true + + local activeLoadout = self.treeTab.activeSpec + + local minVal = m_min(oldIndex, newIndex) + local maxVal = m_max(oldIndex, newIndex) + if activeLoadout < minVal or activeLoadout > maxVal then return end + + if oldIndex == activeLoadout then + activeLoadout = newIndex + elseif minVal <= activeLoadout and activeLoadout < maxVal then + activeLoadout = activeLoadout + (oldIndex > newIndex and 1 or -1) + end + + self:SetActiveLoadout(self:GetLoadoutByName(self.treeTab.specList[activeLoadout].title or "Default")) +end + function buildMode:EstimatePlayerProgress() local PointsUsed, AscUsed, SecondaryAscUsed = self.spec:CountAllocNodes() local extra = self.calcsTab.mainOutput and self.calcsTab.mainOutput.ExtraPoints or 0 @@ -1960,4 +2093,15 @@ function buildMode:SaveDBFile() end end +-- Opens the build set manager +function buildMode:OpenBuildSetManagePopup() + main:OpenPopup(400, 290, "Manage Loadouts", { + new("BuildSetListControl", nil, { 0, 50, 380, 200 }, self), + new("ButtonControl", nil, { 0, 260, 90, 20 }, "Done", function() + main:ClosePopup() + self.controls.buildLoadouts:SetSel(self.treeTab.activeSpec + 1) + end), + }) +end + return buildMode