428 lines
16 KiB
Lua
428 lines
16 KiB
Lua
-- Freewood-OctoWoW-Codex: Lern-Hinweise
|
|
-- Nach einem Stufenaufstieg (ausserhalb des Kampfes) oder einer neuen Berufsstufe zeigt ein kurzer Hinweis
|
|
-- in der Bildschirmmitte, was man jetzt beim Lehrer lernen kann. Grundlage: Lernplan (Lehrer-Daten).
|
|
|
|
local ADDON, NS = ...
|
|
|
|
local FW = _G.Freewood
|
|
local Codex = NS.Codex
|
|
if not FW or not Codex or not FW.UI then
|
|
return
|
|
end
|
|
|
|
local UI = FW.UI
|
|
local Hints = { queue = {}, showing = false, shown = 0 }
|
|
NS.Hints = Hints
|
|
Codex.Hints = Hints
|
|
|
|
local DURATION = 5
|
|
local FADE = 0.6
|
|
local MAX_ROWS = 5
|
|
local ROW_H = 24
|
|
local WIDTH = 380
|
|
|
|
------------------------------------------------------------------------
|
|
-- Einstellungen
|
|
------------------------------------------------------------------------
|
|
|
|
function Hints.ClassOn()
|
|
return Codex.db.settings.hintClass ~= false
|
|
end
|
|
function Hints.ProfOn()
|
|
return Codex.db.settings.hintProf ~= false
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Inhalt
|
|
------------------------------------------------------------------------
|
|
|
|
local function SpellKnown(id)
|
|
if not id then return false end
|
|
local SB = _G.C_SpellBook
|
|
local fn = (SB and SB.IsSpellKnown) or _G.IsSpellKnown or (_G.C_Spell and _G.C_Spell.IsSpellKnown)
|
|
if type(fn) ~= "function" then return false end
|
|
local ok, known = pcall(fn, id)
|
|
return ok and known == true
|
|
end
|
|
|
|
local function Entry(r)
|
|
local cost = Codex.ModePrice(r.pr)
|
|
return { nm = r.nm or "?", sp = r.sp, rk = r.rk or (r.sp and Codex.SpellRank(r.sp)) or nil, cost = cost }
|
|
end
|
|
|
|
-- Fähigkeiten deiner Klasse, die auf dieser Stufe beim Lehrer dazukommen
|
|
function Hints.ClassEntries(level)
|
|
local token = Codex.Time and Codex.Time.PlayerToken()
|
|
if not token then return {} end
|
|
local plan = Codex.LearnPlan(token)
|
|
local g = plan.levels[level]
|
|
local list = {}
|
|
if g then
|
|
for _, sp in ipairs(g.spells) do
|
|
if not SpellKnown(sp.sp) then
|
|
list[#list + 1] = { nm = sp.nm or "?", sp = sp.sp, rk = sp.rk or (sp.sp and Codex.SpellRank(sp.sp)) or nil, cost = sp.cost }
|
|
end
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
-- Berufslehrer-Fähigkeiten, deren Fertigkeitswert in (von, bis] liegt
|
|
function Hints.ProfEntries(skill, from, to)
|
|
local list, seen = {}, {}
|
|
for _, u in pairs(Codex.V().units) do
|
|
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
|
|
for key, r in pairs(u.tr) do
|
|
if r.sn == skill and r.sr and r.sr > from and r.sr <= to then
|
|
local k = r.sp and ("s" .. r.sp) or key
|
|
if not seen[k] then
|
|
seen[k] = true
|
|
list[#list + 1] = Entry(r)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.sort(list, function(a, b)
|
|
if a.nm ~= b.nm then return a.nm < b.nm end
|
|
return (a.rk or "") < (b.rk or "")
|
|
end)
|
|
return list
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Anzeige
|
|
------------------------------------------------------------------------
|
|
|
|
local frame
|
|
|
|
-- gespeicherte Position (verschoben in den Einstellungen) oder Standard: Bildschirmmitte, etwas hoeher
|
|
local function ApplyPos(f)
|
|
f:ClearAllPoints()
|
|
local p = Codex.db.settings.hintPos
|
|
if type(p) == "table" and p.point and p.x and p.y then
|
|
f:SetPoint(p.point, UIParent, p.rel or p.point, p.x, p.y)
|
|
else
|
|
f:SetPoint("CENTER", UIParent, "CENTER", 0, 160)
|
|
end
|
|
end
|
|
|
|
function Hints.SavePos()
|
|
if not frame or not frame.GetPoint then return false end
|
|
local point, _, rel, x, y = frame:GetPoint(1)
|
|
if not point then return false end
|
|
Codex.db.settings.hintPos = { point = point, rel = rel, x = x, y = y }
|
|
return true
|
|
end
|
|
|
|
function Hints.ResetPos()
|
|
Codex.db.settings.hintPos = nil
|
|
if frame then ApplyPos(frame) end
|
|
end
|
|
|
|
local function Build()
|
|
local f = CreateFrame("Button", nil, UIParent)
|
|
f:SetSize(WIDTH, 80)
|
|
f:SetFrameStrata("DIALOG")
|
|
f:SetClampedToScreen(true)
|
|
f:SetMovable(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function(self)
|
|
if Hints.editing then self:StartMoving() end
|
|
end)
|
|
f:SetScript("OnDragStop", function(self)
|
|
self:StopMovingOrSizing()
|
|
if Hints.editing then Hints.SavePos() end
|
|
end)
|
|
ApplyPos(f)
|
|
f:Hide()
|
|
local bg = UI.CreatePanel(f, "bgDeep", "gold")
|
|
bg:SetAllPoints(f)
|
|
-- Inhalt auf eigener Ebene ueber dem Hintergrund (ein Kindfenster zeichnet sonst ueber den Texten des Eltern-Fensters)
|
|
local c = CreateFrame("Frame", nil, f)
|
|
c:SetAllPoints(f)
|
|
c:SetFrameLevel((bg:GetFrameLevel() or 1) + 2)
|
|
f.title = UI.CreateLabel(c, "", { font = "heading", size = 14, color = "goldLight" })
|
|
f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -10)
|
|
f.title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, -10)
|
|
f.title:SetWordWrap(false)
|
|
f.rows = {}
|
|
for i = 1, MAX_ROWS do
|
|
local row = {}
|
|
row.icon = c:CreateTexture(nil, "ARTWORK")
|
|
row.icon:SetSize(ROW_H - 4, ROW_H - 4)
|
|
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
|
row.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -(34 + (i - 1) * ROW_H))
|
|
row.text = UI.CreateLabel(c, "", { size = 12 })
|
|
row.text:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
|
|
row.text:SetWidth(WIDTH - 150)
|
|
row.text:SetWordWrap(false)
|
|
row.cost = UI.CreateLabel(c, "", { size = 11, color = "textDim", justify = "RIGHT" })
|
|
row.cost:SetPoint("RIGHT", f, "RIGHT", -12, 0)
|
|
row.cost:SetPoint("TOP", row.icon, "TOP", 0, 0)
|
|
row.cost:SetPoint("BOTTOM", row.icon, "BOTTOM", 0, 0)
|
|
row.cost:SetWidth(120)
|
|
f.rows[i] = row
|
|
end
|
|
f.more = UI.CreateLabel(c, "", { size = 11, color = "textDim" })
|
|
f.foot = UI.CreateLabel(c, "", { size = 10, color = "textDim" })
|
|
f:SetScript("OnClick", function()
|
|
if Hints.editing then return end
|
|
Hints.Dismiss()
|
|
if f.onClick then f.onClick() end
|
|
end)
|
|
f:SetScript("OnUpdate", function(self, elapsed)
|
|
if Hints.editing then
|
|
self.age = 0
|
|
self:SetAlpha(1)
|
|
return
|
|
end
|
|
self.age = (self.age or 0) + (elapsed or 0)
|
|
if self.age >= DURATION then
|
|
Hints.Dismiss()
|
|
elseif self.age > DURATION - FADE then
|
|
self:SetAlpha(math.max(0, (DURATION - self.age) / FADE))
|
|
end
|
|
end)
|
|
return f
|
|
end
|
|
|
|
local function InCombat()
|
|
if _G.InCombatLockdown and _G.InCombatLockdown() then return true end
|
|
return _G.UnitAffectingCombat and _G.UnitAffectingCombat("player") or false
|
|
end
|
|
|
|
local function Display(h)
|
|
frame = frame or Build()
|
|
ApplyPos(frame)
|
|
local L = FW.L
|
|
frame.title:SetText(h.title)
|
|
local n = math.min(#h.entries, MAX_ROWS)
|
|
local total = 0
|
|
local money = _G.GetMoney and _G.GetMoney() or nil
|
|
for i = 1, MAX_ROWS do
|
|
local row, e = frame.rows[i], h.entries[i]
|
|
if e and i <= n then
|
|
local icon = e.sp and Codex.SpellIcon(e.sp) or nil
|
|
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
|
row.icon:Show()
|
|
row.text:SetText(e.nm .. (e.rk and (" |cff9a9aa3(" .. e.rk .. ")|r") or ""))
|
|
row.cost:SetText((e.cost and e.cost > 0 and Codex.FormatMoney) and Codex.FormatMoney(e.cost) or "")
|
|
else
|
|
row.icon:Hide()
|
|
row.text:SetText("")
|
|
row.cost:SetText("")
|
|
end
|
|
end
|
|
for _, e in ipairs(h.entries) do total = total + (e.cost or 0) end
|
|
local y = 34 + n * ROW_H + 4
|
|
local more = #h.entries - n
|
|
frame.more:ClearAllPoints()
|
|
frame.more:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
|
|
frame.more:SetText(more > 0 and string.format(L.CODEX_HINT_MORE, more) or "")
|
|
if more > 0 then y = y + 16 end
|
|
local foot = Hints.editing and L.CODEX_HINT_DRAG or L.CODEX_HINT_CLICK
|
|
if total > 0 and Codex.FormatMoney and not Hints.editing then
|
|
foot = string.format(L.CODEX_HINT_COST, Codex.FormatMoney(total))
|
|
if money and money < total then
|
|
foot = foot .. " | " .. string.format(L.CODEX_HINT_MISSING, Codex.FormatMoney(total - money))
|
|
end
|
|
foot = foot .. " | " .. L.CODEX_HINT_CLICK
|
|
end
|
|
frame.foot:ClearAllPoints()
|
|
frame.foot:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
|
|
frame.foot:SetText(foot)
|
|
frame:SetHeight(y + 26)
|
|
frame.onClick = h.onClick
|
|
frame.age = 0
|
|
frame:SetAlpha(1)
|
|
frame:Show()
|
|
Hints.showing = true
|
|
Hints.shown = Hints.shown + 1
|
|
Hints.last = h
|
|
end
|
|
|
|
function Hints.Dismiss()
|
|
if Hints.editing then return end
|
|
if frame then frame:Hide() end
|
|
Hints.showing = false
|
|
Hints.Flush()
|
|
end
|
|
|
|
-- naechsten wartenden Hinweis zeigen, wenn kein Kampf ist und keiner laeuft
|
|
function Hints.Flush()
|
|
if Hints.showing or Hints.editing or InCombat() then return end
|
|
local h = table.remove(Hints.queue, 1)
|
|
if h then Display(h) end
|
|
end
|
|
|
|
function Hints.Push(h)
|
|
if not h or #h.entries == 0 then return false end
|
|
Hints.queue[#Hints.queue + 1] = h
|
|
Hints.Flush()
|
|
return true
|
|
end
|
|
|
|
local function OpenPlan(level)
|
|
return function()
|
|
UI:Show("codex")
|
|
if Codex.ui and Codex.ui.Go then Codex.ui.Go("learn", level) end
|
|
end
|
|
end
|
|
|
|
function Hints.OnLevelUp(level)
|
|
level = tonumber(level)
|
|
if not level or not Hints.ClassOn() or not Codex.db.settings.enabled then return false end
|
|
local entries = Hints.ClassEntries(level)
|
|
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, level), entries = entries, onClick = OpenPlan(level) })
|
|
end
|
|
|
|
local prevSkill = {}
|
|
function Hints.OnSkill(skill, value)
|
|
value = tonumber(value)
|
|
if not skill or not value then return false end
|
|
local from = prevSkill[skill] or (value - 1)
|
|
prevSkill[skill] = value
|
|
if not Hints.ProfOn() or not Codex.db.settings.enabled then return false end
|
|
local entries = Hints.ProfEntries(skill, from, value)
|
|
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_PROF_TITLE, skill, value), entries = entries, onClick = function() UI:Show("codex") end })
|
|
end
|
|
|
|
-- Test aus den Einstellungen: letzte Stufe mit Daten, sonst eine Beispielzeile
|
|
function Hints.Demo()
|
|
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
|
|
local entries
|
|
for lv = pl, 1, -1 do
|
|
entries = Hints.ClassEntries(lv)
|
|
if #entries > 0 then
|
|
pl = lv
|
|
break
|
|
end
|
|
end
|
|
if not entries or #entries == 0 then
|
|
entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 } }
|
|
end
|
|
Hints.queue = {}
|
|
Hints.showing = false
|
|
if frame then frame:Hide() end
|
|
Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries, onClick = OpenPlan(pl) })
|
|
end
|
|
|
|
-- Verschieben: der Hinweis bleibt stehen, bis der Modus beendet wird; Position wird beim Loslassen gespeichert
|
|
function Hints.SetEditMode(on)
|
|
on = on and true or false
|
|
if on == (Hints.editing == true) then return end
|
|
if on then
|
|
Hints.queue, Hints.showing = {}, false
|
|
if frame then frame:Hide() end
|
|
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
|
|
local entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 }, { nm = FW.L.CODEX_HINT_DEMO, rk = "2", cost = 25000 } }
|
|
Hints.editing = true
|
|
Display({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries })
|
|
else
|
|
Hints.SavePos()
|
|
Hints.editing = false
|
|
Hints.showing = false
|
|
if frame then frame:Hide() end
|
|
Hints.Flush()
|
|
end
|
|
FW:Fire("CODEX_HINT_EDIT")
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Ereignisse
|
|
------------------------------------------------------------------------
|
|
|
|
-- Muster aus dem Textbaustein des Spiels ("... %s ... %d" oder mit Stellen "%1$s ... %2$d"). Rueckgabe: Muster, Reihenfolge
|
|
-- der Platzhalter (Stelle -> Rang in der Fundstelle), sonst nil
|
|
local function SkillPattern(fmt)
|
|
fmt = fmt or _G.SKILL_RANK_UP
|
|
if type(fmt) ~= "string" then return nil end
|
|
local out, order, i = { "^" }, {}, 1
|
|
while i <= #fmt do
|
|
local pos, kind, len = fmt:match("^%%(%d+)%$([sd])()", i)
|
|
if pos then
|
|
i = len
|
|
else
|
|
kind, len = fmt:match("^%%([sd])()", i)
|
|
pos = #order + 1
|
|
if kind then i = len end
|
|
end
|
|
if kind then
|
|
out[#out + 1] = kind == "s" and "(.+)" or "(%d+)"
|
|
order[#order + 1] = tonumber(pos)
|
|
else
|
|
local ch = fmt:sub(i, i)
|
|
out[#out + 1] = ch:match("%w") and ch or ("%" .. ch)
|
|
i = i + 1
|
|
end
|
|
end
|
|
out[#out + 1] = "$"
|
|
return table.concat(out), order
|
|
end
|
|
Hints.SkillPattern = SkillPattern
|
|
|
|
FW:RegisterEvent("PLAYER_LEVEL_UP", function(_, level)
|
|
if C_Timer and C_Timer.After then
|
|
C_Timer.After(1, function() Hints.OnLevelUp(level) end)
|
|
else
|
|
Hints.OnLevelUp(level)
|
|
end
|
|
end, Hints)
|
|
FW:RegisterEvent("PLAYER_REGEN_ENABLED", function() Hints.Flush() end, Hints)
|
|
FW:RegisterEvent("PLAYER_REGEN_DISABLED", function()
|
|
if Hints.editing then Hints.SetEditMode(false) end
|
|
end, Hints)
|
|
FW:RegisterEvent("CHAT_MSG_SKILL", function(_, text)
|
|
local pat, order = SkillPattern()
|
|
if type(text) ~= "string" or not pat then return end
|
|
local caps = { text:match(pat) }
|
|
if #caps == 0 then return end
|
|
local skill, value = caps[1], caps[2]
|
|
if order and order[1] == 2 then skill, value = caps[2], caps[1] end -- Fertigkeit steht an Stelle 2
|
|
if skill then Hints.OnSkill(skill, value) end
|
|
end, Hints)
|
|
|
|
------------------------------------------------------------------------
|
|
-- Einstellungen: Abschnitt
|
|
------------------------------------------------------------------------
|
|
|
|
if UI.RegisterSettingsSection then
|
|
UI:RegisterSettingsSection("codex-hints", {
|
|
order = 106,
|
|
height = 300,
|
|
build = function(parent)
|
|
local L = FW.L
|
|
local f, stack = UI.CreateSection(parent, L.CODEX_HINT_HEADER)
|
|
local note = UI.CreateLabel(f, L.CODEX_HINT_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
|
|
stack:Add(note, { fit = true, gap = 8 })
|
|
local classBox = UI.CreateCheckbox(f, L.CODEX_HINT_CLASS_SWITCH, function() return Hints.ClassOn() end,
|
|
function(on) Codex.db.settings.hintClass = on and true or false end)
|
|
stack:Add(classBox, { fit = true, gap = 10 })
|
|
local profBox = UI.CreateCheckbox(f, L.CODEX_HINT_PROF_SWITCH, function() return Hints.ProfOn() end,
|
|
function(on) Codex.db.settings.hintProf = on and true or false end)
|
|
stack:Add(profBox, { fit = true, gap = 4 })
|
|
local test = UI.CreateButton(f, L.CODEX_HINT_TEST, 200, 26, function() Hints.Demo() end, "default")
|
|
stack:Add(test, { height = 26, gap = 12 })
|
|
local move
|
|
move = UI.CreateButton(f, L.CODEX_HINT_MOVE, 200, 26, function()
|
|
Hints.SetEditMode(not Hints.editing)
|
|
end, "default")
|
|
stack:Add(move, { height = 26, gap = 6 })
|
|
local reset = UI.CreateButton(f, L.CODEX_HINT_RESET, 200, 26, function() Hints.ResetPos() end, "default")
|
|
stack:Add(reset, { height = 26, gap = 6 })
|
|
stack:Layout()
|
|
f.classBox, f.profBox, f.move = classBox, profBox, move
|
|
FW:On("CODEX_HINT_EDIT", function()
|
|
move:SetLabel(Hints.editing and L.CODEX_HINT_MOVE_DONE or L.CODEX_HINT_MOVE)
|
|
move:SetStyle(Hints.editing and "primary" or "default")
|
|
end, f)
|
|
return f
|
|
end,
|
|
refresh = function(frame)
|
|
frame.classBox:Refresh()
|
|
frame.profBox:Refresh()
|
|
end,
|
|
})
|
|
end
|