diff --git a/Modules/QuestiePlayer.lua b/Modules/QuestiePlayer.lua index 8909379..13b5d08 100644 --- a/Modules/QuestiePlayer.lua +++ b/Modules/QuestiePlayer.lua @@ -52,12 +52,27 @@ function QuestiePlayer:Initialize() end end ---Always compare to the UnitLevel parameter, returning the highest. +-- Keep the cached level aligned with the reported level so prestige resets can +-- move back to level 1. QuestiePlayer:GetPlayerLevel() still defends against +-- temporary client desync by comparing against UnitLevel at read time. ---@param level Level function QuestiePlayer:SetPlayerLevel(level) - if level == nil then return end - local localLevel = UnitLevel("player"); - _QuestiePlayer.playerLevel = math_max(localLevel, level); + if level == nil then return false end + + local localLevel = UnitLevel("player") or level + local previousLevel = _QuestiePlayer.playerLevel + local isPrestigeReset = level == 1 and previousLevel > 1 and localLevel == 1 + + _QuestiePlayer.playerLevel = level + + if isPrestigeReset then + local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest") + if QuestieQuest and QuestieQuest.SmoothReset then + QuestieQuest:SmoothReset() + end + end + + return isPrestigeReset end -- Gets the highest playerlevel available, most of the time playerLevel should be the most correct one diff --git a/Tests/QuestiePlayerPrestigeReset_spec.lua b/Tests/QuestiePlayerPrestigeReset_spec.lua new file mode 100644 index 0000000..57f59ec --- /dev/null +++ b/Tests/QuestiePlayerPrestigeReset_spec.lua @@ -0,0 +1,40 @@ +describe("QuestiePlayer prestige reset", function() + local resetCalls + + before_each(function() + dofile("Tests/wow_api_mock.lua") + + resetCalls = 0 + QuestieQuest.SmoothReset = function() + resetCalls = resetCalls + 1 + end + + dofile("Modules/QuestiePlayer.lua") + QuestiePlayer.private.playerLevel = 70 + end) + + it("drops the cached level to 1 and refreshes quest state when prestige restarts", function() + _G.UnitLevel = function() + return 1 + end + + local resetTriggered = QuestiePlayer:SetPlayerLevel(1) + + assert.is_true(resetTriggered) + assert.equals(1, QuestiePlayer.private.playerLevel) + assert.equals(1, resetCalls) + end) + + it("does not treat an ordinary level 1 login as a prestige reset", function() + QuestiePlayer.private.playerLevel = 1 + _G.UnitLevel = function() + return 1 + end + + local resetTriggered = QuestiePlayer:SetPlayerLevel(1) + + assert.is_false(resetTriggered) + assert.equals(1, QuestiePlayer.private.playerLevel) + assert.equals(0, resetCalls) + end) +end) diff --git a/Tests/wow_api_mock.lua b/Tests/wow_api_mock.lua new file mode 100644 index 0000000..34caef0 --- /dev/null +++ b/Tests/wow_api_mock.lua @@ -0,0 +1,221 @@ +-- Tests/wow_api_mock.lua +-- Minimal mock of World of Warcraft API for Busted unit tests + +_G = _G or {} + +local function _bit_fallback(op, a, b) + local ok, bit32lib = pcall(function() return bit32 end) + if ok and bit32lib then + return bit32lib[op](a, b) + end + + -- Minimal fallback for the few bitwise operations Questie touches in tests. + local result, bitval = 0, 1 + while a > 0 or (b and b > 0) do + local abit = a % 2 + local bbit = b and (b % 2) or 0 + local out = 0 + if op == "band" then + out = (abit == 1 and bbit == 1) and 1 or 0 + elseif op == "bor" then + out = (abit == 1 or bbit == 1) and 1 or 0 + elseif op == "bxor" then + out = ((abit == 1) ~= (bbit == 1)) and 1 or 0 + end + if out == 1 then + result = result + bitval + end + a = math.floor(a / 2) + if b then + b = math.floor(b / 2) + end + bitval = bitval * 2 + end + return result +end + +_G.bit = _G.bit or { + band = function(a, b) return _bit_fallback("band", a, b) end, + bor = function(a, b) return _bit_fallback("bor", a, b) end, + bxor = function(a, b) return _bit_fallback("bxor", a, b) end, + lshift = function(a, b) return a * (2 ^ b) end, + rshift = function(a, b) return math.floor(a / (2 ^ b)) end, + arshift = function(a, b) return math.floor(a / (2 ^ b)) end, +} + +-- Mock Globals +_G.Questie = { + DEBUG_LEARNER = "LEARNER", + DEBUG_DEVELOP = "DEVELOP", + db = { + global = { + learnedData = { + npcs = {}, + quests = {}, + items = {}, + objects = {}, + settings = { + learnQuests = true, + learnNPCs = true, + learnItems = true, + learnObjects = true, + } + } + }, + profile = { + learnedData = { + settings = { + learnQuests = true, + learnNPCs = true, + } + } + } + }, + dbLearner = { + global = { + npcs = {}, + quests = {}, + items = {}, + objects = {}, + settings = { + enabled = true, + prioritizeMyData = true, + minConfidencePins = 2, + }, + } + }, + Debug = function(self, level, ...) + -- print("[" .. tostring(level) .. "]", ...) + end, + Print = function(self, ...) + -- print(...) + end, + Error = function(self, ...) + -- print("[ERROR]", ...) + end +} + +_G.QuestieLoader = { + ImportModule = function(self, name) + if name == "QuestieDB" then return _G.QuestieDB end + if name == "QuestieQuest" then return _G.QuestieQuest end + if name == "QuestiePlayer" then return _G.QuestiePlayer end + if name == "QuestLogCache" then return _G.QuestLogCache end + if name == "QuestieLib" then return {} end + if name == "QuestieCompat" then return _G.QuestieCompat end + if name == "ZoneDB" then return _G.ZoneDB end + if name == "l10n" then return _G.l10n end + if name == "QuestieTooltips" then return _G.QuestieTooltips end + return {} + end, + CreateModule = function(self, name) + _G[name] = name == "QuestiePlayer" and { private = {} } or {} + return _G[name] + end +} + +_G.QuestieDB = { + npcDataOverrides = {}, + objectDataOverrides = {}, + questDataOverrides = {}, + itemDataOverrides = {}, + QueryNPCSingle = function() return nil end, + GetQuest = function() return nil end, +} + +_G.QuestieQuest = {} + +_G.QuestieCompat = { + GetCurrentPlayerPosition = function() return 1, 0.5, 0.5 end, + C_QuestLog = { + IsQuestFlaggedCompleted = function() return false end, + }, + GetQuestLogTitle = function(index) + return "Test Quest", 1, nil, false, nil, nil, nil, 123 + end, + GetQuestTagInfo = function() return nil end, + IsPlayerSpell = function() return false end, + IsSpellKnownOrOverridesKnown = function() return false end, + IsQuestFlaggedCompleted = function() return false end, + C_Timer = { + After = function(delay, fn) fn() end, + NewTicker = function(delay, fn) return { Cancel = function() end } end, + }, +} + +_G.QuestiePlayer = { + GetPlayerLevel = function() return 70 end, +} + +_G.QuestLogCache = { + GetQuestID = function() return 123 end, + GetQuest = function(_, questId) + return nil + end, + GetQuestObjectives = function(self, questId) + return _G._mock_questObjectives or { + { text = "Slay 10 felboars", Icon = 136006, objectiveType = "slay" }, + { text = "Collect 5 herbs", Icon = 134217, objectiveType = "loot" }, + } + end, +} + +_G.ZoneDB = { + zoneIDs = setmetatable({ + ICECROWN = 1, + }, { + __index = function() + return 1 + end, + }), + GetAreaIdByUiMapId = function(self, uiMapId) + -- Sunstrider Isle (1241) → Eversong Woods (3430) + if uiMapId == 1241 then return 3430 end + return nil + end, +} + +_G.l10n = { + GetAreaId = function(self) + return 3430 + end, +} + +_G.QuestieTooltips = { + RegisterObjectiveTooltip = function(self, questId, identifier, data) + _G._lastRegisteredTooltip = { questId = questId, identifier = identifier, data = data } + end, +} + +_G.C_Map = { + GetBestMapForUnit = function(unit) + return _G._mock_uiMapId or 530 + end, + GetPlayerMapPosition = function(uiMapId, unit) + return 0.5, 0.5 + end, +} + +-- string.trim is a WoW API extension +_G.string.trim = function(s) + if s then return s:match("^%s*(.-)%s*$") or "" end + return "" +end + +_G.GetNumQuestLogEntries = function() return 1 end + +_G.GetTime = function() return os.time() end +_G.time = os.time +_G.floor = math.floor +_G.UnitName = function(unit) return "TestUnit" end +_G.UnitLevel = function(unit) return 70 end +_G.UnitGUID = function(unit) return "Creature-0-1234-567-89-1000-0000000000" end +_G.UnitFactionGroup = function(unit) return "Alliance" end +_G.GetRealZoneText = function() return "Shadowmoon Valley" end +_G.GetInstanceInfo = function() return "Shadowmoon Valley", nil, nil, nil, nil, nil, nil, 530 end +_G.CreateFrame = function() return { RegisterEvent = function() end, SetScript = function() end } end +_G.GetPlayerMapPosition = function(unit) + return 0.5, 0.5 +end + +return _G