From a19691912ca62efad934cbda51f6bb3ff41840b0 Mon Sep 17 00:00:00 2001 From: Rasmus Kromann-Larsen Date: Sun, 31 May 2026 13:25:48 +0200 Subject: [PATCH] Handle new quest_stats value sent as aggregated instead of exact lines. Add Tribal Medicine quest --- spec/System/TestImportTab_spec.lua | 116 +++++++++++++++++++++++++++++ src/Classes/ImportTab.lua | 63 ++++++++++------ src/Data/QuestRewards.lua | 16 +++- 3 files changed, 171 insertions(+), 24 deletions(-) diff --git a/spec/System/TestImportTab_spec.lua b/spec/System/TestImportTab_spec.lua index b887aabd1a..24c06eb321 100644 --- a/spec/System/TestImportTab_spec.lua +++ b/spec/System/TestImportTab_spec.lua @@ -44,3 +44,119 @@ describe("ImportTab", function() assert.True(importTab.controls.charSelect.list[1].detail:match("Future Ascendancy") ~= nil) end) end) + +describe("ImportTab quest reward import", function() + before_each(function() + newBuild() + end) + + local function findQuest(info) + for _, quest in ipairs(data.questRewards) do + if quest.Info == info then + return "quest" .. quest.Description .. quest.Area .. quest.Info, quest + end + end + error("quest reward not found for Info: " .. info) + end + + local function importStats(questStats) + build.importTab:ImportQuestRewardConfig(questStats) + return build.configTab.input + end + + it("decomposes 0.5 rolled-up totals across the contributing quests", function() + local input = importStats({ + "+20 to maximum Life", + "+60 to [Spirit|Spirit]", + "+15% to [Resistances|Cold Resistance]", + "+15% to [Resistances|Fire Resistance]", + "+15% to [Resistances|Lightning Resistance]", + }) + + assert.is_true(input[(findQuest("Candlemass"))]) + assert.is_true(input[(findQuest("King In The Mists"))]) + assert.is_true(input[(findQuest("Ignagduk"))]) + assert.is_true(input[(findQuest("Beira"))]) + assert.is_true(input[(findQuest("Blackjaw"))]) + assert.is_true(input[(findQuest("Sisters of Garukhan Shrine"))]) + + local tasalioVar, tasalio = findQuest("Tasalio's Test") + assert.are.equals(tasalio.Options[2], input[tasalioVar]) + local ngamahuVar, ngamahu = findQuest("Ngamahu's Test") + assert.are.equals(ngamahu.Options[2], input[ngamahuVar]) + local tawhoaVar, tawhoa = findQuest("Tawhoa's Test") + assert.are.equals(tawhoa.Options[2], input[tawhoaVar]) + + assert.is_false(input[(findQuest("Lythara"))]) + end) + + it("sums duplicate 0.4 per-line Spirit rewards the same as the 0.5 summed total", function() + local input = importStats({ + "+30 to [Spirit|Spirit]", + "+30 to [Spirit|Spirit]", + "+40 to [Spirit|Spirit]", + }) + assert.is_true(input[(findQuest("King In The Mists"))]) + assert.is_true(input[(findQuest("Ignagduk"))]) + assert.is_true(input[(findQuest("Lythara"))]) + end) + + it("ticks exactly one of the interchangeable +30 Spirit quests when only one is claimed", function() + local input = importStats({ "+30 to [Spirit|Spirit]" }) + local king = input[(findQuest("King In The Mists"))] + local ignagduk = input[(findQuest("Ignagduk"))] + assert.is_false(input[(findQuest("Lythara"))]) + assert.are.equals(1, (king and 1 or 0) + (ignagduk and 1 or 0)) + end) + + it("disambiguates Spirit total 70 to one +30 plus Lythara, not 30+30", function() + local input = importStats({ "+70 to [Spirit|Spirit]" }) + assert.is_true(input[(findQuest("Lythara"))]) + local king = input[(findQuest("King In The Mists"))] + local ignagduk = input[(findQuest("Ignagduk"))] + assert.are.equals(1, (king and 1 or 0) + (ignagduk and 1 or 0)) + end) + + it("disambiguates Spirit total 40 to Lythara alone, not a +30", function() + local input = importStats({ "+40 to [Spirit|Spirit]" }) + assert.is_true(input[(findQuest("Lythara"))]) + assert.is_false(input[(findQuest("King In The Mists"))]) + assert.is_false(input[(findQuest("Ignagduk"))]) + end) + + it("selects the correct multi-line option and leaves unclaimed option quests at None", function() + local medallionVar, medallion = findQuest("Medallion") + local input = importStats({ + "30% increased [Charm] Effect Duration", + "+1 [Charm] Slot", + }) + assert.are.equals(medallion.Options[2], input[medallionVar]) + assert.are.equals("None", input[(findQuest("Seven Pillars"))]) + assert.is_false(input[(findQuest("Beira"))]) + end) + + it("credits Tribal Medicine's Kaom option, not Seven Pillars which shares the stat at 15%", function() + local tribalVar, tribal = findQuest("Tribal Medicine") + local input = importStats({ "30% increased Global [Armour], [Evasion] and [EnergyShield|Energy Shield]" }) + assert.are.equals(tribal.Options[1], input[tribalVar]) + assert.are.equals("None", input[(findQuest("Seven Pillars"))]) + end) + + it("selects Tribal Medicine's Rakiata multi-stat option", function() + local tribalVar, tribal = findQuest("Tribal Medicine") + local input = importStats({ + "+15% of [Armour|Armour] also applies to [ElementalDamage|Elemental Damage]", + "Gain [Deflect|Deflection Rating] equal to 12% of [Evasion|Evasion Rating]", + "12% [FasterESRechargeStart|faster start of Energy Shield Recharge]", + }) + assert.are.equals(tribal.Options[2], input[tribalVar]) + end) + + it("decomposes Global Armour, Evasion and Energy Shield shared by Tribal Medicine (30%) and Seven Pillars (15%)", function() + local tribalVar, tribal = findQuest("Tribal Medicine") + local sevenVar, seven = findQuest("Seven Pillars") + local input = importStats({ "45% increased Global [Armour], [Evasion] and [EnergyShield|Energy Shield]" }) + assert.are.equals(tribal.Options[1], input[tribalVar]) + assert.are.equals(seven.Options[3], input[sevenVar]) + end) +end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index c4c404f68a..450d3ac00e 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -5,12 +5,13 @@ -- local ipairs = ipairs local t_insert = table.insert -local t_remove = table.remove local b_rshift = bit.rshift local band = bit.band local m_max = math.max local dkjson = require "dkjson" +local tradeHelpers = LoadModule("Classes/TradeHelpers") + local realmList = { { label = "PoE2", id = "PoE2", realmCode = "poe2", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, } @@ -646,39 +647,57 @@ end function ImportTabClass:ImportQuestRewardConfig(questStats) local configTab = self.build.configTab - local statLines = {} + + -- Reduce a stat line to a numberless key + value (e.g. "+30 to [Spirit|Spirit]" -> "+# to spirit", 30) + local function statKey(text) + text = escapeGGGString(text):lower():gsub("^%s+", ""):gsub("%s+$", "") + return tradeHelpers.modLineTemplate(text), tradeHelpers.modLineValue(text) or 0 + end + + local statTotals = {} for _, stat in ipairs(questStats) do - t_insert(statLines, escapeGGGString(stat):lower()) + local key, value = statKey(stat) + statTotals[key] = (statTotals[key] or 0) + value + end + + -- Stats shared by 3+ quests can't be split greedily (two +30 Spirit quests make 40/70 ambiguous), + -- so resolve those by exact total then zero their totals. + local disambiguation = { + ["+# to spirit"] = { + [30] = { "King In The Mists" }, + [40] = { "Lythara" }, + [60] = { "King In The Mists", "Ignagduk" }, + [70] = { "King In The Mists", "Lythara" }, + [100] = { "King In The Mists", "Ignagduk", "Lythara" }, + }, + } + local resolved = {} + for stat, byTotal in pairs(disambiguation) do + local taken = byTotal[statTotals[stat] or 0] + if taken then + for _, info in ipairs(taken) do resolved[info] = true end + statTotals[stat] = 0 + end end local function splitLine(text) local out = {} - for line in text:gmatch("[^\r\n]+") do - line = line:gsub("^%s+", ""):lower() - t_insert(out, line) + for line in tostring(text):gmatch("[^\r\n]+") do + local key, value = statKey(line) + t_insert(out, { key = key, value = value }) end return out end - -- Ensure all required lines exist, then remove them so they can't match again + -- True if the totals still hold every line of the reward; consume them so a later quest can't reclaim it local function matchQuest(requiredLines) - local indices = {} - for _, needed in ipairs(requiredLines) do - local found - for idx, line in ipairs(statLines) do - if line == needed then - found = idx - break - end - end - if not found then + for _, line in ipairs(requiredLines) do + if (statTotals[line.key] or 0) < line.value then return false end - t_insert(indices, found) end - table.sort(indices, function(a, b) return a > b end) - for _, idx in ipairs(indices) do - t_remove(statLines, idx) + for _, line in ipairs(requiredLines) do + statTotals[line.key] = statTotals[line.key] - line.value end return true end @@ -688,7 +707,7 @@ function ImportTabClass:ImportQuestRewardConfig(questStats) if quest.useConfig == true then local var = "quest" .. quest.Description .. quest.Area .. quest.Info if quest.Stat then - local matches = matchQuest(splitLine(quest.Stat)) + local matches = resolved[quest.Info] or matchQuest(splitLine(quest.Stat)) if configTab.input[var] ~= matches then configTab.input[var] = matches updated = true diff --git a/src/Data/QuestRewards.lua b/src/Data/QuestRewards.lua index 0b763986a2..0bef14f902 100644 --- a/src/Data/QuestRewards.lua +++ b/src/Data/QuestRewards.lua @@ -157,6 +157,18 @@ return { ["AreaLevel"] = 51, ["useConfig"] = true }, + { + ["Act"] = 4, + ["Description"] = "Act 4", + ["Area"] = "Eye of Hinekora", + ["Info"] = "Tribal Medicine", + ["Options"] = { + "30% increased Global Armour, Evasion and Energy Shield", + "+15% of Armour also applies to Elemental Damage\n\tGain Deflection Rating equal to 12% of Evasion Rating\n\t12% faster start of Energy Shield Recharge", + }, + ["AreaLevel"] = 51, + ["useConfig"] = true + }, { ["Act"] = 4, ["Description"] = "Act 4", @@ -252,11 +264,11 @@ return { ["Options"] = { "+5% to all Elemental Resistances", "3% increased Movement Speed", - "15% increased Global Defences", + "15% increased Global Armour, Evasion and Energy Shield", "20% increased Presence Area Of Effect", "12% increased Cooldown Recovery Rate", "+5 to all Attributes", - "5% increased Experience Gain\n\t-5% to Elemental Resistances\n\t3% reduced Movement Speed\n\t15% reduced Global Defences\n\t20% reduced Presence Area Of Effect\n\t12% reduced Cooldown Recovery Rate\n\t5% reduced Attributes", + "5% increased Experience Gain\n\t-5% to all Elemental Resistances\n\t3% reduced Movement Speed\n\t15% reduced Global Armour, Evasion and Energy Shield\n\t20% reduced Presence Area Of Effect\n\t12% reduced Cooldown Recovery Rate\n\t5% reduced Attributes", }, ["AreaLevel"] = 63, ["useConfig"] = true