mirror of
https://codeberg.org/Duvelcorp/MetaHunt.git
synced 2026-09-22 15:46:59 +00:00
405 lines
14 KiB
Lua
405 lines
14 KiB
Lua
-- hunterbook-model-viewer.lua
|
|
-- Embeds a small 3D creature skin preview at the bottom of the Beast Lore /
|
|
-- Abilities inspector. Clicking it opens a larger draggable popup with
|
|
-- < / > navigation through all skin variants of the selected family.
|
|
--
|
|
-- Uses MTH_DS_BeastSkins[familyName] generated by .tools/generators/gen_beast_skins.py
|
|
-- Skin rendering: PlayerModel:SetUnit(CreatureDisplayInfo.ID)
|
|
|
|
local MTH_MV = {
|
|
inited = false,
|
|
family = nil,
|
|
skins = {},
|
|
index = 1,
|
|
facing = 0.6,
|
|
rotating = false,
|
|
rotateLastX = 0,
|
|
keepPopup = false,
|
|
skinBeastRows = {},
|
|
skinListEmpty = nil,
|
|
-- small preview (parented to book frame, anchored below MTH_BOOK_DetailBackdrop)
|
|
previewFrame = nil,
|
|
previewModel = nil,
|
|
-- popup
|
|
popup = nil,
|
|
popupModel = nil,
|
|
popupTitle = nil,
|
|
popupSkinLabel = nil,
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Internal
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local function MTH_MV_ApplyModel(model, skinId)
|
|
if not model or not skinId then
|
|
return
|
|
end
|
|
model:SetUnit(skinId)
|
|
model:SetPosition(0, 0, 0)
|
|
model:SetModelScale(1)
|
|
local useFacing = (MTH_MV.popupModel ~= nil and model == MTH_MV.popupModel)
|
|
model:SetFacing(useFacing and MTH_MV.facing or 0.6)
|
|
end
|
|
|
|
local MTH_MV_BuildSkinBeastList -- forward declaration
|
|
|
|
local function MTH_MV_DisplayCurrent()
|
|
local skin = MTH_MV.skins[MTH_MV.index]
|
|
if not skin then return end
|
|
local n = table.getn(MTH_MV.skins)
|
|
|
|
MTH_MV_ApplyModel(MTH_MV.previewModel, skin.id)
|
|
MTH_MV_ApplyModel(MTH_MV.popupModel, skin.id)
|
|
|
|
local indexStr = tostring(MTH_MV.index) .. " / " .. tostring(n)
|
|
if MTH_MV.popupSkinLabel then
|
|
MTH_MV.popupSkinLabel:SetText(indexStr .. " · " .. tostring(skin.tex or ""))
|
|
end
|
|
MTH_MV_BuildSkinBeastList(skin.id)
|
|
end
|
|
|
|
MTH_MV_BuildSkinBeastList = function(skinId)
|
|
for i = 1, table.getn(MTH_MV.skinBeastRows) do
|
|
MTH_MV.skinBeastRows[i]:Hide()
|
|
end
|
|
if MTH_MV.skinListEmpty then MTH_MV.skinListEmpty:Hide() end
|
|
if not skinId or not MTH_DS_Beasts then return end
|
|
|
|
local matches = {}
|
|
for npcId, beast in pairs(MTH_DS_Beasts) do
|
|
if type(beast) == "table" and beast.skinId == skinId then
|
|
local nm = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(npcId, beast.name)) or beast.name
|
|
table.insert(matches, { id = npcId, name = nm or "?", lvl = tostring(beast.lvl or "?") })
|
|
end
|
|
end
|
|
table.sort(matches, function(a, b) return (a.name or "") < (b.name or "") end)
|
|
|
|
local maxRows = table.getn(MTH_MV.skinBeastRows)
|
|
local shown = 0
|
|
for i = 1, table.getn(matches) do
|
|
if shown >= maxRows then break end
|
|
shown = shown + 1
|
|
local row = MTH_MV.skinBeastRows[shown]
|
|
row._npcId = matches[i].id
|
|
row.nameText:SetText(matches[i].name)
|
|
row.lvlText:SetText(matches[i].lvl)
|
|
row:Show()
|
|
end
|
|
if shown == 0 and MTH_MV.skinListEmpty then
|
|
MTH_MV.skinListEmpty:Show()
|
|
end
|
|
end
|
|
|
|
local function MTH_MV_Init()
|
|
if MTH_MV.inited then return end
|
|
local backdrop = getglobal("MTH_BOOK_DetailBackdrop")
|
|
if not backdrop then
|
|
return
|
|
end
|
|
MTH_MV.inited = true
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- Model preview container (sibling of inspector, full column width)
|
|
-- Hardcoded offset from book frame TOPLEFT: x=608 (right column),
|
|
-- y = -(166 inspector_top + 178 inspector_h + 6 gap) = -350
|
|
-- ----------------------------------------------------------------
|
|
local bookFrame = backdrop:GetParent()
|
|
MTH_MV.previewFrame = CreateFrame("Frame", "MTH_BOOK_SkinPreview", bookFrame)
|
|
MTH_MV.previewFrame:SetWidth(136)
|
|
MTH_MV.previewFrame:SetHeight(136)
|
|
MTH_MV.previewFrame:SetPoint("TOPLEFT", bookFrame, "TOPLEFT", 608, -384)
|
|
MTH_MV.previewFrame:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 16, edgeSize = 12,
|
|
insets = {left = 3, right = 3, top = 3, bottom = 3},
|
|
})
|
|
MTH_MV.previewFrame:SetBackdropColor(0, 0, 0, 0.7)
|
|
MTH_MV.previewFrame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.8)
|
|
MTH_MV.previewFrame:Hide()
|
|
|
|
-- PlayerModel fills the inner area
|
|
MTH_MV.previewModel = CreateFrame("PlayerModel", nil, MTH_MV.previewFrame)
|
|
MTH_MV.previewModel:SetPoint("TOPLEFT", MTH_MV.previewFrame, "TOPLEFT", 3, -3)
|
|
MTH_MV.previewModel:SetPoint("BOTTOMRIGHT", MTH_MV.previewFrame, "BOTTOMRIGHT", -3, 3)
|
|
|
|
-- Invisible click overlay → opens popup
|
|
local previewBtn = CreateFrame("Button", nil, MTH_MV.previewFrame)
|
|
previewBtn:SetAllPoints(MTH_MV.previewFrame)
|
|
previewBtn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
|
|
previewBtn:SetScript("OnClick", function()
|
|
MTH_BOOK_ModelViewer_OpenPopup()
|
|
end)
|
|
previewBtn:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(MTH_MV.previewFrame, "ANCHOR_LEFT")
|
|
GameTooltip:AddLine("Click to open skin viewer", 1, 1, 1)
|
|
GameTooltip:Show()
|
|
end)
|
|
previewBtn:SetScript("OnLeave", function()
|
|
GameTooltip:Hide()
|
|
end)
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- Popup frame (wider: left side = model viewer, right = beast list)
|
|
-- ----------------------------------------------------------------
|
|
local popup = CreateFrame("Frame", "MTH_BOOK_ModelPopup", UIParent)
|
|
popup:SetWidth(490)
|
|
popup:SetHeight(320)
|
|
popup:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
|
popup:SetFrameStrata("TOOLTIP")
|
|
popup:SetToplevel(true)
|
|
popup:SetMovable(true)
|
|
popup:EnableMouse(true)
|
|
popup:RegisterForDrag("LeftButton")
|
|
popup:SetScript("OnDragStart", function() this:StartMoving() end)
|
|
popup:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
|
popup:SetScript("OnHide", function() MTH_MV.rotating = false end)
|
|
popup:Hide()
|
|
popup:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
|
tile = true, tileSize = 32, edgeSize = 32,
|
|
insets = {left = 11, right = 12, top = 12, bottom = 11},
|
|
})
|
|
popup:SetBackdropColor(0.05, 0.05, 0.08, 0.95)
|
|
|
|
-- Large model background (left side, x=20)
|
|
local modelBg = CreateFrame("Frame", nil, popup)
|
|
modelBg:SetWidth(224)
|
|
modelBg:SetHeight(226)
|
|
modelBg:SetPoint("TOPLEFT", popup, "TOPLEFT", 20, -42)
|
|
modelBg:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 16, edgeSize = 12,
|
|
insets = {left = 3, right = 3, top = 3, bottom = 3},
|
|
})
|
|
modelBg:SetBackdropColor(0, 0, 0, 0.85)
|
|
modelBg:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.9)
|
|
|
|
MTH_MV.popupModel = CreateFrame("PlayerModel", "MTH_BOOK_ModelPopupModel", modelBg)
|
|
MTH_MV.popupModel:SetPoint("TOPLEFT", modelBg, "TOPLEFT", 3, -3)
|
|
MTH_MV.popupModel:SetPoint("BOTTOMRIGHT", modelBg, "BOTTOMRIGHT", -3, 3)
|
|
|
|
-- Mouse rotation: drag on the black model area rotates; outside drags the popup
|
|
MTH_MV.popupModel:EnableMouse(true)
|
|
MTH_MV.popupModel:SetScript("OnMouseDown", function()
|
|
MTH_MV.rotating = true
|
|
MTH_MV.rotateLastX = GetCursorPosition()
|
|
end)
|
|
MTH_MV.popupModel:SetScript("OnMouseUp", function()
|
|
MTH_MV.rotating = false
|
|
end)
|
|
MTH_MV.popupModel:SetScript("OnUpdate", function()
|
|
if not MTH_MV.rotating then return end
|
|
local cx = GetCursorPosition()
|
|
local dx = cx - MTH_MV.rotateLastX
|
|
MTH_MV.rotateLastX = cx
|
|
if dx ~= 0 then
|
|
MTH_MV.facing = MTH_MV.facing - dx * 0.03
|
|
MTH_MV.popupModel:SetFacing(MTH_MV.facing)
|
|
end
|
|
end)
|
|
|
|
-- Title (family name) anchored above the model
|
|
MTH_MV.popupTitle = popup:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
MTH_MV.popupTitle:SetPoint("TOP", modelBg, "TOP", 0, 18)
|
|
MTH_MV.popupTitle:SetTextColor(1, 0.82, 0)
|
|
MTH_MV.popupTitle:SetText("Skin Viewer")
|
|
|
|
-- Close [X] button
|
|
local closeBtn = CreateFrame("Button", nil, popup)
|
|
closeBtn:SetWidth(22)
|
|
closeBtn:SetHeight(22)
|
|
closeBtn:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -10, -10)
|
|
closeBtn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
|
|
local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
closeLbl:SetAllPoints(closeBtn)
|
|
closeLbl:SetText("X")
|
|
closeLbl:SetTextColor(0.9, 0.9, 0.9)
|
|
closeBtn:SetScript("OnClick", function() this:GetParent():Hide() end)
|
|
|
|
-- "Unique Appearance" badge removed (inline texture syntax unsupported in this client)
|
|
|
|
-- Nav: ◀ label ▶ (kept within the left 264px area)
|
|
local prevBtn = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
|
|
prevBtn:SetWidth(28)
|
|
prevBtn:SetHeight(20)
|
|
prevBtn:SetPoint("BOTTOMLEFT", popup, "BOTTOMLEFT", 14, 14)
|
|
prevBtn:SetText("<")
|
|
prevBtn:SetScript("OnClick", function()
|
|
local n = table.getn(MTH_MV.skins)
|
|
if n < 1 then return end
|
|
MTH_MV.index = MTH_MV.index - 1
|
|
if MTH_MV.index < 1 then MTH_MV.index = n end
|
|
MTH_MV_DisplayCurrent()
|
|
end)
|
|
|
|
local nextBtn = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
|
|
nextBtn:SetWidth(28)
|
|
nextBtn:SetHeight(20)
|
|
nextBtn:SetPoint("BOTTOMLEFT", popup, "BOTTOMLEFT", 222, 14)
|
|
nextBtn:SetText(">")
|
|
nextBtn:SetScript("OnClick", function()
|
|
local n = table.getn(MTH_MV.skins)
|
|
if n < 1 then return end
|
|
MTH_MV.index = MTH_MV.index + 1
|
|
if MTH_MV.index > n then MTH_MV.index = 1 end
|
|
MTH_MV_DisplayCurrent()
|
|
end)
|
|
|
|
MTH_MV.popupSkinLabel = popup:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
MTH_MV.popupSkinLabel:SetPoint("BOTTOMLEFT", prevBtn, "BOTTOMRIGHT", 4, 0)
|
|
MTH_MV.popupSkinLabel:SetPoint("BOTTOMRIGHT", nextBtn, "BOTTOMLEFT", -4, 0)
|
|
MTH_MV.popupSkinLabel:SetJustifyH("CENTER")
|
|
MTH_MV.popupSkinLabel:SetTextColor(0.9, 0.9, 0.9)
|
|
|
|
-- ---------------------------------------------------------------
|
|
-- Right panel: vertical divider + beast list
|
|
-- ---------------------------------------------------------------
|
|
local divider = popup:CreateTexture(nil, "ARTWORK")
|
|
divider:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
divider:SetVertexColor(0.35, 0.35, 0.35, 0.8)
|
|
divider:SetPoint("TOPLEFT", popup, "TOPLEFT", 270, -36)
|
|
divider:SetPoint("BOTTOMLEFT", popup, "BOTTOMLEFT", 270, 12)
|
|
divider:SetWidth(1)
|
|
|
|
local listHeader = popup:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
listHeader:SetPoint("TOPLEFT", popup, "TOPLEFT", 278, -18)
|
|
listHeader:SetTextColor(1, 0.82, 0)
|
|
listHeader:SetText("Beasts with this skin")
|
|
|
|
local listSep = popup:CreateTexture(nil, "ARTWORK")
|
|
listSep:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
listSep:SetVertexColor(0.35, 0.35, 0.35, 0.6)
|
|
listSep:SetPoint("TOPLEFT", popup, "TOPLEFT", 278, -30)
|
|
listSep:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -10, -30)
|
|
listSep:SetHeight(1)
|
|
|
|
MTH_MV.skinListEmpty = popup:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
MTH_MV.skinListEmpty:SetPoint("TOPLEFT", popup, "TOPLEFT", 278, -38)
|
|
MTH_MV.skinListEmpty:SetTextColor(0.5, 0.5, 0.5)
|
|
MTH_MV.skinListEmpty:SetText("\226\128\148")
|
|
MTH_MV.skinListEmpty:Hide()
|
|
|
|
-- Beast rows (14 max)
|
|
local ROW_X = 278
|
|
local ROW_START = -36
|
|
local ROW_H = 17
|
|
local MAX_ROWS = 14
|
|
for i = 1, MAX_ROWS do
|
|
local row = CreateFrame("Button", nil, popup)
|
|
row:SetPoint("TOPLEFT", popup, "TOPLEFT", ROW_X, ROW_START - (i - 1) * ROW_H)
|
|
row:SetWidth(200)
|
|
row:SetHeight(ROW_H - 1)
|
|
row._npcId = nil
|
|
|
|
local hl = row:CreateTexture(nil, "HIGHLIGHT")
|
|
hl:SetAllPoints(row)
|
|
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
|
hl:SetBlendMode("ADD")
|
|
hl:SetAlpha(0.35)
|
|
|
|
row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
row.nameText:SetPoint("LEFT", row, "LEFT", 0, 0)
|
|
row.nameText:SetWidth(158)
|
|
row.nameText:SetJustifyH("LEFT")
|
|
|
|
row.lvlText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
row.lvlText:SetPoint("LEFT", row, "LEFT", 160, 0)
|
|
row.lvlText:SetWidth(38)
|
|
row.lvlText:SetJustifyH("LEFT")
|
|
row.lvlText:SetTextColor(0.7, 0.7, 0.7)
|
|
|
|
row:SetScript("OnClick", function()
|
|
if this._npcId and type(MTH_BOOK_JumpToBeastById) == "function" then
|
|
MTH_MV.keepPopup = true
|
|
MTH_BOOK_JumpToBeastById(this._npcId)
|
|
MTH_MV.keepPopup = false
|
|
end
|
|
end)
|
|
row:Hide()
|
|
MTH_MV.skinBeastRows[i] = row
|
|
end
|
|
|
|
MTH_MV.popup = popup
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Public API
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
function MTH_BOOK_ModelViewer_SetFamily(family, skinId)
|
|
MTH_MV_Init()
|
|
if not MTH_MV.inited then return end
|
|
local skins = MTH_DS_BeastSkins and type(MTH_DS_BeastSkins) == "table" and MTH_DS_BeastSkins[family]
|
|
if not skins or table.getn(skins) == 0 then
|
|
MTH_BOOK_ModelViewer_Clear()
|
|
return
|
|
end
|
|
|
|
-- Filter to only skins used by at least one tameable beast
|
|
local usedSkinIds = {}
|
|
if MTH_DS_Beasts then
|
|
for _, beast in pairs(MTH_DS_Beasts) do
|
|
if type(beast) == "table" and beast.skinId then
|
|
usedSkinIds[beast.skinId] = true
|
|
end
|
|
end
|
|
end
|
|
local filteredSkins = {}
|
|
for i = 1, table.getn(skins) do
|
|
if usedSkinIds[skins[i].id] then
|
|
table.insert(filteredSkins, skins[i])
|
|
end
|
|
end
|
|
if table.getn(filteredSkins) == 0 then
|
|
MTH_BOOK_ModelViewer_Clear()
|
|
return
|
|
end
|
|
|
|
MTH_MV.family = family
|
|
MTH_MV.skins = filteredSkins
|
|
MTH_MV.facing = 0.6
|
|
|
|
-- If we have a specific skinId, jump to the matching skin entry.
|
|
MTH_MV.index = 1
|
|
if skinId then
|
|
for i = 1, table.getn(filteredSkins) do
|
|
if filteredSkins[i].id == skinId then
|
|
MTH_MV.index = i
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if MTH_MV.popupTitle then
|
|
MTH_MV.popupTitle:SetText(tostring(family) .. " — Skins")
|
|
end
|
|
|
|
MTH_MV_DisplayCurrent()
|
|
|
|
if MTH_MV.previewFrame then MTH_MV.previewFrame:Show() end
|
|
MTH_MV_DisplayCurrent()
|
|
end
|
|
|
|
function MTH_BOOK_ModelViewer_Clear()
|
|
if MTH_MV.previewFrame then MTH_MV.previewFrame:Hide() end
|
|
if not MTH_MV.keepPopup then
|
|
if MTH_MV.popup and MTH_MV.popup:IsShown() then MTH_MV.popup:Hide() end
|
|
end
|
|
MTH_MV.family = nil
|
|
MTH_MV.skins = {}
|
|
MTH_MV.index = 1
|
|
end
|
|
|
|
function MTH_BOOK_ModelViewer_OpenPopup()
|
|
MTH_MV_Init()
|
|
if not MTH_MV.popup then return end
|
|
if table.getn(MTH_MV.skins) == 0 then return end
|
|
MTH_MV.popup:Show()
|
|
MTH_MV.popup:Raise()
|
|
MTH_MV_DisplayCurrent()
|
|
end
|