1290 lines
45 KiB
Lua
1290 lines
45 KiB
Lua
-- Freewood-OctoWoW-Codex: Rollen (Haendler, Lehrer, Flugmeister ...)
|
|
-- Lernt beim Ansprechen von NPCs und Objekten:
|
|
-- units[id].ro[rolle] = Anzahl Beobachtungen (vendor, repair, flight, innkeeper, stablemaster, spirithealer,
|
|
-- auctioneer, battlemaster, banker, classtrainer, proftrainer); objects[id].ro: mailbox
|
|
-- units[id].ti = Titelzeile ("Waffenschmied")
|
|
-- units[id].wa[itemID] = { pr = { [preis] = Anzahl }, st = Stapel, lim = 1 (begrenzt), ec = 1 (Tauschkosten) }
|
|
-- alle beobachteten Preise (Ruf und Talente senken Preise); Prozent = Preis / hoechster Preis
|
|
-- units[id].tr[schluessel] = { nm, rk, lv = Stufe, sn = Fertigkeit, sr = Fertigkeitswert, pr = { [kosten] = Anzahl } }
|
|
-- units[id].fl[ziel] = { [preis] = Anzahl } Flugziele, units[id].fn = eigener Flugpunkt
|
|
-- Der Ort (Spielerposition beim Ansprechen) landet wie bei allen Datensaetzen in cells.
|
|
-- Alles laeuft geschuetzt; unbekannte Ereignisse werden vom Spiel einfach nicht angeboten.
|
|
|
|
local ADDON, NS = ...
|
|
|
|
local FW = _G.Freewood
|
|
local Codex = NS and NS.Codex
|
|
if not FW or not Codex then
|
|
return
|
|
end
|
|
|
|
local Util = FW.Util
|
|
|
|
local Roles = {}
|
|
NS.Roles = Roles
|
|
Codex.Roles = Roles
|
|
|
|
local MAX_WARES = 250
|
|
local MAX_PRICES = 6
|
|
local MAX_TRAIN = 160
|
|
local MAX_DEST = 60
|
|
local MAX_COUNT = 1000
|
|
local DEBOUNCE = 5
|
|
|
|
Roles.events = {} -- Diagnose: wie oft kam welches Ereignis
|
|
Roles.gossip = {} -- Diagnose: gesehene Gespraechs-Arten
|
|
Roles.last = nil -- zuletzt erfasste Rolle (Diagnose)
|
|
Roles.trainerSources = {} -- Diagnose: woher die Zauber-IDs der Lehrer kamen
|
|
|
|
local debounce = {}
|
|
local visit = {} -- pro Besuch: bereits gezaehlte Preise
|
|
|
|
local function Now()
|
|
return (GetTime and GetTime()) or 0
|
|
end
|
|
|
|
local function Str(v, max)
|
|
return Codex.Str(v, max)
|
|
end
|
|
|
|
local function Num(v)
|
|
if Util.IsSecret(v) or type(v) ~= "number" or v ~= v then
|
|
return nil
|
|
end
|
|
return v
|
|
end
|
|
|
|
local function ItemIDFromLink(link)
|
|
link = Util.Clean(link)
|
|
if type(link) ~= "string" then
|
|
return nil
|
|
end
|
|
return tonumber(link:match("item:(%d+)"))
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Wer wird gerade angesprochen
|
|
------------------------------------------------------------------------
|
|
|
|
-- Rueckgabe: Art ("npc"/"object"), ID, Name, Einheit
|
|
local function Resolve(allowObject, objectOnly)
|
|
for _, unit in ipairs(objectOnly and {} or { "npc", "target" }) do
|
|
local guid = UnitGUID and UnitGUID(unit)
|
|
local kind, id = Codex.ParseGUID(guid)
|
|
if kind then
|
|
local name = UnitName and Str((UnitName(unit)), 60) or nil
|
|
return kind, id, name, unit
|
|
end
|
|
end
|
|
local o = Codex.lastObject
|
|
if allowObject and o and Now() - (o.t or 0) < 10 then
|
|
return "object", o.id, o.name, nil
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function Entity(kind, id)
|
|
if kind == "npc" then
|
|
return Codex:Unit(id, true)
|
|
end
|
|
return Codex:Object(id, true)
|
|
end
|
|
|
|
-- Titelzeile ("Waffenschmied") aus der zweiten Tooltip-Zeile; Stufen- und Fraktionszeilen zaehlen nicht
|
|
local function TitleFrom(text)
|
|
text = Str(text, 60)
|
|
if not text then
|
|
return nil
|
|
end
|
|
text = text:gsub("^<", ""):gsub(">$", "")
|
|
text = text:gsub("^%s+", ""):gsub("%s+$", "")
|
|
if text == "" or text:find("%d") or text:find("%?%?") or #text > 40 then
|
|
return nil
|
|
end
|
|
local level = _G.LEVEL
|
|
if type(level) == "string" and text:sub(1, #level) == level then
|
|
return nil
|
|
end
|
|
return text
|
|
end
|
|
|
|
local function ReadTitle(unit)
|
|
if not unit or not (C_TooltipInfo and C_TooltipInfo.GetUnit) then
|
|
return nil
|
|
end
|
|
local ok, data = pcall(C_TooltipInfo.GetUnit, unit)
|
|
if not ok or type(data) ~= "table" or Util.IsSecret(data) then
|
|
return nil
|
|
end
|
|
if TooltipUtil and TooltipUtil.SurfaceArgs then
|
|
pcall(TooltipUtil.SurfaceArgs, data)
|
|
end
|
|
local line = type(data.lines) == "table" and data.lines[2] or nil
|
|
if type(line) ~= "table" then
|
|
return nil
|
|
end
|
|
return TitleFrom(line.leftText)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Rolle vermerken
|
|
------------------------------------------------------------------------
|
|
|
|
-- Gibt die Einheit/das Objekt zurueck (oder nil), vermerkt Rolle, Namen, Titel und Ort (hoechstens alle 5 s je Rolle)
|
|
function Roles:Mark(role, allowObject)
|
|
local kind, id, name, unit = Resolve(allowObject, role == "mailbox")
|
|
if not kind then
|
|
return nil
|
|
end
|
|
if kind == "object" and role ~= "mailbox" then
|
|
return nil
|
|
end
|
|
if kind == "npc" and role == "mailbox" then
|
|
return nil
|
|
end
|
|
local ent = Entity(kind, id)
|
|
if name then
|
|
ent.n = name
|
|
end
|
|
if kind == "npc" then
|
|
local title = ReadTitle(unit)
|
|
if title then
|
|
ent.ti = title
|
|
end
|
|
end
|
|
local key = kind .. id .. role
|
|
local t = Now()
|
|
if not debounce[key] or t - debounce[key] > DEBOUNCE then
|
|
debounce[key] = t
|
|
ent.ro = ent.ro or {}
|
|
ent.ro[role] = math.min((ent.ro[role] or 0) + 1, MAX_COUNT)
|
|
Codex.AddCell(ent)
|
|
Roles.last = { role = role, kind = kind, id = id }
|
|
end
|
|
FW:Fire("CODEX_CHANGED", kind == "npc" and "unit" or "object", id)
|
|
return ent, kind, id
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Haendler
|
|
------------------------------------------------------------------------
|
|
|
|
local function AddPrice(prices, price)
|
|
if prices[price] then
|
|
prices[price] = math.min(prices[price] + 1, MAX_COUNT)
|
|
return
|
|
end
|
|
local n = 0
|
|
for _ in pairs(prices) do
|
|
n = n + 1
|
|
end
|
|
if n < MAX_PRICES then
|
|
prices[price] = 1
|
|
end
|
|
end
|
|
|
|
local function MerchantInfo(i)
|
|
local name, price, stack, avail, ec
|
|
-- Zuerst die native 1.12-Funktion (Name, Textur, Preis, Menge, Vorrat)
|
|
if GetMerchantItemInfo then
|
|
local ok, n, _, p, q, a = pcall(GetMerchantItemInfo, i)
|
|
if ok then
|
|
name, price, stack, avail = n, p, q, a
|
|
end
|
|
end
|
|
-- Luecken mit der Tabellen-Abfrage fuellen (falls vorhanden)
|
|
if (name == nil or price == nil) and C_MerchantFrame and C_MerchantFrame.GetItemInfo then
|
|
local ok, info = pcall(C_MerchantFrame.GetItemInfo, i)
|
|
if ok and type(info) == "table" and not Util.IsSecret(info) then
|
|
if name == nil then name = info.name end
|
|
if price == nil then price = info.price end
|
|
if stack == nil then stack = info.stackCount end
|
|
if avail == nil then avail = info.numAvailable end
|
|
ec = info.hasExtendedCost
|
|
end
|
|
end
|
|
if ec == nil and GetMerchantItemCostInfo then
|
|
local ok, count = pcall(GetMerchantItemCostInfo, i)
|
|
ec = ok and type(count) == "number" and count > 0
|
|
end
|
|
return Str(name, 80), Num(price), Num(stack), Num(avail), ec == true
|
|
end
|
|
|
|
local function MerchantItemID(i)
|
|
local link
|
|
if GetMerchantItemLink then
|
|
link = GetMerchantItemLink(i)
|
|
end
|
|
local id = ItemIDFromLink(link)
|
|
if id then
|
|
return id
|
|
end
|
|
if C_MerchantFrame and C_MerchantFrame.GetItemInfo then
|
|
local ok, info = pcall(C_MerchantFrame.GetItemInfo, i)
|
|
if ok and type(info) == "table" then
|
|
return Num(info.itemID) or ItemIDFromLink(info.link)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Ruf-Anforderung einer Ware aus dem Tooltip des Haendlerfensters ("Benoetigt Fraktion - Stufe").
|
|
-- Rueckgabe: Tabelle { f, s, ok } | false (kein Ruf noetig) | nil (Tooltip nicht lesbar)
|
|
local function ReqPattern()
|
|
local fmt = _G.ITEM_REQ_REPUTATION
|
|
if type(fmt) ~= "string" or not fmt:find("%%s") then return nil end
|
|
local esc = fmt:gsub("([%^%$%(%)%.%[%]%*%+%-%?])", "%%%1"):gsub("%%s", "(.+)")
|
|
return "^" .. esc .. "$"
|
|
end
|
|
|
|
local function StandingIndex(text)
|
|
local low = tostring(text or ""):lower()
|
|
for i = 1, 8 do
|
|
for _, key in ipairs({ "FACTION_STANDING_LABEL" .. i, "FACTION_STANDING_LABEL" .. i .. "_FEMALE" }) do
|
|
local label = _G[key]
|
|
if type(label) == "string" and label:lower() == low then return i end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function IsRed(c)
|
|
return type(c) == "table" and (c.r or c[1] or 0) > 0.8 and (c.g or c[2] or 1) < 0.3 and (c.b or c[3] or 1) < 0.3
|
|
end
|
|
|
|
function Codex.ReputationRequirement(data)
|
|
if type(data) ~= "table" or type(data.lines) ~= "table" then return nil end
|
|
local pat = ReqPattern() or "^Requires (.+) %- (.+)$"
|
|
for _, line in ipairs(data.lines) do
|
|
local text = type(line) == "table" and Str(line.leftText, 120)
|
|
if text then
|
|
local fac, stand = text:match(pat)
|
|
if not fac then fac, stand = text:match("^Ben\195\182tigt (.+) %- (.+)$") end
|
|
if not fac then
|
|
-- Ersatz: "<Fraktion> - <Ruf-Stufe>" mit einer bekannten Ruf-Stufe am Ende
|
|
local f2, s2 = text:match("^(.-)%s+[%-\226\128\147]+%s+(.+)$")
|
|
if f2 and s2 and StandingIndex(s2) then fac, stand = f2, s2 end
|
|
end
|
|
if fac and stand then
|
|
local idx = StandingIndex(stand)
|
|
if idx then
|
|
return { f = fac:sub(1, 40), s = idx, ok = not IsRed(line.leftColor) }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function MerchantTooltip(i)
|
|
if not (C_TooltipInfo and C_TooltipInfo.GetMerchantItem) then return nil end
|
|
local ok, data = pcall(C_TooltipInfo.GetMerchantItem, i)
|
|
if not ok or type(data) ~= "table" or Util.IsSecret(data) then return nil end
|
|
if TooltipUtil and TooltipUtil.SurfaceArgs then pcall(TooltipUtil.SurfaceArgs, data) end
|
|
return data
|
|
end
|
|
|
|
-- /fwruf: zeigt fuer die ersten Waren des offenen Haendlers die Tooltip-Zeilen und was daraus gelesen wurde
|
|
function Roles.RepDiag()
|
|
if not (GetMerchantNumItems and (GetMerchantNumItems() or 0) > 0) then
|
|
FW:Print("Ruf: Bitte zuerst ein Haendlerfenster oeffnen.")
|
|
return
|
|
end
|
|
if not (C_TooltipInfo and C_TooltipInfo.GetMerchantItem) then
|
|
FW:Print("Ruf: C_TooltipInfo.GetMerchantItem gibt es hier nicht - Ruf-Zeilen koennen nicht gelesen werden.")
|
|
return
|
|
end
|
|
FW:Print("Ruf: Muster = " .. tostring(_G.ITEM_REQ_REPUTATION) .. " | Stufen: " .. tostring(_G.FACTION_STANDING_LABEL5) .. ", " .. tostring(_G.FACTION_STANDING_LABEL6))
|
|
local shown = 0
|
|
for i = 1, math.min(GetMerchantNumItems(), MAX_WARES) do
|
|
local tip = MerchantTooltip(i)
|
|
local rq = tip and Codex.ReputationRequirement(tip)
|
|
if rq or shown < 2 then
|
|
shown = shown + 1
|
|
local texts = {}
|
|
for _, line in ipairs(tip and tip.lines or {}) do
|
|
if type(line) == "table" and type(line.leftText) == "string" then texts[#texts + 1] = line.leftText end
|
|
end
|
|
FW:Print(string.format("Ruf: Ware %d: %s -> %s", i, #texts > 0 and table.concat(texts, " | "):sub(1, 160) or "(kein Tooltip)",
|
|
rq and string.format("%s Stufe %d %s", rq.f, rq.s, rq.ok and "erfuellt" or "NICHT erfuellt") or (rq == false and "kein Ruf noetig" or "nicht lesbar")))
|
|
end
|
|
if shown >= 8 then break end
|
|
end
|
|
end
|
|
SLASH_FREEWOODREPDIAG1 = "/fwruf"
|
|
SlashCmdList["FREEWOODREPDIAG"] = function() Roles.RepDiag() end
|
|
|
|
function Roles:ScanMerchant()
|
|
local count = 0
|
|
if GetMerchantNumItems then
|
|
count = GetMerchantNumItems() or 0
|
|
elseif C_MerchantFrame and C_MerchantFrame.GetNumItems then
|
|
count = C_MerchantFrame.GetNumItems() or 0
|
|
end
|
|
if Util.IsSecret(count) or type(count) ~= "number" or count <= 0 then
|
|
return
|
|
end
|
|
local ent, _, vid = self:Mark("vendor")
|
|
if not ent then
|
|
return
|
|
end
|
|
-- Fraktion des Haendlers (Horde / Allianz) merken
|
|
if vid and UnitFactionGroup then
|
|
local ok, group = pcall(UnitFactionGroup, "npc")
|
|
if ok and not Util.IsSecret(group) and (group == "Alliance" or group == "Horde") then
|
|
Codex.db.vfac = Codex.db.vfac or {}
|
|
Codex.db.vfac[vid] = group == "Alliance" and "A" or "H"
|
|
end
|
|
end
|
|
local wrq
|
|
if vid then
|
|
Codex.db.wrq = Codex.db.wrq or {}
|
|
wrq = Codex.db.wrq[vid]
|
|
end
|
|
if CanMerchantRepair and CanMerchantRepair() then
|
|
self:Mark("repair")
|
|
end
|
|
ent.wa = ent.wa or {}
|
|
local seen = visit.wares or {}
|
|
visit.wares = seen
|
|
local changed = false
|
|
for i = 1, math.min(count, MAX_WARES) do
|
|
local id = MerchantItemID(i)
|
|
local name, price, stack, avail, ec = MerchantInfo(i)
|
|
if i == 1 then
|
|
local lk = GetMerchantItemLink and GetMerchantItemLink(1)
|
|
Roles.diag = { count = count, id = id, name = name, price = price, link = lk and tostring(lk):gsub("|", "!") }
|
|
end
|
|
visit.rep = visit.rep or {}
|
|
if id and wrq ~= false and vid and not visit.rep[id] then
|
|
local tip = MerchantTooltip(i)
|
|
if tip then
|
|
visit.rep[id] = true -- Ruf-Tooltip je Besuch nur einmal lesen (MERCHANT_UPDATE kommt oft)
|
|
local rq = Codex.ReputationRequirement(tip)
|
|
Codex.db.wrq[vid] = Codex.db.wrq[vid] or {}
|
|
wrq = Codex.db.wrq[vid]
|
|
wrq.v = 1
|
|
wrq[id] = rq or nil
|
|
end
|
|
end
|
|
if id and price then
|
|
local ware = ent.wa[id]
|
|
if not ware then
|
|
local n = 0
|
|
for _ in pairs(ent.wa) do
|
|
n = n + 1
|
|
end
|
|
if n < MAX_WARES then
|
|
ware = { pr = {} }
|
|
ent.wa[id] = ware
|
|
end
|
|
end
|
|
if ware then
|
|
Codex.NoteName(id, name)
|
|
if stack and stack > 1 then
|
|
ware.st = math.min(math.floor(stack), 1000)
|
|
end
|
|
if avail and avail >= 0 then
|
|
ware.lim = 1
|
|
end
|
|
if ec then
|
|
ware.ec = 1
|
|
end
|
|
local token = id .. ":" .. math.floor(price)
|
|
if not seen[token] then
|
|
seen[token] = true
|
|
AddPrice(ware.pr, math.floor(price))
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if changed then
|
|
FW:Fire("CODEX_CHANGED", "unit", Roles.last and Roles.last.id)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Flugmeister
|
|
------------------------------------------------------------------------
|
|
|
|
function Roles:ScanTaxi()
|
|
if not NumTaxiNodes then
|
|
return
|
|
end
|
|
local count = NumTaxiNodes()
|
|
if Util.IsSecret(count) or type(count) ~= "number" or count <= 0 then
|
|
return
|
|
end
|
|
local ent = self:Mark("flight")
|
|
if not ent then
|
|
return
|
|
end
|
|
ent.fl = ent.fl or {}
|
|
local seen = visit.taxi or {}
|
|
visit.taxi = seen
|
|
local nDest = 0
|
|
for _ in pairs(ent.fl) do
|
|
nDest = nDest + 1
|
|
end
|
|
for i = 1, math.min(count, 120) do
|
|
local name = TaxiNodeName and Str(TaxiNodeName(i), 60)
|
|
local kind = TaxiNodeGetType and TaxiNodeGetType(i)
|
|
if name and kind == "CURRENT" then
|
|
ent.fn = name
|
|
elseif name and (kind == "REACHABLE" or kind == nil) then
|
|
local cost = Num(TaxiNodeCost and TaxiNodeCost(i)) or 0
|
|
cost = math.floor(cost)
|
|
local dest = ent.fl[name]
|
|
if not dest and nDest < MAX_DEST then
|
|
dest = {}
|
|
ent.fl[name] = dest
|
|
nDest = nDest + 1
|
|
end
|
|
if dest and not seen[name .. ":" .. cost] then
|
|
seen[name .. ":" .. cost] = true
|
|
AddPrice(dest, cost)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Lehrer
|
|
------------------------------------------------------------------------
|
|
|
|
-- Lehrer-Funktionen: erst die klassische Form, dann C_Trainer (Antworten als Liste oder als Tabelle)
|
|
local function TrainerCall(name, ...)
|
|
local fn = _G[name]
|
|
if type(fn) ~= "function" and C_Trainer and type(C_Trainer[name]) == "function" then
|
|
fn = C_Trainer[name]
|
|
end
|
|
if type(fn) ~= "function" then
|
|
return nil
|
|
end
|
|
local r = { pcall(fn, ...) }
|
|
if not r[1] then
|
|
return nil
|
|
end
|
|
table.remove(r, 1)
|
|
return r
|
|
end
|
|
|
|
local function FirstNumber(r, field)
|
|
local v = r and r[1]
|
|
if type(v) == "table" then
|
|
v = v[field] or v.cost or v.moneyCost
|
|
end
|
|
return Num(v)
|
|
end
|
|
|
|
local TRAINER_STATES = { available = true, used = true, unavailable = true, header = true, none = true }
|
|
|
|
-- Rang einer Lehrerzeile aus dem Tooltip (rechte Seite der ersten Zeile, zum Beispiel "Rang 2")
|
|
-- Zauber-ID einer Lehrerzeile: Link der Zeile, Tabellenfeld oder Tooltip-Daten. Rueckgabe: ID, Quelle
|
|
local function TrainerSpell(i, info)
|
|
local r = TrainerCall("GetTrainerServiceItemLink", i)
|
|
local link = r and Util.Clean(r[1])
|
|
if type(link) == "string" then
|
|
local id = tonumber(link:match("spell:(%d+)")) or tonumber(link:match("enchant:(%d+)"))
|
|
if id then
|
|
return id, "link"
|
|
end
|
|
end
|
|
if type(info) == "table" then
|
|
local id = Num(info.spellID) or Num(info.spellId)
|
|
if id and id > 0 then
|
|
return id, "info"
|
|
end
|
|
end
|
|
for _, fname in ipairs({ "GetTrainerServiceSpellID", "GetTrainerServiceSpell" }) do
|
|
local rr = TrainerCall(fname, i)
|
|
local id = rr and Num(rr[1])
|
|
if id and id > 0 then
|
|
return id, fname
|
|
end
|
|
end
|
|
if C_TooltipInfo and C_TooltipInfo.GetTrainerService then
|
|
local ok, data = pcall(C_TooltipInfo.GetTrainerService, i)
|
|
if ok and type(data) == "table" and not Util.IsSecret(data) then
|
|
local id = Num(data.spellID) or Num(data.spellId)
|
|
if not id and Enum and Enum.TooltipDataType and data.type == Enum.TooltipDataType.Spell then
|
|
id = Num(data.id)
|
|
end
|
|
if id and id > 0 then
|
|
return id, "tooltip"
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function SpellRank(id)
|
|
local text
|
|
if C_Spell and C_Spell.GetSpellSubtext then
|
|
local ok, v = pcall(C_Spell.GetSpellSubtext, id)
|
|
text = ok and v or nil
|
|
elseif GetSpellSubtext then
|
|
local ok, v = pcall(GetSpellSubtext, id)
|
|
text = ok and v or nil
|
|
end
|
|
return Str(text, 30)
|
|
end
|
|
|
|
function Codex.SpellRank(id)
|
|
return SpellRank(id)
|
|
end
|
|
|
|
function Codex.SpellIcon(id)
|
|
if C_Spell and C_Spell.GetSpellInfo then
|
|
local ok, info = pcall(C_Spell.GetSpellInfo, id)
|
|
if ok and type(info) == "table" and info.iconID then
|
|
return info.iconID
|
|
end
|
|
end
|
|
if GetSpellTexture then
|
|
local ok, v = pcall(GetSpellTexture, id)
|
|
if ok then
|
|
return v
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function TrainerRank(i)
|
|
if not (C_TooltipInfo and C_TooltipInfo.GetTrainerService) then
|
|
return nil
|
|
end
|
|
local ok, data = pcall(C_TooltipInfo.GetTrainerService, i)
|
|
if not ok or type(data) ~= "table" or Util.IsSecret(data) then
|
|
return nil
|
|
end
|
|
if TooltipUtil and TooltipUtil.SurfaceArgs then
|
|
pcall(TooltipUtil.SurfaceArgs, data)
|
|
end
|
|
for n = 1, 3 do
|
|
local line = type(data.lines) == "table" and data.lines[n] or nil
|
|
if type(line) == "table" then
|
|
local text = Str(line.rightText, 30)
|
|
if text and text:find("%d") and #text <= 20 then
|
|
return (text:gsub("^%(", ""):gsub("%)$", ""))
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Roles:ScanTrainer()
|
|
local trade = TrainerCall("IsTradeskillTrainer")
|
|
local role = (trade and trade[1] == true) and "proftrainer" or "classtrainer"
|
|
local ent = self:Mark(role)
|
|
if not ent then
|
|
return
|
|
end
|
|
local c = TrainerCall("GetNumTrainerServices")
|
|
local count = FirstNumber(c)
|
|
Roles.trainerDiag = string.format("%s: %s", tostring(role), tostring(count))
|
|
if not count or count <= 0 then
|
|
return
|
|
end
|
|
ent.tr = ent.tr or {}
|
|
if Codex.LearnPlanReset then Codex.LearnPlanReset() end
|
|
-- Klasse des Spielers merken: damit gehoert der Lehrer auch dann zu einer Klasse, wenn der Titel unbekannt ist
|
|
if role == "classtrainer" and not ent.trc then
|
|
local token
|
|
if UnitClass then
|
|
local _, t = UnitClass("player")
|
|
token = t
|
|
end
|
|
if type(token) == "string" and not Util.IsSecret(token) and Codex.CLASS_TOKENS[token] then ent.trc = token end
|
|
end
|
|
for key, rec in pairs(ent.tr) do
|
|
if rec.rk and TRAINER_STATES[rec.rk] then
|
|
ent.tr[key] = nil -- aeltere Fehlerfassung: Zustand statt Rang
|
|
end
|
|
end
|
|
local seen = visit.train or {}
|
|
visit.train = seen
|
|
local n = 0
|
|
for _ in pairs(ent.tr) do
|
|
n = n + 1
|
|
end
|
|
for i = 1, math.min(count, 400) do
|
|
local r = TrainerCall("GetTrainerServiceInfo", i) or {}
|
|
local name, rank, category = r[1], r[2], r[3]
|
|
if type(name) == "table" then
|
|
local t = name
|
|
name, rank, category = t.name, t.subText or t.rank or t.subtext, t.category or t.state
|
|
end
|
|
-- Forever liefert (Name, Zustand) ohne Rang: Zustand erkennen, Rang woanders suchen
|
|
if type(rank) == "string" and TRAINER_STATES[rank] then
|
|
category, rank = rank, nil
|
|
end
|
|
name, rank = Str(name, 60), Str(rank, 30)
|
|
local sp, source
|
|
if name and category ~= "header" then
|
|
sp, source = TrainerSpell(i, r[1])
|
|
if sp then
|
|
Roles.trainerSources[source] = (Roles.trainerSources[source] or 0) + 1
|
|
rank = rank or SpellRank(sp)
|
|
end
|
|
if not rank then
|
|
rank = TrainerRank(i)
|
|
end
|
|
end
|
|
if name and category ~= "header" then
|
|
local lv = FirstNumber(TrainerCall("GetTrainerServiceLevelReq", i), "level")
|
|
lv = lv and lv > 0 and math.floor(lv) or nil
|
|
-- Schluessel: Zauber-ID, sonst Name mit Rang, sonst Name mit Stufe
|
|
local plain = name .. (rank and ("#" .. rank) or (lv and ("#L" .. lv)) or "")
|
|
local key = sp and ("s" .. sp) or plain
|
|
if sp then
|
|
ent.tr[plain] = nil -- aeltere Eintraege ohne Zauber-ID
|
|
ent.tr[name .. (lv and ("#L" .. lv) or "")] = nil
|
|
end
|
|
local rec = ent.tr[key]
|
|
if not rec and n < MAX_TRAIN then
|
|
rec = { nm = name, rk = rank, pr = {} }
|
|
ent.tr[key] = rec
|
|
n = n + 1
|
|
end
|
|
if rec and sp then
|
|
rec.sp = sp
|
|
rec.rk = rank or rec.rk
|
|
end
|
|
if rec then
|
|
if lv then
|
|
rec.lv = lv
|
|
end
|
|
local sk = TrainerCall("GetTrainerServiceSkillReq", i)
|
|
if sk then
|
|
local sn, sr = Str(sk[1], 40), Num(sk[2])
|
|
if sn then
|
|
rec.sn, rec.sr = sn, sr and math.floor(sr) or nil
|
|
end
|
|
end
|
|
local cost = FirstNumber(TrainerCall("GetTrainerServiceCost", i), "moneyCost")
|
|
if cost and not seen[key .. ":" .. cost] then
|
|
seen[key .. ":" .. cost] = true
|
|
AddPrice(rec.pr, math.floor(cost))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Gespraeche und allgemeine Fenster
|
|
------------------------------------------------------------------------
|
|
|
|
local GOSSIP_ROLE = {
|
|
binder = "innkeeper", spiritguide = "spirithealer", battlemaster = "battlemaster", banker = "banker",
|
|
auctioneer = "auctioneer", taxi = "flight", vendor = "vendor", stable = "stablemaster", stablemaster = "stablemaster",
|
|
}
|
|
|
|
local function GossipKinds()
|
|
local kinds = {}
|
|
if GetGossipOptions then
|
|
local t = { GetGossipOptions() }
|
|
for i = 2, #t, 2 do
|
|
if type(t[i]) == "string" then
|
|
kinds[#kinds + 1] = t[i]
|
|
end
|
|
end
|
|
end
|
|
if #kinds == 0 and C_GossipInfo and C_GossipInfo.GetOptions then
|
|
local ok, list = pcall(C_GossipInfo.GetOptions)
|
|
if ok and type(list) == "table" then
|
|
local icons
|
|
if Enum and Enum.GossipOptionIcon then
|
|
icons = {}
|
|
for name, value in pairs(Enum.GossipOptionIcon) do
|
|
icons[value] = tostring(name):lower()
|
|
end
|
|
end
|
|
for _, opt in ipairs(list) do
|
|
if type(opt) == "table" then
|
|
local kind = type(opt.type) == "string" and opt.type or (icons and icons[opt.icon]) or nil
|
|
if kind then
|
|
kinds[#kinds + 1] = kind:lower()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return kinds
|
|
end
|
|
|
|
function Roles:OnGossip()
|
|
local done = {}
|
|
for _, kind in ipairs(GossipKinds()) do
|
|
Roles.gossip[kind] = (Roles.gossip[kind] or 0) + 1
|
|
local role = GOSSIP_ROLE[kind]
|
|
if not role then
|
|
for word, r in pairs(GOSSIP_ROLE) do
|
|
if kind:find(word, 1, true) then
|
|
role = r
|
|
end
|
|
end
|
|
end
|
|
if role and not done[role] then
|
|
done[role] = true
|
|
self:Mark(role)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- PLAYER_INTERACTION_MANAGER_FRAME_SHOW: Enum.PlayerInteractionType -> Rolle
|
|
local INTERACTION_ROLE = {
|
|
Banker = "banker", Auctioneer = "auctioneer", TaxiNode = "flight", StableMaster = "stablemaster",
|
|
SpiritHealer = "spirithealer", Binder = "innkeeper", BattleMaster = "battlemaster", MailInfo = "mailbox",
|
|
Merchant = "vendor",
|
|
}
|
|
|
|
function Roles:OnInteraction(ty)
|
|
local names = Roles.interactionNames
|
|
if not names then
|
|
names = {}
|
|
if Enum and Enum.PlayerInteractionType then
|
|
for name, value in pairs(Enum.PlayerInteractionType) do
|
|
names[value] = name
|
|
end
|
|
end
|
|
Roles.interactionNames = names
|
|
end
|
|
local name = names[ty]
|
|
if name then
|
|
Roles.gossip["frame:" .. name] = (Roles.gossip["frame:" .. name] or 0) + 1
|
|
end
|
|
if name == "Trainer" then
|
|
Roles.trainerOpen = true
|
|
visit.train = nil
|
|
self:ScanTrainer()
|
|
return
|
|
end
|
|
local role = name and INTERACTION_ROLE[name]
|
|
if role and role ~= "vendor" and role ~= "flight" then
|
|
self:Mark(role, role == "mailbox")
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Start / Stopp
|
|
------------------------------------------------------------------------
|
|
|
|
local SIMPLE = {
|
|
PET_STABLE_SHOW = "stablemaster",
|
|
AUCTION_HOUSE_SHOW = "auctioneer",
|
|
BANKFRAME_OPENED = "banker",
|
|
BATTLEFIELDS_SHOW = "battlemaster",
|
|
MAIL_SHOW = "mailbox",
|
|
}
|
|
|
|
local EVENTS = { "MERCHANT_SHOW", "MERCHANT_UPDATE", "MERCHANT_CLOSED", "TAXIMAP_OPENED", "TRAINER_SHOW", "TRAINER_UPDATE", "TRAINER_CLOSED",
|
|
"GOSSIP_SHOW", "PLAYER_INTERACTION_MANAGER_FRAME_SHOW" }
|
|
|
|
local function Handler(event)
|
|
return function(_, ...)
|
|
Roles.events[event] = (Roles.events[event] or 0) + 1
|
|
if not (Codex.db and Codex.db.settings and Codex.db.settings.enabled) then
|
|
return
|
|
end
|
|
Codex.Safe(function(...)
|
|
if event == "MERCHANT_SHOW" then
|
|
visit.wares = nil
|
|
visit.rep = nil
|
|
Roles:ScanMerchant()
|
|
elseif event == "MERCHANT_UPDATE" then
|
|
Roles:ScanMerchant()
|
|
elseif event == "MERCHANT_CLOSED" then
|
|
visit.wares = nil
|
|
visit.rep = nil
|
|
elseif event == "TAXIMAP_OPENED" then
|
|
visit.taxi = nil
|
|
Roles:ScanTaxi()
|
|
elseif event == "TRAINER_SHOW" then
|
|
visit.train = nil
|
|
Roles.trainerOpen = true
|
|
Roles:ScanTrainer()
|
|
elseif event == "TRAINER_UPDATE" then
|
|
if Roles.trainerOpen then
|
|
Roles:ScanTrainer()
|
|
end
|
|
elseif event == "TRAINER_CLOSED" then
|
|
Roles.trainerOpen = false
|
|
elseif event == "GOSSIP_SHOW" then
|
|
Roles:OnGossip()
|
|
elseif event == "PLAYER_INTERACTION_MANAGER_FRAME_SHOW" then
|
|
Roles:OnInteraction((...))
|
|
elseif SIMPLE[event] then
|
|
Roles:Mark(SIMPLE[event], event == "MAIL_SHOW")
|
|
end
|
|
end, ...)
|
|
end
|
|
end
|
|
|
|
function Roles:Start()
|
|
if self.started then
|
|
return
|
|
end
|
|
self.started = true
|
|
self.registered = {}
|
|
local function reg(event)
|
|
if FW:RegisterEvent(event, Handler(event), Roles) then
|
|
self.registered[#self.registered + 1] = event
|
|
end
|
|
end
|
|
for _, event in ipairs(EVENTS) do
|
|
reg(event)
|
|
end
|
|
for event in pairs(SIMPLE) do
|
|
reg(event)
|
|
end
|
|
end
|
|
|
|
function Roles:Stop()
|
|
for _, event in ipairs(self.registered or {}) do
|
|
FW:UnregisterEvent(event, Roles)
|
|
end
|
|
self.registered = {}
|
|
self.started = false
|
|
end
|
|
|
|
-- Ausgabe fuer /fwcodex roles
|
|
function Roles:Debug()
|
|
local L = FW.L
|
|
local list = {}
|
|
for event, n in pairs(Roles.events) do
|
|
list[#list + 1] = event .. " x" .. n
|
|
end
|
|
table.sort(list)
|
|
FW:Print(string.format(L.CODEX_ROLES_EVENTS, #list > 0 and table.concat(list, ", ") or "-"))
|
|
local g = {}
|
|
for kind, n in pairs(Roles.gossip) do
|
|
g[#g + 1] = kind .. " x" .. n
|
|
end
|
|
table.sort(g)
|
|
FW:Print(string.format(L.CODEX_ROLES_GOSSIP, #g > 0 and table.concat(g, ", ") or "-"))
|
|
local counts = {}
|
|
for _, group in ipairs({ Codex.db.units, Codex.db.objects }) do
|
|
for _, e in pairs(group) do
|
|
for role in pairs(e.ro or {}) do
|
|
counts[role] = (counts[role] or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
local c = {}
|
|
for role, n in pairs(counts) do
|
|
c[#c + 1] = (L["CODEX_ROLE_" .. role] or role) .. " " .. n
|
|
end
|
|
table.sort(c)
|
|
FW:Print(string.format(L.CODEX_ROLES_COUNTS, #c > 0 and table.concat(c, ", ") or "-"))
|
|
if Roles.trainerDiag then
|
|
local src = {}
|
|
for k, v in pairs(Roles.trainerSources) do src[#src + 1] = k .. " x" .. v end
|
|
table.sort(src)
|
|
FW:Print("Trainer: " .. Roles.trainerDiag .. " | Zauber-IDs: " .. (#src > 0 and table.concat(src, ", ") or "keine"))
|
|
end
|
|
if Roles.last then
|
|
FW:Print(string.format(L.CODEX_ROLES_LAST, tostring(Roles.last.role), tostring(Roles.last.kind), tostring(Roles.last.id)))
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Lesen (fuer die Oberflaeche): eigene + geteilte Daten
|
|
------------------------------------------------------------------------
|
|
|
|
-- Dienste: wer so etwas anbietet, gilt ohne eigene Kampfdaten nicht als Kampfkreatur
|
|
local SERVICE = {
|
|
vendor = true, repair = true, flight = true, innkeeper = true, stablemaster = true, spirithealer = true,
|
|
auctioneer = true, battlemaster = true, banker = true, classtrainer = true, proftrainer = true,
|
|
}
|
|
|
|
-- Rollen einer Kreatur/eines Objekts aus Live-Daten und Vorlage: { rolle = true|Vorlagewert }
|
|
-- kind = "units" | "objects". Zusaetzlich fuer Kreaturen: combat (Kampfkreatur), elite, boss
|
|
function Codex.RolesOf(kind, id)
|
|
local out = {}
|
|
local seedReady = Codex.Seed and Codex.Seed.Ready and Codex.Seed.Ready()
|
|
local seed = seedReady and Codex.Seed.Roles(kind, id) or nil
|
|
for role, v in pairs(seed or {}) do
|
|
out[role] = v
|
|
end
|
|
local e = Codex.V and Codex.V()[kind] and Codex.V()[kind][id]
|
|
if e then
|
|
for role in pairs(e.ro or {}) do
|
|
out[role] = true
|
|
end
|
|
end
|
|
if kind == "units" then
|
|
local fought = e and ((e.k or 0) > 0 or e.tt ~= nil or e.xp ~= nil or e.xm ~= nil or e.d ~= nil)
|
|
local class = e and e.c
|
|
local rank
|
|
local known = fought or class ~= nil
|
|
if seedReady then
|
|
local ent = Codex.Seed.Entity("units", id)
|
|
if ent then
|
|
rank = ent.rank
|
|
known = known or ent.l1 ~= nil
|
|
end
|
|
end
|
|
if class == "rare" or class == "rareelite" or rank == 4 then
|
|
out.rares = true
|
|
end
|
|
if class == "elite" or class == "rareelite" or class == "worldboss" or rank == 1 or rank == 2 or rank == 3 then
|
|
out.elite = true
|
|
end
|
|
if class == "worldboss" or rank == 3 then
|
|
out.boss = true
|
|
end
|
|
local service = false
|
|
for role in pairs(out) do
|
|
if SERVICE[role] then service = true end
|
|
end
|
|
if fought or (known and not service) then
|
|
out.combat = true
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Rollen in fester Reihenfolge (Auswahlliste)
|
|
Roles.ORDER = {
|
|
units = { "combat", "elite", "boss", "vendor", "repair", "flight", "innkeeper", "stablemaster", "spirithealer", "auctioneer",
|
|
"battlemaster", "banker", "classtrainer", "proftrainer", "rares" },
|
|
objects = { "mailbox", "meetingstone", "chests", "herbs", "mines", "fish" },
|
|
}
|
|
-- Nur Auswahl, nicht in Listenzeilen und Details (Rang steht dort schon)
|
|
local FILTER_ONLY = { combat = true, elite = true, boss = true }
|
|
|
|
function Codex.RoleList(kind, id)
|
|
local have = Codex.RolesOf(kind, id)
|
|
local list = {}
|
|
for _, role in ipairs(Roles.ORDER[kind] or {}) do
|
|
if have[role] and not FILTER_ONLY[role] then
|
|
list[#list + 1] = role
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Lernplan: was lernt eine Klasse auf welcher Stufe (aus den Lehrer-Daten)
|
|
------------------------------------------------------------------------
|
|
|
|
-- Titelwoerter je Klasse (Fallback, falls die Klassennamen des Spiels nicht passen). Das laengste Wort gewinnt,
|
|
-- darum steht "teufelsjaeger" vor "jaeger" nicht im Weg.
|
|
local CLASS_WORDS = {
|
|
WARRIOR = { "krieger", "warrior", "guerrier", "guerrero", "guerreiro", "guerriero" },
|
|
PALADIN = { "paladin", "paladín" },
|
|
HUNTER = { "jäger", "jaeger", "hunter", "chasseur", "cazador", "caçador", "cacciatore" },
|
|
ROGUE = { "schurke", "schurken", "rogue", "voleur", "pícaro", "ladino", "ladro" },
|
|
PRIEST = { "priester", "priest", "prêtre", "sacerdote" },
|
|
SHAMAN = { "schamane", "schamanen", "shaman", "chaman", "chamán", "xamã", "sciamano" },
|
|
MAGE = { "magier", "magie", "mage", "mago" },
|
|
WARLOCK = { "hexenmeister", "warlock", "démoniste", "brujo", "bruxo", "stregone" },
|
|
DRUID = { "druide", "druiden", "druid", "druide", "druida" },
|
|
DEMONHUNTER = { "teufelsjäger", "dämonenjäger", "demon hunter", "chasseur de démons", "cazador de demonios" },
|
|
DEATHKNIGHT = { "todesritter", "death knight", "chevalier de la mort", "caballero de la muerte" },
|
|
MONK = { "mönch", "monk", "moine", "monje", "monge", "monaco" },
|
|
EVOKER = { "rufer", "evoker", "évocateur", "evocador" },
|
|
}
|
|
Codex.CLASS_TOKENS = {}
|
|
for token in pairs(CLASS_WORDS) do Codex.CLASS_TOKENS[token] = true end
|
|
|
|
-- Klasse eines Lehrers aus der Titelzeile ("Hexenmeisterlehrer" -> WARLOCK)
|
|
function Codex.ClassOfTitle(title)
|
|
if type(title) ~= "string" or title == "" then return nil end
|
|
local low = title:lower()
|
|
local best, bestLen
|
|
for token, words in pairs(CLASS_WORDS) do
|
|
for _, w in ipairs(words) do
|
|
if #w >= 4 and low:find(w, 1, true) and (not bestLen or #w > bestLen) then
|
|
best, bestLen = token, #w
|
|
end
|
|
end
|
|
end
|
|
for _, names in ipairs({ LOCALIZED_CLASS_NAMES_MALE or {}, LOCALIZED_CLASS_NAMES_FEMALE or {} }) do
|
|
for token, name in pairs(names) do
|
|
if type(name) == "string" and name ~= "" then
|
|
-- auch verkuerzte Formen: "Magier" -> "Magie" (Magielehrer), "Magierin" -> "Magier"
|
|
local full = name:lower()
|
|
local stems = { full, (full:gsub("in$", "")), (full:gsub("er$", "e")) }
|
|
for _, stem in ipairs(stems) do
|
|
if #stem >= 4 and low:find(stem, 1, true) and (not bestLen or #stem > bestLen) then
|
|
best, bestLen = token, #stem
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return best
|
|
end
|
|
|
|
-- Klasse eines Lehrers: aus dem Titel, sonst die Klasse des Spielers, der das Lehrerfenster geoeffnet hat
|
|
-- (Klassenlehrer anderer Klassen lassen sich im Spiel nicht oeffnen)
|
|
function Codex.ClassOfTrainer(u)
|
|
if type(u) ~= "table" or not u.tr then return nil end
|
|
local byTitle = Codex.ClassOfTitle(u.ti)
|
|
if byTitle then return byTitle end
|
|
if u.ro and (u.ro.classtrainer or 0) > 0 and Codex.CLASS_TOKENS[u.trc or ""] then return u.trc end
|
|
return nil
|
|
end
|
|
|
|
function Codex.ModePrice(pr)
|
|
local price, count
|
|
for p, n in pairs(pr or {}) do
|
|
if not count or n > count or (n == count and p > price) then price, count = p, n end
|
|
end
|
|
return price
|
|
end
|
|
|
|
local function ModePrice(pr)
|
|
local price, count
|
|
for p, n in pairs(pr or {}) do
|
|
if not count or n > count or (n == count and p > price) then price, count = p, n end
|
|
end
|
|
return price
|
|
end
|
|
|
|
-- Lehrer fuer Begleiter-Fertigkeiten (Tierausbilder)
|
|
local function IsPetTrainer(u)
|
|
local t = tostring(u and u.ti or ""):lower()
|
|
return t:find("tierausbilder", 1, true) ~= nil or t:find("pet trainer", 1, true) ~= nil or t:find("tiertrainer", 1, true) ~= nil
|
|
end
|
|
|
|
-- Der Plan wird aus allen Lehrerdaten gebaut (ca. 10 ms) und aendert sich selten: Zwischenspeicher mit kurzer Lebensdauer.
|
|
-- Daten-Aenderungen (Scan, Teilen, Bearbeiten) rufen Codex.LearnPlanReset() auf.
|
|
local planCache = {}
|
|
local PLAN_TTL = 20
|
|
function Codex.LearnPlanReset() planCache = {} end
|
|
|
|
local BuildLearnPlan
|
|
|
|
-- Ergebnis: { levels = { [lv] = { lv, spells = {...}, cost, trainers = {id...} } }, order = { lv... } }
|
|
function Codex.LearnPlan(token)
|
|
local now = _G.GetTime and _G.GetTime() or 0
|
|
local c = planCache[token]
|
|
if c and now >= c.t and now - c.t < PLAN_TTL then return c.plan end
|
|
local plan = BuildLearnPlan(token)
|
|
planCache[token] = { t = now, plan = plan }
|
|
return plan
|
|
end
|
|
|
|
BuildLearnPlan = function(token)
|
|
local levels, order = {}, {}
|
|
local seen = {}
|
|
-- Manche Lehrer-Eintraege haben keine Stufe (nicht gelesen). Die Stufe steht aber meist bei einem anderen Lehrer derselben
|
|
-- Klasse: dieselbe Zauber-ID, sonst derselbe Name mit demselben Rang.
|
|
local lvOf = {}
|
|
for _, u in pairs(Codex.V().units) do
|
|
if u.tr and Codex.ClassOfTrainer(u) == token then
|
|
for _, r in pairs(u.tr) do
|
|
if r.lv then
|
|
if r.sp then lvOf["s" .. r.sp] = r.lv end
|
|
if r.nm then lvOf[r.nm .. "|" .. (r.rk or "")] = r.lv end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for id, u in pairs(Codex.V().units) do
|
|
if Codex.ClassOfTrainer(u) == token then
|
|
for key, r in pairs(u.tr) do
|
|
local lv = r.lv or (r.sp and lvOf["s" .. r.sp]) or lvOf[(r.nm or "") .. "|" .. (r.rk or "")] or 0
|
|
local skey = lv .. "\1" .. (r.sp and ("s" .. r.sp) or key)
|
|
local sp = seen[skey]
|
|
if not sp then
|
|
sp = { nm = r.nm, sp = r.sp, rk = r.rk, lv = lv, pr = {}, trainers = {} }
|
|
seen[skey] = sp
|
|
local g = levels[lv]
|
|
if not g then
|
|
g = { lv = lv, spells = {}, trainers = {}, trainerSet = {}, cost = 0 }
|
|
levels[lv] = g
|
|
order[#order + 1] = lv
|
|
end
|
|
g.spells[#g.spells + 1] = sp
|
|
end
|
|
sp.rk = sp.rk or r.rk
|
|
sp.sp = sp.sp or r.sp
|
|
for p, n in pairs(r.pr or {}) do sp.pr[p] = (sp.pr[p] or 0) + n end
|
|
local g = levels[lv]
|
|
if IsPetTrainer(u) then sp.pet = true end
|
|
if not sp.trainers[id] then
|
|
sp.trainers[id] = true
|
|
if not g.trainerSet[id] then
|
|
g.trainerSet[id] = true
|
|
g.trainers[#g.trainers + 1] = id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.sort(order)
|
|
for _, lv in ipairs(order) do
|
|
local g = levels[lv]
|
|
table.sort(g.spells, function(a, b)
|
|
if (a.nm or "") ~= (b.nm or "") then return (a.nm or "") < (b.nm or "") end
|
|
return (a.rk or "") < (b.rk or "")
|
|
end)
|
|
table.sort(g.trainers)
|
|
for _, sp in ipairs(g.spells) do
|
|
sp.cost = ModePrice(sp.pr)
|
|
g.cost = g.cost + (sp.cost or 0)
|
|
end
|
|
end
|
|
return { levels = levels, order = order }
|
|
end
|
|
|
|
-- Klassen, fuer die Lehrer-Daten vorliegen
|
|
function Codex.LearnClasses()
|
|
local set, list = {}, {}
|
|
for _, u in pairs(Codex.V().units) do
|
|
local t = Codex.ClassOfTrainer(u)
|
|
if t and not set[t] then set[t] = true; list[#list + 1] = t end
|
|
end
|
|
table.sort(list)
|
|
return list
|
|
end
|
|
|
|
-- Gesamtkosten aller Stufen in (von, bis]
|
|
function Codex.LearnCost(plan, from, to)
|
|
local sum = 0
|
|
for _, lv in ipairs(plan.order) do
|
|
if lv > from and lv <= to then sum = sum + plan.levels[lv].cost end
|
|
end
|
|
return sum
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Favoriten und Notizen (nur lokal, wird nicht geteilt)
|
|
------------------------------------------------------------------------
|
|
|
|
local FAV_PREFIX = { quests = "q", units = "u", objects = "o", items = "i" }
|
|
local MAX_NOTE = 200
|
|
|
|
local function FavKey(kind, id)
|
|
local p = FAV_PREFIX[kind]
|
|
return (p and id ~= nil) and (p .. tostring(id)) or nil
|
|
end
|
|
|
|
function Codex.FavSupported(kind) return FAV_PREFIX[kind] ~= nil end
|
|
|
|
function Codex.GetFav(kind, id)
|
|
local key = FavKey(kind, id)
|
|
return key and Codex.db.fav and Codex.db.fav[key] or nil
|
|
end
|
|
|
|
function Codex.IsFav(kind, id)
|
|
return Codex.GetFav(kind, id) ~= nil
|
|
end
|
|
|
|
function Codex.SetFav(kind, id, on)
|
|
local key = FavKey(kind, id)
|
|
if not key then return false end
|
|
Codex.db.fav = Codex.db.fav or {}
|
|
if on then
|
|
Codex.db.fav[key] = Codex.db.fav[key] or { t = time and time() or 0 }
|
|
else
|
|
Codex.db.fav[key] = nil
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Codex.SetNote(kind, id, text)
|
|
local key = FavKey(kind, id)
|
|
if not key then return false end
|
|
-- Zeichen, die Farbcodes oder das Austauschformat stoeren wuerden, ersetzen
|
|
text = tostring(text or ""):gsub("[%c|]", " "):gsub("[;=]", ","):gsub("^%s+", ""):gsub("%s+$", "")
|
|
if #text > MAX_NOTE then text = text:sub(1, MAX_NOTE) end
|
|
Codex.db.fav = Codex.db.fav or {}
|
|
local e = Codex.db.fav[key]
|
|
if text == "" then
|
|
if e then e.note = nil end
|
|
return true
|
|
end
|
|
if not e then
|
|
e = { t = time and time() or 0 }
|
|
Codex.db.fav[key] = e
|
|
end
|
|
e.note = text
|
|
return true
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Dev-Notizen: Notizen zu Eintraegen (Quest, Kreatur, Objekt, Item), nur mit dem Modul Freewood-OctoWoW-Dev und
|
|
-- einer Rolle mit Bearbeitungsrecht. Sie liegen lokal beim Entwickler und werden nicht geteilt.
|
|
------------------------------------------------------------------------
|
|
|
|
local MAX_DEVNOTE = 500
|
|
|
|
function Codex.DevNoteSupported(kind) return FAV_PREFIX[kind] ~= nil end
|
|
|
|
-- Darf dieser Charakter Dev-Notizen sehen und schreiben?
|
|
function Codex.CanDevNote()
|
|
return FW.Dev ~= nil and Codex.Trust ~= nil and Codex.Trust.CanEdit() and true or false
|
|
end
|
|
|
|
function Codex.GetDevNote(kind, id)
|
|
local key = FavKey(kind, id)
|
|
local e = key and Codex.db.dnotes and Codex.db.dnotes[key]
|
|
return e and e.n or nil
|
|
end
|
|
|
|
function Codex.SetDevNote(kind, id, text)
|
|
local key = FavKey(kind, id)
|
|
if not key or not Codex.CanDevNote() then return false end
|
|
text = tostring(text or ""):gsub("[%c|]", " "):gsub("^%s+", ""):gsub("%s+$", "")
|
|
if #text > MAX_DEVNOTE then text = text:sub(1, MAX_DEVNOTE) end
|
|
Codex.db.dnotes = Codex.db.dnotes or {}
|
|
if text == "" then
|
|
Codex.db.dnotes[key] = nil
|
|
else
|
|
Codex.db.dnotes[key] = { n = text, t = time and time() or 0 }
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Codex.DevNoteCount()
|
|
local n = 0
|
|
for _ in pairs(Codex.db.dnotes or {}) do n = n + 1 end
|
|
return n
|
|
end
|
|
|
|
-- Austausch: eine Zeile "FWFAV1;q123=Notiz;u55=;i12=Notiz" (Eintraege ohne Notiz haben nach = nichts)
|
|
function Codex.FavExport()
|
|
local keys = {}
|
|
for key in pairs(Codex.db.fav or {}) do keys[#keys + 1] = key end
|
|
table.sort(keys)
|
|
local parts = { "FWFAV1" }
|
|
for _, key in ipairs(keys) do
|
|
local e = Codex.db.fav[key]
|
|
parts[#parts + 1] = key .. "=" .. (e.note or "")
|
|
end
|
|
return table.concat(parts, ";"), #keys
|
|
end
|
|
|
|
-- Ergaenzt vorhandene Favoriten; bestehende Notizen bleiben. Rueckgabe: neu hinzugekommen, Notizen uebernommen, ungueltig
|
|
function Codex.FavImport(text)
|
|
text = tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
if text:sub(1, 6) ~= "FWFAV1" then return nil end
|
|
Codex.db.fav = Codex.db.fav or {}
|
|
local added, notes, bad = 0, 0, 0
|
|
for item in text:sub(8):gmatch("[^;]+") do
|
|
local key, note = item:match("^([quio]%d+)=(.*)$")
|
|
if key then
|
|
local e = Codex.db.fav[key]
|
|
if not e then
|
|
e = { t = time and time() or 0 }
|
|
Codex.db.fav[key] = e
|
|
added = added + 1
|
|
end
|
|
if note ~= "" and not e.note then
|
|
e.note = note:sub(1, 200)
|
|
notes = notes + 1
|
|
end
|
|
else
|
|
bad = bad + 1
|
|
end
|
|
end
|
|
return added, notes, bad
|
|
end
|