--[[-------------------------------------------------------------------------- -- TomTom - A navigational assistant for World of Warcraft -- -- CrazyTaxi: A crazy-taxi style arrow used for waypoint navigation. -- concept taken from MapNotes2 (Thanks to Mery for the idea, along -- with the original artwork.) ----------------------------------------------------------------------------]] local addonName, addon = ... local L = TomTomLocals local ldb = LibStub("LibDataBroker-1.1") local IMAGE_ARROW = "Interface\\Addons\\TomTom\\Images\\Arrow-1024" local IMAGE_ARROW_UP = "Interface\\AddOns\\TomTom\\Images\\Arrow-UP-1024" local IMAGE_STATIC_ARROW = "Interface\\Addons\\TomTom\\Images\\StaticArrow" local themeHandler = addon.CrazyArrowThemeHandler local twopi = math.pi * 2 local wayframe = CreateFrame("Button", "TomTomCrazyArrow", UIParent) wayframe:EnableMouse(true) wayframe:SetMovable(true) wayframe:SetClampedToScreen(true) wayframe:Hide() -- Frame used to control the scaling of the title and friends local titleframe = CreateFrame("Frame", nil, wayframe) wayframe.title = titleframe:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") wayframe.status = titleframe:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") wayframe.tta = titleframe:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") wayframe.title:SetPoint("TOP", wayframe, "BOTTOM", 0, 0) wayframe.status:SetPoint("TOP", wayframe.title, "BOTTOM", 0, 0) wayframe.tta:SetPoint("TOP", wayframe.status, "BOTTOM", 0, 0) local function ShowCrazyArrow() wayframe:Show() end local function OnDragStart(self, button) if not TomTom.db.profile.arrow.lock then self:StartMoving() end end local function OnDragStop(self, button) self:StopMovingOrSizing() self:SetUserPlaced(false) -- point, relativeTo, relativePoint, xOfs, yOfs TomTom.profile.arrow.position = { self:GetPoint() } TomTom.profile.arrow.position[2] = nil -- Note we are relative to UIParent end local function OnEvent(self, event, ...) if (event == "ZONE_CHANGED_NEW_AREA" or event == "ZONE_CHANGED") and TomTom.profile.arrow.enable then ShowCrazyArrow() return end if (event == "PLAYER_ENTERING_WORLD") then wayframe:ClearAllPoints() if not TomTom.profile.arrow.position then TomTom.profile.arrow.position = {"CENTER", nil , "CENTER", 0, 0} end local pos = TomTom.profile.arrow.position wayframe:SetPoint(pos[1], UIParent, pos[3], pos[4], pos[5]) end end wayframe:SetScript("OnDragStart", OnDragStart) wayframe:SetScript("OnDragStop", OnDragStop) wayframe:RegisterForDrag("LeftButton") wayframe:RegisterEvent("ZONE_CHANGED_NEW_AREA") wayframe:RegisterEvent("ZONE_CHANGED") wayframe:RegisterEvent("PLAYER_ENTERING_WORLD") wayframe:SetScript("OnEvent", OnEvent) local active_point, arrive_distance, showDownArrow, point_title function TomTom:SetCrazyArrow(uid, dist, title) if active_point and active_point.corpse and self.db.profile.arrow.stickycorpse then -- do not change the waypoint arrow from corpse return end active_point = uid arrive_distance = dist point_title = title if self.profile.arrow.enable then wayframe.title:SetText(title or L["Unknown waypoint"]) ShowCrazyArrow() if wayframe.crazyFeedFrame then wayframe.crazyFeedFrame:Show() end end end function TomTom:IsCrazyArrowEmpty() return not active_point end local status = wayframe.status local tta = wayframe.tta local arrow = wayframe.arrow local last_distance = 0 local tta_throttle = 0 local speed = 0 local speed_count = 0 local function OnUpdate(self, elapsed) if not active_point then self:Hide() return end local dist,x,y = TomTom:GetDistanceToWaypoint(active_point) -- The only time we cannot calculate the distance is when the waypoint -- is on another continent, or we are in an instance if not dist then if not TomTom:IsValidWaypoint(active_point) then active_point = nil -- Change the crazy arrow to point at the closest waypoint if TomTom.profile.arrow.setclosest then TomTom:SetClosestWaypoint() return end end self:Hide() return end status:SetText(addon:GetFormattedDistance(dist)) -- Showing the arrival arrow? if dist <= arrive_distance then if not showDownArrow then themeHandler:SwitchToArrivalArrow(wayframe) showDownArrow = true end themeHandler:ArrivalArrow_OnUpdate(elapsed) else if showDownArrow then themeHandler:SwitchToNavigationArrow(wayframe) showDownArrow = false end local angle = TomTom:GetDirectionToWaypoint(active_point) local player = GetPlayerFacing() if not player then if not TomTom:IsValidWaypoint(active_point) then active_point = nil -- Change the crazy arrow to point at the closest waypoint if TomTom.profile.arrow.setclosest then TomTom:SetClosestWaypoint() return end end self:Hide() return end angle = angle - player themeHandler:NavigationArrow_OnUpdate(elapsed, angle) end -- Calculate the TTA every second (%01d:%02d) tta_throttle = tta_throttle + elapsed if tta_throttle >= 1.0 then -- Calculate the speed in yards per sec at which we're moving local current_speed = (last_distance - dist) / tta_throttle if last_distance == 0 then current_speed = 0 end if speed_count < 2 then speed = (speed + current_speed) / 2 speed_count = speed_count + 1 else speed_count = 0 speed = current_speed end if speed > 0 then local eta = math.abs(dist / speed) tta:SetFormattedText("%s:%02d", math.floor(eta / 60), math.floor(eta % 60)) else tta:SetText("***") end last_distance = dist tta_throttle = 0 end end function TomTom:ShowHideCrazyArrow() if self.profile.arrow.enable then if self.profile.arrow.hideDuringPetBattles and C_PetBattles and C_PetBattles.IsInBattle() then wayframe:Hide() return end ShowCrazyArrow() if self.profile.arrow.noclick then wayframe:EnableMouse(false) else wayframe:EnableMouse(true) end -- Set the scale and alpha wayframe:SetScale(TomTom.db.profile.arrow.scale) -- Do not allow the arrow to be invisible if TomTom.db.profile.arrow.alpha < 0.1 then TomTom.db.profile.arrow.alpha = 1.0 end -- Set the stratum if TomTom.db.profile.arrow.highstrata then wayframe:SetFrameStrata("HIGH") else wayframe:SetFrameStrata("MEDIUM") end wayframe:SetAlpha(TomTom.db.profile.arrow.alpha) local width = TomTom.db.profile.arrow.title_width local height = TomTom.db.profile.arrow.title_height local scale = TomTom.db.profile.arrow.title_scale wayframe.title:SetWidth(width) wayframe.title:SetHeight(height) titleframe:SetScale(scale) titleframe:SetAlpha(TomTom.db.profile.arrow.title_alpha) if self.profile.arrow.showdistance then wayframe.status:Show() wayframe.tta:ClearAllPoints() wayframe.tta:SetPoint("TOP", wayframe.status, "BOTTOM", 0, 0) else wayframe.status:Hide() wayframe.tta:ClearAllPoints() wayframe.tta:SetPoint("TOP", wayframe, "BOTTOM", 0, 0) end -- Set the theme local theme = self.profile.arrow.theme themeHandler:SetActiveTheme(wayframe, theme, showDownArrow) if self.profile.arrow.showtta then tta:Show() else tta:Hide() end else wayframe:Hide() end end wayframe:SetScript("OnUpdate", OnUpdate) --[[------------------------------------------------------------------------- -- Dropdown -------------------------------------------------------------------------]]-- local function crazyArrowDropdownGenerator(owner, rootDescription) rootDescription:CreateTitle(L["TomTom Waypoint Arrow"]); -- Waypoint communications local sendWaypoint = rootDescription:CreateButton(L["Send waypoint to"]) sendWaypoint:CreateTitle(L["Waypoint communication"]) sendWaypoint:CreateButton(L["Send to party"], function() addon:SendWaypoint(addon.dropdown_uid, "PARTY") end) sendWaypoint:CreateButton(L["Send to raid"], function() addon:SendWaypoint(addon.dropdown_uid, "RAID") end) sendWaypoint:CreateButton(L["Send to battleground"], function() addon:SendWaypoint(addon.dropdown_uid, "BATTLEGROUND") end) sendWaypoint:CreateButton(L["Send to guild"], function() addon:SendWaypoint(addon.dropdown_uid, "GUILD") end) rootDescription:CreateButton(L["Clear waypoint from crazy arrow"], function() addon:ClearCrazyArrowPoint(false) end) rootDescription:CreateButton(L["Remove waypoint"], function() addon:ClearCrazyArrowPoint(true) end) rootDescription:CreateButton(L["Remove all waypoints from this zone"], function() local uid = addon.dropdown_uid local data = uid local mapId = data[1] for key, waypoint in pairs(addon.waypoints[mapId]) do addon:RemoveWaypoint(waypoint) end end) rootDescription:CreateButton(L["Remove all waypoints"], function() if addon.db.profile.general.confirmremoveall then StaticPopup_Show("TOMTOM_REMOVE_ALL_CONFIRM") else StaticPopupDialogs["TOMTOM_REMOVE_ALL_CONFIRM"].OnAccept() return end end) local maybeLocked = addon.db.profile.arrow.lock and L["Unlock crazy arrow"] or L["Lock crazy arrow"] rootDescription:CreateButton(L[maybeLocked], function() addon.db.profile.arrow.lock = not addon.db.profile.arrow.lock addon:ShowHideCrazyArrow() end) end local function WayFrame_OnClick(self, button) if active_point then if addon.db.profile.arrow.menu then addon.dropdown_uid = active_point MenuUtil.CreateContextMenu(self, crazyArrowDropdownGenerator) end end end wayframe:RegisterForClicks("RightButtonUp") wayframe:SetScript("OnClick", WayFrame_OnClick) local function getCoords(column, row) local xstart = (column * 56) / 512 local ystart = (row * 42) / 512 local xend = ((column + 1) * 56) / 512 local yend = ((row + 1) * 42) / 512 return xstart, xend, ystart, yend end local texcoords = setmetatable({}, {__index = function(t, k) local col,row = k:match("(%d+):(%d+)") col,row = tonumber(col), tonumber(row) local obj = {getCoords(col, row)} rawset(t, k, obj) return obj end}) wayframe:RegisterEvent("ADDON_LOADED") local function wayframe_OnEvent(self, event, arg1, ...) if arg1 == "TomTom" then if TomTom.db.profile.feeds.arrow then -- Create a data feed for coordinates local feed_crazy = ldb:NewDataObject("TomTom_CrazyArrow", { type = "data source", icon = IMAGE_ARROW, staticIcon = IMAGE_STATIC_ARROW, text = "Crazy", iconR = 0.2, iconG = 1.0, iconB = 0.2, iconCoords = texcoords["1:1"], OnTooltipShow = function(tooltip) local dist = TomTom:GetDistanceToWaypoint(active_point) if dist then tooltip:AddLine(point_title or L["Unknown waypoint"]) tooltip:AddLine(string.format(L["%d yards"], dist), 1, 1, 1) end end, OnClick = WayFrame_OnClick, }) local crazyFeedFrame = CreateFrame("Frame") local throttle = TomTom.db.profile.feeds.arrow_throttle local counter = 0 function TomTom:UpdateArrowFeedThrottle() throttle = TomTom.db.profile.feeds.arrow_throttle end wayframe.crazyFeedFrame = crazyFeedFrame crazyFeedFrame:SetScript("OnUpdate", function(self, elapsed) counter = counter + elapsed if counter < throttle then return end counter = 0 if not active_point then self:Hide() end local angle = TomTom:GetDirectionToWaypoint(active_point) local player = GetPlayerFacing() if not angle or not player then feed_crazy.iconCoords = texcoords["1:1"] feed_crazy.iconR = 0.2 feed_crazy.iconG = 1.0 feed_crazy.iconB = 0.2 feed_crazy.text = "No waypoint" return end angle = angle - player local perc = math.abs((math.pi - math.abs(angle)) / math.pi) local gr,gg,gb = unpack(TomTom.db.profile.arrow.goodcolor) local mr,mg,mb = unpack(TomTom.db.profile.arrow.middlecolor) local br,bg,bb = unpack(TomTom.db.profile.arrow.badcolor) local r,g,b = addon:ColorGradient(perc, br, bg, bb, mr, mg, mb, gr, gg, gb) -- If we're 98% heading in the right direction, then use the exact -- color instead of the gradient. This allows us to distinguish 'good' -- from 'on target'. Thanks to Gregor_Curse for the suggestion. if perc > 0.98 then r,g,b = unpack(TomTom.db.profile.arrow.exactcolor) end feed_crazy.iconR = r feed_crazy.iconG = g feed_crazy.iconB = b local cell = floor(angle / twopi * 108 + 0.5) % 108 local column = cell % 9 local row = floor(cell / 9) local key = column .. ":" .. row feed_crazy.iconCoords = texcoords[key] feed_crazy.text = point_title or L["Unknown waypoint"] end) end end end wayframe:HookScript("OnEvent", wayframe_OnEvent) --[[------------------------------------------------------------------------- -- API for manual control of Crazy Arrow -- -- This allow for an addon to specify their own control of the waypoint -- arrow without needing to tip-toe around the API. It may not integrate -- properly, but in general it should work. -- -- Example Usage: -- -- if not TomTom:CrazyArrowIsHijacked() then -- TomTom:HijackCrazyArrow(function(self, elapsed) -- -- Random angle -- local angle = math.random(math.pi * 2) -- TomTom:SetCrazyArrowDirection(angle) -- -- Random color -- local r = math.random(100) / 100 -- local g = math.random(100) / 100 -- local b = math.random(100) / 100 -- TomTom:SetCrazyArrowColor(r, g, b) -- -- Titles -- TomTom:SetCrazyArrowTitle("Hijacked arrow", "Hijacked", "You will never arrive") -- end) -- end -- -- -- At some point later -- TomTom:ReleaseCrazyArrow() -------------------------------------------------------------------------]]-- -- Set the direction of the crazy arrow without taking the player's facing -- into consideration. This can be accomplished by subtracting -- GetPlayerFacing() from the angle before passing it in. function TomTom:SetCrazyArrowDirection(angle) local cell = floor(angle / twopi * 108 + 0.5) % 108 local column = cell % 9 local row = floor(cell / 9) local key = column .. ":" .. row arrow:SetTexCoord(unpack(texcoords[key])) end -- Convenience function to set the color of the crazy arrow function TomTom:SetCrazyArrowColor(r, g, b, a) arrow:SetVertexColor(r, g, b, a) end -- Convenience function to set the title/status and time to arrival text -- of the crazy arrow. function TomTom:SetCrazyArrowTitle(title, status, tta) wayframe.title:SetText(title) wayframe.status:SetText(status) wayframe.tta:SetText(tta) end -- Function to actually hijack the crazy arrow by replacing the OnUpdate script function TomTom:HijackCrazyArrow(onupdate) wayframe:SetScript("OnUpdate", onupdate) wayframe.hijacked = true ShowCrazyArrow() end -- Releases the crazy arrow by restoring the original OnUpdate script function TomTom:ReleaseCrazyArrow() wayframe:SetScript("OnUpdate", OnUpdate) wayframe.hijacked = false end -- Returns whether or not the crazy arrow is currently hijacked function TomTom:CrazyArrowIsHijacked() return wayframe.hijacked end -- Logs Crazy Arrow status function TomTom:DebugCrazyArrow() local msg msg = string.format(L["|cffffff78TomTom:|r CrazyArrow %s hijacked"], (wayframe.hijacked and L["is"]) or L["not"]) ChatFrame1:AddMessage(msg) msg = string.format(L["|cffffff78TomTom:|r CrazyArrow %s visible"], (wayframe:IsVisible() and L["is"]) or L["not"]) ChatFrame1:AddMessage(msg) msg = string.format(L["|cffffff78TomTom:|r Waypoint %s valid"], (active_point and TomTom:IsValidWaypoint(active_point) and L["is"]) or L["not"]) ChatFrame1:AddMessage(msg) local dist,x,y = TomTom:GetDistanceToWaypoint(active_point) msg = string.format("|cffffff78TomTom:|r Waypoint distance=%s", tostring(dist)) ChatFrame1:AddMessage(msg) if wayframe:IsVisible() then local point, relativeTo, relativePoint, xOfs, yOfs = wayframe:GetPoint(1) relativeTo = (relativeTo and relativeTo:GetName()) or "UIParent" msg = string.format("|cffffff78TomTom:|r CrazyArrow point=%s frame=%s rpoint=%s xo=%.2f yo=%.2f", point, relativeTo, relativePoint, xOfs, yOfs) ChatFrame1:AddMessage(msg) end end -- Clear the waypoint from the crazy arrow -- remove: true if the waypoint should be removed entirely, otherwise -- this method only clears it from the crazy arrow itself. function TomTom:ClearCrazyArrowPoint(remove) if active_point then if remove then local uid = active_point TomTom:RemoveWaypoint(uid) else local prior = active_point active_point = nil if TomTom.profile.arrow.setclosest then local uid = TomTom:GetClosestWaypoint() if uid and uid ~= prior then TomTom:SetClosestWaypoint() return end end end end end local regionsAreMetric = { false, -- US true, true, true, true, } function TomTom:RegionIsMetric() if self.accountRegion == nil then local gameAccountInfo = C_BattleNet.GetGameAccountInfoByGUID(UnitGUID("player")) local regionID = gameAccountInfo and gameAccountInfo.regionID or GetCurrentRegion() self.accountRegion = regionID end if regionsAreMetric[self.accountRegion] ~= nil then return regionsAreMetric[self.accountRegion] else -- Default to true return true end end local floor = math.floor local function yardsToMeters(yards) local meters = yards * 0.9144 return meters end local function humanizeYardsMiles(yards) local yardsInMile = 1760 -- yards in a mile local miles = floor(yards / yardsInMile) local remaining = floor(yards % yardsInMile) return miles, remaining end local function humanizeYardsKilometers(yards) local meters = yards * 0.9144 local km = floor(meters / 1000) local remaining = floor(meters % 1000) return km, remaining end -- Return the formatted distance, taking the player options -- into account and converting to Metric in appropriate circumstances -- Thanks to Lotimar for the contribution function TomTom:GetFormattedDistance(distanceInYards) local distanceMode = "humanmeters" if self.db.profile.arrow.distanceUnits == "auto" then if not self:RegionIsMetric() then distanceMode = "yards" end elseif self.db.profile.arrow.distanceUnits == "yards" then distanceMode = "yards" elseif self.db.profile.arrow.distanceUnits == "meters" then distanceMode = "meters" elseif self.db.profile.arrow.distanceUnits == "humanyards" then distanceMode = "humanyards" elseif self.db.profile.arrow.distanceUnits == "humanmeters" then distanceMode = "humanmeters" end if distanceMode == "humanmeters" then local km, meters = humanizeYardsKilometers(distanceInYards) if km == 0 then return L["%dm away"]:format(meters) else return L["%dkm %dm away"]:format(km, meters) end elseif distanceMode == "humanyards" then local miles, yards = humanizeYardsMiles(distanceInYards) if miles == 0 then return L["%d yards away"]:format(yards) elseif miles == 1 then return L["%d mile %d yards away"]:format(miles, yards) else return L["%d miles %d yards away"]:format(miles, yards) end elseif distanceMode == "meters" then local meters = yardsToMeters(distanceInYards) return L["%dm away"]:format(meters) elseif distanceMode == "yards" then return L["%d yards away"]:format(distanceInYards) end end