Files
bdot89andClaude Opus 5.5 262333a852 2.23.0: weapon glows are shared, setup pop-up at login
- other DMorph users (2.23+) see your weapon glows: a g token in look
  messages and G messages; receivers put glows on with DmorphHelper and
  keep them on, skipping glows their game doesn't have
- a pop-up a few seconds after login or /reload when VanillaHelpers isn't
  running or DmorphHelper is missing or older (Don't remind me until the
  next helper version)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:20:18 +01:00

4498 lines
150 KiB
Lua

-- DMorph for OctoWoW: the morph window.
--
-- Left: your character in a dressing room (your race's dress-up backdrop),
-- with the morphable slots around it. Right: tabs to browse every creature
-- model (a grid of 3D previews, or a list), skins for your mount, favourites, races and NPC looks,
-- weapon glows, items per slot, item sets and a wardrobe of saved outfits. Everything goes
-- through the DmorphOcto.* functions in DmorphOcto.lua, the same ones the chat
-- commands use.
--
-- No white flashes: a model frame draws its model white until the textures
-- have loaded. So every model here loads out of sight first, below an opaque
-- layer, and is raised above it once it has had time to finish. The character
-- preview keeps two PlayerModels for this and swaps them, and never reloads
-- one that is still loading.
--
-- 1.12 API: scripts read `this`/`arg1` instead of taking arguments, there is
-- no SetSize, lists use FauxScrollFrameTemplate, and regions that share a draw
-- layer have no fixed order, so whatever must stack sits on child frames with
-- explicit frame levels. 3D previews by display ID need ClassicAPI's
-- Model:SetDisplayInfo; without it the Models tab shows a list.
local M = DmorphOcto
local W, H = 922, 534
local ART_W, ART_H, PLATE_H = 318, 383, 44
local PAGE_X, PAGE_Y, PAGE_W, PAGE_H = 352, -74, 550, 382
local ROW_HEIGHT, LIST_ROWS, LIST_WIDTH = 18, 15, 510
-- Every list tab (2.21.0, user: "all tabs with lists should have this same
-- layout"): the list on the left, a preview box on the right.
local BOX_W, LIST_W = 178, 330
local TILE_W, TILE_H, TILE_GAP, GRID_COLS = 128, 132, 6, 4
-- ClassicAPI dresses at most 8 NPC preview models at a time, so 8 tiles.
local PAGE_SIZE = 8
-- How long a model loads out of sight before it's shown.
local REVEAL = 0.45
-- The least time between two model changes in one preview (see LoadView).
local SETTLE = 0.6
-- After a morph the game rebuilds your character; a second change before it
-- has finished can mix the two looks' textures. Clicks closer than this wait.
local GATE = 0.35
local MIN_SCALE, MAX_SCALE = 0.6, 1.6
-- Zooming the character: how far it comes towards you (model units, about a
-- yard each), per mouse wheel notch and at most.
local ZOOM_STEP, ZOOM_MIN, ZOOM_MAX = 0.3, -1.5, 3
local MENU_ROWS, MENU_WIDTH = 20, 170
local WHITE = "Interface\\Buttons\\WHITE8X8"
local RACE_ICONS_FILE = "Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Races"
-- GetInventorySlotInfo() names, for the empty-slot icons.
local SLOT_KEYS = {
[1] = "HeadSlot", [3] = "ShoulderSlot", [4] = "ShirtSlot", [5] = "ChestSlot",
[6] = "WaistSlot", [7] = "LegsSlot", [8] = "FeetSlot", [9] = "WristSlot",
[10] = "HandsSlot", [15] = "BackSlot", [16] = "MainHandSlot",
[17] = "SecondaryHandSlot", [18] = "RangedSlot", [19] = "TabardSlot",
}
-- Paper-doll layout: slots down the left and right of the model.
local LEFT_SLOTS = { 1, 3, 15, 5, 4, 19, 9 }
local RIGHT_SLOTS = { 10, 6, 7, 8, 16, 17, 18 }
-- Per race ID (DmorphOcto.RACES): the dress-up backdrop, and the male icon's
-- cell { left, right, top } in the character-create race icons, from the
-- client's CharacterCreate.lua. The female icon sits half the sheet lower.
local RACE_ART = { "Human", "Orc", "Dwarf", "NightElf", "Scourge", "Tauren", "Gnome", "Troll", "Goblin", "BloodElf" }
local RACE_ICONS = {
{ 0, 0.125, 0 }, { 0.375, 0.5, 0.25 }, { 0.125, 0.25, 0 }, { 0.375, 0.5, 0 }, { 0.125, 0.25, 0.25 },
{ 0, 0.125, 0.25 }, { 0.25, 0.375, 0 }, { 0.25, 0.375, 0.25 }, { 0.5, 0.625, 0 }, { 0.5, 0.625, 0.25 },
}
-- UnitRace() tokens whose dress-up backdrop has another name.
local ART_TOKENS = { HighElf = "BloodElf" }
-- Creature model entries by kind (DmorphOcto.KIND_*). Items and sets use 0 and 2.
local KIND_COLORS = { [0] = { 1, 1, 1 }, [1] = { 0.62, 0.76, 0.92 }, [2] = { 0.45, 1, 0.6 }, [3] = { 0.45, 1, 0.6 }, [4] = { 1, 0.82, 0.3 } }
local KIND_NOTES = {
[0] = "A creature from the world database.",
[1] = "No creature has this look, so it's named after its model file.",
[2] = "New in OctoWoW: a creature from Turtle WoW's database.",
[3] = "New in OctoWoW, named after its model file.",
[4] = "Ported from a later expansion: only on this PC (patch-Z.mpq), others see your own look.",
}
local TABS = {
{ key = "models", text = "Models" },
{ key = "mounts", text = "Mounts" },
{ key = "favourites", text = "Favourites" },
{ key = "races", text = "Races" },
{ key = "glows", text = "Glows" },
{ key = "items", text = "Items" },
{ key = "slutmog", text = "Slutmog" },
{ key = "sets", text = "Sets" },
{ key = "wardrobe", text = "Wardrobe" },
{ key = "weather", text = "Weather" },
}
local TAB_GAP = 3
-- The gold star of the raid target icons marks favourites.
local STAR = "Interface\\TargetingFrame\\UI-RaidTargetingIcons"
local FAV_FILTERS = {
{ text = "All favourites" },
{ text = "Models", mount = false },
{ text = "Mounts", mount = true },
}
local frame -- the window, built the first time it opens
local ui = {
tabs = {}, pages = {}, slots = {}, raceButtons = {}, raceCells = {}, tiles = {}, page = 1,
creator = {}, creatorRows = {}, mountRows = {}, glowSlots = {}, iconAsked = {}, boxes = {},
}
-- Helpers of the preview boxes and the list tabs (one table: Lua 5.0 allows
-- 200 locals in a chunk and 32 upvalues in a function).
local P = {}
local currentTab = "models"
local itemSlot = 1 -- the slot the Items tab is browsing
local selectedOutfit
local pendingDelete
local buttonCount = 0
local function SetStatus(ok, message)
if not frame then
return
end
ui.status:SetText(message or "")
if ok then
ui.status:SetTextColor(0.6, 1, 0.6)
else
ui.status:SetTextColor(1, 0.4, 0.4)
end
end
-- 16296 -> "16,296"
local function Commas(n)
local text, out = tostring(n), ""
while strlen(text) > 3 do
out = "," .. strsub(text, -3) .. out
text = strsub(text, 1, -4)
end
return text .. out
end
-- 1 -> "1 item", 1234 -> "1,234 items"
local function Count(n, word)
if n == 1 then
return "1 " .. word
end
return Commas(n) .. " " .. word .. "s"
end
-- Steps through n positions, wrapping round.
local function Wrap(pos, step, n)
return math.mod(pos - 1 + step + n * 4, n) + 1
end
-- Window settings shared by all characters: views, filters, sorting, size.
local function Prefs()
local account = M.GetAccount()
if type(account.ui) ~= "table" then
account.ui = {}
end
local p = account.ui
if not M.MODEL_FILTERS[p.filter] then
p.filter = 1
end
if not M.ITEM_FILTERS[p.itemFilter] then
p.itemFilter = 1
end
if not M.SET_FILTERS[p.setFilter] then
p.setFilter = 1
end
if not M.QUALITY_FILTERS[p.itemQuality] then
p.itemQuality = 1
end
if not M.QUALITY_FILTERS[p.setQuality] then
p.setQuality = 1
end
-- The item kind per slot (cloth, dagger, ...), and the sets' armour.
if type(p.itemTypes) ~= "table" then
p.itemTypes = {}
end
for slot, kind in pairs(p.itemTypes) do
if not (M.SLOT_NAMES[slot] and M.ITEM_TYPE_NAMES[kind]) then
p.itemTypes[slot] = nil
end
end
if not M.ITEM_TYPE_NAMES[p.setType or 0] then
p.setType = nil
end
if not FAV_FILTERS[p.favFilter] then
p.favFilter = 1
end
if type(p.scale) ~= "number" or p.scale < MIN_SCALE or p.scale > MAX_SCALE then
p.scale = 1
end
return p
end
-- The look -------------------------------------------------------------------
-- 2.21.0, user: "neat like OctoBG and ShootyEPGP, but more vibrant". Flat dark
-- panels with 1 px borders (OctoBG's flat style, seen working in game),
-- DMorph's purple (the project page's #b46bff) for what's picked and hovered,
-- gold for what you have on, and Roboto once the client says it loaded.
local C = {
bg = { 0.05, 0.05, 0.06 }, panel = { 0.075, 0.075, 0.09 }, card = { 0.1, 0.095, 0.125 },
field = { 0.045, 0.045, 0.055 }, btn = { 0.15, 0.13, 0.2 }, line = { 0.25, 0.22, 0.32 },
lineHi = { 0.5, 0.38, 0.7 }, text = { 0.94, 0.93, 0.96 }, dim = { 0.62, 0.6, 0.68 },
accent = { 0.71, 0.42, 1 }, deep = { 0.29, 0.15, 0.5 }, gold = { 1, 0.82, 0.3 },
}
local FONTS = { friz = "Fonts\\FRIZQT__.TTF", roboto = "Interface\\AddOns\\DmorphOcto\\media\\fonts\\Roboto.ttf" }
-- A failed SetFont keeps the old font, so GetFont tells whether Roboto loaded.
local function UIFont()
if not FONTS.ui then
FONTS.ui = FONTS.friz
local probe = UIParent:CreateFontString(nil, "OVERLAY")
probe:SetFont(FONTS.roboto, 12, "")
local now = probe:GetFont()
if type(now) == "string" and strlower(string.gsub(now, "/", "\\")) == strlower(FONTS.roboto) then
FONTS.ui = FONTS.roboto
end
end
return FONTS.ui
end
local function Font(fs, size, col)
fs:SetFont(UIFont(), size or 11, "")
fs:SetShadowColor(0, 0, 0, 0.85)
fs:SetShadowOffset(1, -1)
col = col or C.text
fs:SetTextColor(col[1], col[2], col[3])
return fs
end
-- A frame drawn flat: a colour inside a 1 px line.
local function Flat(f, col, line, alpha)
f:SetBackdrop({ bgFile = WHITE, edgeFile = WHITE, tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 } })
col, line = col or C.panel, line or C.line
f:SetBackdropColor(col[1], col[2], col[3], alpha or 1)
f:SetBackdropBorderColor(line[1], line[2], line[3], 1)
end
-- A purple wash that shows on hover and while locked (LockHighlight).
local function Hover(f, alpha)
local hl = f:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture(WHITE)
hl:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], alpha or 0.22)
hl:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
hl:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
return hl
end
-- Widget helpers --------------------------------------------------------------
-- The template names are only for the size and colour they imply.
local LABEL_LOOK = {
GameFontNormalLarge = { 15, C.accent }, GameFontNormal = { 12, C.gold },
GameFontHighlight = { 12, C.text }, GameFontHighlightSmall = { 11, C.text },
GameFontNormalSmall = { 11, C.gold }, GameFontDisableSmall = { 10, C.dim },
GameFontRedSmall = { 11, { 1, 0.42, 0.42 } },
}
local function Label(parent, text, font, x, y, name)
local label = parent:CreateFontString(name, "OVERLAY", font or "GameFontNormalSmall")
local look = LABEL_LOOK[font or "GameFontNormalSmall"]
if look then
Font(label, look[1], look[2])
end
label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
label:SetJustifyH("LEFT")
label:SetText(text)
return label
end
-- A flat button; b:SetText works (its own font string), LockHighlight keeps
-- the purple wash on (the picked one of a row of buttons).
local function FlatButton(button, text, size)
Flat(button, C.btn, C.line)
local label = button:CreateFontString(nil, "OVERLAY")
Font(label, size or 11)
label:SetPoint("LEFT", button, "LEFT", 3, 0)
label:SetPoint("RIGHT", button, "RIGHT", -3, 0)
button:SetFontString(label)
button.label = label
Hover(button)
button:SetScript("OnMouseDown", function()
this:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 1)
end)
button:SetScript("OnMouseUp", function()
this:SetBackdropBorderColor(C.line[1], C.line[2], C.line[3], 1)
end)
button:SetText(text)
return button
end
local function Button(parent, text, width, x, y, onClick, name)
buttonCount = buttonCount + 1
local button = CreateFrame("Button", name or ("DmorphOctoButton" .. buttonCount), parent)
button:SetWidth(width)
button:SetHeight(22)
button:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
FlatButton(button, text)
button:SetScript("OnClick", onClick)
return button
end
-- Greys a flat button out (it still shows its tooltip).
local function SetEnabled(button, on)
if on then
button:Enable()
button.label:SetTextColor(C.text[1], C.text[2], C.text[3])
else
button:Disable()
button.label:SetTextColor(0.4, 0.38, 0.45)
end
end
local function EditBox(parent, name, width, x, y, maxLetters)
local box = CreateFrame("EditBox", name, parent)
box:SetWidth(width)
box:SetHeight(20)
box:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
Flat(box, C.field, C.line)
box:SetTextInsets(6, 6, 0, 0)
box:SetFont(UIFont(), 11, "")
box:SetTextColor(C.text[1], C.text[2], C.text[3])
box:EnableMouse(true)
box:SetAutoFocus(false)
box:SetMaxLetters(maxLetters or 40)
box:SetScript("OnEscapePressed", function()
this:ClearFocus()
end)
box:SetScript("OnEditFocusGained", function()
this:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 1)
end)
box:SetScript("OnEditFocusLost", function()
this:SetBackdropBorderColor(C.line[1], C.line[2], C.line[3], 1)
end)
return box
end
-- A flat panel in a colour (r, g, b), else the card colour.
local function Panel(f, r, g, b)
Flat(f, r and { r, g, b } or C.card, C.line)
end
-- A FauxScrollFrame's bar made flat: no arrow buttons, a thin track to the
-- right of the list and a purple thumb (OctoBG's FlatScroll). The mouse
-- wheel still scrolls.
local function FlatScroll(scroll)
local name = scroll:GetName()
local bar = getglobal(name .. "ScrollBar")
getglobal(name .. "ScrollBarScrollUpButton"):Hide()
getglobal(name .. "ScrollBarScrollDownButton"):Hide()
bar:ClearAllPoints()
bar:SetPoint("TOPLEFT", scroll, "TOPRIGHT", 6, 0)
bar:SetPoint("BOTTOMLEFT", scroll, "BOTTOMRIGHT", 6, 0)
bar:SetWidth(6)
local track = bar:CreateTexture(nil, "BACKGROUND")
track:SetTexture(WHITE)
track:SetVertexColor(C.field[1], C.field[2], C.field[3], 1)
track:SetAllPoints(bar)
bar:SetThumbTexture(WHITE)
local thumb = bar:GetThumbTexture()
if thumb then
thumb:SetWidth(6)
thumb:SetHeight(36)
thumb:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.85)
end
end
-- A texture with a vertical gradient, bottom colour first.
local function Gradient(parent, layer, r1, g1, b1, a1, r2, g2, b2, a2)
local texture = parent:CreateTexture(nil, layer)
texture:SetTexture(WHITE)
texture:SetGradientAlpha("VERTICAL", r1, g1, b1, a1, r2, g2, b2, a2)
return texture
end
-- A child frame at a fixed level above its parent (see the header).
local function Layer(parent, level)
local f = CreateFrame("Frame", nil, parent)
f:SetFrameLevel(parent:GetFrameLevel() + level)
return f
end
local function Tooltip(owner, title, line1, line2, line3)
GameTooltip:SetOwner(owner, "ANCHOR_TOP")
GameTooltip:SetText(title)
if line1 then
GameTooltip:AddLine(line1, 1, 1, 1)
end
if line2 then
GameTooltip:AddLine(line2, 0.8, 0.8, 0.8)
end
if line3 then
GameTooltip:AddLine(line3, 0.2, 1, 0.6)
end
GameTooltip:Show()
end
local function HideTooltip()
GameTooltip:Hide()
end
-- Runs fn once typing has paused, so long lists aren't filtered per key.
local function Debounce(timer, delay, fn)
timer.wait = delay
timer.callback = fn
timer:Show()
end
local function NewTimer()
local timer = CreateFrame("Frame", nil, frame)
timer:Hide()
timer:SetScript("OnUpdate", function()
this.wait = this.wait - arg1
if this.wait <= 0 then
this:Hide()
this.callback()
end
end)
return timer
end
-- Every morph from the window goes through here. The game rebuilds your
-- character after each change, and a change that lands while it is still
-- rebuilding can leave the earlier look's textures on it. So a click within
-- GATE seconds of the last one waits, and only the latest waiting one runs.
local function Morph(fn, a1, a2)
if ui.gateBusy then
ui.gateNext = { fn, a1, a2 }
return
end
ui.gateBusy = true
SetStatus(fn(a1, a2))
Debounce(ui.gateTimer, GATE, function()
ui.gateBusy = nil
local nextCall = ui.gateNext
ui.gateNext = nil
if nextCall then
Morph(nextCall[1], nextCall[2], nextCall[3])
end
end)
end
-- A drop-down menu shared by every filter button. options: a list of
-- { text = ..., checked = true/nil, header = true/nil }; pick(option) runs when
-- one is clicked. Clicking the same button again closes it.
local function OpenMenu(anchor, options, pick, above)
local menu = ui.menu
if menu:IsShown() and menu.anchor == anchor then
menu:Hide()
return
end
menu.anchor, menu.pick = anchor, pick
local count = math.min(table.getn(options), MENU_ROWS)
for i = 1, MENU_ROWS do
local row, option = menu.rows[i], options[i]
if option then
row.option = option
row.label:SetText(option.text)
if option.header then
row.label:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
row:Disable()
else
row.label:SetTextColor(C.text[1], C.text[2], C.text[3])
row:Enable()
end
if option.checked then
row.check:Show()
else
row.check:Hide()
end
row:Show()
else
row:Hide()
end
end
menu:SetHeight(count * 18 + 12)
menu:ClearAllPoints()
if above then
menu:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, 2)
else
menu:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2)
end
menu:Show()
end
local function BuildMenu()
local menu = CreateFrame("Frame", "DmorphOctoMenu", frame)
menu:Hide()
menu:SetFrameLevel(frame:GetFrameLevel() + 40)
menu:SetWidth(MENU_WIDTH)
menu:EnableMouse(true)
Flat(menu, C.panel, C.accent)
menu.rows = {}
for i = 1, MENU_ROWS do
local row = CreateFrame("Button", "DmorphOctoMenuRow" .. i, menu)
row:SetFrameLevel(menu:GetFrameLevel() + 1)
row:SetWidth(MENU_WIDTH - 12)
row:SetHeight(18)
row:SetPoint("TOPLEFT", menu, "TOPLEFT", 6, -6 - (i - 1) * 18)
Hover(row, 0.3)
row.check = row:CreateTexture(nil, "ARTWORK")
row.check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
row.check:SetWidth(16)
row.check:SetHeight(16)
row.check:SetPoint("LEFT", row, "LEFT", 0, 0)
row.label = Font(row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"), 11)
row.label:SetPoint("LEFT", row, "LEFT", 18, 0)
row:SetScript("OnClick", function()
ui.menu:Hide()
ui.menu.pick(this.option)
end)
menu.rows[i] = row
end
return menu
end
-- A scrolling list of LIST_ROWS rows over a FauxScrollFrame. fill(row, index)
-- draws entry `index` (row.text, row.info), click(index, mouseButton) handles
-- a click, enter(index, row) a hover. infoWidth is the right-hand column.
local function List(parent, name, x, y, infoWidth, fill, click, enter, width, rows)
local list = { rows = {}, count = 0 }
local LIST_ROWS = rows or LIST_ROWS
width = width or LIST_WIDTH
local scroll = CreateFrame("ScrollFrame", name, parent, "FauxScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
scroll:SetWidth(width)
scroll:SetHeight(LIST_ROWS * ROW_HEIGHT)
FlatScroll(scroll)
-- The list sits on a dark well, a line round it.
local well = CreateFrame("Frame", nil, parent)
well:SetPoint("TOPLEFT", scroll, "TOPLEFT", -1, 1)
well:SetPoint("BOTTOMRIGHT", scroll, "BOTTOMRIGHT", 1, -1)
well:SetFrameLevel(scroll:GetFrameLevel())
Flat(well, C.field, C.line)
function list.Refresh()
local offset = FauxScrollFrame_GetOffset(scroll) or 0
for i = 1, LIST_ROWS do
local row = list.rows[i]
local index = offset + i
if index <= list.count then
row.index = index
row:UnlockHighlight()
row.star:Hide()
row.tick:Hide()
row.text:SetTextColor(C.text[1], C.text[2], C.text[3])
fill(row, index)
row:Show()
else
row:Hide()
end
end
FauxScrollFrame_Update(scroll, list.count, LIST_ROWS, ROW_HEIGHT)
end
-- Scrolls so that entry `index` is the top row.
function list.ScrollTo(index)
local offset = index - 1
if offset > list.count - LIST_ROWS then
offset = list.count - LIST_ROWS
end
if offset < 0 then
offset = 0
end
FauxScrollFrame_SetOffset(scroll, offset)
getglobal(name .. "ScrollBar"):SetValue(offset * ROW_HEIGHT)
list.Refresh()
end
-- Scrolls entry `index` into view (about the middle) if it isn't.
function list.Show(index)
local top = list.GetOffset() + 1
if index < top or index > top + LIST_ROWS - 1 then
list.ScrollTo(index - math.floor(LIST_ROWS / 2))
else
list.Refresh()
end
end
-- New contents: back to the top, unless keep is set.
function list.SetCount(count, keep)
list.count = count
if keep then
list.Refresh()
else
list.ScrollTo(1)
end
end
function list.GetOffset()
return FauxScrollFrame_GetOffset(scroll) or 0
end
scroll:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, list.Refresh)
end)
for i = 1, LIST_ROWS do
local row = CreateFrame("Button", name .. "Row" .. i, parent)
-- Above the scroll frame, so clicks reach the rows.
row:SetFrameLevel(scroll:GetFrameLevel() + 1)
row:SetWidth(width)
row:SetHeight(ROW_HEIGHT)
row:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - (i - 1) * ROW_HEIGHT)
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
if math.mod(i, 2) == 0 then
local stripe = row:CreateTexture(nil, "BACKGROUND")
stripe:SetTexture(WHITE)
stripe:SetVertexColor(1, 1, 1, 0.03)
stripe:SetAllPoints(row)
end
Hover(row, 0.24)
row.text = Font(row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"), 11)
row.text:SetPoint("LEFT", row, "LEFT", 6, 0)
row.text:SetWidth(width - infoWidth - 14)
row.text:SetHeight(ROW_HEIGHT)
row.text:SetJustifyH("LEFT")
row.info = Font(row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall"), 10, C.dim)
row.info:SetPoint("RIGHT", row, "RIGHT", -6, 0)
row.info:SetWidth(infoWidth)
row.info:SetHeight(ROW_HEIGHT)
row.info:SetJustifyH("RIGHT")
row.star = row:CreateTexture(nil, "OVERLAY")
row.star:SetTexture(STAR)
row.star:SetTexCoord(0, 0.25, 0, 0.25)
row.star:SetWidth(12)
row.star:SetHeight(12)
row.star:SetPoint("RIGHT", row, "RIGHT", -infoWidth - 10, 0)
row.star:Hide()
-- The tick marks the entry you have on (the Mounts tab's skin).
row.tick = row:CreateTexture(nil, "OVERLAY")
row.tick:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
row.tick:SetWidth(16)
row.tick:SetHeight(16)
row.tick:SetPoint("RIGHT", row, "RIGHT", -infoWidth - 24, 0)
row.tick:Hide()
row:EnableMouseWheel(true)
row:SetScript("OnMouseWheel", function()
local bar = getglobal(name .. "ScrollBar")
bar:SetValue(bar:GetValue() - arg1 * ROW_HEIGHT * 3)
end)
row:SetScript("OnClick", function()
click(this.index, arg1)
end)
if enter then
row:SetScript("OnEnter", function()
enter(this.index, this)
end)
-- The preview box keeps the last row hovered, so the mouse can go over
-- to it (user, 19 Oct: it went back as soon as the mouse left the row).
-- It shows what you have on again after a change (list.leave).
row:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
end
list.rows[i] = row
end
return list
end
-- Sorting by name or by ID: a toggle button for every list that has one.
local function SortButton(page, name, x, y, key, refilter)
local button = Button(page, "", 92, x, y, function()
local prefs = Prefs()
prefs[key] = not prefs[key]
this:SetText(prefs[key] and "Sort: ID" or "Sort: A-Z")
refilter()
end, name)
button:SetText(Prefs()[key] and "Sort: ID" or "Sort: A-Z")
button:SetScript("OnEnter", function()
Tooltip(this, "Sort order", "Click to list by name (A-Z) or by ID.")
end)
button:SetScript("OnLeave", HideTooltip)
return button
end
-- Creature previews -------------------------------------------------------------
-- A view is a Model that shows a display ID. It loads at level `low`, below
-- an opaque layer, and moves up to `high` once REVEAL has passed. aspect:
-- the model frame's width / height. zoom (steps), panX / panY (parts of the
-- view's height) are the user's (preview boxes).
-- 2.21.8 (user: "I can't drag the model around or zoom in myself, use the
-- same controls as the character window"): a plain Model with a camera set
-- ignores SetPosition and SetFacing, the dressing room's PlayerModel moves
-- and turns. So views are PlayerModels when ClassicAPI's SetDisplayInfo works
-- on one (ui.playerViews, checked once), else Models as before.
local function NewView(parent, name, low, high, aspect)
local model = CreateFrame(ui.playerViews and "PlayerModel" or "Model", name, parent)
model:SetFrameLevel(low)
return { model = model, low = low, high = high, facing = 0, aspect = aspect or 1, zoom = 0, panX = 0, panY = 0,
player = ui.playerViews }
end
-- Turns a view (PlayerModels turn with SetRotation, Models with SetFacing).
function P.TurnView(view)
if view.player then
view.model:SetRotation(view.facing or 0)
else
view.model:SetFacing(view.facing or 0)
end
end
-- The whole model in view (2.21.0; user: the previews were "zoomed in too
-- much"). Seen in game with /dmorph frametest (19 Oct): while a model camera
-- is set, this client draws the model where the camera says and ignores
-- SetPosition and SetFacing (only the scale shows), so 2.21.0-2.21.6's
-- "scale to fit and move the middle to the camera" left models cut off or
-- out of view. Now nothing is moved: a model with a full-body camera
-- ("character info", type 1, Blizzard's own framing of the whole model:
-- 2,918 of 2,984 ported models, 389 of 679 stock ones, Data/ModelFrames.lua)
-- is shown through it; the rest keep the portrait camera (the head), as
-- before 2.21. Zoom is the scale on top. While a model loads (view.fresh)
-- the camera and the scale (a hair off every other frame) are sent again:
-- the client seems to drop them when loading ends.
P.ZOOM_FACTOR = 1.25
-- The client's trig functions take degrees or radians depending on its build.
P.ANGLE = math.sin(math.pi / 2) > 0.99 and 1 or 180 / math.pi
-- The camera a display is shown through: 1 (full body) if it has one, else 0.
function P.ViewCamera(displayID)
local _, _, _, _, _, _, _, _, _, full = M.GetModelFrame(displayID)
return full and 1 or 0
end
P.PLAYER_SIZE = 2.2 -- about a player's height in model units
local function FrameView(view)
local zoom = math.pow(P.ZOOM_FACTOR, view.zoom or 0)
if view.fresh then
view.flip = not view.flip
if view.flip then
zoom = zoom * 1.001
end
end
-- 2.21.9 (user: still no drag or zoom with 2.21.8): a PlayerModel gets no
-- camera of the model's (an explicit SetCamera locks it, as in the frame
-- test); like the dressing room it keeps its own player-sized view, where
-- moving and turning work. The model is scaled to a player's height
-- (P.PLAYER_SIZE) from Data/ModelFrames.lua, so it fits that view as you do
-- (a big creature morph filled the dressing room before), then zoom and
-- pan move it as the dressing room does.
if view.player then
local _, _, _, _, _, _, _, height, width = M.GetModelFrame(view.loaded)
local fit = 1
if height then
fit = P.PLAYER_SIZE / math.max(height, width * 0.75)
end
view.model:SetModelScale(fit * ((view.fresh and view.flip) and 1.001 or 1))
view.model:SetPosition((view.zoom or 0) * 0.3, (view.panX or 0) * 2, (view.panY or 0) * 2)
return
end
view.model:SetModelScale(zoom)
end
local function StartView(view, displayID)
local model = view.model
view.loaded = displayID
view.settle = SETTLE
-- Seconds the framing is pushed through while the model loads (FrameView).
view.fresh = 4
-- Drop the old model and its skins first, so none carry over to the new one.
pcall(model.ClearModel, model)
model:SetDisplayInfo(displayID)
-- Its full-body camera if it has one, else the portrait camera (P.ViewCamera);
-- a Model has no light of its own.
if not view.player then
view.camera = P.ViewCamera(displayID)
pcall(model.SetCamera, model, view.camera)
end
pcall(model.SetLight, model, 1, 0, 0, -0.7, -0.7, 0.7, 1, 1, 1, 0.8, 1, 1, 1)
P.TurnView(view)
FrameView(view)
end
-- Clearing a model whose textures are still loading frees them twice (the
-- game's Texture.cpp "Memory Invalid Block" crash, from ClearModel, when
-- hovering or paging fast). So a view changes model at most once per SETTLE;
-- a newer ID waits, and only the latest one is loaded.
local function LoadView(view, displayID)
view.displayID = displayID
if not view.manual then
view.facing = 0
end
view.wait = REVEAL
view.model:SetFrameLevel(view.low)
if view.settle then
view.pending = displayID
return
end
view.pending = nil
StartView(view, displayID)
end
-- Per frame: start a waiting model once the last one has settled, reveal a
-- loaded model, and turn it slowly while `spin` is set.
-- (Plain Model frames turn with SetFacing; SetRotation is PlayerModel's.)
local function TickView(view, elapsed, spin)
if view.settle then
view.settle = view.settle - elapsed
if view.settle <= 0 then
view.settle = nil
local pending = view.pending
view.pending = nil
if pending and pending ~= view.loaded then
view.wait = REVEAL
StartView(view, pending)
end
end
end
if view.settle and view.pending then
return
end
if view.wait then
view.wait = view.wait - elapsed
if view.wait <= 0 then
view.wait = nil
view.model:SetFrameLevel(view.high)
end
end
if not view.loaded then
return
end
-- A loading model forgets its scale, place and camera, so they go on every
-- frame, and are pushed through while it is fresh.
if view.fresh then
view.fresh = view.fresh - elapsed
if view.fresh <= 0 then
view.fresh = nil
end
if not view.player then
pcall(view.model.SetCamera, view.model, view.camera or 0)
end
end
FrameView(view)
if spin and view.displayID and not view.manual then
view.facing = view.facing + elapsed * 1.6
end
P.TurnView(view)
end
local function KindColor(kind)
return KIND_COLORS[kind] or KIND_COLORS[0]
end
local function QualityColor(quality)
local color = ITEM_QUALITY_COLORS[quality or 1] or ITEM_QUALITY_COLORS[1]
return color.r, color.g, color.b
end
-- Character preview ---------------------------------------------------------------
-- The dress-up backdrop of the race you look like: your race morph, else your own.
local function ShowArt()
local state = M.GetState()
local art = state.race and RACE_ART[state.race]
if not art then
local _, token = UnitRace("player")
art = ART_TOKENS[token or ""] or token or "Human"
end
if art ~= ui.artName then
ui.artName = art
for i = 1, 4 do
ui.art[i]:SetTexture("Interface\\DressUpFrame\\DressUpBackground-" .. art .. i)
end
end
end
local function UpdatePlate()
local state = M.GetState()
local title, info
if state.display then
title = M.GetDisplayName(state.display) or "Unlisted model"
info = "Display ID " .. state.display
elseif state.race then
title = M.DescribeBody(state.race, state.gender)
info = "Race morph"
else
title = UnitName("player") or "Your character"
info = "Not morphed"
end
local count = 0
for _ in pairs(state.items) do
count = count + 1
end
-- Short words: the line wrapped into the name above it (user, 18 Oct).
if count == 1 then
info = info .. " - 1 item"
elseif count > 1 then
info = info .. " - " .. count .. " items"
end
if state.mount then
info = info .. " - mount skin"
end
if state.scale then
info = info .. " - size " .. math.floor(state.scale * 100 + 0.5) .. "%"
end
ui.plateTitle:SetText(title)
ui.plateInfo:SetText(info)
-- The size slider shows the picked size without picking it again.
if ui.sizeSlider then
ui.sizeSync = true
ui.sizeSlider:SetValue(state.scale or 1)
ui.sizeSync = nil
end
end
local RequestPreview
-- The new look has loaded behind the backdrop: bring it to the front.
-- Puts a preview model where the zoom and the panning say. Closer also means
-- a little lower, so zooming in heads for the face rather than the belt.
local function ApplyView(model)
local p = ui.preview
model:SetPosition(p.zoom, p.panX, p.panY - p.zoom * 0.28)
-- The creature view (P.StageCreature) follows the same zoom, turn and pan.
local v = ui.stageView
if v then
v.zoom, v.facing, v.manual = p.zoom / ZOOM_STEP, p.rotation, true
v.panX, v.panY = p.panX / 3, p.panY / 3
end
end
-- 2.21.3 (user's screenshots: a big creature morph filled the dressing room):
-- morphed into a creature with frame data, the stage shows that display in a
-- creature view framed like the preview boxes, not your character (item
-- morphs don't show on creatures anyway).
function P.StageCreature()
local display = M.GetState().display
if ui.canPreview and ui.stageView and display and M.GetModelFrame(display) then
return display
end
end
local function Zoom(step)
local p = ui.preview
p.zoom = math.max(ZOOM_MIN, math.min(ZOOM_MAX, p.zoom + step * ZOOM_STEP))
ApplyView(p.models[p.front])
end
local function ResetView()
local p = ui.preview
p.zoom, p.panX, p.panY = 0, 0, 0
ApplyView(p.models[p.front])
end
local function SwapPreview()
local p = ui.preview
p.front = 3 - p.front
local front, back = p.models[p.front], p.models[3 - p.front]
front:SetRotation(p.rotation)
ApplyView(front)
front:SetFrameLevel(p.high)
back:SetFrameLevel(p.low)
back:Hide()
p.loading = nil
if p.again then
p.again = nil
RequestPreview()
end
end
-- A model is only reloaded once it has finished loading: changes made while
-- it loads are picked up by one more load afterwards. Reloading it half way
-- can leave textures from the earlier look on it.
local function LoadPreview()
local p = ui.preview
local creature = P.StageCreature()
local v = ui.stageView
if creature then
p.models[1]:Hide()
p.models[2]:Hide()
v.model:Show()
ApplyView(v.model)
if v.displayID ~= creature then
LoadView(v, creature)
end
return
end
if v then
v.model:Hide()
v.displayID = nil
end
if p.loading then
p.again = true
return
end
p.loading = true
local back = p.models[3 - p.front]
back:SetFrameLevel(p.low)
back:Show()
back:SetUnit("player")
back:SetRotation(p.rotation)
ApplyView(back)
Debounce(ui.revealTimer, REVEAL, SwapPreview)
end
-- After any change: the new look loads in the hidden model and replaces the
-- shown one when it's ready. A burst of changes loads once.
RequestPreview = function()
Debounce(ui.previewTimer, 0.1, LoadPreview)
end
local function UpdateSlots()
local state = M.GetState()
for slot, button in pairs(ui.slots) do
local itemID = state.items[slot]
if itemID == 0 then
-- Shows nothing (Invisible, Birthday suit, own gear hidden under a set):
-- the empty slot, purple. It showed a question mark (user, 18 Oct).
local _, empty = GetInventorySlotInfo(SLOT_KEYS[slot])
button.icon:SetTexture(empty)
button.icon:SetVertexColor(C.accent[1], C.accent[2], C.accent[3])
button.glow:Show()
elseif itemID then
local ok, _, _, _, _, _, _, _, _, texture = pcall(GetItemInfo, itemID)
if not ok then
texture = nil
end
-- A ported piece: the icon of the item it copies.
if not texture and M.IsPortedItem(itemID) then
texture = M.GetPortedIcon(itemID)
end
-- Not in the client's cache yet: asked for once (ported items copy a
-- known item's record, the server knows nothing of their own IDs).
if not texture and not ui.iconAsked[itemID] and not M.IsPortedItem(itemID)
and type(C_Item) == "table" and type(C_Item.RequestLoadItemDataByID) == "function" then
ui.iconAsked[itemID] = true
pcall(C_Item.RequestLoadItemDataByID, itemID)
end
button.icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark")
button.icon:SetVertexColor(1, 1, 1)
button.glow:Show()
else
-- Not morphed: show the gear you really wear, dimmed.
local texture = GetInventoryItemTexture("player", slot)
if texture then
button.icon:SetTexture(texture)
button.icon:SetVertexColor(0.45, 0.45, 0.45)
else
local _, empty = GetInventorySlotInfo(SLOT_KEYS[slot])
button.icon:SetTexture(empty)
button.icon:SetVertexColor(1, 1, 1)
end
button.glow:Hide()
end
if currentTab == "items" and slot == itemSlot then
button:LockHighlight()
else
button:UnlockHighlight()
end
end
end
local function SlotTooltip()
local slot = this.slot
local itemID = M.GetState().items[slot]
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(M.SLOT_NAMES[slot])
if itemID then
GameTooltip:AddLine("Morphed into " .. M.DescribeItem(itemID, slot), 0.6, 1, 0.6)
else
GameTooltip:AddLine("Showing your own gear.", 0.8, 0.8, 0.8)
end
GameTooltip:AddLine("Left-click: browse items for this slot", 1, 1, 1)
GameTooltip:AddLine("Right-click: back to your own gear", 1, 1, 1)
GameTooltip:Show()
end
-- Models tab ------------------------------------------------------------------------
local ShowTab -- defined below; the slot buttons switch to the Items tab
local function PageCount()
local pages = math.ceil((ui.modelTotal or 0) / PAGE_SIZE)
if pages < 1 then
pages = 1
end
return pages
end
local function UpdatePager()
local total = Count(ui.modelTotal or 0, "model")
if ui.listMode then
ui.pagerText:SetText(total)
ui.prevPage:Hide()
ui.nextPage:Hide()
return
end
local pages = PageCount()
ui.pagerText:SetText("Page " .. Commas(ui.page) .. "/" .. Commas(pages) .. " (" .. total .. ")")
ui.prevPage:Show()
ui.nextPage:Show()
SetEnabled(ui.prevPage, ui.page > 1)
SetEnabled(ui.nextPage, ui.page < pages)
end
-- The display ID you're morphed into, if any.
local function TargetDisplay()
return M.GetState().display
end
local function TileBorder(tile)
local line = C.line
if tile.hover then
line = C.accent
elseif tile.displayID and tile.displayID == TargetDisplay() then
line = C.gold
end
tile:SetBackdropBorderColor(line[1], line[2], line[3], 1)
end
local function FillTiles()
if ui.listMode or not ui.modelResult then
return
end
local catalog = M.GetCreatureCatalog()
local first = (ui.page - 1) * PAGE_SIZE
for i = 1, PAGE_SIZE do
local tile = ui.tiles[i]
local entry = ui.modelResult[first + i]
if entry then
local id = catalog.ids[entry]
local color = KindColor(catalog.kind[entry])
tile.entry, tile.displayID = entry, id
tile.title:SetText(catalog.names[entry])
tile.title:SetTextColor(color[1], color[2], color[3])
tile.idText:SetText(id)
if M.IsFavourite(id) then
tile.star:Show()
else
tile.star:Hide()
end
if tile.view.displayID ~= id then
LoadView(tile.view, id)
end
TileBorder(tile)
tile:Show()
else
tile.entry, tile.displayID = nil, nil
tile:Hide()
end
end
UpdatePager()
end
local function RefreshModels()
if ui.listMode then
ui.modelList.Refresh()
P.RefreshModelBox()
else
FillTiles()
end
end
local function FilterModels()
local prefs = Prefs()
local count
ui.modelResult, count = M.FilterCatalog(M.GetCreatureCatalog(), ui.modelSearch:GetText(),
M.MODEL_FILTERS[prefs.filter], nil, prefs.sortModels)
ui.modelTotal = count
ui.page = 1
ui.modelList.SetCount(count)
FillTiles()
UpdatePager()
end
local function TurnPage(step)
local page = ui.page + step
if page > PageCount() then
page = PageCount()
end
if page < 1 then
page = 1
end
if page ~= ui.page then
ui.page = page
FillTiles()
end
end
-- Grid or list. Each picks up where the other was.
local function SetListMode(listMode)
if not ui.canPreview then
listMode = true
end
if listMode and not ui.listMode then
ui.modelList.ScrollTo((ui.page - 1) * PAGE_SIZE + 1)
elseif ui.listMode and not listMode then
ui.page = math.floor(ui.modelList.GetOffset() / PAGE_SIZE) + 1
-- The list's preview box may have taken one of ClassicAPI's 8 NPC
-- dressing slots from a tile, so the tiles load afresh.
for i = 1, PAGE_SIZE do
ui.tiles[i].view.displayID = nil
end
end
ui.listMode = listMode
-- Without ClassicAPI the list is forced; keep the choice for when it is back.
if ui.canPreview then
Prefs().list = listMode
end
if listMode then
ui.grid:Hide()
ui.listBox:Show()
ui.viewButton:SetText("Grid view")
else
ui.listBox:Hide()
ui.grid:Show()
ui.viewButton:SetText("List view")
end
RefreshModels()
UpdatePager()
end
-- Click a model: become it.
local function MorphInto(displayID)
ui.displayBox:SetText(displayID)
Morph(M.SetDisplay, displayID)
end
local function ModelTooltip(owner, entry)
local catalog = M.GetCreatureCatalog()
local color = KindColor(catalog.kind[entry])
GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
GameTooltip:SetText(catalog.names[entry], color[1], color[2], color[3])
GameTooltip:AddLine("Display ID " .. catalog.ids[entry], 1, 1, 1)
local tags = catalog.tags[entry]
if tags then
local words = {}
if strfind(tags, "boss", 1, true) then
table.insert(words, "Boss")
end
if strfind(tags, "rare", 1, true) then
table.insert(words, "Rare creature")
end
if strfind(tags, "mount", 1, true) then
table.insert(words, "Mount")
end
GameTooltip:AddLine(table.concat(words, ", "), 1, 0.82, 0)
end
GameTooltip:AddLine(KIND_NOTES[catalog.kind[entry] or 0], 0.7, 0.7, 0.7)
GameTooltip:AddLine("Click to morph into it.", 0.6, 1, 0.6)
if M.IsFavourite(catalog.ids[entry]) then
GameTooltip:AddLine("Right-click to take it off your favourites.", 1, 0.82, 0)
else
GameTooltip:AddLine("Right-click to add it to your favourites.", 1, 0.82, 0)
end
GameTooltip:Show()
end
-- Right-click on a model: star it, or take the star off.
local function ToggleFavourite(displayID)
SetStatus(M.ToggleFavourite(displayID))
RefreshModels()
end
local function NewTile(parent, i)
local col, row = math.mod(i - 1, GRID_COLS), math.floor((i - 1) / GRID_COLS)
local tile = CreateFrame("Button", "DmorphOctoTile" .. i, parent)
tile:SetWidth(TILE_W)
tile:SetHeight(TILE_H)
tile:SetPoint("TOPLEFT", parent, "TOPLEFT", col * (TILE_W + TILE_GAP), -row * (TILE_H + TILE_GAP))
Flat(tile, C.card, C.line)
local base = tile:GetFrameLevel()
-- Levels: model while loading (+1) < cover (+2) < model once loaded (+3) < text (+4).
tile.view = NewView(tile, "DmorphOctoTile" .. i .. "Model", base + 1, base + 3, (TILE_W - 8) / (TILE_H - 34))
tile.view.model:SetPoint("TOPLEFT", tile, "TOPLEFT", 4, -4)
tile.view.model:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", -4, 30)
local cover = Layer(tile, 2)
cover:SetAllPoints(tile.view.model)
Gradient(cover, "BACKGROUND", 0.04, 0.035, 0.06, 1, 0.16, 0.1, 0.24, 1):SetAllPoints(cover)
local text = Layer(tile, 4)
text:SetAllPoints(tile)
tile.title = Font(text:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"), 11)
tile.title:SetPoint("BOTTOMLEFT", tile, "BOTTOMLEFT", 5, 4)
tile.title:SetWidth(TILE_W - 10)
tile.title:SetHeight(26)
tile.title:SetJustifyH("CENTER")
tile.idText = Font(text:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall"), 10, C.dim)
tile.idText:SetPoint("TOPRIGHT", tile, "TOPRIGHT", -6, -6)
tile.idText:SetJustifyH("RIGHT")
tile.star = text:CreateTexture(nil, "OVERLAY")
tile.star:SetTexture(STAR)
tile.star:SetTexCoord(0, 0.25, 0, 0.25)
tile.star:SetWidth(16)
tile.star:SetHeight(16)
tile.star:SetPoint("TOPLEFT", tile, "TOPLEFT", 5, -5)
tile.star:Hide()
tile:RegisterForClicks("LeftButtonUp", "RightButtonUp")
tile:EnableMouseWheel(true)
tile:SetScript("OnMouseWheel", function()
TurnPage(-arg1)
end)
tile:SetScript("OnClick", function()
if arg1 == "RightButton" then
ToggleFavourite(this.displayID)
if this.hover then
ModelTooltip(this, this.entry)
end
else
MorphInto(this.displayID)
end
end)
tile:SetScript("OnEnter", function()
this.hover = true
TileBorder(this)
ModelTooltip(this, this.entry)
end)
tile:SetScript("OnLeave", function()
this.hover = nil
TileBorder(this)
GameTooltip:Hide()
end)
tile:SetScript("OnUpdate", function()
TickView(this.view, arg1, this.hover)
end)
return tile
end
-- Preview boxes ----------------------------------------------------------------
-- Every list tab has one beside its list (2.21.0, user: "a box on the side and
-- an arrow up and down for scrolling through items one at a time"; it took
-- the place of the cards that floated next to a hovered row). It shows what
-- you hover, else what you have on. A creature shows whole in a Model (kind
-- "model", see FrameView); items, sets and outfits on your character in a
-- DressUpModel (kind "dress", the Dressing Room's model: SetUnit("player")
-- once per opening of the window, then Dress() and TryOn(item) per look, as
-- OctoWoW's DressUpFrame.lua does). Mouse wheel or + / - zoom, drag turns it,
-- right-drag moves it, Reset puts the view back. Previous / Next wear the
-- entry above or below the one you have on in the list.
P.ZOOM_MIN, P.ZOOM_MAX = -6, 10
-- The try-on. The game can only put on items it knows: one it hasn't seen yet
-- is asked for (ClassicAPI) and goes on when it arrives. A ported piece copies
-- a real item's record (its template): if the server hasn't sent that one, a
-- known item of the same slot stands in, as when you wear it (the card waited
-- for the template forever: user, 16 Oct). After P.CARD_WAIT seconds the box
-- says what didn't come.
P.CARD_WAIT = 8
function P.TryOnAll(box)
local missing = 0
for i = 1, table.getn(box.items) do
local id = box.items[i]
if not box.on[id] then
local ported = M.IsPortedItem(id)
if ported and not GetItemInfo(id) and M.CanShowPortedItem then
M.CanShowPortedItem(id)
end
if GetItemInfo(id) then
pcall(box.model.TryOn, box.model, id)
box.on[id] = true
else
missing = missing + 1
-- A ported item's own ID means nothing to the server.
if not ported and not box.asked[id] and type(C_Item) == "table"
and type(C_Item.RequestLoadItemDataByID) == "function" then
box.asked[id] = true
pcall(C_Item.RequestLoadItemDataByID, id)
end
end
end
end
box.pending = missing > 0 or nil
if missing == 0 then
box.status:SetText("")
elseif (box.waited or 0) < P.CARD_WAIT then
box.status:SetText("Loading from the server...")
elseif missing == table.getn(box.items) then
box.status:SetText("The server didn't send this.")
else
box.status:SetText(missing .. (missing == 1 and " piece" or " pieces") .. " didn't load.")
end
end
-- Where the zoom and the panning put a box's model. A creature is framed by
-- FrameView (every frame, from TickView); your character as on the stage:
-- closer is also a little lower, towards the face.
function P.ApplyBoxView(box)
if box.kind == "model" then
local view = box.view
view.zoom, view.panX, view.panY = box.zoom, box.panX, box.panY
return
end
local zoom = box.zoom * ZOOM_STEP
box.model:SetPosition(zoom, box.panX, box.panY - zoom * 0.28)
box.model:SetRotation(box.facing)
end
function P.BoxZoom(box, step)
box.zoom = math.max(P.ZOOM_MIN, math.min(P.ZOOM_MAX, box.zoom + step))
P.ApplyBoxView(box)
end
function P.BoxResetView(box)
box.zoom, box.panX, box.panY, box.manual, box.facing = 0, 0, 0, nil, 0
if box.view then
box.view.manual, box.view.facing = nil, 0
end
P.ApplyBoxView(box)
end
-- Text under the model: the name (in its colour) and a line about it.
function P.BoxText(box, title, color, info)
box.title:SetText(title or "")
color = color or C.gold
box.title:SetTextColor(color[1], color[2], color[3])
box.info:SetText(info or "")
end
-- A creature (kind "model"), or nothing (displayID nil).
function P.BoxShowModel(box, displayID, title, color, info)
P.BoxText(box, title, color, info)
local view = box.view
if not displayID then
view.displayID, view.wait = nil, nil
view.model:SetFrameLevel(view.low)
box.status:SetText("")
return
end
if not ui.canPreview then
box.status:SetText("3D previews need ClassicAPI.")
return
end
box.status:SetText("")
if view.displayID ~= displayID then
LoadView(view, displayID)
end
end
-- Your character wearing items (kind "dress"); no items: as you are.
function P.BoxShowItems(box, items, title, color, info)
P.BoxText(box, title, color, info)
if not box.loaded then
-- Your character as it is now (once per opening of the window).
box.loaded, box.dressed = true, nil
box.model:SetFrameLevel(box.base + 1)
box.model:SetUnit("player")
box.wait = REVEAL
P.ApplyBoxView(box)
end
-- The same items again (a click refreshes the window several times): left
-- as they are. Dressing a model again while its textures load drew your
-- character white until the next hover (user, 19 Oct).
items = items or {}
local same = table.getn(items) == table.getn(box.items)
for i = 1, table.getn(items) do
if items[i] ~= box.items[i] then
same = false
end
end
if same and box.dressed then
return
end
box.dressed = true
box.model:Dress()
box.items, box.on, box.waited, box.retry = items, {}, 0, 0.25
P.TryOnAll(box)
end
function P.BoxButton(parent, text, width, name, onClick, title, line)
local button = Button(parent, text, width, 0, 0, onClick, name)
button:SetHeight(18)
button:SetScript("OnEnter", function()
Tooltip(this, title, line)
end)
button:SetScript("OnLeave", HideTooltip)
return button
end
-- A box on the right of a page, from y down `rows` list rows (as tall as the
-- list beside it). step(-1 / 1) wears the previous / next entry.
function P.NewBox(page, key, kind, y, rows, heading, step)
local height = rows * ROW_HEIGHT
local name = "DmorphOcto" .. key .. "Box"
local box = CreateFrame("Frame", name, page)
box:SetPoint("TOPLEFT", page, "TOPLEFT", PAGE_W - BOX_W - 8, y)
box:SetWidth(BOX_W)
box:SetHeight(height)
Flat(box, C.card, C.line)
box.kind = kind
box.zoom, box.panX, box.panY, box.facing = 0, 0, 0, 0
local base = box:GetFrameLevel()
box.base = base
-- Levels: model while loading (+1) < cover (+2) < model shown (+3) < drag
-- area (+4) < text and buttons (+5).
local modelHeight = height - 22 - 70
if kind == "model" then
box.view = NewView(box, name .. "Model", base + 1, base + 3, (BOX_W - 10) / modelHeight)
box.model = box.view.model
else
box.model = CreateFrame("DressUpModel", name .. "Model", box)
box.model:SetFrameLevel(base + 1)
box.items, box.on, box.asked = {}, {}, {}
end
box.model:SetPoint("TOPLEFT", box, "TOPLEFT", 5, -22)
box.model:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -5, 70)
local cover = Layer(box, 2)
cover:SetAllPoints(box.model)
Gradient(cover, "BACKGROUND", 0.04, 0.035, 0.06, 1, 0.16, 0.1, 0.24, 1):SetAllPoints(cover)
-- Over the model: wheel zooms, drag turns, right-drag moves.
local drag = Layer(box, 4)
drag:SetAllPoints(box.model)
drag:EnableMouse(true)
drag:EnableMouseWheel(true)
drag:SetScript("OnMouseWheel", function()
P.BoxZoom(box, arg1)
end)
drag:SetScript("OnMouseDown", function()
local x, y = GetCursorPosition()
if arg1 == "RightButton" then
this.panX, this.panY = x, y
else
this.dragX = x
end
end)
drag:SetScript("OnMouseUp", function()
this.dragX, this.panX, this.panY = nil, nil, nil
end)
drag:SetScript("OnUpdate", function()
if ui.stageView.model:IsShown() then
TickView(ui.stageView, arg1, false)
end
if this.dragX then
local x = GetCursorPosition()
local turn = (x - this.dragX) * 0.02
this.dragX = x
box.manual = true
box.facing = box.facing + turn
if box.view then
box.view.manual = true
box.view.facing = box.view.facing + turn
end
P.ApplyBoxView(box)
elseif this.panX then
-- Creatures move by parts of the box's height, your character in yards.
local x, y = GetCursorPosition()
local scale = frame:GetEffectiveScale()
local step = 1 / (modelHeight * scale)
if kind ~= "model" then
step = 0.006 / (scale * (1 + math.max(box.zoom * ZOOM_STEP, 0) * 0.45))
end
box.panX = box.panX + (x - this.panX) * step
box.panY = box.panY + (y - this.panY) * step
this.panX, this.panY = x, y
P.ApplyBoxView(box)
end
end)
local text = Layer(box, 5)
text:SetAllPoints(box)
box.heading = Font(text:CreateFontString(name .. "Heading", "OVERLAY"), 12, C.accent)
box.heading:SetPoint("TOPLEFT", box, "TOPLEFT", 7, -5)
box.heading:SetText(heading)
local reset = P.BoxButton(text, "Reset", 40, name .. "ZoomReset", function()
P.BoxResetView(box)
end, "Reset view", "The whole model again, turning.")
reset:ClearAllPoints()
reset:SetPoint("TOPRIGHT", box, "TOPRIGHT", -4, -2)
local zoomIn = P.BoxButton(text, "+", 18, name .. "ZoomIn", function()
P.BoxZoom(box, 1)
end, "Zoom in", "Or: mouse wheel over the model. Drag turns it, right-drag moves it.")
zoomIn:ClearAllPoints()
zoomIn:SetPoint("TOPRIGHT", reset, "TOPLEFT", -2, 0)
local zoomOut = P.BoxButton(text, "-", 18, name .. "ZoomOut", function()
P.BoxZoom(box, -1)
end, "Zoom out", "Or: mouse wheel over the model. Drag turns it, right-drag moves it.")
zoomOut:ClearAllPoints()
zoomOut:SetPoint("TOPRIGHT", zoomIn, "TOPLEFT", -2, 0)
box.title = Font(text:CreateFontString(name .. "Title", "OVERLAY"), 12, C.gold)
box.title:SetPoint("BOTTOM", box, "BOTTOM", 0, 44)
box.title:SetWidth(BOX_W - 12)
box.title:SetHeight(26)
box.info = Font(text:CreateFontString(name .. "Info", "OVERLAY"), 10, C.dim)
box.info:SetPoint("BOTTOM", box, "BOTTOM", 0, 32)
box.info:SetWidth(BOX_W - 12)
box.status = Font(text:CreateFontString(name .. "Status", "OVERLAY"), 11)
box.status:SetPoint("CENTER", box.model, "CENTER", 0, 0)
box.status:SetWidth(BOX_W - 20)
local half = math.floor((BOX_W - 14) / 2)
local prev = Button(text, "Previous", half, 5, -(height - 26), function()
step(-1)
end, "DmorphOcto" .. key .. "Prev")
prev:SetScript("OnEnter", function()
Tooltip(this, "Previous", "Puts on the one above yours in the list.")
end)
prev:SetScript("OnLeave", HideTooltip)
local nextButton = Button(text, "Next", half, BOX_W - 5 - half, -(height - 26), function()
step(1)
end, "DmorphOcto" .. key .. "Next")
nextButton:SetScript("OnEnter", function()
Tooltip(this, "Next", "Puts on the one below yours in the list.")
end)
nextButton:SetScript("OnLeave", HideTooltip)
box:SetScript("OnUpdate", function()
if this.view then
TickView(this.view, arg1, true)
return
end
if this.wait then
this.wait = this.wait - arg1
if this.wait <= 0 then
this.wait = nil
this.model:SetFrameLevel(this.base + 3)
end
end
if not this.manual then
this.facing = this.facing + arg1 * 0.8
end
P.ApplyBoxView(this)
if this.pending then
this.waited = (this.waited or 0) + arg1
this.retry = (this.retry or 0) - arg1
if this.retry <= 0 then
-- Quick at first, then once a second (looking for a stand-in goes through a slot's items).
this.retry = this.waited < 2 and 0.25 or 1
P.TryOnAll(this)
end
end
end)
ui.boxes[key] = box
return box
end
-- The entry `step` away from position `pos` (nil: none worn) in a list of
-- `count`, wrapping round; nil if the list is empty.
function P.StepPosition(pos, step, count)
if count == 0 then
return nil
end
if pos then
return Wrap(pos, step, count)
end
return step > 0 and 1 or count
end
-- Where `value` is in a filtered list (ids[result[k]] == value), or nil.
function P.FindPosition(result, count, ids, value)
if not (value and result) then
return nil
end
for k = 1, count or 0 do
if ids[result[k]] == value then
return k
end
end
end
local function ModelFilterMenu()
local prefs, options = Prefs(), {}
for i = 1, table.getn(M.MODEL_FILTERS) do
table.insert(options, { text = M.MODEL_FILTERS[i].text, index = i, checked = prefs.filter == i })
end
OpenMenu(this, options, function(option)
Prefs().filter = option.index
ui.modelFilter:SetText(option.text)
FilterModels()
end)
end
-- The list view's box: the model you hover, else the one you have on.
function P.RefreshModelBox()
local box = ui.modelBox
if not box then
return
end
local id = TargetDisplay()
if id then
local name, kind = M.GetDisplayName(id)
P.BoxShowModel(box, id, name or "Unlisted model", KindColor(kind), "Display ID " .. id .. " - you have it on")
else
P.BoxShowModel(box, nil, "Your own model", C.dim, "Hover a model to see it here.")
end
end
function P.ShowModelEntry(index)
local entry = ui.modelResult[index]
if entry then
local catalog = M.GetCreatureCatalog()
P.BoxShowModel(ui.modelBox, catalog.ids[entry], catalog.names[entry], KindColor(catalog.kind[entry]),
"Display ID " .. catalog.ids[entry] .. " - click to morph")
end
end
-- Previous / Next: the model above or below yours in the list.
function P.StepModel(step)
local catalog = M.GetCreatureCatalog()
local pos = P.FindPosition(ui.modelResult, ui.modelTotal, catalog.ids, TargetDisplay())
pos = P.StepPosition(pos, step, ui.modelTotal or 0)
if not pos then
return SetStatus(false, "No models in the list: clear the search or pick another filter.")
end
MorphInto(catalog.ids[ui.modelResult[pos]])
ui.modelList.Show(pos)
P.RefreshModelBox()
end
local function BuildModelsPage(page)
local prefs = Prefs()
Label(page, "Search", "GameFontNormalSmall", 10, -12)
ui.modelSearch = EditBox(page, "DmorphOctoModelSearch", 150, 52, -8)
ui.modelSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterModels)
end)
ui.modelSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterModels()
end)
ui.modelFilter = Button(page, M.MODEL_FILTERS[prefs.filter].text, 150, 210, -7, ModelFilterMenu, "DmorphOctoModelFilter")
ui.modelFilter:SetScript("OnEnter", function()
Tooltip(this, "Which models to list", "Creatures, bosses, rare creatures, mounts, models no creature uses,",
"or only the ones OctoWoW added.")
end)
ui.modelFilter:SetScript("OnLeave", HideTooltip)
ui.viewButton = Button(page, "List view", 82, 364, -7, function()
SetListMode(not ui.listMode)
end, "DmorphOctoModelView")
SortButton(page, "DmorphOctoModelSort", PAGE_W - 92 - 8, -7, "sortModels", FilterModels)
-- Pager (grid view) or how many (list view).
ui.prevPage = Button(page, "<", 24, 8, -33, function()
TurnPage(-1)
end, "DmorphOctoPrevPage")
ui.nextPage = Button(page, ">", 24, 210, -33, function()
TurnPage(1)
end, "DmorphOctoNextPage")
ui.pagerText = Font(page:CreateFontString("DmorphOctoPagerText", "OVERLAY", "GameFontHighlightSmall"), 11, C.dim)
ui.pagerText:SetPoint("TOP", page, "TOPLEFT", 121, -38)
-- Grid of previews.
ui.grid = CreateFrame("Frame", "DmorphOctoModelGrid", page)
ui.grid:SetPoint("TOPLEFT", page, "TOPLEFT", 8, -60)
ui.grid:SetWidth(GRID_COLS * TILE_W + (GRID_COLS - 1) * TILE_GAP)
ui.grid:SetHeight(2 * TILE_H + TILE_GAP)
ui.grid:EnableMouseWheel(true)
ui.grid:SetScript("OnMouseWheel", function()
TurnPage(-arg1)
end)
for i = 1, PAGE_SIZE do
ui.tiles[i] = NewTile(ui.grid, i)
end
-- Or a list, with the preview box.
ui.listBox = CreateFrame("Frame", "DmorphOctoModelListBox", page)
ui.listBox:SetAllPoints(page)
ui.modelList = List(ui.listBox, "DmorphOctoModelList", 8, -60, 52, function(row, index)
local catalog = M.GetCreatureCatalog()
local i = ui.modelResult[index]
local color = KindColor(catalog.kind[i])
row.text:SetText(catalog.names[i])
row.text:SetTextColor(color[1], color[2], color[3])
row.info:SetText(catalog.ids[i])
if catalog.ids[i] == TargetDisplay() then
row:LockHighlight()
row.tick:Show()
row.text:SetTextColor(C.gold[1], C.gold[2], C.gold[3])
end
if M.IsFavourite(catalog.ids[i]) then
row.star:Show()
end
end, function(index, button)
local displayID = M.GetCreatureCatalog().ids[ui.modelResult[index]]
if button == "RightButton" then
ToggleFavourite(displayID)
else
MorphInto(displayID)
end
end, P.ShowModelEntry, LIST_W)
ui.modelList.leave = P.RefreshModelBox
ui.modelBox = P.NewBox(ui.listBox, "Models", "model", -60, LIST_ROWS, "Preview", P.StepModel)
-- Any display ID, including ones no list has.
local y = -346
Label(page, "Display ID", "GameFontNormalSmall", 10, y - 5)
ui.displayBox = EditBox(page, "DmorphOctoDisplayID", 70, 76, y, 9)
local function Apply(step)
local displayID = M.ParseID(ui.displayBox:GetText()) or TargetDisplay()
if not displayID then
if step == 0 then
return SetStatus(false, "Type a display ID first.")
end
displayID = 0
end
displayID = displayID + step
if displayID < 1 then
displayID = 1
end
ui.displayBox:ClearFocus()
MorphInto(displayID)
end
ui.displayBox:SetScript("OnEnterPressed", function()
Apply(0)
end)
Button(page, "<", 24, 152, y, function()
Apply(-1)
end, "DmorphOctoDisplayPrev")
Button(page, ">", 24, 178, y, function()
Apply(1)
end, "DmorphOctoDisplayNext")
Button(page, "Morph", 70, 206, y, function()
Apply(0)
end)
Label(page, "< and > step through every ID.", "GameFontDisableSmall", 284, y - 6)
if not ui.canPreview then
ui.viewButton:Hide()
Label(page, "3D previews need ClassicAPI.", "GameFontDisableSmall", 366, -13)
end
ui.listMode = nil
SetListMode(prefs.list)
end
-- Favourites tab --------------------------------------------------------------------
local MOUNT_COLOR = { 1, 0.82, 0.4 }
local function FilterFavourites(keep)
local prefs = Prefs()
local filter = FAV_FILTERS[prefs.favFilter]
local query = strlower(ui.favSearch:GetText() or "")
query = string.gsub(query, "^%s+", "")
query = string.gsub(query, "%s+$", "")
local favourites, list, n = M.GetFavourites(), {}, 0
for i = 1, table.getn(favourites) do
local fav = favourites[i]
if filter.mount == nil or filter.mount == (fav.mount == true) then
local name = M.GetDisplayName(fav.id) or ("Display " .. fav.id)
if query == "" or tostring(fav.id) == query or strfind(strlower(name), query, 1, true) then
n = n + 1
list[n] = { id = fav.id, mount = fav.mount, name = name, lower = strlower(name) }
end
end
end
if prefs.sortFavs then
table.sort(list, function(a, b)
return a.id < b.id or (a.id == b.id and a.lower < b.lower)
end)
else
table.sort(list, function(a, b)
return a.lower < b.lower or (a.lower == b.lower and a.id < b.id)
end)
end
ui.favResult = list
ui.favCount:SetText(Count(n, "favourite"))
ui.favList.SetCount(n, keep)
if table.getn(favourites) == 0 then
ui.favEmpty:Show()
else
ui.favEmpty:Hide()
end
end
local function RefreshFavourites()
FilterFavourites(true)
P.RefreshFavBox()
end
local function FavouriteMenu()
local prefs, options = Prefs(), {}
for i = 1, table.getn(FAV_FILTERS) do
table.insert(options, { text = FAV_FILTERS[i].text, index = i, checked = prefs.favFilter == i })
end
OpenMenu(this, options, function(option)
Prefs().favFilter = option.index
ui.favFilter:SetText(option.text)
FilterFavourites()
end)
end
-- The favourite you have on: your model, else your mount's skin.
function P.WornFavourite()
local state = M.GetState()
for k = 1, table.getn(ui.favResult or {}) do
local fav = ui.favResult[k]
if (fav.mount and state.mount == fav.id) or (not fav.mount and state.display == fav.id) then
return k
end
end
end
function P.ShowFavourite(fav, info)
P.BoxShowModel(ui.favBox, fav.id, fav.name, fav.mount and MOUNT_COLOR or KIND_COLORS[0], info)
end
function P.RefreshFavBox()
local k = P.WornFavourite()
if k then
local fav = ui.favResult[k]
P.ShowFavourite(fav, (fav.mount and "Mount " or "Model ") .. fav.id .. " - you have it on")
else
P.BoxShowModel(ui.favBox, nil, "No favourite on", C.dim, "Hover one to see it here.")
end
end
function P.WearFavourite(fav)
if fav.mount then
Morph(M.SetMount, fav.id)
else
Morph(M.SetDisplay, fav.id)
end
end
function P.StepFavourite(step)
local n = table.getn(ui.favResult or {})
local pos = P.StepPosition(P.WornFavourite(), step, n)
if not pos then
return SetStatus(false, "No favourites in the list.")
end
P.WearFavourite(ui.favResult[pos])
ui.favList.Show(pos)
P.ShowFavourite(ui.favResult[pos], "Click a row to wear it")
end
local function BuildFavouritesPage(page)
local prefs = Prefs()
Label(page, "Search", "GameFontNormalSmall", 10, -12)
ui.favSearch = EditBox(page, "DmorphOctoFavSearch", 150, 52, -8)
ui.favSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterFavourites)
end)
ui.favSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterFavourites()
end)
ui.favFilter = Button(page, FAV_FILTERS[prefs.favFilter].text, 150, 210, -7, FavouriteMenu, "DmorphOctoFavFilter")
SortButton(page, "DmorphOctoFavSort", PAGE_W - 92 - 8, -7, "sortFavs", FilterFavourites)
ui.favList = List(page, "DmorphOctoFavList", 8, -36, 80, function(row, index)
local fav = ui.favResult[index]
local state = M.GetState()
row.text:SetText(fav.name)
local worn
if fav.mount then
row.text:SetTextColor(MOUNT_COLOR[1], MOUNT_COLOR[2], MOUNT_COLOR[3])
row.info:SetText("Mount " .. fav.id)
worn = state.mount == fav.id
else
row.info:SetText("Model " .. fav.id)
worn = state.display == fav.id
end
if worn then
row:LockHighlight()
row.tick:Show()
end
end, function(index, button)
local fav = ui.favResult[index]
if button == "RightButton" then
SetStatus(M.ToggleFavourite(fav.id, fav.mount))
FilterFavourites(true)
P.RefreshFavBox()
else
P.WearFavourite(fav)
end
end, function(index)
local fav = ui.favResult[index]
if fav then
P.ShowFavourite(fav, (fav.mount and "Mount " or "Model ") .. fav.id .. " - click to wear it")
end
end, LIST_W, 16)
ui.favList.leave = P.RefreshFavBox
ui.favBox = P.NewBox(page, "Favourites", "model", -36, 16, "Preview", P.StepFavourite)
ui.favCount = Label(page, "", "GameFontDisableSmall", 10, -334, "DmorphOctoFavCount")
ui.favEmpty = Label(page, "No favourites yet. Right-click a model in the Models tab to star it.",
"GameFontDisableSmall", 14, -44)
ui.favEmpty:SetWidth(LIST_W - 12)
Label(page, "Click to wear it. Right-click to take it off the list.", "GameFontDisableSmall", 10, -350)
Label(page, "Star more: right-click a model (Models tab) or a mount skin (Mounts tab).",
"GameFontDisableSmall", 10, -364)
end
-- Races tab ------------------------------------------------------------------------
-- One tab for both ways of changing race. A portrait morphs you into that race
-- with your own face, skin and hair, and your gear shows (VanillaHelpers' race
-- morph). With DmorphHelper.dll and the game's list of looks
-- (Data/CharOptions.lua), the first five < > rows change that model's real
-- skin colour, face and hair, one row at a time, and the last one wears whole
-- NPC looks. Without them every row steps through the faces, skin colours and
-- hair of the game's own NPCs of that race (Data/Looks.lua); item morphs don't
-- show on an NPC look.
local function RaceIcon(button, race, gender)
local icon = button:CreateTexture(nil, "ARTWORK")
icon:SetAllPoints(button)
icon:SetTexture(RACE_ICONS_FILE)
local c = RACE_ICONS[race]
icon:SetTexCoord(c[1], c[2], c[3] + gender * 0.5, c[3] + gender * 0.5 + 0.25)
button.ring = button:CreateTexture(nil, "OVERLAY")
button.ring:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
button.ring:SetBlendMode("ADD")
button.ring:SetVertexColor(1, 0.82, 0.2)
button.ring:SetWidth(button:GetWidth() * 1.8)
button.ring:SetHeight(button:GetHeight() * 1.8)
button.ring:SetPoint("CENTER", button, "CENTER", 0, 0)
button.ring:Hide()
Hover(button, 0.3)
button.race, button.gender = race, gender
end
local function GenderName(gender)
if gender == 1 then
return "Female"
end
return "Male"
end
-- The race and gender the tab works on, and the NPC look you wear (its
-- position in that race's looks), or nil when you wear your own face.
local function RaceTarget()
local c = ui.creator
local state = M.GetState()
if c.index and c.race then
local look = M.GetLooks(c.race, c.gender)[c.index]
if not (look and state.display == look[1]) then
c.index = nil -- you've morphed into something else since
end
end
if not c.index then
local race, gender = M.GetBodyRace()
c.race, c.gender = race or c.race or 1, gender or c.gender or 0
end
return c.race, c.gender, c.index
end
-- Whether rows 1-5 change your own model's look (DmorphHelper) for a race.
local function OwnLookMode(race, gender)
return M.HasHelper() and M.GetLookOptions(race, gender) ~= nil
end
local function Position(list, value)
for i = 1, table.getn(list) do
if list[i] == value then
return i
end
end
return 1
end
local function RefreshRaces()
local race, gender, index = RaceTarget()
local state = M.GetState()
local nativeRace, nativeGender = M.GetNative()
if nativeRace then
ui.nativeLabel:SetText("Your character: " .. M.DescribeBody(nativeRace, nativeGender))
else
ui.nativeLabel:SetText("")
end
for _, button in pairs(ui.raceButtons) do
if button.race == race and button.gender == gender and (index or not state.display) then
button.ring:Show()
button:LockHighlight()
else
button.ring:Hide()
button:UnlockHighlight()
end
end
local list = M.GetLooks(race, gender)
local look = index and list[index]
if look then
ui.creatorTitle:SetText(M.DescribeBody(race, gender) .. ": NPC look " .. Commas(index) .. " of "
.. Commas(table.getn(list)) .. " (display " .. look[1] .. ")")
elseif state.display then
ui.creatorTitle:SetText("Morphed into a creature. Pick a portrait to go back to a race.")
else
ui.creatorTitle:SetText(M.DescribeBody(race, gender) .. ": your own face, skin and hair")
end
local own = OwnLookMode(race, gender)
local options, values5 = own and M.GetLookOptions(race, gender)
if own and look then
values5 = { look[2], look[3], look[4], look[5], look[6] }
elseif own and not state.display then
local _, _, _, current, picked = M.GetLookState()
values5 = current
if picked then
ui.creatorTitle:SetText(M.DescribeBody(race, gender) .. ": your model, with a look you picked")
end
end
for f = 1, 6 do
local row = ui.creatorRows[f]
local text, n = "yours", 2
if own and f <= 5 then
-- Your model's look among everything the race can wear.
text = "?"
if values5 then
local choices, pos, npc = M.LookRow(options, values5, f)
n = table.getn(choices)
text = (pos > 0 and pos or "?") .. " of " .. n
if npc then
text = text .. " (NPC)"
end
end
elseif look then
-- Each row counts every value the race's NPCs have for it.
local values = M.GetLookValues(race, gender, f)
n = table.getn(values)
text = Position(values, M.LookValue(look, f)) .. " of " .. n
if f == 6 then
text = text .. " (" .. Count(look[7], "piece") .. ")"
end
elseif f == 6 then
text = "your gear"
end
row.value:SetText(text)
if n < 2 then
row.value:SetTextColor(0.5, 0.5, 0.5)
else
row.value:SetTextColor(1, 1, 1)
end
if f == 5 then
if gender == 1 then
row.label:SetText("Features")
else
row.label:SetText("Facial hair")
end
elseif f == 6 then
if own then
row.label:SetText("NPC look")
else
row.label:SetText("Outfit")
end
end
end
if own then
ui.racesHint:SetText("< > change your model's real skin, face and hair; NPC look wears a whole NPC.")
else
ui.racesHint:SetText("< > go to the next value an NPC has; other rows change only if they must.")
end
end
local function WearLook(race, gender, index, message)
local list = M.GetLooks(race, gender)
if not (index and list[index]) then
return SetStatus(false, "The game has no " .. M.DescribeBody(race, gender) .. " NPC looks.")
end
local c = ui.creator
c.race, c.gender, c.index = race, gender, index
Morph(function()
local ok, text = M.SetDisplay(list[index][1])
if ok and message then
text = message
end
return ok, text
end)
end
-- A row's name as the Races tab shows it ("Features" on women).
local function FeatureName(f, gender)
if f == 5 and gender == 1 then
return "Features"
end
return M.LOOK_FEATURES[f]
end
-- Row f (1-6) one value along. It always changes, to the next value an NPC of
-- this race has; if none has it with your other rows, the closest look is
-- worn and the status says which rows came along.
-- Your own model of the race the tab shows, from an NPC look or a creature.
local function ToOwnModel(race, gender, index)
if index or M.GetState().display then
return M.SetRace(race, gender)
end
return true
end
local function StepFeature(f, step)
local race, gender, index = RaceTarget()
local list = M.GetLooks(race, gender)
if f <= 5 and OwnLookMode(race, gender) then
local npc = index and list[index]
ui.creator.index = nil
return Morph(function()
local ok, message = ToOwnModel(race, gender, index)
if not ok then
return ok, message
end
if npc then
-- Start from the NPC's face, skin and hair.
M.SetOwnLook({ npc[2], npc[3], npc[4], npc[5], npc[6] })
end
ok, message = M.StepOwnLook(f, step)
if ok and npc then
message = "Your own model now, with that NPC's face, skin and hair, so your gear shows. " .. message
end
return ok, message
end)
end
if table.getn(list) == 0 then
return WearLook(race, gender, nil)
end
if not index then
-- From your own look, start at the plainest NPC look of this race.
if OwnLookMode(race, gender) then
return WearLook(race, gender, 1, "The plainest " .. M.DescribeBody(race, gender)
.. " NPC. NPC look < > goes through the others; the rows above take you back to your model.")
end
return WearLook(race, gender, 1, "Your own face can't be changed bit by bit, so this is the plainest "
.. M.DescribeBody(race, gender) .. " NPC. Now use the < > rows.")
end
local k, pos, n, changed = M.StepLook(race, gender, f, index, step)
if not k then
return SetStatus(false, "Every " .. M.DescribeBody(race, gender) .. " NPC has the same " .. strlower(FeatureName(f, gender)) .. ".")
end
local message = FeatureName(f, gender) .. " " .. pos .. " of " .. n .. "."
-- Each NPC wears its own clothes, so the outfit nearly always changes with
-- a face or hair row; only the other face and hair rows are worth naming.
local names = {}
for i = 1, table.getn(changed) do
if f == 6 or changed[i] ~= 6 then
table.insert(names, strlower(FeatureName(changed[i], gender)))
end
end
if table.getn(names) > 0 then
local joined = names[1]
if table.getn(names) > 1 then
joined = table.concat(names, ", ", 1, table.getn(names) - 1) .. " and " .. names[table.getn(names)]
end
message = message .. " No NPC has it with your other choices, so the " .. joined .. " changed too."
end
WearLook(race, gender, k, message)
end
local function BuildRacesPage(page)
Label(page, "Races and looks", "GameFontNormal", 10, -10)
ui.nativeLabel = Label(page, "", "GameFontDisableSmall", 130, -12, "DmorphOctoNativeLabel")
for race = 1, table.getn(M.RACES) do
for gender = 0, 1 do
local button = CreateFrame("Button", "DmorphOctoRace" .. race .. GenderName(gender), page)
button:SetWidth(36)
button:SetHeight(36)
-- Spread over the page's width.
local gap = math.floor((PAGE_W - 28 - 36) / (table.getn(M.RACES) - 1))
button:SetPoint("TOPLEFT", page, "TOPLEFT", 14 + (race - 1) * gap, -30 - gender * 40)
RaceIcon(button, race, gender)
button:SetScript("OnClick", function()
local c = ui.creator
c.race, c.gender, c.index = this.race, this.gender, nil
Morph(M.SetRace, this.race, this.gender)
end)
button:SetScript("OnEnter", function()
local after = "Then the < > rows go through the game's " .. M.DescribeBody(this.race, this.gender) .. " NPC looks."
if OwnLookMode(this.race, this.gender) then
after = "Then the < > rows change its skin colour, face and hair."
end
Tooltip(this, M.DescribeBody(this.race, this.gender), "Click: this race with your own face, skin and hair.", after)
end)
button:SetScript("OnLeave", HideTooltip)
table.insert(ui.raceButtons, button)
end
end
ui.creatorTitle = Label(page, "", "GameFontHighlight", 14, -114, "DmorphOctoCreatorTitle")
for f = 1, 6 do
local y = -136 - (f - 1) * 26
local row = {}
row.label = Label(page, M.LOOK_FEATURES[f], "GameFontNormal", 20, y - 4)
Button(page, "<", 26, 190, y, function()
StepFeature(this.feature, -1)
end, "DmorphOctoCreatorPrev" .. f).feature = f
row.value = Font(page:CreateFontString("DmorphOctoCreatorValue" .. f, "OVERLAY", "GameFontHighlight"), 12)
row.value:SetPoint("TOP", page, "TOPLEFT", 320, y - 4)
Button(page, ">", 26, 424, y, function()
StepFeature(this.feature, 1)
end, "DmorphOctoCreatorNext" .. f).feature = f
ui.creatorRows[f] = row
end
local dice = CreateFrame("Button", "DmorphOctoCreatorRandom", page)
dice:SetWidth(28)
dice:SetHeight(28)
dice:SetPoint("TOPLEFT", page, "TOPLEFT", 14, -296)
dice:SetNormalTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Up")
dice:SetPushedTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Down")
dice:SetHighlightTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Highlight")
dice:SetScript("OnClick", function()
local race, gender, index = RaceTarget()
if OwnLookMode(race, gender) then
ui.creator.index = nil
return Morph(function()
local ok, message = ToOwnModel(race, gender, index)
if not ok then
return ok, message
end
return M.RandomOwnLook()
end)
end
local count = table.getn(M.GetLooks(race, gender))
if count == 0 then
return WearLook(race, gender, nil)
end
local pick = math.random(count)
if count > 1 and pick == index then
pick = Wrap(pick, 1, count)
end
WearLook(race, gender, pick)
end)
dice:SetScript("OnEnter", function()
local race, gender = RaceTarget()
if OwnLookMode(race, gender) then
Tooltip(this, "Random look", "Any skin colour, face and hair this race can have, on your model.")
else
Tooltip(this, "Random look", "Any NPC face, skin, hair and outfit of this race.")
end
end)
dice:SetScript("OnLeave", HideTooltip)
Button(page, "My own look", 100, 48, -299, function()
local race, gender = RaceTarget()
ui.creator.index = nil
Morph(function()
local ok, message = M.SetRace(race, gender)
if ok and M.HasHelper() then
local _, _, _, _, picked = M.GetLookState()
if picked then
return M.ResetOwnLook()
end
end
return ok, message
end)
end, "DmorphOctoOwnLook")
Button(page, "Swap gender", 100, 152, -299, function()
local race, gender, index = RaceTarget()
if index then
-- From an NPC look, the other gender's plainest look.
return WearLook(race, 1 - gender, 1)
end
Morph(M.ToggleGender)
end)
local star = Button(page, "Favourite", 92, PAGE_W - 102, -299, function()
local race, gender, index = RaceTarget()
local look = index and M.GetLooks(race, gender)[index]
if not look then
return SetStatus(false, "Pick an NPC look first: your own look is always one portrait away.")
end
SetStatus(M.ToggleFavourite(look[1]))
end, "DmorphOctoCreatorFavourite")
star:SetScript("OnEnter", function()
Tooltip(this, "Favourite this look", "Adds the NPC look you wear to the Favourites tab.")
end)
star:SetScript("OnLeave", HideTooltip)
Label(page, "Portraits keep your own face, skin and hair, and your item morphs show.", "GameFontDisableSmall", 14, -340)
ui.racesHint = Label(page, "", "GameFontDisableSmall", 14, -354, "DmorphOctoRacesHint")
end
-- Mounts tab -------------------------------------------------------------------------
-- Pick a skin for your mount: any model, shown only while you ride, so you
-- mount and dismount as usual. "Skin for" picks all your mounts or one you've
-- ridden on this character.
-- Mounts only, but for "All models" (any model can be a skin). OctoWoW's and
-- the ported ones were every model of their kind, so the Mounts tab listed
-- all 6,399 ported models like the Models tab (user, 17 Oct). Saved by index.
local MOUNT_FILTERS = {
{ text = "Mount models", tag = "mount" },
{ text = "All models" },
{ text = "OctoWoW's new mounts", tag = "mount", kinds = { [2] = true, [3] = true } },
-- Models from later expansions (Data/Ported.lua), only on the PC that has them.
{ text = "Ported mounts", tag = "mount", kinds = { [4] = true } },
}
local function MountName(displayID)
return M.GetDisplayName(displayID) or ("Mount " .. displayID)
end
-- The skin you picked last for the "Skin for" choice: the one on, or the one
-- a click waiting at the gate is putting on.
local function MountPick()
local pick = ui.mountPick
if pick and pick.target == ui.mountFor then
return pick.id
end
return M.GetMountSkin(ui.mountFor)
end
-- The position of a display ID in the filtered mount list, or nil.
local function MountPosition(displayID)
if not (displayID and ui.mountResult) then
return nil
end
local catalog = M.GetCreatureCatalog()
for index = 1, ui.mountTotal or 0 do
if catalog.ids[ui.mountResult[index]] == displayID then
return index
end
end
end
-- Scrolls the skin you have on into view (about the middle of the list).
local function ShowMountPick()
local pos = MountPosition(MountPick())
if pos then
ui.mountList.Show(pos)
else
ui.mountList.Refresh()
end
end
local function PickMount(displayID)
ui.mountPick = { target = ui.mountFor, id = displayID }
Morph(M.SetMountSkin, displayID, ui.mountFor)
end
local function RefreshMountState()
local riding, real, skin = M.GetMountState()
local target = ui.mountFor and MountName(ui.mountFor) or "All my mounts"
ui.mountForButton:SetText(target)
if riding and skin then
ui.mountState:SetText("You're riding |cffffffff" .. MountName(real) .. "|r, shown as |cffffd100" .. MountName(skin) .. "|r.")
elseif riding then
ui.mountState:SetText("You're riding |cffffffff" .. MountName(real) .. "|r, with no skin on it.")
else
ui.mountState:SetText("Not riding. The skin goes on when you mount and comes off when you dismount.")
end
local chosen = MountPick()
if chosen then
P.BoxShowModel(ui.mountBox, chosen, MountName(chosen), C.gold, "Display ID " .. chosen .. " - for " .. target)
else
P.BoxShowModel(ui.mountBox, nil, "No skin", C.dim, "Your mount looks as usual.")
end
end
local function FilterMounts(keep)
local prefs = Prefs()
local count
ui.mountResult, count = M.FilterCatalog(M.GetCreatureCatalog(), ui.mountSearch:GetText(),
MOUNT_FILTERS[prefs.mountFilter or 1], nil, prefs.sortMounts)
ui.mountTotal = count
ui.mountCount:SetText(Count(count, "model"))
ui.mountList.SetCount(count, keep)
if not keep then
ShowMountPick()
end
RefreshMountState()
end
local function RefreshMounts()
ui.mountList.Refresh()
RefreshMountState()
end
-- The next (step 1) or previous (-1) model in the list as your skin.
local function StepMount(step)
local count = ui.mountTotal or 0
if count == 0 then
return SetStatus(false, "No models in the list: clear the search or pick another filter.")
end
local pos = MountPosition(MountPick())
if pos then
pos = Wrap(pos, step, count)
elseif step > 0 then
pos = 1
else
pos = count
end
PickMount(M.GetCreatureCatalog().ids[ui.mountResult[pos]])
ShowMountPick()
RefreshMountState()
end
local function MountForMenu()
local options = { { text = "All my mounts", checked = ui.mountFor == nil } }
local ridden = M.GetRiddenMounts()
if table.getn(ridden) > 0 then
table.insert(options, { text = "Mounts you've ridden", header = true })
end
for i = 1, table.getn(ridden) do
local skin = M.GetMountSkin(ridden[i])
local text = MountName(ridden[i])
if skin then
text = text .. " *"
end
table.insert(options, { text = text, real = ridden[i], checked = ui.mountFor == ridden[i] })
end
OpenMenu(this, options, function(option)
ui.mountFor = option.real
ShowMountPick()
RefreshMountState()
end)
end
local function MountFilterMenu()
local prefs, options = Prefs(), {}
for i = 1, table.getn(MOUNT_FILTERS) do
table.insert(options, { text = MOUNT_FILTERS[i].text, index = i, checked = (prefs.mountFilter or 1) == i })
end
OpenMenu(this, options, function(option)
Prefs().mountFilter = option.index
ui.mountFilter:SetText(option.text)
FilterMounts()
end)
end
local function BuildMountsPage(page)
local prefs = Prefs()
if not MOUNT_FILTERS[prefs.mountFilter] then
prefs.mountFilter = 1
end
Label(page, "Skin for", "GameFontNormal", 10, -12)
ui.mountForButton = Button(page, "All my mounts", 200, 70, -7, MountForMenu, "DmorphOctoMountFor")
ui.mountForButton:SetScript("OnEnter", function()
Tooltip(this, "Which mount gets the skin", "All your mounts, or one you've ridden on this character.",
"Ride a mount once and it appears in this menu.")
end)
ui.mountForButton:SetScript("OnLeave", HideTooltip)
Button(page, "Remove skin", 110, 276, -7, function()
ui.mountPick = { target = ui.mountFor }
Morph(M.SetMountSkin, 0, ui.mountFor)
end, "DmorphOctoMountReset")
ui.mountState = Label(page, "", "GameFontHighlightSmall", 10, -36, "DmorphOctoMountState")
ui.mountState:SetWidth(PAGE_W - 20)
Label(page, "Search", "GameFontNormalSmall", 10, -62)
ui.mountSearch = EditBox(page, "DmorphOctoMountSearch", 150, 52, -58)
ui.mountSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterMounts)
end)
ui.mountSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterMounts()
end)
ui.mountFilter = Button(page, MOUNT_FILTERS[prefs.mountFilter].text, 150, 210, -57, MountFilterMenu, "DmorphOctoMountFilter")
SortButton(page, "DmorphOctoMountSort", PAGE_W - 92 - 8, -57, "sortMounts", FilterMounts)
ui.mountList = List(page, "DmorphOctoMountList", 8, -84, 44, function(row, index)
local catalog = M.GetCreatureCatalog()
local i = ui.mountResult[index]
local color = KindColor(catalog.kind[i])
row.text:SetText(catalog.names[i])
row.text:SetTextColor(color[1], color[2], color[3])
row.info:SetText(catalog.ids[i])
if catalog.ids[i] == MountPick() then
-- The skin you have on: ticked, lit and gold.
row:LockHighlight()
row.tick:Show()
row.text:SetTextColor(C.gold[1], C.gold[2], C.gold[3])
end
if M.IsFavourite(catalog.ids[i], true) then
row.star:Show()
end
end, function(index, button)
local displayID = M.GetCreatureCatalog().ids[ui.mountResult[index]]
if button == "RightButton" then
SetStatus(M.ToggleFavourite(displayID, true))
ui.mountList.Refresh()
else
PickMount(displayID)
RefreshMounts()
end
end, function(index)
local catalog = M.GetCreatureCatalog()
local i = ui.mountResult[index]
if i then
P.BoxShowModel(ui.mountBox, catalog.ids[i], catalog.names[i], KindColor(catalog.kind[i]),
"Display ID " .. catalog.ids[i] .. " - click to use it")
end
end, LIST_W, 14)
ui.mountList.leave = RefreshMountState
ui.mountBox = P.NewBox(page, "Mounts", "model", -84, 14, "Your skin", StepMount)
ui.mountCount = Label(page, "", "GameFontDisableSmall", 10, -346, "DmorphOctoMountCount")
Label(page, "Click: use it as the skin. Right-click: star it. Previous / Next: the one above or below.",
"GameFontDisableSmall", 10, -362)
end
-- Glows tab ---------------------------------------------------------------------------
-- A weapon's enchant glow: any glow the client can draw (Data/Glows.lua), on
-- the main hand, off hand or ranged slot. Needs DmorphHelper.dll.
local glowSlot = 16
local function FilterGlows(keep)
local count
ui.glowResult, count = M.FilterGlows(ui.glowSearch:GetText())
ui.glowCount:SetText(Count(count, "glow"))
ui.glowList.SetCount(count, keep)
end
local function RefreshGlows()
for slot, button in pairs(ui.glowSlots) do
if slot == glowSlot then
button:LockHighlight()
else
button:UnlockHighlight()
end
end
local picked, real = M.GetGlow(glowSlot)
local text = "your own"
if picked then
text = M.DescribeGlow(picked)
elseif real and real > 0 then
text = "your own (" .. M.DescribeGlow(real) .. ")"
end
ui.glowState:SetText(M.SLOT_NAMES[glowSlot] .. ": " .. text)
if M.HasHelper() then
ui.glowState:SetTextColor(1, 1, 1)
else
ui.glowState:SetText("DmorphHelper.dll isn't loaded, so glows can't show: see /dmorph status.")
ui.glowState:SetTextColor(1, 0.4, 0.4)
end
if M.GetGlows().count == 0 then
ui.glowEmpty:Show()
else
ui.glowEmpty:Hide()
end
ui.glowList.Refresh()
ui.glowList.leave()
end
local function GlowTooltip(index, row)
local all = M.GetGlows()
local i = ui.glowResult[index]
if not i then
return
end
P.BoxText(ui.glowBox, all.name[i], C.accent, "Showing on you - click to keep it")
-- The glow goes on your character for a look (a preview model can't draw
-- enchants); it stays until another row, a click, or leaving the tab.
M.PreviewGlow(glowSlot, all.enchant[i])
Debounce(ui.glowTimer, 0.7, P.ReloadGlowBox)
-- On the left, off the preview box.
GameTooltip:SetOwner(row, "ANCHOR_LEFT")
GameTooltip:SetText(all.name[i])
local shown = 0
for name in string.gfind(all.names[i], "[^;]+") do
shown = shown + 1
if shown > 8 then
GameTooltip:AddLine("...", 0.8, 0.8, 0.8)
break
end
local _, _, trimmed = strfind(name, "^%s*(.-)%s*$")
GameTooltip:AddLine(trimmed, 1, 1, 1)
end
if all.models[i] ~= "" then
GameTooltip:AddLine("Effect: " .. all.models[i], 0.8, 0.8, 0.8)
end
if all.ported[i] then
GameTooltip:AddLine("From a later expansion: needs the ported pack to show.", 0.75, 0.55, 1)
end
GameTooltip:AddLine("Glow " .. all.visual[i] .. ", shown with enchant " .. all.enchant[i] .. ".", 0.6, 0.6, 0.6)
GameTooltip:AddLine("Click: on your " .. strlower(M.SLOT_NAMES[glowSlot]) .. ".", 0.6, 1, 0.6)
GameTooltip:Show()
end
-- The glow on the picked slot, in the box (a preview model can't draw an
-- enchant, so the box shows you and names it; the glow is on the stage).
-- The glow box's model again from your character (with the glow now on it),
-- and the dressing room too.
function P.ReloadGlowBox()
local box = ui.glowBox
box.model:SetFrameLevel(box.base + 1)
box.model:SetUnit("player")
box.wait, box.loaded, box.dressed = REVEAL, true, nil
RequestPreview()
end
-- Takes hovered glows off (leaving the tab, closing the window, a click).
function P.EndGlowPreview()
for i = 1, table.getn(M.GLOW_SLOTS) do
M.PreviewGlow(M.GLOW_SLOTS[i], nil)
end
end
function P.RefreshGlowBox()
local picked = M.GetGlow(glowSlot)
local where = "on your " .. strlower(M.SLOT_NAMES[glowSlot]) .. " - see it on the left"
if picked then
P.BoxShowItems(ui.glowBox, nil, M.DescribeGlow(picked), C.accent, where)
else
P.BoxShowItems(ui.glowBox, nil, "Your own glow", C.dim, where)
end
end
function P.StepGlow(step)
local all = M.GetGlows()
local n = table.getn(ui.glowResult or {})
local pos = P.StepPosition(P.FindPosition(ui.glowResult, n, all.enchant, M.GetGlow(glowSlot)), step, n)
if not pos then
return SetStatus(false, "No glows in the list.")
end
Morph(M.SetGlow, glowSlot, all.enchant[ui.glowResult[pos]])
ui.glowList.Show(pos)
end
local function BuildGlowsPage(page)
Label(page, "Weapon glows", "GameFontNormal", 10, -12)
for i = 1, table.getn(M.GLOW_SLOTS) do
local slot = M.GLOW_SLOTS[i]
local button = Button(page, M.SLOT_NAMES[slot], 96, 150 + (i - 1) * 100, -7, function()
P.EndGlowPreview()
glowSlot = this.slot
RefreshGlows()
end, "DmorphOctoGlowSlot" .. slot)
button.slot = slot
ui.glowSlots[slot] = button
end
Label(page, "Search", "GameFontNormalSmall", 10, -40)
ui.glowSearch = EditBox(page, "DmorphOctoGlowSearch", 150, 52, -36)
ui.glowSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterGlows)
end)
ui.glowSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterGlows()
end)
ui.glowCount = Label(page, "", "GameFontDisableSmall", 212, -40, "DmorphOctoGlowCount")
ui.glowList = List(page, "DmorphOctoGlowList", 8, -62, 70, function(row, index)
local all = M.GetGlows()
local i = ui.glowResult[index]
row.text:SetText(all.name[i])
if all.ported[i] then
row.info:SetText("TBC-MoP")
else
row.info:SetText("glow " .. all.visual[i])
end
if M.GetGlow(glowSlot) == all.enchant[i] then
row:LockHighlight()
row.tick:Show()
row.text:SetTextColor(C.gold[1], C.gold[2], C.gold[3])
end
end, function(index)
local all = M.GetGlows()
Morph(M.SetGlow, glowSlot, all.enchant[ui.glowResult[index]])
end, GlowTooltip, LIST_W, 14)
ui.glowList.leave = P.RefreshGlowBox
ui.glowBox = P.NewBox(page, "Glows", "dress", -62, 14, "Glow", P.StepGlow)
ui.glowEmpty = Label(page, "No glow list yet. Close the game, run tools\\update_from_game.ps1 from the "
.. "DmorphOcto folder (it reads every glow from the game files), and start the game again.",
"GameFontDisableSmall", 14, -72, "DmorphOctoGlowEmpty")
ui.glowEmpty:SetWidth(LIST_W - 12)
ui.glowState = Label(page, "", "GameFontHighlightSmall", 10, -324, "DmorphOctoGlowState")
Button(page, "No glow", 90, 8, -346, function()
Morph(M.SetGlow, glowSlot, 0)
end, "DmorphOctoGlowNone")
Button(page, "My own glow", 110, 102, -346, function()
Morph(M.SetGlow, glowSlot, nil)
end, "DmorphOctoGlowOwn")
Label(page, "DMorph users (2.23+) see it too.", "GameFontDisableSmall", 222, -351)
end
-- Items tab -------------------------------------------------------------------------
-- The three filter buttons of the Items and Sets tabs: the kind (cloth,
-- plate, dagger, ...), the quality and where the item comes from.
local function TypeText(kind)
return kind and M.ITEM_TYPE_NAMES[kind] or "Any type"
end
local function QualityText(quality)
if quality > 1 then
return M.QUALITY_FILTERS[quality].text
end
return "Any quality"
end
-- The kinds a catalogue has, with how many of each.
local function TypeMenu(anchor, catalog, current, pick)
local options = { { text = "Any type", checked = not current } }
local kinds = M.GetCatalogTypes(catalog)
for i = 1, table.getn(kinds) do
local k = kinds[i]
table.insert(options, { text = k[2] .. " (" .. Commas(k[3]) .. ")", kind = k[1], checked = current == k[1] })
end
OpenMenu(anchor, options, function(option)
pick(option.kind)
end)
end
local function QualityMenu(anchor, key, done)
local options = {}
for i = 1, table.getn(M.QUALITY_FILTERS) do
table.insert(options, { text = M.QUALITY_FILTERS[i].text, quality = i, checked = Prefs()[key] == i })
end
OpenMenu(anchor, options, function(option)
Prefs()[key] = option.quality
done()
end)
end
local function SourceMenu(anchor, filters, key, done)
local options = {}
for i = 1, table.getn(filters) do
table.insert(options, { text = filters[i].text, source = i, checked = Prefs()[key] == i })
end
OpenMenu(anchor, options, function(option)
Prefs()[key] = option.source
done()
end)
end
local function FilterItems(keep)
local prefs = Prefs()
local count
local kind = prefs.itemTypes[itemSlot]
ui.itemResult, count = M.FilterCatalog(M.GetItemCatalog(itemSlot), ui.itemSearch:GetText(),
M.ITEM_FILTERS[prefs.itemFilter], M.QUALITY_FILTERS[prefs.itemQuality].quality, prefs.sortItems, kind)
ui.itemTypeButton:SetText(TypeText(kind))
ui.itemQualityButton:SetText(QualityText(prefs.itemQuality))
ui.itemSourceButton:SetText(M.ITEM_FILTERS[prefs.itemFilter].text)
ui.itemSlotLabel:SetText(M.SLOT_NAMES[itemSlot])
local note = Count(count, "item")
if itemSlot >= 16 and itemSlot <= 18 then
note = note .. ", NPC weapons included"
end
ui.itemCount:SetText(note)
local _, empty = GetInventorySlotInfo(SLOT_KEYS[itemSlot])
ui.itemSlotIcon:SetTexture(empty)
ui.itemList.SetCount(count, keep)
end
local function RefreshItems()
ui.itemList.Refresh()
ui.itemList.leave()
end
local function ShowItemSlot(slot)
itemSlot = slot
ui.itemBox:SetText(M.GetState().items[slot] or "")
ShowTab("items")
end
local function CycleSlot(step)
local order = M.SLOT_ORDER
local count = table.getn(order)
for i = 1, count do
if order[i] == itemSlot then
ShowItemSlot(order[Wrap(i, step, count)])
return
end
end
end
local function SlotMenu()
local options = {}
for i = 1, table.getn(M.SLOT_ORDER) do
local slot = M.SLOT_ORDER[i]
table.insert(options, { text = M.SLOT_NAMES[slot], slot = slot, checked = slot == itemSlot })
end
OpenMenu(this, options, function(option)
ShowItemSlot(option.slot)
end)
end
-- One item on you in a box (kind "dress"): its name, and a line about it.
function P.ShowItemIn(box, itemID, slot, info)
if itemID == 0 then
P.BoxShowItems(box, nil, "Invisible", C.accent, info or (M.SLOT_NAMES[slot] .. " shows nothing"))
return
end
local catalog = M.GetItemCatalog(slot)
local index = catalog.byID and catalog.byID[itemID]
local r, g, b = QualityColor(index and catalog.quality[index])
-- The plain name (DescribeItem gives "[name] (id)", the id is on the line below).
local name = index and catalog.names[index] or M.DescribeItem(itemID, slot)
P.BoxShowItems(box, { itemID }, name, { r, g, b }, info or ("Item " .. itemID))
end
-- The slot's item you have on, else your own gear.
function P.RefreshItemBox()
local worn = M.GetState().items[itemSlot]
if worn then
P.ShowItemIn(ui.itemPreview, worn, itemSlot, (worn == 0 and "Shows nothing" or ("Item " .. worn)) .. " - you have it on")
else
P.BoxShowItems(ui.itemPreview, nil, "Your own gear", C.dim, "Hover an item to try it on.")
end
end
function P.StepItem(step)
local catalog = M.GetItemCatalog(itemSlot)
local pos = P.FindPosition(ui.itemResult, ui.itemList.count, catalog.ids, M.GetState().items[itemSlot])
pos = P.StepPosition(pos, step, ui.itemList.count)
if not pos then
return SetStatus(false, "No items in the list: clear the search or pick other filters.")
end
local itemID = catalog.ids[ui.itemResult[pos]]
ui.itemBox:SetText(itemID)
Morph(M.SetItem, itemSlot, itemID)
ui.itemList.Show(pos)
end
local function BuildItemsPage(page)
local prefs = Prefs()
-- The slot: click it for a menu of every slot.
local slotButton = CreateFrame("Button", "DmorphOctoItemSlotButton", page)
slotButton:SetWidth(230)
slotButton:SetHeight(30)
slotButton:SetPoint("TOPLEFT", page, "TOPLEFT", 6, -4)
Hover(slotButton)
slotButton:SetScript("OnClick", SlotMenu)
slotButton:SetScript("OnEnter", function()
Tooltip(this, "Slot", "Click to pick another slot.")
end)
slotButton:SetScript("OnLeave", HideTooltip)
ui.itemSlotIcon = slotButton:CreateTexture(nil, "ARTWORK")
ui.itemSlotIcon:SetWidth(26)
ui.itemSlotIcon:SetHeight(26)
ui.itemSlotIcon:SetPoint("TOPLEFT", slotButton, "TOPLEFT", 2, -2)
ui.itemSlotLabel = Label(slotButton, "", "GameFontNormal", 34, -3, "DmorphOctoItemSlotLabel")
ui.itemCount = Label(slotButton, "", "GameFontDisableSmall", 34, -18, "DmorphOctoItemCount")
ui.itemLookup = Label(page, "", "GameFontDisableSmall", 244, -14, "DmorphOctoItemLookup")
Button(page, "<", 24, PAGE_W - 60, -8, function()
CycleSlot(-1)
end)
Button(page, ">", 24, PAGE_W - 32, -8, function()
CycleSlot(1)
end)
Label(page, "Search", "GameFontNormalSmall", 10, -44)
ui.itemSearch = EditBox(page, "DmorphOctoItemSearch", 200, 52, -40)
ui.itemSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterItems)
end)
ui.itemSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterItems()
end)
SortButton(page, "DmorphOctoItemSort", PAGE_W - 92 - 8, -39, "sortItems", FilterItems)
-- Type, quality, source: e.g. Cloth + Epic for every epic cloth helmet.
ui.itemTypeButton = Button(page, "Any type", 170, 8, -66, function()
TypeMenu(this, M.GetItemCatalog(itemSlot), Prefs().itemTypes[itemSlot], function(kind)
Prefs().itemTypes[itemSlot] = kind
FilterItems()
end)
end, "DmorphOctoItemType")
ui.itemTypeButton:SetScript("OnEnter", function()
Tooltip(this, "Type", "Cloth, leather, mail or plate; or the kind of weapon.",
"Only the types this slot has are listed, with how many of each.")
end)
ui.itemTypeButton:SetScript("OnLeave", HideTooltip)
ui.itemQualityButton = Button(page, "Any quality", 170, 190, -66, function()
QualityMenu(this, "itemQuality", FilterItems)
end, "DmorphOctoItemQuality")
ui.itemSourceButton = Button(page, "All items", 170, 372, -66, function()
SourceMenu(this, M.ITEM_FILTERS, "itemFilter", FilterItems)
end, "DmorphOctoItemSource")
ui.itemSourceButton:SetScript("OnEnter", function()
Tooltip(this, "Which items", "All, 1.12's own, or the ones Turtle WoW and OctoWoW added.")
end)
ui.itemSourceButton:SetScript("OnLeave", HideTooltip)
ui.itemList = List(page, "DmorphOctoItemList", 8, -94, 52, function(row, index)
local catalog = M.GetItemCatalog(itemSlot)
local i = ui.itemResult[index]
row.text:SetText(catalog.names[i])
row.text:SetTextColor(QualityColor(catalog.quality[i]))
row.info:SetText(catalog.ids[i])
if catalog.ids[i] == M.GetState().items[itemSlot] then
row:LockHighlight()
row.tick:Show()
end
end, function(index)
local itemID = M.GetItemCatalog(itemSlot).ids[ui.itemResult[index]]
ui.itemBox:SetText(itemID)
Morph(M.SetItem, itemSlot, itemID)
end, function(index)
-- Hover: your character wearing it, without putting it on.
local catalog = M.GetItemCatalog(itemSlot)
local i = ui.itemResult[index]
if i then
local r, g, b = QualityColor(catalog.quality[i])
P.BoxShowItems(ui.itemPreview, { catalog.ids[i] }, catalog.names[i], { r, g, b },
"Item " .. catalog.ids[i] .. " - click to wear it")
end
end, LIST_W, 13)
ui.itemList.leave = P.RefreshItemBox
ui.itemPreview = P.NewBox(page, "Items", "dress", -94, 13, "Try on", P.StepItem)
local y = -346
Label(page, "Item ID", "GameFontNormalSmall", 10, y - 5)
ui.itemBox = EditBox(page, "DmorphOctoItemID", 70, 56, y, 9)
local function Apply()
ui.itemBox:ClearFocus()
Morph(M.SetItem, itemSlot, M.ParseID(ui.itemBox:GetText()) or -1)
end
ui.itemBox:SetScript("OnEnterPressed", Apply)
Button(page, "Apply", 64, 132, y, Apply)
Button(page, "Use my own gear", 124, 202, y, function()
ui.itemBox:SetText("")
Morph(M.SetItem, itemSlot, 0)
end)
-- Nothing in this slot: the piece is invisible (armour slots only).
ui.itemHideButton = Button(page, "Invisible", 90, 332, y, function()
ui.itemBox:SetText("")
Morph(M.HideItem, itemSlot)
end, "DmorphOctoItemHide")
ui.itemHideButton:SetScript("OnEnter", function()
Tooltip(this, "Invisible", "Shows nothing in this slot: no piece and none of your own gear.",
"Use my own gear brings yours back.")
end)
ui.itemHideButton:SetScript("OnLeave", HideTooltip)
end
-- Slutmog tab (user, 18 Oct: "its own tab"): every revealing piece and OctoWoW's
-- invisible Unseen Fashion items, all slots in one list; buttons narrow it to a slot.
local SLUTMOG_SHOW = {
{ text = "All" }, { text = "Chest", slot = 5 }, { text = "Shirt", slot = 4 },
{ text = "Legs", slot = 7 }, { text = "Feet", slot = 8 }, { text = "Unseen", unseen = true },
}
local slutmogShow = 1
local function FilterSlutmog(keep)
local catalog = M.GetSlutmogCatalog()
local found, n = M.FilterCatalog(catalog, ui.slutSearch:GetText())
local show = SLUTMOG_SHOW[slutmogShow]
local result, m = {}, 0
for k = 1, n do
local i = found[k]
if (not show.slot or catalog.slots[i] == show.slot)
and (not show.unseen or strfind(catalog.lower[i], "unseen fashion", 1, true)) then
m = m + 1
result[m] = i
end
end
ui.slutResult = result
for i = 1, table.getn(SLUTMOG_SHOW) do
if i == slutmogShow then
ui.slutButtons[i]:LockHighlight()
else
ui.slutButtons[i]:UnlockHighlight()
end
end
local note = Count(m, "piece")
if show.unseen and m == 0 then
note = "none yet: OctoWoW's Unseen Fashion items show once the server has sent them"
end
ui.slutCount:SetText(note)
ui.slutList.SetCount(m, keep)
end
local function RefreshSlutmog()
ui.slutList.Refresh()
ui.slutList.leave()
end
-- The piece of the list you have on: the one stepped to or clicked last if
-- still on, else the first one on.
function P.WornSlutmog()
local catalog, items = M.GetSlutmogCatalog(), M.GetState().items
local result, n = ui.slutResult or {}, ui.slutList.count
local function Worn(k)
local i = result[k]
return i and items[catalog.slots[i]] == catalog.ids[i]
end
if ui.slutPos and Worn(ui.slutPos) then
return ui.slutPos
end
for k = 1, n do
if Worn(k) then
return k
end
end
end
function P.ShowSlutmog(k, info)
local catalog = M.GetSlutmogCatalog()
local i = ui.slutResult[k]
local r, g, b = QualityColor(catalog.quality[i])
P.BoxShowItems(ui.slutBox, { catalog.ids[i] }, catalog.names[i], { r, g, b },
M.SLOT_NAMES[catalog.slots[i]] .. ", item " .. catalog.ids[i] .. info)
end
function P.RefreshSlutBox()
local k = P.WornSlutmog()
if k then
P.ShowSlutmog(k, " - you have it on")
else
P.BoxShowItems(ui.slutBox, nil, "Your look", C.dim, "Hover a piece to try it on.")
end
end
function P.WearSlutmog(k)
local catalog = M.GetSlutmogCatalog()
local i = ui.slutResult[k]
ui.slutPos = k
Morph(M.SetItem, catalog.slots[i], catalog.ids[i])
end
function P.StepSlutmog(step)
local pos = P.StepPosition(P.WornSlutmog() or ui.slutPos, step, ui.slutList.count)
if not pos then
return SetStatus(false, "No pieces in the list: clear the search or pick another slot.")
end
P.WearSlutmog(pos)
ui.slutList.Show(pos)
end
local function BuildSlutmogPage(page)
ui.slutButtons = {}
for i = 1, table.getn(SLUTMOG_SHOW) do
local index = i
ui.slutButtons[i] = Button(page, SLUTMOG_SHOW[i].text, 84, 8 + (i - 1) * 88, -8, function()
slutmogShow = index
FilterSlutmog()
end, "DmorphOctoSlutmogShow" .. i)
end
Label(page, "Search", "GameFontNormalSmall", 10, -42)
ui.slutSearch = EditBox(page, "DmorphOctoSlutmogSearch", 200, 52, -38)
ui.slutSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterSlutmog)
end)
ui.slutSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterSlutmog()
end)
ui.slutCount = Label(page, "", "GameFontDisableSmall", 262, -42, "DmorphOctoSlutmogCount")
ui.slutList = List(page, "DmorphOctoSlutmogList", 8, -66, 60, function(row, index)
local catalog = M.GetSlutmogCatalog()
local i = ui.slutResult[index]
row.text:SetText(catalog.names[i])
row.text:SetTextColor(QualityColor(catalog.quality[i]))
row.info:SetText(M.SLOT_NAMES[catalog.slots[i]])
if catalog.ids[i] == M.GetState().items[catalog.slots[i]] then
row:LockHighlight()
row.tick:Show()
end
end, P.WearSlutmog, function(index)
if ui.slutResult[index] then
P.ShowSlutmog(index, " - click to wear it")
end
end, LIST_W, 15)
ui.slutList.leave = P.RefreshSlutBox
ui.slutBox = P.NewBox(page, "Slutmog", "dress", -66, 15, "Try on", P.StepSlutmog)
local y = -346
Button(page, "Birthday suit", 120, 8, y, function()
Morph(M.BirthdaySuit)
end, "DmorphOctoSlutmogBirthday")
Label(page, "Revealing pieces, by how much body they cover.", "GameFontDisableSmall", 136, y - 6)
end
-- Sets tab --------------------------------------------------------------------------
local function FilterSets(keep)
local prefs = Prefs()
local count
ui.setResult, count = M.FilterCatalog(M.GetSetCatalog(), ui.setSearch:GetText(),
M.SET_FILTERS[prefs.setFilter], M.QUALITY_FILTERS[prefs.setQuality].quality, prefs.sortSets, prefs.setType)
ui.setTypeButton:SetText(TypeText(prefs.setType))
ui.setQualityButton:SetText(QualityText(prefs.setQuality))
ui.setSourceButton:SetText(M.SET_FILTERS[prefs.setFilter].text)
ui.setCount:SetText(Count(count, "set"))
ui.setList.SetCount(count, keep)
end
local function RefreshSetGear()
if ui.setGearButton then
ui.setGearButton:SetText(M.GetHideGearUnderSets() and "Own gear: hidden" or "Own gear: shown")
end
end
function P.ShowSet(i, info)
local catalog = M.GetSetCatalog()
local r, g, b = QualityColor(catalog.quality[i])
P.BoxShowItems(ui.setBox, catalog.pieces[i], catalog.names[i], { r, g, b },
Count(table.getn(catalog.pieces[i]), "piece") .. info)
end
-- The set you put on last, else your look.
function P.RefreshSetBox()
if ui.selectedSet then
P.ShowSet(ui.selectedSet, " - you have it on")
else
P.BoxShowItems(ui.setBox, nil, "Your look", C.dim, "Hover a set to try it on.")
end
end
local function RefreshSets()
RefreshSetGear()
ui.setList.Refresh()
P.RefreshSetBox()
end
local function SetTooltip(index, row)
local catalog = M.GetSetCatalog()
local i = ui.setResult[index]
if not i then
return
end
GameTooltip:SetOwner(row, "ANCHOR_LEFT")
GameTooltip:SetText(catalog.names[i], QualityColor(catalog.quality[i]))
local pieces = catalog.pieces[i]
for k = 1, table.getn(pieces) do
local slots = M.GetItemSlots(pieces[k])
if slots then
GameTooltip:AddLine(M.SLOT_NAMES[slots[1]] .. ": " .. M.DescribeItem(pieces[k], slots[1]), 1, 1, 1)
else
GameTooltip:AddLine("Item " .. pieces[k] .. ": still being looked up", 0.6, 0.6, 0.6)
end
end
GameTooltip:AddLine("Click to wear every piece.", 0.6, 1, 0.6)
GameTooltip:Show()
-- And your character wearing the whole set.
P.ShowSet(i, " - click to wear them")
end
function P.WearSetAt(pos)
ui.selectedSet = ui.setResult[pos]
ui.setList.Refresh()
Morph(M.WearSet, ui.selectedSet)
P.RefreshSetBox()
end
function P.StepSet(step)
local pos
for k = 1, ui.setList.count do
if ui.setResult[k] == ui.selectedSet then
pos = k
end
end
pos = P.StepPosition(pos, step, ui.setList.count)
if not pos then
return SetStatus(false, "No sets in the list: clear the search or pick other filters.")
end
P.WearSetAt(pos)
ui.setList.Show(pos)
end
local function BuildSetsPage(page)
Label(page, "Search", "GameFontNormalSmall", 10, -12)
ui.setSearch = EditBox(page, "DmorphOctoSetSearch", 200, 52, -8)
ui.setSearch:SetScript("OnTextChanged", function()
Debounce(ui.searchTimer, 0.25, FilterSets)
end)
ui.setSearch:SetScript("OnEnterPressed", function()
this:ClearFocus()
FilterSets()
end)
SortButton(page, "DmorphOctoSetSort", PAGE_W - 92 - 8, -7, "sortSets", FilterSets)
ui.setTypeButton = Button(page, "Any type", 170, 8, -34, function()
TypeMenu(this, M.GetSetCatalog(), Prefs().setType, function(kind)
Prefs().setType = kind
FilterSets()
end)
end, "DmorphOctoSetType")
ui.setTypeButton:SetScript("OnEnter", function()
Tooltip(this, "Type", "Cloth, leather, mail or plate: what most of the set's armour is made of.")
end)
ui.setTypeButton:SetScript("OnLeave", HideTooltip)
ui.setQualityButton = Button(page, "Any quality", 170, 190, -34, function()
QualityMenu(this, "setQuality", FilterSets)
end, "DmorphOctoSetQuality")
ui.setSourceButton = Button(page, "All sets", 170, 372, -34, function()
SourceMenu(this, M.SET_FILTERS, "setFilter", FilterSets)
end, "DmorphOctoSetSource")
ui.setList = List(page, "DmorphOctoSetList", 8, -62, 70, function(row, index)
local catalog = M.GetSetCatalog()
local i = ui.setResult[index]
row.text:SetText(catalog.names[i])
row.text:SetTextColor(QualityColor(catalog.quality[i]))
row.info:SetText(Count(table.getn(catalog.pieces[i]), "piece"))
if i == ui.selectedSet then
row:LockHighlight()
row.tick:Show()
end
end, P.WearSetAt, SetTooltip, LIST_W, 14)
ui.setList.leave = P.RefreshSetBox
ui.setBox = P.NewBox(page, "Sets", "dress", -62, 14, "Try on", P.StepSet)
ui.setCount = Label(page, "", "GameFontDisableSmall", 10, -322, "DmorphOctoSetCount")
ui.setLookup = Label(page, "", "GameFontDisableSmall", 110, -322, "DmorphOctoSetLookup")
-- Your own gear in the slots a set has no piece for: hidden (the default) or shown.
ui.setGearButton = Button(page, "", 140, 8, -346, function()
Morph(M.SetHideGearUnderSets, not M.GetHideGearUnderSets())
RefreshSetGear()
end, "DmorphOctoSetGear")
ui.setGearButton:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("Your own gear under sets")
GameTooltip:AddLine("Hidden: the slots a set has no piece for show nothing,", 1, 1, 1)
GameTooltip:AddLine("so your real gear doesn't show through the set.", 1, 1, 1)
GameTooltip:AddLine("Shown: those slots keep your own gear.", 1, 1, 1)
GameTooltip:AddLine("Click to switch. It applies from the next set you wear.", 0.6, 1, 0.6)
GameTooltip:Show()
end)
ui.setGearButton:SetScript("OnLeave", HideTooltip)
RefreshSetGear()
Label(page, "Click a set to wear every piece. Hover: the pieces.", "GameFontDisableSmall", 156, -351)
end
-- Wardrobe tab ------------------------------------------------------------------------
-- An outfit's items on your character (a creature or race look can't be
-- tried on, so the line under it says which).
function P.ShowOutfit(name, info)
local outfit = M.GetOutfit(name)
if not outfit then
return
end
local items = {}
for _, id in pairs(outfit.items) do
if id ~= 0 then
table.insert(items, id)
end
end
P.BoxShowItems(ui.outfitBox, items, name, C.gold, M.DescribeOutfit(outfit) .. info)
end
function P.RefreshOutfitBox()
if selectedOutfit and M.GetOutfit(selectedOutfit) then
P.ShowOutfit(selectedOutfit, "")
else
P.BoxShowItems(ui.outfitBox, nil, "No outfit picked", C.dim, "Hover an outfit to try it on.")
end
end
local function RefreshWardrobe()
ui.outfitNames = M.GetOutfitNames()
local count = table.getn(ui.outfitNames)
if count == ui.outfitList.count then
ui.outfitList.Refresh()
else
ui.outfitList.SetCount(count)
end
if count == 0 then
ui.outfitEmpty:Show()
else
ui.outfitEmpty:Hide()
end
P.RefreshOutfitBox()
end
local function LoadSelected()
if not selectedOutfit then
return SetStatus(false, "Pick an outfit in the list first.")
end
Morph(M.LoadOutfit, selectedOutfit)
end
function P.StepOutfit(step)
local names = ui.outfitNames or {}
local pos
for k = 1, table.getn(names) do
if names[k] == selectedOutfit then
pos = k
end
end
pos = P.StepPosition(pos, step, table.getn(names))
if not pos then
return SetStatus(false, "No outfits yet: build a look, type a name and press Save.")
end
selectedOutfit = names[pos]
ui.outfitName:SetText(selectedOutfit)
ui.outfitList.Show(pos)
LoadSelected()
end
local function BuildWardrobePage(page)
Label(page, "Wardrobe", "GameFontNormal", 10, -12)
Label(page, "Outfits are shared by all your characters.", "GameFontDisableSmall", 86, -14)
Label(page, "Name", "GameFontNormalSmall", 10, -42)
ui.outfitName = EditBox(page, "DmorphOctoOutfitName", 200, 52, -38, 32)
local function Save()
ui.outfitName:ClearFocus()
local ok, message = M.SaveOutfit(ui.outfitName:GetText())
if ok then
selectedOutfit = M.FindOutfit(ui.outfitName:GetText())
end
SetStatus(ok, message)
end
ui.outfitName:SetScript("OnEnterPressed", Save)
Button(page, "Save current look", 140, 260, -37, Save)
ui.outfitList = List(page, "DmorphOctoOutfitList", 8, -66, 150, function(row, index)
local name = ui.outfitNames[index]
row.text:SetText(name)
row.info:SetText(M.DescribeOutfit(M.GetOutfit(name)))
if name == selectedOutfit then
row:LockHighlight()
row.tick:Show()
end
end, function(index)
local name = ui.outfitNames[index]
if name == selectedOutfit then
-- A second click on the chosen outfit puts it on.
return LoadSelected()
end
selectedOutfit = name
ui.outfitName:SetText(name)
ui.outfitList.Refresh()
P.RefreshOutfitBox()
SetStatus(true, "Click \"" .. name .. "\" again, or press Load, to put it on.")
end, function(index)
local name = ui.outfitNames[index]
if name then
P.ShowOutfit(name, " - click twice to put it on")
end
end, LIST_W, 15)
ui.outfitList.leave = P.RefreshOutfitBox
ui.outfitBox = P.NewBox(page, "Wardrobe", "dress", -66, 15, "Try on", P.StepOutfit)
ui.outfitEmpty = Label(page, "No outfits yet. Build a look, type a name above and press Save.",
"GameFontDisableSmall", 14, -74)
ui.outfitEmpty:SetWidth(LIST_W - 12)
local y = -346
Button(page, "Load", 90, 8, y, LoadSelected)
Button(page, "Delete", 90, 102, y, function()
if not selectedOutfit then
return SetStatus(false, "Pick an outfit in the list first.")
end
pendingDelete = selectedOutfit
StaticPopup_Show("DMORPHOCTO_DELETE_OUTFIT", selectedOutfit)
end)
end
StaticPopupDialogs["DMORPHOCTO_DELETE_OUTFIT"] = {
text = "Delete the outfit \"%s\"?",
button1 = TEXT(YES),
button2 = TEXT(NO),
OnAccept = function()
if pendingDelete == selectedOutfit then
selectedOutfit = nil
end
SetStatus(M.DeleteOutfit(pendingDelete))
pendingDelete = nil
end,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
}
-- Setup reminder (user, 21 Oct: "if someone has an outdated version or hasn't
-- run the DmorphHelper installer, a pop-up when logging in or reloading"): once
-- per login or /reload, a few seconds in, when VanillaHelpers isn't running or
-- DmorphHelper is missing or older than this DMorph's. "Don't remind me" holds
-- until DMorph comes with a newer DmorphHelper.
local SETUP_NAG_DELAY = 5
function P.SetupProblem()
if not M.HasMorphAPI() then
return "DMorph can't morph anything: VanillaHelpers isn't running.\n\n" .. M.MissingAPI()
end
local running, wanted = M.GetHelperVersion()
if not running then
return "DmorphHelper isn't running, so weapon glows, size, your real skin, face and hair, weather and "
.. "keeping your own faction during race morphs don't work.\n\nClose the game, double-click "
.. "\"Install DmorphHelper.bat\" in Interface\\AddOns\\DmorphOcto, then start the game from OctoLauncher."
end
if running < wanted then
return "Your DmorphHelper is out of date: v" .. running .. " is running, this DMorph needs v" .. wanted
.. ".\n\nClose the game, double-click \"Install DmorphHelper.bat\" in Interface\\AddOns\\DmorphOcto, "
.. "then start the game from OctoLauncher."
end
end
StaticPopupDialogs["DMORPHOCTO_SETUP"] = {
text = "DMorph: %s",
button1 = "OK",
button2 = "Don't remind me",
-- (No hideOnEscape: Escape would count as "Don't remind me".)
OnCancel = function()
local _, wanted = M.GetHelperVersion()
DmorphOctoAccount.setupNagOff = wanted
end,
timeout = 0,
whileDead = 1,
}
local setupNag = CreateFrame("Frame")
setupNag:RegisterEvent("PLAYER_ENTERING_WORLD")
setupNag:SetScript("OnEvent", function()
this:UnregisterEvent("PLAYER_ENTERING_WORLD") -- once per login or /reload
this.wait = SETUP_NAG_DELAY
end)
setupNag:SetScript("OnUpdate", function()
if not this.wait then
return
end
this.wait = this.wait - arg1
if this.wait > 0 then
return
end
this.wait = nil
local _, wanted = M.GetHelperVersion()
if DmorphOctoAccount and DmorphOctoAccount.setupNagOff == wanted and M.HasMorphAPI() then
return
end
local problem = P.SetupProblem()
if problem then
StaticPopup_Show("DMORPHOCTO_SETUP", problem)
end
end)
-- Weather tab ---------------------------------------------------------------------
-- A button per weather (M.WEATHERS): Clear skies on its own row, then light,
-- normal and heavy rain, snow and sandstorm, with a slider for the strength
-- of the one picked. Below, the time of day (M.TIMES and a slider).
local WEATHER_ROW_NAMES = { "Rain", "Snow", "Sandstorm" }
local function Highlight(button, on)
if on then
button:LockHighlight()
else
button:UnlockHighlight()
end
end
local function RefreshWeather()
local kind, intensity, serverKind, serverIntensity = M.GetWeather()
for i = 1, table.getn(M.WEATHERS) do
local w = M.WEATHERS[i]
Highlight(ui.weatherButtons[w.key], kind == w.kind and (w.kind == 0 or math.abs(intensity - w.intensity) < 0.001))
end
local minutes, realMinutes = M.GetTime()
for i = 1, table.getn(M.TIMES) do
local t = M.TIMES[i]
Highlight(ui.timeButtons[t.key], minutes == t.minutes)
end
ui.weatherSync = true
ui.weatherSlider:SetValue(kind and kind > 0 and intensity or 0.55)
ui.timeSlider:SetValue(minutes or realMinutes or 720)
ui.weatherSync = nil
local weather = "Weather: the server's"
if kind then
weather = "Weather: |cff33ff99" .. M.DescribeWeather(kind, intensity) .. "|r (yours)"
end
if serverKind then
weather = weather .. "; the server's is " .. M.DescribeWeather(serverKind, serverIntensity)
end
local time = "Time: the real time"
if minutes then
time = "Time: |cff33ff99" .. M.DescribeTime(minutes) .. "|r (yours)"
end
if realMinutes then
time = time .. "; the real time is " .. M.DescribeTime(realMinutes)
end
ui.weatherState:SetText(weather .. ".\n" .. time .. ".")
if M.HasWeather() and M.HasTime() then
ui.weatherMissing:Hide()
else
ui.weatherMissing:Show()
end
end
-- A slider like the size slider; onStill runs once it stands still.
local function WeatherSlider(page, name, y, min, max, step, onChange, onStill)
local slider = CreateFrame("Slider", name, page)
slider:SetOrientation("HORIZONTAL")
slider:SetWidth(PAGE_W - 130)
slider:SetHeight(8)
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 110, y - 4)
Flat(slider, C.field, C.line)
slider:SetThumbTexture(WHITE)
local thumb = slider:GetThumbTexture()
if thumb then
thumb:SetWidth(10)
thumb:SetHeight(14)
thumb:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
end
slider:SetMinMaxValues(min, max)
slider:SetValueStep(step)
slider.timer = NewTimer()
slider:SetScript("OnValueChanged", function()
local value = math.floor(this:GetValue() / step + 0.5) * step
onChange(value)
if ui.weatherSync then
return
end
Debounce(this.timer, 0.3, function()
onStill(value)
end)
end)
slider:SetScript("OnLeave", HideTooltip)
return slider
end
local function BuildWeatherPage(page)
Label(page, "Weather and time", "GameFontNormal", 10, -12)
Label(page, "Only on your screen; it stays on in every zone.", "GameFontDisableSmall", 136, -15)
ui.weatherButtons = {}
local function Pick()
Morph(M.SetWeather, this.weatherKind, this.intensity)
end
for i = 1, table.getn(M.WEATHERS) do
local w = M.WEATHERS[i]
local x, y = 110, -36
if w.kind > 0 then
-- Light, normal, heavy in columns; one row per kind.
x = 110 + math.mod(i - 2, 3) * 118
y = -64 - (w.kind - 1) * 28
end
local button = Button(page, w.text, 112, x, y, Pick, "DmorphOctoWeather" .. w.key)
button.weatherKind, button.intensity = w.kind, w.intensity
ui.weatherButtons[w.key] = button
end
Label(page, "Sky", "GameFontHighlight", 14, -40)
for i = 1, 3 do
Label(page, WEATHER_ROW_NAMES[i], "GameFontHighlight", 14, -68 - (i - 1) * 28)
end
ui.weatherStrength = Label(page, "Strength", "GameFontHighlight", 14, -155, "DmorphOctoWeatherStrengthLabel")
ui.weatherSlider = WeatherSlider(page, "DmorphOctoWeatherStrength", -154, 0.05, 1, 0.05, function(value)
ui.weatherStrength:SetText("Strength " .. math.floor(value * 100 + 0.5) .. "%")
end, function(value)
local kind = M.GetWeather()
if kind and kind > 0 then
SetStatus(M.SetWeather(kind, value))
else
SetStatus(false, "Pick rain, snow or a sandstorm first; the slider sets how strong it is.")
RefreshWeather()
end
end)
ui.weatherSlider:SetScript("OnEnter", function()
Tooltip(this, "Strength", "How strong the rain, snow or sandstorm you picked is.",
"Its sound changes with it: light below 40%, heavy from 70%.")
end)
-- Time of day: the world's light, sky, sun and moon.
Label(page, "Time of day", "GameFontHighlight", 14, -196)
ui.timeButtons = {}
for i = 1, table.getn(M.TIMES) do
local t = M.TIMES[i]
local button = Button(page, t.text, 84, 110 + (i - 1) * 88, -192, function()
Morph(M.SetTime, this.minutes)
end, "DmorphOctoTime" .. t.key)
button.minutes = t.minutes
ui.timeButtons[t.key] = button
end
ui.timeLabel = Label(page, "Time", "GameFontHighlight", 14, -227, "DmorphOctoTimeLabel")
ui.timeSlider = WeatherSlider(page, "DmorphOctoTimeSlider", -226, 0, 1425, 15, function(value)
ui.timeLabel:SetText("Time " .. M.DescribeTime(value))
end, function(value)
SetStatus(M.SetTime(value))
end)
ui.timeSlider:SetScript("OnEnter", function()
Tooltip(this, "Time of day", "Drag to light the world as at that time: day, dusk, night.",
"Your clock and everyone else keep the real time.")
end)
ui.weatherState = Label(page, "", "GameFontHighlightSmall", 10, -258, "DmorphOctoWeatherState")
ui.weatherState:SetWidth(PAGE_W - 20)
ui.weatherMissing = Label(page, "Weather and time need DmorphHelper v9 or newer: close the game and "
.. "double-click \"Install DmorphHelper.bat\" in the DmorphOcto folder.", "GameFontRedSmall", 10, -290,
"DmorphOctoWeatherMissing")
ui.weatherMissing:SetWidth(PAGE_W - 20)
Label(page, "Weather shows outdoors only. Stormy skies come with rain or snow where the zone has them.",
"GameFontDisableSmall", 10, -322)
Button(page, "Server's weather", 140, 8, -352, function()
Morph(M.SetWeather, nil)
end, "DmorphOctoWeatherServer")
Button(page, "Real time", 100, 152, -352, function()
Morph(M.SetTime, nil)
end, "DmorphOctoTimeReal")
Label(page, ".weather rain heavy and .time 22:00 in chat.", "GameFontDisableSmall", 258, -357)
end
local REFRESH = {
weather = RefreshWeather,
models = RefreshModels,
mounts = RefreshMounts,
favourites = RefreshFavourites,
races = RefreshRaces,
glows = RefreshGlows,
items = RefreshItems,
slutmog = RefreshSlutmog,
sets = RefreshSets,
wardrobe = RefreshWardrobe,
}
local function RefreshAll()
if not (frame and frame:IsVisible()) then
return
end
UpdateSlots()
UpdatePlate()
ShowArt()
REFRESH[currentTab]()
RequestPreview()
end
-- Once a second: slot icons (1.12 has no event for item info arriving) and
-- items the background lookup has found.
local function Poll()
UpdateSlots()
local version = M.GetItemsVersion()
if version ~= ui.itemsVersion then
ui.itemsVersion = version
if ui.itemResult and currentTab == "items" then
FilterItems(true)
end
if ui.setResult and currentTab == "sets" then
FilterSets(true)
end
if ui.slutResult and currentTab == "slutmog" then
FilterSlutmog(true)
end
end
local left = M.GetLookupStatus()
local text = ""
if left > 0 then
text = "Looking up " .. Count(left, "OctoWoW item") .. "..."
end
ui.itemLookup:SetText(text)
ui.setLookup:SetText(text)
end
local function TabLook(tab, active)
-- The open tab is filled purple, the others plain.
if active then
tab:SetBackdropColor(C.deep[1], C.deep[2], C.deep[3], 1)
tab:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 1)
tab.label:SetTextColor(1, 1, 1)
else
tab:SetBackdropColor(C.btn[1], C.btn[2], C.btn[3], 1)
tab:SetBackdropBorderColor(C.line[1], C.line[2], C.line[3], 1)
tab.label:SetTextColor(C.dim[1], C.dim[2], C.dim[3])
end
end
ShowTab = function(key)
if key ~= "glows" then
P.EndGlowPreview()
end
currentTab = key
ui.menu:Hide()
for i = 1, table.getn(TABS) do
local tab = TABS[i]
if tab.key == key then
ui.pages[tab.key]:Show()
else
ui.pages[tab.key]:Hide()
end
TabLook(ui.tabs[tab.key], tab.key == key)
end
if key == "models" and not ui.modelResult then
FilterModels()
elseif key == "items" then
FilterItems()
elseif key == "slutmog" then
FilterSlutmog(ui.slutResult ~= nil)
elseif key == "sets" and not ui.setResult then
FilterSets()
elseif key == "glows" then
FilterGlows(ui.glowResult ~= nil)
elseif key == "favourites" then
FilterFavourites()
elseif key == "mounts" and not ui.mountResult then
FilterMounts()
end
UpdateSlots()
REFRESH[key]()
end
-- Window ------------------------------------------------------------------------------
local function BuildStage()
-- The dressing room: backdrop art, your character, the slots around it.
local stage = CreateFrame("Frame", "DmorphOctoStage", frame)
stage:SetWidth(ART_W)
stage:SetHeight(ART_H)
stage:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -44)
local base = stage:GetFrameLevel()
-- Levels: model loading (+1) < art (+2) < model shown (+3) < drag area (+4)
-- < name plate (+5) < slots (+6) < border (+7).
local art = Layer(stage, 2)
art:SetAllPoints(stage)
local black = art:CreateTexture(nil, "BACKGROUND")
black:SetTexture(0, 0, 0, 1)
black:SetAllPoints(art)
ui.art = {}
local sizes = { { 256, 255 }, { 62, 255 }, { 256, 128 }, { 62, 128 } }
for i = 1, 4 do
local texture = art:CreateTexture("DmorphOctoArt" .. i, "ARTWORK")
texture:SetWidth(sizes[i][1])
texture:SetHeight(sizes[i][2])
ui.art[i] = texture
end
ui.art[1]:SetPoint("TOPLEFT", art, "TOPLEFT", 0, 0)
ui.art[2]:SetPoint("TOPLEFT", ui.art[1], "TOPRIGHT", 0, 0)
ui.art[3]:SetPoint("TOPLEFT", ui.art[1], "BOTTOMLEFT", 0, 0)
ui.art[4]:SetPoint("TOPLEFT", ui.art[1], "BOTTOMRIGHT", 0, 0)
local p = { models = {}, front = 1, rotation = 0.61, low = base + 1, high = base + 3, zoom = 0, panX = 0, panY = 0 }
for i = 1, 2 do
local model = CreateFrame("PlayerModel", "DmorphOctoModel" .. i, stage)
model:SetPoint("TOPLEFT", stage, "TOPLEFT", 0, -4)
model:SetPoint("BOTTOMRIGHT", stage, "BOTTOMRIGHT", 0, PLATE_H)
model:SetFrameLevel(p.low)
model:Hide()
p.models[i] = model
end
ui.preview = p
-- Creature morphs show here instead (P.StageCreature).
ui.stageView = NewView(stage, "DmorphOctoStageModel", base + 1, base + 3, ART_W / (ART_H - 4 - PLATE_H))
ui.stageView.model:SetPoint("TOPLEFT", stage, "TOPLEFT", 0, -4)
ui.stageView.model:SetPoint("BOTTOMRIGHT", stage, "BOTTOMRIGHT", 0, PLATE_H)
ui.stageView.model:Hide()
-- Over the model: drag to turn it, right-drag to move it, mouse wheel to
-- zoom in and out.
local drag = CreateFrame("Frame", "DmorphOctoModelDrag", stage)
drag:SetFrameLevel(base + 4)
drag:SetAllPoints(p.models[1])
drag:EnableMouse(true)
drag:EnableMouseWheel(true)
drag:SetScript("OnMouseDown", function()
local x, y = GetCursorPosition()
if arg1 == "RightButton" then
this.panX, this.panY = x, y
else
this.dragX = x
end
end)
drag:SetScript("OnMouseUp", function()
this.dragX, this.panX, this.panY = nil, nil, nil
end)
drag:SetScript("OnUpdate", function()
if this.dragX then
local x = GetCursorPosition()
p.rotation = p.rotation + (x - this.dragX) * 0.02
this.dragX = x
p.models[p.front]:SetRotation(p.rotation)
ui.stageView.facing = p.rotation
elseif this.panX then
-- The farther in, the smaller a step: the model follows the cursor.
local x, y = GetCursorPosition()
local step = 0.006 / (frame:GetEffectiveScale() * (1 + math.max(p.zoom, 0) * 0.45))
p.panX = p.panX + (x - this.panX) * step
p.panY = p.panY + (y - this.panY) * step
this.panX, this.panY = x, y
ApplyView(p.models[p.front])
end
end)
drag:SetScript("OnMouseWheel", function()
Zoom(arg1)
end)
local function ZoomButton(text, width, x, name, onClick, line)
local button = Button(stage, text, width, x, -10, onClick, name)
button:SetFrameLevel(base + 6)
button:SetScript("OnEnter", function()
Tooltip(this, "Zoom", line, "Or: mouse wheel over your character to zoom, right-drag to move it, drag to turn it.")
end)
button:SetScript("OnLeave", HideTooltip)
end
local middle = ART_W / 2
ZoomButton("-", 24, middle - 117, "DmorphOctoZoomOut", function()
Zoom(-1)
end, "Zoom out.")
ZoomButton("+", 24, middle - 91, "DmorphOctoZoomIn", function()
Zoom(1)
end, "Zoom in, to see the details.")
ZoomButton("Reset view", 80, middle - 65, "DmorphOctoZoomReset", ResetView, "Back to the whole character.")
-- Birthday suit (user, 18 Oct): every armour slot shows nothing, to build up from.
local birthday = Button(stage, "Birthday suit", 92, middle + 19, -10, function()
Morph(M.BirthdaySuit)
end, "DmorphOctoBirthdaySuit")
birthday:SetFrameLevel(base + 6)
birthday:SetScript("OnEnter", function()
Tooltip(this, "Birthday suit", "Every armour slot shows nothing: build a look up from bare skin.",
"Weapons stay. Use my own gear (Items tab) or Reset everything brings your gear back.")
end)
birthday:SetScript("OnLeave", HideTooltip)
local plate = Layer(stage, 5)
plate:SetPoint("BOTTOMLEFT", stage, "BOTTOMLEFT", 0, 0)
plate:SetPoint("BOTTOMRIGHT", stage, "BOTTOMRIGHT", 0, 0)
plate:SetHeight(PLATE_H + 20)
Gradient(plate, "BACKGROUND", 0.03, 0.02, 0.05, 0.95, 0.03, 0.02, 0.05, 0):SetAllPoints(plate)
ui.plateTitle = Font(plate:CreateFontString("DmorphOctoLookName", "OVERLAY", "GameFontNormalLarge"), 15, C.gold)
ui.plateTitle:SetPoint("BOTTOM", plate, "BOTTOM", 0, 22)
ui.plateTitle:SetWidth(ART_W - 16)
ui.plateTitle:SetHeight(18)
ui.plateInfo = Font(plate:CreateFontString("DmorphOctoLookInfo", "OVERLAY", "GameFontHighlightSmall"), 11, C.dim)
ui.plateInfo:SetPoint("BOTTOM", plate, "BOTTOM", 0, 8)
ui.plateInfo:SetWidth(ART_W - 16)
ui.plateInfo:SetHeight(14) -- one line; a longer one ends in "..."
-- Your size in the world, 25% to 300% (DmorphHelper v2). Applied once the
-- slider stands still: each change rebuilds your model.
ui.sizeTimer = NewTimer()
local size = CreateFrame("Slider", "DmorphOctoSize", plate)
size:SetFrameLevel(base + 6)
size:SetOrientation("HORIZONTAL")
size:SetWidth(132)
size:SetHeight(8)
size:SetPoint("BOTTOMLEFT", plate, "BOTTOMLEFT", 124, 50)
-- A thin flat track and a purple thumb (OctoBG's slider).
Flat(size, C.field, C.line)
size:SetThumbTexture(WHITE)
local thumb = size:GetThumbTexture()
if thumb then
thumb:SetWidth(10)
thumb:SetHeight(14)
thumb:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
end
size:SetMinMaxValues(0.25, 3)
size:SetValueStep(0.05)
ui.sizeLabel = Font(size:CreateFontString("DmorphOctoSizeLabel", "OVERLAY", "GameFontHighlightSmall"), 11)
ui.sizeLabel:SetPoint("RIGHT", size, "LEFT", -8, 0)
-- "in game": it changes your character in the world, not only here (user, 18 Oct).
ui.sizeLabel:SetText("Size in game 100%")
size:SetScript("OnValueChanged", function()
local value = math.floor(this:GetValue() * 20 + 0.5) / 20
ui.sizeLabel:SetText("Size in game " .. math.floor(value * 100 + 0.5) .. "%")
if ui.sizeSync then
return
end
Debounce(ui.sizeTimer, 0.3, function()
if math.abs(value - 1) < 0.001 then
SetStatus(M.SetScale(nil))
else
SetStatus(M.SetScale(value))
end
UpdatePlate() -- back to the size you have if it couldn't change
end)
end)
size:SetScript("OnMouseUp", function()
if arg1 == "RightButton" then
this:SetValue(1)
end
end)
size:SetScript("OnEnter", function()
Tooltip(this, "Size in game", "Drag to make your character smaller or bigger in the world, not only here.",
"Right-click or Reset: your own size. Needs DmorphHelper (Setup shows it).")
end)
size:SetScript("OnLeave", HideTooltip)
ui.sizeSlider = size
local sizeReset = Button(plate, "Reset", 46, 0, 0, function()
size:SetValue(1)
end, "DmorphOctoSizeReset")
sizeReset:ClearAllPoints()
sizeReset:SetPoint("LEFT", size, "RIGHT", 8, 0)
sizeReset:SetHeight(18)
sizeReset:SetFrameLevel(base + 6)
sizeReset:SetScript("OnEnter", function()
Tooltip(this, "Normal size", "Your character back to its own size, 100%.")
end)
sizeReset:SetScript("OnLeave", HideTooltip)
local function AddSlots(slots, x)
for i = 1, table.getn(slots) do
local slot = slots[i]
local button = CreateFrame("Button", "DmorphOctoSlot" .. slot, stage)
button:SetFrameLevel(base + 6)
button:SetWidth(36)
button:SetHeight(36)
button:SetPoint("TOPLEFT", stage, "TOPLEFT", x, -10 - (i - 1) * 46)
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
-- A flat square: the icon inside a 1 px line, a purple frame
-- round it while the slot is morphed.
Flat(button, C.field, C.line)
button.icon = button:CreateTexture(nil, "BORDER")
button.icon:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
button.icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -2, 2)
button.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
button.glow = CreateFrame("Frame", nil, button)
button.glow:SetFrameLevel(base + 7)
button.glow:SetPoint("TOPLEFT", button, "TOPLEFT", -1, 1)
button.glow:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1)
button.glow:SetBackdrop({ edgeFile = WHITE, edgeSize = 2 })
button.glow:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 1)
Hover(button, 0.3)
button.slot = slot
button:SetScript("OnClick", function()
if arg1 == "RightButton" then
Morph(M.SetItem, this.slot, 0)
else
ShowItemSlot(this.slot)
end
end)
button:SetScript("OnEnter", SlotTooltip)
button:SetScript("OnLeave", HideTooltip)
ui.slots[slot] = button
end
end
AddSlots(LEFT_SLOTS, 8)
AddSlots(RIGHT_SLOTS, ART_W - 44)
local border = Layer(stage, 7)
border:SetPoint("TOPLEFT", stage, "TOPLEFT", -1, 1)
border:SetPoint("BOTTOMRIGHT", stage, "BOTTOMRIGHT", 1, -1)
border:SetBackdrop({ edgeFile = WHITE, edgeSize = 1 })
border:SetBackdropBorderColor(C.line[1], C.line[2], C.line[3], 1)
Button(frame, "Reset model", 156, 18, -436, function()
Morph(M.ResetModel)
end)
Button(frame, "Reset everything", 156, 180, -436, function()
Morph(M.ResetAll)
end)
end
-- Scales the window, keeping its top-left corner where it is.
local function ScaleWindow(scale)
if scale < MIN_SCALE then
scale = MIN_SCALE
elseif scale > MAX_SCALE then
scale = MAX_SCALE
end
local old, left, top = frame:GetScale(), frame:GetLeft(), frame:GetTop()
frame:SetScale(scale)
if left and top then
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left * old / scale, top * old / scale)
end
Prefs().scale = scale
end
-- The corner grip: drag it to make the whole window bigger or smaller.
local function BuildGrip()
local grip = CreateFrame("Button", "DmorphOctoResize", frame)
grip:SetWidth(18)
grip:SetHeight(18)
grip:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -8, 8)
grip:SetFrameLevel(frame:GetFrameLevel() + 10)
grip:EnableMouse(true)
grip.dots = {}
local spots = { { 2, 2 }, { 7, 2 }, { 12, 2 }, { 2, 7 }, { 7, 7 }, { 2, 12 } }
for i = 1, table.getn(spots) do
local dot = grip:CreateTexture(nil, "OVERLAY")
dot:SetTexture(WHITE)
dot:SetWidth(3)
dot:SetHeight(3)
dot:SetPoint("BOTTOMRIGHT", grip, "BOTTOMRIGHT", -spots[i][1], spots[i][2])
dot:SetVertexColor(C.lineHi[1], C.lineHi[2], C.lineHi[3])
grip.dots[i] = dot
end
local function Tint(r, g, b)
for i = 1, table.getn(grip.dots) do
grip.dots[i]:SetVertexColor(r, g, b)
end
end
grip:SetScript("OnEnter", function()
Tint(C.accent[1], C.accent[2], C.accent[3])
Tooltip(this, "Window size", "Drag to make the window bigger or smaller.")
end)
grip:SetScript("OnLeave", function()
Tint(C.lineHi[1], C.lineHi[2], C.lineHi[3])
GameTooltip:Hide()
end)
grip:SetScript("OnMouseDown", function()
this.startX = GetCursorPosition()
this.startScale = frame:GetScale()
end)
grip:SetScript("OnMouseUp", function()
this.startX = nil
end)
grip:SetScript("OnUpdate", function()
if this.startX then
local x = GetCursorPosition()
ScaleWindow(this.startScale + (x - this.startX) / (W * UIParent:GetEffectiveScale()))
end
end)
end
local function BuildTabs()
for i = 1, table.getn(TABS) do
local info = TABS[i]
local tab = CreateFrame("Button", "DmorphOctoTab" .. info.text, frame)
tab:SetHeight(26)
Flat(tab, C.btn, C.line)
tab.label = Font(tab:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"), 12)
tab.label:SetPoint("CENTER", tab, "CENTER", 0, 0)
tab.label:SetText(info.text)
Hover(tab, 0.18)
tab.key = info.key
tab:SetScript("OnClick", function()
PlaySound("igCharacterInfoTab")
ShowTab(this.key)
end)
ui.tabs[info.key] = tab
local page = CreateFrame("Frame", "DmorphOctoPage" .. info.text, frame)
page:SetPoint("TOPLEFT", frame, "TOPLEFT", PAGE_X, PAGE_Y)
page:SetWidth(PAGE_W)
page:SetHeight(PAGE_H)
Flat(page, C.panel, C.line)
page:Hide()
ui.pages[info.key] = page
end
-- Each tab as wide as its name, the room left shared out evenly.
local count, used = table.getn(TABS), 0
for i = 1, count do
used = used + ui.tabs[TABS[i].key].label:GetStringWidth()
end
local pad = math.floor((PAGE_W - (count - 1) * TAB_GAP - used) / count)
local x = PAGE_X
for i = 1, count do
local tab = ui.tabs[TABS[i].key]
local width = math.floor(tab.label:GetStringWidth() + pad)
tab:SetWidth(width)
tab:SetPoint("TOPLEFT", frame, "TOPLEFT", x, -42)
x = x + width + TAB_GAP
end
BuildModelsPage(ui.pages.models)
BuildMountsPage(ui.pages.mounts)
BuildFavouritesPage(ui.pages.favourites)
BuildRacesPage(ui.pages.races)
BuildGlowsPage(ui.pages.glows)
BuildItemsPage(ui.pages.items)
BuildSlutmogPage(ui.pages.slutmog)
BuildSetsPage(ui.pages.sets)
BuildWardrobePage(ui.pages.wardrobe)
BuildWeatherPage(ui.pages.weather)
end
-- Sharing (Share.lua): whether others see your morph and you see theirs.
local function ShareMenu()
local sharing, seeing = M.GetShareStatus()
local options = {
{ text = "Others see my morph", key = "share", checked = sharing },
{ text = "I see their morphs", key = "see", checked = seeing },
}
OpenMenu(this, options, function(option)
if option.key == "share" then
SetStatus(M.SetSharing(not sharing))
else
SetStatus(M.SetSeeing(not seeing))
end
end, true)
end
-- Setup check (M.GetSetupReport): a footer button that says whether morphing
-- works, and a panel with each part DMorph uses and how to fix what doesn't.
local CHECK_COLOURS = { ok = { 0.2, 1, 0.6 }, bad = { 1, 0.33, 0.33 }, off = { 0.6, 0.6, 0.6 } }
local CHECK_WORDS = { ok = "OK", bad = "Not working", off = "Not running" }
local function UpdateSetupButton()
if not ui.setupButton then
return
end
-- The helper's version shows on the button: an old one needs the installer.
local helper, wanted = M.GetHelperVersion()
if not M.HasMorphAPI() then
ui.setupButton:SetText("|cffff5555Setup: fix me|r")
elseif helper and helper < wanted then
ui.setupButton:SetText("|cffffcc00Old helper v" .. helper .. ": fix|r")
elseif helper then
ui.setupButton:SetText("|cff33ff99Setup OK|r |cffccccccHelper v" .. helper .. "|r")
else
ui.setupButton:SetText("|cff33ff99Setup OK|r |cff999999No helper|r")
end
end
local function FillSetupPanel()
local report = M.GetSetupReport()
if M.HasMorphAPI() then
ui.setupSummary:SetText("|cff33ff99Morphing works.|r The optional parts below add extras.")
else
ui.setupSummary:SetText("|cffff5555Morphing doesn't work yet.|r Follow the VanillaHelpers line, then press Check again.")
end
for i = 1, table.getn(ui.setupRows) do
local row, entry = ui.setupRows[i], report[i]
if entry then
local colour = CHECK_COLOURS[entry.state]
row.name:SetText(entry.name)
row.state:SetText(CHECK_WORDS[entry.state])
row.state:SetTextColor(colour[1], colour[2], colour[3])
row.text:SetText(entry.text)
else
row.name:SetText("")
row.state:SetText("")
row.text:SetText("")
end
end
UpdateSetupButton()
end
local function ShowSetupPanel()
FillSetupPanel()
ui.setupPanel:Show()
end
local function BuildSetupPanel()
local panel = CreateFrame("Frame", "DmorphOctoSetup", frame)
panel:Hide()
panel:SetWidth(580)
panel:SetHeight(340)
panel:SetPoint("CENTER", frame, "CENTER", 0, 10)
panel:SetFrameLevel(frame:GetFrameLevel() + 30)
panel:EnableMouse(true)
Flat(panel, C.panel, C.accent)
Label(panel, "DMorph setup check", "GameFontNormalLarge", 16, -14)
ui.setupSummary = Label(panel, "", "GameFontHighlight", 16, -40, "DmorphOctoSetupSummary")
ui.setupRows = {}
for i = 1, 4 do
local y = -68 - (i - 1) * 60
local row = {}
row.name = Label(panel, "", "GameFontNormal", 16, y, "DmorphOctoSetupName" .. i)
row.state = Label(panel, "", "GameFontHighlight", 140, y, "DmorphOctoSetupState" .. i)
row.text = Label(panel, "", "GameFontHighlightSmall", 16, y - 18, "DmorphOctoSetupText" .. i)
row.text:SetWidth(548)
row.text:SetHeight(38)
row.text:SetJustifyV("TOP")
ui.setupRows[i] = row
end
Button(panel, "Check again", 110, 16, -306, FillSetupPanel, "DmorphOctoSetupRecheck")
Label(panel, "/dmorph check prints this in chat.", "GameFontDisableSmall", 140, -311)
Button(panel, "Close", 80, 484, -306, function()
ui.setupPanel:Hide()
end, "DmorphOctoSetupClose")
ui.setupPanel = panel
end
-- The window closes. (Outside Build: Lua 5.0 allows 32 upvalues a function.)
function P.OnWindowHide()
PlaySound("igCharacterInfoClose")
P.EndGlowPreview()
-- The try-on boxes load your character afresh next time.
for _, box in pairs(ui.boxes) do
box.loaded, box.dressed = nil, nil
end
ui.menu:Hide()
-- A click still waiting at the gate is applied now.
local nextCall = ui.gateNext
ui.gateBusy, ui.gateNext = nil, nil
if nextCall then
Morph(nextCall[1], nextCall[2], nextCall[3])
end
end
local function Build()
frame = CreateFrame("Frame", "DmorphOctoFrame", UIParent)
frame:Hide()
frame:SetWidth(W)
frame:SetHeight(H)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
frame:SetFrameStrata("DIALOG")
frame:SetScale(Prefs().scale)
Flat(frame, C.bg, C.deep)
frame:EnableMouse(true)
frame:SetMovable(true)
frame:SetClampedToScreen(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function()
this:StartMoving()
end)
frame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
end)
frame:SetScript("OnShow", function()
PlaySound("igCharacterInfoOpen")
UpdateSetupButton()
if not M.HasMorphAPI() then
-- Nothing can morph: show what's wrong and how to fix it straight away.
SetStatus(false, M.MissingAPI())
ShowSetupPanel()
end
ShowTab(currentTab)
ShowArt()
UpdatePlate()
RequestPreview()
end)
frame:SetScript("OnHide", P.OnWindowHide)
frame:SetScript("OnUpdate", function()
this.sincePoll = (this.sincePoll or 0) + arg1
if this.sincePoll > 1 then
this.sincePoll = 0
Poll()
end
end)
-- Shapeshifts, zoning and the like rebuild your model.
frame:RegisterEvent("UNIT_MODEL_CHANGED")
frame:SetScript("OnEvent", function()
if arg1 == "player" then
RequestPreview()
end
end)
-- Escape closes the window.
table.insert(UISpecialFrames, "DmorphOctoFrame")
-- The title bar: a purple strip, the name, a flat close button.
local bar = frame:CreateTexture(nil, "BORDER")
bar:SetTexture(WHITE)
bar:SetGradientAlpha("HORIZONTAL", C.deep[1], C.deep[2], C.deep[3], 1, C.bg[1], C.bg[2], C.bg[3], 1)
bar:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
bar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
bar:SetHeight(32)
local line = frame:CreateTexture(nil, "ARTWORK")
line:SetTexture(WHITE)
line:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.8)
line:SetPoint("TOPLEFT", bar, "BOTTOMLEFT", 0, 0)
line:SetPoint("TOPRIGHT", bar, "BOTTOMRIGHT", 0, 0)
line:SetHeight(1)
local title = Font(frame:CreateFontString("DmorphOctoTitle", "OVERLAY", "GameFontNormal"), 16, C.text)
title:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -9)
title:SetText("|cffb46bffD|rMorph |cff9b98a8for OctoWoW|r")
local close = CreateFrame("Button", "DmorphOctoFrameClose", frame)
close:SetWidth(22)
close:SetHeight(22)
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -5)
FlatButton(close, "x", 13)
close:SetScript("OnClick", function()
frame:Hide()
end)
ui.searchTimer = NewTimer()
ui.previewTimer = NewTimer()
ui.revealTimer = NewTimer()
ui.gateTimer = NewTimer()
ui.glowTimer = NewTimer()
-- ClassicAPI's Model:SetDisplayInfo shows creatures by display ID.
local probe = CreateFrame("Model", nil, frame)
ui.canPreview = type(probe.SetDisplayInfo) == "function"
probe:Hide()
local player = CreateFrame("PlayerModel", nil, frame)
ui.playerViews = ui.canPreview and type(player.SetDisplayInfo) == "function" or nil
player:Hide()
ui.menu = BuildMenu()
BuildStage()
BuildTabs()
BuildGrip()
ui.status = Label(frame, "", "GameFontHighlightSmall", 20, -466, "DmorphOctoFrameStatus")
ui.status:SetWidth(W - 40)
ui.status:SetHeight(26)
-- The footer: a thin line, setup, a hint, sharing, the version.
local foot = frame:CreateTexture(nil, "ARTWORK")
foot:SetTexture(WHITE)
foot:SetVertexColor(C.line[1], C.line[2], C.line[3], 1)
foot:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -494)
foot:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -494)
foot:SetHeight(1)
ui.setupButton = Button(frame, "", 160, 18, -502, ShowSetupPanel, "DmorphOctoSetupButton")
ui.setupButton:SetScript("OnEnter", function()
local helper, wanted = M.GetHelperVersion()
local line = "DmorphHelper: not running (optional)."
if helper then
line = "DmorphHelper: v" .. helper .. " running, v" .. wanted .. " comes with this DMorph."
end
Tooltip(this, "Setup check", "Whether VanillaHelpers and the optional client mods are running,",
"and how to fix what isn't.", line)
end)
ui.setupButton:SetScript("OnLeave", HideTooltip)
UpdateSetupButton()
Label(frame, "DMorph users see each other's morphs. /dmorph help: commands.",
"GameFontDisableSmall", 190, -508)
BuildSetupPanel()
local share = Button(frame, "Sharing", 80, W - 200, -502, ShareMenu, "DmorphOctoShare")
share:SetScript("OnEnter", function()
local _, _, known, morphed = M.GetShareStatus()
Tooltip(this, "Sharing with DMorph users", "Who sees whose morph. Only friendly players can be morphed.",
"DMorph users seen: " .. known .. " (" .. morphed .. " morphed).")
end)
share:SetScript("OnLeave", HideTooltip)
local version = Font(frame:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall"), 10, C.dim)
version:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -34, -508)
version:SetText("v" .. M.VERSION)
end
function M.ToggleUI()
if not frame then
Build()
end
if frame:IsVisible() then
frame:Hide()
else
frame:Show()
end
end
M.RegisterListener(RefreshAll)
-- /dmorph frametest [displayID] (2.21.6, simplified in 2.21.7): one display
-- five ways, for an in-game check of how this client draws a framed model.
-- The first run (19 Oct) showed that with a camera set, position and facing
-- are ignored. Now: A the full-body camera (type 1), B the portrait camera,
-- C / D the full-body camera zoomed out / in (scale 0.7 / 1.4), E no camera.
-- Top row facing 0, bottom row facing a quarter turn.
function P.FrameTest(args)
local display = M.ParseID(args[1] or "") or 63568
if not ui.canPreview then
DEFAULT_CHAT_FRAME:AddMessage("DMorph frame test: 3D previews need ClassicAPI.")
return
end
local test = DmorphOctoFrameTest
if not test then
test = CreateFrame("Frame", "DmorphOctoFrameTest", UIParent)
test:SetFrameStrata("FULLSCREEN_DIALOG")
test:SetWidth(5 * 150 + 20)
test:SetHeight(2 * 170 + 50)
test:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
Flat(test, C.bg, C.accent)
test:EnableMouse(true)
test.title = Font(test:CreateFontString(nil, "OVERLAY"), 13, C.gold)
test.title:SetPoint("TOPLEFT", test, "TOPLEFT", 10, -8)
local close = Button(test, "Close", 60, 5 * 150 + 20 - 70, -6, function()
DmorphOctoFrameTest:Hide()
end)
close:SetHeight(18)
test.cells = {}
for row = 1, 2 do
for col = 1, 5 do
local cell = CreateFrame("Frame", nil, test)
cell:SetWidth(144)
cell:SetHeight(160)
cell:SetPoint("TOPLEFT", test, "TOPLEFT", 10 + (col - 1) * 150, -36 - (row - 1) * 170)
Flat(cell, C.card, C.line)
local model = CreateFrame("Model", nil, cell)
model:SetPoint("TOPLEFT", cell, "TOPLEFT", 2, -2)
model:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", -2, 16)
local label = Font(cell:CreateFontString(nil, "OVERLAY"), 12, C.accent)
label:SetPoint("BOTTOM", cell, "BOTTOM", 0, 2)
label:SetText(string.sub("ABCDE", col, col) .. row)
table.insert(test.cells, { model = model, col = col, row = row })
end
end
local SCALES = { 1, 1, 0.7, 1.4, 1 }
local CAMERAS = { 1, 0, 1, 1 }
test:SetScript("OnUpdate", function()
for i = 1, table.getn(this.cells) do
local c = this.cells[i]
if CAMERAS[c.col] then
pcall(c.model.SetCamera, c.model, CAMERAS[c.col])
end
c.model:SetModelScale(SCALES[c.col])
c.model:SetFacing(c.row == 2 and 1.5708 or 0)
end
end)
end
test.title:SetText("DMorph frame test: display " .. display .. " (" .. (M.GetDisplayName(display) or "?") .. ")")
for i = 1, table.getn(test.cells) do
local m = test.cells[i].model
pcall(m.ClearModel, m)
m:SetDisplayInfo(display)
pcall(m.SetLight, m, 1, 0, 0, -0.7, -0.7, 0.7, 1, 1, 1, 0.8, 1, 1, 1)
end
test:Show()
end
M.commands.frametest = P.FrameTest
-- Minimap button ------------------------------------------------------------------
local function PlaceMinimapButton(button)
local pos = M.GetAccount().minimap
button:ClearAllPoints()
button:SetPoint("CENTER", Minimap, "CENTER", pos.x, pos.y)
end
-- Keeps the button on the minimap's edge, pointing at the cursor. Plain
-- vectors instead of angles: the 1.12 trig functions' units vary by build.
local function DragMinimapButton()
local x, y = GetCursorPosition()
local scale = Minimap:GetEffectiveScale()
local cx, cy = Minimap:GetCenter()
x, y = x / scale - cx, y / scale - cy
local length = math.sqrt(x * x + y * y)
if length > 0 then
local pos = M.GetAccount().minimap
pos.x, pos.y = x / length * 80, y / length * 80
PlaceMinimapButton(this)
end
end
local function CreateMinimapButton()
local account = M.GetAccount()
local pos = account.minimap
if type(pos) ~= "table" or type(pos.x) ~= "number" or type(pos.y) ~= "number" then
account.minimap = { x = -75, y = -27 }
end
local button = CreateFrame("Button", "DmorphOctoMinimapButton", Minimap)
button:SetWidth(31)
button:SetHeight(31)
button:SetFrameStrata("MEDIUM")
button:SetFrameLevel(8)
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
button:RegisterForDrag("LeftButton")
local icon = button:CreateTexture(nil, "BACKGROUND")
icon:SetTexture("Interface\\Icons\\Spell_Nature_Polymorph")
icon:SetWidth(20)
icon:SetHeight(20)
icon:SetPoint("TOPLEFT", button, "TOPLEFT", 6, -5)
local border = button:CreateTexture(nil, "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
border:SetWidth(53)
border:SetHeight(53)
border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
local highlight = button:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
highlight:SetBlendMode("ADD")
highlight:SetAllPoints(button)
button:SetScript("OnClick", M.ToggleUI)
button:SetScript("OnDragStart", function()
this:SetScript("OnUpdate", DragMinimapButton)
end)
button:SetScript("OnDragStop", function()
this:SetScript("OnUpdate", nil)
end)
button:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:SetText("DMorph |cff33ff99(OctoWoW)|r")
GameTooltip:AddLine("Click to open the morph window.", 1, 1, 1)
GameTooltip:AddLine("Drag to move this button.", 0.8, 0.8, 0.8)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
PlaceMinimapButton(button)
end
local loader = CreateFrame("Frame")
loader:RegisterEvent("VARIABLES_LOADED")
loader:SetScript("OnEvent", function()
if not DmorphOctoMinimapButton then
CreateMinimapButton()
end
end)