Files
Nanami-UI/FlightMap.lua
2026-03-16 13:48:46 +08:00

1283 lines
47 KiB
Lua

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