Force pushing all my changes

This commit is contained in:
User
2026-08-16 20:28:28 -05:00
parent 76505bf183
commit bb237b8fae
58 changed files with 1107 additions and 312 deletions
+48
View File
@@ -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
+1 -1
View File
@@ -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
+43
View File
@@ -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
+5
View File
@@ -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
+88 -4
View File
@@ -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
+73
View File
@@ -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.
@@ -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
@@ -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)
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+207
View File
@@ -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 d<bestDist then bestDist=d; best=t end
end
return best,bestDist
end
function A:OnUpdate()
local frame=self.frame
if not frame then return end
if not Settings():Get("showQuestArrow") then
if frame:IsShown() then frame:Hide() end
return
end
local px,py
if GetPlayerMapPosition then px,py=GetPlayerMapPosition("player") end
px,py=(px or 0)*100,(py or 0)*100
if px==0 and py==0 then
if frame:IsShown() then frame:Hide() end
return
end
local mapID=CurrentMapID()
if mapID~=frame.mapID or (frame.refresh or 0)<GetTime() then
frame.mapID=mapID
frame.targets=CollectTargets(mapID)
frame.refresh=GetTime()+1
end
local best,bestDist=NearestTarget(frame.targets,px,py)
if not best then
if frame:IsShown() then frame:Hide() end
return
end
if not frame:IsShown() then frame:Show() end
-- Arrow direction and sprite-sheet cell math taken from pfQuest/TomTomVanilla.
local xDelta=(best[1]-px)*1.5
local yDelta=(best[2]-py)
local dir=atan2(xDelta,-(yDelta))
dir=dir>0 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")
+9 -1
View File
@@ -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
+21
View File
@@ -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)
+4 -4
View File
@@ -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={}
+41 -4
View File
@@ -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)
+107 -43
View File
@@ -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*yPos<radiusSquared
end
if inside then
frameIndex=frameIndex+1
local pin=self:GetOrCreate(frameIndex)
local bound=(pin.boundDescriptor==desc and pin.boundRevision==revision)
if not bound then bound=BindDescriptor(pin,desc,revision) end
-- There is intentionally no hard node cap here. Item-start entries from the
-- base plan are skipped, then the same dedicated item-start plan used by the
-- World Map is scanned so ultra-rare sources have identical representation.
-- Keep this as a two-pass loop instead of allocating a closure/table in the
-- moving-player hot path.
local planPass=1
while planPass<=2 do
local scanPlan=planPass==1 and self.plan or self.itemStartPlan
local allowItemStart=planPass==2
local bindRevision=(tonumber(revision) or 0)*2+(allowItemStart and 1 or 0)
if bound then
pin.questieOctoBaseX=xPos
pin.questieOctoBaseY=-yPos
local groupKey=pin.coordKey or tostring(x)..":"..tostring(y)
visibleGroups[groupKey]=visibleGroups[groupKey] or {}
table.insert(visibleGroups[groupKey],pin)
table.insert(activeFrames,pin)
if not pin:IsShown() then pin:Show() end
CountVisible(pin)
for _,desc in ipairs(scanPlan or {}) do
if DescriptorHasVisibleEntry(desc,revision,allowItemStart) 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
frameIndex=frameIndex-1
inside=xPos*xPos+yPos*yPos<radiusSquared
end
if inside then
frameIndex=frameIndex+1
local pin=self:GetOrCreate(frameIndex)
local bound=(pin.boundDescriptor==desc and pin.boundRevision==bindRevision)
if not bound then bound=BindDescriptor(pin,desc,bindRevision,allowItemStart) end
if bound then
pin.questieOctoBaseX=xPos
pin.questieOctoBaseY=-yPos
local groupKey=pin.coordKey or tostring(x)..":"..tostring(y)
visibleGroups[groupKey]=visibleGroups[groupKey] or {}
table.insert(visibleGroups[groupKey],pin)
table.insert(activeFrames,pin)
if not pin:IsShown() then pin:Show() end
CountVisible(pin)
else
frameIndex=frameIndex-1
end
end
end
end
end
planPass=planPass+1
end
local offsets={{0,0},{10,0},{-10,0},{0,10},{0,-10},{8,8},{-8,8},{8,-8},{-8,-8}}
@@ -785,7 +838,7 @@ function MM:UpdatePositions(force)
self.activeFrames=activeFrames
self.stats.active=frameIndex
self.stats.positionUpdates=self.stats.positionUpdates+1
self.stats.scannedDescriptors=table.getn(self.plan or {})
self.stats.scannedDescriptors=table.getn(self.plan or {})+table.getn(self.itemStartPlan or {})
self.stats.poolSize=table.getn(self.frames or {})
end
@@ -829,9 +882,20 @@ function MM:Start()
f:RegisterEvent("ZONE_CHANGED")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:RegisterEvent("MINIMAP_ZONE_CHANGED")
f:RegisterEvent("PLAYER_LEVEL_UP")
f:SetScript("OnEvent",function()
local eventName=event
if eventName=="PLAYER_LEVEL_UP" then
-- Force descriptor rebinding so +25/+26 gray presentation changes as
-- soon as the player levels, without touching the prepared map geometry.
QuestieOcto.Scheduler:After(0.01,function()
MM.bindRevision=(MM.bindRevision or 0)+1
MM:RefreshPlan()
end,"minimap-gray-level-refresh")
return
end
QuestieOcto.Scheduler:After(0.01,function()
RestoreCurrentZoneMapContext(eventName)
MM:RefreshPlan()
+9
View File
@@ -90,6 +90,9 @@ S.defaults={
questLogShowLevels=true,
questLogDifficultyColors=true,
-- Quest objective arrow is opt-in.
showQuestArrow=false,
-- Options-frame dark theme is off by default so the config window opens with
-- native Blizzard colors until the player opts in.
useDarkTheme=false,
@@ -237,6 +240,7 @@ function S:Set(key,value)
key=="enableTooltipsNPCID" or key=="enableTooltipsItemID" or
key=="enableTooltipDroprates" or
key=="questLogShowLevels" or key=="questLogDifficultyColors" or
key=="showQuestArrow" or
key=="useDarkTheme" or
key=="trackerEnabled" or key=="trackerLocked" or key=="trackerAutoTrack" or
key=="trackerShowCompleted" or key=="trackerHideCompletedObjectives" or key=="trackerHideInCombat" or
@@ -310,6 +314,11 @@ function S:Set(key,value)
return true
end
-- The quest arrow reads its setting live each frame; just persist it.
if key=="showQuestArrow" then
return true
end
if key=="showLowLevelQuests" or key=="lowLevelQuestRange" or key=="showEventQuests" or key=="showRepeatableQuests" or key=="showPvPRelatedQuests" then
if QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.FastRefresh then
QuestieOcto.AvailableQuests:FastRefresh()
+32 -3
View File
@@ -17,6 +17,7 @@ N.stats={
objectiveCreature=0,
objectiveObject=0,
objectiveItemSource=0,
objectiveArea=0,
turnin=0,
flightMaster=0,
auctioneer=0,
@@ -41,6 +42,7 @@ local function NewStats()
objectiveCreature=0,
objectiveObject=0,
objectiveItemSource=0,
objectiveArea=0,
turnin=0,
flightMaster=0,
auctioneer=0,
@@ -134,15 +136,25 @@ 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 AddCreatureNode(questID,role,creatureID,itemID,chance,objectiveState,vendor)
local q=QuestieOcto.QuestModel:Get(questID)
local node={
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,sourceKind="creature",sourceID=creatureID,
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=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
sourceRank=QuestieOcto.DatabaseAPI:GetCreatureRank(creatureID),
respawnSeconds=QuestieOcto.DatabaseAPI:GetCreatureRespawnSeconds(creatureID),
itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil,
chance=chance,vendor=vendor and true or false,coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
chance=chance,vendor=vendor and true or false,coords=ConditionalCreatureCoords(q,creatureID,role) or QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID),
conditionalOffer=q and q.conditionalOffer or nil
}
AddNode(ApplyObjectiveState(node,objectiveState))
end
@@ -150,7 +162,7 @@ end
local function AddObjectNode(questID,role,objectID,itemID,chance,objectiveState)
local q=QuestieOcto.QuestModel:Get(questID)
local node={
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,sourceKind="gameObject",sourceID=objectID,
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=objectID,
sourceName=QuestieOcto.DatabaseAPI:GetObjectName(objectID),
itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil,
chance=chance,coords=QuestieOcto.DatabaseAPI:GetObjectCoords(objectID)
@@ -158,6 +170,16 @@ local function AddObjectNode(questID,role,objectID,itemID,chance,objectiveState)
AddNode(ApplyObjectiveState(node,objectiveState))
end
local function AddAreaTriggerNode(questID,source)
if not source or not source.mapID or not source.x or not source.y then return end
local q=QuestieOcto.QuestModel:Get(questID)
AddNode(ApplyObjectiveState({
questID=questID,role="objectiveArea",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="areaTrigger",sourceID=source.id,sourceName="Exploration Mark",
coords={{source.x,source.y,source.mapID}}
},source))
end
local function BuildAvailableQuestNodes(questID)
local q=QuestieOcto.QuestModel:Get(questID)
if not q then return end
@@ -479,6 +501,12 @@ local function BuildActiveQuestNodes(questID)
end
end
end
for _,src in pairs(resolved.areaTrigger or {}) do
if not src.complete then
AddAreaTriggerNode(questID,src)
CurrentStats().objectiveArea=(CurrentStats().objectiveArea or 0)+1
end
end
end
local function BuildActiveNodes()
@@ -496,6 +524,7 @@ local function StatKeyForNode(node)
if node.role=="objectiveCreature" then return "objectiveCreature" end
if node.role=="objectiveObject" then return "objectiveObject" end
if node.role=="objectiveItemSource" then return "objectiveItemSource" end
if node.role=="objectiveArea" then return "objectiveArea" end
if node.role=="turnin" then return "turnin" end
if node.role=="flightMaster" then return "flightMaster" end
if node.role=="auctioneer" then return "auctioneer" end
+1 -1
View File
@@ -31,7 +31,7 @@ function P:GetCurrentDensitySignature()
end
local function ExactRole(role)
return role=="available" or role=="turnin" or role=="flightMaster"
return role=="available" or role=="turnin" or role=="objectiveArea" or role=="flightMaster"
or role=="auctioneer" or role=="banker" or role=="mailbox"
or role=="battlemaster" or role=="innkeeper" or role=="meetingStone"
or role=="repair" or role=="spiritHealer" or role=="stableMaster" or role=="vendor"
+74 -41
View File
@@ -124,6 +124,28 @@ local function FormatDropRate(rate)
return string.format("%.3f",rate)
end
local function ItemStartAreaDropRateText(area)
local minimum=nil
local maximum=nil
for _,source in pairs((area and area.sourceList) or {}) do
local chance=tonumber(source.chance)
if chance and chance>0 then
if not minimum or chance<minimum then minimum=chance end
if not maximum or chance>maximum 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
+2 -1
View File
@@ -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
+15 -4
View File
@@ -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
+9 -3
View File
@@ -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
+15 -3
View File
@@ -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)
+84 -22
View File
@@ -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.
+78 -82
View File
@@ -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,
+2 -1
View File
@@ -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
+4 -23
View File
@@ -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
+2
View File
@@ -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
+40 -7
View File
@@ -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
+16
View File
@@ -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)
+8
View File
@@ -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`.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+27 -22
View File
@@ -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)