Dateien nach "/" hochladen
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
-- Freewood-OctoWoW-Codex: mitgelieferte Startdaten (Addon Freewood-OctoWoW-ForeverData)
|
||||
--
|
||||
-- ForeverData enthaelt, was Spieler in Forever bereits erfasst haben: Quests mit Geber, Abgabe, Zielen, Belohnungen und Ketten,
|
||||
-- Kreaturen, Objekte, Beute, Wegenetz, Verkehrsverbindungen, Ortspunkte. Die Daten bilden die unterste Schicht:
|
||||
-- eigene Daten > empfangene Daten > ForeverData > Classic-Vorlage (Freewood-OctoWoW-CodexData)
|
||||
-- Sie liegen nur im Speicher (nichts davon wird gespeichert oder weitergesendet) und gehen durch dieselbe Pruefung
|
||||
-- (Share.Sanitize) wie empfangene Datensaetze. Abschalten: /fwforever aus
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS and NS.Codex
|
||||
if not FW or not Codex then
|
||||
return
|
||||
end
|
||||
|
||||
local Forever = { loaded = false, count = 0 }
|
||||
Codex.Forever = Forever
|
||||
NS.Forever = Forever
|
||||
|
||||
local DATA_ADDON = "Freewood-OctoWoW-ForeverData"
|
||||
|
||||
function Forever.Disabled()
|
||||
local db = Codex.db
|
||||
return db and db.settings and db.settings.noForever == true or false
|
||||
end
|
||||
|
||||
-- Zusatztabellen (Item-Qualitaet, Item-Werte, Anzeige-IDs, Namen, Rezepte): nur fehlende Eintraege werden ergaenzt,
|
||||
-- eigene Werte gehen vor. Einmal je Datenstand (db.foreverRev).
|
||||
local function FillTable(dst, src, check)
|
||||
if type(dst) ~= "table" or type(src) ~= "table" then return end
|
||||
for k, v in pairs(src) do
|
||||
if dst[k] == nil and check(k, v) then dst[k] = v end
|
||||
end
|
||||
end
|
||||
|
||||
local function IsId(k) return type(k) == "number" and k >= 1 and k == math.floor(k) and k < 1e9 end
|
||||
|
||||
local EXTRAS = {
|
||||
iq = function(k, v) return IsId(k) and type(v) == "number" and v >= 0 and v <= 8 end,
|
||||
disp = function(k, v) return IsId(k) and type(v) == "number" and v > 0 end,
|
||||
vfac = function(k, v) return IsId(k) and (v == "A" or v == "H") end,
|
||||
wrq = function(k, v) return IsId(k) and type(v) == "table" end,
|
||||
names = function(k, v) return IsId(k) and type(v) == "string" and #v <= 80 end,
|
||||
istats = function(k, v)
|
||||
if not IsId(k) or type(v) ~= "table" then return false end
|
||||
for sk, sv in pairs(v) do
|
||||
if sk == "_fx" then
|
||||
if type(sv) ~= "table" then return false end
|
||||
elseif type(sk) ~= "string" or #sk > 60 or (type(sv) ~= "number" and type(sv) ~= "string" and type(sv) ~= "boolean") then return false end
|
||||
end
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local function MergeExtras(g)
|
||||
local db = Codex.db
|
||||
if not db or not g.extra or db.foreverRev == g.rev then return end
|
||||
for name, check in pairs(EXTRAS) do
|
||||
if type(g.extra[name]) == "table" then
|
||||
db[name] = db[name] or {}
|
||||
FillTable(db[name], g.extra[name], check)
|
||||
end
|
||||
end
|
||||
if type(g.extra.recipes) == "table" then
|
||||
db.recipes = db.recipes or {}
|
||||
for prof, list in pairs(g.extra.recipes) do
|
||||
if type(prof) == "string" and type(list) == "table" then
|
||||
db.recipes[prof] = db.recipes[prof] or {}
|
||||
FillTable(db.recipes[prof], list, function(k, v) return type(k) == "string" and #k <= 80 and type(v) == "table" end)
|
||||
end
|
||||
end
|
||||
end
|
||||
if type(g.extra.qd) == "table" then
|
||||
db.cat = db.cat or {}
|
||||
db.cat.qd = db.cat.qd or {}
|
||||
FillTable(db.cat.qd, g.extra.qd, function(k, v) return IsId(k) and type(v) == "table" end)
|
||||
if Codex.Catalog then Codex.Catalog.rev = (Codex.Catalog.rev or 0) + 1 end
|
||||
end
|
||||
-- Karten-Scan (Karten, Gebiete, Gebiete je Karte, Trigger, Flugpunkte): nur fehlende Eintraege, eigener Scan geht vor
|
||||
if type(g.extra.scan) == "table" then
|
||||
db.cat = db.cat or {}
|
||||
for _, name in ipairs({ "m", "a", "ma", "t", "x" }) do
|
||||
if type(g.extra.scan[name]) == "table" then
|
||||
db.cat[name] = db.cat[name] or {}
|
||||
FillTable(db.cat[name], g.extra.scan[name], function(k, v)
|
||||
return type(k) == "number" and (type(v) == "string" or type(v) == "table")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
db.foreverRev = g.rev
|
||||
end
|
||||
|
||||
-- Daten des Addons laden (bei Bedarf wird das Addon geladen). Rueckgabe: true, wenn Daten da sind
|
||||
function Forever.Load()
|
||||
if Forever.loaded then return true end
|
||||
if Forever.Disabled() or not Codex.Share or not Codex.Share.SetBuiltin then return false end
|
||||
local g = _G.FreewoodForeverData
|
||||
if not (g and g.ready) then
|
||||
local load = (_G.C_AddOns and _G.C_AddOns.LoadAddOn) or _G.LoadAddOn
|
||||
if type(load) == "function" then pcall(load, DATA_ADDON) end
|
||||
g = _G.FreewoodForeverData
|
||||
end
|
||||
if not (g and g.ready and type(g.list) == "table") then
|
||||
Forever.missing = true
|
||||
return false
|
||||
end
|
||||
if Forever.loading then return false end
|
||||
Forever.loading = true
|
||||
local list = g.list
|
||||
local total, slice, from = #list, 600, 1
|
||||
Codex.Share.BuiltinBegin()
|
||||
Forever.count = 0
|
||||
local function Finish()
|
||||
Codex.Share.BuiltinDone()
|
||||
pcall(MergeExtras, g)
|
||||
Forever.version, Forever.rev = g.version, g.rev
|
||||
Forever.loaded = true
|
||||
Forever.loading = false
|
||||
Forever.missing = false
|
||||
if Codex.chainRev then Codex.chainRev = Codex.chainRev + 1 end
|
||||
FW:Fire("CODEX_CHANGED", "forever")
|
||||
end
|
||||
local function Step()
|
||||
if not Forever.loading then return end -- zwischendurch ausgeschaltet
|
||||
local to = math.min(total, from + slice - 1)
|
||||
Forever.count = Forever.count + Codex.Share.BuiltinAdd(list, from, to)
|
||||
from = to + 1
|
||||
if from > total then
|
||||
Finish()
|
||||
elseif _G.C_Timer and _G.C_Timer.After then
|
||||
_G.C_Timer.After(0.05, Step)
|
||||
else
|
||||
Step()
|
||||
end
|
||||
end
|
||||
Step()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Quellen eines Items aus den Startdaten: { u = { {id, chance} }, o = { ... }, v = { Haendler-IDs } } oder nil
|
||||
local itemCache = {}
|
||||
function Forever.Item(id)
|
||||
if not Forever.loaded or Forever.Disabled() then return nil end
|
||||
local g = _G.FreewoodForeverData
|
||||
local s = g and type(g.src) == "table" and g.src[id]
|
||||
if type(s) ~= "string" then return nil end
|
||||
local c = itemCache[id]
|
||||
if c then return c end
|
||||
c = { u = {}, o = {}, v = {} }
|
||||
for part in s:gmatch("[^;]+") do
|
||||
local tag, body = part:sub(1, 1), part:sub(2)
|
||||
if tag == "u" or tag == "o" then
|
||||
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
|
||||
local t = c[tag]
|
||||
t[#t + 1] = { id = tonumber(k), chance = tonumber(v) }
|
||||
end
|
||||
elseif tag == "v" then
|
||||
for k in body:gmatch("%d+") do c.v[#c.v + 1] = tonumber(k) end
|
||||
end
|
||||
end
|
||||
itemCache[id] = c
|
||||
return c
|
||||
end
|
||||
|
||||
-- Alle Item-IDs mit Quellen in den Startdaten (Tabelle id -> Text; nur lesen)
|
||||
function Forever.ItemIds()
|
||||
if not Forever.loaded or Forever.Disabled() then return {} end
|
||||
local g = _G.FreewoodForeverData
|
||||
return (g and type(g.src) == "table") and g.src or {}
|
||||
end
|
||||
|
||||
function Forever.ItemHasVendor(id)
|
||||
local it = Forever.Item(id)
|
||||
return it ~= nil and #it.v > 0
|
||||
end
|
||||
|
||||
-- Schalter: an = Startdaten nutzen (Standard)
|
||||
function Forever.SetEnabled(on)
|
||||
local db = Codex.db
|
||||
if not db then return end
|
||||
db.settings.noForever = (not on) or nil
|
||||
if on then
|
||||
Forever.Load()
|
||||
elseif Forever.loading then
|
||||
Forever.loading = false
|
||||
Codex.Share.SetBuiltin({})
|
||||
Forever.count = 0
|
||||
elseif Forever.loaded then
|
||||
Codex.Share.SetBuiltin({})
|
||||
Forever.loaded, Forever.count = false, 0
|
||||
if Codex.chainRev then Codex.chainRev = Codex.chainRev + 1 end
|
||||
FW:Fire("CODEX_CHANGED", "forever")
|
||||
end
|
||||
end
|
||||
|
||||
function Forever.Status()
|
||||
local L = FW.L
|
||||
if Forever.Disabled() then return L.CODEX_FOREVER_OFF end
|
||||
if Forever.loaded then return string.format(L.CODEX_FOREVER_ON, Forever.count, tostring(Forever.version or "?")) end
|
||||
return L.CODEX_FOREVER_MISSING
|
||||
end
|
||||
|
||||
SLASH_FREEWOODFOREVER1 = "/fwforever"
|
||||
SlashCmdList["FREEWOODFOREVER"] = function(msg)
|
||||
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if msg == "aus" or msg == "off" then
|
||||
Forever.SetEnabled(false)
|
||||
elseif msg == "an" or msg == "on" then
|
||||
Forever.SetEnabled(true)
|
||||
end
|
||||
FW:Print(Forever.Status())
|
||||
end
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
-- Freewood-OctoWoW-Codex: Lern-Hinweise
|
||||
-- Nach einem Stufenaufstieg (ausserhalb des Kampfes) oder einer neuen Berufsstufe zeigt ein kurzer Hinweis
|
||||
-- in der Bildschirmmitte, was man jetzt beim Lehrer lernen kann. Grundlage: Lernplan (Lehrer-Daten).
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS.Codex
|
||||
if not FW or not Codex or not FW.UI then
|
||||
return
|
||||
end
|
||||
|
||||
local UI = FW.UI
|
||||
local Hints = { queue = {}, showing = false, shown = 0 }
|
||||
NS.Hints = Hints
|
||||
Codex.Hints = Hints
|
||||
|
||||
local DURATION = 5
|
||||
local FADE = 0.6
|
||||
local MAX_ROWS = 5
|
||||
local ROW_H = 24
|
||||
local WIDTH = 380
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Einstellungen
|
||||
------------------------------------------------------------------------
|
||||
|
||||
function Hints.ClassOn()
|
||||
return Codex.db.settings.hintClass ~= false
|
||||
end
|
||||
function Hints.ProfOn()
|
||||
return Codex.db.settings.hintProf ~= false
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Inhalt
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local function SpellKnown(id)
|
||||
if not id then return false end
|
||||
local SB = _G.C_SpellBook
|
||||
local fn = (SB and SB.IsSpellKnown) or _G.IsSpellKnown or (_G.C_Spell and _G.C_Spell.IsSpellKnown)
|
||||
if type(fn) ~= "function" then return false end
|
||||
local ok, known = pcall(fn, id)
|
||||
return ok and known == true
|
||||
end
|
||||
|
||||
local function Entry(r)
|
||||
local cost = Codex.ModePrice(r.pr)
|
||||
return { nm = r.nm or "?", sp = r.sp, rk = r.rk or (r.sp and Codex.SpellRank(r.sp)) or nil, cost = cost }
|
||||
end
|
||||
|
||||
-- Fähigkeiten deiner Klasse, die auf dieser Stufe beim Lehrer dazukommen
|
||||
function Hints.ClassEntries(level)
|
||||
local token = Codex.Time and Codex.Time.PlayerToken()
|
||||
if not token then return {} end
|
||||
local plan = Codex.LearnPlan(token)
|
||||
local g = plan.levels[level]
|
||||
local list = {}
|
||||
if g then
|
||||
for _, sp in ipairs(g.spells) do
|
||||
if not SpellKnown(sp.sp) then
|
||||
list[#list + 1] = { nm = sp.nm or "?", sp = sp.sp, rk = sp.rk or (sp.sp and Codex.SpellRank(sp.sp)) or nil, cost = sp.cost }
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Berufslehrer-Fähigkeiten, deren Fertigkeitswert in (von, bis] liegt
|
||||
function Hints.ProfEntries(skill, from, to)
|
||||
local list, seen = {}, {}
|
||||
for _, u in pairs(Codex.V().units) do
|
||||
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
|
||||
for key, r in pairs(u.tr) do
|
||||
if r.sn == skill and r.sr and r.sr > from and r.sr <= to then
|
||||
local k = r.sp and ("s" .. r.sp) or key
|
||||
if not seen[k] then
|
||||
seen[k] = true
|
||||
list[#list + 1] = Entry(r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.nm ~= b.nm then return a.nm < b.nm end
|
||||
return (a.rk or "") < (b.rk or "")
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Anzeige
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local frame
|
||||
|
||||
-- gespeicherte Position (verschoben in den Einstellungen) oder Standard: Bildschirmmitte, etwas hoeher
|
||||
local function ApplyPos(f)
|
||||
f:ClearAllPoints()
|
||||
local p = Codex.db.settings.hintPos
|
||||
if type(p) == "table" and p.point and p.x and p.y then
|
||||
f:SetPoint(p.point, UIParent, p.rel or p.point, p.x, p.y)
|
||||
else
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 160)
|
||||
end
|
||||
end
|
||||
|
||||
function Hints.SavePos()
|
||||
if not frame or not frame.GetPoint then return false end
|
||||
local point, _, rel, x, y = frame:GetPoint(1)
|
||||
if not point then return false end
|
||||
Codex.db.settings.hintPos = { point = point, rel = rel, x = x, y = y }
|
||||
return true
|
||||
end
|
||||
|
||||
function Hints.ResetPos()
|
||||
Codex.db.settings.hintPos = nil
|
||||
if frame then ApplyPos(frame) end
|
||||
end
|
||||
|
||||
local function Build()
|
||||
local f = CreateFrame("Button", nil, UIParent)
|
||||
f:SetSize(WIDTH, 80)
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetClampedToScreen(true)
|
||||
f:SetMovable(true)
|
||||
f:RegisterForDrag("LeftButton")
|
||||
f:SetScript("OnDragStart", function(self)
|
||||
if Hints.editing then self:StartMoving() end
|
||||
end)
|
||||
f:SetScript("OnDragStop", function(self)
|
||||
self:StopMovingOrSizing()
|
||||
if Hints.editing then Hints.SavePos() end
|
||||
end)
|
||||
ApplyPos(f)
|
||||
f:Hide()
|
||||
local bg = UI.CreatePanel(f, "bgDeep", "gold")
|
||||
bg:SetAllPoints(f)
|
||||
-- Inhalt auf eigener Ebene ueber dem Hintergrund (ein Kindfenster zeichnet sonst ueber den Texten des Eltern-Fensters)
|
||||
local c = CreateFrame("Frame", nil, f)
|
||||
c:SetAllPoints(f)
|
||||
c:SetFrameLevel((bg:GetFrameLevel() or 1) + 2)
|
||||
f.title = UI.CreateLabel(c, "", { font = "heading", size = 14, color = "goldLight" })
|
||||
f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -10)
|
||||
f.title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, -10)
|
||||
f.title:SetWordWrap(false)
|
||||
f.rows = {}
|
||||
for i = 1, MAX_ROWS do
|
||||
local row = {}
|
||||
row.icon = c:CreateTexture(nil, "ARTWORK")
|
||||
row.icon:SetSize(ROW_H - 4, ROW_H - 4)
|
||||
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||
row.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -(34 + (i - 1) * ROW_H))
|
||||
row.text = UI.CreateLabel(c, "", { size = 12 })
|
||||
row.text:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
|
||||
row.text:SetWidth(WIDTH - 150)
|
||||
row.text:SetWordWrap(false)
|
||||
row.cost = UI.CreateLabel(c, "", { size = 11, color = "textDim", justify = "RIGHT" })
|
||||
row.cost:SetPoint("RIGHT", f, "RIGHT", -12, 0)
|
||||
row.cost:SetPoint("TOP", row.icon, "TOP", 0, 0)
|
||||
row.cost:SetPoint("BOTTOM", row.icon, "BOTTOM", 0, 0)
|
||||
row.cost:SetWidth(120)
|
||||
f.rows[i] = row
|
||||
end
|
||||
f.more = UI.CreateLabel(c, "", { size = 11, color = "textDim" })
|
||||
f.foot = UI.CreateLabel(c, "", { size = 10, color = "textDim" })
|
||||
f:SetScript("OnClick", function()
|
||||
if Hints.editing then return end
|
||||
Hints.Dismiss()
|
||||
if f.onClick then f.onClick() end
|
||||
end)
|
||||
f:SetScript("OnUpdate", function(self, elapsed)
|
||||
if Hints.editing then
|
||||
self.age = 0
|
||||
self:SetAlpha(1)
|
||||
return
|
||||
end
|
||||
self.age = (self.age or 0) + (elapsed or 0)
|
||||
if self.age >= DURATION then
|
||||
Hints.Dismiss()
|
||||
elseif self.age > DURATION - FADE then
|
||||
self:SetAlpha(math.max(0, (DURATION - self.age) / FADE))
|
||||
end
|
||||
end)
|
||||
return f
|
||||
end
|
||||
|
||||
local function InCombat()
|
||||
if _G.InCombatLockdown and _G.InCombatLockdown() then return true end
|
||||
return _G.UnitAffectingCombat and _G.UnitAffectingCombat("player") or false
|
||||
end
|
||||
|
||||
local function Display(h)
|
||||
frame = frame or Build()
|
||||
ApplyPos(frame)
|
||||
local L = FW.L
|
||||
frame.title:SetText(h.title)
|
||||
local n = math.min(#h.entries, MAX_ROWS)
|
||||
local total = 0
|
||||
local money = _G.GetMoney and _G.GetMoney() or nil
|
||||
for i = 1, MAX_ROWS do
|
||||
local row, e = frame.rows[i], h.entries[i]
|
||||
if e and i <= n then
|
||||
local icon = e.sp and Codex.SpellIcon(e.sp) or nil
|
||||
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
row.icon:Show()
|
||||
row.text:SetText(e.nm .. (e.rk and (" |cff9a9aa3(" .. e.rk .. ")|r") or ""))
|
||||
row.cost:SetText((e.cost and e.cost > 0 and Codex.FormatMoney) and Codex.FormatMoney(e.cost) or "")
|
||||
else
|
||||
row.icon:Hide()
|
||||
row.text:SetText("")
|
||||
row.cost:SetText("")
|
||||
end
|
||||
end
|
||||
for _, e in ipairs(h.entries) do total = total + (e.cost or 0) end
|
||||
local y = 34 + n * ROW_H + 4
|
||||
local more = #h.entries - n
|
||||
frame.more:ClearAllPoints()
|
||||
frame.more:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
|
||||
frame.more:SetText(more > 0 and string.format(L.CODEX_HINT_MORE, more) or "")
|
||||
if more > 0 then y = y + 16 end
|
||||
local foot = Hints.editing and L.CODEX_HINT_DRAG or L.CODEX_HINT_CLICK
|
||||
if total > 0 and Codex.FormatMoney and not Hints.editing then
|
||||
foot = string.format(L.CODEX_HINT_COST, Codex.FormatMoney(total))
|
||||
if money and money < total then
|
||||
foot = foot .. " | " .. string.format(L.CODEX_HINT_MISSING, Codex.FormatMoney(total - money))
|
||||
end
|
||||
foot = foot .. " | " .. L.CODEX_HINT_CLICK
|
||||
end
|
||||
frame.foot:ClearAllPoints()
|
||||
frame.foot:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
|
||||
frame.foot:SetText(foot)
|
||||
frame:SetHeight(y + 26)
|
||||
frame.onClick = h.onClick
|
||||
frame.age = 0
|
||||
frame:SetAlpha(1)
|
||||
frame:Show()
|
||||
Hints.showing = true
|
||||
Hints.shown = Hints.shown + 1
|
||||
Hints.last = h
|
||||
end
|
||||
|
||||
function Hints.Dismiss()
|
||||
if Hints.editing then return end
|
||||
if frame then frame:Hide() end
|
||||
Hints.showing = false
|
||||
Hints.Flush()
|
||||
end
|
||||
|
||||
-- naechsten wartenden Hinweis zeigen, wenn kein Kampf ist und keiner laeuft
|
||||
function Hints.Flush()
|
||||
if Hints.showing or Hints.editing or InCombat() then return end
|
||||
local h = table.remove(Hints.queue, 1)
|
||||
if h then Display(h) end
|
||||
end
|
||||
|
||||
function Hints.Push(h)
|
||||
if not h or #h.entries == 0 then return false end
|
||||
Hints.queue[#Hints.queue + 1] = h
|
||||
Hints.Flush()
|
||||
return true
|
||||
end
|
||||
|
||||
local function OpenPlan(level)
|
||||
return function()
|
||||
UI:Show("codex")
|
||||
if Codex.ui and Codex.ui.Go then Codex.ui.Go("learn", level) end
|
||||
end
|
||||
end
|
||||
|
||||
function Hints.OnLevelUp(level)
|
||||
level = tonumber(level)
|
||||
if not level or not Hints.ClassOn() or not Codex.db.settings.enabled then return false end
|
||||
local entries = Hints.ClassEntries(level)
|
||||
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, level), entries = entries, onClick = OpenPlan(level) })
|
||||
end
|
||||
|
||||
local prevSkill = {}
|
||||
function Hints.OnSkill(skill, value)
|
||||
value = tonumber(value)
|
||||
if not skill or not value then return false end
|
||||
local from = prevSkill[skill] or (value - 1)
|
||||
prevSkill[skill] = value
|
||||
if not Hints.ProfOn() or not Codex.db.settings.enabled then return false end
|
||||
local entries = Hints.ProfEntries(skill, from, value)
|
||||
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_PROF_TITLE, skill, value), entries = entries, onClick = function() UI:Show("codex") end })
|
||||
end
|
||||
|
||||
-- Test aus den Einstellungen: letzte Stufe mit Daten, sonst eine Beispielzeile
|
||||
function Hints.Demo()
|
||||
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
|
||||
local entries
|
||||
for lv = pl, 1, -1 do
|
||||
entries = Hints.ClassEntries(lv)
|
||||
if #entries > 0 then
|
||||
pl = lv
|
||||
break
|
||||
end
|
||||
end
|
||||
if not entries or #entries == 0 then
|
||||
entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 } }
|
||||
end
|
||||
Hints.queue = {}
|
||||
Hints.showing = false
|
||||
if frame then frame:Hide() end
|
||||
Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries, onClick = OpenPlan(pl) })
|
||||
end
|
||||
|
||||
-- Verschieben: der Hinweis bleibt stehen, bis der Modus beendet wird; Position wird beim Loslassen gespeichert
|
||||
function Hints.SetEditMode(on)
|
||||
on = on and true or false
|
||||
if on == (Hints.editing == true) then return end
|
||||
if on then
|
||||
Hints.queue, Hints.showing = {}, false
|
||||
if frame then frame:Hide() end
|
||||
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
|
||||
local entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 }, { nm = FW.L.CODEX_HINT_DEMO, rk = "2", cost = 25000 } }
|
||||
Hints.editing = true
|
||||
Display({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries })
|
||||
else
|
||||
Hints.SavePos()
|
||||
Hints.editing = false
|
||||
Hints.showing = false
|
||||
if frame then frame:Hide() end
|
||||
Hints.Flush()
|
||||
end
|
||||
FW:Fire("CODEX_HINT_EDIT")
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Ereignisse
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- Muster aus dem Textbaustein des Spiels ("... %s ... %d" oder mit Stellen "%1$s ... %2$d"). Rueckgabe: Muster, Reihenfolge
|
||||
-- der Platzhalter (Stelle -> Rang in der Fundstelle), sonst nil
|
||||
local function SkillPattern(fmt)
|
||||
fmt = fmt or _G.SKILL_RANK_UP
|
||||
if type(fmt) ~= "string" then return nil end
|
||||
local out, order, i = { "^" }, {}, 1
|
||||
while i <= #fmt do
|
||||
local pos, kind, len = fmt:match("^%%(%d+)%$([sd])()", i)
|
||||
if pos then
|
||||
i = len
|
||||
else
|
||||
kind, len = fmt:match("^%%([sd])()", i)
|
||||
pos = #order + 1
|
||||
if kind then i = len end
|
||||
end
|
||||
if kind then
|
||||
out[#out + 1] = kind == "s" and "(.+)" or "(%d+)"
|
||||
order[#order + 1] = tonumber(pos)
|
||||
else
|
||||
local ch = fmt:sub(i, i)
|
||||
out[#out + 1] = ch:match("%w") and ch or ("%" .. ch)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
out[#out + 1] = "$"
|
||||
return table.concat(out), order
|
||||
end
|
||||
Hints.SkillPattern = SkillPattern
|
||||
|
||||
FW:RegisterEvent("PLAYER_LEVEL_UP", function(_, level)
|
||||
if C_Timer and C_Timer.After then
|
||||
C_Timer.After(1, function() Hints.OnLevelUp(level) end)
|
||||
else
|
||||
Hints.OnLevelUp(level)
|
||||
end
|
||||
end, Hints)
|
||||
FW:RegisterEvent("PLAYER_REGEN_ENABLED", function() Hints.Flush() end, Hints)
|
||||
FW:RegisterEvent("PLAYER_REGEN_DISABLED", function()
|
||||
if Hints.editing then Hints.SetEditMode(false) end
|
||||
end, Hints)
|
||||
FW:RegisterEvent("CHAT_MSG_SKILL", function(_, text)
|
||||
local pat, order = SkillPattern()
|
||||
if type(text) ~= "string" or not pat then return end
|
||||
local caps = { text:match(pat) }
|
||||
if #caps == 0 then return end
|
||||
local skill, value = caps[1], caps[2]
|
||||
if order and order[1] == 2 then skill, value = caps[2], caps[1] end -- Fertigkeit steht an Stelle 2
|
||||
if skill then Hints.OnSkill(skill, value) end
|
||||
end, Hints)
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Einstellungen: Abschnitt
|
||||
------------------------------------------------------------------------
|
||||
|
||||
if UI.RegisterSettingsSection then
|
||||
UI:RegisterSettingsSection("codex-hints", {
|
||||
order = 106,
|
||||
height = 300,
|
||||
build = function(parent)
|
||||
local L = FW.L
|
||||
local f, stack = UI.CreateSection(parent, L.CODEX_HINT_HEADER)
|
||||
local note = UI.CreateLabel(f, L.CODEX_HINT_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
|
||||
stack:Add(note, { fit = true, gap = 8 })
|
||||
local classBox = UI.CreateCheckbox(f, L.CODEX_HINT_CLASS_SWITCH, function() return Hints.ClassOn() end,
|
||||
function(on) Codex.db.settings.hintClass = on and true or false end)
|
||||
stack:Add(classBox, { fit = true, gap = 10 })
|
||||
local profBox = UI.CreateCheckbox(f, L.CODEX_HINT_PROF_SWITCH, function() return Hints.ProfOn() end,
|
||||
function(on) Codex.db.settings.hintProf = on and true or false end)
|
||||
stack:Add(profBox, { fit = true, gap = 4 })
|
||||
local test = UI.CreateButton(f, L.CODEX_HINT_TEST, 200, 26, function() Hints.Demo() end, "default")
|
||||
stack:Add(test, { height = 26, gap = 12 })
|
||||
local move
|
||||
move = UI.CreateButton(f, L.CODEX_HINT_MOVE, 200, 26, function()
|
||||
Hints.SetEditMode(not Hints.editing)
|
||||
end, "default")
|
||||
stack:Add(move, { height = 26, gap = 6 })
|
||||
local reset = UI.CreateButton(f, L.CODEX_HINT_RESET, 200, 26, function() Hints.ResetPos() end, "default")
|
||||
stack:Add(reset, { height = 26, gap = 6 })
|
||||
stack:Layout()
|
||||
f.classBox, f.profBox, f.move = classBox, profBox, move
|
||||
FW:On("CODEX_HINT_EDIT", function()
|
||||
move:SetLabel(Hints.editing and L.CODEX_HINT_MOVE_DONE or L.CODEX_HINT_MOVE)
|
||||
move:SetStyle(Hints.editing and "primary" or "default")
|
||||
end, f)
|
||||
return f
|
||||
end,
|
||||
refresh = function(frame)
|
||||
frame.classBox:Refresh()
|
||||
frame.profBox:Refresh()
|
||||
end,
|
||||
})
|
||||
end
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
-- Freewood-OctoWoW-Codex: Kreaturen ueber ihre ID nachschlagen (OctoWoW)
|
||||
--
|
||||
-- C_CreatureInfo.GetCreatureInfoByID liefert zu einer Kreatur-ID: creatureID, displayID, name (englisch), type (Nummer),
|
||||
-- family, rank, subName. Ist die Kreatur dem Client noch unbekannt, laedt RequestLoadCreatureByID sie nach
|
||||
-- (Antwort: Ereignis CREATURE_DATA_LOAD_RESULT).
|
||||
-- Im Hintergrund werden bei allen Kreaturen, die wir schon kennen, fehlende Angaben ergaenzt:
|
||||
-- ct Typ (sprachunabhaengig, aus der Typ-Nummer)
|
||||
-- c Klasse (elite, selten, Boss ...) falls noch keine bekannt
|
||||
-- ne englischer Name (Ersatz, wenn kein gesehener Name da ist)
|
||||
-- Es werden nur bestehende Datensaetze ergaenzt, keine neuen angelegt.
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS and NS.Codex
|
||||
if not FW or not Codex then
|
||||
return
|
||||
end
|
||||
|
||||
local Lookup = { started = false }
|
||||
NS.Lookup = Lookup
|
||||
Codex.Lookup = Lookup
|
||||
|
||||
local TYPE_CODE = {
|
||||
[1] = "beast", [2] = "dragonkin", [3] = "demon", [4] = "elemental", [5] = "giant", [6] = "undead",
|
||||
[7] = "humanoid", [8] = "critter", [9] = "mechanical", [10] = "other", [11] = "totem", [12] = "pet", [13] = "gascloud",
|
||||
}
|
||||
local RANK_CODE = { [1] = "elite", [2] = "rareelite", [3] = "worldboss", [4] = "rare" }
|
||||
|
||||
local TICK = 0.4 -- Abstand zwischen zwei Schritten (Sekunden)
|
||||
local WAIT = 1.6 -- so lange auf die Antwort einer Anfrage warten
|
||||
local PER_TICK = 25 -- bereits bekannte Kreaturen je Schritt (nur lesen, kostet fast nichts)
|
||||
local RETRY_AFTER = 7 * 86400 -- erfolglose IDs nach einer Woche wieder versuchen
|
||||
local FIRE_EVERY = 40 -- Oberflaeche hoechstens alle so viele Aenderungen auffrischen
|
||||
|
||||
local state = {
|
||||
queue = {}, first = 1, last = 0,
|
||||
pending = nil, pendingAt = 0,
|
||||
changed = 0, lastFire = 0, generation = 0,
|
||||
stats = { read = 0, requested = 0, filled = 0, failed = 0 },
|
||||
}
|
||||
Lookup.state = state
|
||||
|
||||
local function Now()
|
||||
return GetTime and GetTime() or 0
|
||||
end
|
||||
|
||||
local function Store()
|
||||
local db = Codex.db
|
||||
db.lookup = db.lookup or {}
|
||||
db.lookup.fail = db.lookup.fail or {}
|
||||
return db.lookup
|
||||
end
|
||||
|
||||
local function Enabled()
|
||||
return Codex.db ~= nil and Codex.db.settings ~= nil and Codex.db.settings.lookup ~= false
|
||||
and _G.C_CreatureInfo ~= nil and _G.C_CreatureInfo.GetCreatureInfoByID ~= nil
|
||||
end
|
||||
|
||||
local function ReadInfo(id)
|
||||
local ok, info = pcall(_G.C_CreatureInfo.GetCreatureInfoByID, id)
|
||||
if ok and type(info) == "table" and info.creatureID == id then
|
||||
return info
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function NeedsInfo(u)
|
||||
return type(u) == "table" and (not u.ct or not u.ne)
|
||||
end
|
||||
|
||||
-- Schreibt die Antwort in den bestehenden Datensatz. Rueckgabe: true, wenn sich etwas geaendert hat
|
||||
function Lookup.Apply(id, info)
|
||||
local u = Codex.db.units[id]
|
||||
if type(u) ~= "table" or type(info) ~= "table" then
|
||||
return false
|
||||
end
|
||||
local changed = false
|
||||
local code = TYPE_CODE[info.type]
|
||||
if code and u.ct ~= code then
|
||||
u.ct = code
|
||||
changed = true
|
||||
end
|
||||
local rc = RANK_CODE[info.rank]
|
||||
if rc and not u.c then
|
||||
u.c = rc
|
||||
changed = true
|
||||
end
|
||||
local name = info.name
|
||||
if type(name) == "string" and name ~= "" and not u.ne then
|
||||
u.ne = name:sub(1, 60)
|
||||
changed = true
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
local function Changed(id)
|
||||
state.changed = state.changed + 1
|
||||
state.stats.filled = state.stats.filled + 1
|
||||
if state.changed - state.lastFire >= FIRE_EVERY then
|
||||
state.lastFire = state.changed
|
||||
FW:Fire("CODEX_CHANGED", "unit", id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Warteschlange (mit Zaehlern, nicht table.remove)
|
||||
local function Push(id)
|
||||
state.last = state.last + 1
|
||||
state.queue[state.last] = id
|
||||
end
|
||||
|
||||
local function Pop()
|
||||
if state.first > state.last then
|
||||
return nil
|
||||
end
|
||||
local id = state.queue[state.first]
|
||||
state.queue[state.first] = nil
|
||||
state.first = state.first + 1
|
||||
if state.first > state.last then
|
||||
state.first, state.last = 1, 0
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
function Lookup.QueueSize()
|
||||
return state.last - state.first + 1
|
||||
end
|
||||
|
||||
-- Alle bekannten Kreaturen, denen etwas fehlt, in die Schlange stellen
|
||||
function Lookup.Scan()
|
||||
if not Enabled() then
|
||||
return 0
|
||||
end
|
||||
local fail = Store().fail
|
||||
local now = time and time() or 0
|
||||
local queued = {}
|
||||
for i = state.first, state.last do
|
||||
queued[state.queue[i]] = true
|
||||
end
|
||||
local ids = {}
|
||||
for id, u in pairs(Codex.db.units) do
|
||||
if type(id) == "number" and NeedsInfo(u) and not queued[id] then
|
||||
local f = fail[id]
|
||||
if not f or now - f > RETRY_AFTER then
|
||||
ids[#ids + 1] = id
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(ids)
|
||||
for i = 1, #ids do
|
||||
Push(ids[i])
|
||||
end
|
||||
return #ids
|
||||
end
|
||||
|
||||
local function Finish(id, info)
|
||||
state.pending = nil
|
||||
if info then
|
||||
if Lookup.Apply(id, info) then
|
||||
Changed(id)
|
||||
end
|
||||
else
|
||||
state.stats.failed = state.stats.failed + 1
|
||||
Store().fail[id] = time and time() or 1
|
||||
end
|
||||
end
|
||||
|
||||
function Lookup.OnLoadResult(_, id, ok)
|
||||
id = tonumber(id)
|
||||
if state.pending and id == state.pending then
|
||||
Finish(id, ReadInfo(id))
|
||||
end
|
||||
end
|
||||
|
||||
function Lookup.Step()
|
||||
if not Enabled() then
|
||||
return
|
||||
end
|
||||
local now = Now()
|
||||
if state.pending then
|
||||
if now - state.pendingAt < WAIT then
|
||||
return
|
||||
end
|
||||
-- keine Antwort: noch einmal lesen, sonst als erfolglos merken
|
||||
Finish(state.pending, ReadInfo(state.pending))
|
||||
end
|
||||
local n = 0
|
||||
while n < PER_TICK do
|
||||
local id = Pop()
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
local u = Codex.db.units[id]
|
||||
if NeedsInfo(u) then
|
||||
local info = ReadInfo(id)
|
||||
if info then
|
||||
state.stats.read = state.stats.read + 1
|
||||
if Lookup.Apply(id, info) then
|
||||
Changed(id)
|
||||
end
|
||||
n = n + 1
|
||||
else
|
||||
-- dem Client unbekannt: nachladen lassen und auf die Antwort warten
|
||||
state.pending, state.pendingAt = id, now
|
||||
state.stats.requested = state.stats.requested + 1
|
||||
local ok = pcall(_G.C_CreatureInfo.RequestLoadCreatureByID, id)
|
||||
if not ok then
|
||||
Finish(id, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Lookup:Start()
|
||||
if self.started then
|
||||
return
|
||||
end
|
||||
self.started = true
|
||||
state.generation = state.generation + 1
|
||||
local gen = state.generation
|
||||
FW:RegisterEvent("CREATURE_DATA_LOAD_RESULT", Lookup.OnLoadResult, Lookup)
|
||||
local function Loop(delay, fn)
|
||||
C_Timer.After(delay, function()
|
||||
if not Lookup.started or gen ~= state.generation then
|
||||
return
|
||||
end
|
||||
fn()
|
||||
Loop(delay, fn)
|
||||
end)
|
||||
end
|
||||
Loop(TICK, Lookup.Step)
|
||||
-- erste Runde kurz nach dem Einloggen, danach alle 10 Minuten fuer neu gesehene Kreaturen
|
||||
C_Timer.After(20, function()
|
||||
if Lookup.started and gen == state.generation then
|
||||
Lookup.Scan()
|
||||
end
|
||||
end)
|
||||
Loop(600, Lookup.Scan)
|
||||
end
|
||||
|
||||
function Lookup:Stop()
|
||||
self.started = false
|
||||
state.generation = state.generation + 1
|
||||
FW:UnregisterEvent("CREATURE_DATA_LOAD_RESULT", Lookup)
|
||||
end
|
||||
|
||||
-- /fwlookup Stand anzeigen
|
||||
-- /fwlookup run sofort neu durchsuchen
|
||||
-- /fwlookup retry auch fruehere Fehlschlaege noch einmal versuchen
|
||||
-- /fwlookup id <N> eine einzelne Kreatur nachschlagen und anzeigen
|
||||
-- /fwlookup clean Reste ohne Daten anzeigen; "clean yes" entfernt sie
|
||||
function Lookup.Command(msg)
|
||||
msg = (msg or ""):lower()
|
||||
if msg == "retry" then
|
||||
Store().fail = {}
|
||||
msg = "run"
|
||||
end
|
||||
if msg == "run" then
|
||||
local n = Lookup.Scan()
|
||||
FW:Print(string.format("Nachschlagen: %d Kreaturen vorgemerkt", n))
|
||||
return
|
||||
end
|
||||
if msg == "clean" or msg == "clean yes" then
|
||||
-- Reste ohne Beobachtungen: Kreaturen, die der Client nicht kennt (Nachschlagen erfolglos), ohne Kills, Positionen,
|
||||
-- Rollen, Beute oder sonstige Daten. Erst anzeigen, nur mit "clean yes" loeschen.
|
||||
local Share = Codex.Share
|
||||
local list = {}
|
||||
for id in pairs(Store().fail) do
|
||||
local u = Codex.db.units[id]
|
||||
if type(u) == "table" and not (Share and Share.IsShareable and Share.IsShareable("u", u)) and not u.ed and not u.cde and not u.cen
|
||||
and not u.ro and not u.d and not u.cells and (u.k or 0) == 0 then
|
||||
list[#list + 1] = id
|
||||
end
|
||||
end
|
||||
table.sort(list)
|
||||
if #list == 0 then
|
||||
FW:Print("Aufraeumen: keine Reste gefunden")
|
||||
return
|
||||
end
|
||||
for _, id in ipairs(list) do
|
||||
local u = Codex.db.units[id]
|
||||
FW:Print(string.format(" %d %s", id, tostring(u.n or u.ne or "?")))
|
||||
end
|
||||
if msg == "clean yes" then
|
||||
for _, id in ipairs(list) do
|
||||
Codex.db.units[id] = nil
|
||||
Store().fail[id] = nil
|
||||
end
|
||||
FW:Fire("CODEX_CHANGED", "unit", list[1])
|
||||
FW:Print(string.format("Aufraeumen: %d Kreaturen entfernt", #list))
|
||||
else
|
||||
FW:Print(string.format("Aufraeumen: %d Kreaturen ohne Daten. Zum Entfernen: /fwlookup clean yes", #list))
|
||||
end
|
||||
return
|
||||
end
|
||||
local want = msg:match("^id%s+(%d+)$")
|
||||
if want then
|
||||
local id = tonumber(want)
|
||||
local info = ReadInfo(id)
|
||||
if info then
|
||||
FW:Print(string.format("Kreatur %d: %s, Typ %s, Rang %s, Familie %s, Anzeige %s", id, tostring(info.name),
|
||||
tostring(info.type), tostring(info.rank), tostring(info.family), tostring(info.displayID)))
|
||||
else
|
||||
FW:Print("Kreatur " .. id .. " ist dem Client noch unbekannt (/fwnpc " .. id .. " laedt sie nach)")
|
||||
end
|
||||
return
|
||||
end
|
||||
local total, withType, withName = 0, 0, 0
|
||||
for id, u in pairs(Codex.db.units) do
|
||||
if type(id) == "number" then
|
||||
total = total + 1
|
||||
if u.ct then withType = withType + 1 end
|
||||
if u.ne or u.n then withName = withName + 1 end
|
||||
end
|
||||
end
|
||||
local failed = 0
|
||||
for _ in pairs(Store().fail) do failed = failed + 1 end
|
||||
local s = state.stats
|
||||
FW:Print(string.format("Nachschlagen: %s | Kreaturen %d, mit Typ %d, mit Namen %d | gelesen %d, angefragt %d, ergaenzt %d, erfolglos %d (gemerkt %d) | Schlange %d",
|
||||
Enabled() and "an" or "aus", total, withType, withName, s.read, s.requested, s.filled, s.failed, failed, Lookup.QueueSize()))
|
||||
end
|
||||
|
||||
if _G.SlashCmdList then
|
||||
_G.SLASH_FWLOOKUP1 = "/fwlookup"
|
||||
_G.SlashCmdList["FWLOOKUP"] = function(msg) Lookup.Command(msg) end
|
||||
end
|
||||
+764
@@ -0,0 +1,764 @@
|
||||
-- Freewood-OctoWoW-Codex: Fundorte auf der Weltkarte
|
||||
-- Zeichnet Punkte mit weissem Rand ueber Blizzards Kartenflaeche: schwarz fuer Kreaturen,
|
||||
-- rot fuer Objekte, blau fuer Quest-Geber und -Abgabe. Fundorte werden gebuendelt (BIN Zellen je Punkt).
|
||||
-- Quelle sind die Zellen, die Codex beim Spielen erfasst hat (Raster 200 x 200 je Karte).
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS.Codex
|
||||
if not FW or not Codex then
|
||||
return
|
||||
end
|
||||
|
||||
local UI = FW.UI
|
||||
local GRID = Codex.GRID
|
||||
local MAX_DOTS = 600
|
||||
local BIN = 3 -- Zellen je Punkt (3 Zellen = 1,5 % der Kartenbreite)
|
||||
local DOT_MIN = 9 -- Punktgroesse in Bildschirm-Pixeln (kleinster / groesster Punkt)
|
||||
local DOT_MAX = 14
|
||||
local RIM = 1.5 -- weisser Rand in Pixeln
|
||||
local LAYER_LEVEL = 1500 -- weit ueber Blizzards Kartensymbolen, damit nichts die Punkte verdeckt
|
||||
|
||||
local COLORS = {
|
||||
u = { 0, 0, 0 },
|
||||
o = { 0.9, 0.1, 0.1 },
|
||||
p = { 0.2, 0.55, 1 },
|
||||
}
|
||||
local ORDER = { u = 1, o = 2, p = 3 }
|
||||
local MASK = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
|
||||
|
||||
local Overlay = { points = nil, label = nil, hooked = false }
|
||||
Codex.Map = Overlay
|
||||
|
||||
local layer, dots, infoButton, StartHover
|
||||
local segDash = {} -- [Abschnitt] = { sig, tex = {...}, n }: Striche je Abschnitt, damit nur der veraenderte neu gesetzt wird
|
||||
local DASH_LEN, DASH_GAP, DASH_THICK = 8, 6, 3 -- Pixel
|
||||
local MAX_DASHES = 900
|
||||
local atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
|
||||
|
||||
-- Runder Punkt: weisse Scheibe mit schwarzer/roter/blauer Scheibe darauf. Rund wird er durch eine
|
||||
-- Maske aus dem Spiel; fehlt sie, bleiben es Quadrate.
|
||||
local function MaskAvailable()
|
||||
if Codex.db and Codex.db.settings.squareDots then
|
||||
return false
|
||||
end
|
||||
if type(GetFileIDFromPath) == "function" then
|
||||
return GetFileIDFromPath(MASK) ~= nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function NewDot(f)
|
||||
local dot = {}
|
||||
local useMask = MaskAvailable()
|
||||
for _, name in ipairs({ "rim", "fill" }) do
|
||||
local tex = f:CreateTexture(nil, name == "rim" and "ARTWORK" or "OVERLAY", nil, name == "rim" and 1 or 2)
|
||||
tex:SetColorTexture(1, 1, 1, 1)
|
||||
if useMask and f.CreateMaskTexture and tex.AddMaskTexture then
|
||||
local ok, mask = pcall(f.CreateMaskTexture, f)
|
||||
if ok and mask then
|
||||
mask:SetTexture(MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
|
||||
mask:SetAllPoints(tex)
|
||||
tex:AddMaskTexture(mask)
|
||||
tex.mask = mask
|
||||
end
|
||||
end
|
||||
dot[name] = tex
|
||||
end
|
||||
return dot
|
||||
end
|
||||
|
||||
local function HideDot(dot)
|
||||
dot.rim:Hide()
|
||||
dot.fill:Hide()
|
||||
end
|
||||
|
||||
local API = Codex.MapAPI
|
||||
|
||||
local function Canvas()
|
||||
return API.Canvas()
|
||||
end
|
||||
|
||||
local function EnsureLayer()
|
||||
if layer then
|
||||
return layer
|
||||
end
|
||||
local canvas = Canvas()
|
||||
if not canvas then
|
||||
return nil
|
||||
end
|
||||
-- Die Ebene haengt am Kartenfenster selbst (nicht an der Kartenflaeche): dort ist sie sicher sichtbar.
|
||||
-- Die Punkte werden an die Kartenflaeche geheftet und folgen ihr beim Verschieben und Zoomen.
|
||||
local wm = _G.WorldMapFrame
|
||||
if API.Classic() then
|
||||
-- 1.12-Karte: Ebene direkt auf der Kartenflaeche, Anteile 0..1 gelten fuer die ganze Flaeche
|
||||
layer = CreateFrame("Frame", nil, canvas)
|
||||
layer:SetAllPoints(canvas)
|
||||
layer:SetFrameLevel((canvas:GetFrameLevel() or 1) + 20)
|
||||
if wm.GetFrameStrata then layer:SetFrameStrata(wm:GetFrameStrata()) end
|
||||
else
|
||||
layer = CreateFrame("Frame", nil, wm)
|
||||
layer:SetAllPoints(wm.ScrollContainer or wm)
|
||||
layer:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL)
|
||||
if wm.GetFrameStrata then
|
||||
layer:SetFrameStrata(wm:GetFrameStrata())
|
||||
end
|
||||
end
|
||||
if layer.SetClipsChildren then
|
||||
layer:SetClipsChildren(true)
|
||||
end
|
||||
layer:SetScript("OnSizeChanged", function(_, w)
|
||||
if w and w > 1 and Overlay.points then
|
||||
Overlay:Refresh()
|
||||
end
|
||||
end)
|
||||
dots = {}
|
||||
StartHover(layer)
|
||||
return layer
|
||||
end
|
||||
|
||||
local function EnsureInfoButton()
|
||||
if infoButton then
|
||||
return infoButton
|
||||
end
|
||||
local wm = _G.WorldMapFrame
|
||||
if not wm then
|
||||
return nil
|
||||
end
|
||||
infoButton = UI.CreateButton(wm, "", 300, 22, function()
|
||||
Overlay:Clear()
|
||||
end, "default")
|
||||
if API.Classic() then
|
||||
local canvas = Canvas() or wm
|
||||
if wm.GetFrameStrata then infoButton:SetFrameStrata(wm:GetFrameStrata()) end
|
||||
infoButton:SetFrameLevel((canvas:GetFrameLevel() or 1) + 40)
|
||||
infoButton:SetPoint("TOPLEFT", canvas, "TOPLEFT", 12, -12)
|
||||
else
|
||||
infoButton:SetFrameStrata("HIGH")
|
||||
infoButton:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL + 100)
|
||||
local anchor = wm.ScrollContainer or wm
|
||||
infoButton:SetPoint("TOPLEFT", anchor, "TOPLEFT", 12, -42)
|
||||
end
|
||||
infoButton:Hide()
|
||||
return infoButton
|
||||
end
|
||||
|
||||
local function CurrentMapID()
|
||||
return API.CurrentMapID(function()
|
||||
local list, seen = {}, {}
|
||||
for _, p in ipairs(Overlay.points or {}) do
|
||||
if p.m and not seen[p.m] then seen[p.m] = true; list[#list + 1] = p.m end
|
||||
end
|
||||
return list
|
||||
end)
|
||||
end
|
||||
|
||||
-- Faktor Kartenflaeche -> Bildschirm (aendert sich beim Zoomen)
|
||||
function Overlay:CanvasScale()
|
||||
local canvas = Canvas()
|
||||
local s = 1
|
||||
if canvas and canvas.GetEffectiveScale and _G.UIParent then
|
||||
local a, b = canvas:GetEffectiveScale(), _G.UIParent:GetEffectiveScale()
|
||||
if type(a) == "number" and type(b) == "number" and b > 0 and a > 0 then
|
||||
s = a / b
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- Skalierung der Punkte-Ebene relativ zur Oberflaeche
|
||||
local function LayerScale()
|
||||
local s = 1
|
||||
if layer and layer.GetEffectiveScale and _G.UIParent then
|
||||
local a, b = layer:GetEffectiveScale(), _G.UIParent:GetEffectiveScale()
|
||||
if type(a) == "number" and type(b) == "number" and b > 0 and a > 0 then
|
||||
s = a / b
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- Punkte einer Karte zusammenfassen: je Klasse (u, o, p) und BIN x BIN Zellen ein Punkt
|
||||
function Overlay:Bins(mapID)
|
||||
local bins = {}
|
||||
for _, p in ipairs(self.points or {}) do
|
||||
if p.m == mapID then
|
||||
local cls = p.pin and "p" or (p.k == "o" and "o" or "u")
|
||||
local cx = math.min(GRID - 1, math.max(0, math.floor(p.x * GRID)))
|
||||
local cy = math.min(GRID - 1, math.max(0, math.floor(p.y * GRID)))
|
||||
local base = math.floor(cy / BIN) * 1000 + math.floor(cx / BIN)
|
||||
local key = base .. cls
|
||||
local b = bins[key]
|
||||
if not b then
|
||||
b = { sx = 0, sy = 0, n = 0, names = {}, cls = cls, base = base }
|
||||
bins[key] = b
|
||||
end
|
||||
local w = p.n or 1
|
||||
b.sx, b.sy, b.n = b.sx + p.x * w, b.sy + p.y * w, b.n + w
|
||||
if p.name then
|
||||
b.names[p.name] = (b.names[p.name] or 0) + w
|
||||
end
|
||||
end
|
||||
end
|
||||
local list, maxN, perBase = {}, 1, {}
|
||||
for _, b in pairs(bins) do
|
||||
b.x, b.y = b.sx / b.n, b.sy / b.n
|
||||
list[#list + 1] = b
|
||||
perBase[b.base] = (perBase[b.base] or 0) + 1
|
||||
if b.n > maxN then
|
||||
maxN = b.n
|
||||
end
|
||||
end
|
||||
for _, b in ipairs(list) do
|
||||
b.shared = perBase[b.base] > 1
|
||||
end
|
||||
-- Reihenfolge: Kreaturen zuerst, Quest-Punkte zuletzt (oben)
|
||||
table.sort(list, function(a, b)
|
||||
if ORDER[a.cls] ~= ORDER[b.cls] then return ORDER[a.cls] < ORDER[b.cls] end
|
||||
if a.y ~= b.y then return a.y < b.y end
|
||||
return a.x < b.x
|
||||
end)
|
||||
return list, maxN
|
||||
end
|
||||
|
||||
-- Gestrichelte Linie durch die Wegpunkte (Luftlinie von Station zu Station).
|
||||
-- Jeder Abschnitt merkt sich seine Lage: bleibt er gleich, werden die Striche nur wieder eingeblendet.
|
||||
local function HideDashes(from)
|
||||
for i = from or 1, #segDash do
|
||||
local s = segDash[i]
|
||||
for k = 1, s.n do s.tex[k]:Hide() end
|
||||
s.n, s.sig = 0, nil
|
||||
end
|
||||
end
|
||||
|
||||
local function DrawPath(f, canvas, path, width, height, ratio, layerScale)
|
||||
local total = 0
|
||||
local len = DASH_LEN / layerScale
|
||||
local step = (DASH_LEN + DASH_GAP) / layerScale
|
||||
for i = 2, #path do
|
||||
local s = segDash[i - 1]
|
||||
if not s then s = { tex = {}, n = 0 }; segDash[i - 1] = s end
|
||||
local x1, y1 = path[i - 1].x * width * ratio, -path[i - 1].y * height * ratio
|
||||
local x2, y2 = path[i].x * width * ratio, -path[i].y * height * ratio
|
||||
local sig = string.format("%.2f,%.2f,%.2f,%.2f,%.4f", x1, y1, x2, y2, layerScale)
|
||||
if s.sig == sig then
|
||||
for k = 1, s.n do s.tex[k]:Show() end
|
||||
else
|
||||
local dx, dy = x2 - x1, y2 - y1
|
||||
local dist = math.sqrt(dx * dx + dy * dy)
|
||||
local count = 0
|
||||
if dist > 0 then
|
||||
local ux, uy = dx / dist, dy / dist
|
||||
local angle = atan2(dy, dx)
|
||||
local t = len / 2
|
||||
while t <= dist and total + count < MAX_DASHES do
|
||||
count = count + 1
|
||||
local tex = s.tex[count]
|
||||
if not tex then
|
||||
tex = f:CreateTexture(nil, "OVERLAY", nil, 0)
|
||||
tex:SetColorTexture(1, 0.85, 0.2, 0.95)
|
||||
s.tex[count] = tex
|
||||
end
|
||||
tex:ClearAllPoints()
|
||||
tex:SetSize(len, DASH_THICK / layerScale)
|
||||
tex:SetPoint("CENTER", canvas, "TOPLEFT", x1 + ux * t, y1 + uy * t)
|
||||
if tex.SetRotation then tex:SetRotation(angle) end
|
||||
tex:Show()
|
||||
t = t + step
|
||||
end
|
||||
end
|
||||
for k = count + 1, s.n do s.tex[k]:Hide() end
|
||||
s.n, s.sig = count, sig
|
||||
end
|
||||
total = total + s.n
|
||||
end
|
||||
HideDashes(#path) -- Abschnitte, die es nicht mehr gibt
|
||||
return total
|
||||
end
|
||||
|
||||
function Overlay:Refresh()
|
||||
local f = EnsureLayer()
|
||||
if not f then
|
||||
return
|
||||
end
|
||||
for _, d in ipairs(dots) do
|
||||
HideDot(d)
|
||||
end
|
||||
if not (self.path and #self.path > 1 and self.points) then
|
||||
HideDashes()
|
||||
end
|
||||
f:Show()
|
||||
local btn = EnsureInfoButton()
|
||||
if not self.points or #self.points == 0 then
|
||||
if btn then
|
||||
btn:Hide()
|
||||
end
|
||||
return
|
||||
end
|
||||
if btn then
|
||||
btn:SetLabel(string.format(FW.L.CODEX_MAP_LABEL, self.label or ""))
|
||||
btn:SetWidth(math.max(200, (btn.label:GetStringWidth() or 200) + 24))
|
||||
btn:Show()
|
||||
end
|
||||
|
||||
local mapID = CurrentMapID()
|
||||
if not mapID then
|
||||
return
|
||||
end
|
||||
local canvas = Canvas() or f
|
||||
local width, height = canvas:GetWidth() or 1, canvas:GetHeight() or 1
|
||||
if width < 2 or height < 2 then
|
||||
if _G.C_Timer and not self.retrying then
|
||||
self.retrying = true
|
||||
_G.C_Timer.After(0.2, function()
|
||||
Overlay.retrying = false
|
||||
if Overlay.points then
|
||||
Overlay:Refresh()
|
||||
end
|
||||
end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local bins, maxN = self:Bins(mapID)
|
||||
self.bins = bins
|
||||
local scale = self:CanvasScale()
|
||||
local layerScale = LayerScale()
|
||||
local ratio = scale / layerScale -- Kartenflaeche-Einheiten -> Einheiten der Punkte-Ebene
|
||||
self.lastScale = scale
|
||||
self.drawnMap = mapID
|
||||
self.hoverR = (DOT_MAX / 2 + 3) / scale / width -- Trefferradius als Anteil der Kartenbreite
|
||||
self.hoverRY = (DOT_MAX / 2 + 3) / scale / height
|
||||
self.lastDraw = { mapID = mapID, dots = #bins }
|
||||
if self.path and #self.path > 1 and self.pathMap == mapID then
|
||||
self.lastDraw.dashes = DrawPath(f, canvas, self.path, width, height, ratio, layerScale)
|
||||
else
|
||||
HideDashes()
|
||||
end
|
||||
|
||||
for i, b in ipairs(bins) do
|
||||
if i > MAX_DOTS then
|
||||
break
|
||||
end
|
||||
local dot = dots[i]
|
||||
if not dot then
|
||||
dot = NewDot(f)
|
||||
dots[i] = dot
|
||||
end
|
||||
local px = DOT_MIN + (DOT_MAX - DOT_MIN) * math.min(1, b.n / maxN)
|
||||
local d = px / layerScale
|
||||
local rim = (px + RIM * 2) / layerScale
|
||||
local off, offY = 0, 0
|
||||
if b.shared then
|
||||
-- Kreatur links, Objekt rechts, Quest-Punkt darueber: nebeneinander statt uebereinander
|
||||
local step = (px * 0.7) / layerScale
|
||||
if b.cls == "u" then off = -step elseif b.cls == "o" then off = step else offY = step end
|
||||
end
|
||||
local x, y = b.x * width * ratio + off, -b.y * height * ratio + offY
|
||||
local c = COLORS[b.cls]
|
||||
dot.rim:ClearAllPoints()
|
||||
dot.rim:SetSize(rim, rim)
|
||||
dot.rim:SetPoint("CENTER", canvas, "TOPLEFT", x, y)
|
||||
dot.rim:SetVertexColor(1, 1, 1, 1)
|
||||
dot.rim:Show()
|
||||
dot.fill:ClearAllPoints()
|
||||
dot.fill:SetSize(d, d)
|
||||
dot.fill:SetPoint("CENTER", canvas, "TOPLEFT", x, y)
|
||||
dot.fill:SetVertexColor(c[1], c[2], c[3], 1)
|
||||
dot.fill:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- Mouseover: Namen der Fundorte in der Naehe des Mauszeigers
|
||||
local function MouseOverMap()
|
||||
if API.Classic() then
|
||||
return API.CursorNorm()
|
||||
end
|
||||
local wm = _G.WorldMapFrame
|
||||
local sc = wm and wm.ScrollContainer
|
||||
if type(sc) ~= "table" or type(sc.GetNormalizedCursorPosition) ~= "function" then
|
||||
return nil
|
||||
end
|
||||
local focus
|
||||
if GetMouseFoci then
|
||||
focus = GetMouseFoci()
|
||||
focus = focus and focus[1]
|
||||
elseif GetMouseFocus then
|
||||
focus = GetMouseFocus()
|
||||
end
|
||||
local f, ok = focus, (focus == nil)
|
||||
local depth = 0
|
||||
while f and depth < 12 do
|
||||
if f == sc then
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
f = f.GetParent and f:GetParent() or nil
|
||||
depth = depth + 1
|
||||
end
|
||||
if not ok then
|
||||
return nil
|
||||
end
|
||||
local x, y = sc:GetNormalizedCursorPosition()
|
||||
if not x or x < 0 or x > 1 or y < 0 or y > 1 then
|
||||
return nil
|
||||
end
|
||||
return x, y
|
||||
end
|
||||
|
||||
function Overlay:HideHover()
|
||||
self.hoverKey = nil
|
||||
if self.hoverShown then
|
||||
self.hoverShown = nil
|
||||
local tip = _G.GameTooltip
|
||||
if tip:GetOwner() == _G.WorldMapFrame then
|
||||
tip:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Overlay:UpdateHover()
|
||||
if not self.points or not self.bins or not self.drawnMap then
|
||||
return self:HideHover()
|
||||
end
|
||||
local x, y = MouseOverMap()
|
||||
if not x then
|
||||
return self:HideHover()
|
||||
end
|
||||
local key = math.floor(x * GRID) * 1000 + math.floor(y * GRID)
|
||||
if key == self.hoverKey and self.hoverShown ~= nil then
|
||||
return
|
||||
end
|
||||
self.hoverKey = key
|
||||
local r, ry = self.hoverR or 0.02, self.hoverRY or 0.03
|
||||
local names = {}
|
||||
local hit = false
|
||||
for i, b in ipairs(self.bins) do
|
||||
if i > MAX_DOTS then
|
||||
break
|
||||
end
|
||||
if math.abs(b.x - x) <= r and math.abs(b.y - y) <= ry then
|
||||
hit = true
|
||||
for name, n in pairs(b.names) do
|
||||
names[name] = (names[name] or 0) + n
|
||||
end
|
||||
end
|
||||
end
|
||||
if not hit then
|
||||
return self:HideHover()
|
||||
end
|
||||
local list = {}
|
||||
for name, n in pairs(names) do
|
||||
list[#list + 1] = { name = name, n = n }
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
if a.n ~= b.n then return a.n > b.n end
|
||||
return a.name < b.name
|
||||
end)
|
||||
local tip = _G.GameTooltip
|
||||
tip:SetOwner(_G.WorldMapFrame, "ANCHOR_CURSOR")
|
||||
tip:AddLine(FW.L.CODEX_MAP_HOVER, 0.94, 0.75, 0.36)
|
||||
for i, e in ipairs(list) do
|
||||
if i > 10 then
|
||||
tip:AddLine(string.format(FW.L.CODEX_MAP_MORE, #list - 10), 0.8, 0.8, 0.8)
|
||||
break
|
||||
end
|
||||
tip:AddLine(e.name, 0.85, 0.85, 0.85)
|
||||
end
|
||||
tip:Show()
|
||||
self.hoverShown = true
|
||||
end
|
||||
|
||||
local hoverElapsed = 0
|
||||
StartHover = function(f)
|
||||
f:SetScript("OnUpdate", function(_, elapsed)
|
||||
hoverElapsed = hoverElapsed + elapsed
|
||||
if hoverElapsed < 0.08 then
|
||||
return
|
||||
end
|
||||
hoverElapsed = 0
|
||||
if Overlay.points then
|
||||
local sig = API.Signature()
|
||||
if sig ~= Overlay.lastSig then
|
||||
Overlay.lastSig = sig
|
||||
Overlay:Refresh()
|
||||
end
|
||||
local sc = Overlay:CanvasScale()
|
||||
if Overlay.lastScale and math.abs(sc - Overlay.lastScale) > 0.01 then
|
||||
Overlay:Refresh()
|
||||
end
|
||||
Overlay:UpdateHover()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Overlay:Hook()
|
||||
if self.hooked then
|
||||
return true
|
||||
end
|
||||
local wm = _G.WorldMapFrame
|
||||
if not wm or not Canvas() then
|
||||
return false
|
||||
end
|
||||
self.hooked = true
|
||||
if wm.OnMapChanged then
|
||||
pcall(hooksecurefunc, wm, "OnMapChanged", function()
|
||||
Overlay:Refresh()
|
||||
end)
|
||||
end
|
||||
if wm.HookScript then
|
||||
wm:HookScript("OnShow", function()
|
||||
Overlay:Refresh()
|
||||
end)
|
||||
end
|
||||
if API.Classic() and _G.hooksecurefunc and _G.SetMapZoom then
|
||||
pcall(hooksecurefunc, "SetMapZoom", function() Overlay:Refresh() end)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Karte mit dem meisten Gewicht, bevorzugt die aktuelle, wenn sie Punkte hat
|
||||
function Overlay:BestMap()
|
||||
local weights = {}
|
||||
for _, p in ipairs(self.points or {}) do
|
||||
weights[p.m] = (weights[p.m] or 0) + (p.n or 1)
|
||||
end
|
||||
local current = CurrentMapID()
|
||||
if current and weights[current] then
|
||||
return current
|
||||
end
|
||||
local best, bestN
|
||||
for m, n in pairs(weights) do
|
||||
if not bestN or n > bestN or (n == bestN and m < best) then
|
||||
best, bestN = m, n
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Weltkarte oeffnen und auf das passende Gebiet stellen
|
||||
function Overlay:Focus()
|
||||
local wm = _G.WorldMapFrame
|
||||
if not wm or not self.points then
|
||||
return
|
||||
end
|
||||
local target = self:BestMap()
|
||||
if not wm:IsShown() then
|
||||
if type(ToggleWorldMap) == "function" then
|
||||
pcall(ToggleWorldMap)
|
||||
else
|
||||
pcall(wm.Show, wm)
|
||||
end
|
||||
end
|
||||
if target and target ~= CurrentMapID() then
|
||||
API.SetMap(target)
|
||||
end
|
||||
self:Refresh()
|
||||
return target
|
||||
end
|
||||
|
||||
-- points: Liste { m, x, y, n, name, pin }, label: Text fuers Knoepfchen auf der Karte
|
||||
function Overlay:Show(points, label, path, pathMap)
|
||||
if not points or #points == 0 then
|
||||
FW:Print(FW.L.CODEX_MAP_NONE)
|
||||
return false
|
||||
end
|
||||
self.route = nil -- eine andere Anzeige beendet die laufende Quest-Route
|
||||
self.points = points
|
||||
self.label = label
|
||||
self.path = path
|
||||
self.pathMap = pathMap
|
||||
self:Hook()
|
||||
self:Refresh()
|
||||
self:Focus()
|
||||
return true
|
||||
end
|
||||
|
||||
function Overlay:Clear()
|
||||
self.route = nil
|
||||
self:HideHover()
|
||||
self.points = nil
|
||||
self.label = nil
|
||||
self.path = nil
|
||||
self.pathMap = nil
|
||||
self.bins = nil
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Laufende Quest-Route: folgt dir, solange die Karte offen ist
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local ROUTE_MOVE = 0.005 -- Bewegung (Anteil der Kartenbreite), ab der neu gerechnet wird
|
||||
local ROUTE_ARRIVE = 0.015 -- so nah dran gilt die Station als erreicht
|
||||
local ROUTE_TICK = 1 -- Sekunden zwischen zwei Pruefungen
|
||||
local routeGen = 0
|
||||
|
||||
-- Status der Stationen der aktuellen Route; aendert er sich (Quest angenommen), wird neu gerechnet
|
||||
local function RouteSignature(points)
|
||||
local t = {}
|
||||
for i, p in ipairs(points or {}) do
|
||||
t[i] = Codex.QuestStatus(p.id) and "x" or "o"
|
||||
end
|
||||
return table.concat(t)
|
||||
end
|
||||
|
||||
local function PlayerLoc()
|
||||
local loc = FW.Util.GetPlayerLocation()
|
||||
if loc and loc.mapID and loc.x and loc.y then return loc end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Reihenfolge und Linie neu rechnen (billig: die Positionen der Quests stehen schon in r.cands).
|
||||
-- Rueckgabe: false, wenn keine Station mehr uebrig ist
|
||||
function Overlay:RouteRecompute(first)
|
||||
local r = self.route
|
||||
if not r then return false end
|
||||
local loc = PlayerLoc()
|
||||
local ax, ay = Codex.PlayerPosOn(r.mapID, loc)
|
||||
if ax then
|
||||
for _, p in ipairs(r.points or {}) do
|
||||
if p.m == r.mapID and (p.x - ax) ^ 2 + (p.y - ay) ^ 2 < ROUTE_ARRIVE ^ 2 then
|
||||
r.skip[p.id] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- nur Quests, die noch offen sind (nicht angenommen, nicht abgegeben) und nicht schon erreicht wurden
|
||||
local live = {}
|
||||
for _, c in ipairs(r.cands) do
|
||||
if not r.skip[c.id] and Codex.QuestStatus(c.id) == nil then live[#live + 1] = c end
|
||||
end
|
||||
r.cands = live
|
||||
local points, mapID, path = Codex.QuestRouteOrder(live, loc)
|
||||
if #points == 0 then
|
||||
-- nichts mehr uebrig: Ende; sonst nur nichts in der Naehe (anderes Gebiet): alte Anzeige behalten
|
||||
if #live == 0 then self:Clear() end
|
||||
r.last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil
|
||||
return false
|
||||
end
|
||||
local mapChanged = r.mapID ~= mapID
|
||||
self.points, self.path, self.pathMap = points, path, mapID
|
||||
self.label = string.format(FW.L.CODEX_ROUTE_LABEL, #points)
|
||||
self.bins = nil
|
||||
r.points, r.mapID, r.sig = points, mapID, RouteSignature(points)
|
||||
r.last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil
|
||||
local wm = _G.WorldMapFrame
|
||||
if mapChanged and not first and wm and wm.IsShown and wm:IsShown() then
|
||||
API.SetMap(mapID)
|
||||
end
|
||||
self:Refresh()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Eine Pruefung: neu rechnen, wenn du dich bewegt hast oder sich eine Quest geaendert hat
|
||||
function Overlay:RouteStep()
|
||||
local r = self.route
|
||||
if not r then return false end
|
||||
local wm = _G.WorldMapFrame
|
||||
if wm and wm.IsShown and not wm:IsShown() then return false end
|
||||
local loc = PlayerLoc()
|
||||
local changed = RouteSignature(r.points) ~= r.sig
|
||||
if not changed and loc then
|
||||
local last = r.last
|
||||
changed = not last or last.m ~= loc.mapID or (loc.x - last.x) ^ 2 + (loc.y - last.y) ^ 2 > ROUTE_MOVE ^ 2
|
||||
end
|
||||
if changed then return self:RouteRecompute(false) end
|
||||
return false
|
||||
end
|
||||
|
||||
local function RouteTick(gen)
|
||||
local r = Overlay.route
|
||||
if not r or r.gen ~= gen then return end
|
||||
Overlay:RouteStep()
|
||||
if Overlay.route and Overlay.route.gen == gen and _G.C_Timer then
|
||||
_G.C_Timer.After(ROUTE_TICK, function() RouteTick(gen) end)
|
||||
end
|
||||
end
|
||||
|
||||
-- ids: Quest-IDs der Liste. Zeigt die Route und haelt sie danach aktuell.
|
||||
function Overlay:StartRoute(ids, opts)
|
||||
local loc = PlayerLoc()
|
||||
local cands = Codex.QuestRouteCandidates(ids, opts)
|
||||
local points, mapID, path = Codex.QuestRouteOrder(cands, loc)
|
||||
if #points == 0 then
|
||||
FW:Print(FW.L.CODEX_ROUTE_NONE)
|
||||
return false
|
||||
end
|
||||
self:Show(points, string.format(FW.L.CODEX_ROUTE_LABEL, #points), path, mapID)
|
||||
routeGen = routeGen + 1
|
||||
self.route = { cands = cands, skip = {}, points = points, mapID = mapID, gen = routeGen,
|
||||
sig = RouteSignature(points), last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil }
|
||||
if _G.C_Timer then
|
||||
_G.C_Timer.After(ROUTE_TICK, function() RouteTick(routeGen) end)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
FW:RegisterEvent("ADDON_LOADED", function(_, name)
|
||||
if name == "Blizzard_WorldMap" then
|
||||
Overlay:Hook()
|
||||
Overlay:Refresh()
|
||||
end
|
||||
end, Overlay)
|
||||
|
||||
-- Diagnose: /fwcodex map
|
||||
function Overlay:Debug()
|
||||
local canvas = Canvas()
|
||||
local d = self.lastDraw
|
||||
FW:Print("Karte: " .. tostring(CurrentMapID()) .. ", Flaeche " .. tostring(canvas and canvas:GetWidth()) .. " x "
|
||||
.. tostring(canvas and canvas:GetHeight()) .. ", Ebene " .. tostring(canvas and canvas:GetFrameLevel())
|
||||
.. ", Overlay-Ebene " .. tostring(layer and layer:GetFrameLevel()))
|
||||
FW:Print("Punkte geladen: " .. tostring(self.points and #self.points or 0) .. ", gezeichnet: " .. tostring(d and d.dots)
|
||||
.. ", Maske: " .. tostring(MaskAvailable()) .. ", Skalierung: " .. string.format("%.2f", self:CanvasScale()))
|
||||
local wm = _G.WorldMapFrame
|
||||
FW:Print("Kartenfenster sichtbar: " .. tostring(wm and wm:IsShown()) .. ", Kartenflaeche sichtbar: " .. tostring(canvas and canvas:IsVisible())
|
||||
.. ", Punkte-Ebene sichtbar: " .. tostring(layer and layer:IsVisible()) .. ", Ebenen-Skalierung: " .. string.format("%.2f", LayerScale()))
|
||||
API.Debug()
|
||||
local seen = {}
|
||||
for _, p in ipairs(self.points or {}) do
|
||||
if p.m and not seen[p.m] then
|
||||
seen[p.m] = true
|
||||
local c, z = API.Resolve(p.m)
|
||||
FW:Print("Punkte-Karte " .. p.m .. " (" .. tostring(_G.C_Map and _G.C_Map.GetMapInfo and (_G.C_Map.GetMapInfo(p.m) or {}).name) .. ") -> Kontinent "
|
||||
.. tostring(c) .. ", Zone " .. tostring(z))
|
||||
end
|
||||
end
|
||||
local first = dots and dots[1]
|
||||
if first then
|
||||
FW:Print("Erster Punkt: sichtbar " .. tostring(first.fill:IsVisible()) .. ", Grösse " .. tostring(first.fill:GetWidth())
|
||||
.. ", Ebene der Ebene sichtbar " .. tostring(layer and layer:IsVisible()))
|
||||
end
|
||||
end
|
||||
|
||||
-- Test: grosser roter Kasten in der Kartenmitte, zeigt ob ueberhaupt gezeichnet wird
|
||||
function Overlay:Test()
|
||||
local f = EnsureLayer()
|
||||
if not f then
|
||||
FW:Print("Weltkarte nicht gefunden.")
|
||||
return
|
||||
end
|
||||
if not f.testTex then
|
||||
f.testTex = f:CreateTexture(nil, "OVERLAY")
|
||||
f.testTex:SetColorTexture(1, 0.1, 0.1, 0.6)
|
||||
f.testTex:SetSize(160, 160)
|
||||
f.testTex:SetPoint("CENTER", f, "CENTER", 0, 0)
|
||||
end
|
||||
if f.testTex:IsShown() then
|
||||
f.testTex:Hide()
|
||||
else
|
||||
f.testTex:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- /fwcodex map square: Punkte als Quadrate (falls die runde Maske Probleme macht)
|
||||
function Overlay:ToggleSquare()
|
||||
local st = Codex.db.settings
|
||||
st.squareDots = not st.squareDots
|
||||
for _, d in ipairs(dots or {}) do
|
||||
HideDot(d)
|
||||
end
|
||||
dots = {}
|
||||
FW:Print("Codex-Karte: " .. (st.squareDots and "Quadrate" or "runde Punkte"))
|
||||
self:Refresh()
|
||||
end
|
||||
Reference in New Issue
Block a user