Dateien nach "/" hochladen

This commit is contained in:
2026-10-27 14:09:07 +00:00
parent 1e5ba3048a
commit dea535ff5c
5 changed files with 4758 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
-- Freewood-OctoWoW-Codex: Rezepte der Berufe
-- Lernt beim Oeffnen eines Berufsfensters jedes Rezept: Ergebnis-Item, Material mit Mengen und die Schwierigkeit
-- (orange/gelb/gruen/grau) bei deinem damaligen Skill. Zusammen mit den Lehrer-Daten (Skill-Anforderung, Preis)
-- weiss der Plan so, was ein Rezept braucht und was es bringt, auch bevor du es gelernt hast.
-- db.recipes[Beruf][Rezeptname] = { it = Ergebnis-Item, rg = { [Materialitem] = Anzahl },
-- ob = { optimal|medium|easy|trivial = { kleinster, groesster Skill } }, t = Zeit }
-- Nur lokal; das Teilen mit anderen Spielern kommt spaeter.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local R = {}
NS.Recipes = R
Codex.Recipes = R
local DIFFS = { optimal = true, medium = true, easy = true, trivial = true }
local MAX_RECIPES = 600
local function Store()
local db = Codex.db
if not db then return nil end
db.recipes = db.recipes or {}
return db.recipes
end
local function ItemIDOf(link)
return type(link) == "string" and tonumber(link:match("item:(%d+)")) or nil
end
-- Schwierigkeit aus Zahl (Enum: 0 optimal, 1 mittel, 2 leicht, 3 trivial) oder Text
local DIFF_NUM = { [0] = "optimal", [1] = "medium", [2] = "easy", [3] = "trivial" }
local function DiffOf(v)
if type(v) == "number" then return DIFF_NUM[v] end
if type(v) == "string" then
v = v:lower()
if DIFFS[v] then return v end
end
return nil
end
local function Try(fn, ...)
if type(fn) ~= "function" then return nil end
local ok, a, b, c, d = pcall(fn, ...)
if ok then return a, b, c, d end
return nil
end
-- Neue Berufsoberflaeche (C_TradeSkillUI): { profession, skill, max, list = { { name, item, diff, reag, learned } } }
local function SnapshotNew()
local T = _G.C_TradeSkillUI
if type(T) ~= "table" then return nil end
local base = Try(T.GetBaseProfessionInfo)
local name = type(base) == "table" and (base.professionName or base.parentProfessionName) or nil
local skill = type(base) == "table" and tonumber(base.skillLevel) or nil
local max = type(base) == "table" and tonumber(base.maxSkillLevel) or nil
if not name or name == "" then return nil end
local ids = Try(T.GetAllRecipeIDs)
if type(ids) ~= "table" or #ids == 0 then return nil end
local list = {}
for _, id in ipairs(ids) do
local info = Try(T.GetRecipeInfo, id)
if type(info) == "table" and type(info.name) == "string" then
local diff = DiffOf(info.relativeDifficulty) or DiffOf(info.difficulty)
local item
local link = Try(T.GetRecipeItemLink, id)
item = ItemIDOf(link)
local reag = {}
local schem = Try(T.GetRecipeSchematic, id, false)
if type(schem) == "table" then
item = item or tonumber(schem.outputItemID)
for _, slot in ipairs(schem.reagentSlotSchematics or {}) do
local first = slot.reagents and slot.reagents[1]
local rid = first and tonumber(first.itemID)
if rid and tonumber(slot.quantityRequired) then reag[rid] = tonumber(slot.quantityRequired) end
end
end
if not next(reag) then
local n = Try(T.GetRecipeNumReagents, id) or 0
for j = 1, n do
local _, _, need = Try(T.GetRecipeReagentInfo, id, j)
local rid = ItemIDOf(Try(T.GetRecipeReagentItemLink, id, j))
if rid and tonumber(need) then reag[rid] = tonumber(need) end
end
end
list[#list + 1] = { name = info.name, item = item, diff = diff or "optimal", reag = reag,
learned = info.learned ~= false, hasDiff = diff ~= nil, trivial = tonumber(info.maxTrivialLevel) }
end
end
return { profession = name, skill = skill or 0, max = max or 0, list = list }
end
-- Alte Handwerks-Schnittstelle (GetTradeSkill...)
local function SnapshotOld()
if not (GetTradeSkillLine and GetNumTradeSkills and GetTradeSkillInfo) then return nil end
local ok, name, rank, maxRank = pcall(GetTradeSkillLine)
if not ok or type(name) ~= "string" or name == "" or name == "UNKNOWN" then return nil end
local list = {}
for i = 1, math.min(GetNumTradeSkills() or 0, MAX_RECIPES) do
local rname, rtype = GetTradeSkillInfo(i)
if type(rname) == "string" and DIFFS[rtype] then
local item = GetTradeSkillItemLink and ItemIDOf(GetTradeSkillItemLink(i)) or nil
local reag = {}
local nr = GetTradeSkillNumReagents and GetTradeSkillNumReagents(i) or 0
for j = 1, nr do
local _, _, need = GetTradeSkillReagentInfo(i, j)
local rid = GetTradeSkillReagentItemLink and ItemIDOf(GetTradeSkillReagentItemLink(i, j)) or nil
if rid and tonumber(need) then reag[rid] = tonumber(need) end
end
list[#list + 1] = { name = rname, item = item, diff = rtype, reag = reag, learned = true, hasDiff = true }
end
end
return { profession = name, skill = tonumber(rank) or 0, max = tonumber(maxRank) or 0, list = list }
end
-- Offenes Berufsfenster lesen: neue Oberflaeche zuerst, sonst die alte
function R.Snapshot()
return SnapshotNew() or SnapshotOld()
end
function R.ReadOpenWindow()
local store = Store()
if not store then return false end
local snap = R.Snapshot()
if not snap then return false end
R.last = snap
local prof = store[snap.profession]
if not prof then prof = {}; store[snap.profession] = prof end
local n = 0
for _ in pairs(prof) do n = n + 1 end
for _, r in ipairs(snap.list) do
local rec = prof[r.name]
if not rec then
if n >= MAX_RECIPES then break end
rec = { rg = {}, ob = {} }
prof[r.name] = rec
n = n + 1
end
if r.item then rec.it = r.item end
if r.trivial and r.trivial > 0 then rec.tv = r.trivial end -- ab diesem Skill grau
for rid, need in pairs(r.reag) do rec.rg[rid] = need end
if r.learned and r.hasDiff then
local range = rec.ob[r.diff]
if range then
range[1], range[2] = math.min(range[1], snap.skill), math.max(range[2], snap.skill)
else
rec.ob[r.diff] = { snap.skill, snap.skill }
end
end
rec.t = time and time() or 0
end
FW:Fire("CODEX_RECIPES_CHANGED", snap.profession)
return true, #snap.list
end
local reading = false
local function OnTradeSkill()
if reading then return end
reading = true
local function Run()
reading = false
Codex.Safe(R.ReadOpenWindow)
end
if C_Timer and C_Timer.After then
C_Timer.After(0.5, Run)
else
Run()
end
end
-- Diagnose fuer /fwberufdump: zeigt, was das Spiel zurueckgibt
function R.Dump()
local function Say(msg) print("|cffe0b84dFreewood Beruf:|r " .. tostring(msg)) end
local T = _G.C_TradeSkillUI
Say("C_TradeSkillUI: " .. tostring(type(T) == "table") .. ", alte Schnittstelle (GetTradeSkillLine): " .. tostring(type(GetTradeSkillLine) == "function"))
if type(T) == "table" then
local base = Try(T.GetBaseProfessionInfo)
if type(base) == "table" then
local keys = {}
for k, v in pairs(base) do keys[#keys + 1] = k .. "=" .. tostring(v) end
table.sort(keys)
Say("GetBaseProfessionInfo: " .. table.concat(keys, ", "))
else
Say("GetBaseProfessionInfo: nichts (Berufsfenster offen?)")
end
local ids = Try(T.GetAllRecipeIDs)
Say("GetAllRecipeIDs: " .. (type(ids) == "table" and (#ids .. " Rezepte") or "nichts"))
local id = type(ids) == "table" and ids[1]
if id then
local info = Try(T.GetRecipeInfo, id)
if type(info) == "table" then
local keys = {}
for k, v in pairs(info) do
if type(v) ~= "table" then keys[#keys + 1] = k .. "=" .. tostring(v) end
end
table.sort(keys)
Say("GetRecipeInfo(" .. id .. "): " .. table.concat(keys, ", "))
else
Say("GetRecipeInfo(" .. id .. "): nichts")
end
local schem = Try(T.GetRecipeSchematic, id, false)
if type(schem) == "table" then
local keys = {}
for k, v in pairs(schem) do keys[#keys + 1] = k .. "=" .. (type(v) == "table" and ("#" .. #v) or tostring(v)) end
table.sort(keys)
Say("GetRecipeSchematic: " .. table.concat(keys, ", "))
else
Say("GetRecipeSchematic: nichts; NumReagents=" .. tostring(Try(T.GetRecipeNumReagents, id)) .. ", ItemLink=" .. tostring(Try(T.GetRecipeItemLink, id)))
end
end
end
local snap = R.Snapshot()
if snap then
local learned = 0
for _, r in ipairs(snap.list) do if r.learned then learned = learned + 1 end end
Say(string.format("Auslesen: %s %d/%d, %d Rezepte (%d gelernt)", snap.profession, snap.skill, snap.max, #snap.list, learned))
local r = snap.list[1]
if r then
local mats = {}
for rid, n in pairs(r.reag) do mats[#mats + 1] = rid .. "x" .. n end
Say(string.format("Beispiel: %s, Ergebnis-Item %s, Farbe %s, Material %s", r.name, tostring(r.item), tostring(r.diff), table.concat(mats, " ")))
end
else
Say("Auslesen: nichts. Bitte das Berufsfenster oeffnen und den Befehl erneut eingeben.")
end
local pn, rn = R.Count()
Say("In der Datenbank: " .. pn .. " Berufe, " .. rn .. " Rezepte")
end
SLASH_FREEWOODRECIPEDUMP1 = "/fwberufdump"
SlashCmdList["FREEWOODRECIPEDUMP"] = function() R.Dump() end
function R:Start()
if self.started then return end
self.started = true
for _, event in ipairs({ "TRADE_SKILL_SHOW", "TRADE_SKILL_UPDATE", "TRADE_SKILL_LIST_UPDATE", "TRADE_SKILL_DATA_SOURCE_CHANGED" }) do
FW:RegisterEvent(event, OnTradeSkill, R)
end
end
function R:Stop()
for _, event in ipairs({ "TRADE_SKILL_SHOW", "TRADE_SKILL_UPDATE", "TRADE_SKILL_LIST_UPDATE", "TRADE_SKILL_DATA_SOURCE_CHANGED" }) do
FW:UnregisterEvent(event, R)
end
self.started = false
end
function R.Get(profession)
local store = Store()
return store and store[profession] or {}
end
function R.Count()
local store = Store()
local professions, recipes = 0, 0
for _, prof in pairs(store or {}) do
professions = professions + 1
for _ in pairs(prof) do recipes = recipes + 1 end
end
return professions, recipes
end
-- Skill-Anforderung eines Rezepts: vom Lehrer (kleinster Wert), sonst kleinster Skill, bei dem es orange war
function R.ReqOf(profession, name)
local best
for _, u in pairs(Codex.V().units) do
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
for _, r in pairs(u.tr) do
if r.nm == name and r.sn == profession and tonumber(r.sr) and (not best or r.sr < best) then best = r.sr end
end
end
end
if best then return best, true end
local rec = R.Get(profession)[name]
local o = rec and rec.ob and rec.ob.optimal
if o then return o[1], false end
return nil
end
-- Schwierigkeit bei gegebenem Skill: aus Beobachtungen, sonst geschaetzt (je 10 Skill pro Farbe).
-- Rueckgabe: "optimal" | "medium" | "easy" | "trivial" | "locked", und ob geschaetzt (true) oder beobachtet (false)
function R.DiffAt(profession, name, skill)
local rec = R.Get(profession)[name]
local ob = rec and rec.ob or {}
for diff, range in pairs(ob) do
if skill >= range[1] and skill <= range[2] then return diff, false end
end
local req = R.ReqOf(profession, name)
if not req and not next(ob) and not (rec and rec.tv) then return nil end
-- Schwellen: gelb, gruen, grau beginnen bei
local base = req or (ob.optimal and ob.optimal[1]) or 0
local tv = rec and rec.tv
-- Ist die graue Schwelle (maxTrivialLevel) bekannt, liegen gruen und gelb je 10 Skill darunter
local gray = ob.trivial and ob.trivial[1] or tv or math.max(base + 30, ob.easy and ob.easy[2] + 1 or 0)
local green = ob.easy and ob.easy[1] or (tv and math.max(gray - 10, ob.medium and ob.medium[2] + 1 or 0)) or math.max(gray - 10, ob.medium and ob.medium[2] + 1 or 0)
local yellow = ob.medium and ob.medium[1] or math.max(green - 10, ob.optimal and ob.optimal[2] + 1 or 0)
if not req and not ob.optimal and tv then base = math.max(0, yellow - 10) end
local first = req or (ob.optimal and ob.optimal[1]) or 0
if skill < first then return "locked", true end
if skill < yellow then return "optimal", true end
if skill < green then return "medium", true end
if skill < gray then return "easy", true end
return "trivial", true
end
-- Rezepte beim Lehrer: Liste { name, profession, req, price, trainerID, rec = Rezept oder nil }
function R.TrainerRows()
local out, seen = {}, {}
for uid, u in pairs(Codex.V().units) do
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
for _, r in pairs(u.tr) do
if r.nm and r.sn then
local key = r.sn .. "\1" .. r.nm
local e = seen[key]
local price = Codex.ModePrice and Codex.ModePrice(r.pr) or nil
if not e then
e = { name = r.nm, profession = r.sn, req = tonumber(r.sr), price = price, trainerID = uid,
rec = R.Get(r.sn)[r.nm] }
seen[key] = e
out[#out + 1] = e
elseif price and (not e.price or price < e.price) then
e.price, e.trainerID = price, uid
end
end
end
end
end
return out
end
+1289
View File
File diff suppressed because it is too large Load Diff
+708
View File
@@ -0,0 +1,708 @@
-- Freewood-OctoWoW-Codex: Vorlage (Startdaten aus Freewood-OctoWoW-CodexData, Vanilla)
-- Das Datenaddon ist "LoadOnDemand" und wird erst beim ersten Zugriff geladen. Alles hier ist nur eine Vorlage:
-- eigene und geteilte Live-Daten gehen immer vor. Die Daten liegen als kurze Zeichenketten vor und werden erst
-- beim Zugriff entpackt (und gemerkt).
--
-- Formate (siehe tools/pf2seed.lua):
-- Quest "Stufe|Mindeststufe|Rassen|Klassen|Beruf|Vorquests|Start|Abgabe|Ziele" (Refs: "U123.O55.I6497")
-- Kreatur "Stufe|Fraktion|Rang|Gebiet:Zelle,Zelle;Gebiet:Zelle" (Zelle = x*100+y in 1 %)
-- Item "u<id>=<Chance %>,...;o<id>=<Chance>,...;v<haendlerID>,..."
-- Rollen meta[rolle] = "id:wert,id:wert" (negative IDs sind Objekte)
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS.Codex
if not FW or not Codex then
return
end
local Util = FW.Util
local GRID = Codex.GRID
local Seed = {}
Codex.Seed = Seed
local DATA_ADDON = "Freewood-OctoWoW-CodexData"
local S -- FreewoodCodexSeed, sobald geladen
local failed = false
local floor = math.floor
-- Rollen: Reihenfolge der Anzeige
Seed.ROLES = {
units = { "vendor", "repair", "flight", "innkeeper", "stablemaster", "spirithealer", "auctioneer", "battlemaster", "banker", "rares" },
objects = { "mailbox", "meetingstone", "chests", "herbs", "mines", "fish" },
}
------------------------------------------------------------------------
-- Laden
------------------------------------------------------------------------
function Seed.Load()
if S then
return true
end
if failed then
return false
end
local g = _G.FreewoodCodexSeed
if not (g and g.ready) then
local load = (C_AddOns and C_AddOns.LoadAddOn) or LoadAddOn
if type(load) == "function" then
pcall(load, DATA_ADDON)
end
g = _G.FreewoodCodexSeed
end
if g and g.ready then
S = g
return true
end
failed = true
return false
end
-- Laedt bei Bedarf (nur die Oberflaeche ruft das auf, damit der Ladevorgang beim Oeffnen des Tabs geschieht)
function Seed.Disabled()
local db = Codex.db
return db and db.settings and db.settings.noSeed == true or false
end
function Seed.Available()
if Seed.Disabled() then return false end
return Seed.Load()
end
-- Nur pruefen, ob schon geladen ist, ohne zu laden (fuer Code, der waehrend des Spielens laeuft)
function Seed.Ready()
return S ~= nil and not Seed.Disabled()
end
-- fuer Tests: Zustand zuruecksetzen
local caches
function Seed.Reset()
S, failed = nil, false
caches = { quest = {}, unit = {}, object = {}, item = {}, lists = {}, hay = {} }
Seed.nextIdx, Seed.rev, Seed.roleIdx, Seed.areaMap = nil, nil, nil, nil
end
Seed.Reset()
local function Lang()
return Codex.Lang()
end
------------------------------------------------------------------------
-- Kleine Helfer
------------------------------------------------------------------------
local function Fields(s, n)
local out, pos = {}, 1
for i = 1, n do
local e = s:find("|", pos, true)
if e then
out[i] = s:sub(pos, e - 1)
pos = e + 1
else
out[i] = s:sub(pos)
pos = #s + 2
end
end
return out
end
local function Nums(s)
local list = {}
for n in (s or ""):gmatch("%d+") do
list[#list + 1] = tonumber(n)
end
return list
end
local KIND_OF = { U = "units", O = "objects", I = "items" }
local function Refs(s)
local list = {}
for k, id in (s or ""):gmatch("(%a)(%d+)") do
list[#list + 1] = { kind = KIND_OF[k], id = tonumber(id) }
end
return list
end
local function AreaMap()
if Seed.areaMap then
return Seed.areaMap
end
local map = {}
for a, m in (S.areas or ""):gmatch("(%d+)=(%d+)") do
map[tonumber(a)] = tonumber(m)
end
Seed.areaMap = map
return map
end
local function Pick(de, en)
if Lang() == "en" then
return en or de
end
return de or en
end
------------------------------------------------------------------------
-- Quests
------------------------------------------------------------------------
function Seed.Quest(id)
local c = caches.quest[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = S.q[id]
if not s then
caches.quest[id] = false
return nil
end
local f = Fields(s, 9)
local q = {
id = id, lvl = tonumber(f[1]), min = tonumber(f[2]), race = tonumber(f[3]), class = tonumber(f[4]),
skill = tonumber(f[5]), pre = Nums(f[6]), start = Refs(f[7]), stop = Refs(f[8]), obj = Refs(f[9]),
}
caches.quest[id] = q
return q
end
function Seed.QuestTitle(id)
if not Seed.Load() then
return nil
end
return Pick(S.qd[id], S.qe[id])
end
-- Folgequests: alle Quests, die diese als Vorquest haben
function Seed.NextQuests(id)
if not Seed.Load() then
return {}
end
if not Seed.nextIdx then
local idx = {}
for qid, s in pairs(S.q) do
local pre = Fields(s, 6)[6]
for p in pre:gmatch("%d+") do
local n = tonumber(p)
local l = idx[n]
if not l then
l = {}
idx[n] = l
end
l[#l + 1] = qid
end
end
for _, l in pairs(idx) do
table.sort(l)
end
Seed.nextIdx = idx
end
return Seed.nextIdx[id] or {}
end
-- Position in der Questkette: pos, total (zum Beispiel 2, 4), nil ausserhalb einer Kette.
-- Bei Verzweigungen zaehlt der laengste Weg vor und nach der Quest.
local chainMemo = {}
local function ChainLen(id, dir, depth)
local key = dir .. id
local c = chainMemo[key]
if c then
return c
end
if depth > 40 then
return 1
end
local best = 0
local list
if dir == "u" then
local q = Seed.Quest(id)
list = q and q.pre or {}
else
list = Seed.NextQuests(id)
end
for _, other in ipairs(list) do
if other ~= id then
best = math.max(best, ChainLen(other, dir, depth + 1))
end
end
chainMemo[key] = best + 1
return best + 1
end
function Seed.ChainPos(id)
if not Seed.Quest(id) then
return nil
end
local up, down = ChainLen(id, "u", 0), ChainLen(id, "d", 0)
if up + down <= 2 then
return nil
end
return up, up + down - 1
end
-- Stellung in einer Questkette: "start" (hat Folgequests, keine Vorquest), "mid", "end" (nur Vorquests)
-- oder nil (gehoert zu keiner Kette)
function Seed.ChainRole(id)
local q = Seed.Quest(id)
if not q then
return nil
end
-- manche Quests nennen sich selbst als Vorquest: das zaehlt nicht als Kette
local hasPre, hasNext = false, false
for _, p in ipairs(q.pre or {}) do
if p ~= id then hasPre = true break end
end
for _, n in ipairs(Seed.NextQuests(id)) do
if n ~= id then hasNext = true break end
end
if hasPre and hasNext then return "mid" end
if hasNext then return "start" end
if hasPre then return "end" end
return nil
end
------------------------------------------------------------------------
-- Kreaturen und Objekte
------------------------------------------------------------------------
local function EntityData(kind)
return kind == "units" and S.u or S.o
end
-- Rueckgabe: { id, l1, l2, fac, rank, spots, cells }
function Seed.Entity(kind, id)
local cache = kind == "units" and caches.unit or caches.object
local c = cache[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = EntityData(kind)[id]
if not s then
cache[id] = false
return nil
end
local f = Fields(s, 4)
local l1, l2 = f[1]:match("^(%d+)%-?(%d*)$")
if l1 and l2 == "" then
l2 = l1
end
local e = {
id = id, l1 = tonumber(l1), l2 = tonumber(l2), fac = f[2] ~= "" and f[2] or nil,
rank = tonumber(f[3]), spots = f[4],
}
cache[id] = e
return e
end
function Seed.Name(kind, id)
if not Seed.Load() then
return nil
end
if kind == "units" then
return Pick(S.ud[id], S.ue[id])
elseif kind == "objects" then
return Pick(S.od[id], S.oe[id])
elseif kind == "items" then
return Pick(S.id[id], S.ie[id])
end
return nil
end
-- Zellen wie im Codex: { [Karte*GRID^2 + y*GRID + x] = 1 }
function Seed.Cells(kind, id)
local e = Seed.Entity(kind, id)
if not e then
return {}
end
if e.cells then
return e.cells
end
local cells = {}
local areas = AreaMap()
for area, list in e.spots:gmatch("(%d+):([%d,]+)") do
local m = areas[tonumber(area)]
if m then
for c in list:gmatch("%d+") do
c = tonumber(c)
local cx = math.min(GRID - 1, floor(c / 100) * 2 + 1)
local cy = math.min(GRID - 1, (c % 100) * 2 + 1)
cells[m * GRID * GRID + cy * GRID + cx] = 1
end
end
end
e.cells = cells
return cells
end
-- Erste Fundstelle: Karte, x, y (Anteile 0..1)
function Seed.FirstSpot(kind, id)
local e = Seed.Entity(kind, id)
if not e then
return nil
end
local area, list = e.spots:match("(%d+):([%d,]+)")
local m = area and AreaMap()[tonumber(area)]
local c = list and tonumber(list:match("%d+"))
if not (m and c) then
return nil
end
return m, (floor(c / 100) + 0.5) / 100, ((c % 100) + 0.5) / 100
end
-- Gebiet einer Kreatur / eines Objekts (Karten-ID der ersten Fundstelle)
function Seed.MapOf(kind, id)
local m = Seed.FirstSpot(kind, id)
return m
end
-- Kleinster Abstand (Kartenanteil) einer Zellenmenge zum Spieler auf der aktuellen Karte, sonst nil
function Seed.Distance(cells, loc)
loc = loc or (Util.GetPlayerLocation and Util.GetPlayerLocation())
if not (loc and loc.mapID and loc.x and loc.y) then
return nil
end
local best
local g2 = GRID * GRID
for key in pairs(cells or {}) do
if floor(key / g2) == loc.mapID then
local idx = key % g2
local dx = ((idx % GRID) + 0.5) / GRID - loc.x
local dy = (floor(idx / GRID) + 0.5) / GRID - loc.y
local d = dx * dx + dy * dy
if not best or d < best then
best = d
end
end
end
return best and math.sqrt(best) or nil
end
------------------------------------------------------------------------
-- Items: Quellen
------------------------------------------------------------------------
-- Rueckgabe: { u = { { id, chance } }, o = { { id, chance } }, v = { haendlerID } }
function Seed.Item(id)
local c = caches.item[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = S.i[id]
if not s then
caches.item[id] = false
return nil
end
local it = { u = {}, o = {}, v = {} }
for part in s:gmatch("[^;]+") do
local tag, body = part:sub(1, 1), part:sub(2)
if tag == "u" or tag == "o" then
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
local t = it[tag]
t[#t + 1] = { id = tonumber(k), chance = tonumber(v) }
end
elseif tag == "v" then
it.v = Nums(body)
end
end
caches.item[id] = it
return it
end
function Seed.ItemHasVendor(id)
if not Seed.Load() then
return false
end
local s = S.i[id]
return s ~= nil and (s:find(";v", 1, true) ~= nil or s:sub(1, 1) == "v")
end
local function Reverse()
if Seed.rev then
return Seed.rev
end
-- flache Listen { Item, Chance, Item, Chance ... } sparen Speicher
local rev = { u = {}, o = {}, v = {} }
for itemID, s in pairs(S.i) do
for part in s:gmatch("[^;]+") do
local tag, body = part:sub(1, 1), part:sub(2)
if tag == "u" or tag == "o" then
local t = rev[tag]
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
local key = tonumber(k)
local l = t[key]
if not l then
l = {}
t[key] = l
end
l[#l + 1] = itemID
l[#l + 1] = tonumber(v)
end
elseif tag == "v" then
for n in body:gmatch("%d+") do
local key = tonumber(n)
local l = rev.v[key]
if not l then
l = {}
rev.v[key] = l
end
l[#l + 1] = itemID
end
end
end
end
Seed.rev = rev
return rev
end
-- Beute einer Kreatur / eines Objekts laut Vorlage: { { item, chance (Prozent) } }, nach Chance sortiert
function Seed.Drops(kind, id)
if not Seed.Load() then
return {}
end
local flat = Reverse()[kind == "units" and "u" or "o"][id]
local list = {}
for i = 1, #(flat or {}), 2 do
list[#list + 1] = { item = flat[i], chance = flat[i + 1] }
end
table.sort(list, function(a, b)
if a.chance ~= b.chance then return a.chance > b.chance end
return a.item < b.item
end)
return list
end
-- Waren eines Haendlers: { itemID }
function Seed.VendorItems(id)
if not Seed.Load() then
return {}
end
local list = {}
for i, itemID in ipairs(Reverse().v[id] or {}) do
list[i] = itemID
end
table.sort(list)
return list
end
------------------------------------------------------------------------
-- Quests je Kreatur / Objekt (Geber, Abgabe, Ziel)
------------------------------------------------------------------------
function Seed.QuestsOf(kind, id)
if not Seed.Load() then
return {}, {}, {}
end
if not Seed.questRev then
local rev = { give = {}, turn = {}, target = {} }
local function Add(t, key, qid)
local l = t[key]
if not l then
l = {}
t[key] = l
end
if l[#l] ~= qid then
l[#l + 1] = qid
end
end
for qid in pairs(S.q) do
local q = Seed.Quest(qid)
for _, r in ipairs(q.start) do
if r.kind ~= "items" then Add(rev.give, r.kind .. r.id, qid) end
end
for _, r in ipairs(q.stop) do
if r.kind ~= "items" then Add(rev.turn, r.kind .. r.id, qid) end
end
for _, r in ipairs(q.obj) do
if r.kind ~= "items" then Add(rev.target, r.kind .. r.id, qid) end
end
end
Seed.questRev = rev
end
local key = kind .. id
local rev = Seed.questRev
return rev.give[key] or {}, rev.turn[key] or {}, rev.target[key] or {}
end
------------------------------------------------------------------------
-- Rollen (Haendler, Flugmeister ...)
------------------------------------------------------------------------
local function RoleIndex()
if Seed.roleIdx then
return Seed.roleIdx
end
local idx = { units = {}, objects = {}, ids = {} }
for _, group in ipairs({ Seed.ROLES.units, Seed.ROLES.objects }) do
for _, role in ipairs(group) do
local list = {}
for k, v in (S.meta[role] or ""):gmatch("(%-?%d+):([^,]*)") do
local n = tonumber(k)
local kind = n > 0 and "units" or "objects"
n = math.abs(n)
local t = idx[kind][n]
if not t then
t = {}
idx[kind][n] = t
end
t[role] = v ~= "" and v or true
list[#list + 1] = { kind = kind, id = n }
end
idx.ids[role] = list
end
end
Seed.roleIdx = idx
return idx
end
-- Rollen einer Kreatur / eines Objekts: { vendor = "H", herbs = 280, ... } oder nil
function Seed.Roles(kind, id)
if not Seed.Load() then
return nil
end
return RoleIndex()[kind][id]
end
-- Rollen als Liste in fester Reihenfolge
function Seed.RoleList(kind, id)
local roles = Seed.Roles(kind, id)
if not roles then
return {}
end
local list = {}
for _, role in ipairs(Seed.ROLES[kind]) do
if roles[role] then
list[#list + 1] = role
end
end
return list
end
function Seed.RoleIDs(role)
if not Seed.Load() then
return {}
end
return RoleIndex().ids[role] or {}
end
-- Naechste Kreaturen / Objekte mit einer Rolle auf der aktuellen Karte: { { kind, id, dist, m, x, y } }
function Seed.Nearest(role, limit, faction)
local loc = Util.GetPlayerLocation and Util.GetPlayerLocation()
local list = {}
if not (loc and loc.mapID and loc.x and loc.y) or not Seed.Load() then
return list
end
local side = faction or (UnitFactionGroup and select(1, UnitFactionGroup("player"))) or nil
local letter = side == "Alliance" and "A" or side == "Horde" and "H" or nil
for _, ref in ipairs(Seed.RoleIDs(role)) do
local e = Seed.Entity(ref.kind, ref.id)
local roles = Seed.Roles(ref.kind, ref.id)
local value = roles and roles[role]
local fac = (type(value) == "string" and value:match("^[AH]+$")) or (e and e.fac)
if e and (not letter or not fac or fac:find(letter, 1, true)) then
local d = Seed.Distance(Seed.Cells(ref.kind, ref.id))
if d then
local best, bm, bx, by
local g2 = GRID * GRID
for key in pairs(Seed.Cells(ref.kind, ref.id)) do
if floor(key / g2) == loc.mapID then
local idx = key % g2
local x, y = ((idx % GRID) + 0.5) / GRID, (floor(idx / GRID) + 0.5) / GRID
local dd = (x - loc.x) ^ 2 + (y - loc.y) ^ 2
if not best or dd < best then
best, bm, bx, by = dd, loc.mapID, x, y
end
end
end
list[#list + 1] = { kind = ref.kind, id = ref.id, dist = d, m = bm, x = bx, y = by }
end
end
end
table.sort(list, function(a, b)
if a.dist ~= b.dist then return a.dist < b.dist end
return a.id < b.id
end)
if limit then
for i = #list, limit + 1, -1 do
list[i] = nil
end
end
return list
end
------------------------------------------------------------------------
-- Listen fuer die Oberflaeche
------------------------------------------------------------------------
-- Alle IDs einer Art (sortiert)
function Seed.IDs(kind)
if not Seed.Load() then
return {}
end
local c = caches.lists[kind]
if c then
return c
end
local src
if kind == "quests" then src = S.q
elseif kind == "units" then src = S.u
elseif kind == "objects" then src = S.o
elseif kind == "items" then src = S.ie end
local list = {}
for id in pairs(src or {}) do
list[#list + 1] = id
end
if kind == "items" then
for id in pairs(S.i) do
if not S.ie[id] then list[#list + 1] = id end
end
end
table.sort(list)
caches.lists[kind] = list
return list
end
-- Kleingeschriebene Namen (deutsch und englisch) zur Suche
function Seed.Haystack(kind, id)
local c = caches.hay[kind]
if not c then
c = {}
caches.hay[kind] = c
end
local h = c[id]
if h then
return h
end
local de, en
if kind == "quests" then de, en = S.qd[id], S.qe[id]
elseif kind == "units" then de, en = S.ud[id], S.ue[id]
elseif kind == "objects" then de, en = S.od[id], S.oe[id]
else de, en = S.id[id], S.ie[id] end
h = ((de or "") .. "\n" .. (en or "")):lower()
c[id] = h
return h
end
function Seed.Count()
if not Seed.Load() then
return 0, 0, 0, 0
end
local function n(t)
local c = 0
for _ in pairs(t) do c = c + 1 end
return c
end
return n(S.q), n(S.u), n(S.o), n(S.i)
end
+2334
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
-- Freewood-OctoWoW-Codex: Abschnitt "Datenbank teilen" in den Einstellungen
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
local Sync = NS and NS.Sync
local Share = NS and NS.Share
if not FW or not Codex or not Sync or not Share or not FW.UI or not FW.UI.RegisterSettingsSection then
return
end
local UI = FW.UI
local function Build(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_SHARE_HEADER)
local note = UI.CreateLabel(f, L.CODEX_SHARE_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local box = UI.CreateCheckbox(f, L.CODEX_SHARE_SWITCH, function()
return Sync:IsEnabled()
end, function(on)
Sync:SetEnabled(on)
end)
stack:Add(box, { fit = true, gap = 10 })
UI.AttachTooltip(box, function() return L.CODEX_SHARE_SWITCH end, function() return L.CODEX_SHARE_SWITCH_TIP end)
local callButton = UI.CreateButton(f, L.CODEX_SHARE_NOW, 200, 26, function()
if Sync:IsEnabled() then
Sync:JoinGlobal()
Share.Scan(true)
Sync:SendHello()
end
end, "default")
stack:Add(callButton, { height = 26, gap = 12 })
local clearButton
local armed = false
clearButton = UI.CreateButton(f, L.CODEX_SHARE_CLEAR, 240, 26, function()
if not armed then
armed = true
clearButton:SetLabel(L.CODEX_SHARE_CLEAR_SURE)
C_Timer.After(5, function()
armed = false
clearButton:SetLabel(L.CODEX_SHARE_CLEAR)
end)
return
end
armed = false
clearButton:SetLabel(L.CODEX_SHARE_CLEAR)
local n = Share.ClearRemote()
FW:Print(string.format(L.CODEX_SHARE_CLEARED, n))
FW:Fire("CODEX_CHANGED", "import")
FW:Fire("CODEX_SYNC_CHANGED")
end, "default")
stack:Add(clearButton, { height = 26, gap = 8 })
local status = UI.CreateLabel(f, "", { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(status, { fit = true, gap = 10 })
function f.Refresh()
box:Refresh()
local st = Sync:Stats()
local text = string.format(L.CODEX_SHARE_STATS, st.sent, st.received)
text = text .. "\n" .. string.format(L.CODEX_SHARE_SOURCES, st.senders, st.records)
text = text .. "\n" .. string.format(L.CODEX_SHARE_HEARD, st.heard or 0)
if Sync:IsEnabled() then
local key = "CODEX_SHARE_GLOBAL_" .. string.upper(st.global or "off")
text = text .. "\n" .. (L[key] or L.CODEX_SHARE_GLOBAL_OFF)
end
status:SetText(text)
stack:Layout()
end
FW:On("CODEX_SYNC_CHANGED", function()
if f:IsVisible() then
f.Refresh()
end
end, f)
stack:Layout()
return f
end
UI:RegisterSettingsSection("codex-share", {
order = 105,
height = 360,
build = Build,
refresh = function(frame)
frame.Refresh()
end,
})