Files
2026-10-27 14:09:39 +00:00

4416 lines
192 KiB
Lua

-- Freewood-OctoWoW-Codex: Tab "Datenbank"
-- Oben die Arten (Quests, Kreaturen, Objekte, Items), links Suche und Liste, rechts das Detail.
-- Das Detail ist eine Folge von Zeilen; Zeilen mit Verweis fuehren zum passenden Eintrag.
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 Util = FW.Util
local LIST_W = 540
local ROW_H, ROW_GAP = 44, 4
local DEFAULT_ROWS = 300
local function MaxRows()
local n = Codex.db and Codex.db.settings and tonumber(Codex.db.settings.listMax)
if not n or n < 50 then return DEFAULT_ROWS end
return math.floor(n)
end
local LINE_H = 24
local KINDS = {
{ id = "quests", label = "CODEX_KIND_QUESTS" },
{ id = "units", label = "CODEX_KIND_UNITS" },
{ id = "objects", label = "CODEX_KIND_OBJECTS" },
{ id = "items", label = "CODEX_KIND_ITEMS" },
{ id = "zones", label = "CODEX_KIND_ZONES" },
{ id = "xp", label = "CODEX_KIND_XP" },
{ id = "learn", label = "CODEX_KIND_LEARN" },
}
local SORTS = {
quests = { "name", "zone", "recent", "lvl", "xp", "money", "qtime", "dist" },
units = { "name", "recent", "gold", "ttk", "gpm", "ahkill", "gpmah", "lvl", "xpm", "data", "dist" },
objects = { "name", "recent", "gold", "data", "dist" },
items = { "name", "price", "buy", "ah", "flip", "ilvl", "quality", "chance", "gph" },
zones = { "name", "zlvl", "cnt", "recent" },
xp = { "name", "recent", "xpm" },
learn = { "step" },
}
-- Standardrichtung: true = aufsteigend (kleinster Wert zuerst)
local SORT_ASC = { zlvl = true, zone = true, step = true, name = true, ttk = true, qtime = true, buy = true, dist = true, data = true }
-- Anzeige des Sortierwerts in der Zeile: "money" (Kupfer), "secs", "num"
local SORT_FORMAT = {
gold = "money", gpm = "money", ahkill = "money", gpmah = "money", price = "money", buy = "money", ah = "money", flip = "money", gph = "money", money = "money",
ttk = "secs", qtime = "secs", lvl = "num", xp = "num", xpm = "num", data = "num", cnt = "num",
}
local SORT_METRIC = {
step = "step", price = "price", ilvl = "ilvl", quality = "quality", chance = "chance", gold = "gold", ttk = "ttk", gpm = "gpm", ahkill = "ahkill", gpmah = "gpmah",
lvl = "lvl", xpm = "xpm", xp = "xp", money = "money", qtime = "qtime", buy = "buy", ah = "ah", flip = "flip", gph = "gph", data = "data",
zlvl = "zlvl", cnt = "cnt",
}
local XPTitle, XPKey
local ICON = {
quests = "Interface\\GossipFrame\\AvailableQuestIcon",
units = "Interface\\Icons\\INV_Misc_Bone_HumanSkull_01",
objects = "Interface\\Icons\\INV_Misc_Bag_10",
items = "Interface\\Icons\\INV_Misc_QuestionMark",
zones = "Interface\\Icons\\INV_Misc_Map_01",
xp = "Interface\\Icons\\INV_Misc_Book_09",
learn = "Interface\\Icons\\INV_Misc_Note_01",
plus = "Interface\\Buttons\\UI-PlusButton-Up",
minus = "Interface\\Buttons\\UI-MinusButton-Up",
}
-- Kreaturentypen, deren Modelle in ClassicAPI keinen Ankleide-Auftrag brauchen:
-- 1 Wildtier, 2 Drachkin, 3 Daemon, 4 Elementar, 5 Riese, 8 Kleintier, 9 Mechanisch
local SAFE_MODEL_TYPE = { [1] = true, [2] = true, [3] = true, [4] = true, [5] = true, [8] = true, [9] = true }
-- Reihenfolge = Vorrang
local ROLE_ICONS = {
{ "classtrainer", "Interface\\Icons\\INV_Misc_Book_09" },
{ "proftrainer", "Interface\\Icons\\INV_Misc_Book_06" },
{ "flight", "Interface\\Icons\\INV_Misc_Map_01" },
{ "banker", "Interface\\Icons\\INV_Misc_Coin_02" },
{ "auctioneer", "Interface\\Icons\\INV_Misc_Coin_01" },
{ "innkeeper", "Interface\\Icons\\INV_Misc_Key_03" },
{ "stablemaster", "Interface\\Icons\\Ability_Hunter_BeastTaming" },
{ "battlemaster", "Interface\\Icons\\INV_BannerPVP_01" },
{ "spirithealer", "Interface\\Icons\\Spell_Holy_GuardianSpirit" },
{ "repair", "Interface\\Icons\\Trade_BlackSmithing" },
{ "vendor", "Interface\\Icons\\INV_Misc_Bag_08" },
}
-- Symbole je Kreaturtyp (Spiel-Symbole; fehlt eines im Client, erscheint ein leeres Feld: dann hier tauschen)
local CT_ICONS = {
beast = "Interface\\Icons\\Ability_Hunter_BeastCall",
humanoid = "Interface\\Icons\\INV_Misc_Head_Human_01",
undead = "Interface\\Icons\\Spell_Shadow_RaiseDead",
demon = "Interface\\Icons\\Spell_Shadow_SummonImp",
dragonkin = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
elemental = "Interface\\Icons\\Spell_Fire_Elemental_Totem",
giant = "Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem",
mechanical = "Interface\\Icons\\INV_Gizmo_01",
critter = "Interface\\Icons\\INV_Misc_Rabbit_2",
totem = "Interface\\Icons\\Spell_Nature_ManaRegenTotem",
gascloud = "Interface\\Icons\\Spell_Nature_Cyclone",
other = "Interface\\Icons\\INV_Misc_QuestionMark",
pet = "Interface\\Icons\\INV_Box_PetCarrier_01",
}
-- Symbol einer Kreatur: Familien-Symbol (wenn das Pet-Addon die Familie kennt), sonst Totenkopf
local function UnitIcon(u)
local Pet = FW.Pet
if u and u.f and Pet and Pet.GetFamilyID and Pet.FamilyIcon and Pet.GetFamilyID(u.f) then
return Pet.FamilyIcon(u.f)
end
-- Rolle (Haendler, Lehrer, Bank ...) als Symbol; ein 2D-Portrait gibt es fuer reine IDs nicht
local ro = u and u.ro
if type(ro) == "table" then
for _, pair in ipairs(ROLE_ICONS) do
if (ro[pair[1]] or 0) > 0 then
return pair[2]
end
end
end
-- Kreaturtyp (Wildtier, Untoter, ...) als Symbol
if u and u.ct and CT_ICONS[u.ct] then
return CT_ICONS[u.ct]
end
return ICON.units
end
------------------------------------------------------------------------
-- Formate
------------------------------------------------------------------------
local function FormatMoney(copper)
copper = math.floor(tonumber(copper) or 0)
local g = math.floor(copper / 10000)
local s = math.floor((copper % 10000) / 100)
local c = copper % 100
local parts = {}
if g > 0 then parts[#parts + 1] = "|cffffd100" .. g .. "g|r" end
if s > 0 or g > 0 then parts[#parts + 1] = "|cffc7c7cf" .. s .. "s|r" end
parts[#parts + 1] = "|cffeda55f" .. c .. "c|r"
return table.concat(parts, " ")
end
Codex.FormatMoney = FormatMoney
-- Preise eines Artikels beim Haendler: hoechster Preis = 100 %, billigere mit Prozent (Ruf, Talente)
local function PriceText(prices, ec)
local list = {}
for price, n in pairs(prices or {}) do list[#list + 1] = { price = price, n = n } end
if #list == 0 then return nil end
table.sort(list, function(a, b) return a.price > b.price end)
local top = list[1].price
local parts = {}
for i, e in ipairs(list) do
if i > 3 then break end
local text = e.price > 0 and FormatMoney(e.price) or "-"
if i > 1 and top > 0 then
text = text .. string.format(" (%d %%)", math.floor(e.price / top * 100 + 0.5))
end
parts[#parts + 1] = text
end
local out = table.concat(parts, " ")
if ec then out = out .. " +" .. FW.L.CODEX_WARE_TOKEN end
return out
end
local function FormatDuration(seconds)
seconds = math.floor((tonumber(seconds) or 0) + 0.5)
if seconds >= 3600 then
return string.format("%d:%02d:%02d", math.floor(seconds / 3600), math.floor((seconds % 3600) / 60), seconds % 60)
end
return string.format("%d:%02d", math.floor(seconds / 60), seconds % 60)
end
-- Kampfdauer fuer die eigene Klasse: Sekunden, Anzahl (Erfahrung-Zeilen und Details)
local function OwnTTK(pl, ml)
local token = Codex.Time and Codex.Time.PlayerToken()
if not (token and Codex.Share and Codex.Share.TTKMeasured) then
return nil
end
return Codex.Share.TTKMeasured(token, pl, ml - pl)
end
local function FormatChance(chance)
local p = chance * 100
if p >= 10 then
return string.format("%.0f%%", p)
end
return string.format("%.1f%%", p)
end
local function Colored(text, quality)
return "|cff" .. UI.RarityHex(quality or 1) .. text .. "|r"
end
local function SeedOK()
return Codex.Seed ~= nil and Codex.Seed.Available()
end
-- Angaben des Servers zu einer Kreatur / einem Objekt ueber die ID (ClassicAPI: C_CreatureInfo, C_GameObjectInfo).
-- request = true laedt Unbekanntes einmal nach und zeichnet die Detailseite danach neu (nie in Listen: sonst Anfrageflut)
local srvAsked, srvRedraw = {}, false
local function ServerInfo(kind, id, request)
if type(id) ~= "number" then return nil end
local info, asked
local C = kind == "units" and _G.C_CreatureInfo or _G.C_GameObjectInfo
local get = C and (kind == "units" and C.GetCreatureInfoByID or C.GetGameObjectInfoByID)
if get then
local ok, r = pcall(get, id)
if ok and type(r) == "table" then info = r end
end
if not info and Codex.Catalog and Codex.Catalog.GetEntity then
local nm, sb, f = Codex.Catalog.GetEntity(kind == "units" and "c" or "o", id)
if nm then
info = { name = nm, subName = sb, type = f and f.type, rank = f and f.rank, family = f and f.family, fromCatalog = true }
end
end
if not info and request and C then
local key = (kind == "units" and "u" or "o") .. id
local req = kind == "units" and C.RequestLoadCreatureByID or C.RequestLoadGameObjectByID
if req and not srvAsked[key] then
srvAsked[key] = true
asked = true
pcall(req, id)
end
end
if asked and not srvRedraw and _G.C_Timer then
srvRedraw = true
_G.C_Timer.After(1.5, function()
srvRedraw = false
if Codex.RedrawDetail then Codex.RedrawDetail() end
end)
end
return info
end
-- Spielobjekt-Typen (GameObjectType): englisch, deutsch
local GO_TYPES = {
[0] = { "Door", "Tür" }, [1] = { "Button", "Schalter" }, [2] = { "Quest giver", "Questgeber" }, [3] = { "Chest", "Truhe" },
[5] = { "Generic", "Allgemein" }, [6] = { "Trap", "Falle" }, [7] = { "Chair", "Stuhl" }, [8] = { "Spell focus", "Zauberfokus" },
[9] = { "Text", "Text" }, [10] = { "Interactive object", "Interaktives Objekt" }, [11] = { "Transport", "Transport" },
[17] = { "Fishing node", "Angelstelle" }, [19] = { "Mailbox", "Briefkasten" }, [22] = { "Spell caster", "Zauberauslöser" },
[25] = { "Fishing hole", "Fischschwarm" },
}
-- Name einer Kreatur / eines Objekts: eigener Eintrag, sonst die Vorlage (nur wenn die Art bekannt ist), sonst der Server
local function UnitTitle(u, id, kind)
local name = Codex.NameOf(u)
if not name and kind and SeedOK() then
name = Codex.Seed.Name(kind, id)
end
if not name and kind then
local si = ServerInfo(kind, id, false)
if si and type(si.name) == "string" and si.name ~= "" then name = si.name end
end
return name or ("#" .. tostring(id))
end
local function SeedTag()
return "|cff8a8a96[" .. FW.L.CODEX_SEED_TAG .. "]|r"
end
local function SeedPct(chance)
if not chance or chance <= 0 then return "" end -- Chance unbekannt
if chance >= 10 then
return string.format(FW.L.CODEX_SEED_CHANCE, string.format("%.0f", chance))
end
if chance < 1 then
return string.format(FW.L.CODEX_SEED_CHANCE, string.format("%.2f", chance))
end
return string.format(FW.L.CODEX_SEED_CHANCE, string.format("%.1f", chance))
end
local function RoleNames(kind, id)
local list = {}
for _, role in ipairs(Codex.RoleList(kind, id)) do
list[#list + 1] = FW.L["CODEX_ROLE_" .. role] or role
end
return list
end
local function LevelText(u)
local L = FW.L
if u.l1 and u.l2 then
if u.l1 == u.l2 then
return string.format(L.CODEX_LEVEL, u.l1)
end
return string.format(L.CODEX_LEVEL_RANGE, u.l1, u.l2)
end
return nil
end
------------------------------------------------------------------------
-- Verzeichnisse (Rueckwaerts-Suche), bei Aenderung neu aufgebaut
------------------------------------------------------------------------
local dirty = true
local itemIndex, itemIds
-- Rueckgabe: index[itemID] = { u, o, r, qi, zones = { [mapID] = true }, best = hoechste Drop-Chance }, Liste der IDs
local function ItemIndex()
if itemIndex and not dirty then
return itemIndex, itemIds
end
local index, ids = {}, {}
local grid2 = Codex.GRID * Codex.GRID
local function Facts(id)
local f = index[id]
if not f then
f = { zones = {}, best = 0 }
index[id] = f
ids[#ids + 1] = id
end
return f
end
local function FromEntities(tbl, flag)
for _, ent in pairs(tbl) do
local maps = {}
for key in pairs(ent.cells or {}) do
maps[math.floor(key / grid2)] = true
end
for _, m in ipairs(ent.ez or {}) do maps[m] = true end
for iid in pairs(ent.iz or {}) do maps[-iid] = true end
for id in pairs(ent.wa or {}) do
local f = Facts(id)
f.v = true
for m in pairs(maps) do f.zones[m] = true end
end
for bi, bucket in ipairs({ ent, ent.alt or {} }) do
for id, d in pairs(bucket.d or {}) do
local f = Facts(id)
f[flag] = true
if d.qi then f.qi = true end
if bi == 1 and (bucket.k or 0) > 0 and d.n then
local chance = d.n / bucket.k
if chance > f.best then f.best = chance end
end
for m in pairs(maps) do f.zones[m] = true end
end
end
end
end
FromEntities(Codex.V().units, "u")
FromEntities(Codex.V().objects, "o")
-- im Auktionshaus gescannte Items (nicht in ids: sonst Flut von Item-Anfragen)
if Codex.ExtraItems then
local ok, extra = pcall(Codex.ExtraItems)
if ok and type(extra) == "table" then
for id in pairs(extra) do
if not index[id] then index[id] = { zones = {}, best = 0, ah = true } end
end
end
end
-- Items aus den Startdaten, deren Itemdaten schon geladen sind (sonst nur Zahlen ohne Namen)
if Codex.Forever and Codex.Forever.ItemIds then
for id in pairs(Codex.Forever.ItemIds()) do
if not index[id] and Util.GetItem(id) then
local it = Codex.Forever.Item(id)
local f = { zones = {}, best = 0, fe = true }
if it then
f.u = (#it.u > 0) or nil
f.o = (#it.o > 0) or nil
f.v = (#it.v > 0) or nil
for _, e in ipairs(it.u) do if e.chance / 100 > f.best then f.best = e.chance / 100 end end
end
index[id] = f
end
end
end
for _, q in pairs(Codex.V().quests) do
for _, list in ipairs({ q.fixed or {}, q.choice or {} }) do
for _, r in ipairs(list) do
local f = Facts(r.id)
f.r = true
if q.zone then f.zones[q.zone] = true end
end
end
for id in pairs(q.qi or {}) do
local f = Facts(id)
f.qi = true
if q.zone then f.zones[q.zone] = true end
end
end
-- von Hand gesetzte Zonen der Items (nur lokal)
for id, rec in pairs(Codex.V().ied or Codex.db.ied or {}) do
local z = rec.ez or (rec.ed and rec.ed.z)
if z and index[id] then
for _, m in ipairs(z) do index[id].zones[m] = true end
end
end
itemIndex, itemIds = index, ids
dirty = false
return index, ids
end
function Codex.AllItemIds()
local _, ids = ItemIndex()
local list, seen = {}, {}
for _, id in ipairs(ids) do seen[id] = true list[#list + 1] = id end
if Codex.Forever and Codex.Forever.ItemIds then
for id in pairs(Codex.Forever.ItemIds()) do
if not seen[id] then seen[id] = true list[#list + 1] = id end
end
end
for id in pairs(Codex.db.cat and Codex.db.cat.i or {}) do
if type(id) == "number" and not seen[id] then seen[id] = true list[#list + 1] = id end
end
return list
end
function Codex.MarkItemsDirty() dirty = true end
-- Wert-Filter: flt.stats = { { key, min, max }, ... } (bis zu 3 Zeilen), flt.ops[i] = "and" | "or" zwischen
-- Zeile i und der naechsten. UND bindet staerker als ODER. Aeltere Form: flt.stat / statMin / statMax (eine Zeile).
local MAX_STAT_ROWS = 3
local function StatRows(flt)
local rows = {}
if flt.stats then
for i = 1, MAX_STAT_ROWS do
local r = flt.stats[i]
if r and r.key then
rows[#rows + 1] = { key = r.key, min = r.min, max = r.max, op = flt.ops and flt.ops[i] or "and" }
end
end
elseif flt.stat then
rows[1] = { key = flt.stat, min = flt.statMin, max = flt.statMax, op = "and" }
end
return rows
end
local function StatRowPass(st, r)
local v = st[r.key] or 0
if r.min then
if v < r.min then return false end
elseif v <= 0 then
return false
end
if r.max and v > r.max then return false end
return true
end
-- Gruppen: ODER trennt, UND verbindet. Das Operatorfeld einer Zeile gilt zur naechsten aktiven Zeile.
local function StatGroups(rows)
local groups = {}
for i, r in ipairs(rows) do
if i == 1 or rows[i - 1].op == "or" then
groups[#groups + 1] = { r }
else
local g = groups[#groups]
g[#g + 1] = r
end
end
return groups
end
local function StatsPass(st, flt)
local rows = StatRows(flt)
if #rows == 0 then return true end
if not st then return false end
for _, g in ipairs(StatGroups(rows)) do
local all = true
for _, r in ipairs(g) do
if not StatRowPass(st, r) then all = false break end
end
if all then return true end
end
return false
end
local function StatRowText(r)
local name = Codex.StatName(r.key)
if r.min and r.max then
return string.format("%s %s-%s", name, Codex.FormatStat(r.min), Codex.FormatStat(r.max))
elseif r.min then
return string.format("%s >= %s", name, Codex.FormatStat(r.min))
elseif r.max then
return string.format("%s <= %s", name, Codex.FormatStat(r.max))
end
return name
end
-- Klartext des Wert-Filters, zum Beispiel "(Beweglichkeit 8-10 UND Intelligenz 5-7) ODER Schwimmtempo"
local function StatPreview(flt)
local rows = StatRows(flt)
if #rows < 2 then return "" end
local L = FW.L
local groups = StatGroups(rows)
local parts = {}
for _, g in ipairs(groups) do
local texts = {}
for _, r in ipairs(g) do texts[#texts + 1] = StatRowText(r) end
local text = table.concat(texts, " " .. L.CODEX_OP_AND .. " ")
if #g > 1 and #groups > 1 then text = "(" .. text .. ")" end
parts[#parts + 1] = text
end
return table.concat(parts, " " .. L.CODEX_OP_OR .. " ")
end
-- Filter fuer Items. Filter, die Itemdaten brauchen, schliessen noch nicht geladene Items aus.
local function FilterActive(flt)
local n = 0
if next(flt.q) then n = n + 1 end
for _, k in ipairs({ "type", "sub", "slot", "src", "zone", "ilMin", "ilMax", "rlMin", "rlMax", "pMin", "pMax", "aMin", "aMax", "stat", "statMin", "statMax" }) do
if flt[k] ~= nil then n = n + 1 end
end
for i = 1, MAX_STAT_ROWS do
local r = flt.stats and flt.stats[i]
if r and r.key then n = n + 1 end
end
return n
end
local function PassFilter(flt, facts, info, id)
if flt.src == "vendor" and not (facts.v or (id and SeedOK() and Codex.Seed.ItemHasVendor(id)) or (id and Codex.Forever and Codex.Forever.ItemHasVendor and Codex.Forever.ItemHasVendor(id))) then return false end
if flt.src == "unit" and not facts.u then return false end
if flt.src == "object" and not facts.o then return false end
if flt.src == "reward" and not facts.r then return false end
if flt.src == "quest" and not facts.qi then return false end
if flt.zone and not facts.zones[flt.zone] then return false end
local hasStats = #StatRows(flt) > 0
local needInfo = next(flt.q) or flt.type or flt.sub or flt.slot or flt.ilMin or flt.ilMax
or flt.rlMin or flt.rlMax or flt.pMin or flt.pMax or flt.aMin or flt.aMax or hasStats or flt.statMin or flt.statMax
if not needInfo then
return true
end
if not info then return false end
if next(flt.q) and not flt.q[info.light and (info.quality or -1) or (info.quality or 1)] then return false end
if flt.type and info.type ~= flt.type then return false end
if flt.sub and info.subType ~= flt.sub then return false end
if flt.slot and info.equipLoc ~= flt.slot then return false end
local il, rl, price = info.itemLevel or 0, info.minLevel or 0, (info.sellPrice or 0) / 10000
if flt.ilMin and il < flt.ilMin then return false end
if flt.ilMax and il > flt.ilMax then return false end
if flt.rlMin and rl < flt.rlMin then return false end
if flt.rlMax and rl > flt.rlMax then return false end
if flt.pMin and price < flt.pMin then return false end
if flt.pMax and price > flt.pMax then return false end
if flt.aMin or flt.aMax then
local ah = info.id and Codex.AuctionProvider and Codex.AuctionPrice(info.id)
ah = (ah or 0) / 10000
if not (ah > 0) then return false end
if flt.aMin and ah < flt.aMin then return false end
if flt.aMax and ah > flt.aMax then return false end
end
if hasStats then
local st = info.id and Codex.db.istats and Codex.db.istats[info.id]
if not StatsPass(st, flt) then return false end
elseif flt.statMin or flt.statMax then
if not (info.id and Codex.db.istats and Codex.db.istats[info.id]) then return false end
end
return true
end
------------------------------------------------------------------------
-- Eintraege fuer die Liste
------------------------------------------------------------------------
local function Match(text, haystack)
return haystack and haystack:lower():find(text, 1, true) ~= nil
end
-- Suchtext passt auf einen der Namen (Spiel, Deutsch, Englisch)?
local function MatchNames(text, e)
return e and (Match(text, e.n) or Match(text, e.cde) or Match(text, e.cen))
end
local function MapOfCells(cells)
local m = Codex.MainMap(cells)
return m and Codex.MapName(m) or nil
end
-- Zone eines Datensatzes: von Hand gesetzte zuerst, sonst die Hauptkarte der Fundorte
-- "Level 1-4" / "Level 3" fuer eine Gebietsspanne
local function RangeText(lo, hi)
if not lo then return nil end
if not hi or hi == lo then return string.format(FW.L.CODEX_LEVEL, lo) end
return string.format(FW.L.CODEX_LEVEL, lo .. "-" .. hi)
end
local function ZoneOf(ent)
local z = Codex.ZonesOf(ent)[1]
return z and Codex.MapName(z) or nil
end
local function ClassLabel(cls)
local L = FW.L
if cls == "elite" or cls == "rareelite" then return L.CODEX_XP_ELITE end
if cls == "rare" then return L.CODEX_XP_RARE end
if cls == "worldboss" then return L.CODEX_XP_BOSS end
return nil
end
XPTitle = function(r)
local text = string.format(FW.L.CODEX_XP_TITLE, r.pl, r.ml)
local label = ClassLabel(r.cls)
if label then text = text .. " (" .. label .. ")" end
if r.rested then
text = text .. " [" .. FW.L.CODEX_XP_RESTED_TAG .. "]"
elseif r.bonus then
text = text .. " [" .. FW.L.CODEX_XP_BONUS_TAG .. "]"
end
return text
end
XPKey = function(r)
return r.pl .. ":" .. r.ml .. ":" .. r.cls .. ":" .. (r.rested and "r" or r.bonus and "b" or "a")
end
-- Vorlage: Eintraege fuer Quests, Kreaturen und Objekte, die es nur in der Vorlage gibt (einmal gebaut und gemerkt)
local seedEntries = {}
local mapNames = {}
local function CachedMapName(m)
local n = mapNames[m]
if not n then
n = Codex.MapName(m)
mapNames[m] = n
end
return n
end
local function RankLabel(rank)
local L = FW.L
if rank == 1 or rank == 2 then return L.CODEX_XP_ELITE end
if rank == 3 then return L.CODEX_XP_BOSS end
if rank == 4 then return L.CODEX_XP_RARE end
return nil
end
local ChainText
local function SeedList(kind)
local key = kind .. ":" .. Codex.Lang()
local cached = seedEntries[key]
if cached then
return cached
end
local L = FW.L
local Seed = Codex.Seed
local list = {}
for _, id in ipairs(Seed.IDs(kind)) do
local e
if kind == "quests" then
local q = Seed.Quest(id)
local title = Seed.QuestTitle(id) or ("Quest #" .. id)
local sub = {}
if q.lvl and q.lvl > 0 then sub[#sub + 1] = string.format(L.CODEX_LEVEL, q.lvl) end
local ref = q.start[1] or q.stop[1]
local zone
if ref and ref.kind ~= "items" then
local m = Seed.MapOf(ref.kind, ref.id)
zone = m and CachedMapName(m) or nil
end
if zone then sub[#sub + 1] = zone end
local chainText = ChainText(id)
if chainText then sub[#sub + 1] = chainText end
sub[#sub + 1] = SeedTag()
e = { id = id, title = title, sub = table.concat(sub, " | "), icon = ICON.quests, name = title:lower(),
recent = 0, seed = true, zone = zone and zone:lower() or "", zoneName = zone, l1 = q.lvl, l2 = q.lvl }
else
local ent = Seed.Entity(kind, id)
local title = Seed.Name(kind, id) or ("#" .. id)
local sub = {}
if kind == "units" and ent.l1 then
sub[#sub + 1] = ent.l1 == ent.l2 and string.format(L.CODEX_LEVEL, ent.l1) or string.format(L.CODEX_LEVEL_RANGE, ent.l1, ent.l2)
end
local m = Seed.MapOf(kind, id)
local zone = m and CachedMapName(m) or nil
if zone then sub[#sub + 1] = zone end
local rank = kind == "units" and RankLabel(ent.rank) or nil
if rank then sub[#sub + 1] = rank end
local roles = RoleNames(kind, id)
if #roles > 0 then sub[#sub + 1] = table.concat(roles, ", ") end
sub[#sub + 1] = SeedTag()
e = { id = id, title = title, sub = table.concat(sub, " | "), icon = ICON[kind], name = title:lower(), recent = 0,
seed = true, gold = -1, unitID = kind == "units" and id or nil, zone = zone and zone:lower() or "",
l1 = ent.l1, l2 = ent.l2 }
end
list[#list + 1] = e
end
seedEntries[key] = list
return list
end
-- Katalog: Quests, von denen nur Titel und Stufe bekannt sind (gebaut je Stand des Katalogs)
local catEntries, catRev, catLang
local function CatList()
local C = Codex.Catalog
if not C then return {} end
local rev = C.Rev()
if catEntries and catRev == rev then return catEntries end
local L = FW.L
local list = {}
for _, id in ipairs(C.IDs()) do
local title, lv = C.Get(id)
if title then
local sub = {}
if lv then sub[#sub + 1] = string.format(L.CODEX_LEVEL, lv) end
sub[#sub + 1] = "|cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r"
local meta = C.QuestMeta and C.QuestMeta(id)
if meta then
if meta.zone then table.insert(sub, #sub, meta.zone) end
if meta.info then table.insert(sub, #sub, meta.info) end
end
local zn = meta and meta.zone
list[#list + 1] = { id = id, title = title, sub = table.concat(sub, " | "), icon = ICON.quests, name = title:lower(),
recent = 0, cat = true, zone = zn and zn:lower() or "", zoneName = zn, l1 = lv, l2 = lv }
end
end
catEntries, catRev = list, rev
return list
end
local function SeedHit(text, kind, e)
if Codex.Seed.Haystack(kind, e.id):find(text, 1, true) then
return true
end
return e.zone ~= nil and e.zone ~= "" and e.zone:find(text, 1, true) ~= nil
end
local function RolePass(kind, id, role)
if not role then return true end
return Codex.RolesOf(kind, id)[role] ~= nil
end
local CHAIN_STEPS = { "in", "start", "mid", "end", "none" }
-- Questkette (nur mit Vorlage bekannt): chain = nil/"in"/"start"/"mid"/"end"/"none"
local function ChainPass(id, chain)
if not chain then return true end
local role = Codex.ChainRole(id)
if chain == "in" then return role ~= nil end
if chain == "none" then return role == nil end
return role == chain
end
local STATUS_STEPS = { "done", "active", "open" }
local STATUS_MARK = {
done = "Interface\\RaidFrame\\ReadyCheck-Ready",
active = "Interface\\Icons\\Ability_Rogue_Sprint",
}
local function StatusPass(id, status)
if not status then return true end
local st = Codex.QuestStatus(id)
if status == "open" then return st == nil end
return st == status
end
-- "Kette 2/4" fuer die Zeile, wenn die Vorlage die Quest in einer Kette kennt
ChainText = function(id)
if not SeedOK() then return nil end
local pos, total = Codex.Seed.ChainPos(id)
if pos then return string.format(FW.L.CODEX_CHAIN_POS, pos, total) end
return nil
end
-- Kampfdauer einer Kreatur: eigene Klasse, sonst Durchschnitt aller Klassen
local function UnitTTK(u)
if not (Codex.Time and u) then return nil end
local own = Codex.Time.UnitAverage(u)
if own and own > 0 then return own end
local list = Codex.Time.UnitClasses(u)
if #list == 0 then return nil end
local sum = 0
for _, c in ipairs(list) do sum = sum + c.secs end
local avg = sum / #list
return avg > 0 and avg or nil
end
-- Erfahrung je Kill der eigenen Stufe (sonst der hoechsten bekannten)
local function UnitXPAvg(u)
if not (u and u.xp) then return nil end
local pl = UnitLevel and UnitLevel("player") or nil
local s = pl and u.xp[pl]
if not s then
local best
for lv, st in pairs(u.xp) do
if not best or lv > best then best, s = lv, st end
end
end
return s and s.n and s.n > 0 and (s.sum / s.n) or nil
end
-- niedrigster beobachteter Einkaufspreis bei Haendlern (Kupfer) je Item
local function LowestBuyPrices()
local lowest = {}
for _, u in pairs(Codex.V().units) do
for id, w in pairs(u.wa or {}) do
for price in pairs(w.pr or {}) do
if price > 0 and (not lowest[id] or price < lowest[id]) then lowest[id] = price end
end
end
end
return lowest
end
local function ItemValueOf(id)
local ah = Codex.AuctionProvider and Codex.AuctionPrice(id)
if ah and ah > 0 then return ah end
local info = Util.GetItem(id)
return info and info.sellPrice or nil
end
-- Suchkuerzel: stufe:20-25, zone:Durotar, haendler:Ruestung (werden aus dem Suchtext herausgenommen)
local OP_KEYS = {
stufe = "lvl", level = "lvl", lvl = "lvl", lv = "lvl",
zone = "zone", gebiet = "zone",
["händler"] = "vendor", haendler = "vendor", vendor = "vendor", ["verkäufer"] = "vendor",
}
local function ParseSearch(text)
local ops
local rest = text:gsub("(%S-):(%S+)", function(key, value)
local op = OP_KEYS[key]
if not op then
return nil
end
ops = ops or {}
if op == "lvl" then
local a, b = value:match("^(%d+)%-(%d+)$")
if not a then a = value:match("^(%d+)$"); b = a end
if not a then return nil end
ops.lvl = { tonumber(a), tonumber(b) }
else
ops[op] = value:gsub("_", " ")
end
return ""
end)
rest = rest:gsub("^%s+", ""):gsub("%s+$", ""):gsub("%s+", " ")
return rest, ops
end
-- Nachfilter fuer die Kuerzel; e = Listeneintrag (mit l1/l2/zone)
local function OpsPass(kind, e, ops)
if not ops then return true end
if ops.lvl then
local lo, hi = e.l1, e.l2 or e.l1
if kind == "items" then
local info = Util.GetItem(e.id)
lo = info and info.minLevel or nil
hi = lo
end
if not lo or hi < ops.lvl[1] or lo > ops.lvl[2] then return false end
end
if ops.zone then
if not (e.zone and e.zone:find(ops.zone, 1, true)) then return false end
end
if ops.vendor then
if kind ~= "units" then return false end
local want = ops.vendor
local found = false
local live = Codex.V().units[e.id]
for itemID in pairs(live and live.wa or {}) do
local name = Codex:ItemName(itemID)
if name and name:lower():find(want, 1, true) then found = true break end
end
if not found and SeedOK() then
local roles = Codex.Seed.Roles("units", e.id)
if roles and roles.vendor then
for _, itemID in ipairs(Codex.Seed.VendorItems(e.id)) do
local name = Codex:ItemName(itemID) or Codex.Seed.Name("items", itemID)
if name and name:lower():find(want, 1, true) then found = true break end
end
end
end
if not found then return false end
end
return true
end
-- Katalog-Eintraege fuer Kreaturen/Objekte (aus der Server-Abfrage und den Zielen der Quests), je Stand des Katalogs gemerkt
local catEnt, catEntRev = {}, {}
local ctNames = {}
local function CreatureTypeName(t)
if type(t) ~= "number" then return nil end
local n = ctNames[t]
if n == nil then
n = false
local CC = _G.C_CreatureInfo
if CC and CC.GetCreatureTypeInfo then
local ok, ti = pcall(CC.GetCreatureTypeInfo, t)
if ok and type(ti) == "table" and type(ti.name) == "string" then n = ti.name end
end
ctNames[t] = n
end
return n or nil
end
local function CatEntities(kind)
local C = Codex.Catalog
if not (C and C.Reverse) then return {} end
local rev = C.Rev() .. ":" .. Codex.Lang()
if catEnt[kind] and catEntRev[kind] == rev then return catEnt[kind] end
local L = FW.L
local store = Codex.db and Codex.db.cat and Codex.db.cat[kind == "units" and "c" or "o"] or {}
local targets = C.Reverse()[kind == "units" and "monster" or "object"]
local list, seen = {}, {}
local function Add(id)
if seen[id] then return end
seen[id] = true
local name, subName, f = C.GetEntity(kind == "units" and "c" or "o", id)
local qs = targets[id]
local sub = {}
if f then
if kind == "units" then
local tn = CreatureTypeName(f.type)
if tn then sub[#sub + 1] = tn end
if RankLabel(f.rank) then sub[#sub + 1] = RankLabel(f.rank) end
else
local g = GO_TYPES[f.type]
if g then sub[#sub + 1] = Codex.Lang() == "en" and g[1] or g[2] end
end
end
if subName then sub[#sub + 1] = "<" .. subName .. ">" end
if qs then sub[#sub + 1] = string.format(L.CODEX_CAT_TARGET, #qs) end
sub[#sub + 1] = "|cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r"
local title = name or ("#" .. id)
list[#list + 1] = {
id = id, title = title, name = title:lower(), subName = subName, sub = table.concat(sub, " | "),
icon = kind == "units" and ICON.units or ICON[kind], recent = 0, cat = true,
unitID = kind == "units" and id or nil, gold = -1, zone = "",
}
end
for id in pairs(store) do if type(id) == "number" then Add(id) end end
for id in pairs(targets) do Add(id) end
catEnt[kind], catEntRev[kind] = list, rev
return list
end
-- opts: seed = Vorlage anzeigen, role = nur Kreaturen/Objekte mit dieser Rolle, chain = Questkette
local function BuildEntries(kind, text, flt, opts)
local L = FW.L
local db = Codex.V()
opts = opts or {}
text = (text or ""):lower()
local ops
text, ops = ParseSearch(text)
local list = {}
local useSeed = opts.seed and SeedOK()
local role = opts.role
local chain = SeedOK() and opts.chain or nil
local status = opts.status
if kind == "quests" then
for id, q in pairs(db.quests) do
local zone = q.zone and Codex.MapName(q.zone) or nil
if not zone and Codex.Catalog and Codex.Catalog.QuestMeta then
local m = Codex.Catalog.QuestMeta(id)
zone = m and m.zone or nil
end
local ok = text == ""
or MatchNames(text, q) or Match(text, q.t) or Match(text, zone)
or (q.give and Match(text, q.give.n)) or (q.turn and Match(text, q.turn.n))
or tostring(id) == text
if ok and ChainPass(id, chain) and StatusPass(id, status) then
local sub = {}
local title = Codex.QuestTitle(q, id)
if q.lvl then sub[#sub + 1] = string.format(L.CODEX_LEVEL, q.lvl) end
if zone then sub[#sub + 1] = zone end
local chainText = ChainText(id)
if chainText then sub[#sub + 1] = chainText end
if (q.done or 0) > 0 then sub[#sub + 1] = string.format(L.CODEX_DONE, q.done) end
list[#list + 1] = {
id = id, title = title, sub = table.concat(sub, " | "),
icon = ICON.quests, name = title:lower(), recent = q.last or 0,
l1 = q.lvl, l2 = q.lvl, zone = zone and zone:lower() or "", zoneName = zone,
}
end
end
if useSeed then
for _, e in ipairs(SeedList("quests")) do
if not db.quests[e.id] and (text == "" or SeedHit(text, "quests", e) or tostring(e.id) == text)
and ChainPass(e.id, chain) and StatusPass(e.id, status) then
list[#list + 1] = e
end
end
end
-- Katalog: nur Titel und Stufe (nicht, wenn es schon einen Datensatz oder die Vorlage gibt)
if not chain then
local seedOn = SeedOK()
for _, e in ipairs(CatList()) do
if not db.quests[e.id] and not (seedOn and Codex.Seed.Quest(e.id))
and (text == "" or e.name:find(text, 1, true) or tostring(e.id) == text)
and StatusPass(e.id, status) then
list[#list + 1] = e
end
end
end
elseif kind == "units" or kind == "objects" then
local src = kind == "units" and db.units or db.objects
for id, u in pairs(src) do
local zone = ZoneOf(u)
if (text == "" or MatchNames(text, u) or Match(text, zone) or Match(text, u.ti) or tostring(id) == text)
and RolePass(kind, id, role) then
local sub = {}
local lvl = LevelText(u)
if lvl then sub[#sub + 1] = lvl end
if zone then sub[#sub + 1] = zone end
if (u.k or 0) > 0 then sub[#sub + 1] = string.format(L.CODEX_KILLS, u.k) end
local value = Codex.KillValue(u)
if value and value >= 1 then sub[#sub + 1] = string.format(L.CODEX_VALUE_SHORT, FormatMoney(value)) end
local roles = RoleNames(kind, id)
if #roles > 0 then sub[#sub + 1] = table.concat(roles, ", ") end
if u.ti then sub[#sub + 1] = u.ti end
if kind == "units" and u.ct and CT_ICONS[u.ct] then sub[#sub + 1] = L["CODEX_CT_" .. u.ct] end
list[#list + 1] = {
id = id, title = UnitTitle(u, id, kind), sub = table.concat(sub, " | "),
icon = kind == "units" and UnitIcon(u) or ICON[kind], name = (UnitTitle(u, id, kind)):lower(), recent = u.lt or 0,
unitID = kind == "units" and id or nil,
gold = value or -1,
l1 = u.l1, l2 = u.l2, zone = zone and zone:lower() or "",
}
end
end
if useSeed then
for _, e in ipairs(SeedList(kind)) do
if not src[e.id] and (text == "" or SeedHit(text, kind, e) or tostring(e.id) == text)
and RolePass(kind, e.id, role) then
list[#list + 1] = e
end
end
end
-- Katalog: Kreaturen/Objekte aus der Server-Abfrage und Ziele aus den Quest-Einzelheiten
if not role then
for _, e in ipairs(CatEntities(kind)) do
if not src[e.id] and not (useSeed and Codex.Seed.Name(kind, e.id))
and (text == "" or e.name:find(text, 1, true) or tostring(e.id) == text
or (e.subName and e.subName:lower():find(text, 1, true))) then
list[#list + 1] = e
end
end
end
elseif kind == "zones" then
local pl = UnitLevel and UnitLevel("player") or 1
for key, z in pairs(Codex.Zones.All()) do
local name = Codex.MapName(key)
local pass = true
if opts.zoneSel == "world" then pass = not z.dungeon
elseif opts.zoneSel == "dungeon" then pass = z.dungeon
elseif opts.zoneSel == "fit" then pass = Codex.Zones.Compare(key, pl, 2) == "in"
elseif opts.zoneSel == "data" then pass = not z.few end
if pass and (text == "" or Match(text, name) or tostring(math.abs(key)) == text) then
local sub = {}
local range = RangeText(z.lo, z.hi)
if range then
sub[#sub + 1] = range .. ((z.avg and not z.manual) and string.format(" (%s %.1f)", L.CODEX_ZONE_AVG, z.avg) or "")
end
if z.manual then sub[#sub + 1] = L.CODEX_ZONE_MANUAL end
if z.few and not z.manual then sub[#sub + 1] = L.CODEX_ZONE_FEW end
sub[#sub + 1] = string.format(L.CODEX_ZONE_UNITS, z.units)
if #z.out > 0 then sub[#sub + 1] = string.format(L.CODEX_ZONE_OUTLIERS, #z.out) end
if #z.special > 0 then sub[#sub + 1] = string.format(L.CODEX_ZONE_SPECIAL, #z.special) end
if z.dungeon then sub[#sub + 1] = L.CODEX_PROPS_DG_YES end
list[#list + 1] = {
id = key, title = name, sub = table.concat(sub, " | "), icon = ICON.zones, name = name:lower(),
recent = z.units, l1 = z.lo, l2 = z.hi, zone = name:lower(),
zlvl = z.lo and (z.lo * 1000 + (z.hi or z.lo)) or -1, cnt = z.units,
}
end
end
elseif kind == "learn" then
local plan = opts.class and Codex.LearnPlan(opts.class)
if plan then
local pl = UnitLevel and UnitLevel("player") or 1
for _, lv in ipairs(plan.order) do
local g = plan.levels[lv]
local hit = text == "" or tostring(lv) == text
if not hit then
for _, sp in ipairs(g.spells) do
if Match(text, sp.nm) then hit = true break end
end
end
if hit then
local sub = string.format(L.CODEX_LEARN_SUB, #g.spells, FormatMoney(g.cost))
if lv > pl then
sub = sub .. " | " .. string.format(L.CODEX_LEARN_SUMSHORT, FormatMoney(Codex.LearnCost(plan, pl, lv)))
end
list[#list + 1] = {
id = lv, title = lv > 0 and string.format(L.CODEX_LEARN_TITLE, lv) or L.CODEX_LEARN_LVUNKNOWN,
sub = sub, icon = ICON.learn, name = string.format("%03d", lv), recent = lv,
step = lv > 0 and lv or 1e9,
}
end
end
end
elseif kind == "xp" then
for _, r in ipairs(Codex:XPRows(true)) do
local title = XPTitle(r)
if text == "" or Match(text, title) or tostring(r.pl) == text or tostring(r.ml) == text then
local secs = (not r.bonus and r.cls == "normal") and OwnTTK(r.pl, r.ml) or nil
list[#list + 1] = {
id = XPKey(r), title = title,
sub = string.format(L.CODEX_XP_ROW, r.avg, r.pred, r.diff, r.n)
.. ((secs and secs > 0) and (" | " .. string.format(L.CODEX_TTK_ROW, secs, r.avg / secs * 60)) or ""),
xpm = (secs and secs > 0) and (r.avg / secs * 60) or nil,
icon = ICON.xp, name = string.format("%03d%03d%s%s", 999 - r.pl, r.ml, r.cls, r.bonus and "b" or "a"),
recent = r.n,
}
end
end
else
local index = ItemIndex()
local function ItemEntry(id, facts, name, seed)
local info = Util.GetItem(id)
-- Das Spiel kennt viele Items noch nicht (nicht im Zwischenspeicher). Dann reichen gemerkte Werte:
-- Qualitaet aus dem Merker, Stufe/Mindeststufe/Preis aus dem Katalog. So filtern Qualitaet, Stufe und Preis
-- auch fuer Items, die noch nicht geladen sind (Typ, Platz und Werte brauchen weiter echte Itemdaten).
local light
if not info then
local nm, cq, il, ml, sp = nil, nil, nil, nil, nil
local Cat = Codex.Catalog
if Cat and Cat.GetItem then nm, cq, il, ml, sp = Cat.GetItem(id) end
local q = (Codex.db.iq and Codex.db.iq[id]) or cq
if q or il then
light = { id = id, name = nm, quality = q, itemLevel = il, minLevel = ml, sellPrice = sp, light = true }
end
end
if not PassFilter(flt, facts, info or light, id) then
return
end
if not info then info = light end
local sub = {}
if info then
if (info.itemLevel or 0) > 1 then sub[#sub + 1] = string.format(L.CODEX_ILVL_SHORT, info.itemLevel) end
if info.type then
sub[#sub + 1] = info.type .. ((info.subType and info.subType ~= "" and info.subType ~= info.type) and (" · " .. info.subType) or "")
end
if (info.sellPrice or 0) > 0 then sub[#sub + 1] = FormatMoney(info.sellPrice) end
if info.light then sub[#sub + 1] = "#" .. id end
else
sub[#sub + 1] = "#" .. id
end
if seed then sub[#sub + 1] = SeedTag() end
if facts.ah then sub[#sub + 1] = "|cff8a8a96[" .. L.CODEX_AH_TAG .. "]|r" end
list[#list + 1] = {
id = id, title = name or ("#" .. id), sub = table.concat(sub, " | "), itemID = id,
name = (name or ""):lower(), recent = seed and 0 or id,
price = info and (info.sellPrice or 0) or -1, ilvl = info and (info.itemLevel or 0) or -1,
quality = info and (info.quality or (info.light and -1 or 1)) or -1, chance = facts.best or 0, seed = seed or nil,
}
end
for id, facts in pairs(index) do
local name = Codex:ItemName(id)
if text == "" or Match(text, name) or tostring(id) == text then
ItemEntry(id, facts, name, false)
end
end
-- Items, die es nur in der Vorlage gibt
if useSeed then
local Seed = Codex.Seed
for _, id in ipairs(Seed.IDs("items")) do
if not index[id] and (text == "" or Seed.Haystack("items", id):find(text, 1, true) or tostring(id) == text) then
local it = Seed.Item(id)
local facts = { zones = {}, best = it and it.u[1] and it.u[1].chance / 100 or 0,
u = it and #it.u > 0 or nil, o = it and #it.o > 0 or nil }
ItemEntry(id, facts, Seed.Name("items", id), true)
end
end
end
end
if opts.fav and Codex.FavSupported(kind) then
local kept = {}
for _, e in ipairs(list) do
if Codex.IsFav(kind, e.id) then kept[#kept + 1] = e end
end
list = kept
end
if ops then
local kept = {}
for _, e in ipairs(list) do
if OpsPass(kind, e, ops) then kept[#kept + 1] = e end
end
list = kept
end
return list
end
------------------------------------------------------------------------
-- Tab
------------------------------------------------------------------------
local function Build(content)
local L = FW.L
local f = CreateFrame("Frame", nil, content)
local kind, selected, search = "quests", nil, ""
local sortBy, sortRev, flt, filterOpen = {}, {}, { q = {} }, false
local farmOn = false -- Farm-Ranking (Kreaturen)
local history = {} -- Verlauf fuer "Zurueck"
local listRows, detailRows = {}, {}
local Refresh, RefreshList, ShowDetail, Go, UpdateFilterUI, Layout
local nameCtl
local roleSel = {} -- gewaehlte Rolle je Art (Kreaturen, Objekte)
local chainSel = nil -- gewaehlte Questkette (nur Quests)
local statusSel = nil -- gewaehlter Quest-Status (nur Quests)
local zoneSel = nil -- gewaehlte Gebietsart (nur Gebiete)
local learnSel = nil -- gewaehlte Klasse (Lernplan)
local favOnly = {} -- nur Favoriten je Art
local collapsed = {} -- eingeklappte Gebiete (Quests nach Gebiet)
local dataVersion = 0 -- zaehlt jede Datenaenderung; merkt sich die sortierte Liste dazwischen
local memo = {}
local function SeedOn()
return SeedOK() and Codex.db.settings.showSeed ~= false
end
---------------- Kopf: Arten ----------------
local kindButtons = {}
local KIND_GAP = 4
for i, k in ipairs(KINDS) do local k = k
local b = UI.CreateButton(f, L[k.label], 60, 28, function()
kind, selected = k.id, nil
Refresh()
end, "default")
kindButtons[k.id] = b
end
-- "Erfahrung" ist ein Werkzeug fuer Entwickler: der Knopf erscheint nur mit dem Modul Freewood-OctoWoW-Dev
local function KindVisible(id)
return id ~= "xp" or FW.Dev ~= nil
end
local function LayoutKinds()
local shown = {}
for _, k in ipairs(KINDS) do
if KindVisible(k.id) then shown[#shown + 1] = k end
end
local w = math.floor((LIST_W - KIND_GAP * (#shown - 1)) / #shown)
for _, k in ipairs(KINDS) do
local b = kindButtons[k.id]
if KindVisible(k.id) then
local idx
for i, s in ipairs(shown) do if s == k then idx = i end end
b:SetWidth(w)
b:ClearAllPoints()
b:SetPoint("TOPLEFT", f, "TOPLEFT", (idx - 1) * (w + KIND_GAP), 0)
b:Show()
-- OctoWoW: die Schrift ist breiter als im Forever-Client; Text verkleinern, bis er in den Knopf passt
local size = 12
UI.SetFont(b.label, "body", size)
while size > 8 and (b.label:GetStringWidth() or 0) > w - 8 do
size = size - 1
UI.SetFont(b.label, "body", size)
end
else
b:Hide()
end
end
if not KindVisible(kind) then kind, selected = "quests", nil end
end
LayoutKinds()
---------------- links: Suche und Liste ----------------
local box = UI.CreatePanel(f, "bgDeep", "bronze")
box:SetHeight(26)
box:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -38)
box:SetWidth(LIST_W - 188 - 30)
local edit = CreateFrame("EditBox", nil, box)
edit:SetAutoFocus(false)
edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2)
edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2)
edit:SetMaxLetters(60)
if not edit:SetFont(UI.GetFontPath("body"), 12, "") then
edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
end
edit:SetTextColor(UI.Color("text"))
edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
local hint = UI.CreateLabel(box, L.CODEX_SEARCH, { size = 12, color = "textDim" })
hint:SetPoint("LEFT", box, "LEFT", 10, 0)
edit:SetScript("OnTextChanged", function(self)
search = self:GetText() or ""
if search == "" then hint:Show() else hint:Hide() end
if Refresh then Refresh() end
end)
local function CurrentSort()
return sortBy[kind] or SORTS[kind][1]
end
local function SortModes()
local list = {}
for _, m in ipairs(SORTS[kind]) do
if (m ~= "ah" and m ~= "ahkill" and m ~= "gpmah" and m ~= "flip") or Codex.AuctionProvider then
list[#list + 1] = { value = m, text = L["CODEX_SORTBY_" .. m] or m }
end
end
return list
end
local sortDD = UI.CreateDropdown(f, 150, 26, {
getOptions = SortModes,
textOf = function(v) return L["CODEX_SORTBY_" .. v] or v end,
onChange = function(v)
sortBy[kind] = v
sortRev[kind] = false
Refresh()
end,
})
sortDD:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W - 150, -38)
local dirButton = UI.CreateButton(f, "", 26, 26, function()
sortRev[kind] = not sortRev[kind]
Refresh()
end, "default")
dirButton:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W - 180, -38)
local dirTip = function(self)
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(L.CODEX_SORT_DIR_TIP)
GameTooltip:Show()
end
end
if dirButton.HookScript then
dirButton:HookScript("OnEnter", dirTip)
dirButton:HookScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
end
-- Pfeil als Textur: haengt davon ab, ob aktuell aufsteigend sortiert wird
dirButton.arrow = dirButton:CreateTexture(nil, "OVERLAY")
-- derselbe Pfeil wie bei der Wegfindung (zeigt nach oben); abwaerts wird er gespiegelt
dirButton.arrow:SetTexture("Interface\\Minimap\\ROTATING-MINIMAPARROW")
dirButton.arrow:SetVertexColor(UI.Color("goldLight"))
dirButton.arrow:SetSize(16, 16)
dirButton.arrow:SetPoint("CENTER", dirButton, "CENTER", 0, 0)
-- nur Favoriten (Stern neben der Suche)
local favToggle = UI.CreateButton(f, "", 26, 26, function()
favOnly[kind] = not favOnly[kind]
dataVersion = dataVersion + 1
Refresh()
end, "default")
favToggle:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W - 188 - 26, -38)
favToggle.star = favToggle:CreateTexture(nil, "OVERLAY")
-- gelber Stern aus dem Zielmarkierungs-Bogen (Feld oben links)
favToggle.star:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
favToggle.star:SetTexCoord(0, 0.25, 0, 0.25)
favToggle.star:SetSize(16, 16)
favToggle.star:SetPoint("CENTER", favToggle, "CENTER", 0, 0)
if favToggle.HookScript then
favToggle:HookScript("OnEnter", function(self)
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(L.CODEX_FAV_ONLY)
GameTooltip:Show()
end
end)
favToggle:HookScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
end
local countLabel = UI.CreateLabel(f, "", { size = 11, color = "textDim" })
countLabel:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -72)
countLabel:SetWidth(LIST_W)
countLabel:SetWordWrap(false)
---------------- Filter (nur Items) ----------------
local ROW, PAD = 26, 4
local function FilterH()
return (Codex.AuctionProvider and 9 or 8) * (ROW + PAD) - PAD
end
local FILTER_H = FilterH()
local fp = CreateFrame("Frame", nil, f)
fp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -92)
fp:SetSize(LIST_W, FILTER_H)
fp:Hide()
local syncing = false
local qualityButtons, dropdowns, numBoxes, opButtons = {}, {}, {}, {}
local previewLabel, ahFilter
local function RowY(row) return -(row * (ROW + PAD)) end
local qw = math.floor((LIST_W - 5 * PAD) / 6)
for q = 0, 5 do local q = q
local b = UI.CreateButton(fp, Colored(L["CODEX_QUAL_" .. q], q), qw, ROW - 2, function()
if flt.q[q] then flt.q[q] = nil else flt.q[q] = true end
UpdateFilterUI()
RefreshList()
end, "default")
b:SetPoint("TOPLEFT", fp, "TOPLEFT", q * (qw + PAD), RowY(0))
qualityButtons[q] = b
end
local function InfoValues(getter, pred)
local seen, list = {}, {}
for id in pairs((ItemIndex())) do
local info = Util.GetItem(id)
if info and (not pred or pred(info)) then
local v = getter(info)
if v and v ~= "" and not seen[v] then
seen[v] = true
list[#list + 1] = v
end
end
end
table.sort(list, function(a, b) return tostring(a) < tostring(b) end)
return list
end
local SRC_KEYS = { "unit", "object", "reward", "quest", "vendor" }
local function SrcLabel(v) return L["CODEX_SRC_" .. v:upper()] or v end
local function Zones()
local seen, list = {}, {}
for _, facts in pairs((ItemIndex())) do
for m in pairs(facts.zones) do
if not seen[m] then seen[m] = true; list[#list + 1] = m end
end
end
table.sort(list, function(a, b) return Codex.MapName(a) < Codex.MapName(b) end)
return list
end
-- Liste von Werten in Dropdown-Eintraege umwandeln
local function AsOptions(values, display)
local list = {}
for _, v in ipairs(values) do
list[#list + 1] = { value = v, text = display and display(v) or tostring(v) }
end
return list
end
local function Prefix(key) return (L[key]:gsub(":%s*%%s%s*$", "")) end
local third = math.floor((LIST_W - 2 * PAD) / 3)
local function MakeDrop(x, row, w, prefix, getValue, setValue, optionsFn, display)
local dd = UI.CreateDropdown(fp, w, ROW - 2, {
prefix = prefix, allText = L.CODEX_FILTER_ALL,
getOptions = function() return AsOptions(optionsFn(), display) end,
textOf = function(v) return display and display(v) or tostring(v) end,
onChange = function(v)
setValue(v)
UpdateFilterUI()
RefreshList()
end,
})
dd:SetPoint("TOPLEFT", fp, "TOPLEFT", x, RowY(row))
dropdowns[#dropdowns + 1] = { dd = dd, get = getValue }
return dd
end
local function FieldDrop(col, row, labelKey, field, optionsFn, display, onSet)
return MakeDrop(col * (third + PAD), row, third, Prefix(labelKey),
function() return flt[field] end,
function(v) flt[field] = v; if onSet then onSet() end end, optionsFn, display)
end
FieldDrop(0, 1, "CODEX_FILTER_TYPE", "type", function() return InfoValues(function(i) return i.type end) end, tostring,
function() flt.sub = nil end)
FieldDrop(1, 1, "CODEX_FILTER_SUB", "sub", function()
return InfoValues(function(i) return i.subType end, function(i) return not flt.type or i.type == flt.type end)
end, tostring)
FieldDrop(2, 1, "CODEX_FILTER_SLOT", "slot", function() return InfoValues(function(i) return i.equipLoc end) end,
function(v) return _G[v] or v end)
FieldDrop(0, 2, "CODEX_FILTER_SRC", "src", function() return SRC_KEYS end, SrcLabel)
FieldDrop(1, 2, "CODEX_FILTER_ZONE", "zone", Zones, Codex.MapName)
local resetButton = UI.CreateButton(fp, L.CODEX_FILTER_RESET, third, ROW - 2, function()
flt = { q = {} }
UpdateFilterUI()
RefreshList()
end, "default")
resetButton:SetPoint("TOPLEFT", fp, "TOPLEFT", 2 * (third + PAD), RowY(2))
local NUM_W = 38
local function MakeNum(x, row, getValue, setValue)
local box = UI.CreatePanel(fp, "bgDeep", "bronze")
box:SetSize(NUM_W, ROW - 2)
box:SetPoint("TOPLEFT", fp, "TOPLEFT", x, RowY(row))
local e = CreateFrame("EditBox", nil, box)
e:SetAutoFocus(false)
e:SetPoint("TOPLEFT", box, "TOPLEFT", 5, -2)
e:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -5, 2)
e:SetMaxLetters(7)
if not e:SetFont(UI.GetFontPath("body"), 11, "") then
e:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
end
e:SetTextColor(UI.Color("text"))
e:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
e:SetScript("OnTextChanged", function(self)
if syncing then return end
setValue(tonumber(((self:GetText() or ""):gsub(",", "."))))
UpdateFilterUI()
RefreshList()
end)
numBoxes[#numBoxes + 1] = { e = e, box = box, get = getValue }
end
local function FieldNum(x, row, field)
MakeNum(x, row, function() return flt[field] end, function(v) flt[field] = v end)
end
local function NumLabel(x, row, text, w)
local l = UI.CreateLabel(fp, text, { size = 10, color = "textDim" })
l:SetPoint("TOPLEFT", fp, "TOPLEFT", x, RowY(row) - 7)
l:SetWidth(w)
l:SetWordWrap(false)
end
-- Zeile 3: Itemstufe, Benoetigt, Preis, je mit Min und Max
local LBL_W = math.max(44, math.min(92, third - 2 * NUM_W - 8)) -- Beschriftung bekommt den Platz, der neben den zwei Feldern bleibt
local groupsDef = { { "CODEX_FILTER_ILVL", "ilMin", "ilMax" }, { "CODEX_FILTER_RLVL", "rlMin", "rlMax" },
{ "CODEX_FILTER_PRICE", "pMin", "pMax" } }
for i, g in ipairs(groupsDef) do
local x0 = (i - 1) * (third + PAD)
NumLabel(x0, 3, L[g[1]], LBL_W)
FieldNum(x0 + LBL_W, 3, g[2])
FieldNum(x0 + LBL_W + NUM_W + 2, 3, g[3])
end
-- Zeilen 4 bis 6: bis zu drei Werte, dahinter UND/ODER fuer die naechste Zeile
local OP_W = 62
local statDropW = LIST_W - 2 * NUM_W - OP_W - 3 * PAD
local function StatRow(i)
flt.stats = flt.stats or {}
flt.stats[i] = flt.stats[i] or {}
return flt.stats[i]
end
local function StatOptions()
local _, ids = ItemIndex()
Codex:CaptureStats(ids)
return Codex:StatKeys(ids)
end
for i = 1, MAX_STAT_ROWS do local i = i
local row = 3 + i
MakeDrop(0, row, statDropW, Prefix("CODEX_FILTER_STAT") .. " " .. i,
function() return flt.stats and flt.stats[i] and flt.stats[i].key end,
function(v)
local r = StatRow(i)
r.key = v
if v == nil then r.min, r.max = nil, nil end
end, StatOptions, Codex.StatName)
MakeNum(statDropW + PAD, row,
function() return flt.stats and flt.stats[i] and flt.stats[i].min end,
function(v) StatRow(i).min = v end)
MakeNum(statDropW + PAD + NUM_W + PAD, row,
function() return flt.stats and flt.stats[i] and flt.stats[i].max end,
function(v) StatRow(i).max = v end)
if i < MAX_STAT_ROWS then
local b = UI.CreateButton(fp, "", OP_W, ROW - 2, function()
flt.ops = flt.ops or {}
flt.ops[i] = (flt.ops[i] == "or") and "and" or "or"
UpdateFilterUI()
RefreshList()
end, "default")
b:SetPoint("TOPLEFT", fp, "TOPLEFT", statDropW + 2 * PAD + 2 * NUM_W + PAD, RowY(row))
opButtons[i] = b
end
end
previewLabel = UI.CreateLabel(fp, "", { size = 10, color = "textDim" })
previewLabel:SetPoint("TOPLEFT", fp, "TOPLEFT", 2, RowY(7) - 4)
-- Zeile 8: Auktionspreis (nur mit Auktions-Modul)
ahFilter = {}
ahFilter.label = UI.CreateLabel(fp, L.CODEX_FILTER_AH, { size = 10, color = "textDim" })
ahFilter.label:SetPoint("TOPLEFT", fp, "TOPLEFT", 0, RowY(8) - 7)
ahFilter.label:SetWidth(LBL_W + 20)
ahFilter.label:SetWordWrap(false)
MakeNum(LBL_W + 24, 8, function() return flt.aMin end, function(v) flt.aMin = v end)
MakeNum(LBL_W + 24 + NUM_W + 2, 8, function() return flt.aMax end, function(v) flt.aMax = v end)
numBoxes[#numBoxes].ah, numBoxes[#numBoxes - 1].ah = true, true
previewLabel:SetWidth(LIST_W - 4)
previewLabel:SetWordWrap(false)
local filterButton = UI.CreateButton(f, L.CODEX_FILTER, 96, 20, function()
filterOpen = not filterOpen
Layout()
end, "default")
filterButton:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W - 96, -68)
filterButton:Hide()
-- Vorlage (Vanilla-Startdaten) ein/aus und Rolle (Haendler, Flugmeister ...)
local seedButton = UI.CreateButton(f, "", 100, 20, function()
Codex.db.settings.showSeed = not SeedOn()
dataVersion = dataVersion + 1
Refresh()
end, "default")
seedButton:Hide()
-- Auswahl-Listen unter der Zaehlzeile: Rolle (Kreaturen, Objekte), Kette und Status (Quests)
local function BumpAndRefresh()
dataVersion = dataVersion + 1
Refresh()
end
local roleDD = UI.CreateDropdown(f, 186, 20, {
prefix = Prefix("CODEX_FILTER_ROLE"), allText = L.CODEX_FILTER_ALL,
getOptions = function()
local list = {}
for _, r in ipairs(Codex.Roles.ORDER[kind] or {}) do
list[#list + 1] = { value = r, text = L["CODEX_ROLE_" .. r] or r }
end
return list
end,
textOf = function(v) return L["CODEX_ROLE_" .. v] or v end,
onChange = function(v) roleSel[kind] = v; Refresh() end,
})
roleDD:Hide()
local chainDD = UI.CreateDropdown(f, 186, 20, {
prefix = Prefix("CODEX_FILTER_CHAIN"), allText = L.CODEX_FILTER_ALL,
getOptions = function()
local list = {}
for _, c in ipairs(CHAIN_STEPS) do list[#list + 1] = { value = c, text = L["CODEX_CHAIN_" .. c:upper()] } end
return list
end,
textOf = function(v) return L["CODEX_CHAIN_" .. v:upper()] end,
onChange = function(v) chainSel = v; BumpAndRefresh() end,
})
chainDD:Hide()
local statusDD = UI.CreateDropdown(f, 186, 20, {
prefix = Prefix("CODEX_FILTER_STATUS"), allText = L.CODEX_FILTER_ALL,
getOptions = function()
local list = {}
for _, c in ipairs(STATUS_STEPS) do list[#list + 1] = { value = c, text = L["CODEX_STATUS_" .. c:upper()] } end
return list
end,
textOf = function(v) return L["CODEX_STATUS_" .. v:upper()] end,
onChange = function(v) statusSel = v; BumpAndRefresh() end,
})
statusDD:Hide()
local ZONE_STEPS = { "world", "dungeon", "fit", "data" }
local zoneDD = UI.CreateDropdown(f, 186, 20, {
prefix = Prefix("CODEX_FILTER_ZONE"), allText = L.CODEX_FILTER_ALL,
getOptions = function()
local list = {}
for _, c in ipairs(ZONE_STEPS) do list[#list + 1] = { value = c, text = L["CODEX_ZONEF_" .. c:upper()] } end
return list
end,
textOf = function(v) return L["CODEX_ZONEF_" .. v:upper()] end,
onChange = function(v) zoneSel = v; BumpAndRefresh() end,
})
zoneDD:Hide()
-- Klasse des Lernplans: die eigene, sonst die erste mit Daten
local function EnsureLearnSel()
local classes = Codex.LearnClasses()
local have = {}
for _, t in ipairs(classes) do have[t] = true end
if not (learnSel and have[learnSel]) then
local mine = Codex.Time and Codex.Time.PlayerToken()
learnSel = (mine and have[mine]) and mine or classes[1]
end
return learnSel
end
local learnDD = UI.CreateDropdown(f, 186, 20, {
prefix = Prefix("CODEX_LEARN_CLASS"),
getOptions = function()
local list = {}
for _, t in ipairs(Codex.LearnClasses()) do
list[#list + 1] = { value = t, text = Codex.Time.ClassLabel(t) }
end
return list
end,
textOf = function(v) return Codex.Time.ClassLabel(v) end,
onChange = function(v) learnSel = v; selected = nil; BumpAndRefresh() end,
})
learnDD:Hide()
-- Routen-Planer: Stufenbereich (leer = keine Grenze), Haken = +/- 3 Stufen
local routeOpen, routeSyncing = false, false
local routeLvl = {}
local ROUTE_H = 62
local rp = CreateFrame("Frame", nil, f)
rp:SetSize(LIST_W, ROUTE_H)
rp:Hide()
local routeBoxes = {}
local routeCheck
local function RouteBox(x, getValue, setValue)
local box = UI.CreatePanel(rp, "bgDeep", "bronze")
box:SetSize(38, 22)
box:SetPoint("TOPLEFT", rp, "TOPLEFT", x, -34)
local e = CreateFrame("EditBox", nil, box)
e:SetAutoFocus(false)
e:SetPoint("TOPLEFT", box, "TOPLEFT", 5, -2)
e:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -5, 2)
e:SetMaxLetters(3)
if not e:SetFont(UI.GetFontPath("body"), 11, "") then e:SetFont("Fonts\\FRIZQT__.TTF", 11, "") end
e:SetTextColor(UI.Color("text"))
e:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
e:SetScript("OnTextChanged", function(self)
if routeSyncing then return end
local v = tonumber(self:GetText() or "")
setValue(v and math.max(0, math.floor(v)) or nil)
if routeCheck then routeCheck:Refresh() end
end)
routeBoxes[#routeBoxes + 1] = { e = e, get = getValue }
end
local function RouteSync()
routeSyncing = true
for _, b in ipairs(routeBoxes) do
local v = b.get()
local text = v ~= nil and tostring(v) or ""
if b.e:GetText() ~= text and not b.e:HasFocus() then b.e:SetText(text) end
end
routeSyncing = false
if routeCheck then routeCheck:Refresh() end
end
routeCheck = UI.CreateCheckbox(rp, L.CODEX_ROUTE_RANGE, function()
return routeLvl.below == 3 and routeLvl.above == 3
end, function(on)
if on then routeLvl.below, routeLvl.above = 3, 3 else routeLvl.below, routeLvl.above = nil, nil end
RouteSync()
end)
routeCheck:SetPoint("TOPLEFT", rp, "TOPLEFT", 0, -2)
local lblBelow = UI.CreateLabel(rp, L.CODEX_ROUTE_BELOW, { size = 10, color = "textDim" })
lblBelow:SetPoint("TOPLEFT", rp, "TOPLEFT", 0, -41)
lblBelow:SetWidth(86)
lblBelow:SetWordWrap(false)
RouteBox(88, function() return routeLvl.below end, function(v) routeLvl.below = v end)
local lblAbove = UI.CreateLabel(rp, L.CODEX_ROUTE_ABOVE, { size = 10, color = "textDim" })
lblAbove:SetPoint("TOPLEFT", rp, "TOPLEFT", 140, -41)
lblAbove:SetWidth(86)
lblAbove:SetWordWrap(false)
RouteBox(228, function() return routeLvl.above end, function(v) routeLvl.above = v end)
local routeGo = UI.CreateButton(rp, L.CODEX_ROUTE_GO, 140, 22, function()
local ids = {}
for _, e in ipairs(f.Entries()) do ids[#ids + 1] = e.id end
Codex.Map:StartRoute(ids, (routeLvl.below or routeLvl.above) and { below = routeLvl.below, above = routeLvl.above } or nil)
end, "primary")
routeGo:SetPoint("TOPLEFT", rp, "TOPLEFT", 280, -33)
local routeButton = UI.CreateButton(f, L.CODEX_ROUTE, 96, 20, function()
routeOpen = not routeOpen
RouteSync()
Layout()
end, "default")
if routeButton.HookScript then
routeButton:HookScript("OnEnter", function(self)
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(L.CODEX_ROUTE_TIP, nil, nil, nil, nil, true)
GameTooltip:Show()
end
end)
routeButton:HookScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
end
routeButton:Hide()
function UpdateFilterUI()
for q, b in pairs(qualityButtons) do
b:SetStyle(flt.q[q] and "primary" or "default")
end
for _, d in ipairs(dropdowns) do
d.dd:SetValue(d.get())
end
syncing = true
for _, n in ipairs(numBoxes) do
local v = n.get()
local text = v ~= nil and tostring(v) or ""
if n.e:GetText() ~= text and not n.e:HasFocus() then
n.e:SetText(text)
end
end
syncing = false
for i, b in pairs(opButtons) do
local isOr = flt.ops and flt.ops[i] == "or"
b:SetLabel(isOr and L.CODEX_OP_OR or L.CODEX_OP_AND)
b:SetStyle(isOr and "primary" or "default")
end
previewLabel:SetText(StatPreview(flt))
local n = FilterActive(flt)
filterButton:SetLabel(n > 0 and string.format(L.CODEX_FILTER_N, n) or L.CODEX_FILTER)
filterButton:SetStyle(n > 0 and "primary" or "default")
end
local farmButton = UI.CreateButton(f, L.CODEX_FARM, 120, 20, function()
farmOn = not farmOn
if farmOn then
roleSel.units = "combat"
sortBy.units = "gpm"
sortRev.units = false
else
roleSel.units = nil
sortBy.units = nil
end
Refresh()
end, "default")
farmButton:Hide()
local backButton = UI.CreateButton(f, L.CODEX_BACK, 100, 26, function()
local prev = table.remove(history)
if prev then
kind, selected = prev.kind, prev.id
edit:SetText("")
edit:ClearFocus()
Refresh()
end
end, "default")
backButton:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, 0)
backButton:Hide()
local listScroll = UI.CreateScrollFrame(f)
listScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -92)
listScroll:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
listScroll:SetWidth(LIST_W)
function Layout()
local isItems = kind == "items"
local open = isItems and filterOpen
if isItems then filterButton:Show() else filterButton:Hide() end
if open then fp:Show() else fp:Hide() end
-- rechts neben dem Zaehler: [Vorlage] [Filter]
local right = LIST_W - (isItems and 100 or 0)
local hasSeed = SeedOK() and kind ~= "xp" and kind ~= "learn"
if hasSeed then
right = right - 100
seedButton:ClearAllPoints()
seedButton:SetPoint("TOPLEFT", f, "TOPLEFT", right, -68)
seedButton:SetLabel(SeedOn() and L.CODEX_SEED_ON or L.CODEX_SEED_OFF)
seedButton:SetStyle(SeedOn() and "primary" or "default")
seedButton:Show()
else
seedButton:Hide()
end
countLabel:SetWidth(math.max(60, right - 6))
-- eigene Zeile fuer die Auswahl-Listen: Rolle, Kette, Status
local extra = false
local hasRole = Codex.Roles.ORDER[kind] ~= nil
if hasRole then
roleDD:ClearAllPoints()
roleDD:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -94)
roleDD:SetValue(roleSel[kind])
roleDD:Show()
extra = true
else
roleDD:Hide()
end
if kind == "units" then
farmButton:ClearAllPoints()
farmButton:SetPoint("TOPLEFT", f, "TOPLEFT", 192, -94)
farmButton:SetStyle(farmOn and "primary" or "default")
farmButton:Show()
else
farmButton:Hide()
end
local isQuests = kind == "quests"
local x = 0
if isQuests and hasSeed then
chainDD:ClearAllPoints()
chainDD:SetPoint("TOPLEFT", f, "TOPLEFT", x, -94)
chainDD:SetValue(chainSel)
chainDD:Show()
x = x + 190
extra = true
else
chainDD:Hide()
end
if isQuests then
statusDD:ClearAllPoints()
statusDD:SetPoint("TOPLEFT", f, "TOPLEFT", x, -94)
statusDD:SetValue(statusSel)
statusDD:Show()
routeButton:ClearAllPoints()
routeButton:SetPoint("TOPLEFT", f, "TOPLEFT", x + 190, -94)
routeButton:Show()
extra = true
else
statusDD:Hide()
routeButton:Hide()
end
if kind == "zones" then
zoneDD:ClearAllPoints()
zoneDD:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -94)
zoneDD:SetValue(zoneSel)
zoneDD:Show()
extra = true
else
zoneDD:Hide()
end
if Codex.FavSupported(kind) then
favToggle:Show()
favToggle:SetStyle(favOnly[kind] and "primary" or "default")
else
favToggle:Hide()
end
if kind == "learn" and EnsureLearnSel() then
learnDD:ClearAllPoints()
learnDD:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -94)
learnDD:SetValue(learnSel)
learnDD:Show()
extra = true
else
learnDD:Hide()
end
local top = 92 + (extra and 28 or 0)
local showRoute = kind == "quests" and routeOpen
if showRoute then
rp:ClearAllPoints()
rp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -top)
rp:Show()
else
rp:Hide()
end
routeButton:SetStyle(showRoute and "primary" or "default")
if showRoute then top = top + ROUTE_H + 6 end
listScroll:ClearAllPoints()
FILTER_H = FilterH()
fp:SetHeight(FILTER_H)
if ahFilter then ahFilter.label:SetShown(Codex.AuctionProvider ~= nil) end
for _, nb in ipairs(numBoxes) do
if nb.ah then nb.box:SetShown(Codex.AuctionProvider ~= nil) end
end
listScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -(top + (open and (FILTER_H + 6) or 0)))
listScroll:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
listScroll:SetWidth(LIST_W)
end
local listEmpty = UI.CreateLabel(listScroll.child, "", { size = 12, color = "textDim", justifyV = "TOP" })
listEmpty:SetPoint("TOPLEFT", listScroll.child, "TOPLEFT", 4, -6)
listEmpty:SetPoint("TOPRIGHT", listScroll.child, "TOPRIGHT", -4, -6)
listEmpty:SetWordWrap(true)
local separator = f:CreateTexture(nil, "ARTWORK")
separator:SetColorTexture(UI.Color("bronze", 0.5))
separator:SetWidth(1)
separator:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 10, 0)
separator:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", LIST_W + 10, 0)
---------------- rechts: Detail ----------------
local detail = UI.CreateScrollFrame(f)
detail:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -34)
detail:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
local detailEmpty = UI.CreateLabel(detail.child, L.CODEX_SELECT, { size = 12, color = "textDim" })
detailEmpty:SetPoint("TOPLEFT", detail.child, "TOPLEFT", 4, -6)
-- Namen von Hand eintragen (Deutsch / Englisch)
local UpdateFavUIRef
if Codex.NameUI then
nameCtl = Codex.NameUI.Attach(f, function() return kind, selected end, function()
dataVersion = dataVersion + 1
Refresh()
end, { x = LIST_W + 22, y = 34, relayout = function() if UpdateFavUIRef then UpdateFavUIRef() end end })
end
---------------- Favorit und Notiz ----------------
local favButton = UI.CreateButton(f, L.CODEX_FAV_ADD, 140, 26, function()
if selected ~= nil and Codex.FavSupported(kind) then
Codex.SetFav(kind, selected, not Codex.IsFav(kind, selected))
dataVersion = dataVersion + 1
Refresh()
end
end, "default")
favButton:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22 + 108, 0)
favButton:Hide()
local noteBox = UI.CreatePanel(f, "bgDeep", "bronze")
noteBox:SetHeight(24)
noteBox:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -34)
noteBox:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -34)
noteBox:Hide()
local noteEdit = CreateFrame("EditBox", nil, noteBox)
noteEdit:SetAutoFocus(false)
noteEdit:SetPoint("TOPLEFT", noteBox, "TOPLEFT", 8, -2)
noteEdit:SetPoint("BOTTOMRIGHT", noteBox, "BOTTOMRIGHT", -8, 2)
noteEdit:SetMaxLetters(200)
if not noteEdit:SetFont(UI.GetFontPath("body"), 12, "") then
noteEdit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
end
noteEdit:SetTextColor(UI.Color("text"))
local noteHint = UI.CreateLabel(noteBox, L.CODEX_NOTE_HINT, { size = 11, color = "textDim" })
noteHint:SetPoint("LEFT", noteBox, "LEFT", 10, 0)
local noteTarget = nil
local function SaveNote()
if noteTarget then Codex.SetNote(noteTarget.kind, noteTarget.id, noteEdit:GetText()) end
end
noteEdit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
noteEdit:SetScript("OnEnterPressed", function(self) SaveNote(); self:ClearFocus() end)
noteEdit:SetScript("OnEditFocusLost", function() SaveNote() end)
noteEdit:SetScript("OnTextChanged", function(self)
if (self:GetText() or "") == "" then noteHint:Show() else noteHint:Hide() end
end)
-- Dev-Notiz (nur mit dem Modul Freewood-OctoWoW-Dev und Bearbeitungsrecht): lokal, wird nicht geteilt
local devNoteBox = UI.CreatePanel(f, "bgDeep", "gold")
devNoteBox:SetHeight(24)
devNoteBox:Hide()
local devNoteEdit = CreateFrame("EditBox", nil, devNoteBox)
devNoteEdit:SetAutoFocus(false)
devNoteEdit:SetPoint("TOPLEFT", devNoteBox, "TOPLEFT", 8, -2)
devNoteEdit:SetPoint("BOTTOMRIGHT", devNoteBox, "BOTTOMRIGHT", -8, 2)
devNoteEdit:SetMaxLetters(500)
if not devNoteEdit:SetFont(UI.GetFontPath("body"), 12, "") then
devNoteEdit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
end
devNoteEdit:SetTextColor(UI.Color("text"))
local devNoteHint = UI.CreateLabel(devNoteBox, L.CODEX_DEVNOTE_HINT, { size = 11, color = "textDim" })
devNoteHint:SetPoint("LEFT", devNoteBox, "LEFT", 10, 0)
local devNoteTarget = nil
local function SaveDevNote()
if devNoteTarget then Codex.SetDevNote(devNoteTarget.kind, devNoteTarget.id, devNoteEdit:GetText()) end
end
devNoteEdit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
devNoteEdit:SetScript("OnEnterPressed", function(self) SaveDevNote(); self:ClearFocus() end)
devNoteEdit:SetScript("OnEditFocusLost", function() SaveDevNote() end)
devNoteEdit:SetScript("OnTextChanged", function(self)
if (self:GetText() or "") == "" then devNoteHint:Show() else devNoteHint:Hide() end
end)
local function UpdateFavUI()
local ok = selected ~= nil and Codex.FavSupported(kind)
local same = noteTarget and noteTarget.kind == kind and noteTarget.id == selected
if noteEdit:HasFocus() then
if same and ok and Codex.IsFav(kind, selected) then return end
SaveNote()
noteEdit:ClearFocus()
end
noteTarget = nil
local devH = nameCtl and nameCtl.height or 0 -- Reihe mit den Bearbeiten-Knoepfen (nur fuer Berechtigte)
local top = 34 + devH
noteBox:ClearAllPoints()
noteBox:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -top)
noteBox:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -top)
if ok then
local fav = Codex.IsFav(kind, selected)
favButton:Show()
favButton:SetLabel(fav and L.CODEX_FAV_REMOVE or L.CODEX_FAV_ADD)
favButton:SetStyle(fav and "primary" or "default")
if fav then
noteTarget = { kind = kind, id = selected }
local e = Codex.GetFav(kind, selected)
noteEdit:SetText(e and e.note or "")
noteBox:Show()
top = top + 30
else
noteBox:Hide()
end
else
favButton:Hide()
noteBox:Hide()
end
-- Dev-Notiz
if devNoteEdit:HasFocus() then SaveDevNote() devNoteEdit:ClearFocus() end
devNoteTarget = nil
if selected ~= nil and Codex.DevNoteSupported(kind) and Codex.CanDevNote() then
devNoteTarget = { kind = kind, id = selected }
devNoteBox:ClearAllPoints()
devNoteBox:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -top)
devNoteBox:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -top)
devNoteEdit:SetText(Codex.GetDevNote(kind, selected) or "")
devNoteBox:Show()
top = top + 30
else
devNoteBox:Hide()
end
detail:ClearAllPoints()
detail:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -top)
detail:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
end
UpdateFavUIRef = UpdateFavUI
-- Bild (Kreatur: 3D-Modell mit Symbol als Rueckfall, Objekt: Symbol)
local portrait = UI.CreatePanel(detail.child, "bgDeep", "bronze")
portrait:SetSize(64, 64)
portrait:SetPoint("TOPLEFT", detail.child, "TOPLEFT", 0, 0)
portrait:Hide()
local portraitIcon = portrait:CreateTexture(nil, "ARTWORK")
portraitIcon:SetPoint("TOPLEFT", portrait, "TOPLEFT", 6, -6)
portraitIcon:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", -6, 6)
portraitIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
-- Modell haengt am Hauptfenster (nicht am Scroll-Kind), sonst landet es an falscher Stelle
local model = CreateFrame("PlayerModel", nil, f)
model:SetAlpha(0)
model:Hide()
local function PlaceModel()
model:ClearAllPoints()
model:SetPoint("TOPLEFT", portrait, "TOPLEFT", 2, -2)
model:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", -2, 2)
model:SetFrameLevel((detail.scroll and detail.scroll:GetFrameLevel() or 1) + 6)
local sc = detail.scroll and detail.scroll:GetVerticalScroll() or 0
if portrait:IsShown() and model.fwNPC and sc <= 2 then
model:Show()
else
model:Hide()
end
end
if detail.scroll and detail.scroll.HookScript then
detail.scroll:HookScript("OnVerticalScroll", function() PlaceModel() end)
end
pcall(model.SetScript, model, "OnModelLoaded", function(self)
self:SetAlpha(1)
portraitIcon:Hide()
if self.fwNPC then
local ok, d = pcall(function() return self.GetDisplayInfo and self:GetDisplayInfo() end)
if ok and type(d) == "number" and Codex:SetDisplayID(self.fwNPC, d) then
if f.UpdateRowModels then f.UpdateRowModels() end
end
end
end)
local function ShowPortrait(iconPath, npcID)
portrait:Show()
portraitIcon:SetTexture(iconPath)
portraitIcon:Show()
model:SetAlpha(0)
model.fwNPC = npcID
PlaceModel()
if npcID then
local function Check()
if model.fwNPC ~= npcID then return end
local fileID = model.GetModelFileID and model:GetModelFileID()
local ok, d = pcall(function() return model.GetDisplayInfo and model:GetDisplayInfo() end)
if model.fwShown or (type(fileID) == "number" and fileID > 0) or (ok and type(d) == "number" and d > 0) then
model:SetAlpha(1)
portraitIcon:Hide()
if ok and type(d) == "number" and Codex:SetDisplayID(npcID, d) and f.UpdateRowModels then
f.UpdateRowModels()
end
end
end
Codex.EnsureCreature(npcID, function()
if model.fwNPC ~= npcID then return end
-- ClassicAPI zieht "humanoide" Anzeigen (mit Extra-Eintrag) ueber einen Ankleide-Auftrag an.
-- Dessen Aufraeumen beim /reload stuerzt das Spiel ab. Nur Typen ohne solche Modelle zeigen.
local okI, info = pcall(function() return C_CreatureInfo.GetCreatureInfoByID(npcID) end)
local ctype = okI and type(info) == "table" and tonumber(info.type) or nil
if not (ctype and SAFE_MODEL_TYPE[ctype]) then return end
local cd = Codex.CacheDisplayID(npcID)
if cd and Codex:SetDisplayID(npcID, cd) and f.UpdateRowModels then
f.UpdateRowModels()
end
model.fwShown = nil
pcall(function()
model:ClearModel()
if cd and model.SetDisplayInfo then
model:SetDisplayInfo(cd)
model.fwShown = true
else
model:SetCreature(npcID)
end
if model.SetPortraitZoom then
model:SetPortraitZoom(1)
end
end)
Check()
C_Timer.After(0.4, Check)
C_Timer.After(0.1, function()
if model.fwNPC == npcID and model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, 1) end
end)
end)
end
end
local D = { y = 0, used = 0 }
local function NewDetailRow()
local row = CreateFrame("Button", nil, detail.child)
row.bg = row:CreateTexture(nil, "BACKGROUND")
row.bg:SetAllPoints()
row.bg:SetColorTexture(UI.Color("hover", 0))
row.icon = row:CreateTexture(nil, "ARTWORK")
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.left = UI.CreateLabel(row, "", { size = 12 })
row.right = UI.CreateLabel(row, "", { size = 11, color = "textDim", justify = "RIGHT" })
row.line = row:CreateTexture(nil, "ARTWORK")
row.line:SetColorTexture(UI.Color("bronze", 0.6))
row.line:SetHeight(1)
row:SetScript("OnEnter", function(self)
if self.clickable then
self.bg:SetColorTexture(UI.Color("hover", 0.8))
end
if self.itemID and GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
pcall(GameTooltip.SetItemByID, GameTooltip, self.itemID)
GameTooltip:Show()
elseif self.spellID and GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
local ok = pcall(GameTooltip.SetSpellByID, GameTooltip, self.spellID)
if not ok then pcall(GameTooltip.SetHyperlink, GameTooltip, "spell:" .. self.spellID) end
GameTooltip:Show()
end
end)
row:SetScript("OnLeave", function(self)
self.bg:SetColorTexture(UI.Color("hover", 0))
if GameTooltip then GameTooltip:Hide() end
end)
row:SetScript("OnClick", function(self)
if self.onClick then
self.onClick()
end
end)
return row
end
-- Verlaufsdiagramm: history = Liste { day, avg, min }, neuester zuerst
local chart
local CHART_H = 84
function D:Chart(history)
if not history or #history < 2 then
if chart then chart:Hide() end
return
end
if not chart then
chart = CreateFrame("Frame", nil, detail.child)
chart.segs, chart.dots = {}, {}
chart.bg = chart:CreateTexture(nil, "BACKGROUND")
chart.bg:SetAllPoints()
chart.bg:SetColorTexture(UI.Color("bgDeep", 0.6))
chart.hi = UI.CreateLabel(chart, "", { size = 10, color = "textDim", justify = "RIGHT" })
chart.lo = UI.CreateLabel(chart, "", { size = 10, color = "textDim", justify = "RIGHT" })
chart.from = UI.CreateLabel(chart, "", { size = 10, color = "textDim" })
chart.to = UI.CreateLabel(chart, "", { size = 10, color = "textDim", justify = "RIGHT" })
end
local w = detail.child:GetWidth()
if not w or w < 120 then w = 320 end
local LABEL_W, PADX, PADY = 78, 6, 8
local pw, ph = w - LABEL_W - PADX * 2, CHART_H - PADY * 2 - 12
local pts = {}
local lo, hi = math.huge, 0
for i = #history, 1, -1 do
local v = history[i].avg or history[i].min or 0
pts[#pts + 1] = { day = history[i].day, v = v }
if v < lo then lo = v end
if v > hi then hi = v end
end
if hi == lo then lo = lo * 0.9; hi = hi * 1.1 + 1 end
local n = #pts
local firstDay, lastDay = pts[1].day, pts[n].day
local span = math.max(1, lastDay - firstDay)
for i, p in ipairs(pts) do
p.x = PADX + (p.day - firstDay) / span * pw
p.y = PADY + (1 - (p.v - lo) / (hi - lo)) * ph
end
chart:ClearAllPoints()
chart:SetPoint("TOPLEFT", detail.child, "TOPLEFT", 0, -(self.y + 6))
chart:SetSize(w, CHART_H)
local function Tex(list, k)
local t = list[k]
if not t then
t = chart:CreateTexture(nil, "ARTWORK")
list[k] = t
end
return t
end
for i = 1, n - 1 do
local a, b = pts[i], pts[i + 1]
local dx, dy = b.x - a.x, b.y - a.y
local t = Tex(chart.segs, i)
t:SetColorTexture(1, 0.82, 0, 0.9)
t:ClearAllPoints()
t:SetSize(math.max(1, math.sqrt(dx * dx + dy * dy)), 2)
t:SetPoint("CENTER", chart, "TOPLEFT", (a.x + b.x) / 2, -(a.y + b.y) / 2)
if t.SetRotation then t:SetRotation(math.atan2 and math.atan2(-dy, dx) or math.atan(-dy, dx)) end
t:Show()
end
for i = n, #chart.segs do if chart.segs[i] then chart.segs[i]:Hide() end end
for i, p in ipairs(pts) do
local t = Tex(chart.dots, i)
t:SetColorTexture(1, 0.92, 0.4, 1)
t:SetSize(5, 5)
t:ClearAllPoints()
t:SetPoint("CENTER", chart, "TOPLEFT", p.x, -p.y)
t:Show()
end
for i = n + 1, #chart.dots do chart.dots[i]:Hide() end
chart.hi:ClearAllPoints(); chart.hi:SetPoint("TOPRIGHT", chart, "TOPRIGHT", -4, -4); chart.hi:SetWidth(LABEL_W)
chart.lo:ClearAllPoints(); chart.lo:SetPoint("BOTTOMRIGHT", chart, "BOTTOMRIGHT", -4, 16); chart.lo:SetWidth(LABEL_W)
chart.hi:SetText(FormatMoney(hi)); chart.lo:SetText(FormatMoney(lo))
chart.from:ClearAllPoints(); chart.from:SetPoint("BOTTOMLEFT", chart, "BOTTOMLEFT", PADX, 2)
chart.to:ClearAllPoints(); chart.to:SetPoint("BOTTOMRIGHT", chart, "BOTTOMRIGHT", -LABEL_W - PADX, 2)
chart.from:SetText(string.format(L.CODEX_AH_DAYS, lastDay - firstDay))
chart.to:SetText(L.CODEX_AH_TODAY)
chart:Show()
self.y = self.y + 6 + CHART_H + 4
end
function D:Reset()
if chart then chart:Hide() end
portrait:Hide()
model.fwNPC = nil
model:Hide()
for _, row in ipairs(detailRows) do
row:Hide()
end
self.y, self.used = 0, 0
end
-- Hoehe eines umbrechenden Textblocks (grobe Schaetzung nach Zeichenzahl und Breite)
local function TextHeight(text)
local width = detail.child and detail.child.GetWidth and detail.child:GetWidth() or 0
if not width or width < 100 then width = detail.GetWidth and detail:GetWidth() or 0 end
if not width or width < 100 then width = 900 end
local perLine = math.max(20, math.floor((width - 40) / 6.8))
local lines = 0
text = (text or "") .. "\n"
for seg in text:gmatch("([^\n]*)\n") do
lines = lines + math.max(1, math.ceil(#seg / perLine))
end
return lines * 14 + 8
end
-- opts: style ("title","sub","heading","line","item","link","text"), text, right, icon, itemID, onClick, indent
function D:Add(opts)
self.used = self.used + 1
local row = detailRows[self.used]
if not row then
row = NewDetailRow()
detailRows[self.used] = row
end
local style = opts.style or "line"
local h = LINE_H
local top = 0
if style == "title" then h = 32 elseif style == "sub" then h = 22
elseif style == "heading" then h = 28; top = 12
elseif style == "item" or style == "link" then h = 26
elseif style == "text" then h = TextHeight(opts.text) end
if opts.spellID then h = 26 end
row:ClearAllPoints()
row:SetPoint("TOPLEFT", detail.child, "TOPLEFT", 0, -(self.y + top))
row:SetPoint("TOPRIGHT", detail.child, "TOPRIGHT", 0, -(self.y + top))
row:SetHeight(h)
self.y = self.y + top + h
local indent = opts.indent or 0
row.left:ClearAllPoints()
row.right:ClearAllPoints()
row.icon:Hide()
row.line:Hide()
row.clickable = opts.onClick ~= nil
row.onClick = opts.onClick
row.itemID = opts.itemID
row.spellID = opts.spellID
row:EnableMouse(row.clickable or opts.itemID ~= nil or opts.spellID ~= nil)
local leftX = 4 + indent
if opts.icon then
row.icon:SetSize(h - 4, h - 4)
row.icon:ClearAllPoints()
row.icon:SetPoint("LEFT", row, "LEFT", leftX, 0)
row.icon:SetTexture(opts.icon)
row.icon:Show()
leftX = leftX + h
end
row.left:SetPoint("LEFT", row, "LEFT", leftX, 0)
row.right:SetPoint("RIGHT", row, "RIGHT", -6, 0)
row.left:SetPoint("RIGHT", row, "RIGHT", opts.right and -(opts.rightWidth or 150) or -6, 0)
row.left:SetWordWrap(false)
if style == "title" then
UI.SetFont(row.left, "heading", 20)
row.left:SetTextColor(UI.Color("goldLight"))
elseif style == "sub" then
UI.SetFont(row.left, "body", 12)
row.left:SetTextColor(UI.Color("textDim"))
elseif style == "heading" then
UI.SetFont(row.left, "heading", 14)
row.left:SetTextColor(UI.Color("goldLight"))
row.line:ClearAllPoints()
row.line:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
row.line:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
row.line:Show()
else
UI.SetFont(row.left, "body", 12)
row.left:SetTextColor(UI.Color(row.clickable and "goldLight" or "text"))
end
if style == "text" then
-- umbrechender Block ueber die ganze Breite
row.left:ClearAllPoints()
row.left:SetPoint("TOPLEFT", row, "TOPLEFT", leftX, -3)
row.left:SetPoint("TOPRIGHT", row, "TOPRIGHT", -6, -3)
row.left:SetWordWrap(true)
row.left:SetJustifyH("LEFT")
row.left:SetJustifyV("TOP")
-- feste Hoehe: ohne sie schneidet der Client den Text nach den ersten Zeilen mit "..." ab
row.left:SetHeight(math.max(16, h - 4))
end
row.left:SetText(opts.text or "")
row.right:SetText(opts.right or "")
row:Show()
return row
end
function D:Finish()
detail:SetContentHeight(self.y + 16)
detailEmpty:SetShown(self.used == 0)
end
-- Zeile fuer ein Item (Symbol und Name asynchron)
local function ItemRow(itemID, right, indent, extraText)
local row = D:Add({ style = "item", text = "#" .. itemID, right = right, itemID = itemID, indent = indent,
icon = "Interface\\Icons\\INV_Misc_QuestionMark",
onClick = function() Go("items", itemID) end })
local resolved = false
Util.RequestItem(itemID, function(info)
if row.itemID ~= itemID or not info then
return
end
resolved = true
Codex.RememberQuality(itemID, info.quality)
row.icon:SetTexture(info.icon)
row.left:SetText(Colored(info.name, info.quality) .. (extraText or ""))
end)
local cached = Codex.db.names[itemID]
if cached and not resolved then
row.left:SetText(Colored(cached, Codex.QualityOf(itemID) or 1) .. (extraText or ""))
end
return row
end
local function RefRow(ref, labelKind)
if not ref then
D:Add({ text = "-" })
return
end
local tkind = ref.k == "npc" and "units" or ref.k == "object" and "objects" or nil
local refEnt = tkind and ref.id and Codex.V()[tkind][ref.id]
local text = (refEnt and Codex.NameOf(refEnt)) or ref.n or (ref.id and ("#" .. ref.id)) or "-"
D:Add({ style = tkind and ref.id and "link" or "line", text = text,
onClick = tkind and ref.id and function() Go(tkind, ref.id) end or nil,
right = ref.m and ref.x and string.format(FW.L.CODEX_POS, Codex.MapName(ref.m), (ref.x or 0) * 100, (ref.y or 0) * 100) or nil })
end
local function QuestLink(id, q, right)
local lvl = q and q.lvl
if not q and SeedOK() then
local sq = Codex.Seed.Quest(id)
lvl = sq and sq.lvl ~= 0 and sq.lvl or nil
right = right or SeedTag()
end
local title = Codex.QuestTitle(q, id)
if not q and Codex.Catalog then
local ct, cl = Codex.Catalog.Get(id)
if ct then
title = ct
lvl = lvl or cl
end
end
D:Add({ style = "link", text = title .. (lvl and (" (" .. string.format(FW.L.CODEX_LEVEL, lvl) .. ")") or ""),
right = right, onClick = function() Go("quests", id) end, icon = ICON.quests })
end
local function EntityLink(tkind, id, ent, right)
local lvl = ent and LevelText(ent)
if not lvl and tkind == "units" and SeedOK() then
lvl = LevelText(Codex.Seed.Entity(tkind, id) or {})
end
D:Add({ style = "link", text = UnitTitle(ent, id, tkind) .. (lvl and (" (" .. lvl .. ")") or ""),
right = right, onClick = function() Go(tkind, id) end, icon = ICON[tkind] })
end
-- Verweis auf eine Kreatur / ein Objekt / ein Item aus der Vorlage
local function SeedRef(ref)
if ref.kind == "items" then
ItemRow(ref.id, SeedTag(), 0)
return
end
local m, x, y = Codex.Seed.FirstSpot(ref.kind, ref.id)
local live = Codex.V()[ref.kind][ref.id]
local lvl = ref.kind == "units" and LevelText(Codex.Seed.Entity(ref.kind, ref.id) or {}) or nil
D:Add({ style = "link", text = UnitTitle(live, ref.id, ref.kind) .. (lvl and (" (" .. lvl .. ")") or "") .. " " .. SeedTag(),
icon = ICON[ref.kind], onClick = function() Go(ref.kind, ref.id) end,
right = m and string.format(FW.L.CODEX_POS, Codex.MapName(m), x * 100, y * 100) or nil })
end
---------------- Details je Art ----------------
local function LootSection(ent, alt)
local list = Codex.DropList(ent, alt)
if #list == 0 then
return
end
D:Add({ style = "heading", text = alt and L.CODEX_SEC_ALT or L.CODEX_SEC_LOOT })
local k = list[1].k
D:Add({ style = "sub", text = string.format(L.CODEX_KILLS, k) .. ((k < 10) and (" | " .. L.CODEX_LOW_SAMPLE) or "") })
for _, e in ipairs(list) do
local extra = e.qi and (" |cff9d9d9d[" .. L.CODEX_QUEST_ITEM .. "]|r") or ""
local ahp = Codex.AuctionProvider and Codex.AuctionPrice(e.id)
if ahp and ahp > 0 then
extra = extra .. " |cffffd100" .. L.CODEX_AH_SHORT .. " " .. FormatMoney(ahp) .. "|r"
end
local right = string.format(L.CODEX_DROP_CHANCE, FormatChance(e.chance), e.n, e.k)
if e.k < 10 then right = "|cff9d9d9d~ " .. right .. "|r" end
ItemRow(e.id, right, 0, extra)
end
end
local detailMissing = false
local function ValueSection(ent)
local total, coin, items, missing = Codex.KillValue(ent)
detailMissing = false
if not total then
return
end
D:Add({ style = "heading", text = L.CODEX_VALUE_HEAD })
D:Add({ text = L.CODEX_VALUE_COIN, right = FormatMoney(coin) })
D:Add({ text = L.CODEX_VALUE_ITEMS, right = FormatMoney(items) })
D:Add({ text = L.CODEX_VALUE_TOTAL, right = FormatMoney(total) })
if Codex.AuctionProvider then
local ahTotal, _, ahItems, ahMissing = Codex.KillValue(ent, true)
if ahTotal then
D:Add({ text = L.CODEX_VALUE_ITEMS_AH, right = FormatMoney(ahItems) })
D:Add({ text = L.CODEX_VALUE_TOTAL_AH, right = FormatMoney(ahTotal) })
if ahMissing > 0 then
D:Add({ style = "sub", text = string.format(L.CODEX_VALUE_MISSING, ahMissing) })
end
end
end
if missing > 0 then
detailMissing = true
D:Add({ style = "sub", text = string.format(L.CODEX_VALUE_MISSING, missing) })
end
end
local function QuestsOfUnit(tkind, id, uname)
local gives, turns, targets = {}, {}, {}
local refKind = tkind == "units" and "npc" or "object"
if SeedOK() and Codex.RepairObjectives then Codex.RepairObjectives() end
local lname = uname and uname:lower() or nil
for qid, q in pairs(Codex.V().quests) do
if q.give and q.give.k == refKind and q.give.id == id then gives[#gives + 1] = qid end
if q.turn and q.turn.k == refKind and q.turn.id == id then turns[#turns + 1] = qid end
if tkind == "units" then
for _, o in ipairs(q.obj or {}) do
if (o.ids and o.ids[id]) or (lname and o.ty == "monster" and o.name and o.name:lower() == lname) then
targets[#targets + 1] = qid
break
end
end
end
end
if SeedOK() then
-- Vorlage: Quests, die wir noch nicht mit diesem Geber / Abgabeort / Ziel kennen
local sg, st, sx = Codex.Seed.QuestsOf(tkind, id)
for _, def in ipairs({ { gives, sg }, { turns, st }, { targets, sx } }) do
local have = {}
for _, qid in ipairs(def[1]) do have[qid] = true end
for _, qid in ipairs(def[2]) do
if not have[qid] then
have[qid] = true
def[1][#def[1] + 1] = qid
end
end
end
end
if Codex.Catalog and Codex.Catalog.Reverse then
local list = Codex.Catalog.Reverse()[tkind == "units" and "monster" or "object"][id]
if list then
local have = {}
for _, qid in ipairs(targets) do have[qid] = true end
for _, qid in ipairs(list) do
if not have[qid] then
have[qid] = true
targets[#targets + 1] = qid
end
end
end
end
local function QInfo(qid)
local q = Codex.V().quests[qid]
local lvl = q and q.lvl
if not lvl and SeedOK() then
local sq = Codex.Seed.Quest(qid)
lvl = sq and sq.lvl
end
local title = Codex.QuestTitle(q, qid)
if not q and Codex.Catalog then
local ct, cl = Codex.Catalog.Get(qid)
if ct then
title = ct
lvl = lvl or cl
end
end
return lvl or 0, title
end
local function Sorted(list)
table.sort(list, function(a, b)
local la, ta = QInfo(a)
local lb, tb = QInfo(b)
if la ~= lb then return la < lb end
if ta ~= tb then return ta < tb end
return a < b
end)
return list
end
-- von Hand zugeordnet (Properties) und gelernte Quest-Item-Quellen
local manualKill, manualItems = Codex.ManualQuestLinks(tkind, id)
do
local have = {}
for _, qid in ipairs(targets) do have[qid] = true end
for _, qid in ipairs(manualKill) do
if not have[qid] then have[qid] = true targets[#targets + 1] = qid end
end
end
local items, haveItem = {}, {}
local srcKey = (tkind == "units" and "u" or "o") .. id
for qid, q in pairs(Codex.V().quests) do
for itemID, srcs in pairs(q.qi or {}) do
if srcs[srcKey] and not haveItem[qid .. ":" .. itemID] then
haveItem[qid .. ":" .. itemID] = true
items[#items + 1] = { qid = qid, item = itemID }
end
end
end
for _, s in ipairs(manualItems) do
local qs, is = tostring(s):match("^(%d+):(%d+)$")
local qid, itemID = tonumber(qs), tonumber(is)
if qid and itemID and not haveItem[qid .. ":" .. itemID] then
haveItem[qid .. ":" .. itemID] = true
items[#items + 1] = { qid = qid, item = itemID, manual = true }
end
end
if #items > 0 then
table.sort(items, function(a, b) if a.qid ~= b.qid then return a.qid < b.qid end return a.item < b.item end)
D:Add({ style = "heading", text = L.CODEX_SEC_QITEMS })
for i, e in ipairs(items) do
if i > 30 then break end
QuestLink(e.qid, Codex.V().quests[e.qid], (Codex:ItemName(e.item) or ("#" .. e.item)) .. (e.manual and (" [" .. L.CODEX_MANUAL_TAG .. "]") or ""))
end
end
local belongs = Codex.EntityQuests(tkind == "units" and "u" or "o", id)
if #belongs > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_QBEL })
for i, qid in ipairs(belongs) do
if i > 30 then break end
QuestLink(qid, Codex.V().quests[qid], L.CODEX_MANUAL_TAG)
end
end
for _, def in ipairs({ { gives, L.CODEX_SEC_GIVES }, { turns, L.CODEX_SEC_TURNS }, { targets, L.CODEX_SEC_TARGET } }) do
if #def[1] > 0 then
D:Add({ style = "heading", text = def[2] })
for i, qid in ipairs(Sorted(def[1])) do
if i > 30 then break end
QuestLink(qid, Codex.V().quests[qid])
end
end
end
end
local MAP_ICON = "Interface\\Icons\\INV_Misc_Map_01"
local function MapLink(spec, label)
D:Add({ style = "link", text = L.CODEX_MAP_SHOW, icon = MAP_ICON,
onClick = function()
Codex.Map:Show(Codex.CollectPoints(spec), label)
end })
end
local function SpotRows(cells, limit, noHits)
for _, s in ipairs(Codex.TopSpots(cells, limit or 8)) do
D:Add({ text = string.format(L.CODEX_POS, Codex.MapName(s.m), s.x * 100, s.y * 100),
right = (not noHits) and string.format(L.CODEX_SPOT_HITS, s.n) or nil })
end
end
-- Fundorte: eigene Beobachtungen, sonst die Vorlage
local function PlacesSection(ent, spec, label, seedCells)
if ent and ent.cells and next(ent.cells) ~= nil then
D:Add({ style = "heading", text = L.CODEX_SEC_SPOTS })
SpotRows(ent.cells, 8)
elseif seedCells and next(seedCells) ~= nil then
D:Add({ style = "heading", text = L.CODEX_SEC_SPOTS .. " " .. SeedTag() })
SpotRows(seedCells, 8, true)
else
return
end
if spec then
MapLink(spec, label)
end
end
local function ShowQuest(id)
local live = Codex.V().quests[id]
local sq = SeedOK() and Codex.Seed.Quest(id) or nil
local catTitle, catLvl
if not live and not sq and Codex.Catalog then catTitle, catLvl = Codex.Catalog.Get(id) end
if not live and not sq and not catTitle then return end
local q = live or {}
D:Add({ style = "title", text = catTitle or Codex.QuestTitle(live, id) })
local sub = {}
local lvl = q.lvl or (sq and sq.lvl and sq.lvl > 0 and sq.lvl) or catLvl or nil
if lvl then sub[#sub + 1] = string.format(L.CODEX_LEVEL, lvl) end
if sq and sq.min and sq.min > 0 then sub[#sub + 1] = string.format(L.CODEX_MINLEVEL, sq.min) end
if q.zone then
sub[#sub + 1] = Codex.MapName(q.zone)
elseif sq then
local ref = sq.start[1] or sq.stop[1]
local m = ref and ref.kind ~= "items" and Codex.Seed.MapOf(ref.kind, ref.id)
if m then sub[#sub + 1] = Codex.MapName(m) end
end
local wm = Codex.Catalog and Codex.Catalog.QuestMeta and Codex.Catalog.QuestMeta(id)
if wm then
if wm.zone and not q.zone and not (sq and sq.start[1]) then sub[#sub + 1] = wm.zone end
if wm.info then sub[#sub + 1] = wm.info end
end
if q.dg ~= nil then sub[#sub + 1] = q.dg and L.CODEX_PROPS_DG_YES or L.CODEX_PROPS_DG_NO end
if (q.seen or 0) > 0 then sub[#sub + 1] = string.format(L.CODEX_SEEN, q.seen) end
if (q.done or 0) > 0 then sub[#sub + 1] = string.format(L.CODEX_DONE, q.done) end
local det0 = Codex.Catalog and Codex.Catalog.Details and Codex.Catalog.Details(id)
if det0 and det0.qtype then sub[#sub + 1] = det0.qtype end
sub[#sub + 1] = "ID " .. id
local startOnly = live and Codex.Share and Codex.Share.Origin and Codex.Share.Origin("q", id) == "start"
if not live then sub[#sub + 1] = catTitle and ("|cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r") or SeedTag()
elseif startOnly then sub[#sub + 1] = "|cff8a8a96[" .. L.CODEX_START_TAG .. "]|r" end
D:Add({ style = "sub", text = table.concat(sub, " | ") })
if not live then
D:Add({ style = "sub", text = catTitle and L.CODEX_CAT_NOTE or L.CODEX_SEED_NOTE })
elseif startOnly then
D:Add({ style = "sub", text = L.CODEX_START_NOTE })
end
local status = Codex.QuestStatus(id)
D:Add({ style = "sub", text = string.format(L.CODEX_STATUS_LINE,
status == "done" and L.CODEX_STATUS_DONE or status == "active" and L.CODEX_STATUS_ACTIVE or L.CODEX_STATUS_OPEN) })
if q.rem then
D:Add({ style = "sub", text = L.CODEX_REMOTE_ONLY })
end
if q.partial then
D:Add({ style = "sub", text = L.CODEX_PARTIAL })
end
if q.tm and q.tm.n and q.tm.n > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_TIME })
D:Add({ text = L.CODEX_QT_EFF, right = FormatDuration(q.tm.sum / q.tm.n / 10) })
if q.tr and q.tr.n and q.tr.n > 0 then
D:Add({ text = L.CODEX_QT_RAW, right = FormatDuration(q.tr.sum / q.tr.n / 10) })
end
if q.ta and q.ta.n and q.ta.n > 0 then
D:Add({ text = L.CODEX_QT_ALL, right = FormatDuration(q.ta.sum / q.ta.n / 10) })
end
D:Add({ text = L.CODEX_QT_N, right = tostring(q.tm.n) })
end
D:Add({ style = "heading", text = L.CODEX_SEC_GIVE })
if q.si then
-- Quest startet durch ein Item: das Item (mit seinen Fundorten im Item-Eintrag) statt einer Spielerposition
D:Add({ style = "sub", text = L.CODEX_GIVE_BY_ITEM })
ItemRow(q.si, nil, 0, " |cff9d9d9d[" .. L.CODEX_START_ITEM .. "]|r")
elseif q.give or not (sq and #sq.start > 0) then
RefRow(q.give)
else
for _, r in ipairs(sq.start) do SeedRef(r) end
end
D:Add({ style = "heading", text = L.CODEX_SEC_TURN })
if q.turn or not (sq and #sq.stop > 0) then
RefRow(q.turn)
else
for _, r in ipairs(sq.stop) do SeedRef(r) end
end
if #Codex.CollectPoints({ kind = "quests", id = id }) > 0 then
MapLink({ kind = "quests", id = id }, Codex.QuestTitle(q, id))
end
if q.si then
D:Add({ style = "sub", text = L.CODEX_PROPS_STARTITEM .. " " .. tostring(q.si) .. " " .. tostring(Codex.ItemName and Codex:ItemName(q.si) or "") })
end
if q.dtx then
D:Add({ style = "heading", text = L.CODEX_SEC_QTEXT })
D:Add({ style = "sub", text = q.dtx })
end
local hasReward = q.xp or q.money or q.rep or q.fixed or q.choice
if hasReward then
D:Add({ style = "heading", text = L.CODEX_SEC_REWARD })
if q.xp then D:Add({ text = L.CODEX_XP, right = tostring(q.xp) }) end
if q.money then
D:Add({ text = L.CODEX_MONEY, right = FormatMoney(q.money) })
end
if q.rep then
local names = {}
for name in pairs(q.rep) do names[#names + 1] = name end
table.sort(names)
for _, name in ipairs(names) do
local v = q.rep[name]
D:Add({ text = L.CODEX_REP .. ": " .. name, right = (v > 0 and "+" or "") .. v })
end
end
for _, r in ipairs(q.fixed or {}) do
ItemRow(r.id, r.c and r.c > 1 and ("x" .. r.c) or nil, 0, " |cff9d9d9d[" .. L.CODEX_FIXED .. "]|r")
end
for _, r in ipairs(q.choice or {}) do
local chosen = q.chosen and q.chosen[r.id]
ItemRow(r.id, chosen and string.format(L.CODEX_CHOSEN, chosen) or (r.c and r.c > 1 and ("x" .. r.c) or nil), 0,
" |cff9d9d9d[" .. L.CODEX_CHOICE .. "]|r")
end
end
if q.obj and #q.obj > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_OBJ })
for _, o in ipairs(q.obj) do
local label = L["CODEX_TYPE_" .. tostring(o.ty)] or L.CODEX_TYPE_other
local itemIDs = o.ty == "item" and Codex.ObjectiveItems(q, o, id) or nil
local shownName = o.name
if not shownName and itemIDs and itemIDs[1] then
shownName = Codex:ItemName(itemIDs[1])
end
D:Add({ text = label .. ": " .. (shownName or o.txt or "?"), right = o.req and ("x" .. o.req) or nil })
local lname = o.name and o.name:lower() or nil
if o.ty == "monster" then
local shown = {}
local ids = {}
for uid, hits in pairs(o.ids or {}) do ids[#ids + 1] = { id = uid, n = hits } end
table.sort(ids, function(a, b) return a.n > b.n end)
for _, e in ipairs(ids) do
shown[e.id] = true
EntityLink("units", e.id, Codex.V().units[e.id], nil)
end
for key in pairs(Codex.QuestKillKeys(id)) do
local sid = tonumber(key:sub(2))
if key:sub(1, 1) == "u" and sid and not shown[sid] then
shown[sid] = true
EntityLink("units", sid, Codex.V().units[sid], L.CODEX_MANUAL_TAG)
end
end
if lname then
local count = 0
for uid, u in pairs(Codex.V().units) do
if not shown[uid] and ((u.n and u.n:lower() == lname) or (u.cde and u.cde:lower() == lname) or (u.cen and u.cen:lower() == lname)) then
count = count + 1
if count > 4 then break end
EntityLink("units", uid, u, nil)
end
end
end
elseif itemIDs then
for _, itemID in ipairs(itemIDs) do
for key in pairs(Codex.QuestItemKeys(id, itemID)) do
local tk = key:sub(1, 1) == "u" and "units" or "objects"
local sid = tonumber(key:sub(2))
local ent = tk == "units" and Codex.V().units[sid] or Codex.V().objects[sid]
EntityLink(tk, sid, ent, L.CODEX_MANUAL_TAG)
end
local srcs = q.qi and q.qi[itemID]
if srcs then
local list = {}
for key, n in pairs(srcs) do list[#list + 1] = { key = key, n = n } end
table.sort(list, function(a, b) return a.n > b.n end)
for i, e in ipairs(list) do
if i > 6 then break end
local tk = e.key:sub(1, 1) == "u" and "units" or "objects"
local sid = tonumber(e.key:sub(2))
local ent = tk == "units" and Codex.V().units[sid] or Codex.V().objects[sid]
EntityLink(tk, sid, ent, tostring(e.n) .. "x")
end
end
end
end
end
elseif sq and #sq.obj > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_OBJ .. " " .. SeedTag() })
for _, r in ipairs(sq.obj) do SeedRef(r) end
end
-- Gehoert zur Quest (von Hand zugeordnet): Items, Kreaturen, Objekte
do
local rel = Codex.QuestRelated(id)
if #rel.i + #rel.u + #rel.o > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_QREL })
for _, itemID in ipairs(rel.i) do ItemRow(itemID, L.CODEX_MANUAL_TAG, 0) end
for _, uid in ipairs(rel.u) do EntityLink("units", uid, Codex.V().units[uid], L.CODEX_MANUAL_TAG) end
for _, oid in ipairs(rel.o) do EntityLink("objects", oid, Codex.V().objects[oid], L.CODEX_MANUAL_TAG) end
end
end
-- Quest-Text (Beschreibung, Ziel, Zwischentext, Abschlusstext); $N wird zum eigenen Namen
do
local qv = Codex.V().quests[id]
local tx = qv and qv.tx
if type(tx) == "table" then
local myName = (UnitName and UnitName("player")) or "?"
local first = true
for _, f in ipairs({ "d", "o", "p", "r" }) do
local v = tx[f]
if type(v) == "string" and v ~= "" then
if first then
D:Add({ style = "heading", text = L.CODEX_SEC_QTEXT })
first = false
end
D:Add({ style = "sub", text = L["CODEX_QT_" .. f:upper()] })
local shown = v:gsub("{br}", "\n\n"):gsub("%$[nN]", (myName:gsub("%%", "%%%%")))
D:Add({ style = "text", text = shown })
end
end
end
end
-- Ruf-Ziel aus dem Quest-Cache
do
local m = Codex.Catalog and Codex.Catalog.QuestMeta and Codex.Catalog.QuestMeta(id)
if m and m.rep then
D:Add({ style = "heading", text = L.CODEX_SEC_REP .. " |cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r" })
D:Add({ text = m.rep })
end
end
-- Laut Server (Katalog): Ziele, Belohnung und Texte aus der Server-Antwort; nur, was die eigenen Daten nicht haben
do
local det = Codex.Catalog and Codex.Catalog.Details and Codex.Catalog.Details(id)
if det then
local qv = Codex.V().quests[id]
local tag = " |cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r"
if #det.req > 0 and not (qv and qv.obj and #qv.obj > 0) then
D:Add({ style = "heading", text = L.CODEX_SEC_OBJ .. tag })
for _, r in ipairs(det.req) do
local cnt = r.count and r.count > 1 and ("x" .. r.count) or nil
if r.kind == "monster" then
ServerInfo("units", r.id, true)
EntityLink("units", r.id, Codex.V().units[r.id], cnt)
elseif r.kind == "object" then
ServerInfo("objects", r.id, true)
EntityLink("objects", r.id, Codex.V().objects[r.id], cnt)
else
ItemRow(r.id, cnt, 0)
end
end
end
local hasRew = (det.money or 0) > 0 or #det.rew > 0 or #det.choice > 0 or det.spell
if hasRew and not (qv and (qv.fixed or qv.choice or qv.money)) then
D:Add({ style = "heading", text = L.CODEX_SEC_REWARD .. tag })
if (det.money or 0) > 0 then D:Add({ text = L.CODEX_MONEY, right = FormatMoney(det.money) }) end
for _, it in ipairs(det.rew) do
ItemRow(it.id, it.count > 1 and ("x" .. it.count) or nil, 0, " |cff9d9d9d[" .. L.CODEX_FIXED .. "]|r")
end
for _, it in ipairs(det.choice) do
ItemRow(it.id, it.count > 1 and ("x" .. it.count) or nil, 0, " |cff9d9d9d[" .. L.CODEX_CHOICE .. "]|r")
end
if det.spell then D:Add({ text = L.CODEX_SRV_SPELL, right = tostring(det.spell) }) end
end
if det.src and not (qv and qv.si) then
D:Add({ style = "heading", text = L.CODEX_PROPS_STARTITEM .. tag })
ItemRow(det.src, nil, 0)
end
if not (qv and type(qv.tx) == "table") and (det.desc or det.obj or det.done) then
local myName = (UnitName and UnitName("player")) or "?"
local function Fill(v)
v = v:gsub("%$[bB]", "\n"):gsub("%$[nN]", (myName:gsub("%%", "%%%%")))
return v
end
D:Add({ style = "heading", text = L.CODEX_SEC_QTEXT .. tag })
for _, def in ipairs({ { "D", det.desc }, { "O", det.obj }, { "R", det.done } }) do
if def[2] and def[2] ~= "" then
D:Add({ style = "sub", text = L["CODEX_QT_" .. def[1]] })
D:Add({ style = "text", text = Fill(def[2]) })
end
end
end
end
end
-- Questkette: Vorquests und Folgequests (Daten und Vorlage)
do
local function Chain(heading, ids)
if #ids == 0 then return end
D:Add({ style = "heading", text = heading })
for i, qid in ipairs(ids) do
if i > 30 then break end
local lq = Codex.V().quests[qid]
QuestLink(qid, lq, lq and (lq.done or 0) > 0 and string.format(L.CODEX_DONE, lq.done) or nil)
end
end
Chain(L.CODEX_SEC_PRE, Codex.QuestPre(id))
Chain(L.CODEX_SEC_NEXT, Codex.QuestNext(id))
end
end
local function XPSection(ent)
if not ent.xp or next(ent.xp) == nil then
return
end
D:Add({ style = "heading", text = L.CODEX_SEC_XP })
local levels = {}
for pl in pairs(ent.xp) do levels[#levels + 1] = pl end
table.sort(levels, function(a, b) return a > b end)
local rows = 0
for _, pl in ipairs(levels) do
local xm = ent.xm and ent.xm[pl]
if xm and next(xm) then
-- nach Gegnerstufe getrennt: zu jeder Zeile gehoert ein Formelwert
local mls = {}
for ml in pairs(xm) do mls[#mls + 1] = ml end
table.sort(mls, function(a, b) return a > b end)
for _, ml in ipairs(mls) do
if rows >= 12 then break end
rows = rows + 1
local s = xm[ml]
local pred = Codex.PredictXP(pl, ml, ent.c)
D:Add({
text = string.format(L.CODEX_XP_UNIT_ROW2, pl, ml),
right = string.format("Ø %.1f (n=%d)", s.sum / s.n, s.n) .. " | " .. pred,
})
local rs = ent.xr and ent.xr[pl] and ent.xr[pl][ml]
if rs and rs.n and rs.n > 0 and rows < 12 then
rows = rows + 1
D:Add({
style = "sub",
text = " " .. L.CODEX_XP_WITH_RESTED,
right = string.format("Ø %.1f (n=%d)", rs.sum / rs.n, rs.n),
})
end
end
elseif rows < 12 then
rows = rows + 1
local s = ent.xp[pl]
local right = string.format("Ø %.1f (n=%d)", s.sum / s.n, s.n)
if ent.l1 and ent.l1 == ent.l2 then
right = right .. " | " .. Codex.PredictXP(pl, ent.l1, ent.c)
end
D:Add({ text = string.format(L.CODEX_XP_UNIT_ROW, pl), right = right })
end
end
end
-- Beute und Waren laut Vorlage; was wir selbst gesehen haben, steht schon oben und wird hier uebersprungen
local function SeedLootSection(tkind, id, ent)
local shown = {}
for _, e in ipairs(Codex.Seed.Drops(tkind, id)) do
local seen = (ent and ent.d and ent.d[e.item]) or (ent and ent.alt and ent.alt.d and ent.alt.d[e.item])
if not seen then shown[#shown + 1] = e end
end
if #shown > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_LOOT_SEED })
for i, e in ipairs(shown) do
if i > 40 then break end
ItemRow(e.item, SeedPct(e.chance), 0)
end
end
local roles = Codex.Seed.Roles(tkind, id)
if tkind == "units" and roles and roles.vendor then
local goods = Codex.Seed.VendorItems(id)
if #goods > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_SELLS })
local shownWares = 0
for _, itemID in ipairs(goods) do
if not (ent and ent.wa and ent.wa[itemID]) then
shownWares = shownWares + 1
if shownWares > 60 then break end
ItemRow(itemID, nil, 0)
end
end
end
end
end
-- Rollen laut eigenen und geteilten Beobachtungen: Waren mit allen Preisen, Lehrstoff, Flugziele
local function RolesSection(tkind, ent)
if tkind ~= "units" then return end
if ent.ti then
D:Add({ style = "sub", text = string.format(L.CODEX_TITLE_LINE, ent.ti) })
end
if ent.wa and next(ent.wa) then
local goods = {}
for itemID, w in pairs(ent.wa) do goods[#goods + 1] = { id = itemID, w = w } end
table.sort(goods, function(a, b) return a.id < b.id end)
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_WARES, #goods) })
for i, g in ipairs(goods) do
if i > 120 then break end
local extra = {}
if g.w.st and g.w.st > 1 then extra[#extra + 1] = "x" .. g.w.st end
if g.w.lim then extra[#extra + 1] = L.CODEX_WARE_LIMITED end
ItemRow(g.id, PriceText(g.w.pr, g.w.ec), 0, #extra > 0 and (" |cff9a9aa3" .. table.concat(extra, " ") .. "|r") or nil)
end
end
if ent.tr and next(ent.tr) then
local list = {}
for _, r in pairs(ent.tr) do list[#list + 1] = r end
table.sort(list, function(a, b)
if (a.lv or 0) ~= (b.lv or 0) then return (a.lv or 0) < (b.lv or 0) end
return (a.nm or "") < (b.nm or "")
end)
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_TRAINS, #list) })
for i, r in ipairs(list) do
if i > 150 then break end
local right = {}
if r.lv then right[#right + 1] = string.format(L.CODEX_TRAIN_LEVEL, r.lv) end
if r.sn then right[#right + 1] = r.sr and string.format("%s %d", r.sn, r.sr) or r.sn end
local price = PriceText(r.pr)
if price then right[#right + 1] = price end
local rank = r.rk or (r.sp and Codex.SpellRank(r.sp)) or nil
local icon = r.sp and Codex.SpellIcon(r.sp) or nil
D:Add({ text = (r.nm or "?") .. (rank and (" |cff9a9aa3(" .. rank .. ")|r") or ""),
right = table.concat(right, " | "), rightWidth = 330,
spellID = r.sp, icon = icon or (r.sp and "Interface\\Icons\\INV_Misc_QuestionMark") or nil })
end
end
if ent.fl and next(ent.fl) then
local list = {}
for dest, pr in pairs(ent.fl) do list[#list + 1] = { dest = dest, pr = pr } end
table.sort(list, function(a, b) return a.dest < b.dest end)
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_FLIGHTS, #list) })
if ent.fn then D:Add({ style = "sub", text = string.format(L.CODEX_FLIGHT_FROM, ent.fn) }) end
for _, e in ipairs(list) do
D:Add({ text = e.dest, right = PriceText(e.pr) })
end
end
end
local function ShowEntity(tkind, id)
local live = tkind == "units" and Codex.V().units[id] or Codex.V().objects[id]
local seedEnt = SeedOK() and Codex.Seed.Entity(tkind, id) or nil
local srv = ServerInfo(tkind, id, true)
local catRev = Codex.Catalog and Codex.Catalog.Reverse and Codex.Catalog.Reverse()[tkind == "units" and "monster" or "object"][id]
if not live and not seedEnt and not srv and not catRev then return end
local ent = live or {}
ShowPortrait(tkind == "units" and UnitIcon(ent) or ICON.objects, tkind == "units" and id or nil)
D:Add({ style = "title", text = UnitTitle(ent, id, tkind), indent = 76 })
local sub = {}
local lvl = LevelText(ent) or (seedEnt and tkind == "units" and LevelText(seedEnt)) or nil
if lvl then sub[#sub + 1] = lvl end
if ent.c then
sub[#sub + 1] = (ent.ed and ent.ed.cl and L["CODEX_PROPS_CLASS_" .. ent.ed.cl]) or ent.c
elseif ent.c == false then
sub[#sub + 1] = L.CODEX_PROPS_CLASS_normal
elseif seedEnt and tkind == "units" and RankLabel(seedEnt.rank) then
sub[#sub + 1] = RankLabel(seedEnt.rank)
end
if ent.dg ~= nil then
sub[#sub + 1] = ent.dg and L.CODEX_PROPS_DG_YES or L.CODEX_PROPS_DG_NO
elseif ent.iz and next(ent.iz) then
sub[#sub + 1] = L.CODEX_PROPS_DG_YES
end
if not (ent.ez and #ent.ez > 0) and ent.iz and next(ent.iz) then
local names = {}
for iid in pairs(ent.iz) do names[#names + 1] = Codex.InstanceName(iid) end
table.sort(names)
sub[#sub + 1] = string.format(L.CODEX_PROPS_ZONES_LINE, table.concat(names, ", "))
end
if ent.ez and #ent.ez > 0 then
local names = {}
for _, m in ipairs(ent.ez) do names[#names + 1] = Codex.MapName(m) end
sub[#sub + 1] = string.format(L.CODEX_PROPS_ZONES_LINE, table.concat(names, ", "))
end
if ent.f then sub[#sub + 1] = string.format(L.CODEX_FAMILY, ent.f) end
if seedEnt and (seedEnt.fac == "A" or seedEnt.fac == "H") then
sub[#sub + 1] = seedEnt.fac == "A" and L.CODEX_FAC_A or L.CODEX_FAC_H
end
if srv then
if tkind == "units" then
local CC = _G.C_CreatureInfo
local okT, ti = pcall(CC and CC.GetCreatureTypeInfo or function() end, srv.type)
if okT and type(ti) == "table" and type(ti.name) == "string" then sub[#sub + 1] = ti.name end
if not ent.c and RankLabel(srv.rank) then sub[#sub + 1] = RankLabel(srv.rank) end
if type(srv.subName) == "string" and srv.subName ~= "" then sub[#sub + 1] = "<" .. srv.subName .. ">" end
else
local g = GO_TYPES[srv.type]
if g then sub[#sub + 1] = Codex.Lang() == "en" and g[1] or g[2] end
end
end
local roleNames = RoleNames(tkind, id)
if #roleNames > 0 then sub[#sub + 1] = table.concat(roleNames, ", ") end
sub[#sub + 1] = "ID " .. id
local startOnly = live and Codex.Share and Codex.Share.Origin and Codex.Share.Origin(tkind == "units" and "u" or "o", id) == "start"
if not live then sub[#sub + 1] = seedEnt and SeedTag() or ("|cff8a8a96[" .. L.CODEX_CAT_TAG .. "]|r")
elseif startOnly then sub[#sub + 1] = "|cff8a8a96[" .. L.CODEX_START_TAG .. "]|r" end
D:Add({ style = "sub", text = table.concat(sub, " | "), indent = 76 })
D.y = math.max(D.y, 72)
if not live then
D:Add({ style = "sub", text = seedEnt and L.CODEX_SEED_NOTE or L.CODEX_SRV_NOTE })
elseif startOnly then
D:Add({ style = "sub", text = L.CODEX_START_NOTE })
end
-- Gebiet mit Stufenspanne (Verweis in die Gebietsansicht)
for i, zkey in ipairs(Codex.ZonesOf(ent)) do local zkey = zkey
if i > 3 then break end
local zd = Codex.Zones and Codex.Zones.Get(zkey)
D:Add({ style = "link", text = Codex.MapName(zkey), icon = ICON.zones,
right = zd and RangeText(zd.lo, zd.hi) or nil, onClick = function() Go("zones", zkey) end })
end
local ttkClasses = Codex.Time and Codex.Time.UnitClasses(ent) or {}
if #ttkClasses > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_TTK_CLASSES })
for _, c in ipairs(ttkClasses) do
D:Add({
text = Codex.Time.ClassLabel(c.cls) .. (c.own and (" " .. L.CODEX_TTK_YOU) or ""),
right = string.format("%.1f s (n=%d)", c.secs, c.n),
})
end
end
if ent.rk and ent.rk > 0 then
D:Add({ style = "sub", text = string.format(L.CODEX_REMOTE_HINT, ent.rk, ent.k or 0) })
elseif ent.remote and ent.remote > 0 then
D:Add({ style = "sub", text = L.CODEX_REMOTE_ONLY })
end
local avg = Codex.AverageCoin(ent)
if avg then
D:Add({ text = string.format(L.CODEX_COIN_AVG, ""), right = FormatMoney(avg) })
end
PlacesSection(ent, { kind = tkind, id = id }, UnitTitle(ent, id, tkind), seedEnt and Codex.Seed.Cells(tkind, id) or nil)
RolesSection(tkind, ent)
XPSection(ent)
ValueSection(ent)
QuestsOfUnit(tkind, id, UnitTitle(ent, id, tkind))
LootSection(ent, false)
if ent.alt then LootSection(ent, true) end
if seedEnt then SeedLootSection(tkind, id, live) end
end
local function ShowZone(key)
local zd = Codex.Zones.Get(key)
if not zd then return end
ShowPortrait(ICON.zones, nil)
D:Add({ style = "title", text = Codex.MapName(key), indent = 76 })
local sub = { zd.dungeon and L.CODEX_ZONE_DUNGEON or L.CODEX_ZONE_WORLD, (key < 0 and "Instanz " or "Karte ") .. math.abs(key) }
D:Add({ style = "sub", text = table.concat(sub, " | "), indent = 76 })
D.y = math.max(D.y, 72)
D:Add({ style = "heading", text = L.CODEX_SEC_ZONE_LEVELS })
D:Add({ text = L.CODEX_ZONE_TYPICAL, right = RangeText(zd.lo, zd.hi) or "-" })
if zd.avg then D:Add({ text = L.CODEX_ZONE_AVG, right = string.format("%.1f", zd.avg) }) end
D:Add({ text = L.CODEX_ZONE_COUNT, right = tostring(zd.units) })
if zd.manual then D:Add({ style = "sub", text = L.CODEX_ZONE_MANUAL_NOTE }) end
if zd.few and not zd.manual then D:Add({ style = "sub", text = L.CODEX_ZONE_FEW_NOTE }) end
if #zd.out > 0 then
local parts = {}
for _, e in ipairs(zd.out) do
local ent = Codex.V().units[e.id]
parts[#parts + 1] = string.format("%s (%s)", UnitTitle(ent, e.id, "units"), RangeText(e.l1, e.l2) or "?")
if #parts >= 6 then break end
end
D:Add({ style = "sub", text = L.CODEX_ZONE_OUTLIER_LIST .. ": " .. table.concat(parts, ", ") })
end
-- eigene Messung (Modul Quest): Erfahrung pro Stunde in diesem Gebiet, nur fuer die aktuellen Einstellungen
local rates = Codex.ZoneRateProvider and Codex.ZoneRateProvider(key) or {}
if #rates > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_ZONE_RATE })
D:Add({ style = "sub", text = L.CODEX_ZONE_RATE_NOTE })
for _, r in ipairs(rates) do
local mins = r.hours and (r.hours * 60) or nil
D:Add({ text = string.format(L.CODEX_ZONE_RATE_LINE, r.pl), right = string.format("%d XP/h%s (%d min)",
r.rate, mins and string.format(L.CODEX_ZONE_RATE_LEVEL, mins) or "", r.secs / 60) })
end
end
-- Kreaturen des Gebiets, nach Stufe
local members = Codex.Zones.Members(key)
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_ZONE_UNITS, #members) })
for i, m in ipairs(members) do local m = m
if i > 150 then break end
local ent = Codex.V().units[m.id]
local tags = {}
if m.c then tags[#tags + 1] = (L["CODEX_PROPS_CLASS_" .. tostring(m.c)] or tostring(m.c)) end
if m.out then tags[#tags + 1] = L.CODEX_ZONE_OUTLIER end
D:Add({ style = "link", text = UnitTitle(ent, m.id, "units") .. " (" .. (RangeText(m.l1, m.l2) or "?") .. ")",
right = #tags > 0 and table.concat(tags, ", ") or nil, onClick = function() Go("units", m.id) end, icon = ICON.units })
end
-- Quests, deren Geber in diesem Gebiet steht (nur beim Oeffnen berechnet)
if key > 0 then
local pl = UnitLevel and UnitLevel("player") or 1
local here, foryou = {}, {}
for qid, q in pairs(Codex.V().quests) do
local g = q.give
if g and g.m == key then
local e = { id = qid, q = q, lvl = q.lvl or 0, mn = q.mn or 0, title = Codex.QuestTitle(q, qid) or "" }
here[#here + 1] = e
if e.mn <= pl and e.lvl >= pl - 4 then
local st = Codex.QuestStatus(qid)
if st ~= "done" and st ~= "active" then foryou[#foryou + 1] = e end
end
end
end
local function ByLevel(a, b)
if a.lvl ~= b.lvl then return a.lvl < b.lvl end
if a.title ~= b.title then return a.title < b.title end
return a.id < b.id
end
table.sort(here, ByLevel)
table.sort(foryou, ByLevel)
local function Row(e)
local right = e.mn > 1 and string.format(L.CODEX_ZONE_QMIN, e.mn) or nil
local st = Codex.QuestStatus(e.id)
if st == "done" then right = "|cff6fd66f" .. L.CODEX_STATUS_DONE .. "|r" elseif st == "active" then right = "|cffffd060" .. L.CODEX_STATUS_ACTIVE .. "|r" end
QuestLink(e.id, e.q, right)
end
if #foryou > 0 then
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_ZONE_FORYOU, #foryou, pl) })
for i, e in ipairs(foryou) do
if i > 20 then break end
Row(e)
end
end
if #here > 0 then
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_ZONE_QUESTS, #here) })
for i, e in ipairs(here) do
if i > 80 then break end
Row(e)
end
end
-- Flugpunkte aus dem Karten-Scan
local flights = {}
for _, v in pairs((Codex.db.cat and Codex.db.cat.x) or {}) do
if type(v) == "string" then
local a = tonumber(v:match("areaID=(%d+)"))
local name = v:match("|(.+)$")
if a == key and name then flights[#flights + 1] = name end
end
end
table.sort(flights)
if #flights > 0 then
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_ZONE_FLIGHT, #flights) })
for _, name in ipairs(flights) do D:Add({ text = name }) end
end
end
-- Spanne von Hand setzen (nur Berechtigte)
if Codex.Trust and Codex.Trust.CanEdit() and Codex.NameUI and Codex.NameUI.OpenZone then
D:Add({ style = "heading", text = L.CODEX_SEC_ZONE_EDIT })
D:Add({ style = "link", text = L.CODEX_ZONE_EDIT, onClick = function() Codex.NameUI.OpenZone(key) end })
end
end
local function ShowLearn(lv)
local plan = learnSel and Codex.LearnPlan(learnSel)
local g = plan and plan.levels[lv]
if not g then return end
local pl = UnitLevel and UnitLevel("player") or 1
ShowPortrait(ICON.learn, nil)
local head = lv > 0 and string.format(L.CODEX_LEARN_TITLE, lv) or L.CODEX_LEARN_LVUNKNOWN
D:Add({ style = "title", text = head .. " - " .. Codex.Time.ClassLabel(learnSel), indent = 76 })
D:Add({ style = "sub", text = string.format(L.CODEX_LEARN_SUB, #g.spells, FormatMoney(g.cost)), indent = 76 })
D.y = math.max(D.y, 72)
if lv > pl then
D:Add({ text = string.format(L.CODEX_LEARN_CUM, pl + 1, FormatMoney(Codex.LearnCost(plan, pl, lv))) })
end
D:Add({ style = "sub", text = L.CODEX_LEARN_NOTE })
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_LEARN_SPELLS, #g.spells) })
for _, sp in ipairs(g.spells) do
local rank = sp.rk or (sp.sp and Codex.SpellRank(sp.sp)) or nil
local icon = sp.sp and Codex.SpellIcon(sp.sp) or nil
D:Add({ text = (sp.nm or "?") .. (rank and (" |cff9a9aa3(" .. rank .. ")|r") or ""),
right = (sp.cost and sp.cost > 0) and FormatMoney(sp.cost) or "-", rightWidth = 150,
spellID = sp.sp, icon = icon or (sp.sp and "Interface\\Icons\\INV_Misc_QuestionMark") or nil })
end
D:Add({ style = "heading", text = string.format(L.CODEX_SEC_LEARN_TRAINERS, #g.trainers) })
for i, id in ipairs(g.trainers) do
if i > 20 then break end
local ent = Codex.V().units[id]
local spot = ent and ZoneOf(ent)
EntityLink("units", id, ent, spot)
end
end
local function ShowXP(key)
local pl, ml, cls, kindFlag = key:match("^(%d+):(%d+):(%a+):(%a)$")
pl, ml = tonumber(pl), tonumber(ml)
if not pl then return end
local rows = Codex:XPRows(true)
local r
for _, row in ipairs(rows) do
if XPKey(row) == key then r = row break end
end
if not r then return end
ShowPortrait(ICON.xp, nil)
D:Add({ style = "title", text = string.format(L.CODEX_XP_TITLE, pl, ml), indent = 76 })
local sub = {}
local label = ClassLabel(cls)
if label then sub[#sub + 1] = label end
if r.rested then
sub[#sub + 1] = L.CODEX_XP_RESTED_TAG
elseif r.bonus then
sub[#sub + 1] = L.CODEX_XP_BONUS_TAG
end
D:Add({ style = "sub", text = table.concat(sub, " | "), indent = 76 })
D.y = math.max(D.y, 72)
D:Add({ style = "heading", text = L.CODEX_SEC_XPMEAS })
D:Add({ text = L.CODEX_XP_SAMPLES, right = tostring(r.n) })
D:Add({ text = L.CODEX_XP_AVG, right = string.format("%.2f", r.avg) })
D:Add({ text = L.CODEX_XP_MINMAX, right = string.format("%d / %d", r.min, r.max) })
local secs, nTTK
if not r.bonus and cls == "normal" then
secs, nTTK = OwnTTK(pl, ml)
end
if secs and secs > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_TIME })
D:Add({ text = string.format(L.CODEX_TTK_AVG, Codex.Time.ClassName()), right = string.format("%.1f s (n=%d)", secs, nTTK) })
D:Add({ text = L.CODEX_TTK_XPM, right = string.format("%d", r.avg / secs * 60) })
end
if not r.bonus and cls == "normal" and Codex.Share and Codex.Share.TTKClasses then
local classes = Codex.Share.TTKClasses(pl, ml - pl)
if #classes > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_TTK_CLASSES })
local mine = Codex.Time and Codex.Time.PlayerToken()
for _, c in ipairs(classes) do
D:Add({
text = Codex.Time.ClassLabel(c.cls) .. (c.cls == mine and (" " .. L.CODEX_TTK_YOU) or ""),
right = string.format("%.1f s (n=%d) | %d XP/min", c.secs, c.n, r.avg / c.secs * 60),
})
end
end
end
D:Add({ style = "heading", text = L.CODEX_SEC_XPCALC })
D:Add({ text = L.CODEX_XP_BASE, right = tostring((ml >= pl and pl or ml) * 5 + 45) })
D:Add({ text = L.CODEX_XP_COLOR, right = L["CODEX_COLOR_" .. r.color] or r.color })
D:Add({ text = L.CODEX_XP_ZD, right = tostring(Codex.ZeroDiff(pl)) })
D:Add({ text = L.CODEX_XP_GRAY, right = tostring(Codex.GrayLevel(pl)) })
D:Add({ text = L.CODEX_XP_PRED, right = tostring(r.pred) })
D:Add({ text = L.CODEX_XP_DIFF, right = string.format("%+.2f", r.diff) })
D:Add({ style = "sub", text = L.CODEX_XP_NOTE })
end
local function ShowItem(itemID)
local row = D:Add({ style = "title", text = Codex:ItemName(itemID) or ("#" .. itemID), icon = "Interface\\Icons\\INV_Misc_QuestionMark" })
row.itemID = itemID
row:EnableMouse(true)
local q0 = Codex.QualityOf(itemID)
if q0 then row.left:SetText(Colored(Codex:ItemName(itemID) or ("#" .. itemID), q0)) end
Util.RequestItem(itemID, function(info)
if info then
Codex.RememberQuality(itemID, info.quality)
row.icon:SetTexture(info.icon)
row.left:SetText(Colored(Codex:ItemName(itemID) or info.name, info.quality))
end
end)
do
local fx = ItemIndex()[itemID]
if fx and fx.ah then
D:Add({ style = "sub", text = "ID " .. itemID .. " | |cff8a8a96[" .. L.CODEX_AH_TAG .. "]|r" })
D:Add({ style = "sub", text = L.CODEX_AH_NOTE })
else
D:Add({ style = "sub", text = "ID " .. itemID })
end
end
local ied = (Codex.V().ied or {})[itemID]
local iz = ied and (ied.ez or (ied.ed and ied.ed.z))
local idg = ied and (ied.dg ~= nil and ied.dg or (ied.ed and ied.ed.dg))
if iz or idg ~= nil then
local bits = {}
if iz and #iz > 0 then
local names = {}
for _, m in ipairs(iz) do names[#names + 1] = Codex.MapName(m) end
bits[#bits + 1] = string.format(L.CODEX_PROPS_ZONES_LINE, table.concat(names, ", "))
end
if idg ~= nil then bits[#bits + 1] = idg and L.CODEX_PROPS_DG_YES or L.CODEX_PROPS_DG_NO end
D:Add({ style = "sub", text = table.concat(bits, " | ") })
end
do
local belongs = Codex.EntityQuests("i", itemID)
if #belongs > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_QBEL })
for i, qid in ipairs(belongs) do
if i > 30 then break end
QuestLink(qid, Codex.V().quests[qid], L.CODEX_MANUAL_TAG)
end
end
end
-- Auktionshaus (nur mit Auktions-Modul)
local ai = Codex.AuctionInfo and Codex.AuctionInfo(itemID)
if ai then
D:Add({ style = "heading", text = L.CODEX_SEC_AH })
local thin = ai.thin and " |cffff8000~|r" or ""
if ai.price then D:Add({ text = L.CODEX_AH_PRICE, right = FormatMoney(ai.price) .. thin }) end
if ai.min then D:Add({ text = L.CODEX_AH_MIN, right = FormatMoney(ai.min) }) end
if ai.avg then D:Add({ text = L.CODEX_AH_AVG, right = FormatMoney(ai.avg) }) end
if ai.qty then D:Add({ text = L.CODEX_AH_QTY, right = tostring(ai.qty) .. thin }) end
if ai.age then
local when = ai.age == 0 and L.CODEX_AH_TODAY or string.format(L.CODEX_AH_DAYS, ai.age)
if ai.stale then when = "|cff9d9d9d" .. when .. " (" .. L.CODEX_AH_OLD .. ")|r" end
D:Add({ text = L.CODEX_AH_AGE, right = when })
end
if ai.thin then D:Add({ style = "sub", text = L.CODEX_AH_THIN }) end
local function Pct(v)
local color = v > 0.005 and "|cff1eff00" or (v < -0.005 and "|cffff4040" or "|cff9d9d9d")
return string.format("%s%+.0f %%|r", color, v * 100)
end
if ai.trend then D:Add({ text = L.CODEX_AH_TREND_DAY, right = Pct(ai.trend) }) end
if ai.trendAll then D:Add({ text = L.CODEX_AH_TREND_ALL, right = Pct(ai.trendAll) }) end
if ai.net then D:Add({ text = L.CODEX_AH_NET, right = FormatMoney(ai.net) }) end
local info = Util.GetItem(itemID)
local vendor = info and info.sellPrice or 0
if vendor > 0 and ai.net then
D:Add({ text = L.CODEX_AH_VENDOR_SELL, right = FormatMoney(vendor) })
local diff = ai.net - vendor
if diff > 0 then
D:Add({ text = L.CODEX_AH_SELL_AH, right = "|cff1eff00+" .. FormatMoney(diff) .. "|r" })
else
D:Add({ text = L.CODEX_AH_SELL_VENDOR, right = "|cff9d9d9d+" .. FormatMoney(-diff) .. "|r" })
end
end
local buy = LowestBuyPrices()[itemID]
if buy and ai.net then
D:Add({ text = L.CODEX_AH_VENDOR_BUY, right = FormatMoney(buy) })
local margin = ai.net - buy
if margin > 0 then
D:Add({ text = L.CODEX_AH_FLIP, right = "|cff1eff00+" .. FormatMoney(margin) .. " (" .. string.format("%+.0f %%", margin / buy * 100) .. ")|r" })
else
D:Add({ text = L.CODEX_AH_FLIP, right = "|cffff4040-" .. FormatMoney(-margin) .. "|r" })
end
end
D:Chart(ai.history)
end
-- Werte (Staerke, Beweglichkeit ...), vom Spiel gelesen
local stats = Codex:GetStats(itemID)
if stats and next(stats) then
local keys = Codex:StatKeys({ itemID })
D:Add({ style = "heading", text = L.CODEX_SEC_STATS })
if stats._dmin and stats._dmax and stats._spd then
D:Add({ style = "line", text = L.CODEX_WEAPON_DAMAGE,
right = string.format("%s - %s", Codex.FormatStat(stats._dmin), Codex.FormatStat(stats._dmax)) })
D:Add({ style = "line", text = L.CODEX_WEAPON_SPEED, right = string.format("%.2f", stats._spd) })
end
for _, key in ipairs(keys) do
if not Codex.FXFLAGS[key] and type(stats[key]) == "number" then
D:Add({ style = "line", text = Codex.StatName(key), right = Codex.FormatStat(stats[key]) })
end
end
if type(stats._fx) == "table" and #stats._fx > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_EFFECTS })
for _, txt in ipairs(stats._fx) do
D:Add({ style = "text", text = txt })
end
end
end
local sources = {}
for _, pair in ipairs({ { "units", Codex.V().units }, { "objects", Codex.V().objects } }) do
for id, ent in pairs(pair[2]) do
local chance, n, k = Codex.DropChance(ent, itemID, false)
if chance then
sources[#sources + 1] = { kind = pair[1], id = id, ent = ent, chance = chance, n = n, k = k }
end
end
end
table.sort(sources, function(a, b)
if a.chance ~= b.chance then return a.chance > b.chance end
return a.k > b.k
end)
local value = ItemValueOf(itemID)
if value and value > 0 then
local ranked = {}
for _, src in ipairs(sources) do
local ttk = src.kind == "units" and UnitTTK(src.ent) or nil
if ttk then
ranked[#ranked + 1] = { src = src, ttk = ttk, gph = src.chance * value / ttk * 3600 }
end
end
table.sort(ranked, function(a, b) return a.gph > b.gph end)
if #ranked > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_BESTSRC })
for i = 1, math.min(3, #ranked) do
local r = ranked[i]
EntityLink(r.src.kind, r.src.id, r.src.ent, string.format(L.CODEX_BESTSRC_ROW, FormatMoney(r.gph), r.ttk))
end
local top = ranked[1].src
D:Add({ style = "link", text = L.CODEX_MAP_SHOW, icon = MAP_ICON,
onClick = function() Codex.Map:Show(Codex.CollectPoints({ kind = top.kind, id = top.id }), UnitTitle(top.ent, top.id, top.kind)) end })
end
end
if #sources > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_DROPS })
for i, s in ipairs(sources) do
if i > 40 then break end
EntityLink(s.kind, s.id, s.ent, string.format(L.CODEX_DROP_CHANCE, FormatChance(s.chance), s.n, s.k))
end
local merged = {}
for _, s in ipairs(sources) do
Codex.AddCells(merged, s.ent.cells)
end
if next(merged) then
D:Add({ style = "heading", text = L.CODEX_SEC_SPOTS })
SpotRows(merged, 6)
MapLink({ kind = "items", id = itemID }, Codex:ItemName(itemID) or ("#" .. itemID))
end
end
-- Haendler laut eigenen und geteilten Beobachtungen (mit allen Preisen)
local liveVendors = {}
for vid, vent in pairs(Codex.V().units) do
local w = vent.wa and vent.wa[itemID]
if w then liveVendors[#liveVendors + 1] = { id = vid, ent = vent, w = w } end
end
for _, v in ipairs(liveVendors) do
local low
for price in pairs(v.w.pr or {}) do
if not low or price < low then low = price end
end
v.low = low or 0
end
table.sort(liveVendors, function(a, b)
if a.low ~= b.low then return a.low < b.low end
return a.id < b.id
end)
local liveVendorSet = {}
if #liveVendors > 0 then
D:Add({ style = "heading", text = L.CODEX_SEC_SOLDBY_LIVE })
for i, v in ipairs(liveVendors) do
liveVendorSet[v.id] = true
if i <= 30 then
local text = PriceText(v.w.pr, v.w.ec)
if i == 1 and #liveVendors > 1 then text = "|cff6fd66f" .. L.CODEX_CHEAPEST .. "|r " .. (text or "") end
EntityLink("units", v.id, v.ent, text)
end
end
end
-- Quellen laut Vorlage (Vanilla): nur das, was wir selbst noch nicht gesehen haben
local it = SeedOK() and Codex.Seed.Item(itemID) or nil
local itFromStart = false
if not it and Codex.Forever and Codex.Forever.Item then
it = Codex.Forever.Item(itemID)
itFromStart = it ~= nil
end
if it then
local liveSet = {}
for _, src in ipairs(sources) do liveSet[src.kind .. src.id] = true end
local rows = {}
for _, def in ipairs({ { "units", it.u, 25 }, { "objects", it.o, 8 } }) do
local n = 0
for _, src in ipairs(def[2]) do
if not liveSet[def[1] .. src.id] and n < def[3] then
n = n + 1
rows[#rows + 1] = { kind = def[1], id = src.id, chance = src.chance }
end
end
end
if #rows > 0 then
D:Add({ style = "heading", text = itFromStart and L.CODEX_SEC_SRC_START or L.CODEX_SEC_SRC_SEED })
for _, r in ipairs(rows) do
EntityLink(r.kind, r.id, Codex.V()[r.kind][r.id], SeedPct(r.chance))
end
end
if #it.v > 0 then
D:Add({ style = "heading", text = itFromStart and L.CODEX_SEC_SOLDBY_START or L.CODEX_SEC_SOLDBY })
local shownV = 0
for _, vid in ipairs(it.v) do
if not liveVendorSet[vid] then
shownV = shownV + 1
if shownV > 20 then break end
EntityLink("units", vid, Codex.V().units[vid], nil)
end
end
end
if #sources == 0 and #Codex.CollectPoints({ kind = "items", id = itemID }) > 0 then
MapLink({ kind = "items", id = itemID }, Codex:ItemName(itemID) or ("#" .. itemID))
end
end
local rewardOf, itemFor = {}, {}
for qid, q in pairs(Codex.V().quests) do
local found = false
for _, r in ipairs(q.fixed or {}) do if r.id == itemID then found = true end end
for _, r in ipairs(q.choice or {}) do if r.id == itemID then found = true end end
if found then rewardOf[#rewardOf + 1] = qid end
if q.qi and q.qi[itemID] then itemFor[#itemFor + 1] = qid end
end
local startsQ = {}
if Codex.Catalog and Codex.Catalog.Reverse then
local rev = Codex.Catalog.Reverse()
for _, def in ipairs({ { rewardOf, rev.reward[itemID] }, { itemFor, rev.item[itemID] }, { startsQ, rev.src[itemID] } }) do
local have = {}
for _, qid in ipairs(def[1]) do have[qid] = true end
for _, qid in ipairs(def[2] or {}) do
if not have[qid] then
have[qid] = true
def[1][#def[1] + 1] = qid
end
end
end
end
for _, def in ipairs({ { rewardOf, L.CODEX_SEC_REWARDOF }, { itemFor, L.CODEX_SEC_QUESTITEM }, { startsQ, L.CODEX_SEC_STARTS } }) do
if #def[1] > 0 then
D:Add({ style = "heading", text = def[2] })
for i, qid in ipairs(def[1]) do
if i > 30 then break end
QuestLink(qid, Codex.V().quests[qid])
end
end
end
end
function ShowDetail()
UpdateFavUI()
D:Reset()
if selected then
if kind == "quests" then ShowQuest(selected)
elseif kind == "items" then ShowItem(selected)
elseif kind == "xp" then ShowXP(selected)
elseif kind == "learn" then ShowLearn(selected)
elseif kind == "zones" then ShowZone(selected)
else ShowEntity(kind, selected) end
end
D:Finish()
detail:ScrollToTop()
end
-- Neuzeichnen der Detailseite (Antworten des Servers auf Namensanfragen), Scrollstand bleibt erhalten
function Codex.RedrawDetail()
if not (f:IsShown() and selected) then return end
local sc = detail.scroll
local v = sc and sc.GetVerticalScroll and sc:GetVerticalScroll() or 0
ShowDetail()
if v > 0 and sc and sc.SetVerticalScroll then sc:SetVerticalScroll(v) end
end
---------------- Liste ----------------
local function FilterKey()
local parts = {}
for q in pairs(flt.q) do parts[#parts + 1] = "q" .. q end
table.sort(parts)
for _, k in ipairs({ "type", "sub", "slot", "src", "zone", "ilMin", "ilMax", "rlMin", "rlMax", "pMin", "pMax", "aMin", "aMax", "stat", "statMin", "statMax" }) do
if flt[k] ~= nil then parts[#parts + 1] = k .. "=" .. tostring(flt[k]) end
end
for i = 1, MAX_STAT_ROWS do
local r = flt.stats and flt.stats[i]
if r and r.key then
parts[#parts + 1] = string.format("s%d=%s:%s:%s:%s", i, r.key, tostring(r.min), tostring(r.max),
flt.ops and flt.ops[i] or "and")
end
end
return table.concat(parts, ",")
end
-- Sortierwerte erst berechnen, wenn danach sortiert wird; fehlende Werte bleiben nil und stehen am Ende
local function Enrich(entries, mode)
local V = Codex.V()
if kind == "units" or kind == "objects" then
for _, e in ipairs(entries) do
local u = V[kind][e.id]
if mode == "ahkill" and kind == "units" then
local value = u and Codex.KillValue(u, true)
e.ahkill = value and value >= 0 and value or nil
elseif mode == "ttk" or mode == "gpm" or mode == "gpmah" or mode == "xpm" then
local ttk = kind == "units" and UnitTTK(u) or nil
e.ttk = ttk
if ttk then
if mode == "gpm" or mode == "gpmah" then
local value = Codex.KillValue(u, mode == "gpmah")
e[mode] = value and value >= 0 and (value / ttk * 60) or nil
elseif mode == "xpm" then
local xp = UnitXPAvg(u)
e.xpm = xp and (xp / ttk * 60) or nil
end
end
elseif mode == "data" then
e.data = u and ((u.k or 0) + (u.tt and 1 or 0)) or 0
elseif mode == "lvl" then
local lv = u and u.l1
if not lv and SeedOK() then
local ent = Codex.Seed.Entity(kind, e.id)
lv = ent and ent.l1
end
e.lvl = lv
end
end
elseif kind == "quests" then
for _, e in ipairs(entries) do
local q = V.quests[e.id]
if mode == "lvl" then
e.lvl = q and q.lvl or nil
elseif mode == "xp" then
e.xp = q and q.xp or nil
elseif mode == "money" then
e.money = q and q.money or nil
elseif mode == "qtime" then
e.qtime = q and q.tm and q.tm.n and q.tm.n > 0 and (q.tm.sum / q.tm.n / 10) or nil
end
end
elseif kind == "items" then
if mode == "buy" then
local lowest = LowestBuyPrices()
for _, e in ipairs(entries) do e.buy = lowest[e.id] end
elseif mode == "flip" then
-- Gewinn je Stueck: Auktion (netto nach Gebuehr) minus Einkauf beim Haendler
local lowest = LowestBuyPrices()
for _, e in ipairs(entries) do
local info = Codex.AuctionInfo and Codex.AuctionInfo(e.id)
local buy = lowest[e.id]
e.flip = info and info.net and buy and (info.net - buy) or nil
end
elseif mode == "ah" then
for _, e in ipairs(entries) do
local v = Codex.AuctionProvider and Codex.AuctionPrice(e.id)
e.ah = v and v > 0 and v or nil
end
elseif mode == "gph" then
local best = {}
for _, u in pairs(V.units) do
local k = u.k or 0
local ttk = k > 0 and UnitTTK(u) or nil
if ttk then
for id, d in pairs(u.d or {}) do
local p = ItemValueOf(id)
if p and p > 0 then
local g = (d.q or d.n or 0) / k * p / ttk * 3600
if g > (best[id] or 0) then best[id] = g end
end
end
end
end
for _, e in ipairs(entries) do e.gph = best[e.id] end
end
end
-- Sortierwert in der Zeile anzeigen
local fmt = SORT_FORMAT[mode]
local metric = SORT_METRIC[mode]
for _, e in ipairs(entries) do
e.baseSub = e.baseSub or e.sub
local v = metric and e[metric]
if fmt and v and v >= 0 and not (mode == "gold" or mode == "price") then
local text
if fmt == "money" then text = FormatMoney(v)
elseif fmt == "secs" then text = string.format("%.1f s", v)
else text = string.format("%.0f", v) end
e.sub = (e.baseSub ~= "" and (e.baseSub .. " | ") or "") .. (L["CODEX_SORTBY_" .. mode] or mode) .. ": " .. text
else
e.sub = e.baseSub
end
end
end
local function Missing(v)
return v == nil or v < 0 or v >= 1e9
end
local function SortedBase()
local mode = CurrentSort()
local seed = SeedOn()
local role = roleSel[kind]
local reverse = sortRev[kind] == true
-- unveraendert (zum Beispiel beim Anklicken einer Zeile): die gemerkte Liste wiederverwenden
local key = table.concat({ kind, search, mode, tostring(reverse), tostring(farmOn and kind == "units"), tostring(seed), tostring(role), tostring(chainSel), tostring(statusSel), tostring(zoneSel), tostring(learnSel), tostring(favOnly[kind]), FilterKey(), dataVersion }, "\1")
-- Quest-Status aendert sich im Spiel laufend: mit Statusfilter immer neu aufbauen
if memo.key == key and mode ~= "dist" and not statusSel then
return memo.list
end
local entries = BuildEntries(kind, search, flt, { seed = seed, role = role, chain = chainSel, status = statusSel, zoneSel = zoneSel,
class = kind == "learn" and EnsureLearnSel() or nil, fav = favOnly[kind] })
local metric = SORT_METRIC[mode]
if mode == "dist" then
local Seed = Codex.Seed
local loc = Util.GetPlayerLocation() -- einmal lesen, nicht je Eintrag
for _, e in ipairs(entries) do
if kind == "quests" then
local q = Codex.V().quests[e.id]
local g = q and q.give
local d
if g and g.m and g.x and g.y and loc and loc.mapID == g.m and loc.x and loc.y then
d = math.sqrt((g.x - loc.x) ^ 2 + (g.y - loc.y) ^ 2)
end
e.dist = d or 1e9
else
local live = Codex.V()[kind][e.id]
local d1 = live and live.cells and Seed.Distance(live.cells, loc)
local d2 = SeedOK() and Seed.Distance(Seed.Cells(kind, e.id), loc)
e.dist = math.min(d1 or 1e9, d2 or 1e9)
end
end
end
if farmOn and kind == "units" then
local loc = Util.GetPlayerLocation()
local zone = loc and loc.mapID and CachedMapName(loc.mapID)
zone = zone and zone:lower() or nil
local pl = UnitLevel and UnitLevel("player") or 1
Enrich(entries, "gpm")
local kept = {}
for _, e in ipairs(entries) do
if e.gpm and e.l1 and (e.l2 or e.l1) >= pl - 5 and e.l1 <= pl + 3 and (not zone or e.zone == zone) then
kept[#kept + 1] = e
end
end
entries = kept
end
Enrich(entries, mode)
local asc = SORT_ASC[mode] == true
if reverse then asc = not asc end
table.sort(entries, function(a, b)
local va, vb
if mode == "zone" then
local za, zb = a.zone ~= "" and a.zone or "\255", b.zone ~= "" and b.zone or "\255"
if za ~= zb then
if asc then return za < zb end
return za > zb
end
if a.name ~= b.name then return a.name < b.name end
return a.id < b.id
end
if mode == "recent" then
va, vb = a.recent, b.recent
elseif mode == "dist" then
va, vb = a.dist, b.dist
local ma, mb = Missing(va), Missing(vb)
if ma ~= mb then return mb end
if ma then va, vb = 0, 0 end
elseif metric then
va, vb = a[metric], b[metric]
local ma, mb = Missing(va), Missing(vb)
if ma ~= mb then return mb end -- fehlende Werte immer ans Ende
if ma then va, vb = 0, 0 end
end
if va ~= nil and va ~= vb then
if asc then return va < vb end
return va > vb
end
if a.name ~= b.name then
if mode == "name" then
if asc then return a.name < b.name end
return a.name > b.name
end
return a.name < b.name
end
return a.id < b.id
end)
memo.key, memo.list = key, entries
return entries
end
-- Sortierung nach Gebiet: Kopfzeilen mit Zaehlern, einklappbar. Zaehler immer frisch (Questlog aendert sich).
local function SortedEntries()
local entries = SortedBase()
if kind ~= "quests" or CurrentSort() ~= "zone" then return entries end
local L = FW.L
local out, i = {}, 1
while i <= #entries do
local zone = entries[i].zone or ""
local j, open, active, done = i, 0, 0, 0
while j <= #entries and (entries[j].zone or "") == zone do
local st = Codex.QuestStatus(entries[j].id)
if st == "done" then done = done + 1 elseif st == "active" then active = active + 1 else open = open + 1 end
j = j + 1
end
local count = j - i
local name = entries[i].zoneName or L.CODEX_ZONE_UNKNOWN
local closed = collapsed[zone] == true
out[#out + 1] = { header = zone, id = "zone:" .. zone, title = string.format("%s (%d)", name, count),
sub = string.format(L.CODEX_GROUP_ROW, open, active, done), icon = closed and ICON.plus or ICON.minus,
name = "", zone = zone }
if not closed then
for k = i, j - 1 do out[#out + 1] = entries[k] end
end
i = j
end
return out
end
-- Kreatur-Bilder in der Liste: 2D-Portrait aus der gespeicherten Display-ID.
-- Kreaturen ohne ID zeigen das Symbol; ein einziges unsichtbares Modell holt die IDs
-- der sichtbaren Zeilen nach und zwar eine nach der anderen.
local resolver = CreateFrame("PlayerModel", nil, f)
resolver:SetSize(8, 8)
resolver:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
resolver:SetAlpha(0)
local resolverBusy, resolverTries = false, {}
local UpdateRowIcons
local function ReadDisplayID(model)
local ok, id = pcall(function() return model.GetDisplayInfo and model:GetDisplayInfo() end)
if ok and type(id) == "number" and id > 0 then return id end
return nil
end
local function ResolveNext()
-- Ohne 2D-Portrait in der Liste (Octo) braucht niemand mehr die Display-IDs der Zeilen;
-- unbekannte Kreaturen ueber ein Modell zu laden waere ausserdem riskant (siehe SAFE_MODEL_TYPE).
if true then return end
if resolverBusy then return end
local sf = listScroll.scroll
local top = sf:GetVerticalScroll() or 0
local bottom = top + (sf:GetHeight() or 0)
local pick
for i, row in ipairs(listRows) do
local npc = row.unitID
if npc and row:IsShown() and not Codex:GetDisplayID(npc) and not resolverTries[npc] then
local y = (i - 1) * (ROW_H + ROW_GAP)
if y + ROW_H >= top and y <= bottom then pick = npc break end
end
end
if not pick then return end
resolverTries[pick] = true
resolverBusy = true
local function Finish()
local d = ReadDisplayID(resolver)
if d then
Codex.dispStats.read = Codex.dispStats.read + 1
if Codex:SetDisplayID(pick, d) then UpdateRowIcons() end
else
Codex.dispStats.fail = Codex.dispStats.fail + 1
end
resolverBusy = false
C_Timer.After(0.05, ResolveNext)
end
Codex.EnsureCreature(pick, function()
local cd = Codex.CacheDisplayID(pick)
if cd then
Codex.dispStats.read = Codex.dispStats.read + 1
if Codex:SetDisplayID(pick, cd) then UpdateRowIcons() end
resolverBusy = false
C_Timer.After(0.05, ResolveNext)
return
end
pcall(function()
resolver:ClearModel()
resolver:SetCreature(pick)
end)
C_Timer.After(0.35, Finish)
end)
end
-- Octo hat kein SetPortraitTextureFromCreatureDisplayID und ein 2D-Bild geht nur fuer
-- Einheiten-Token. Mehrere 3D-Modelle in der Liste fuehrten zu Abstuerzen, daher bleibt
-- die Liste bei den Symbolen; das 3D-Modell gibt es nur im Detailfenster.
function UpdateRowIcons()
for _, row in ipairs(listRows) do
if row.fwModel then row.fwModel:Hide() end
row.icon:SetAlpha(1)
end
end
local function UpdateRowModels()
UpdateRowIcons()
ResolveNext()
end
f.UpdateRowModels = UpdateRowModels
if listScroll.scroll.HookScript then
listScroll.scroll:HookScript("OnVerticalScroll", function() UpdateRowModels() end)
end
function RefreshList()
if kind == "items" or kind == "units" or kind == "objects" then
local _, ids = ItemIndex()
Codex:LoadItemInfo(ids)
if kind == "items" then Codex:CaptureStats(ids) end
end
local entries = SortedEntries()
-- Namen der Katalog-Ziele beim Server nachfragen (je Aufruf wenige, jede ID nur einmal)
if (kind == "units" or kind == "objects") and Codex.Catalog and Codex.Catalog.Reverse then
local asked = 0
for _, e in ipairs(entries) do
if asked >= 12 then break end
if e.title:sub(1, 1) == "#" and not srvAsked[(kind == "units" and "u" or "o") .. e.id] then
ServerInfo(kind, e.id, true)
asked = asked + 1
end
end
if asked > 0 and _G.C_Timer then
_G.C_Timer.After(1.6, function() catEnt = {} dataVersion = dataVersion + 1 if f:IsShown() and RefreshList then RefreshList() end end)
end
end
local total = #entries
local headers = 0
if kind == "quests" and CurrentSort() == "zone" then
for _, e in ipairs(entries) do
if e.header then headers = headers + 1 end
end
end
local shown = math.min(total, MaxRows())
for i = 1, shown do
local e = entries[i]
local row = listRows[i]
if not row then
row = UI.CreatePanel(listScroll.child, "card", "bronze")
row:SetHeight(ROW_H)
row:EnableMouse(true)
row.icon = row:CreateTexture(nil, "ARTWORK")
row.icon:SetSize(30, 30)
row.icon:SetPoint("LEFT", row, "LEFT", 7, 0)
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.title = UI.CreateLabel(row, "", { size = 12 })
row.title:SetPoint("TOPLEFT", row, "TOPLEFT", 46, -7)
row.title:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -7)
row.title:SetWordWrap(false)
row.sub = UI.CreateLabel(row, "", { size = 10, color = "textDim" })
row.sub:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 46, 7)
row.sub:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -8, 7)
row.sub:SetWordWrap(false)
-- Quest-Status rechts: Haken (erledigt) oder Sprint-Symbol (in Arbeit)
row.mark = row:CreateTexture(nil, "OVERLAY")
row.mark:SetSize(18, 18)
row.mark:SetPoint("RIGHT", row, "RIGHT", -8, 0)
row.mark:Hide()
row.fav = row:CreateTexture(nil, "OVERLAY")
row.fav:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcon_1")
row.fav:SetSize(14, 14)
row.fav:Hide()
row:SetScript("OnMouseUp", function(self)
if self.headerKey then
collapsed[self.headerKey] = not collapsed[self.headerKey]
RefreshList()
return
end
selected = self.entryID
RefreshList()
ShowDetail()
if nameCtl then nameCtl.Update() end
end)
row:SetScript("OnEnter", function(self)
UI.SetBorderColor(self, "gold")
if self.itemID and GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
local ok = pcall(GameTooltip.SetItemByID, GameTooltip, self.itemID)
if not ok then pcall(GameTooltip.SetHyperlink, GameTooltip, "item:" .. self.itemID) end
GameTooltip:Show()
end
end)
row:SetScript("OnLeave", function(self)
UI.SetBorderColor(self, (self.entryID == selected or self.headerKey) and "gold" or "bronze")
if GameTooltip then GameTooltip:Hide() end
end)
listRows[i] = row
end
row.entryID = e.id
row.headerKey = e.header
row.itemID = e.itemID
row.unitID = e.unitID
row.dispShown = nil
row:ClearAllPoints()
local y = (i - 1) * (ROW_H + ROW_GAP)
row:SetPoint("TOPLEFT", listScroll.child, "TOPLEFT", 0, -y)
row:SetPoint("TOPRIGHT", listScroll.child, "TOPRIGHT", 0, -y)
row.title:SetText(e.header and ("|cffffd100" .. e.title .. "|r") or e.title)
row.sub:SetText(e.sub or "")
local mark = (kind == "quests" and not e.header) and STATUS_MARK[Codex.QuestStatus(e.id) or ""] or nil
row.markState = mark
local isFav = (not e.header) and Codex.FavSupported(kind) and Codex.IsFav(kind, e.id)
row.fav:ClearAllPoints()
row.fav:SetPoint("RIGHT", row, "RIGHT", mark and -30 or -8, 0)
row.fav:SetShown(isFav and true or false)
local rightGap = (mark and 32 or 8) + (isFav and 18 or 0)
row.title:ClearAllPoints()
row.title:SetPoint("TOPLEFT", row, "TOPLEFT", 46, -7)
row.title:SetPoint("TOPRIGHT", row, "TOPRIGHT", -rightGap, -7)
row.sub:ClearAllPoints()
row.sub:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 46, 7)
row.sub:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -rightGap, 7)
if mark then
row.mark:SetTexture(mark)
row.mark:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.mark:Show()
else
row.mark:Hide()
end
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
if e.itemID then
row.icon:SetTexture(ICON.items)
local q = Codex.QualityOf(e.itemID)
if q then row.title:SetText(Colored(e.title, q)) end
Util.RequestItem(e.itemID, function(info)
if row.entryID == e.itemID and info then
Codex.RememberQuality(e.itemID, info.quality)
row.icon:SetTexture(info.icon)
row.title:SetText(Colored(info.name, info.quality))
end
end)
else
row.icon:SetTexture(e.icon)
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
end
UI.SetBorderColor(row, (e.id == selected or e.header) and "gold" or "bronze")
if e.header then row.icon:SetTexCoord(0, 1, 0, 1) end
row:Show()
end
for i = shown + 1, #listRows do
listRows[i]:Hide()
end
listScroll:SetContentHeight(shown * (ROW_H + ROW_GAP))
UpdateRowModels()
local q, u, o = Codex:Counts()
if total == 0 then
listEmpty:SetText(kind == "learn" and #Codex.LearnClasses() == 0 and L.CODEX_LEARN_EMPTY
or (q + u + o) == 0 and L.CODEX_EMPTY or L.CODEX_NO_MATCH)
listEmpty:Show()
countLabel:SetText("")
else
listEmpty:Hide()
local text = total > shown and string.format(L.CODEX_COUNT_MORE, total - headers, shown - headers) or string.format(L.CODEX_COUNT, total - headers)
local pending = Codex.runtime.pendingInfo or 0
if pending > 0 and kind == "items" then
text = text .. " | " .. string.format(L.CODEX_LOADING_ITEMS, pending)
end
countLabel:SetText(text)
end
end
function Refresh()
LayoutKinds()
for id, b in pairs(kindButtons) do
b:SetStyle(id == kind and "primary" or "default")
end
if #history > 0 then backButton:Show() else backButton:Hide() end
sortDD:SetValue(CurrentSort())
local asc = SORT_ASC[CurrentSort()] == true
if sortRev[kind] then asc = not asc end
dirButton.arrow:SetTexCoord(0, 1, asc and 0 or 1, asc and 1 or 0)
Layout()
UpdateFilterUI()
RefreshList()
ShowDetail()
if nameCtl then nameCtl.Update() end
end
function Go(newKind, id)
if selected ~= nil and (kind ~= newKind or selected ~= id) then
history[#history + 1] = { kind = kind, id = selected }
if #history > 30 then table.remove(history, 1) end
end
kind, selected = newKind, id
edit:SetText("")
edit:ClearFocus()
Refresh()
end
f.Refresh = function()
dirty = true
dataVersion = dataVersion + 1
Refresh()
end
f.Go = Go
-- fuer Tests
f.Entries = SortedEntries
f.SetFilter = function(t)
flt = t or {}
flt.q = flt.q or {}
Refresh()
end
f.SetSort = function(mode, reverse)
sortBy[kind] = mode
sortRev[kind] = reverse == true
Refresh()
end
f.ToggleZone = function(zone) collapsed[zone] = not collapsed[zone]; Refresh() end
f.ToggleRoute = function() routeOpen = not routeOpen; RouteSync(); Layout(); return routeOpen end
f.RouteLevels = function() return routeLvl, routeOpen end
f.SetRouteRange = function(on)
if on then routeLvl.below, routeLvl.above = 3, 3 else routeLvl.below, routeLvl.above = nil, nil end
RouteSync()
end
f.SetFarm = function(on)
farmOn = on and true or false
if farmOn then roleSel.units, sortBy.units, sortRev.units = "combat", "gpm", false end
Refresh()
end
f.Back = function()
local prev = table.remove(history)
if prev then
kind, selected = prev.kind, prev.id
Refresh()
end
return prev ~= nil
end
f.HistoryCount = function() return #history end
f.SetLearnClass = function(token) learnSel = token; selected = nil; dataVersion = dataVersion + 1; Refresh() end
f.SetFavOnly = function(on) favOnly[kind] = on and true or false; dataVersion = dataVersion + 1; Refresh() end
f.Kind = function() return kind end
f.SetRole = function(role)
roleSel[kind] = role
Refresh()
end
f.FilterText = function() return StatPreview(flt) end
f.SetStatus = function(status)
statusSel = status
Refresh()
end
f.SetChain = function(chain)
chainSel = chain
Refresh()
end
f.SetSearch = function(text)
search = text or ""
Refresh()
end
-- Text der angezeigten Detail-Zeilen (links | rechts), fuer Tests
f.DetailText = function()
local t = {}
for i = 1, D.used do
t[#t + 1] = (detailRows[i].left:GetText() or "") .. "|" .. (detailRows[i].right:GetText() or "")
end
return table.concat(t, "\n")
end
FW:On("CODEX_CHANGED", function()
dirty = true
dataVersion = dataVersion + 1
if f:IsShown() then
RefreshList()
end
end, f)
FW:On("CODEX_ITEMS_LOADED", function()
dataVersion = dataVersion + 1
if f:IsShown() and (kind == "items" or kind == "units" or kind == "objects") then
RefreshList()
if (kind == "units" or kind == "objects") and selected and detailMissing then
ShowDetail()
end
end
end, f)
return f
end
local built
UI:RegisterTab("codex", {
label = function()
return FW.L.CODEX_TAB
end,
order = 30,
build = function(content)
built = Build(content)
Codex.ui = built
return built
end,
onShow = function(f)
f.Refresh()
end,
})
------------------------------------------------------------------------
-- /fwcodex near <rolle>: naechste Haendler, Flugmeister ... auf dieser Karte (Vorlage)
------------------------------------------------------------------------
local NEAR_WORDS = {
{ "vendor", "haendler", "händler", "vendor", "shop" }, { "repair", "repair", "reparatur", "rep" },
{ "flight", "flug", "flight", "fm" }, { "innkeeper", "gast", "inn", "wirt" },
{ "stablemaster", "stall", "stable" }, { "banker", "bank" }, { "auctioneer", "auktion", "auction", "ah" },
{ "mailbox", "post", "mail", "brief" }, { "spirithealer", "geist", "spirit", "heiler" },
}
function Codex.Near(word)
local L = FW.L
word = (word or ""):lower()
local role
for _, def in ipairs(NEAR_WORDS) do
for i = 2, #def do
if word == def[i] then role = def[1] end
end
end
if not role then
FW:Print(L.CODEX_NEAR_HELP)
return
end
local list = Codex.Seed.Available() and Codex.Seed.Nearest(role, 5) or {}
if #list == 0 then
FW:Print(L.CODEX_NEAR_NONE)
return
end
FW:Print(string.format(L.CODEX_NEAR_HEAD, L["CODEX_ROLE_" .. role] or role))
local points = {}
for _, e in ipairs(list) do
local name = (Codex.NameOf(Codex.V()[e.kind][e.id])) or Codex.Seed.Name(e.kind, e.id) or ("#" .. e.id)
FW:Print(string.format(L.CODEX_NEAR_ROW, name, e.x * 100, e.y * 100))
points[#points + 1] = { m = e.m, x = e.x, y = e.y, n = 4, pin = true, name = name }
end
if Codex.Map and Codex.Map.Show then
Codex.Map:Show(points, L["CODEX_ROLE_" .. role] or role)
end
end
------------------------------------------------------------------------
-- Befehl
------------------------------------------------------------------------
SLASH_FREEWOODCODEX1 = "/fwcodex"
SlashCmdList["FREEWOODCODEX"] = function(msg)
msg = (msg or ""):lower():match("^%s*(.-)%s*$")
if msg == "test" then
Codex:RunTest()
elseif msg == "share log" then
if Codex.Sync then Codex.Sync:SetLog(not Codex.Sync.state.log) end
elseif msg == "share on" or msg == "share off" then
if Codex.Sync then
Codex.Sync:SetEnabled(msg == "share on")
Codex.Sync:Debug()
end
elseif msg == "enc" then
if Codex.Sync then Codex.Sync:EncTest() end
elseif msg == "share" then
if Codex.Sync then Codex.Sync:Debug() end
elseif msg == "time" then
if Codex.Time then Codex.Time:Debug() end
elseif msg == "icons" then
local st = Codex.dispStats
local n = 0
for _ in pairs(Codex.db.disp or {}) do n = n + 1 end
FW:Print(string.format("Kreatur-Bilder: %d Display-IDs gespeichert, gelesen %d, nicht lesbar %d, in der Liste gesetzt %d", n, st.read, st.fail, st.shown))
elseif msg == "seed" then
if Codex.Seed and Codex.Seed.Available() then
FW:Print(string.format(FW.L.CODEX_SEED_STATUS, Codex.Seed.Count()))
else
FW:Print(FW.L.CODEX_SEED_MISSING)
end
elseif msg == "roles" then
Codex.Roles:Debug()
elseif msg == "trainers" then
-- Diagnose Lernplan: alle erfassten Lehrer mit Titel, Klasse und Anzahl Zauber
local n = 0
for id, u in pairs(Codex.V().units) do
if u.ro and ((u.ro.classtrainer or 0) > 0 or (u.ro.proftrainer or 0) > 0) then
n = n + 1
local cnt = 0
for _ in pairs(u.tr or {}) do cnt = cnt + 1 end
FW:Print(string.format("#%d %s | Titel: %s | Klasse: %s (Titel: %s, Spieler: %s) | Zauber: %d | Rolle: %s", id, tostring(u.n), tostring(u.ti),
tostring(Codex.ClassOfTrainer(u)), tostring(Codex.ClassOfTitle(u.ti)), tostring(u.trc), cnt, (u.ro.classtrainer or 0) > 0 and "Klasse" or "Beruf"))
end
end
FW:Print(n .. " Lehrer erfasst.")
elseif msg:find("^weapon") then
local id = tonumber(msg:match("item:(%d+)")) or tonumber(msg:match("(%d+)"))
if id then Codex.WeaponDebug(id) else FW:Print("/fwcodex weapon <Item-ID oder Link>") end
elseif msg:find("^itemload") then
if msg:find("stop") then Codex.ItemLoadStop()
elseif msg:find("status") then Codex.ItemLoadStatus()
else Codex.ItemLoadStart(Codex.AllItemIds()) end
elseif msg:find("^tipscan") then
if msg:find("stop") then Codex.TipScanStop() else local _, ids = ItemIndex() Codex.TipScan(ids) end
elseif msg:find("^near") then
Codex.Near(msg:match("^near%s*(.-)$"))
elseif msg == "names" then
if Codex.Names then Codex.Names:Debug() end
elseif msg == "map" then
Codex.Map:Debug()
elseif msg == "map test" then
Codex.Map:Test()
elseif msg == "map square" then
Codex.Map:ToggleSquare()
else
UI:Toggle("codex")
end
end
------------------------------------------------------------------------
-- Einstellungen: Anzahl der Listenzeilen in der Datenbank
------------------------------------------------------------------------
if UI.RegisterSettingsSection then
UI:RegisterSettingsSection("codex-list", {
order = 104,
height = 110,
build = function(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_LISTMAX_HEADER)
stack:Add(UI.CreateLabel(f, L.CODEX_LISTMAX_NOTE, { size = 11, color = "textDim", justifyV = "TOP" }), { fit = true, gap = 8 })
local val = UI.CreateLabel(f, "", { size = 12 })
stack:Add(val, { fit = true, gap = 6 })
local slider = UI.CreateSlider(f, 240, {
min = 100, max = 6000, step = 100,
get = function() return MaxRows() end,
set = function(v)
Codex.db.settings.listMax = v
val:SetText(string.format(L.CODEX_LISTMAX_VALUE, v))
end,
})
slider.onText = function(v) val:SetText(string.format(L.CODEX_LISTMAX_VALUE, v)) end
stack:Add(slider, { height = 20, gap = 4 })
stack:Layout()
f.slider = slider
return f
end,
refresh = function(frame) frame.slider:Refresh() end,
})
end