diff --git a/API/ClassicAPI.lua b/API/ClassicAPI.lua index 39ab28f..04b6109 100644 --- a/API/ClassicAPI.lua +++ b/API/ClassicAPI.lua @@ -42,6 +42,9 @@ function A:Validate() queryQuestsCompleted = type(QueryQuestsCompleted)=="function", questFlaggedCompleted = type(IsQuestFlaggedCompleted)=="function" or Has(C_QuestLog,"IsQuestFlaggedCompleted"), mapWorldSize = Has(C_Map,"GetMapWorldSize"), + instanceInfo = type(GetInstanceInfo)=="function", + leaderboardObjectiveID = type(GetQuestLogLeaderBoardID)=="function", + areaTriggerInfo = Has(C_Map,"GetAreaTriggerInfo"), } return self.valid @@ -109,6 +112,51 @@ function A:GetBestMapForPlayer() return C_Map.GetBestMapForUnit("player") end +-- ClassicAPI backports the modern GetInstanceInfo() tuple to Vanilla 1.12. +-- Keep instance classification behind the API contract instead of teaching +-- presentation modules about DLL/global availability details. +function A:GetInstanceType() + if type(GetInstanceInfo)~="function" then return "none" end + local ok,name,instanceType=pcall(GetInstanceInfo) + if not ok or type(instanceType)~="string" then return "none" end + return instanceType +end + +function A:IsInDungeonOrRaid() + local instanceType=self:GetInstanceType() + return instanceType=="party" or instanceType=="raid" +end + +-- pfQuest-classicAPI resolves quest-bound exploration objectives directly from +-- ClassicAPI's AreaTrigger.dbc bridge. Keep this behind the API contract so +-- generic map code never needs to know whether the DLL/global exists. +function A:GetAreaTriggerInfo(areaTriggerID) + areaTriggerID=tonumber(areaTriggerID) + if not areaTriggerID or not C_Map or type(C_Map.GetAreaTriggerInfo)~="function" then return nil end + + local ok,info=pcall(C_Map.GetAreaTriggerInfo,areaTriggerID) + if not ok or type(info)~="table" then return nil end + + local mapID=tonumber(info.areaID) + local x=tonumber(info.mapX) + local y=tonumber(info.mapY) + if not mapID or not x or not y then return nil end + + return { + id=areaTriggerID, + areaID=mapID, + mapX=x, + mapY=y + } +end + + +function A:GetQuestLogLeaderBoardID(objectiveIndex,questLogIndex) + if type(GetQuestLogLeaderBoardID)~="function" then return nil end + local ok,id=pcall(GetQuestLogLeaderBoardID,objectiveIndex,questLogIndex) + if not ok then return nil end + return tonumber(id) +end function A:GetQuestObjectives(questID,questLogIndex) if C_QuestLog and type(C_QuestLog.GetQuestObjectives)=="function" then diff --git a/Core/Bootstrap.lua b/Core/Bootstrap.lua index 809f5ef..881b6ce 100644 --- a/Core/Bootstrap.lua +++ b/Core/Bootstrap.lua @@ -7,7 +7,7 @@ function QuestieOcto:OnFoundationServiceReady() if self.Database.ready and self.DatabaseAPI:IsReady() and self.Completion.ready and self.QuestLog.snapshot then self.ready=true local q=self.DatabaseAPI:GetQuestCount() - self:Print("foundation ready - "..tostring(q).." quests, "..tostring(self.Completion:Count()).. + self:Print("Foundation ready - "..tostring(q).." quests, "..tostring(self.Completion:Count()).. " completed, "..tostring(self.QuestLog.stats.resolved).." active quest IDs resolved.") self:SendMessage("FOUNDATION_READY") end diff --git a/Data/pfDB/overwrites-octo.lua b/Data/pfDB/overwrites-octo.lua index 5f6034e..2972ba7 100644 --- a/Data/pfDB/overwrites-octo.lua +++ b/Data/pfDB/overwrites-octo.lua @@ -1326,6 +1326,15 @@ do -- units/npcs -- Barltok Kettleburn (Dun Agrath) pfDB["units"]["data-turtle"][52068]["coords"] = { [1] = { 21, 71.4, 11, 120 } } + + -- Hurl Cinderfist (Restless, 41640): his live world spawn at Rugford's + -- Mountain Rest falls inside both Turtle's expanded Dun Morogh map and the + -- overlapping Grim Reaches map. Keep both map representations so the quest + -- objective is visible from either local map without changing server truth. + pfDB["units"]["data-turtle"][62200]["coords"] = { + [1] = { 83.2, 70.8, 1, 300 }, + [2] = { 4.1, 92, 5602, 300 }, + } end do -- quests @@ -1523,6 +1532,13 @@ do -- items end do -- quests + -- Onu is meditating (960): stale legacy pfDB quest-giver relation. + -- The supplied Turtle server data retains the quest template but has no live + -- creature/object/item quest-start relation for 960. Quest 961 is the actual + -- repeatable Onu helper quest and keeps its active-944 prerequisite. Remove + -- 960 from the merged runtime quest database without altering 961. + pfDB["quests"]["data-turtle"][960] = "_" + -- Add "Dark Iron Gunpowder Keg" to objectives of "Vile Dwarven Pigs" -- The extractor scripts did not catch those, as the objetive is filled via a Gossip on use. pfDB["quests"]["data-turtle"][41682]["obj"]["O"] = { 2020173 } @@ -1537,3 +1553,30 @@ end -- cost 166 silently dropped corrections before 1.0.2. patchtable.lua -- reports it. pfDB["octo-overwrites-complete"] = true + +do -- Cloth reputation donations: event 159 is a permanent content-release gate, not quest event membership. + -- Verified against the supplied Turtle server data and live server behavior in August 2026. + -- Keep every other quest field intact: faction/race restrictions, prerequisites, objectives, + -- levels and the repeatable classification of the "Additional Runecloth" follow-ups. + -- + -- Silvermoon Remnants (80370-80374) and Revantusk Tribe (80375-80379) already ship + -- without event metadata, so only the legacy capital-city donation chains need correction. + local clothDonationQuestIDs = { + 7791, 7792, 7793, 7794, 7795, 7796, + 7798, 7799, 7800, 7801, + 7802, 7803, 7804, 7805, 7806, + 7807, 7808, 7809, 7811, 7812, + 7813, 7814, 7817, 7818, 7819, + 7820, 7821, 7822, 7823, 7824, 7825, + 7826, 7827, 7831, 7832, + 7833, 7834, 7835, 7836, 7837, + } + + for _, questID in ipairs(clothDonationQuestIDs) do + local quest = pfDB["quests"]["data-turtle"][questID] + if type(quest) == "table" then + quest["event"] = nil + end + end +end + diff --git a/Database/Database.lua b/Database/Database.lua index 7f2d2a8..182abc0 100644 --- a/Database/Database.lua +++ b/Database/Database.lua @@ -259,6 +259,11 @@ function DB:GetCreatureFaction(id) return raw and (raw["fac"] or raw["faction"]) or nil end +function DB:GetObjectFaction(id) + local raw=self:GetObjectRaw(id) + return raw and (raw["fac"] or raw["faction"]) or nil +end + function DB:GetMapIDByName(name) if not self.ready or not name then return nil end diff --git a/Database/QuestModel.lua b/Database/QuestModel.lua index 4d89448..527bd5a 100644 --- a/Database/QuestModel.lua +++ b/Database/QuestModel.lua @@ -9,7 +9,19 @@ QM.cache = {} local function CopyArray(src) if not src then return nil end local out={} - for _,v in pairs(src) do table.insert(out,v) end + local keys={} + + -- These sources are arrays, but pairs() does not guarantee their numeric + -- order on Lua 5.0. Preserve the database/compiler order explicitly so + -- objective #1/#2 cannot be swapped before BuildObjectiveData consumes it. + for k in pairs(src) do + if type(k)=="number" then table.insert(keys,k) end + end + table.sort(keys) + + for i=1,table.getn(keys) do + table.insert(out,src[keys[i]]) + end return out end @@ -29,12 +41,37 @@ local ITEM_OBJECTIVE_FIRST={ [5088]=true, } --- Quests that are not normal static NPC offers. Questie 6 blacklists 7946 --- because Morja only exposes it after the Dark Iron Ale/Jubjub interaction. +-- Quests that are not normal static NPC offers. Questie 6 blacklists these +-- because the NPC only exposes the quest after a scripted world interaction. +-- Keeping the real starter relation is useful for quest truth/tooltips, but it +-- must not become a permanent map pickup marker. local CONDITIONAL_OFFERS={ + [3861]="Use /chicken on a Chicken until it temporarily offers CLUCK!.", [7946]="Requires Jubjub to be lured back with Dark Iron Ale.", } +-- Some situational quests benefit from one deliberately chosen discovery marker +-- without pretending every scripted source is a permanent questgiver. CLUCK! can +-- be triggered from many Chickens, but the Westfall Chicken at 55.6,30.9 is used +-- as the single representative pickup/turn-in marker so new players can discover +-- that the quest exists without covering every Chicken spawn with quest icons. +-- The underlying starter/finisher relation remains NPC 620 and server truth is +-- unchanged; this table is presentation only. +local CONDITIONAL_MAP_MARKERS={ + [3861]={ + creatureID=620, + coords={{55.6,30.9,40,300}}, + }, +} + +-- Server repeatability and map presentation are separate. CLUCK! is technically +-- repeatable, but Questie-Octo already treats it as one-and-done after the first +-- completion. Present any marker that is legitimately shown for it as an +-- ordinary yellow quest rather than a blue repeatable quest. +local NORMAL_REPEATABLE_PRESENTATION={ + [3861]=true, +} + local function AddObjectiveData(list,kind,id) if not id then return end @@ -51,6 +88,30 @@ local function AddObjectiveData(list,kind,id) }) end +-- 1+4+8+64=77 Alliance races, 2+16+32+128=178 Horde races (pfQuest bitraces). +local function NoteFaction(fac,seen) + if fac=="A" then seen.a=true + elseif fac=="H" then seen.h=true + elseif fac=="AH" or fac=="HA" then seen.a=true; seen.h=true end +end + +-- Mirror pfQuest's GetRaceMaskByID starter fallback so a faction-specific quest +-- with no explicit race field is derived from its quest giver's faction. +local function StarterRaceMask(starts) + if not starts then return 0 end + local seen={} + for _,id in pairs(starts.creature or {}) do + NoteFaction(QuestieOcto.DatabaseAPI:GetCreatureFaction(id),seen) + end + for _,id in pairs(starts.gameObject or {}) do + NoteFaction(QuestieOcto.DatabaseAPI:GetObjectFaction(id),seen) + end + if seen.a and seen.h then return 255 end + if seen.a then return 77 end + if seen.h then return 178 end + return 0 +end + local function BuildObjectiveData(questID,objectives) local result={} local itemFirst=ITEM_OBJECTIVE_FIRST[questID] and true or false @@ -96,7 +157,10 @@ function QM:MarkObservedRepeatable(questID) local db=ObservedRepeatables() if db[questID] then return false end db[questID]=true - if self.cache[questID] then self.cache[questID].repeatable=true end + if self.cache[questID] then + self.cache[questID].repeatable=true + self.cache[questID].presentationRepeatable=not NORMAL_REPEATABLE_PRESENTATION[questID] + end if QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.Schedule then QuestieOcto.AvailableQuests:Schedule(true,0.02) end @@ -154,6 +218,7 @@ function QM:Get(questID) timed=raw["timed"] and true or false, disabled=raw["disabled"] and true or false, conditionalOffer=CONDITIONAL_OFFERS[tonumber(questID)], + conditionalMapMarker=CONDITIONAL_MAP_MARKERS[tonumber(questID)], exclusive=raw["exclusive"] and true or false, nextChain=tonumber(raw["nextChain"]), @@ -184,14 +249,33 @@ function QM:Get(questID) gameObject=raw["obj"] and CopyArray(raw["obj"]["O"]) or nil, item=raw["obj"] and CopyArray(raw["obj"]["I"]) or nil, irItems=raw["obj"] and CopyArray(raw["obj"]["IR"]) or nil, + -- A = quest-bound AreaTrigger objective. This is deliberately distinct + -- from generic map exploration/fog data: only triggers referenced by an + -- actual quest are retained here. + areaTrigger=raw["obj"] and CopyArray(raw["obj"]["A"]) or nil, }, } + -- Keep server repeatability authoritative for completion/filtering while + -- allowing a narrow presentation exception such as CLUCK!. + q.presentationRepeatable=q.repeatable and not NORMAL_REPEATABLE_PRESENTATION[tonumber(questID)] and true or false + -- CLUCK! was explicitly chosen to stay an ordinary yellow discovery marker. + -- Keep that presentation exception independent from the Turtle low-level gray + -- marker rule so a high-level player does not turn its representative marker gray. + q.presentationAlwaysNormal=NORMAL_REPEATABLE_PRESENTATION[tonumber(questID)] and true or false + -- IR items are intentionally NOT added to objectiveData. They are a -- pfQuest-special interaction relationship that becomes target guidance -- only while the player actually possesses the required item. q.objectiveData=BuildObjectiveData(questID,q.objectives) + -- A neutral race field lets faction-only quests leak to the wrong faction; + -- fall back to the quest starter's faction just like pfQuest does. + if not q.raceMask or tonumber(q.raceMask)==0 then + local starterMask=StarterRaceMask(q.starts) + if starterMask>0 then q.raceMask=starterMask end + end + self.cache[questID]=q return q end diff --git a/Docs/QUEST_SYSTEM_AUDIT.md b/Docs/QUEST_SYSTEM_AUDIT.md index ede64a4..d12ed3e 100644 --- a/Docs/QUEST_SYSTEM_AUDIT.md +++ b/Docs/QUEST_SYSTEM_AUDIT.md @@ -121,6 +121,16 @@ Repeatable flags come from the server-derived enrichment or trustworthy live observation. One-and-done repeatables can opt into `hideAfterFirstCompletion`; `CLUCK!` uses that rule. +`CLUCK!` (3861) is also a scripted/conditional offer rather than a normal static +Chicken pickup: `/chicken` temporarily exposes the quest. To keep the quest +discoverable without covering every Chicken spawn, Questie-Octo deliberately uses +one representative Westfall Chicken at 55.6,30.9 as the static pickup/turn-in map +marker. The tooltip explains that `/chicken` is required. This does not change +server truth: other Chickens can still trigger/complete the scripted interaction. +The server repeatable flag remains intact for completion logic, but the representative +marker uses ordinary yellow quest presentation instead of blue repeatable artwork. +After the first completion, `hideAfterFirstCompletion` removes the discovery marker. + The cloth donation chain is intentionally not flattened into one repeatable category: Wool/Silk/Mageweave/Runecloth donation steps are one-time quests, while the `Additional Runecloth` follow-ups carry the repeatable flag. @@ -354,3 +364,66 @@ once historical completion says it was rewarded. ### Darkmoon logical event alias Turtle/pfQuest quest rows use both `event = 4` and `event = 5` for quest offers that are shared by the same Darkmoon Faire NPCs in both Elwynn and Mulgore. Questie-Octo preserves the authoritative value as `rawEventID`, but normalizes runtime `eventID` 5 to logical event 4. Availability and presentation therefore see one `DARKMOON_FAIRE` event; the anchored 14-day schedule and NPC coordinates determine the physical location. Construction events 23/24 remain distinct. + +## Restless / custom Dun Morogh map note + +`Restless` (41640) must be interpreted against Turtle/Octo's custom Dun Morogh, not +the stock Vanilla artwork/boundary assumptions. The current Dun Morogh map visibly +includes custom southeastern terrain such as Rugford's Mountain Rest. Durmir Rugford +(62199) has map representations in both Dun Morogh (1) and Grim Reaches (5602). +Hurl Cinderfist (62200), the 100% source for Durmir's Belongings (41686), now also +has both representations: Dun Morogh 83.2/70.8 and Grim Reaches 4.1/92.0. Both are +the same live world spawn transformed through the two overlapping WorldMapArea +bounds. Keep both so the active objective is visible on Turtle's expanded Dun +Morogh map as well as Grim Reaches; do not collapse these coordinates based on +stock Vanilla Dun Morogh assumptions. + +## Turtle low-level gray available marker + +Questie-Octo 1.0.77 adds a gray available `!` presentation for ordinary +available quests and ordinary item-start quests using the Turtle-specific +accepted boundary: + +- player level <= quest level + 25: normal yellow available marker; +- player level >= quest level + 26: gray available marker. + +The boundary is strict: exactly +25 remains yellow and +26 becomes gray. + +The source basis is Turtle's custom quest XP rule in Tortoise +`src/game/QuestDef.cpp`, which retains full quest XP through `qLevel + 25` +before reducing it at +26 and beyond. That server function computes XP, not +client icon color directly, so the project does not claim it is itself the +native marker-color implementation. It was adopted because it matches the +observed Turtle native quest behavior and replaces the stock Questie +`GetQuestGreenRange()` assumption that was contradicted in-game. + +Special presentation remains higher priority than gray: PvP stays red, +repeatable stays blue, and verified event/seasonal stays green. CLUCK! (3861) +is the deliberate exception already chosen by the project: its single +representative discovery marker stays ordinary yellow rather than repeatable +blue or low-level gray. Turn-in `?` markers are unchanged. + +This gray classification is presentation-only and remains independent from the +`Levels Below` visibility slider. Level-up refreshes rebind visible Map and +Minimap marker textures without rebuilding quest data or map geometry. + + +## 1.0.78 zone-wide rare item-start tooltip compaction + +The representative-marker rule for item-start sources below 0.50% must remain +representative in the tooltip as well as on the map. Previously the map correctly +collapsed those sources to one marker per quest/item/zone, but hovering that marker +still printed every represented creature type and spawn count. For large world-drop +items such as Pendant of Myzrael this could create a tooltip taller than the screen +and hide the actual quest/item information. + +Zone-wide representative tooltips now show only a compact aggregate source count, +the quest, and the actual represented minimum/maximum drop-rate range. The underlying +source list remains unchanged. Ordinary non-zone-wide clustered item-start tooltips +still list their nearby source creatures normally. + +## 1.0.79 representative-tooltip wording cleanup + +Removed the redundant `One zone marker represents item-start sources below 0.50%.` +line from zone-wide rare item-start tooltips. The representative threshold and all +source/drop data remain unchanged; this is tooltip presentation only. diff --git a/Libs/AceConfigDialog-3.0/AceConfigDialog-3.0.lua b/Libs/AceConfigDialog-3.0/AceConfigDialog-3.0.lua index 9ce0970..ca1b1e0 100644 --- a/Libs/AceConfigDialog-3.0/AceConfigDialog-3.0.lua +++ b/Libs/AceConfigDialog-3.0/AceConfigDialog-3.0.lua @@ -4,12 +4,12 @@ -- @release $Id: AceConfigDialog-3.0.lua 1139 2016-07-03 07:43:51Z nevcairiel $ local LibStub = LibStub -local MAJOR, MINOR = "AceConfigDialog-3.0", 63 +local MAJOR, MINOR = "QuestieOcto_AceConfigDialog-3.0", 63 local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not AceConfigDialog then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local wipe, strsplit = AceCore.wipe, AceCore.strsplit local safecall = AceCore.safecall local Dispatchers = AceCore.Dispatchers @@ -23,8 +23,8 @@ AceConfigDialog.frame.apps = AceConfigDialog.frame.apps or {} AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {} AceConfigDialog.frame.closeAllOverride = AceConfigDialog.frame.closeAllOverride or {} -local gui = LibStub("AceGUI-3.0") -local reg = LibStub("AceConfigRegistry-3.0") +local gui = LibStub("QuestieOcto_AceGUI-3.0") +local reg = LibStub("QuestieOcto_AceConfigRegistry-3.0") -- Lua APIs local tconcat, tinsert, tsort, tremove, tgetn, tsetn = table.concat, table.insert, table.sort, table.remove, table.getn, table.setn diff --git a/Libs/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua b/Libs/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua index 5e45cd0..99d5e34 100644 --- a/Libs/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua +++ b/Libs/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua @@ -9,14 +9,14 @@ -- @class file -- @name AceConfigRegistry-3.0 -- @release $Id: AceConfigRegistry-3.0.lua 1139 2016-07-03 07:43:51Z nevcairiel $ -local MAJOR, MINOR = "AceConfigRegistry-3.0", 16 +local MAJOR, MINOR = "QuestieOcto_AceConfigRegistry-3.0", 16 local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR) if not AceConfigRegistry then return end AceConfigRegistry.tables = AceConfigRegistry.tables or {} -local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0") +local CallbackHandler = LibStub:GetLibrary("QuestieOcto_CallbackHandler-1.0") if not AceConfigRegistry.callbacks then AceConfigRegistry.callbacks = CallbackHandler:New(AceConfigRegistry) diff --git a/Libs/AceCore-3.0/AceCore-3.0.lua b/Libs/AceCore-3.0/AceCore-3.0.lua index e8bf464..2a2b142 100644 --- a/Libs/AceCore-3.0/AceCore-3.0.lua +++ b/Libs/AceCore-3.0/AceCore-3.0.lua @@ -1,4 +1,4 @@ -local ACECORE_MAJOR, ACECORE_MINOR = "AceCore-3.0", 1 +local ACECORE_MAJOR, ACECORE_MINOR = "QuestieOcto_AceCore-3.0", 1 local AceCore, oldminor = LibStub:NewLibrary(ACECORE_MAJOR, ACECORE_MINOR) if not AceCore then return end -- No upgrade needed @@ -48,7 +48,7 @@ AceCore.errorhandler = errorhandler local function CreateSafeDispatcher(argCount) local code = [[ - local errorhandler = LibStub("AceCore-3.0").errorhandler + local errorhandler = LibStub("QuestieOcto_AceCore-3.0").errorhandler local method, UP_ARGS local function call() local func, ARGS = method, UP_ARGS diff --git a/Libs/AceGUI-3.0/AceGUI-3.0.lua b/Libs/AceGUI-3.0/AceGUI-3.0.lua index a0cddf8..9c97840 100644 --- a/Libs/AceGUI-3.0/AceGUI-3.0.lua +++ b/Libs/AceGUI-3.0/AceGUI-3.0.lua @@ -8,7 +8,7 @@ -- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we"ll -- implement a proper API to modify it. -- @usage --- local AceGUI = LibStub("AceGUI-3.0") +-- local AceGUI = LibStub("QuestieOcto_AceGUI-3.0") -- -- Create a container frame -- local f = AceGUI:Create("Frame") -- f:SetCallback("OnClose",function(widget) AceGUI:Release(widget) end) @@ -25,12 +25,12 @@ -- @class file -- @name AceGUI-3.0 -- @release $Id: AceGUI-3.0.lua 1102 2013-10-25 14:15:23Z nevcairiel $ -local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 34 +local ACEGUI_MAJOR, ACEGUI_MINOR = "QuestieOcto_AceGUI-3.0", 34 local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR) if not AceGUI then return end -- No upgrade needed -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local hooksecurefunc = AceCore.hooksecurefunc local safecall = AceCore.safecall diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua index 9a48f8b..cea0451 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua @@ -3,7 +3,7 @@ BlizOptionsGroup Container Simple container widget for the integration of AceGUI into the Blizzard Interface Options -------------------------------------------------------------------------------]] local Type, Version = "BlizOptionsGroup", 21 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-DropDownGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-DropDownGroup.lua index 777dbef..94ee13b 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-DropDownGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-DropDownGroup.lua @@ -3,7 +3,7 @@ DropdownGroup Container Container controlled by a dropdown on the top. -------------------------------------------------------------------------------]] local Type, Version = "DropdownGroup", 21 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua index 19e0a0c..592312a 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua @@ -2,10 +2,10 @@ Frame Container -------------------------------------------------------------------------------]] local Type, Version = "Frame", 24 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local wipe = AceCore.wipe -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-InlineGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-InlineGroup.lua index f3db7d6..8657695 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-InlineGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-InlineGroup.lua @@ -3,7 +3,7 @@ InlineGroup Container Simple container widget that creates a visible "box" with an optional title. -------------------------------------------------------------------------------]] local Type, Version = "InlineGroup", 21 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua index ac1d343..9da61e1 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua @@ -3,7 +3,7 @@ ScrollFrame Container Plain container that scrolls its content and doesn't grow in height. -------------------------------------------------------------------------------]] local Type, Version = "ScrollFrame", 24 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end local fixlevels = AceGUI.fixlevels diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-SimpleGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-SimpleGroup.lua index 57512c3..f9ab61f 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-SimpleGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-SimpleGroup.lua @@ -3,7 +3,7 @@ SimpleGroup Container Simple container widget that just groups widgets. -------------------------------------------------------------------------------]] local Type, Version = "SimpleGroup", 20 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua index c195b59..a178bb0 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua @@ -3,10 +3,10 @@ TabGroup Container Container that uses tabs on top to switch between groups. -------------------------------------------------------------------------------]] local Type, Version = "TabGroup", 35 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local wipe = AceCore.wipe -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua index 9f6d40f..af2e853 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua @@ -3,10 +3,10 @@ TreeGroup Container Container that uses a tree control to switch between groups. -------------------------------------------------------------------------------]] local Type, Version = "TreeGroup", 40 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local strsplit = AceCore.strsplit local _G = AceCore._G diff --git a/Libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua b/Libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua index 6074e18..35fe636 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua @@ -1,4 +1,4 @@ -local AceGUI = LibStub("AceGUI-3.0") +local AceGUI = LibStub("QuestieOcto_AceGUI-3.0") -- Lua APIs local pairs, assert, type = pairs, assert, type diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua index 59be0e3..d2c13b1 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua @@ -3,10 +3,10 @@ Button Widget Graphical Button. -------------------------------------------------------------------------------]] local Type, Version = "Button", 23 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") -- Lua APIs local pairs = pairs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua index f71a6e2..1d4d898 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua @@ -2,7 +2,7 @@ Checkbox Widget -------------------------------------------------------------------------------]] local Type, Version = "CheckBox", 22 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua index 8feaca4..0dceba6 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua @@ -2,7 +2,7 @@ ColorPicker Widget -------------------------------------------------------------------------------]] local Type, Version = "ColorPicker", 23 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua index f970ade..268f16b 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua @@ -1,6 +1,6 @@ --[[ $Id: AceGUIWidget-DropDown-Items.lua 1137 2016-05-15 10:57:36Z nevcairiel $ ]]-- -local AceGUI = LibStub("AceGUI-3.0") +local AceGUI = LibStub("QuestieOcto_AceGUI-3.0") local IsLegion = false @@ -201,7 +201,7 @@ function ItemBase.Create(type) end -- Register a dummy LibStub library to retrieve the ItemBase, so other addons can use it. -local IBLib = LibStub:NewLibrary("AceGUI-3.0-DropDown-ItemBase", ItemBase.version) +local IBLib = LibStub:NewLibrary("QuestieOcto_AceGUI-3.0-DropDown-ItemBase", ItemBase.version) if IBLib then IBLib.GetItemBase = function() return ItemBase end end diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua index 157cfdf..ea1d9ba 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua @@ -1,7 +1,7 @@ --[[ $Id: AceGUIWidget-DropDown.lua 1116 2014-10-12 08:15:46Z nevcairiel $ ]]-- -local AceGUI = LibStub("AceGUI-3.0") +local AceGUI = LibStub("QuestieOcto_AceGUI-3.0") -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") -- Lua APIs local min, max, floor = math.min, math.max, math.floor diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua index a08d937..4b2b76a 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua @@ -2,10 +2,10 @@ EditBox Widget -------------------------------------------------------------------------------]] local Type, Version = "EditBox", 26 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local hooksecurefunc = AceCore.hooksecurefunc local _G = AceCore._G local GetCursorInfo = _G.GetCursorInfo diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Heading.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Heading.lua index e4793a6..4960588 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Heading.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Heading.lua @@ -2,7 +2,7 @@ Heading Widget -------------------------------------------------------------------------------]] local Type, Version = "Heading", 20 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Icon.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Icon.lua index c4d96fd..ace8656 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Icon.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Icon.lua @@ -2,7 +2,7 @@ Icon Widget -------------------------------------------------------------------------------]] local Type, Version = "Icon", 21 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua index 542d20d..f211407 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua @@ -2,7 +2,7 @@ InteractiveLabel Widget -------------------------------------------------------------------------------]] local Type, Version = "InteractiveLabel", 20 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua index 3640432..34b6909 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua @@ -3,7 +3,7 @@ Keybinding Widget Set Keybindings in the Config UI. -------------------------------------------------------------------------------]] local Type, Version = "Keybinding", 25 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua index 53dd0e0..a6c0b3c 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua @@ -3,7 +3,7 @@ Label Widget Displays text and optionally an icon. -------------------------------------------------------------------------------]] local Type, Version = "Label", 23 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua index 9adee87..b97569a 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua @@ -1,8 +1,8 @@ local Type, Version = "MultiLineEditBox", 28 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -local AceCore = LibStub("AceCore-3.0") +local AceCore = LibStub("QuestieOcto_AceCore-3.0") local hooksecurefunc = AceCore.hooksecurefunc -- Lua APIs diff --git a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua index 75756ce..335d849 100644 --- a/Libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua +++ b/Libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua @@ -3,7 +3,7 @@ Slider Widget Graphical Slider, like, for Range values. -------------------------------------------------------------------------------]] local Type, Version = "Slider", 22 -local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local AceGUI = LibStub and LibStub("QuestieOcto_AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end -- Lua APIs diff --git a/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua b/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua index 0515c72..7e18373 100644 --- a/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua +++ b/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua @@ -2,7 +2,7 @@ -- Original Questie foundation: CallbackHandler-1.0 r1131 / MINOR 6. -- Turtle 1.12 Lua 5.0 supports vararg declarations but consumes them through -- the implicit 'arg' table rather than Lua 5.1's '...' expression. -local MAJOR, MINOR = "CallbackHandler-1.0", 6 +local MAJOR, MINOR = "QuestieOcto_CallbackHandler-1.0", 6 local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR) if not CallbackHandler then return end diff --git a/Map/Arrow.lua b/Map/Arrow.lua new file mode 100644 index 0000000..cda6cbd --- /dev/null +++ b/Map/Arrow.lua @@ -0,0 +1,207 @@ +-- Quest arrow that points toward the nearest active quest objective in the +-- current zone, adapted from pfQuest's route arrow. +QuestieOcto.Arrow = QuestieOcto.Arrow or {} +local A = QuestieOcto.Arrow + +local ARROW_TEXTURE="Interface\\AddOns\\Questie-Octo\\UI\\Icons\\arrow" +local atan2=atan2 or math.atan2 + +local function Settings() + return QuestieOcto.MinimapSettings +end + +local function modulo(value,by) + return value-math.floor(value/by)*by +end + +-- Red at the player's back, yellow at the sides, green straight ahead. +local function ColorGradient(perc) + if perc>1 then perc=1 elseif perc<0 then perc=0 end + local r1,g1,b1,r2,g2,b2 + if perc<=0.5 then + perc=perc*2 + r1,g1,b1=1,0,0 + r2,g2,b2=1,1,0 + else + perc=perc*2-1 + r1,g1,b1=1,1,0 + r2,g2,b2=0,1,0 + end + return r1+(r2-r1)*perc, g1+(g2-g1)*perc, b1+(b2-b1)*perc +end + +local function PlayerFacing() + if GetPlayerFacing then return GetPlayerFacing() or 0 end + if MiniMapCompassRing and MiniMapCompassRing.GetFacing then + return MiniMapCompassRing:GetFacing()*-1 + end + return 0 +end + +local function CurrentMapID() + if GetRealZoneText and QuestieOcto.DatabaseAPI and QuestieOcto.DatabaseAPI.GetMapIDByName then + local id=QuestieOcto.DatabaseAPI:GetMapIDByName(GetRealZoneText()) + if id then return tonumber(id) end + end + if QuestieOcto.API and QuestieOcto.API.GetBestMapForPlayer then + return tonumber(QuestieOcto.API:GetBestMapForPlayer()) + end + return nil +end + +local function IsArrowRole(role) + return role=="objectiveCreature" or role=="objectiveObject" + or role=="objectiveItemSource" or role=="turnin" +end + +-- Pull the current map's prepared plan and keep only objective/turn-in slots. +local function CollectTargets(mapID) + local targets={} + local plan=mapID and QuestieOcto.PreparedMap and QuestieOcto.PreparedMap:Get(mapID) or nil + if not plan then return targets end + + for _,desc in pairs(plan) do + local x,y,questID + if desc.type=="nodeSlot" then + for _,entry in pairs(desc.entries or {}) do + local node=entry.node + if node and IsArrowRole(node.role) then + x,y,questID=tonumber(desc.x),tonumber(desc.y),node.questID + break + end + end + elseif desc.type=="node" and desc.node and IsArrowRole(desc.node.role) then + x,y,questID=tonumber(desc.x),tonumber(desc.y),desc.node.questID + end + + if x and y then table.insert(targets,{x,y,questID}) end + end + + return targets +end + +local function NearestTarget(targets,px,py) + local best,bestDist + for _,t in pairs(targets or {}) do + local dx,dy=(px-t[1])*1.5,(py-t[2]) + local d=math.sqrt(dx*dx+dy*dy) + if not bestDist or d0 and (math.pi*2)-dir or -dir + if dir<0 then dir=dir+360 end + local angle=math.rad(dir)-PlayerFacing() + local perc=math.abs((math.pi-math.abs(angle))/math.pi) + local r,g,b=ColorGradient(math.floor(perc*100)/100) + local cell=modulo(math.floor(angle/(math.pi*2)*108+0.5),108) + local column=modulo(cell,9) + local row=math.floor(cell/9) + frame.model:SetTexCoord((column*56)/512,((column+1)*56)/512,(row*42)/512,((row+1)*42)/512) + frame.model:SetVertexColor(r,g,b) + + if best[3]~=frame.lastQuest then + frame.lastQuest=best[3] + local q=best[3] and QuestieOcto.QuestModel:Get(best[3]) or nil + local level=q and tonumber(q.level) + local label=q and q.title or "Quest" + if level and level>0 then label="["..tostring(level).."] "..label end + frame.title:SetText(label) + local cr,cg,cb=1,.82,0 + if q and QuestieOcto.GetNativeQuestDifficultyColor then + local nr,ng,nb=QuestieOcto:GetNativeQuestDifficultyColor(q.level,q.id) + if nr then cr,cg,cb=nr,ng,nb end + end + frame.title:SetTextColor(cr,cg,cb) + end + + local shown=math.floor(bestDist*10)/10 + if shown~=frame.shownDistance then + frame.shownDistance=shown + frame.distance:SetText("Distance: "..string.format("%.1f",bestDist)) + end +end + +function A:Initialize() + if self.frame then return end + + local frame=CreateFrame("Frame","QuestieOctoQuestArrow",UIParent) + frame:SetPoint("CENTER",UIParent,"CENTER",0,-100) + frame:SetWidth(48) + frame:SetHeight(36) + if frame.SetClampedToScreen then frame:SetClampedToScreen(true) end + frame:SetMovable(true) + frame:EnableMouse(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart",function() if IsShiftKeyDown() then this:StartMoving() end end) + frame:SetScript("OnDragStop",function() this:StopMovingOrSizing() end) + + local model=frame:CreateTexture("QuestieOctoQuestArrowModel","ARTWORK") + model:SetTexture(ARROW_TEXTURE) + model:SetTexCoord(0,56/512,0,42/512) + model:SetAllPoints(frame) + frame.model=model + + local font=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF" + + local title=frame:CreateFontString(nil,"OVERLAY","GameFontNormal") + title:SetPoint("TOP",model,"BOTTOM",0,-10) + title:SetFont(font,13,"OUTLINE") + title:SetTextColor(1,.82,0) + title:SetJustifyH("CENTER") + frame.title=title + + local distance=frame:CreateFontString(nil,"OVERLAY","GameFontNormal") + distance:SetPoint("TOP",title,"BOTTOM",0,-2) + distance:SetFont(font,11,"OUTLINE") + distance:SetTextColor(.8,.8,.8) + distance:SetJustifyH("CENTER") + frame.distance=distance + + frame:Hide() + self.frame=frame + + -- A hidden frame receives no OnUpdate, so drive the logic from an always-shown + -- frame that shows/hides the arrow. + local driver=CreateFrame("Frame","QuestieOctoQuestArrowDriver",UIParent) + driver:SetScript("OnUpdate",function() A:OnUpdate() end) + self.driver=driver +end + +QuestieOcto:RegisterMessage("FOUNDATION_READY",A,"Initialize") diff --git a/Map/CandidateIndex.lua b/Map/CandidateIndex.lua index ccdcff3..b1c593b 100644 --- a/Map/CandidateIndex.lua +++ b/Map/CandidateIndex.lua @@ -37,11 +37,19 @@ local function IndexCoords(coords,questID) end end +local function StarterCreatureCoords(q,creatureID) + local marker=q and q.conditionalMapMarker or nil + if marker and tonumber(marker.creatureID)==tonumber(creatureID) then + return marker.coords + end + return QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID) +end + local function IndexQuest(q) if not q then return end for _,id in pairs(q.starts.creature or {}) do - IndexCoords(QuestieOcto.DatabaseAPI:GetCreatureCoords(id),q.id) + IndexCoords(StarterCreatureCoords(q,id),q.id) end for _,id in pairs(q.starts.gameObject or {}) do diff --git a/Map/ContinentProjection.lua b/Map/ContinentProjection.lua index 9705f09..5a42a9a 100644 --- a/Map/ContinentProjection.lua +++ b/Map/ContinentProjection.lua @@ -56,6 +56,27 @@ C.zones={ [1637]={continent=1,name="Ogrimmar",xMin=1338.460571,yMin=-5083.205566,xMax=2273.877197,yMax=-3680.601074}, [1638]={continent=1,name="ThunderBluff",xMin=-1545.833252,yMin=-527.083313,xMax=-849.999939,yMax=516.666626}, [1657]={continent=1,name="Darnassis",xMin=9532.586914,yMin=1880.029541,xMax=10238.31641,yMax=2938.362793}, + + -- Turtle custom world zones reported missing from continent/world overview. + -- Bounds come from the supplied pfQuest WorldMapArea_turtle extractor data, + -- using the same coordinate convention as the Vanilla entries above. + [408]={continent=0,name="Gillijim",xMin=-14575.719727,yMin=1344.119995,xMax=-12528.709961,yMax=4436.220215}, + [409]={continent=0,name="Lapidis",xMin=-12959.419922,yMin=2031.880005,xMax=-11043.480469,yMax=4933.330078}, + [616]={continent=1,name="Hyjal",xMin=3983,yMin=-4286,xMax=6125,yMax=-1080}, + [2040]={continent=0,name="AlahThalas",xMin=3931,yMin=-3637,xMax=4907,yMax=-2169}, + [4012]={continent=0,name="ScarletEnclave",xMin=979,yMin=-7209,xMax=3087,yMax=-4050}, + [5121]={continent=1,name="TelAbim",xMin=-9371,yMin=-8406,xMax=-7184,yMax=-5179}, + [5179]={continent=0,name="Gilneas",xMin=-2801,yMin=-413,xMax=-359,yMax=3253}, + [5225]={continent=0,name="ThalassianHighlands",xMin=2891,yMin=-4087,xMax=4952,yMax=-1005}, + [5536]={continent=1,name="BlackstoneIsland",xMin=-866,yMin=-8746,xMax=799,yMax=-6274}, + [5561]={continent=0,name="Balor",xMin=-9645,yMin=1630,xMax=-7577,yMax=4728}, + [5581]={continent=0,name="Northwind",xMin=-8484,yMin=-983,xMax=-6327,yMax=2258}, + [5602]={continent=0,name="GrimReaches",xMin=-6488,yMin=-7459,xMax=-2904,yMax=-2072}, + + -- Moonwhisper Coast post-dates the supplied WorldMapArea_turtle snapshot. + -- These bounds are reconstructed from the current Turtle server creature + -- world positions plus Questie-Octo's shipped 7856x5241 minimap world size. + [5642]={continent=1,name="MoonwhisperCoast",xMin=4963,yMin=-9210,xMax=10204,yMax=-1354}, } function C:GetClientContinentMapID(clientContinentID) diff --git a/Map/ItemStartAreas.lua b/Map/ItemStartAreas.lua index ec0abfa..a1e1337 100644 --- a/Map/ItemStartAreas.lua +++ b/Map/ItemStartAreas.lua @@ -8,7 +8,7 @@ QuestieOcto.ItemStartAreas = QuestieOcto.ItemStartAreas or {} local A = QuestieOcto.ItemStartAreas A.radius=14.0 -A.zoneWideRareThreshold=0.1 +A.zoneWideRareThreshold=0.5 function A:IsZoneWideRareChance(chance) chance=tonumber(chance) or 0 @@ -184,10 +184,10 @@ function A:BuildForMap(nodes,mapID,includeNode) end --- World-map-only presentation helper for extremely low-rate starter items. +-- Shared zone-wide presentation helper for extremely low-rate starter items. -- These drops are often spread across many unrelated creatures throughout a --- zone. Keep every source in the data/minimap, but let the World Map represent --- all <0.10% sources for the same quest/item with one zone marker. +-- zone. Keep every source in the underlying data, but represent all <0.50% +-- sources for the same quest/item with one zone marker on Map and Minimap. function A:BuildZoneWideRareForMap(nodes,mapID) local groups={} diff --git a/Map/Map.lua b/Map/Map.lua index fba4ca9..0ab4abd 100644 --- a/Map/Map.lua +++ b/Map/Map.lua @@ -79,6 +79,7 @@ M.stats={active=0,created=0,reused=0,hidden=0,exact=0,objectiveAreas=0,itemStart local ICON_ROOT="Interface\\AddOns\\Questie-Octo\\UI\\Icons\\" local TEX_AVAILABLE=ICON_ROOT.."available" +local TEX_AVAILABLE_GRAY=ICON_ROOT.."available_gray" local TEX_MOBDROP=ICON_ROOT.."available_mobdrop" local TEX_OBJECTSTART=ICON_ROOT.."available_object" local TEX_COMPLETE=ICON_ROOT.."complete" @@ -106,6 +107,28 @@ local TEX_STABLEMASTER=ICON_ROOT.."stablemaster" local TEX_VENDOR=ICON_ROOT.."vendor" local TEX_RARE=ICON_ROOT.."rares" +-- Turtle deliberately keeps full quest XP through 25 levels above the quest +-- (Tortoise src/game/QuestDef.cpp). The server file is XP logic rather than a +-- client icon-color function, but it matches the observed Turtle native quest +-- presentation and is the project's accepted low-level gray-marker boundary. +-- Exactly +25 stays normal; +26 and beyond becomes gray. +local TURTLE_GRAY_QUEST_DELTA=25 + +local function IsGrayAvailableQuest(node) + if not node or (node.role~="available" and node.role~="itemStart") then return false end + local questID=tonumber(node.questID) + if not questID then return false end + + local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil + if not q or q.presentationAlwaysNormal then return false end + + local questLevel=tonumber(q.level) + local playerLevel=UnitLevel and tonumber(UnitLevel("player")) or nil + if not questLevel or questLevel<=0 or not playerLevel or playerLevel<=0 then return false end + + return playerLevel>questLevel+TURTLE_GRAY_QUEST_DELTA +end + local function TextureForNode(node) if node.role=="flightMaster" then return TEX_FLIGHT end if node.role=="auctioneer" then return TEX_AUCTIONEER end @@ -120,16 +143,18 @@ local function TextureForNode(node) if node.role=="vendor" then return TEX_VENDOR end if node.role=="rareMob" then return TEX_RARE end if node.role=="itemStart" then - -- Presentation priority: PvP > Repeatable > Event > Normal. + -- Presentation priority: PvP > Repeatable > Event > Turtle low-level gray > Normal. if node.pvp then return TEX_PVP_AVAILABLE end if node.repeatable then return TEX_REPEATABLE_AVAILABLE end if node.event then return TEX_EVENT_AVAILABLE end + if IsGrayAvailableQuest(node) then return TEX_AVAILABLE_GRAY end return TEX_AVAILABLE end if node.role=="available" then if node.pvp then return TEX_PVP_AVAILABLE end if node.repeatable then return TEX_REPEATABLE_AVAILABLE end if node.event then return TEX_EVENT_AVAILABLE end + if IsGrayAvailableQuest(node) then return TEX_AVAILABLE_GRAY end return TEX_AVAILABLE end if node.role=="turnin" then @@ -146,6 +171,7 @@ local function TextureForNode(node) end if node.role=="objectiveObject" then return TEX_OBJECT end if node.role=="objectiveCreature" then return TEX_SLAY end + if node.role=="objectiveArea" then return TEX_EVENT end return TEX_INCOMPLETE end local function RolePriority(role) @@ -157,7 +183,7 @@ local function RolePriority(role) -- At a shared Full Nodes coordinate, a direct objective should own the -- displayed quest color over an indirect item-drop source. The merged pin -- still retains every quest/objective entry for its tooltip. - if role=="objectiveObject" or role=="objectiveCreature" then return 20 end + if role=="objectiveObject" or role=="objectiveCreature" or role=="objectiveArea" then return 20 end if role=="objectiveItemSource" then return 15 end return 10 end @@ -219,6 +245,10 @@ function M:GetTextureForNode(node) return TextureForNode(node) end +function M:IsGrayAvailableQuest(node) + return IsGrayAvailableQuest(node) +end + function M:GetRolePriority(role) return RolePriority(role) end @@ -596,7 +626,7 @@ function M:RenderItemStartArea(area,generation,continentZoneMapID) if itemQuest and itemQuest.pvp and not DisplaySettings():Get("showPvPRelatedQuests") then return end local itemEvent=itemQuest and itemQuest.eventID and QuestieOcto.EventAvailability and QuestieOcto.EventAvailability:IsPresentationEvent(itemQuest.eventID) or false local itemPvP=itemQuest and itemQuest.pvp or false - local itemRepeatable=itemQuest and itemQuest.repeatable or false + local itemRepeatable=itemQuest and itemQuest.presentationRepeatable or false local key="itemarea:"..tostring(area.key) local pin=self.frames[key] @@ -636,7 +666,7 @@ function M:RenderItemStartArea(area,generation,continentZoneMapID) pin.sourceKind="area" pin.displayName=area.displayName pin.clusterCount=area.n - pin.texture:SetTexture(TextureForNode({role="itemStart",event=pin.event,pvp=pin.pvp,repeatable=pin.repeatable})) + pin.texture:SetTexture(TextureForNode({role="itemStart",questID=area.questID,event=pin.event,pvp=pin.pvp,repeatable=pin.repeatable})) pin.texture:SetDrawLayer("OVERLAY",5) if QuestieOcto.Visuals then QuestieOcto.Visuals:ApplyPin(pin,{role="itemStart",questID=area.questID,pvp=pin.pvp,repeatable=pin.repeatable},false,1) @@ -1374,11 +1404,18 @@ QuestieOcto:RegisterMessage("PREPARED_MAP_READY",M,"OnPreparedMapReady") local f=CreateFrame("Frame","QuestieOctoWorldMapEvents",UIParent) f:RegisterEvent("WORLD_MAP_UPDATE") +f:RegisterEvent("PLAYER_LEVEL_UP") f:SetScript("OnEvent",function() if event=="WORLD_MAP_UPDATE" then if WorldMapFrame and WorldMapFrame:IsVisible() then M:EnsureDisplayedContextCurrent() end + elseif event=="PLAYER_LEVEL_UP" then + -- Gray classification depends only on the current player/quest levels. + -- Rebind the visible map presentation; do not rebuild geometry or quest truth. + QuestieOcto.Scheduler:After(0.01,function() + if WorldMapFrame and WorldMapFrame:IsVisible() then M:RequestSync(true) end + end,"map-gray-level-refresh") end end) diff --git a/Map/Minimap.lua b/Map/Minimap.lua index b85fc35..978cf4b 100644 --- a/Map/Minimap.lua +++ b/Map/Minimap.lua @@ -462,8 +462,32 @@ local function PvPNodeVisible(node) return Settings():Get("showPvPRelatedQuests") and true or false end -local function ItemAreaVisible(area) - if not area or not IsRoleEnabled("itemStart") then return false end +-- Map and Minimap share the same item-start presentation plan. The regular +-- prepared plan still contains raw item-start descriptors because Full Nodes +-- can share physical slots with active objectives, so the minimap explicitly +-- ignores item-start entries from that base plan and consumes the dedicated +-- World Map item-start plan instead. This gives both displays the same <0.50% +-- zone-wide representative marker without changing the underlying source data. +-- Inside dungeons/raids, only those ultra-rare representative markers are +-- hidden; meaningful >=0.50% item starters remain visible. +local function MinimapNodeVisible(node,allowItemStart) + if not node or not IsRoleEnabled(node.role) or not PvPNodeVisible(node) then return false end + + if node.role=="itemStart" then + if not allowItemStart then return false end + if MM.inDungeonOrRaid and QuestieOcto.ItemStartAreas + and QuestieOcto.ItemStartAreas:IsZoneWideRareChance(node.chance) then + return false + end + end + + return true +end + +local function ItemAreaVisible(area,allowItemStart) + if not allowItemStart or not area or not IsRoleEnabled("itemStart") then return false end + if MM.inDungeonOrRaid and area.zoneWideRare then return false end + local q=QuestieOcto.QuestModel:Get(area.questID) if q and q.pvp and not Settings():Get("showPvPRelatedQuests") then return false end return true @@ -479,34 +503,35 @@ local function DescriptorCoordinates(desc) return nil,nil end -local function DescriptorHasVisibleEntry(desc,revision) +local function DescriptorHasVisibleEntry(desc,revision,allowItemStart) if not desc then return false end - if desc.minimapVisibilityRevision==revision then return desc.minimapVisible and true or false end + local visibilityRevision=(tonumber(revision) or 0)*2+(allowItemStart and 1 or 0) + if desc.minimapVisibilityRevision==visibilityRevision then return desc.minimapVisible and true or false end local visible=false if desc.type=="itemStartArea" then - visible=ItemAreaVisible(desc.area) + visible=ItemAreaVisible(desc.area,allowItemStart) elseif desc.type=="nodeSlot" then for _,entry in pairs(desc.entries or {}) do local node=entry.node - if node and IsRoleEnabled(node.role) and PvPNodeVisible(node) then visible=true; break end + if MinimapNodeVisible(node,allowItemStart) then visible=true; break end end elseif desc.type=="node" and desc.node then - visible=IsRoleEnabled(desc.node.role) and PvPNodeVisible(desc.node) + visible=MinimapNodeVisible(desc.node,allowItemStart) end - desc.minimapVisibilityRevision=revision + desc.minimapVisibilityRevision=visibilityRevision desc.minimapVisible=visible and true or false return visible end -local function BindDescriptor(pin,desc,revision) +local function BindDescriptor(pin,desc,revision,allowItemStart) ResetPin(pin) pin.boundDescriptor=desc pin.boundRevision=revision if desc.type=="itemStartArea" and desc.area then - if not ItemAreaVisible(desc.area) then return false end + if not ItemAreaVisible(desc.area,allowItemStart) then return false end local area=desc.area pin.itemStartArea=area pin.displayName=area.displayName @@ -519,9 +544,9 @@ local function BindDescriptor(pin,desc,revision) local q=QuestieOcto.QuestModel:Get(area.questID) pin.event=q and q.eventID and QuestieOcto.EventAvailability and QuestieOcto.EventAvailability:IsPresentationEvent(q.eventID) or false pin.pvp=q and q.pvp or false - pin.repeatable=q and q.repeatable or false + pin.repeatable=q and q.presentationRepeatable or false pin.visualPriority=40 - pin.texture:SetTexture(QuestieOcto.Map:GetTextureForNode({role="itemStart",event=pin.event,pvp=pin.pvp,repeatable=pin.repeatable})) + pin.texture:SetTexture(QuestieOcto.Map:GetTextureForNode({role="itemStart",questID=area.questID,event=pin.event,pvp=pin.pvp,repeatable=pin.repeatable})) pin.texture:SetDrawLayer("OVERLAY",5) if QuestieOcto.Visuals then QuestieOcto.Visuals:ApplyPin(pin,{role="itemStart",questID=area.questID,pvp=pin.pvp,repeatable=pin.repeatable},true,pin.lastAlpha or 1) @@ -544,7 +569,7 @@ local function BindDescriptor(pin,desc,revision) local fullNode=nil for _,entry in pairs(entries or {}) do local node=entry.node - if node and IsRoleEnabled(node.role) and PvPNodeVisible(node) then + if MinimapNodeVisible(node,allowItemStart) then visible=true pin.clusterCount=math.max(pin.clusterCount or 1,entry.clusterCount or 1) AddEntry(pin,node) @@ -615,6 +640,7 @@ function MM:RefreshPlan() if not mapID then self.mapID=nil self.plan=nil + self.itemStartPlan=nil self.planRevision=nil self:HideAll() return @@ -628,9 +654,20 @@ function MM:RefreshPlan() self.lastZoom=nil end + -- Instance type changes at the same lifecycle boundaries that refresh the + -- minimap plan (entering world / zone changes). Evaluate it here rather + -- than polling GetInstanceInfo every 0.05 seconds. + local inDungeonOrRaid=QuestieOcto.API and QuestieOcto.API:IsInDungeonOrRaid() or false + if self.inDungeonOrRaid~=inDungeonOrRaid then + self.inDungeonOrRaid=inDungeonOrRaid + self.bindRevision=(self.bindRevision or 0)+1 + end + local plan=QuestieOcto.PreparedMap:Get(mapID) - if not plan then + local itemStartPlan=QuestieOcto.PreparedMap:GetWorldItemStarts(mapID) + if not plan or not itemStartPlan then self.plan=nil + self.itemStartPlan=nil self.planRevision=nil self:HideAll() if QuestieOcto.ZoneBootstrap then QuestieOcto.ZoneBootstrap:Request(mapID,0.01) end @@ -638,6 +675,7 @@ function MM:RefreshPlan() end self.plan=plan + self.itemStartPlan=itemStartPlan self.planRevision=QuestieOcto.PreparedMap.stateRevision self.stats.refreshes=self.stats.refreshes+1 self:UpdatePositions(true) @@ -673,6 +711,8 @@ function MM:UpdatePositions(force) if self.planRevision~=QuestieOcto.PreparedMap.stateRevision then self:RefreshPlan(); return end local published=QuestieOcto.PreparedMap:Get(self.mapID) if published and published~=self.plan then self:RefreshPlan(); return end + local publishedItemStarts=QuestieOcto.PreparedMap:GetWorldItemStarts(self.mapID) + if publishedItemStarts and publishedItemStarts~=self.itemStartPlan then self:RefreshPlan(); return end local px,py=PlayerPosition() if not px or not py then self:HideAll(); return end @@ -717,40 +757,53 @@ function MM:UpdatePositions(force) -- pfQuest architecture: scan coordinate data, but allocate/reuse UI Buttons -- only for coordinates that are actually inside the current minimap circle. - -- There is intentionally no hard node cap here. - for _,desc in ipairs(self.plan or {}) do - if DescriptorHasVisibleEntry(desc,revision) then - local x,y=DescriptorCoordinates(desc) - if x and y then - local xPos=(x-px)*xDraw - local yPos=(y-py)*yDraw - local inside=false - if squareMinimap then - inside=math.abs(xPos)<(width/2) and math.abs(yPos)<(height/2) - else - inside=xPos*xPos+yPos*yPos0 then + if not minimum or chancemaximum then maximum=chance end + end + end + + if not minimum or not maximum then return nil end + + local minText=FormatDropRate(minimum) + local maxText=FormatDropRate(maximum) + if not minText or not maxText then return nil end + + if minText==maxText then return minText.."%" end + return minText.."%~"..maxText.."%" +end + local function DifficultyColor(level,questID) -- Use the same authority as the native Quest Log. Octo/Turtle modifies the -- low-level/easy band, so reproducing stock Classic thresholds makes map @@ -150,12 +172,22 @@ end local function RespawnText(seconds) seconds=tonumber(seconds) if not seconds or seconds<=0 then return nil end + seconds=math.floor(seconds) - if math.mod(seconds,60)==0 then - return "~"..tostring(math.floor(seconds/60)).." min" + if seconds<60 then return "~"..tostring(seconds).."s" end + + local minutes=math.floor(seconds/60) + -- Roll long respawns up into hours so 18000s reads "~5h", not "~300 min". + if minutes>=60 then + local hours=math.floor(minutes/60) + local mins=math.mod(minutes,60) + if mins==0 then return "~"..tostring(hours).."h" end + return "~"..tostring(hours).."h "..tostring(mins).."m" end - return "~"..tostring(math.floor(seconds/60)).."m "..tostring(math.mod(seconds,60)).."s" + local secs=math.mod(seconds,60) + if secs==0 then return "~"..tostring(minutes).." min" end + return "~"..tostring(minutes).."m "..tostring(secs).."s" end local function SourceDisplayName(source) @@ -207,6 +239,9 @@ local function LiveObjectiveText(node) end local function Extra(node) + if node.role=="available" and node.conditionalOffer then + return tostring(node.conditionalOffer) + end if node.role=="itemStart" and node.itemName then local text if node.vendor then text="Sells ["..tostring(node.itemName).."]" @@ -216,7 +251,8 @@ local function Extra(node) end if node.role=="objectiveCreature" or node.role=="objectiveObject" - or node.role=="objectiveItemSource" then + or node.role=="objectiveItemSource" + or node.role=="objectiveArea" then local liveText=LiveObjectiveText(node) if liveText and liveText~="" then local text=liveText @@ -1101,27 +1137,38 @@ function T:Show(pin) if pin.itemStartArea then local area=pin.itemStartArea - -- Use AddDoubleLine with an empty right column for every source row. - -- This avoids the first source row inheriting the tooltip's title-sized - -- presentation while keeping every monster line visually identical. - for _,source in pairs(area.sourceList or {}) do - local sourceText=SourceDisplayName(source).." ("..tostring(source.count).. - (area.zoneWideRare and " zone spawns)" or " nearby spawns)") - if area.zoneWideRare and source.chance and Settings():Get("enableTooltipDroprates") then - sourceText=sourceText.." - "..(FormatDropRate(source.chance) or tostring(source.chance)).."%" - end - tooltip:AddDoubleLine( - sourceText, - "", - .2,1,.35, - .2,1,.35 + if area.zoneWideRare then + -- A zone-wide representative exists specifically to make enormous + -- world-drop source sets readable. Do not undo that simplification by + -- dumping every represented creature into the tooltip (Pendant of + -- Myzrael can have dozens of source types in a single zone). Keep the + -- full source list in the data and present only a compact aggregate. + local sourceCount=table.getn(area.sourceList or {}) + local spawnCount=tonumber(area.n) or 0 + tooltip:SetText("Rare item-start sources",.2,1,.35) + tooltip:AddLine( + tostring(sourceCount).." creature types, "..tostring(spawnCount).." zone spawns", + .65,.65,.65 ) + else + -- Ordinary clustered item-start areas still list their nearby sources. + -- Use AddDoubleLine with an empty right column for every source row so + -- the first source does not inherit the tooltip's title-sized font. + for _,source in pairs(area.sourceList or {}) do + local sourceText=SourceDisplayName(source).." ("..tostring(source.count).." nearby spawns)" + tooltip:AddDoubleLine( + sourceText, + "", + .2,1,.35, + .2,1,.35 + ) - local rank=RareRankText(source.rank) - local respawn=rank and RespawnText(source.respawnSeconds) or nil - if respawn then - local rareTag=RareRankText(source.rank) or "Rare" - tooltip:AddLine("["..rareTag.."] Respawn: "..respawn,.75,.75,.75) + local rank=RareRankText(source.rank) + local respawn=rank and RespawnText(source.respawnSeconds) or nil + if respawn then + local rareTag=RareRankText(source.rank) or "Rare" + tooltip:AddLine("["..rareTag.."] Respawn: "..respawn,.75,.75,.75) + end end end @@ -1136,33 +1183,19 @@ function T:Show(pin) ) end - local chance=nil - for _,source in pairs(area.sourceList or {}) do - if source.chance then - chance=source.chance - break - end - end + local dropRateText=ItemStartAreaDropRateText(area) local itemLabel=tostring(area.itemName or ("Item "..tostring(area.itemID))) if Settings():Get("enableTooltipsItemID") and area.itemID then itemLabel=itemLabel.." (Item "..tostring(area.itemID)..")" end - if area.zoneWideRare then - local threshold=tonumber(area.rareThreshold) or 0.1 - tooltip:AddLine( - "One zone marker represents item-start sources below "..string.format("%.2f",threshold).."%.", - .65,.65,.65,true - ) - end - local line="Drops ["..itemLabel.."]" - if chance and not area.zoneWideRare and Settings():Get("enableTooltipDroprates") then - line=line.." ("..(FormatDropRate(chance) or tostring(chance)).."%)" + if dropRateText and Settings():Get("enableTooltipDroprates") then + line=line.." ("..dropRateText..")" end tooltip:AddLine(line.." - item starts quest",.82,.82,.82,true) - NormalizeFirstRowFont(tooltip) + if not area.zoneWideRare then NormalizeFirstRowFont(tooltip) end tooltip:Show() return end diff --git a/Map/Visuals.lua b/Map/Visuals.lua index 3689cea..b049721 100644 --- a/Map/Visuals.lua +++ b/Map/Visuals.lua @@ -90,7 +90,8 @@ function V:ApplyPin(pin,node,isMinimap,alpha) local r,g,b=1,1,1 -- Quest pickup/turn-in presentation priority is PvP > Repeatable > Event > - -- Normal. PvP and repeatable use dedicated artwork and keep its native color. + -- Turtle low-level gray > Normal. Gray uses dedicated artwork like the other + -- quest-start variants; objective tinting never recolors pickup/turn-in icons. if (node.pvp or node.repeatable) and (node.role=="available" or node.role=="itemStart" or node.role=="turnin") then r,g,b=1,1,1 elseif objective and colorEnabled then diff --git a/Map/ZoneBootstrap.lua b/Map/ZoneBootstrap.lua index 0dc6316..e85f74e 100644 --- a/Map/ZoneBootstrap.lua +++ b/Map/ZoneBootstrap.lua @@ -42,23 +42,33 @@ local function ApplyObjectiveState(node,state) return node end +local function ConditionalCreatureCoords(q,creatureID,role) + local marker=q and q.conditionalMapMarker or nil + if marker and tonumber(marker.creatureID)==tonumber(creatureID) + and (role=="available" or role=="turnin") then + return marker.coords + end + return nil +end + local function CreatureNode(questID,role,id,itemID,chance,objectiveState) local q=QuestieOcto.QuestModel:Get(questID) return ApplyObjectiveState({ - questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.repeatable or false, + questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.presentationRepeatable or false, sourceKind="creature",sourceID=id, sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(id), sourceRank=QuestieOcto.DatabaseAPI:GetCreatureRank(id), respawnSeconds=QuestieOcto.DatabaseAPI:GetCreatureRespawnSeconds(id), itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil, - chance=chance,coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(id) + chance=chance,coords=ConditionalCreatureCoords(q,id,role) or QuestieOcto.DatabaseAPI:GetCreatureCoords(id), + conditionalOffer=q and q.conditionalOffer or nil },objectiveState) end local function ObjectNode(questID,role,id,itemID,chance,objectiveState) local q=QuestieOcto.QuestModel:Get(questID) return ApplyObjectiveState({ - questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.repeatable or false, + questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.presentationRepeatable or false, sourceKind="gameObject",sourceID=id, sourceName=QuestieOcto.DatabaseAPI:GetObjectName(id), itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil, @@ -116,7 +126,8 @@ end local function StarterTouchesMap(q,mapID) for _,id in pairs(q.starts.creature or {}) do - if CoordsContainMap(QuestieOcto.DatabaseAPI:GetCreatureCoords(id),mapID) then return true end + local coords=ConditionalCreatureCoords(q,id,"available") or QuestieOcto.DatabaseAPI:GetCreatureCoords(id) + if CoordsContainMap(coords,mapID) then return true end end for _,id in pairs(q.starts.gameObject or {}) do diff --git a/Quest/AvailableQuests.lua b/Quest/AvailableQuests.lua index f2a69b7..2f78c0d 100644 --- a/Quest/AvailableQuests.lua +++ b/Quest/AvailableQuests.lua @@ -300,9 +300,11 @@ function A:EvaluateQuest(questID,trackStats) end -- Some quests are conditionally exposed by scripted world interactions rather - -- than normal NPC availability. Do not advertise them as static pickups. If - -- the player actually accepts one, the Quest Log path still handles it. - if q.conditionalOffer then + -- than normal NPC availability. Most stay hidden as static pickups. A narrow + -- presentation exception may provide one representative discovery marker; in + -- that case continue through all ordinary level/race/filter gates and let the + -- node builders use only that representative coordinate. + if q.conditionalOffer and not q.conditionalMapMarker then return false,"conditional" end @@ -389,6 +391,10 @@ function A:EvaluateQuest(questID,trackStats) return false,"noStarter" end + if q.conditionalOffer and q.conditionalMapMarker then + return true,"conditional" + end + return true,"available" end diff --git a/Quest/CreatureDiagnostics.lua b/Quest/CreatureDiagnostics.lua index da909c2..e1349d2 100644 --- a/Quest/CreatureDiagnostics.lua +++ b/Quest/CreatureDiagnostics.lua @@ -13,10 +13,22 @@ end local function RespawnText(seconds) seconds=tonumber(seconds) if not seconds or seconds<=0 then return "unknown" end - if math.mod(seconds,60)==0 then - return "~"..tostring(math.floor(seconds/60)).." min" + seconds=math.floor(seconds) + + if seconds<60 then return "~"..tostring(seconds).."s" end + + local minutes=math.floor(seconds/60) + -- Roll long respawns up into hours so 18000s reads "~5h", not "~300 min". + if minutes>=60 then + local hours=math.floor(minutes/60) + local mins=math.mod(minutes,60) + if mins==0 then return "~"..tostring(hours).."h" end + return "~"..tostring(hours).."h "..tostring(mins).."m" end - return "~"..tostring(math.floor(seconds/60)).."m "..tostring(math.mod(seconds,60)).."s" + + local secs=math.mod(seconds,60) + if secs==0 then return "~"..tostring(minutes).." min" end + return "~"..tostring(minutes).."m "..tostring(secs).."s" end local function ArrayContains(t,value) diff --git a/Quest/Objectives.lua b/Quest/Objectives.lua index bc5a6ad..da00786 100644 --- a/Quest/Objectives.lua +++ b/Quest/Objectives.lua @@ -7,7 +7,7 @@ O.running=false O.pending=false O.generation=0 O.stats={ - quests=0, creature=0, object=0, item=0, itemSources=0, irTargets=0, + quests=0, creature=0, object=0, item=0, areaTrigger=0, itemSources=0, irTargets=0, mapped=0, unmapped=0, direct=0, fallback=0, fuzzy=0, single=0, typeMismatch=0, rebuilds=0, dependencyWaits=0 } @@ -18,7 +18,7 @@ local function ResetStats() local rebuilds=O.stats.rebuilds or 0 local waits=O.stats.dependencyWaits or 0 O.stats={ - quests=0, creature=0, object=0, item=0, itemSources=0, irTargets=0, + quests=0, creature=0, object=0, item=0, areaTrigger=0, itemSources=0, irTargets=0, mapped=0, unmapped=0, direct=0, fallback=0, fuzzy=0, single=0, typeMismatch=0, rebuilds=rebuilds, dependencyWaits=waits } @@ -213,6 +213,25 @@ local function NormalizeLogType(typ) return typ end +-- Sparse presentation-only corrections for capture/transform objectives where +-- the server awards quest credit against a temporary NPC entry that has no +-- natural spawn points. Keep the authoritative live objective ID for progress +-- and completion; only map/minimap guidance uses the actionable source NPC. +-- +-- Plagued Lands (2118): Tortoise darkshore.cpp handles spell 9439 by changing +-- Rabid Thistle Bear (2164) into Captured Rabid Thistle Bear (11836), then +-- awards KilledMonsterCredit for 11836. The player must therefore find 2164, +-- while 11836 remains the correct completion target in the quest log/database. +local CREATURE_OBJECTIVE_SOURCE_OVERRIDES={ + [2118]={ [11836]=2164 }, +} + +local function PresentationCreatureSourceID(questID,objectiveID) + local byQuest=CREATURE_OBJECTIVE_SOURCE_OVERRIDES[tonumber(questID)] + local sourceID=byQuest and byQuest[tonumber(objectiveID)] + return tonumber(sourceID) or tonumber(objectiveID) +end + local function Levenshtein(str1,str2) str1=string.lower(tostring(str1 or "")) str2=string.lower(tostring(str2 or "")) @@ -236,6 +255,7 @@ end local function MergeState(entry,row) entry.objectiveIndex=row.index + entry.objectiveID=row.objectiveID entry.objectiveText=row.text entry.rawObjectiveText=row.rawText or row.text entry.objectiveType=row.type @@ -253,20 +273,31 @@ local function ObjectiveTypesMatch(logType,dbType) return false end -local function EntryFromOrderedObjective(q,row) - local ordered=q.objectiveData - -- row.dataIndex is the objectiveData index bound by name in QuestLog's - -- LocalizeObjectiveRows. row.index is only the server leaderboard position, - -- which does not necessarily line up with pfQuest's objectiveData ordering, - -- so prefer the name-resolved index and fall back to position only when the - -- row was never localized (e.g. database not ready at snapshot time). - local index=tonumber(row.dataIndex) or tonumber(row.index) - if not ordered or not index then return nil end - local entry=ordered[index] - if not entry then return nil end - if ObjectiveTypesMatch(row.type,entry.type) then return entry end - O.stats.typeMismatch=O.stats.typeMismatch+1 - return nil +local function EntryFromLiveObjectiveID(q,row) + local id=tonumber(row and row.objectiveID) + if not q or not id then return nil end + local typ=string.lower(tostring(row.type or "")) + local creatureMatch=nil + local objectMatch=nil + local itemMatch=nil + + for i=1,table.getn(q.objectiveData or {}) do + local entry=q.objectiveData[i] + if entry and tonumber(entry.id)==id then + if entry.kind=="creature" then creatureMatch=entry + elseif entry.kind=="gameObject" then objectMatch=entry + elseif entry.kind=="item" then itemMatch=entry end + end + end + + if typ=="item" then return itemMatch end + if typ=="object" or typ=="gameobject" then return objectMatch end + if typ=="monster" or typ=="creature" or typ=="kill" then + -- pfQuest notes that Vanilla can return "monster" for some object-style + -- objectives; let the live ClassicAPI ID disambiguate the namespace. + return creatureMatch or objectMatch + end + return creatureMatch or objectMatch or itemMatch end local function CandidateName(kind,id) @@ -314,7 +345,7 @@ function O:ResolveQuest(questID) if not q then return nil end local state=QuestieOcto.QuestLog.active[questID] - local result={questID=questID,creature={},gameObject={},item={}} + local result={questID=questID,creature={},gameObject={},item={},areaTrigger={}} -- Failed quests retain their tracker state but have no active objective map -- guidance until the server permits a retry. @@ -324,15 +355,21 @@ function O:ResolveQuest(questID) local irTargets,skipCreature,skipObject=BuildIRTargets(q) for _,row in pairs(state and state.objectives or {}) do - local ordered=EntryFromOrderedObjective(q,row) + local direct=EntryFromLiveObjectiveID(q,row) local kind=nil local id=nil - if ordered then - kind=ordered.kind - id=ordered.id + if direct then + -- pfQuest-style primary path: use ClassicAPI's live leaderboard entity + -- ID so server objective ordering cannot attach one boss's progress to + -- another boss's map node. + kind=direct.kind + id=direct.id O.stats.direct=O.stats.direct+1 else + -- Questie 5/6-style fallback: when no trustworthy live ID is available, + -- match the native objective text against same-type DB candidates rather + -- than assuming objective array positions are identical. kind=NormalizeLogType(row.type) id=BestSameTypeCandidate(q,kind,row,used) if id then O.stats.fallback=O.stats.fallback+1 end @@ -345,7 +382,11 @@ function O:ResolveQuest(questID) O.stats.mapped=O.stats.mapped+1 if kind=="creature" and not skipCreature[id] then - table.insert(result.creature,MergeState({kind="creature",id=id},row)) + local sourceID=PresentationCreatureSourceID(questID,id) + table.insert(result.creature,MergeState({ + kind="creature",id=sourceID,objectiveTargetID=id, + sourceOverride=(sourceID~=id) and true or nil + },row)) O.stats.creature=O.stats.creature+1 elseif kind=="gameObject" and not skipObject[id] then table.insert(result.gameObject,MergeState({kind="gameObject",id=id},row)) @@ -384,6 +425,27 @@ function O:ResolveQuest(questID) end end + -- Quest-bound area triggers are not generic exploration/fog helpers. pfQuest + -- resolves obj["A"] through ClassicAPI and shows those points while the quest + -- is active. Keep them separate from ordinary leaderboard entity matching: + -- Vanilla does not provide a reliable creature/object-style namespace for + -- these event triggers, and pfQuest likewise keeps them until the quest as a + -- whole is complete. + for _,areaTriggerID in pairs(q.objectives.areaTrigger or {}) do + local info=QuestieOcto.API and QuestieOcto.API.GetAreaTriggerInfo + and QuestieOcto.API:GetAreaTriggerInfo(areaTriggerID) or nil + if info then + table.insert(result.areaTrigger,{ + kind="areaTrigger", + id=tonumber(areaTriggerID), + mapID=info.areaID, + x=info.mapX, + y=info.mapY + }) + O.stats.areaTrigger=(O.stats.areaTrigger or 0)+1 + end + end + -- IR target guidance appears only while the required quest item is in the -- player's bags/equipment. Its corresponding ordinary U/O objective is -- suppressed even while the item is missing, matching pfQuest SearchQuestID. diff --git a/Quest/QuestLog.lua b/Quest/QuestLog.lua index 79dd331..8c274d7 100644 --- a/Quest/QuestLog.lua +++ b/Quest/QuestLog.lua @@ -29,6 +29,16 @@ local function ObjectiveProgressFallback(text) return tonumber(current),tonumber(required) end +local function SortedNumericKeys(src) + local keys={} + if type(src)~="table" then return keys end + for k in pairs(src) do + if type(k)=="number" then table.insert(keys,k) end + end + table.sort(keys) + return keys +end + local function ReadObjectives(index,questID) local objectives={} local snapshot={} @@ -42,7 +52,15 @@ local function ReadObjectives(index,questID) allDone=false local nonLogCount=0 - for i,row in pairs(apiObjectives) do + -- ClassicAPI returns objectives indexed by their quest-log objective + -- number. Keep that numeric identity/order instead of relying on pairs(), + -- whose iteration order can differ between Lua tables/clients. The row's + -- text, progress and completion therefore stay attached to the same + -- objective while LocalizeObjectiveRows applies the compact DB ordinal. + local objectiveKeys=SortedNumericKeys(apiObjectives) + for keyIndex=1,table.getn(objectiveKeys) do + local i=objectiveKeys[keyIndex] + local row=apiObjectives[i] local typ=row.type local text=row.text local current=tonumber(row.numFulfilled) @@ -53,6 +71,8 @@ local function ReadObjectives(index,questID) if required==nil then required=parsedRequired end end local finished=row.finished and true or false + local objectiveID=QuestieOcto.API and QuestieOcto.API.GetQuestLogLeaderBoardID + and QuestieOcto.API:GetQuestLogLeaderBoardID(i,index) or nil -- ClassicAPI/Turtle can publish the numerical counter before its separate -- `finished` boolean catches up. Treat a fulfilled numerical objective as @@ -76,7 +96,8 @@ local function ReadObjectives(index,questID) current=current, required=required, numFulfilled=current, - numRequired=required + numRequired=required, + objectiveID=objectiveID }) if not finished then allDone=false end end @@ -86,6 +107,7 @@ local function ReadObjectives(index,questID) table.insert(snapshot,finished and "1" or "0") table.insert(snapshot,tostring(current or -1)) table.insert(snapshot,tostring(required or -1)) + table.insert(snapshot,tostring(objectiveID or 0)) -- Map nodes only care whether an objective is still active. pfQuest's -- quest-state signature likewise records todo/done, not 3/10 -> 4/10. @@ -95,6 +117,7 @@ local function ReadObjectives(index,questID) table.insert(mapSnapshot,tostring(i)) table.insert(mapSnapshot,tostring(typ or "")) table.insert(mapSnapshot,finished and "1" or "0") + table.insert(mapSnapshot,tostring(objectiveID or 0)) end end @@ -111,6 +134,8 @@ local function ReadObjectives(index,questID) local text,typ,done=GetQuestLogLeaderBoard(i,index) local current,required=ObjectiveProgressFallback(text) local finished=done and true or false + local objectiveID=QuestieOcto.API and QuestieOcto.API.GetQuestLogLeaderBoardID + and QuestieOcto.API:GetQuestLogLeaderBoardID(i,index) or nil if current and required and required>0 and current>=required then finished=true @@ -123,7 +148,8 @@ local function ReadObjectives(index,questID) index=i,text=text,rawText=text,type=typ, complete=finished,finished=finished, current=current,required=required, - numFulfilled=current,numRequired=required + numFulfilled=current,numRequired=required, + objectiveID=objectiveID }) if not finished then allDone=false end end @@ -133,11 +159,13 @@ local function ReadObjectives(index,questID) table.insert(snapshot,finished and "1" or "0") table.insert(snapshot,tostring(current or -1)) table.insert(snapshot,tostring(required or -1)) + table.insert(snapshot,tostring(objectiveID or 0)) if typ~="log" then table.insert(mapSnapshot,tostring(i)) table.insert(mapSnapshot,tostring(typ or "")) table.insert(mapSnapshot,finished and "1" or "0") + table.insert(mapSnapshot,tostring(objectiveID or 0)) end end @@ -145,48 +173,35 @@ local function ReadObjectives(index,questID) end -local function ObjectiveDefName(def) - if not def or not def.id then return nil end - if def.kind=="creature" then return QuestieOcto.DatabaseAPI:GetCreatureName(def.id) end - if def.kind=="gameObject" then return QuestieOcto.DatabaseAPI:GetObjectName(def.id) end - if def.kind=="item" then return QuestieOcto.DatabaseAPI:GetItemName(def.id) end - return nil +local function ContainsObjectiveID(list,id) + id=tonumber(id) + if not id then return false end + for i=1,table.getn(list or {}) do + if tonumber(list[i])==id then return true end + end + return false end --- The live quest-log leaderboard is ordered by the server. pfQuest's compiled --- objectiveData is ordered by DB category (creatures, then objects, then items) --- and, within a category, by raw U/O/I array order. Those two orderings are NOT --- guaranteed to agree. A leaderboard row must therefore be bound to its DB --- objective definition by the entity NAME embedded in the native leaderboard --- text, never by shared array position. --- --- Positional binding attaches the wrong entity label to a row -- and because the --- x/y counter comes from the leaderboard row while the name comes from the DB --- definition, two same-type objectives whose orderings differ end up with their --- counts swapped (Shaman 7/10 / Bonesnapper 2/10 instead of 2/10 / 7/10). The --- Tracker escaped this because it renders rawText; tooltips render row.text --- produced here, so they showed the swapped values. -local function MatchDefIndexForRow(row,defs,used) - local nativeText=string.lower(tostring(row.rawText or row.text or "")) - if nativeText=="" then return nil end +local function LiveObjectiveKind(q,row) + local id=tonumber(row and row.objectiveID) + if not q or not id then return nil end + local typ=string.lower(tostring(row.type or "")) - -- Longest matching name wins so "Stonesplinter Shaman Elder" is preferred over - -- "Stonesplinter Shaman" when both appear as substrings of the same row. - local bestIndex=nil - local bestLen=-1 - for i=1,table.getn(defs) do - if not used[i] then - local name=ObjectiveDefName(defs[i]) - if name and name~="" then - local lowerName=string.lower(name) - if string.find(nativeText,lowerName,1,true) then - local len=string.len(lowerName) - if len>bestLen then bestLen=len; bestIndex=i end - end - end - end + -- pfQuest uses ClassicAPI's GetQuestLogLeaderBoardID and then determines + -- whether the returned ID is a unit/object/item from the quest/database. + -- Vanilla can report some object interactions as "monster", so prefer the + -- native type but allow the live ID to disambiguate creature vs game object. + if typ=="item" and ContainsObjectiveID(q.objectives.item,id) then return "item" end + if (typ=="object" or typ=="gameobject") and ContainsObjectiveID(q.objectives.gameObject,id) then return "gameObject" end + if typ=="monster" or typ=="creature" or typ=="kill" then + if ContainsObjectiveID(q.objectives.creature,id) then return "creature" end + if ContainsObjectiveID(q.objectives.gameObject,id) then return "gameObject" end end - return bestIndex + + if ContainsObjectiveID(q.objectives.creature,id) then return "creature" end + if ContainsObjectiveID(q.objectives.gameObject,id) then return "gameObject" end + if ContainsObjectiveID(q.objectives.item,id) then return "item" end + return nil end local function LocalizeObjectiveRows(questID,objectives) @@ -194,52 +209,27 @@ local function LocalizeObjectiveRows(questID,objectives) if not QuestieOcto.DatabaseAPI or not QuestieOcto.DatabaseAPI:IsReady() then return objectives end local q=QuestieOcto.QuestModel and QuestieOcto.QuestModel:Get(questID) or nil - local defs=q and q.objectiveData or nil - if type(defs)~="table" then return objectives end + if not q then return objectives end - local used={} - local boundIndex={} - - -- Pass 1: bind each row to the definition whose name appears in its native text. for i=1,table.getn(objectives) do local row=objectives[i] - if row then - local matched=MatchDefIndexForRow(row,defs,used) - if matched then - used[matched]=true - boundIndex[i]=matched - -- Objectives:ResolveQuest reads this to map map/hover nodes to the same - -- definition, so node.objectiveIndex stays consistent with row.text. - row.dataIndex=matched - end - end - end + local id=tonumber(row and row.objectiveID) + local kind=LiveObjectiveKind(q,row) + local name=nil - -- Pass 2: positional fallback for rows the name pass could not bind, using only - -- definitions no name match has already claimed. Preserves prior behavior on - -- data where the two orderings happened to agree. - for i=1,table.getn(objectives) do - if objectives[i] and not boundIndex[i] and defs[i] and not used[i] then - used[i]=true - boundIndex[i]=i - objectives[i].dataIndex=i - end - end + if id and kind=="creature" then name=QuestieOcto.DatabaseAPI:GetCreatureName(id) + elseif id and kind=="gameObject" then name=QuestieOcto.DatabaseAPI:GetObjectName(id) + elseif id and kind=="item" then name=QuestieOcto.DatabaseAPI:GetItemName(id) end - -- Pass 3: apply the localized label from the bound definition. Preserve live - -- progress/completion from the quest log; replace only the entity name. Rows - -- with no bound definition keep Turtle's native text as the final fallback. - for i=1,table.getn(objectives) do - local row=objectives[i] - local def=row and boundIndex[i] and defs[boundIndex[i]] or nil - if row and def and def.id then - local name=ObjectiveDefName(def) - if name and name~="" then - if row.current~=nil and row.required~=nil then - row.text=name..": "..tostring(row.current).."/"..tostring(row.required) - else - row.text=name - end + -- Never localize by objective array position. Questie 5/6 only assumes + -- positional identity when both sides contain one objective; pfQuest uses + -- ClassicAPI's live leaderboard ID. If no trustworthy ID is available, + -- keep Turtle's native objective text intact for the fallback matcher. + if row and name and name~="" then + if row.current~=nil and row.required~=nil then + row.text=name..": "..tostring(row.current).."/"..tostring(row.required) + else + row.text=name end end end @@ -409,6 +399,7 @@ function QL:Refresh(fastRefresh) local info=QuestieOcto.API:GetQuestLogInfo(index) local title=info and info.title local level=info and info.level + local tag=info and info.tag local isHeader=info and info.isHeader local isComplete=info and info.isComplete if title and isHeader then @@ -493,6 +484,7 @@ function QL:Refresh(fastRefresh) table.insert(snapshotParts,tostring(questID or 0)) table.insert(snapshotParts,title) table.insert(snapshotParts,tostring(level or 0)) + table.insert(snapshotParts,tostring(tag or "")) table.insert(snapshotParts,tostring(status)) table.insert(snapshotParts,objectiveSnapshot) @@ -502,6 +494,10 @@ function QL:Refresh(fastRefresh) logIndex=index, title=title, level=level, + -- Vanilla/pfQuest uses GetQuestLogTitle().tag as the live elite + -- marker. Preserve it with the quest instead of reclassifying + -- quests from database metadata. + tag=tag, zoneGroup=currentHeader, status=status, complete=complete, diff --git a/Questie-Octo.toc b/Questie-Octo.toc index 15d5a54..757a358 100644 --- a/Questie-Octo.toc +++ b/Questie-Octo.toc @@ -2,7 +2,7 @@ ## Title: Questie-Octo ## Notes: Quest helper. ## Author: Questie-Octo -## Version: 1.0.58 +## Version: 1.0.79 ## SavedVariables: QuestieOctoGlobalDB ## SavedVariablesPerCharacter: QuestieOctoDB @@ -45,6 +45,7 @@ Map\ContinentProjection.lua Map\Map.lua Map\MinimapSettings.lua Map\Minimap.lua +Map\Arrow.lua Libs\LibStub\LibStub.lua Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml Libs\AceCore-3.0\AceCore-3.0.xml diff --git a/QuestieOcto.lua b/QuestieOcto.lua index 6295e9d..8dbfcc0 100644 --- a/QuestieOcto.lua +++ b/QuestieOcto.lua @@ -2,7 +2,7 @@ QuestieOcto = QuestieOcto or {} local QO = QuestieOcto -QO.version = "1.0.58" +QO.version = "1.0.79" QO.enabled = false QO.ready = false QO.messages = {} @@ -24,42 +24,23 @@ local function RGBFromDifficultyFunction(level) return nil,nil,nil end -local function SameRGB(r,g,b,color) - if not color then return false end - local cr,cg,cb=tonumber(color.r),tonumber(color.g),tonumber(color.b) - if not r or not g or not b or not cr or not cg or not cb then return false end - return math.abs(r-cr)<0.01 and math.abs(g-cg)<0.01 and math.abs(b-cb)<0.01 -end - function QO:GetNativeQuestDifficultyColor(level,questID) - -- First authority: the actual color Turtle's native QuestLog_Update stored on - -- this quest's Blizzard title button. QuestLogEnhancements caches that value - -- before it changes the displayed text. This remains independent of skins. + -- First authority: the actual color Turtle's native QuestLog_Update local qle=self.QuestLogEnhancements if qle and qle.GetCachedQuestColor and questID then local r,g,b=qle:GetCachedQuestColor(questID) if r then return r,g,b end end + -- Fallback until the native Quest Log color has been observed and cached local r,g,b=RGBFromDifficultyFunction(level) if not r then return nil,nil,nil end - - -- Turtle/Octo keeps quests in the easy/green presentation instead of letting - -- the stock trivial band turn gray. ClassicAPI can expose stock 1.12's - -- GetDifficultyColor(), so normalize only that one band as a fallback until - -- the native Quest Log color for this quest has been observed and cached. - if QuestDifficultyColor and SameRGB(r,g,b,QuestDifficultyColor["trivial"]) then - local standard=QuestDifficultyColor["standard"] - if standard and standard.r then - return tonumber(standard.r),tonumber(standard.g),tonumber(standard.b) - end - end return r,g,b end function QO:Print(text) if DEFAULT_CHAT_FRAME then - DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccQuestie-Octo|r: "..tostring(text)) + DEFAULT_CHAT_FRAME:AddMessage("|cffffd700Questie-Octo|r: "..tostring(text)) end end diff --git a/Tracker/TrackerDriver.lua b/Tracker/TrackerDriver.lua index 186d280..751a801 100644 --- a/Tracker/TrackerDriver.lua +++ b/Tracker/TrackerDriver.lua @@ -136,6 +136,7 @@ function D:Rebuild() logIndex=state.logIndex, title=state.title, level=state.level or 0, + tag=state.tag, zoneGroup=state.zoneGroup or "Other", complete=state.complete and true or false, failed=state.failed and true or false, @@ -156,6 +157,7 @@ function D:Rebuild() local row=ordered[i] table.insert(snapshot,tostring(row.id)) table.insert(snapshot,row.zoneGroup or "") + table.insert(snapshot,tostring(row.tag or "")) table.insert(snapshot,row.complete and "1" or "0") table.insert(snapshot,row.failed and "1" or "0") for j=1,table.getn(row.objectives or {}) do diff --git a/Tracker/TrackerFrame.lua b/Tracker/TrackerFrame.lua index 1d0d0ed..df8adca 100644 --- a/Tracker/TrackerFrame.lua +++ b/Tracker/TrackerFrame.lua @@ -399,6 +399,24 @@ local function WrapTextToWidth(fs,text,maxWidth) return table.concat(lines,"\n"),count end +local function ZoneSpacerHeight(fontSize) + -- Keep zone groups visually separated without consuming a full text row. + -- The spacer scales with the configured tracker font and adds a small amount + -- for OUTLINE/THICKOUTLINE so heavier glyph edges do not feel crowded. + local height=(tonumber(fontSize) or 10)-4 + if height<4 then height=4 end + if height>13 then height=13 end + + if Settings():Get("trackerThickOutlineText") then + height=height+2 + elseif Settings():Get("trackerOutlineText") then + height=height+1 + end + + if height>15 then height=15 end + return height +end + local function ApplyRowStyle(row, contentWidth, constrainWidth) local size=tonumber(Settings():Get("trackerFontSize")) or 10 local left=0 @@ -407,7 +425,7 @@ local function ApplyRowStyle(row, contentWidth, constrainWidth) if kind=="zone" then size=size+1 - r,g,b=1,0.82,0 + r,g,b=0.88,0.69,0.24 left=0 elseif kind=="quest" then -- Match the classic Questie/Quest Log presentation: completion is shown @@ -464,7 +482,12 @@ local function ApplyRowStyle(row, contentWidth, constrainWidth) row.textIndent=left local baseHeight=size+4 - local height=baseHeight*math.max(1,lineCount) + local height + if kind=="spacer" then + height=ZoneSpacerHeight(size) + else + height=baseHeight*math.max(1,lineCount) + end row:SetHeight(height) end @@ -563,12 +586,20 @@ function T:UpdateTimerRows() end local function ObjectiveDisplayText(objective) - local text=objective and (objective.rawText or objective.text) or "" + -- Turtle can return leaderboard text without its entity name prefer the DB-localized text + local raw=objective and objective.rawText or nil + local loc=objective and objective.text or nil + local text + if raw and string.find(raw,"%a") then + text=raw + elseif loc and string.find(loc,"%a") then + text=loc + else + text=raw or loc or "" + end if not text then text="" end - -- The native Vanilla leaderboard text already contains the desired 5/10 form. -- Only synthesize amounts when the compatibility API supplied numeric progress - -- separately and the text itself omitted it. Never display percentages. local current=objective and tonumber(objective.current) local required=objective and tonumber(objective.required) if current and required and required>0 and not string.find(text,"%d+%s*/%s*%d+") then @@ -628,7 +659,9 @@ function T:Render() local prefix="" if quest.level and tonumber(quest.level) and tonumber(quest.level)>0 then - prefix="["..tostring(quest.level).."] " + -- Match pfQuest's Vanilla tracker convention: the native quest-log tag + -- adds a '+' inside the level brackets, e.g. [40+]. + prefix="["..tostring(quest.level)..(quest.tag and "+" or "").."] " end local title=tostring(quest.title or "Quest") if quest.failed then @@ -768,7 +801,7 @@ function T:InstallNativeTimerSuppression() if type(QuestTimerFrame_Update)=="function" then local secureHook=hooksecurefunc if type(secureHook)~="function" and LibStub then - local aceCore=LibStub("AceCore-3.0",true) + local aceCore=LibStub("QuestieOcto_AceCore-3.0",true) secureHook=aceCore and aceCore.hooksecurefunc end diff --git a/UI/GameMenu.lua b/UI/GameMenu.lua index e206f46..1a2b621 100644 --- a/UI/GameMenu.lua +++ b/UI/GameMenu.lua @@ -72,6 +72,21 @@ local function InstallDragonflightMenu() return true end +local function SkinPfUIGameMenuButton(button) + if not button or not pfUI or not pfUI.api or not pfUI.api.SkinButton then return end + if not pfUI.skin or not pfUI.skin["Game Menu"] then return end + + local disabled=pfUI_config and pfUI_config["disabled"] + if disabled and disabled["skin_Game Menu"]=="1" then return end + + -- pfUI skins Turtle WoW buttons that are added after its initial Game Menu + -- skin pass the same way: normalize the label, then use pfUI's own button + -- skin helper instead of imitating pfUI's appearance inside Questie-Octo. + local font=button.GetFontString and button:GetFontString() + if font and font.SetTextColor then font:SetTextColor(1,1,1,1) end + pfUI.api.SkinButton(button) +end + local function InstallBlizzardMenu() if not GameMenuFrame or not GameMenuButtonUIOptions or not GameMenuButtonKeybindings then return false @@ -92,6 +107,7 @@ local function InstallBlizzardMenu() button:SetPoint("TOP",anchor,"BOTTOM",0,-1) button:SetText("Questie Options") + SkinPfUIGameMenuButton(button) button:SetScript("OnClick",function() if HideUIPanel then HideUIPanel(GameMenuFrame) diff --git a/UI/Icons/LICENSE.md b/UI/Icons/LICENSE.md index 3d7f4cd..1493132 100644 --- a/UI/Icons/LICENSE.md +++ b/UI/Icons/LICENSE.md @@ -22,3 +22,11 @@ Files not identified above retain their respective upstream/source licensing. `pvp_available.tga` and `pvp_complete.tga` are conversions of the red PvP quest marker references supplied directly for the Questie-Octo project. + +## Questie-Octo gray available artwork + +`available_gray.tga` is a WoW-compatible TGA conversion of the 32x32 gray +available-quest marker image supplied directly by the Questie-Octo project +maintainer on 2026-08-17. The conversion preserves the supplied pixels and +alpha; it is not copied from Questie's `available_gray.blp`. + diff --git a/UI/Icons/arrow.tga b/UI/Icons/arrow.tga new file mode 100644 index 0000000..55c4669 Binary files /dev/null and b/UI/Icons/arrow.tga differ diff --git a/UI/Icons/available_gray.tga b/UI/Icons/available_gray.tga new file mode 100644 index 0000000..34829e2 Binary files /dev/null and b/UI/Icons/available_gray.tga differ diff --git a/UI/Options.lua b/UI/Options.lua index befedee..63d3c3d 100644 --- a/UI/Options.lua +++ b/UI/Options.lua @@ -21,9 +21,6 @@ local INNER_R,INNER_G,INNER_B,INNER_A=0.035,0.035,0.035,0.84 local INNER_BORDER_R,INNER_BORDER_G,INNER_BORDER_B,INNER_BORDER_A=0.20,0.20,0.20,0.95 local TAB_R,TAB_G,TAB_B,TAB_A=0.28,0.28,0.28,1.0 --- Capture each frame/texture's original Blizzard color exactly once, before the --- dark pass first mutates it. Unchecking Dark Theme then restores the precise --- on-load appearance rather than guessing at Blizzard's default color values. -- 1.12 reports nil for a backdrop color/border that was never explicitly set -- (the AceGUI Frame shell never sets a border color, so its default is white). -- Fill nil with 1 so the value is restorable. `x or 1` is safe here because Lua @@ -259,6 +256,9 @@ end function O:ApplyDarkTheme(apply) if not self.configFrame or not self.configFrame.frame then return end + -- No-argument callers (the initial open and AceConfig's post-refresh reapply) + -- must honor the saved setting, not force the theme on. Only the toggle's own + -- setter passes an explicit boolean. if apply == nil then apply=(QuestieOcto.MinimapSettings and QuestieOcto.MinimapSettings:Get("useDarkTheme")) and true or false end @@ -267,7 +267,9 @@ function O:ApplyDarkTheme(apply) if apply then DarkenOuterShell(shell) - -- Apply the inner treatment only below the outer shell. Keeping the shell seperate + -- Apply the inner treatment only below the outer shell. Keeping the shell + -- separate is important because its DialogFrame textures need vertex tinting, + -- while AceConfig's content containers look better with dark backdrops. if shell.GetChildren then local children={shell:GetChildren()} for _,child in pairs(children) do @@ -275,11 +277,13 @@ function O:ApplyDarkTheme(apply) end end else - -- Unchecked: return every frame/texture the dark pass touched to the exact blizzard color + -- Unchecked: return every frame/texture the dark pass touched to the exact + -- Blizzard color it had on load. RestoreDefaultTheme(shell) end - -- Run after backdrop darkening so the scrollbar is always the final/top UI layer + -- Run after backdrop darkening so the scrollbar is always the final/top UI + -- layer, including after AceConfig rebuilds the currently selected tab. RaiseScrollbar(shell) end @@ -288,7 +292,7 @@ local function Settings() end local function ClearSavedConfigPosition() - local Dialog=LibStub and LibStub("AceConfigDialog-3.0",true) + local Dialog=LibStub and LibStub("QuestieOcto_AceConfigDialog-3.0",true) if not Dialog or not Dialog.GetStatusTable then return end local status=Dialog:GetStatusTable(APP_NAME) @@ -350,32 +354,33 @@ local function CreateGeneralTab() }}, quest_options={type="group",order=10,name="Quest Options",inline=true,args={ - questLogShowLevels={type="toggle",order=1,name="Show Quest Levels",desc="Show quest levels in the Quest Log.",width="full",get=GetValue,set=SetValue}, - showLowLevelQuests={type="toggle",order=2,name="Show Low-Level Quests",desc="Show quests below the normal green quest range.",get=GetValue,set=SetValue}, - lowLevelQuestRange={type="range",order=2.1,name=function() + showQuestArrow={type="toggle",order=1,name="Show Quest Arrow",desc="Shows arrow that points towards nearest quest objective.",width="full",get=GetValue,set=SetValue}, + questLogShowLevels={type="toggle",order=2,name="Show Quest Levels",desc="Show quest levels in the Quest Log.",width="full",get=GetValue,set=SetValue}, + showLowLevelQuests={type="toggle",order=3,name="Show Low-Level Quests",desc="Show quests below the normal green quest range.",get=GetValue,set=SetValue}, + lowLevelQuestRange={type="range",order=4,name=function() local value=tonumber(Settings():Get("lowLevelQuestRange")) or 35 if value>=35 then return "Levels Below: All" end return "Levels Below: "..tostring(value) end,desc="Limit how many levels below you are shown. All removes the limit.",width="normal",min=5,max=35,step=5,arg={questieHideEditBox=true,questieMaxLabel="All",questieCommitOnMouseUp=true,questieLiveLabelPrefix="Levels Below: "},disabled=function() return not Settings():Get("showLowLevelQuests") end,get=GetValue,set=SetValue}, - showRepeatableQuests={type="toggle",order=3,name="Show Repeatable Quests",desc="Show available repeatable quests.",width="full",get=GetValue,set=SetValue}, - showEventQuests={type="toggle",order=4,name="Show Event Quests",desc="Show available event quests.",width="full",get=GetValue,set=SetValue}, - showPvPRelatedQuests={type="toggle",order=5,name="Show PVP Related Quests",desc="Show PvP quest icons on the map and minimap.",width="full",get=GetValue,set=SetValue}, + showRepeatableQuests={type="toggle",order=5,name="Show Repeatable Quests",desc="Show available repeatable quests.",width="full",get=GetValue,set=SetValue}, + showEventQuests={type="toggle",order=6,name="Show Event Quests",desc="Show available event quests.",width="full",get=GetValue,set=SetValue}, + showPvPRelatedQuests={type="toggle",order=7,name="Show PVP Related Quests",desc="Show PvP quest icons on the map and minimap.",width="full",get=GetValue,set=SetValue}, }}, reset_header={type="header",order=90,name="Questie Options"}, enableDarkTheme={type="toggle",order=91,name="Enable Dark Theme",desc="Darken the configuration frame backdrops.",width="full", - get=function() - return Settings():Get("useDarkTheme") + get=function() + return Settings():Get("useDarkTheme") end, - set=function(_, value) + set=function(_, value) Settings():Set("useDarkTheme", value) - O:ApplyDarkTheme(value) -- Passes true/false directly + O:ApplyDarkTheme(value) end }, reset_text={type="description",order=92,name="Restore Questie-Octo options to their defaults. Quest data and completed-quest history are not deleted.",fontSize="medium"}, resetOptions={type="execute",order=93,name="Reset Options",desc="Restore option defaults.",func=function() Settings():Reset() ClearSavedConfigPosition() - local Registry=LibStub and LibStub("AceConfigRegistry-3.0",true) + local Registry=LibStub and LibStub("QuestieOcto_AceConfigRegistry-3.0",true) if Registry and Registry.NotifyChange then Registry:NotifyChange(APP_NAME) end end}, }, @@ -500,9 +505,9 @@ end function O:Initialize() if self.initialized then return true end - local AceGUI=LibStub and LibStub("AceGUI-3.0",true) - local Registry=LibStub and LibStub("AceConfigRegistry-3.0",true) - local Dialog=LibStub and LibStub("AceConfigDialog-3.0",true) + local AceGUI=LibStub and LibStub("QuestieOcto_AceGUI-3.0",true) + local Registry=LibStub and LibStub("QuestieOcto_AceConfigRegistry-3.0",true) + local Dialog=LibStub and LibStub("QuestieOcto_AceConfigDialog-3.0",true) self.stats.aceGUI=AceGUI and true or false self.stats.aceRegistry=Registry and true or false @@ -609,7 +614,7 @@ end function O:Show() if not self:Initialize() then return end - local Dialog=LibStub("AceConfigDialog-3.0") + local Dialog=LibStub("QuestieOcto_AceConfigDialog-3.0") -- Questie 3.3.5 refreshes the existing standalone frame through Open(). Dialog:Open(APP_NAME,self.configFrame) RecenterConfigFrame(self.configFrame)