Files
Freewood-OctoWoW-Beasts/BeastsUI.lua
T
2026-10-27 14:14:36 +00:00

1123 lines
39 KiB
Lua

-- Freewood-OctoWoW-Beasts: Tab "Wildtiere"
-- Links Suche, Filter und Liste, rechts Familienfaktoren, Nahrung, Fundorte und Faehigkeiten.
local ADDON, NS = ...
local FW = _G.Freewood
local Beasts = NS.Beasts
if not FW or not Beasts then
return
end
local UI = FW.UI
local Pet = FW.Pet
local LIST_W = 300
local ROW_H, ROW_GAP = 50, 6
local CARD_W, CARD_H, CARD_GAP = 170, 48, 10
local ITEM_H = 24
local FAMILY_ICON = {
bat = "Ability_Hunter_Pet_Bat", bear = "Ability_Hunter_Pet_Bear", boar = "Ability_Hunter_Pet_Boar",
carrionbird = "Ability_Hunter_Pet_Vulture", cat = "Ability_Hunter_Pet_Cat", crab = "Ability_Hunter_Pet_Crab",
crocolisk = "Ability_Hunter_Pet_Crocolisk", gorilla = "Ability_Hunter_Pet_Gorilla",
hyena = "Ability_Hunter_Pet_Hyena", owl = "Ability_Hunter_Pet_Owl", raptor = "Ability_Hunter_Pet_Raptor",
scorpid = "Ability_Hunter_Pet_Scorpid", spider = "Ability_Hunter_Pet_Spider",
tallstrider = "Ability_Hunter_Pet_TallStrider", turtle = "Ability_Hunter_Pet_Turtle",
windserpent = "Ability_Hunter_Pet_WindSerpent", wolf = "Ability_Hunter_Pet_Wolf",
}
local function FamilyIcon(fid)
return "Interface\\Icons\\" .. (FAMILY_ICON[fid] or "Ability_Hunter_BeastTaming")
end
local STATUS_COLOR = { tamed = "success", lored = "goldLight", seen = "text", unknown = "textDim" }
local function StatusText(status)
local L = FW.L
if status == "tamed" then return L.BEAST_STATUS_TAMED end
if status == "lored" then return L.BEAST_STATUS_LORED end
if status == "seen" then return L.BEAST_STATUS_SEEN end
return L.BEAST_STATUS_UNKNOWN
end
local function LevelText(s)
local L = FW.L
if not s.lvl1 then
return nil
end
local range = (s.lvl2 and s.lvl2 ~= s.lvl1) and (s.lvl1 .. "-" .. s.lvl2) or tostring(s.lvl1)
local text = string.format(L.BEAST_LEVEL, range)
if s.c == "elite" then
text = text .. " " .. L.BEAST_ELITE
elseif s.c == "rare" then
text = text .. " " .. L.BEAST_RARE
elseif s.c == "rareelite" then
text = text .. " " .. L.BEAST_RAREELITE
elseif s.c == "worldboss" then
text = text .. " " .. L.BEAST_BOSS
elseif type(s.c) == "string" and s.c ~= "" then
local key = string.lower(s.c)
if key == "rare" then text = text .. " " .. L.BEAST_RARE end
end
return text
end
local function Subtitle(s)
local parts = { s.family or FW.L.BEAST_FAMILY_UNKNOWN }
local lvl = LevelText(s)
if lvl then
parts[#parts + 1] = lvl
end
if s.zone then
parts[#parts + 1] = s.zone
end
return table.concat(parts, " - ")
end
local function FoodList(codes)
local out = {}
if type(codes) == "string" then
for i = 1, #codes do
local c = codes:sub(i, i)
out[#out + 1] = FW.L["BEAST_FOOD_" .. c] or c
end
end
return out
end
local function SpellName(spellID)
if C_Spell and C_Spell.GetSpellInfo then
local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
if ok and type(info) == "table" and info.name then
return info.name
end
end
return "Spell " .. tostring(spellID)
end
local function SpellRank(spellID)
if C_Spell and C_Spell.GetSpellSubtext then
local ok, text = pcall(C_Spell.GetSpellSubtext, spellID)
if ok and type(text) == "string" and text ~= "" then
return text
end
end
return nil
end
------------------------------------------------------------------------
-- Auswahlliste (Dropdown)
------------------------------------------------------------------------
local popup
local function ClosePopup()
if popup then
popup:Hide()
popup.owner = nil
end
end
local function GetPopup()
if popup then
return popup
end
popup = UI.CreatePanel(UIParent, "bgDeep", "gold")
popup:SetFrameStrata("FULLSCREEN_DIALOG")
popup:SetClampedToScreen(true)
popup:EnableMouse(true)
popup.scroll = UI.CreateScrollFrame(popup)
popup.scroll:SetPoint("TOPLEFT", popup, "TOPLEFT", 3, -3)
popup.scroll:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -3, 3)
popup.items = {}
popup:Hide()
return popup
end
local function OpenPopup(anchor, options, currentKey, onPick)
local p = GetPopup()
if p:IsShown() and p.owner == anchor then
ClosePopup()
return
end
p.owner = anchor
for i, opt in ipairs(options) do
local item = p.items[i]
if not item then
item = CreateFrame("Button", nil, p.scroll.child)
item:SetHeight(ITEM_H)
item.bg = item:CreateTexture(nil, "BACKGROUND")
item.bg:SetAllPoints()
item.label = UI.CreateLabel(item, "", { size = 12 })
item.label:SetPoint("LEFT", item, "LEFT", 8, 0)
item.label:SetPoint("RIGHT", item, "RIGHT", -8, 0)
item.label:SetWordWrap(false)
item:SetScript("OnEnter", function(self)
self.bg:SetColorTexture(UI.Color("hover"))
end)
item:SetScript("OnLeave", function(self)
self.bg:SetColorTexture(UI.Color(self.active and "card" or "bgDeep"))
end)
item:SetScript("OnClick", function(self)
local cb = p.onPick
ClosePopup()
if cb then
cb(self.key)
end
end)
p.items[i] = item
end
item.key = opt.key
item.active = (opt.key == currentKey)
item.bg:SetColorTexture(UI.Color(item.active and "card" or "bgDeep"))
item.label:SetText(opt.text)
item.label:SetTextColor(UI.Color(item.active and "goldLight" or "text"))
item:ClearAllPoints()
item:SetPoint("TOPLEFT", p.scroll.child, "TOPLEFT", 0, -(i - 1) * ITEM_H)
item:SetPoint("TOPRIGHT", p.scroll.child, "TOPRIGHT", 0, -(i - 1) * ITEM_H)
item:Show()
end
for i = #options + 1, #p.items do
p.items[i]:Hide()
end
p.onPick = onPick
p.scroll:SetContentHeight(#options * ITEM_H)
p.scroll:ScrollToTop()
p:SetScale((anchor:GetEffectiveScale() or 1) / (UIParent:GetEffectiveScale() or 1))
p:SetSize(math.max(anchor:GetWidth() or 150, 190), math.min(#options * ITEM_H + 6, 270))
p:ClearAllPoints()
p:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2)
p:Show()
end
local function CreateSelect(parent, width, getOptions, getKey, onPick)
local button
button = UI.CreateButton(parent, "", width, 24, function()
OpenPopup(button, getOptions(), getKey(), function(key)
onPick(key)
end)
end, "default")
button.label:ClearAllPoints()
button.label:SetPoint("LEFT", button, "LEFT", 8, 0)
button.label:SetPoint("RIGHT", button, "RIGHT", -8, 0)
button.label:SetJustifyH("LEFT")
button.label:SetWordWrap(false)
return button
end
------------------------------------------------------------------------
-- Suchfeld
------------------------------------------------------------------------
local function CreateSearchBox(parent, width, placeholder, onChange)
local box = UI.CreatePanel(parent, "bgDeep", "bronze")
box:SetSize(width, 26)
local edit = CreateFrame("EditBox", nil, box)
edit:SetAutoFocus(false)
edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2)
edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2)
edit:SetMaxLetters(48)
if not edit:SetFont(UI.GetFontPath("body"), 12, "") then
edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
end
edit:SetTextColor(UI.Color("text"))
local hint = UI.CreateLabel(box, placeholder, { size = 12, color = "textDim" })
hint:SetPoint("LEFT", box, "LEFT", 10, 0)
local function Sync()
if (edit:GetText() or "") == "" then
hint:Show()
else
hint:Hide()
end
end
edit:SetScript("OnTextChanged", function()
Sync()
onChange(edit:GetText() or "")
end)
edit:SetScript("OnEscapePressed", function(self)
self:ClearFocus()
end)
edit:SetScript("OnEditFocusGained", function()
UI.SetBorderColor(box, "gold")
end)
edit:SetScript("OnEditFocusLost", function()
UI.SetBorderColor(box, "bronze")
end)
box.edit = edit
Sync()
return box
end
------------------------------------------------------------------------
-- Listenzeile
------------------------------------------------------------------------
local function CreateListRow(parent, onSelect)
local row = CreateFrame("Button", nil, parent)
row:SetHeight(ROW_H)
row.bg = row:CreateTexture(nil, "BACKGROUND")
row.bg:SetAllPoints()
UI.AddBorder(row, "bronze", 0.6)
row.icon = row:CreateTexture(nil, "ARTWORK")
row.icon:SetSize(34, 34)
row.icon:SetPoint("LEFT", row, "LEFT", 8, 0)
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.name = UI.CreateLabel(row, "", { font = "heading", size = 13, color = "goldLight" })
row.name:SetPoint("TOPLEFT", row, "TOPLEFT", 52, -10)
row.name:SetWidth(LIST_W - 150)
row.name:SetWordWrap(false)
row.sub = UI.CreateLabel(row, "", { size = 11, color = "textDim" })
row.sub:SetPoint("TOPLEFT", row.name, "BOTTOMLEFT", 0, -6)
row.sub:SetWidth(LIST_W - 76)
row.sub:SetWordWrap(false)
row.chip = UI.CreateChip(row, "", "textDim")
row.chip:SetPoint("TOPRIGHT", row, "TOPRIGHT", -10, -9)
local function Refresh()
if row.selected then
row.bg:SetColorTexture(UI.Color("hover"))
UI.SetBorderColor(row, "gold")
elseif row.hover then
row.bg:SetColorTexture(UI.Color("hover", 0.7))
UI.SetBorderColor(row, "bronze")
else
row.bg:SetColorTexture(UI.Color("card"))
UI.SetBorderColor(row, "bronze", 0.6)
end
end
function row:SetSelected(selected)
self.selected = selected
Refresh()
end
row:SetScript("OnEnter", function()
row.hover = true
Refresh()
end)
row:SetScript("OnLeave", function()
row.hover = false
Refresh()
end)
row:SetScript("OnClick", function()
onSelect(row.id)
end)
Refresh()
return row
end
------------------------------------------------------------------------
-- Stapel im Detailbereich (wiederverwendete Bausteine)
------------------------------------------------------------------------
local function NewStack(scroll)
local s = { child = scroll.child, y = 0, pool = { label = {}, row = {}, chip = {} }, used = { label = 0, row = 0, chip = 0 } }
local function take(kind, create)
s.used[kind] = s.used[kind] + 1
local item = s.pool[kind][s.used[kind]]
if not item then
item = create()
s.pool[kind][s.used[kind]] = item
end
item:Show()
return item
end
function s:Reset()
for kind, list in pairs(self.pool) do
for _, item in ipairs(list) do
item:Hide()
end
self.used[kind] = 0
end
self.y = 0
end
function s:Gap(h)
self.y = self.y + h
end
function s:Heading(text)
local fs = take("label", function()
return UI.CreateLabel(self.child, "", { size = 12 })
end)
UI.SetFont(fs, "heading", 14)
fs:SetTextColor(UI.Color("goldLight"))
fs:SetWordWrap(false)
fs:ClearAllPoints()
fs:SetPoint("TOPLEFT", self.child, "TOPLEFT", 0, -self.y)
fs:SetPoint("TOPRIGHT", self.child, "TOPRIGHT", 0, -self.y)
fs:SetHeight(20)
fs:SetText(text)
self.y = self.y + 24
end
function s:Text(text, colorKey)
local fs = take("label", function()
return UI.CreateLabel(self.child, "", { size = 12 })
end)
UI.SetFont(fs, "body", 12)
fs:SetTextColor(UI.Color(colorKey or "textDim"))
fs:SetWordWrap(true)
fs:SetJustifyV("TOP")
fs:ClearAllPoints()
fs:SetPoint("TOPLEFT", self.child, "TOPLEFT", 0, -self.y)
fs:SetPoint("TOPRIGHT", self.child, "TOPRIGHT", 0, -self.y)
local lines = math.max(1, math.ceil(#text / 75))
fs:SetHeight(lines * 15)
fs:SetText(text)
self.y = self.y + lines * 15 + 4
end
-- Marken nebeneinander (einzeilig, bricht bei Bedarf um)
function s:Chips(list, colorKey)
local x, rowY = 0, self.y
for _, text in ipairs(list) do
local chip = take("chip", function()
return UI.CreateChip(self.child, "", "bronze")
end)
chip:SetText(text, colorKey or "goldAntique")
local w = chip:GetWidth() or 60
if x > 0 and x + w > 500 then
x = 0
rowY = rowY + 26
end
chip:ClearAllPoints()
chip:SetPoint("TOPLEFT", self.child, "TOPLEFT", x, -rowY)
x = x + w + 6
end
self.y = rowY + 28
end
-- Zeile: links Text, rechts Text oder Marke; optional klickbar
function s:Row(left, right, rightColor, onClick, selected)
local row = take("row", function()
local r = CreateFrame("Button", nil, self.child)
r:SetHeight(26)
r.bg = r:CreateTexture(nil, "BACKGROUND")
r.bg:SetAllPoints()
UI.AddBorder(r, "bronze", 0.6)
r.left = UI.CreateLabel(r, "", { size = 12 })
r.left:SetPoint("LEFT", r, "LEFT", 10, 0)
r.left:SetWordWrap(false)
r.right = UI.CreateLabel(r, "", { size = 11, color = "textDim", justify = "RIGHT" })
r.right:SetPoint("RIGHT", r, "RIGHT", -10, 0)
r.right:SetWordWrap(false)
r.left:SetPoint("RIGHT", r.right, "LEFT", -8, 0)
local function paint()
if r.selected then
r.bg:SetColorTexture(UI.Color("hover"))
UI.SetBorderColor(r, "gold")
elseif r.hover and r.clickable then
r.bg:SetColorTexture(UI.Color("hover", 0.7))
UI.SetBorderColor(r, "bronze")
else
r.bg:SetColorTexture(UI.Color("card"))
UI.SetBorderColor(r, "bronze", 0.6)
end
end
r.paint = paint
r:SetScript("OnEnter", function()
r.hover = true
paint()
end)
r:SetScript("OnLeave", function()
r.hover = false
paint()
end)
r:SetScript("OnClick", function()
if r.onClick then
r.onClick()
end
end)
return r
end)
row.onClick = onClick
row.clickable = onClick ~= nil
row.selected = selected and true or false
row.left:SetText(left or "")
row.right:SetText(right or "")
row.right:SetTextColor(UI.Color(rightColor or "textDim"))
row:ClearAllPoints()
row:SetPoint("TOPLEFT", self.child, "TOPLEFT", 0, -self.y)
row:SetPoint("TOPRIGHT", self.child, "TOPRIGHT", 0, -self.y)
row.paint()
self.y = self.y + 30
return row
end
function s:Height()
return self.y
end
return s
end
------------------------------------------------------------------------
-- Tab
------------------------------------------------------------------------
local function FactorColor(f)
if f > 1.0001 then
return "goldLight"
elseif f < 0.9999 then
return "danger"
end
return "text"
end
local function Build(content)
local L = FW.L
local f = CreateFrame("Frame", nil, content)
local rows = {}
local filter = { text = "", fid = nil, zone = nil, status = nil, ability = nil, rank = nil, rankMin = false }
local selectedGroup
---------------- linke Spalte ----------------
local title = UI.CreateLabel(f, L.BEAST_TITLE, { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
local countLabel = UI.CreateLabel(f, "", { size = 11, color = "textDim", justify = "RIGHT" })
countLabel:SetPoint("TOPRIGHT", f, "TOPLEFT", LIST_W, 0)
countLabel:SetWidth(LIST_W - 90)
countLabel:SetWordWrap(false)
local listDivider = UI.CreateDivider(f)
listDivider:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -26)
listDivider:SetWidth(LIST_W)
local search = CreateSearchBox(f, LIST_W, L.BEAST_SEARCH, function(text)
filter.text = text
if f.Refresh then f.Refresh() end
end)
search:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -36)
local familySelect, zoneSelect
familySelect = CreateSelect(f, 146, function()
local fams = Beasts:FilterChoices()
local opts = { { key = false, text = FW.L.BEAST_ALL_FAMILIES } }
for _, fam in ipairs(fams) do
opts[#opts + 1] = { key = fam.id, text = fam.text }
end
return opts
end, function()
return filter.fid or false
end, function(key)
filter.fid = key or nil
if f.Refresh then f.Refresh() end
end)
familySelect:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -68)
zoneSelect = CreateSelect(f, 146, function()
local _, zones = Beasts:FilterChoices()
local opts = { { key = false, text = FW.L.BEAST_ALL_ZONES } }
for _, z in ipairs(zones) do
opts[#opts + 1] = { key = z, text = z }
end
return opts
end, function()
return filter.zone or false
end, function(key)
filter.zone = key or nil
if f.Refresh then f.Refresh() end
end)
zoneSelect:SetPoint("TOPLEFT", f, "TOPLEFT", 154, -68)
-- Faehigkeit, die das Zaehmen lehrt (und Rang)
local abilitySelect, rankSelect
abilitySelect = CreateSelect(f, 186, function()
local opts = { { key = false, text = FW.L.BEAST_ALL_ABILITIES } }
for _, name in ipairs(Beasts:AbilityChoices()) do
opts[#opts + 1] = { key = name, text = name }
end
return opts
end, function()
return filter.ability or false
end, function(key)
filter.ability = key or nil
filter.rank, filter.rankMin = nil, false
if f.Refresh then f.Refresh() end
end)
abilitySelect:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -98)
rankSelect = CreateSelect(f, 108, function()
local opts = { { key = false, text = FW.L.BEAST_ALL_RANKS } }
if filter.ability then
local ranks = Beasts:AbilityRanks(filter.ability)
for _, r in ipairs(ranks) do
opts[#opts + 1] = { key = r, text = string.format(FW.L.BEAST_RANK_EXACT, r) }
end
for i, r in ipairs(ranks) do
if i > 1 then
opts[#opts + 1] = { key = -r, text = string.format(FW.L.BEAST_RANK_MIN, r) }
end
end
end
return opts
end, function()
if not filter.rank then
return false
end
return filter.rankMin and -filter.rank or filter.rank
end, function(key)
if key then
filter.rank, filter.rankMin = math.abs(key), key < 0
else
filter.rank, filter.rankMin = nil, false
end
if f.Refresh then f.Refresh() end
end)
rankSelect:SetPoint("TOPLEFT", f, "TOPLEFT", 192, -98)
-- Statusfilter
local statusButtons = {}
local statusDefs = {
{ key = false, text = function() return FW.L.BEAST_STATUS_ALL end, w = 44 },
{ key = "seen", text = function() return FW.L.BEAST_STATUS_SEEN end, w = 58 },
{ key = "lored", text = function() return FW.L.BEAST_STATUS_LORED end, w = 66 },
{ key = "tamed", text = function() return FW.L.BEAST_STATUS_TAMED end, w = 60 },
{ key = "unknown", text = function() return FW.L.BEAST_STATUS_UNKNOWN end, w = 60 },
}
local prev
for _, def in ipairs(statusDefs) do local def = def
local b = UI.CreateButton(f, def.text(), def.w, 22, function()
filter.status = def.key or nil
if f.Refresh then f.Refresh() end
end, "ghost")
UI.SetFont(b.label, "body", 11)
if prev then
b:SetPoint("LEFT", prev, "RIGHT", 3, 0)
else
b:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -128)
end
b.def = def
statusButtons[#statusButtons + 1] = b
prev = b
end
local listScroll = UI.CreateScrollFrame(f)
listScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -158)
listScroll:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 62)
listScroll:SetWidth(LIST_W)
local emptyLabel = UI.CreateLabel(listScroll.child, "", { size = 12, color = "textDim", justifyV = "TOP" })
emptyLabel:SetPoint("TOPLEFT", listScroll.child, "TOPLEFT", 4, -6)
emptyLabel:SetPoint("TOPRIGHT", listScroll.child, "TOPRIGHT", -4, -6)
emptyLabel:SetHeight(120)
emptyLabel:SetWordWrap(true)
-- Schalter: Startdaten anzeigen oder selbst erkunden
local seedToggle = UI.CreateCheckbox(f, L.BEAST_SEED_TOGGLE, function()
return Beasts:SeedEnabled()
end, function(on)
Beasts:SetSeedEnabled(on)
end)
seedToggle:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 4)
UI.AttachTooltip(seedToggle, L.BEAST_SEED_TOGGLE, { L.BEAST_SEED_TIP1, L.BEAST_SEED_TIP2 })
-- Auf Octo/1.12 zeichnet die Codex-Karte (dieselbe wie "Auf Karte zeigen" in der Datenbank);
-- ohne Codex bleibt die alte Beasts-Ebene.
local Overlay = NS.MapOverlay
do
local C = FW.Codex
local M = C and C.Map
if M and M.Show then
local W = { _mode = nil, _single = nil, pts = nil }
setmetatable(W, { __index = function(t, k)
if k == "mode" or k == "singleID" then
if rawget(t, "pts") and M.points == rawget(t, "pts") then
return rawget(t, k == "mode" and "_mode" or "_single")
end
return nil
end
end })
local function Put(ids, label, mode, single)
local pts = Beasts:MapPoints(ids)
if M:Show(pts, label) then
W.pts = M.points
W._mode, W._single = mode, single
end
FW:Fire("BEASTS_MAP_CHANGED")
end
function W:ShowSingle(id, name)
Put({ [id] = true }, name, "single", id)
end
function W:ShowMany(ids, count)
Put(ids, string.format(FW.L.BEAST_MAP_MANY, count), "many", nil)
end
function W:Clear()
if M.points and M.points == rawget(W, "pts") then
M:Clear()
end
W.pts, W._mode, W._single = nil, nil, nil
FW:Fire("BEASTS_MAP_CHANGED")
end
function W:Focus() end
Overlay = W
end
end
local mapToggle = UI.CreateCheckbox(f, L.BEAST_MAP_FILTERED, function()
return Overlay and Overlay.mode == "many"
end, function(on)
if not Overlay then
return
end
if on then
f.ShowFilteredOnMap()
else
Overlay:Clear()
end
end)
mapToggle:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 32)
UI.AttachTooltip(mapToggle, L.BEAST_MAP_FILTERED, { L.BEAST_MAP_FILTERED_TIP })
local separator = f:CreateTexture(nil, "ARTWORK")
separator:SetColorTexture(UI.Color("bronze", 0.4))
separator:SetWidth(1)
separator:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 10, 0)
separator:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", LIST_W + 10, 0)
---------------- rechte Spalte ----------------
local detail = CreateFrame("Frame", nil, f)
detail:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, 0)
detail:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
local hint = UI.CreateLabel(f, L.BEAST_SELECT, { size = 12, color = "textDim", justifyV = "TOP" })
hint:SetPoint("TOPLEFT", f, "TOPLEFT", LIST_W + 22, -4)
hint:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -4)
hint:SetWordWrap(true)
-- Bild: 3D-Modell des Tiers (aus der NPC-Nummer), solange es nicht laedt das Familien-Symbol
local portrait = UI.CreatePanel(detail, "bgDeep", "bronze")
portrait:SetSize(64, 64)
portrait:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, 0)
local portraitIcon = portrait:CreateTexture(nil, "ARTWORK")
portraitIcon:SetPoint("TOPLEFT", portrait, "TOPLEFT", 6, -6)
portraitIcon:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", -6, 6)
portraitIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
local model = CreateFrame("PlayerModel", nil, portrait)
model:SetPoint("TOPLEFT", portrait, "TOPLEFT", 2, -2)
model:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", -2, 2)
model:SetAlpha(0)
-- 1.12 kennt OnModelLoaded nicht: dann bleibt das Modell unsichtbar und das Symbol steht
pcall(model.SetScript, model, "OnModelLoaded", function(self)
self:SetAlpha(1)
portraitIcon:Hide()
end)
local nameLabel = UI.CreateLabel(detail, "", { font = "heading", size = 20, color = "goldLight" })
nameLabel:SetPoint("TOPLEFT", detail, "TOPLEFT", 76, -2)
nameLabel:SetWidth(330)
nameLabel:SetWordWrap(false)
local subLabel = UI.CreateLabel(detail, "", { size = 12, color = "textDim" })
subLabel:SetPoint("TOPLEFT", detail, "TOPLEFT", 76, -32)
subLabel:SetWidth(400)
subLabel:SetWordWrap(false)
local mapButton = UI.CreateButton(detail, L.BEAST_MAP_SHOW, 170, 22, function()
local id = Beasts.selectedID
if not (Overlay and id) then
return
end
if Overlay.mode == "single" and Overlay.singleID == id then
Overlay:Clear()
else
Overlay:ShowSingle(id, Beasts:Summary(id).name)
Overlay:Focus(selectedGroup)
end
end, "primary")
mapButton:SetPoint("TOPLEFT", detail, "TOPLEFT", 76, -46)
local stateChip = UI.CreateChip(detail, "", "success")
stateChip:SetPoint("TOPRIGHT", detail, "TOPRIGHT", 0, -4)
local sourceChip = UI.CreateChip(detail, "", "textDim")
sourceChip:SetPoint("RIGHT", stateChip, "LEFT", -6, 0)
local headDivider = UI.CreateDivider(detail)
headDivider:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, -76)
headDivider:SetPoint("TOPRIGHT", detail, "TOPRIGHT", 0, -76)
-- Familienfaktoren
local cards = {}
local defs = {
{ key = "hp", label = L.PET_HEALTH },
{ key = "dmg", label = L.PET_DAMAGE },
{ key = "armor", label = L.PET_ARMOR },
}
for i, def in ipairs(defs) do
local card = UI.CreateStatCard(detail, def.label, CARD_W, CARD_H)
UI.SetFont(card.value, "heading", 15)
card:SetPoint("TOPLEFT", detail, "TOPLEFT", (i - 1) * (CARD_W + CARD_GAP), -88)
cards[def.key] = card
end
local coordsArea = UI.CreateTextArea(detail, 100, 64)
coordsArea:SetPoint("BOTTOMLEFT", detail, "BOTTOMLEFT", 0, 0)
coordsArea:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", 0, 0)
coordsArea:SetHeight(64)
local coordsHint = UI.CreateLabel(detail, L.BEAST_COORDS_HINT, { size = 11, color = "textDim" })
coordsHint:SetPoint("BOTTOMLEFT", coordsArea, "TOPLEFT", 0, 4)
coordsHint:SetPoint("BOTTOMRIGHT", coordsArea, "TOPRIGHT", 0, 4)
coordsHint:SetWordWrap(false)
local sectionScroll = UI.CreateScrollFrame(detail)
sectionScroll:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, -146)
sectionScroll:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", 0, 90)
local stack = NewStack(sectionScroll)
---------------- Inhalt fuellen ----------------
local function CoordsText(g)
local lines = { g.name }
local function add(tag, list)
local limit = math.min(#list, 40)
for i = 1, limit do
local p = list[i]
local times = (p[3] and p[3] > 1) and string.format(" (x%d)", p[3]) or ""
lines[#lines + 1] = string.format("[%s] %.1f, %.1f%s", tag, p[1] / 10, p[2] / 10, times)
end
if #list > limit then
lines[#lines + 1] = string.format("[%s] ... +%d", tag, #list - limit)
end
end
add(FW.L.BEAST_TAG_OWN, g.own)
add(FW.L.BEAST_TAG_IMPORT, g.imp)
add(FW.L.BEAST_TAG_START, g.seed)
return table.concat(lines, "\n")
end
local ShowDetail
local function FillSections(s)
local L2 = FW.L
stack:Reset()
-- Nahrung
stack:Heading(L2.BEAST_FOOD)
local foods = FoodList(s.diet)
if #foods == 0 then
stack:Text(L2.BEAST_FOOD_UNKNOWN, "textDim")
else
stack:Chips(foods, "goldAntique")
end
stack:Gap(6)
-- Fundorte
stack:Heading(L2.BEAST_LOCATIONS)
local groups = Beasts:Locations(s)
if #groups == 0 then
stack:Text(L2.BEAST_LOCATIONS_NONE, "textDim")
coordsArea:SetText("")
else
local keep
for _, g in ipairs(groups) do
if selectedGroup == g.mapID then
keep = g
end
end
if not keep then
keep = groups[1]
selectedGroup = keep.mapID
end
for _, g in ipairs(groups) do local g = g
local tags = {}
if #g.own > 0 then tags[#tags + 1] = L2.BEAST_TAG_OWN .. " " .. #g.own end
if #g.imp > 0 then tags[#tags + 1] = L2.BEAST_TAG_IMPORT .. " " .. #g.imp end
if #g.seed > 0 then tags[#tags + 1] = L2.BEAST_TAG_START .. " " .. #g.seed end
stack:Row(g.name, table.concat(tags, " | "), "textDim", function()
selectedGroup = g.mapID
FillSections(s)
end, g.mapID == selectedGroup)
end
coordsArea:SetText(CoordsText(keep))
end
stack:Gap(6)
-- Faehigkeiten durch Zaehmen (Startdaten)
if s.seed and s.seed.t and #s.seed.t > 0 then
stack:Heading(L2.BEAST_TEACHES)
for _, spellID in ipairs(s.seed.t) do
local name, rank = Beasts.SpellInfo(spellID)
stack:Row(SpellName(spellID), SpellRank(spellID) or "", "goldLight", function()
if name then
filter.ability = name
filter.rank, filter.rankMin = (rank and rank > 0) and rank or nil, false
if f.Refresh then f.Refresh() end
end
end)
end
stack:Gap(6)
end
-- Faehigkeiten des Begleiters, beim Zaehmen erfasst
local pa = (s.own and s.own.petAbilities) or (s.imp and s.imp.petAbilities)
if pa and #pa > 0 then
stack:Heading(L2.BEAST_PETABIL)
for _, a in ipairs(pa) do local a = a
local right = a.rank and a.rank > 0 and (FW.L.PET_RANK and string.format(FW.L.PET_RANK, a.rank) or ("Rang " .. a.rank)) or ""
if a.passive then
right = (right ~= "" and (right .. " | ") or "") .. L2.BEAST_PASSIVE
end
stack:Row(a.name, right, "goldLight", function()
if a.name then
filter.ability = a.name
filter.rank, filter.rankMin = (tonumber(a.rank) and tonumber(a.rank) > 0) and tonumber(a.rank) or nil, false
if f.Refresh then f.Refresh() end
end
end)
end
stack:Gap(6)
end
-- Hinweise aus Wildtierkunde
local extra = (s.own and s.own.extra) or (s.imp and s.imp.extra)
if extra and #extra > 0 then
stack:Heading(L2.BEAST_EXTRA)
for _, line in ipairs(extra) do
stack:Text(line, "text")
end
end
sectionScroll:SetContentHeight(stack:Height() + 4)
end
function ShowDetail(id)
local s = id and Beasts:Summary(id)
if not s then
detail:Hide()
hint:Show()
return
end
hint:Hide()
detail:Show()
nameLabel:SetText(s.name)
subLabel:SetText(Subtitle(s))
portraitIcon:SetTexture(FamilyIcon(s.fid))
portraitIcon:Show()
model:SetAlpha(0)
pcall(function()
model:ClearModel()
model:SetCreature(s.id)
if model.SetPortraitZoom then
model:SetPortraitZoom(1)
end
end)
local fileID = model.GetModelFileID and model:GetModelFileID()
if type(fileID) == "number" and fileID > 0 then
model:SetAlpha(1)
portraitIcon:Hide()
end
f.UpdateMapButton()
stateChip:SetText(StatusText(s.status), STATUS_COLOR[s.status])
if s.own then
sourceChip:SetText(FW.L.BEAST_TAG_OWN, "textDim")
sourceChip:Show()
elseif s.imp then
sourceChip:SetText(FW.L.BEAST_TAG_IMPORT, "textDim")
sourceChip:Show()
elseif s.seed then
sourceChip:SetText(FW.L.BEAST_TAG_START, "textDim")
sourceChip:Show()
else
sourceChip:Hide()
end
local mods = s.fid and Pet.GetFamilyModsByID(s.fid) or nil
for key, card in pairs(cards) do
local v = mods and mods[key]
if v then
card:SetValue(string.format("x%.2f", v), FactorColor(v))
else
card:SetValue(FW.L.BEAST_FACTOR_UNKNOWN, "textDim")
end
end
FillSections(s)
sectionScroll:ScrollToTop()
end
function f.UpdateMapButton()
local id = Beasts.selectedID
if not (Overlay and id) then
return
end
if Overlay.mode == "single" and Overlay.singleID == id then
mapButton:SetLabel(FW.L.BEAST_MAP_HIDE)
else
mapButton:SetLabel(FW.L.BEAST_MAP_SHOW)
end
end
function f.ShowFilteredOnMap()
local ids, count = {}, 0
for _, s in ipairs(Beasts:Filtered(filter)) do
ids[s.id] = true
count = count + 1
end
local wasMany = Overlay.mode == "many"
Overlay:ShowMany(ids, count)
if not wasMany then
Overlay:Focus(nil)
end
end
local function SelectBeast(id)
if Beasts.selectedID ~= id then
selectedGroup = nil
end
Beasts.selectedID = id
for _, row in ipairs(rows) do
row:SetSelected(row.id == id)
end
ShowDetail(id)
end
function f.Refresh()
local L2 = FW.L
local list, missing = Beasts:Filtered(filter)
local total = #Beasts:Index()
if filter.ability then
countLabel:SetText(string.format(L2.BEAST_COUNT_NOABIL, #list, total, missing or 0))
else
countLabel:SetText(string.format(L2.BEAST_COUNT, #list, total))
end
abilitySelect:SetLabel(filter.ability or L2.BEAST_ALL_ABILITIES)
local rankText = L2.BEAST_ALL_RANKS
if filter.rank then
rankText = string.format(filter.rankMin and L2.BEAST_RANK_MIN or L2.BEAST_RANK_EXACT, filter.rank)
end
rankSelect:SetLabel(rankText)
rankSelect:SetButtonEnabled(filter.ability ~= nil)
local famText, zoneText = L2.BEAST_ALL_FAMILIES, L2.BEAST_ALL_ZONES
if filter.fid then famText = Beasts:FamilyText(filter.fid) end
if filter.zone then zoneText = filter.zone end
familySelect:SetLabel(famText)
zoneSelect:SetLabel(zoneText)
for _, b in ipairs(statusButtons) do
local active = (b.def.key or nil) == filter.status
b:SetStyle(active and "primary" or "ghost")
end
seedToggle:Refresh()
if Beasts:HasSeed() then
seedToggle:Show()
else
seedToggle:Hide()
end
local selectedStillListed = false
for i, s in ipairs(list) do
local row = rows[i]
if not row then
row = CreateListRow(listScroll.child, SelectBeast)
rows[i] = row
end
row.id = s.id
row.name:SetText(s.name)
row.sub:SetText(Subtitle(s))
row.icon:SetTexture(FamilyIcon(s.fid))
row.icon:SetDesaturated(s.status == "unknown")
row.chip:SetText(StatusText(s.status), STATUS_COLOR[s.status])
row:ClearAllPoints()
row:SetPoint("TOPLEFT", listScroll.child, "TOPLEFT", 0, -(i - 1) * (ROW_H + ROW_GAP))
row:SetPoint("TOPRIGHT", listScroll.child, "TOPRIGHT", 0, -(i - 1) * (ROW_H + ROW_GAP))
row:SetSelected(s.id == Beasts.selectedID)
row:Show()
if s.id == Beasts.selectedID then
selectedStillListed = true
end
end
for i = #list + 1, #rows do
rows[i]:Hide()
end
listScroll:SetContentHeight(math.max(#list * (ROW_H + ROW_GAP), 130))
if #list == 0 then
emptyLabel:SetText((total == 0) and L2.BEAST_EMPTY_EXPLORE or L2.BEAST_EMPTY_FILTER)
emptyLabel:Show()
else
emptyLabel:Hide()
end
if not selectedStillListed then
Beasts.selectedID = list[1] and list[1].id or nil
selectedGroup = nil
for _, row in ipairs(rows) do
row:SetSelected(row.id == Beasts.selectedID and row:IsShown())
end
end
ShowDetail(Beasts.selectedID)
if Overlay and Overlay.mode == "many" then
f.ShowFilteredOnMap()
end
mapToggle:Refresh()
end
FW:On("BEASTS_MAP_CHANGED", function()
mapToggle:Refresh()
if f.UpdateMapButton then
f.UpdateMapButton()
end
end)
f:SetScript("OnHide", ClosePopup)
FW:On("BEASTS_UPDATED", function()
if f:IsVisible() then
if f.Refresh then f.Refresh() end
end
end)
FW:On("PET_UPDATED", function()
if f:IsVisible() then
if f.Refresh then f.Refresh() end
end
end)
if f.Refresh then f.Refresh() end
return f
end
UI:RegisterTab("beasts", {
label = function()
return FW.L.BEAST_TAB
end,
order = 50,
build = Build,
onShow = function(f)
if f.Refresh then f.Refresh() end
end,
})
SLASH_FREEWOODBEASTS1 = "/fwbeasts"
SlashCmdList["FREEWOODBEASTS"] = function(msg)
msg = string.lower(strtrim(msg or ""))
if msg == "map" or msg == "karte" then
if NS.MapOverlay then
NS.MapOverlay:Debug()
end
elseif string.sub(msg, 1, 7) == "mapsize" then
local n = tonumber(string.match(msg, "(%d+)"))
if n then
Beasts.db.settings.mapMinSize = math.max(8, math.min(128, n))
if NS.MapOverlay then
NS.MapOverlay:Refresh()
end
end
FW:Print(string.format(FW.L.BEAST_MAP_MINSIZE, Beasts.db.settings.mapMinSize or 32))
elseif msg == "maptest" then
if NS.MapOverlay then
NS.MapOverlay:Test()
end
else
UI:Toggle("beasts")
end
end