-------------------------------------------------------------------------------- -- Nanami-UI: Flight Map (FlightMap.lua) -- Skins TaxiFrame with Nanami-UI theme, destination list, in-flight timer -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.FlightMap = {} local FM = SFrames.FlightMap SFramesDB = SFramesDB or {} SFramesGlobalDB = SFramesGlobalDB or {} -------------------------------------------------------------------------------- -- Theme (Pink Cat-Paw) -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ currentText = { 0.40, 1.0, 0.40 }, moneyGold = { 1, 0.84, 0.0 }, moneySilver = { 0.78, 0.78, 0.78 }, moneyCopper = { 0.71, 0.43, 0.18 }, arrivedText = { 0.40, 1.0, 0.40 }, }) -------------------------------------------------------------------------------- -- Layout -------------------------------------------------------------------------------- local DEST_PANEL_W = 220 local DEST_ROW_H = 22 local HEADER_H = 34 local SIDE_PAD = 10 local MAX_DEST_ROWS = 30 local BAR_W = 160 local TRACK_H = 170 local TRACK_X = 20 -------------------------------------------------------------------------------- -- State -------------------------------------------------------------------------------- local skinApplied = false local DestPanel = nil local DestRows = {} local FlightBar = nil local flightState = { pendingFlight = false, inFlight = false, source = "", dest = "", startTime = 0, estimated = 0, lingerTime = 0, } FM.currentSource = "" -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function FormatMoney(copper) if not copper or copper <= 0 then return 0, 0, 0 end local g = math.floor(copper / 10000) local s = math.floor(math.mod(copper, 10000) / 100) local c = math.mod(copper, 100) return g, s, c end local function FormatTime(seconds) if not seconds or seconds < 0 then seconds = 0 end local m = math.floor(seconds / 60) local s = math.floor(math.mod(seconds, 60)) return string.format("%d:%02d", m, s) end local function StripTextures(frame) if not frame or not frame.GetRegions then return end local regions = { frame:GetRegions() } for i = 1, table.getn(regions) do local region = regions[i] if region and region.SetTexture and not region._nanamiKeep then local drawLayer = region.GetDrawLayer and region:GetDrawLayer() if drawLayer == "BACKGROUND" or drawLayer == "BORDER" or drawLayer == "ARTWORK" then region:SetTexture(nil) region:SetAlpha(0) region:Hide() end end end end local function CreateShadow(parent) local s = CreateFrame("Frame", nil, parent) s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) s:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) s:SetBackdropColor(0, 0, 0, 0.45) s:SetBackdropBorderColor(0, 0, 0, 0.6) s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) return s end -------------------------------------------------------------------------------- -- Flight Time Database (self-learning, stored in SFramesGlobalDB) -------------------------------------------------------------------------------- local function GetFlightDB() if not SFramesGlobalDB then SFramesGlobalDB = {} end if not SFramesGlobalDB.flightTimes then SFramesGlobalDB.flightTimes = {} end return SFramesGlobalDB.flightTimes end local function GetRouteKey(src, dst) return (src or "") .. "->" .. (dst or "") end local function GetTaxiNodeHash(nodeIndex) local x, y = TaxiNodePosition(nodeIndex) if not x then return nil end return tostring(math.floor(x * 100000000)) end local function GetPlayerFaction() local faction = UnitFactionGroup("player") if faction == "Alliance" then return "Alliance" end return "Horde" end -- Hash-based index built when taxi map opens: hash -> nodeIndex local hashToIndex = {} local indexToHash = {} local hashCorrection = {} -- actualHash -> ftcHash (fuzzy match) local function BuildHashIndex() hashToIndex = {} indexToHash = {} hashCorrection = {} local numNodes = NumTaxiNodes() local faction = GetPlayerFaction() local factionData = FTCData and FTCData[faction] -- Collect all hashes used in FTCData for fuzzy matching local ftcHashes = {} if factionData then for srcH, routes in pairs(factionData) do ftcHashes[srcH] = true for dstH in pairs(routes) do ftcHashes[dstH] = true end end end for i = 1, numNodes do local h = GetTaxiNodeHash(i) if h then hashToIndex[h] = i indexToHash[i] = h if ftcHashes[h] then hashCorrection[h] = h else -- Turtle WoW coords may differ slightly from Classic; try ±10 local hNum = tonumber(h) if hNum then for delta = -10, 10 do local candidate = tostring(hNum + delta) if ftcHashes[candidate] then hashCorrection[h] = candidate break end end end end end end end local function LookupFTCData(srcHash, dstHash) if not srcHash or not dstHash then return nil end local faction = GetPlayerFaction() -- Priority 1: self-learned hash DB (exact hash, no correction needed) local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash if hdb and hdb[faction] then local lr = hdb[faction][srcHash] if lr and lr[dstHash] then return lr[dstHash] end end -- Priority 2: FTCData pre-recorded (with fuzzy hash correction) if not FTCData then return nil end local factionData = FTCData[faction] if not factionData then return nil end local corrSrc = hashCorrection[srcHash] or srcHash local corrDst = hashCorrection[dstHash] or dstHash local srcRoutes = factionData[corrSrc] if not srcRoutes then return nil end return srcRoutes[corrDst] end local function GetEstimatedTime(src, dst) local routeKey = GetRouteKey(src, dst) -- Priority 1: per-account learned database (SavedVariables) local db = GetFlightDB() local learned = db[routeKey] if learned then return learned end -- Priority 2: shared learned database (FlightData.lua, cross-account) if NanamiLearnedFlights and NanamiLearnedFlights[routeKey] then return NanamiLearnedFlights[routeKey] end -- Priority 3: FTCData pre-recorded database (by hash) if FM.currentSourceHash and dst then local numNodes = NumTaxiNodes() for i = 1, numNodes do if TaxiNodeName(i) == dst then local dstHash = indexToHash[i] or GetTaxiNodeHash(i) if dstHash then local ftcTime = LookupFTCData(FM.currentSourceHash, dstHash) if ftcTime then return ftcTime end end break end end end return nil end local function GetEstimatedTimeByHash(srcHash, dstHash) -- Check learned DB by resolving hash to names local srcIdx = hashToIndex[srcHash] local dstIdx = hashToIndex[dstHash] if srcIdx and dstIdx then local db = GetFlightDB() local learned = db[GetRouteKey(TaxiNodeName(srcIdx), TaxiNodeName(dstIdx))] if learned then return learned end end return LookupFTCData(srcHash, dstHash) end local function GetFlightHashDB() if not SFramesGlobalDB then SFramesGlobalDB = {} end if not SFramesGlobalDB.flightTimesHash then SFramesGlobalDB.flightTimesHash = {} end return SFramesGlobalDB.flightTimesHash end local function SaveFlightTime(src, dst, duration, srcHash, dstHash) if not src or src == "" or not dst or dst == "" then return end if duration < 5 then return end local secs = math.floor(duration + 0.5) local db = GetFlightDB() db[GetRouteKey(src, dst)] = secs -- Also save in hash format for cross-account export if srcHash and dstHash then local faction = GetPlayerFaction() local hdb = GetFlightHashDB() if not hdb[faction] then hdb[faction] = {} end if not hdb[faction][srcHash] then hdb[faction][srcHash] = {} end hdb[faction][srcHash][dstHash] = secs end end -------------------------------------------------------------------------------- -- Money Layout (icon-based, like Merchant.lua) -------------------------------------------------------------------------------- local function LayoutRowMoney(row, copper) row.gTxt:Hide(); row.gTex:Hide() row.sTxt:Hide(); row.sTex:Hide() row.cTxt:Hide(); row.cTex:Hide() if not copper or copper <= 0 then row.gTxt:SetText("--") row.gTxt:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.gTxt:ClearAllPoints() row.gTxt:SetPoint("RIGHT", row, "RIGHT", -6, 0) row.gTxt:Show() return end local vG, vS, vC = FormatMoney(copper) local anchor = nil local function AttachPair(txt, tex, val, cr, cg, cb) txt:SetText(val) txt:SetTextColor(cr, cg, cb) txt:ClearAllPoints() if not anchor then txt:SetPoint("RIGHT", row, "RIGHT", -6, 0) else txt:SetPoint("RIGHT", anchor, "LEFT", -3, 0) end txt:Show() tex:ClearAllPoints() tex:SetPoint("RIGHT", txt, "LEFT", -1, 0) tex:Show() anchor = tex end if vC > 0 then AttachPair(row.cTxt, row.cTex, vC, T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) end if vS > 0 then AttachPair(row.sTxt, row.sTex, vS, T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) end if vG > 0 then AttachPair(row.gTxt, row.gTex, vG, T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) end end -------------------------------------------------------------------------------- -- Destination Row Factory -------------------------------------------------------------------------------- local function CreateDestRow(parent, index) local row = CreateFrame("Button", "NanamiFlightDest" .. index, parent) row:SetHeight(DEST_ROW_H) local font = GetFont() local dot = row:CreateTexture(nil, "ARTWORK") dot:SetTexture("Interface\\Buttons\\WHITE8X8") dot:SetWidth(6) dot:SetHeight(6) dot:SetPoint("LEFT", row, "LEFT", 6, 0) row.dot = dot local nameFS = row:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 10, "OUTLINE") nameFS:SetPoint("LEFT", dot, "RIGHT", 5, 0) nameFS:SetPoint("RIGHT", row, "RIGHT", -112, 0) nameFS:SetJustifyH("LEFT") row.nameFS = nameFS local timeFS = row:CreateFontString(nil, "OVERLAY") timeFS:SetFont(font, 9, "OUTLINE") timeFS:SetPoint("RIGHT", row, "RIGHT", -78, 0) timeFS:SetJustifyH("RIGHT") timeFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.timeFS = timeFS row.gTxt = row:CreateFontString(nil, "OVERLAY") row.gTxt:SetFont(font, 9, "OUTLINE") row.gTex = row:CreateTexture(nil, "ARTWORK") row.gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") row.gTex:SetTexCoord(0, 0.25, 0, 1) row.gTex:SetWidth(10); row.gTex:SetHeight(10) row.sTxt = row:CreateFontString(nil, "OVERLAY") row.sTxt:SetFont(font, 9, "OUTLINE") row.sTex = row:CreateTexture(nil, "ARTWORK") row.sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") row.sTex:SetTexCoord(0.25, 0.5, 0, 1) row.sTex:SetWidth(10); row.sTex:SetHeight(10) row.cTxt = row:CreateFontString(nil, "OVERLAY") row.cTxt:SetFont(font, 9, "OUTLINE") row.cTex = row:CreateTexture(nil, "ARTWORK") row.cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") row.cTex:SetTexCoord(0.5, 0.75, 0, 1) row.cTex:SetWidth(10); row.cTex:SetHeight(10) local hl = row:CreateTexture(nil, "HIGHLIGHT") hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") hl:SetBlendMode("ADD") hl:SetAllPoints(row) hl:SetAlpha(0.15) row.nodeIndex = nil row.nodeType = nil row:SetScript("OnEnter", function() if this.nodeType == "REACHABLE" and this.nodeIndex then GameTooltip:SetOwner(this, "ANCHOR_LEFT") GameTooltip:AddLine(TaxiNodeName(this.nodeIndex), 1, 1, 1) local cost = TaxiNodeCost(this.nodeIndex) if cost and cost > 0 then SetTooltipMoney(GameTooltip, cost) end local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(this.nodeIndex)) if est then GameTooltip:AddLine(" ") GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0) end GameTooltip:AddLine(" ") GameTooltip:AddLine("点击飞往此处", T.dimText[1], T.dimText[2], T.dimText[3]) GameTooltip:Show() end end) row:SetScript("OnLeave", function() GameTooltip:Hide() end) row:SetScript("OnClick", function() if this.nodeType == "REACHABLE" and this.nodeIndex then TakeTaxiNode(this.nodeIndex) end end) return row end -------------------------------------------------------------------------------- -- Skin TaxiFrame -------------------------------------------------------------------------------- function FM:ApplySkin() if skinApplied then return end if not TaxiFrame then return end skinApplied = true local font = GetFont() StripTextures(TaxiFrame) if TaxiPortrait then TaxiPortrait:Hide(); TaxiPortrait:SetAlpha(0) end if TaxiTitleText then TaxiTitleText:Hide() end local origClose = TaxiCloseButton if origClose then origClose:Hide() origClose:SetAlpha(0) origClose.Show = function() end end TaxiFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) TaxiFrame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) TaxiFrame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) CreateShadow(TaxiFrame) -- Trim empty space at the bottom (TaxiMap anchored to top, safe to shrink from bottom) if TaxiMap then local mapBottom = TaxiMap:GetBottom() local frameBottom = TaxiFrame:GetBottom() if mapBottom and frameBottom and mapBottom > frameBottom then local excess = mapBottom - frameBottom - 10 if excess > 10 then TaxiFrame:SetHeight(TaxiFrame:GetHeight() - excess) end end end TaxiFrame:SetMovable(true) TaxiFrame:EnableMouse(true) TaxiFrame:RegisterForDrag("LeftButton") TaxiFrame:SetScript("OnDragStart", function() this:StartMoving() end) TaxiFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) local header = CreateFrame("Frame", nil, TaxiFrame) header:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", 0, 0) header:SetHeight(HEADER_H) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) header:SetFrameLevel(TaxiFrame:GetFrameLevel() + 5) local titleIco = SFrames:CreateIcon(header, "mount", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) local titleFS = header:CreateFontString(nil, "OVERLAY") titleFS:SetFont(font, 14, "OUTLINE") titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) titleFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) titleFS:SetJustifyH("LEFT") titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) FM.titleFS = titleFS local closeBtn = CreateFrame("Button", nil, header) closeBtn:SetWidth(20) closeBtn:SetHeight(20) closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) closeBtn:SetFrameLevel(header:GetFrameLevel() + 1) local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) closeTex:SetAllPoints() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) closeBtn:SetScript("OnClick", function() CloseTaxiMap() end) closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) local headerSep = TaxiFrame:CreateTexture(nil, "OVERLAY") headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") headerSep:SetHeight(1) headerSep:SetPoint("TOPLEFT", TaxiFrame, "TOPLEFT", 4, -HEADER_H) headerSep:SetPoint("TOPRIGHT", TaxiFrame, "TOPRIGHT", -4, -HEADER_H) headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) headerSep._nanamiKeep = true if TaxiMap then local mapBorder = CreateFrame("Frame", nil, TaxiFrame) mapBorder:SetPoint("TOPLEFT", TaxiMap, "TOPLEFT", -3, 3) mapBorder:SetPoint("BOTTOMRIGHT", TaxiMap, "BOTTOMRIGHT", 3, -3) mapBorder:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) mapBorder:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) mapBorder:SetFrameLevel(TaxiFrame:GetFrameLevel() + 3) end FM:CreateDestPanel() local origOnHide = TaxiFrame:GetScript("OnHide") TaxiFrame:SetScript("OnHide", function() if origOnHide then origOnHide() end if DestPanel then DestPanel:Hide() end end) end -------------------------------------------------------------------------------- -- Destination Panel -------------------------------------------------------------------------------- function FM:CreateDestPanel() local font = GetFont() DestPanel = CreateFrame("Frame", "NanamiFlightDestPanel", UIParent) DestPanel:SetWidth(DEST_PANEL_W) DestPanel:SetPoint("TOPLEFT", TaxiFrame, "TOPRIGHT", 4, 0) DestPanel:SetPoint("BOTTOMLEFT", TaxiFrame, "BOTTOMRIGHT", 4, 0) DestPanel:SetFrameStrata(TaxiFrame:GetFrameStrata()) DestPanel:SetFrameLevel(TaxiFrame:GetFrameLevel() + 1) DestPanel:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) DestPanel:SetBackdropColor(T.listBg[1], T.listBg[2], T.listBg[3], T.listBg[4]) DestPanel:SetBackdropBorderColor(T.listBorder[1], T.listBorder[2], T.listBorder[3], T.listBorder[4]) CreateShadow(DestPanel) local panelTitle = DestPanel:CreateFontString(nil, "OVERLAY") panelTitle:SetFont(font, 12, "OUTLINE") panelTitle:SetPoint("TOP", DestPanel, "TOP", 0, -8) panelTitle:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) panelTitle:SetText("目的地列表") FM.currentLabel = DestPanel:CreateFontString(nil, "OVERLAY") FM.currentLabel:SetFont(font, 10, "OUTLINE") FM.currentLabel:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -28) FM.currentLabel:SetPoint("RIGHT", DestPanel, "RIGHT", -8, 0) FM.currentLabel:SetJustifyH("LEFT") FM.currentLabel:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3]) local sepLine = DestPanel:CreateTexture(nil, "OVERLAY") sepLine:SetTexture("Interface\\Buttons\\WHITE8X8") sepLine:SetHeight(1) sepLine:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 8, -46) sepLine:SetPoint("TOPRIGHT", DestPanel, "TOPRIGHT", -8, -46) sepLine:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) FM.noDestLabel = DestPanel:CreateFontString(nil, "OVERLAY") FM.noDestLabel:SetFont(font, 10, "OUTLINE") FM.noDestLabel:SetPoint("TOP", DestPanel, "TOP", 0, -60) FM.noDestLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) FM.noDestLabel:SetText("暂无可用航线") FM.noDestLabel:Hide() local scrollFrame = CreateFrame("ScrollFrame", "NanamiFlightDestScroll", DestPanel, "UIPanelScrollFrameTemplate") scrollFrame:SetPoint("TOPLEFT", DestPanel, "TOPLEFT", 2, -50) scrollFrame:SetPoint("BOTTOMRIGHT", DestPanel, "BOTTOMRIGHT", -20, 6) local scrollChild = CreateFrame("Frame", "NanamiFlightDestScrollChild", scrollFrame) scrollChild:SetWidth(DEST_PANEL_W - 24) scrollChild:SetHeight(MAX_DEST_ROWS * DEST_ROW_H + 20) scrollFrame:SetScrollChild(scrollChild) local scrollBar = getglobal("NanamiFlightDestScrollScrollBar") if scrollBar then scrollBar:SetWidth(12) local regions = { scrollBar:GetRegions() } for i = 1, table.getn(regions) do local region = regions[i] if region and region.GetObjectType and region:GetObjectType() == "Texture" then region:SetTexture(nil) region:SetAlpha(0) end end local thumb = scrollBar:GetThumbTexture() if thumb then thumb:SetTexture("Interface\\Buttons\\WHITE8X8") thumb:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.6) thumb:SetWidth(10) thumb:SetHeight(40) end end local scrollUp = getglobal("NanamiFlightDestScrollScrollBarScrollUpButton") if scrollUp then scrollUp:SetAlpha(0); scrollUp:SetWidth(1); scrollUp:SetHeight(1) end local scrollDown = getglobal("NanamiFlightDestScrollScrollBarScrollDownButton") if scrollDown then scrollDown:SetAlpha(0); scrollDown:SetWidth(1); scrollDown:SetHeight(1) end for i = 1, MAX_DEST_ROWS do local row = CreateDestRow(scrollChild, i) row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -((i - 1) * DEST_ROW_H)) row:SetPoint("RIGHT", scrollChild, "RIGHT", 0, 0) row:Hide() DestRows[i] = row end DestPanel:Hide() end -------------------------------------------------------------------------------- -- Update Destinations -------------------------------------------------------------------------------- function FM:UpdateDestinations() if not DestPanel then return end local numNodes = NumTaxiNodes() local currentName = "" local reachable = {} local unreachableCount = 0 for i = 1, numNodes do local name = TaxiNodeName(i) local ntype = TaxiNodeGetType(i) local cost = TaxiNodeCost(i) if ntype == "CURRENT" then currentName = name elseif ntype == "REACHABLE" then table.insert(reachable, { index = i, name = name, cost = cost }) elseif ntype == "NONE" then unreachableCount = unreachableCount + 1 end end FM.currentSource = currentName -- Build hash index for FTCData lookup BuildHashIndex() FM.currentSourceHash = nil for i = 1, numNodes do if TaxiNodeGetType(i) == "CURRENT" then FM.currentSourceHash = indexToHash[i] or GetTaxiNodeHash(i) break end end table.sort(reachable, function(a, b) return a.cost < b.cost end) if table.getn(reachable) == 0 then CloseTaxiMap() return end local npcName = UnitName("NPC") or "飞行管理员" FM.titleFS:SetText(npcName .. " - 飞行路线") if currentName ~= "" then FM.currentLabel:SetText("|cFF66E666*|r " .. currentName) else FM.currentLabel:SetText("|cFF66E666*|r 未知") end local rowIdx = 0 for _, dest in ipairs(reachable) do rowIdx = rowIdx + 1 if rowIdx <= MAX_DEST_ROWS then local row = DestRows[rowIdx] row.nodeIndex = dest.index row.nodeType = "REACHABLE" row.dot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3]) row.nameFS:SetText(dest.name) row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) LayoutRowMoney(row, dest.cost) local est = GetEstimatedTime(currentName, dest.name) if not est and FM.currentSourceHash then local dstHash = indexToHash[dest.index] or GetTaxiNodeHash(dest.index) if dstHash then est = LookupFTCData(FM.currentSourceHash, dstHash) end end if est then row.timeFS:SetText(FormatTime(est)) row.timeFS:Show() else row.timeFS:SetText("") row.timeFS:Hide() end row:Show() end end if unreachableCount > 0 then rowIdx = rowIdx + 1 if rowIdx <= MAX_DEST_ROWS then local row = DestRows[rowIdx] row.nodeIndex = nil row.nodeType = "NONE" row.dot:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.nameFS:SetText("(" .. unreachableCount .. " 个未发现)") row.nameFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.gTxt:Hide(); row.gTex:Hide() row.sTxt:Hide(); row.sTex:Hide() row.cTxt:Hide(); row.cTex:Hide() row.timeFS:SetText(""); row.timeFS:Hide() row:Show() end end for i = rowIdx + 1, MAX_DEST_ROWS do DestRows[i]:Hide() end if rowIdx == 0 then FM.noDestLabel:Show() else FM.noDestLabel:Hide() end local child = getglobal("NanamiFlightDestScrollChild") if child then child:SetHeight(math.max(rowIdx * DEST_ROW_H + 10, 50)) end DestPanel:Show() end -------------------------------------------------------------------------------- -- Hook Node Buttons (tooltip only) -------------------------------------------------------------------------------- function FM:HookNodeButtons() local numNodes = NumTaxiNodes() for i = 1, numNodes do local btn = getglobal("TaxiButton" .. i) if btn and not btn._nanamiHooked then btn._nanamiHooked = true local origEnter = btn:GetScript("OnEnter") btn:SetScript("OnEnter", function() if origEnter then origEnter() end local id = this:GetID() if TaxiNodeGetType(id) == "REACHABLE" then local cost = TaxiNodeCost(id) if cost and cost > 0 then GameTooltip:AddLine(" ") SetTooltipMoney(GameTooltip, cost) end local est = GetEstimatedTime(FM.currentSource, TaxiNodeName(id)) if est then GameTooltip:AddLine("预计飞行: " .. FormatTime(est), 0.6, 0.8, 1.0) end GameTooltip:Show() end for _, row in ipairs(DestRows) do if row:IsShown() and row.nodeIndex == id then row.nameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) end end end) local origLeave = btn:GetScript("OnLeave") btn:SetScript("OnLeave", function() if origLeave then origLeave() end for _, row in ipairs(DestRows) do if row:IsShown() and row.nodeType == "REACHABLE" then row.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) end end end) end end end -------------------------------------------------------------------------------- -- Flight Progress Bar (vertical axis: departure → destination with countdown) -------------------------------------------------------------------------------- local COLLAPSED_H = 34 local barCollapsed = false local function SetBarCollapsed(collapsed) barCollapsed = collapsed if not FlightBar then return end local bar = FlightBar if collapsed then bar:SetHeight(COLLAPSED_H) for _, el in ipairs(bar.expandElements) do el:Hide() end bar.collapseBtn.label:SetText("+") -- Vertically center all visible elements bar.titleFS:ClearAllPoints() bar.titleFS:SetPoint("LEFT", bar, "LEFT", 10, 0) bar.compactFS:ClearAllPoints() bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, 0) bar.collapseBtn:ClearAllPoints() bar.collapseBtn:SetPoint("RIGHT", bar, "RIGHT", -6, 0) bar.compactFS:Show() else bar:SetHeight(bar.expandedH) for _, el in ipairs(bar.expandElements) do el:Show() end bar.collapseBtn.label:SetText("-") -- Restore top-aligned positions bar.titleFS:ClearAllPoints() bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, -8) bar.compactFS:ClearAllPoints() bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, -9) bar.collapseBtn:ClearAllPoints() bar.collapseBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6) bar.compactFS:Hide() end end local function CreateFlightBar() if FlightBar then return end local font = GetFont() local panelH = 8 + 18 + 6 + 12 + 6 + TRACK_H + 6 + 12 + 10 + 14 + 4 + 14 + 4 + 14 + 10 local bar = CreateFrame("Frame", "NanamiFlightBar", UIParent) bar:SetWidth(BAR_W) bar:SetHeight(panelH) bar.expandedH = panelH bar:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -80, -300) bar:SetFrameStrata("HIGH") bar:SetMovable(true) bar:EnableMouse(true) bar:RegisterForDrag("LeftButton") bar:SetScript("OnDragStart", function() this:StartMoving() end) bar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) bar:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) bar:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) bar:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) CreateShadow(bar) bar.expandElements = {} local yOff = -8 -- Title (always visible) bar.titleFS = bar:CreateFontString(nil, "OVERLAY") bar.titleFS:SetFont(font, 13, "OUTLINE") bar.titleFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 10, yOff) bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) bar.titleFS:SetText("飞行中...") -- Compact remaining time (shown only when collapsed, next to title) bar.compactFS = bar:CreateFontString(nil, "OVERLAY") bar.compactFS:SetFont(font, 13, "OUTLINE") bar.compactFS:SetPoint("RIGHT", bar, "RIGHT", -28, yOff - 1) bar.compactFS:SetJustifyH("RIGHT") bar.compactFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) bar.compactFS:Hide() -- Collapse / expand button local colBtn = CreateFrame("Button", nil, bar) colBtn:SetWidth(18) colBtn:SetHeight(18) colBtn:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -6, -6) colBtn:SetFrameLevel(bar:GetFrameLevel() + 3) local colLabel = colBtn:CreateFontString(nil, "OVERLAY") colLabel:SetFont(font, 14, "OUTLINE") colLabel:SetPoint("CENTER", 0, 1) colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) colLabel:SetText("-") colBtn.label = colLabel colBtn:SetScript("OnClick", function() SetBarCollapsed(not barCollapsed) end) colBtn:SetScript("OnEnter", function() colLabel:SetTextColor(1, 0.7, 0.85) end) colBtn:SetScript("OnLeave", function() colLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) bar.collapseBtn = colBtn yOff = yOff - 18 -- Separator local sep1 = bar:CreateTexture(nil, "OVERLAY") sep1:SetTexture("Interface\\Buttons\\WHITE8X8") sep1:SetHeight(1) sep1:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff - 2) sep1:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff - 2) sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) table.insert(bar.expandElements, sep1) yOff = yOff - 6 -- Source dot + name local srcDot = bar:CreateTexture(nil, "OVERLAY") srcDot:SetTexture("Interface\\Buttons\\WHITE8X8") srcDot:SetWidth(9) srcDot:SetHeight(9) srcDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff) srcDot:SetVertexColor(T.currentText[1], T.currentText[2], T.currentText[3]) bar.srcDot = srcDot table.insert(bar.expandElements, srcDot) bar.srcNameFS = bar:CreateFontString(nil, "OVERLAY") bar.srcNameFS:SetFont(font, 10, "OUTLINE") bar.srcNameFS:SetPoint("LEFT", srcDot, "RIGHT", 6, 0) bar.srcNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0) bar.srcNameFS:SetJustifyH("LEFT") bar.srcNameFS:SetTextColor(T.currentText[1], T.currentText[2], T.currentText[3]) table.insert(bar.expandElements, bar.srcNameFS) yOff = yOff - 14 -- Vertical track area local trackTop = yOff local trackBg = bar:CreateTexture(nil, "BACKGROUND") trackBg:SetTexture("Interface\\Buttons\\WHITE8X8") trackBg:SetWidth(3) trackBg:SetHeight(TRACK_H) trackBg:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X + 3, trackTop) trackBg:SetVertexColor(T.sectionBg[1], T.sectionBg[2], T.sectionBg[3], T.sectionBg[4]) bar.trackBg = trackBg table.insert(bar.expandElements, trackBg) -- Track fill (grows from top downward) local trackFillFrame = CreateFrame("Frame", nil, bar) trackFillFrame:SetPoint("TOPLEFT", trackBg, "TOPLEFT", 0, 0) trackFillFrame:SetWidth(3) trackFillFrame:SetHeight(1) local trackFill = trackFillFrame:CreateTexture(nil, "ARTWORK") trackFill:SetTexture("Interface\\Buttons\\WHITE8X8") trackFill:SetAllPoints(trackFillFrame) trackFill:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], T.progressFill[4]) bar.trackFillFrame = trackFillFrame bar.trackFill = trackFill table.insert(bar.expandElements, trackFillFrame) -- Progress indicator local progDot = bar:CreateTexture(nil, "OVERLAY") progDot:SetTexture("Interface\\Buttons\\WHITE8X8") progDot:SetWidth(13) progDot:SetHeight(5) progDot:SetPoint("CENTER", trackBg, "TOP", 0, 0) progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4]) bar.progDot = progDot table.insert(bar.expandElements, progDot) -- Progress glow local progGlow = bar:CreateTexture(nil, "ARTWORK") progGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") progGlow:SetBlendMode("ADD") progGlow:SetWidth(28) progGlow:SetHeight(28) progGlow:SetPoint("CENTER", progDot, "CENTER", 0, 0) progGlow:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.4) bar.progGlow = progGlow table.insert(bar.expandElements, progGlow) -- Elapsed time bar.elapsedFS = bar:CreateFontString(nil, "OVERLAY") bar.elapsedFS:SetFont(font, 11, "OUTLINE") bar.elapsedFS:SetPoint("LEFT", trackBg, "RIGHT", 12, 0) bar.elapsedFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) table.insert(bar.expandElements, bar.elapsedFS) -- Remaining time (follows progress dot) bar.remainFS = bar:CreateFontString(nil, "OVERLAY") bar.remainFS:SetFont(font, 14, "OUTLINE") bar.remainFS:SetPoint("LEFT", progDot, "RIGHT", 10, 0) bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) table.insert(bar.expandElements, bar.remainFS) yOff = trackTop - TRACK_H -- Destination dot + name local dstDot = bar:CreateTexture(nil, "OVERLAY") dstDot:SetTexture("Interface\\Buttons\\WHITE8X8") dstDot:SetWidth(9) dstDot:SetHeight(9) dstDot:SetPoint("TOPLEFT", bar, "TOPLEFT", TRACK_X, yOff - 4) dstDot:SetVertexColor(T.nameText[1], T.nameText[2], T.nameText[3]) bar.dstDot = dstDot table.insert(bar.expandElements, dstDot) bar.dstNameFS = bar:CreateFontString(nil, "OVERLAY") bar.dstNameFS:SetFont(font, 10, "OUTLINE") bar.dstNameFS:SetPoint("LEFT", dstDot, "RIGHT", 6, 0) bar.dstNameFS:SetPoint("RIGHT", bar, "RIGHT", -8, 0) bar.dstNameFS:SetJustifyH("LEFT") bar.dstNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) table.insert(bar.expandElements, bar.dstNameFS) yOff = yOff - 18 -- Bottom info local sep2 = bar:CreateTexture(nil, "OVERLAY") sep2:SetTexture("Interface\\Buttons\\WHITE8X8") sep2:SetHeight(1) sep2:SetPoint("TOPLEFT", bar, "TOPLEFT", 8, yOff) sep2:SetPoint("TOPRIGHT", bar, "TOPRIGHT", -8, yOff) sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) table.insert(bar.expandElements, sep2) yOff = yOff - 6 bar.totalFS = bar:CreateFontString(nil, "OVERLAY") bar.totalFS:SetFont(font, 10, "OUTLINE") bar.totalFS:SetPoint("TOPLEFT", bar, "TOPLEFT", 12, yOff) bar.totalFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) table.insert(bar.expandElements, bar.totalFS) bar:Hide() FlightBar = bar end local function ShowFlightBar() CreateFlightBar() local bar = FlightBar bar.srcNameFS:SetText(flightState.source) bar.dstNameFS:SetText(flightState.dest) if flightState.estimated > 0 then bar.totalFS:SetText("预计: " .. FormatTime(flightState.estimated)) else bar.totalFS:SetText("首次飞行 - 记录中...") end bar.titleFS:SetText("飞行中...") bar.titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) bar:SetAlpha(1) bar.compactFS:SetText("") -- Reset track fill bar.trackFillFrame:SetHeight(1) bar.progDot:ClearAllPoints() bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, 0) bar.progGlow:ClearAllPoints() bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) bar.elapsedFS:SetText("0:00") bar.remainFS:SetText("") -- Restore collapse state SetBarCollapsed(barCollapsed) bar:Show() end local function UpdateFlightBar() if not FlightBar or not FlightBar:IsShown() then return end local bar = FlightBar local elapsed = GetTime() - flightState.startTime local estimated = flightState.estimated local progress = 0 local compactText = FormatTime(elapsed) if estimated > 0 then progress = math.min(1, elapsed / estimated) local remain = math.max(0, estimated - elapsed) bar.remainFS:SetText(FormatTime(remain)) compactText = FormatTime(remain) if remain <= 0 then bar.remainFS:SetText("即将到达") bar.remainFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) compactText = "即将到达" else bar.remainFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) end else bar.remainFS:SetText("记录中...") bar.remainFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) progress = math.mod(elapsed / 10, 1) * 0.8 end bar.compactFS:SetText(compactText) bar.elapsedFS:SetText("已飞行 " .. FormatTime(elapsed)) if not barCollapsed then local fillH = math.max(1, progress * TRACK_H) bar.trackFillFrame:SetHeight(fillH) bar.progDot:ClearAllPoints() bar.progDot:SetPoint("CENTER", bar.trackBg, "TOP", 0, -fillH) bar.progGlow:ClearAllPoints() bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) end end local function OnFlightArrived() if not FlightBar then return end local bar = FlightBar local elapsed = GetTime() - flightState.startTime SaveFlightTime(flightState.source, flightState.dest, elapsed, flightState.srcHash, flightState.dstHash) bar.titleFS:SetText("已到达!") bar.titleFS:SetTextColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) bar.remainFS:SetText("") bar.elapsedFS:SetText("飞行用时 " .. FormatTime(elapsed)) bar.totalFS:SetText("") -- Fill track to 100% bar.trackFillFrame:SetHeight(TRACK_H) bar.progDot:ClearAllPoints() bar.progDot:SetPoint("CENTER", bar.trackBg, "BOTTOM", 0, 0) bar.progGlow:ClearAllPoints() bar.progGlow:SetPoint("CENTER", bar.progDot, "CENTER", 0, 0) bar.progDot:SetVertexColor(T.arrivedText[1], T.arrivedText[2], T.arrivedText[3]) flightState.lingerTime = 4 end local function HideFlightBar() if FlightBar then FlightBar:Hide() FlightBar.progDot:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4]) end end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function FM:Initialize() -- TaxiMap skin updater local updater = CreateFrame("Frame") updater:RegisterEvent("TAXIMAP_OPENED") updater.needsUpdate = false updater:SetScript("OnEvent", function() if event == "TAXIMAP_OPENED" then this.needsUpdate = true end end) updater:SetScript("OnUpdate", function() if this.needsUpdate then this.needsUpdate = false FM:ApplySkin() FM:UpdateDestinations() FM:HookNodeButtons() end end) -- Hook TakeTaxiNode to capture route info before flight starts local origTakeTaxiNode = TakeTaxiNode TakeTaxiNode = function(index) local numNodes = NumTaxiNodes() local srcName = "" local srcHash = nil for i = 1, numNodes do if TaxiNodeGetType(i) == "CURRENT" then srcName = TaxiNodeName(i) srcHash = indexToHash[i] or GetTaxiNodeHash(i) break end end local dstName = TaxiNodeName(index) or "" local dstHash = indexToHash[index] or GetTaxiNodeHash(index) flightState.source = srcName flightState.dest = dstName flightState.srcHash = srcHash flightState.dstHash = dstHash flightState.pendingFlight = true flightState.inFlight = false flightState.lingerTime = 0 -- Lookup estimated time: learned DB first, then FTCData local est = GetEstimatedTime(srcName, dstName) if not est and srcHash and dstHash then est = LookupFTCData(srcHash, dstHash) end flightState.estimated = est or 0 origTakeTaxiNode(index) end -- Flight state monitor local monitor = CreateFrame("Frame") monitor:SetScript("OnUpdate", function() local dt = arg1 or 0 local onTaxi = UnitOnTaxi("player") if flightState.pendingFlight and onTaxi then flightState.pendingFlight = false flightState.inFlight = true flightState.startTime = GetTime() ShowFlightBar() end if flightState.inFlight then if not onTaxi then flightState.inFlight = false OnFlightArrived() else UpdateFlightBar() end end -- Linger after arrival then hide if flightState.lingerTime > 0 then flightState.lingerTime = flightState.lingerTime - dt if flightState.lingerTime <= 1 and FlightBar then FlightBar:SetAlpha(math.max(0, flightState.lingerTime)) end if flightState.lingerTime <= 0 then flightState.lingerTime = 0 HideFlightBar() end end end) end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame") bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB.enableFlightMap == nil then SFramesDB.enableFlightMap = true end if SFramesDB.enableFlightMap ~= false then FM:Initialize() end end end) -------------------------------------------------------------------------------- -- Debug: /ftcdebug (open taxi map first, then type this command) -------------------------------------------------------------------------------- SLASH_FTCDEBUG1 = "/ftcdebug" SlashCmdList["FTCDEBUG"] = function() local faction = GetPlayerFaction() DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r faction=" .. tostring(faction) .. " FTCData=" .. tostring(FTCData ~= nil)) if FTCData then DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r FTCData[" .. faction .. "]=" .. tostring(FTCData[faction] ~= nil)) end local numNodes = NumTaxiNodes() if not numNodes or numNodes == 0 then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightDebug]|r Taxi map not open!") return end DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightDebug]|r Nodes=" .. numNodes .. " srcHash=" .. tostring(FM.currentSourceHash)) for i = 1, numNodes do local name = TaxiNodeName(i) local ntype = TaxiNodeGetType(i) local x, y = TaxiNodePosition(i) local h = tostring(math.floor(x * 100000000)) local corr = hashCorrection[h] local corrTag = "" if corr and corr ~= h then corrTag = " |cFFFFFF00->fuzzy:" .. corr .. "|r" elseif corr then corrTag = " |cFF00FF00exact|r" else corrTag = " |cFFFF6666NO_MATCH|r" end local tag = "" if ntype == "CURRENT" then tag = " |cFF66E666<< YOU|r" end DEFAULT_CHAT_FRAME:AddMessage(" #" .. i .. " [" .. ntype .. "] " .. name .. " h=" .. h .. corrTag .. tag) end end -------------------------------------------------------------------------------- -- Export: /ftcexport (prints learned flight times in FlightData.lua format) -------------------------------------------------------------------------------- SLASH_FTCEXPORT1 = "/ftcexport" SlashCmdList["FTCEXPORT"] = function() local hdb = SFramesGlobalDB and SFramesGlobalDB.flightTimesHash if not hdb then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet.") return end local count = 0 for faction, sources in pairs(hdb) do DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r -- " .. faction .. " learned routes:") for srcHash, dests in pairs(sources) do for dstHash, secs in pairs(dests) do DEFAULT_CHAT_FRAME:AddMessage(" FTCData." .. faction .. "['" .. srcHash .. "']['" .. dstHash .. "'] = " .. secs) count = count + 1 end end end if count == 0 then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[NanamiFlightExport]|r No learned flight times yet. Fly some routes first!") else DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00[NanamiFlightExport]|r Total: " .. count .. " routes. Copy lines above into FlightData.lua for permanent cross-account sharing.") end end