-- 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_Sauce = PizzaSauce -- Fade a model frame in when a new skin is displayed, unless disabled. local function MTH_MV_AnimateModel(model) if not MTH_MV_Sauce or not model then return end if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.model") then return end if type(model.SetAlpha) ~= "function" then return end if not model:IsShown() then return end model:SetAlpha(0) MTH_MV_Sauce:FadeTo(model, 1, 0.30, "outQuad") end 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) MTH_MV_AnimateModel(MTH_MV.previewModel) MTH_MV_AnimateModel(MTH_MV.popupModel) 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