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
+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