Files
Freewood-OctoWoW-Codex/CodexRecipes.lua
T
2026-10-27 14:09:07 +00:00

334 lines
14 KiB
Lua

-- 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