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