Files
2026-06-13 11:51:05 +04:00

536 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: wow-335a-ui
description: >
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
---
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
## Ключевые ограничения патча 3.3.5a
- **API версии**: `## Interface: 30300` (build 12340)
- **Lua версия**: Lua 5.1
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
- Таймеры — через `OnUpdate` с накоплением `elapsed`
---
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
### Главная проблема: система координат WoW ≠ пиксели экрана
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
| Разрешение | Виртуальная ширина | Виртуальная высота |
|-----------|-------------------|-------------------|
| 1024×768 | 1024 | 768 |
| 1280×960 | 1024 | 768 |
| 1920×1080 | 1365 | 768 |
| 2560×1440 | 1365 | 768 |
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
### Пересчёт координат из Figma в WoW
```lua
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
-- Если макет сделан на 1080px:
local SCALE = 768 / 1080 -- ≈ 0.711
local function px(figma_px)
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
end
-- Использование:
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
```
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
### UIParent scale и как он ломает пиксели
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
```lua
-- Получить текущий масштаб UIParent
local uiScale = UIParent:GetScale() -- обычно 0.641.0
-- Перевести экранные пиксели в виртуальные единицы WoW
local function screenPxToWow(px)
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
return px * (768 / screenH) / uiScale
end
-- Принудительно зафиксировать scale (использовать с умом!)
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
```
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
```lua
-- Масштабируй только свой фрейм
frame:SetScale(0.85)
-- Тогда все дочерние элементы тоже масштабируются
```
---
### Цвета: Figma HEX/RGBA → WoW RGBA
WoW принимает цвета в диапазоне **0.01.0** (не 0255).
```lua
-- Конвертер HEX → WoW RGBA
local function hex(str, a)
str = str:gsub("#", "")
local r = tonumber(str:sub(1,2), 16) / 255
local g = tonumber(str:sub(3,4), 16) / 255
local b = tonumber(str:sub(5,6), 16) / 255
return r, g, b, a or 1.0
end
-- Использование:
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
fs:SetTextColor(hex("#FFD100"))
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
frame:SetBackdropBorderColor(hex("#4A90D9"))
```
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
```lua
-- Figma: opacity 60%
frame:SetAlpha(0.60)
tex:SetAlpha(0.60)
```
---
### Шрифты: как приблизить к дизайну
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
```lua
-- Кастомный шрифт из аддона
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
```
**Соответствие Figma → WoW размер шрифта**:
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|---------------------|--------------------------|
| 12 px | 9 |
| 14 px | 10 |
| 16 px | 11 |
| 18 px | 13 |
| 24 px | 17 |
| 32 px | 23 |
```lua
-- Автоматический пересчёт размера шрифта
local function fontSize(figma_pt)
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
end
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
```
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
**Встроенные шрифты WoW 3.3.5a**:
```
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
Fonts\MORPHEUS.ttf — декоративный (заголовки)
Fonts\ARIALN.TTF — Arial Narrow
Fonts\skurri.ttf — шрифт повреждений
```
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
---
### Текстуры: подготовка под ограничения движка
#### Требования к текстурам WoW 3.3.5a
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
- Максимум: **512×512 px**
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
#### Как уместить большой элемент дизайна
Если элемент из Figma больше 512px — разрезай на части:
```
[512×256] [512×256] ← верхняя половина (левая + правая)
[512×256] [512×256] ← нижняя половина
```
```lua
-- Сборка из 4 текстур
local tl = frame:CreateTexture(nil, "BACKGROUND")
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
tl:SetWidth(256); tl:SetHeight(128)
local tr = frame:CreateTexture(nil, "BACKGROUND")
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
tr:SetWidth(256); tr:SetHeight(128)
-- ... и так далее
```
#### TexCoord — показать только часть текстуры
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
```lua
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
-- Формула для ячейки в сетке (col/row с нуля):
local cols, rows = 4, 4
local col, row = 2, 1 -- третья колонка, вторая строка
tex:SetTexCoord(
col/cols, -- left
(col+1)/cols, -- right
row/rows, -- top
(row+1)/rows -- bottom
)
```
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
---
### Позиционирование: от Figma к SetPoint
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
**Алгоритм переноса**:
1. Определи ближайший угол/сторону в Figma (от чего считать)
2. Вычисли отступ от этой стороны
3. Пересчитай через `px()`
```lua
-- Figma (холст 1365×768):
-- Элемент: X=50, Y=30, W=200, H=100
-- → привязка от TOPLEFT UIParent
frame:SetWidth(px(200))
frame:SetHeight(px(100))
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
-- ^^^ ^^^
-- X offset Y offset (отрицательный! вниз)
```
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
| Figma anchor | WoW SetPoint |
|-------------|-------------|
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
---
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
```lua
-- PixelHelper — pixel-perfect вёрстка из Figma
local PH = {}
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
function PH.scale()
return 768 / PH.CANVAS_HEIGHT
end
-- Пересчёт px из Figma → WoW единицы
function PH.px(v)
return math.floor(v * PH.scale() + 0.5)
end
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
function PH.color(hex, alpha)
hex = hex:gsub("#","")
local r = tonumber(hex:sub(1,2),16)/255
local g = tonumber(hex:sub(3,4),16)/255
local b = tonumber(hex:sub(5,6),16)/255
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
return r, g, b, a
end
-- Размер шрифта из Figma → WoW
function PH.font(pt)
return math.max(8, math.floor(pt * PH.scale() + 0.5))
end
-- Установка якоря с автопересчётом
function PH.point(frame, point, relativeTo, relPoint, x, y)
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
end
-- Размер фрейма из Figma
function PH.size(frame, w, h)
frame:SetWidth(PH.px(w))
frame:SetHeight(PH.px(h))
end
-- Экспорт глобально (или оставь локальным)
MyAddon_PH = PH
```
**Использование**:
```lua
local px = MyAddon_PH.px
local color = MyAddon_PH.color
local pxfont = MyAddon_PH.font
MyAddon_PH.size(frame, 400, 250)
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
frame:SetBackdropColor(color("#1A1A2E", 0.95))
fs:SetTextColor(color("#FFD100"))
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
```
---
### Чеклист pixel-perfect переноса
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
- [ ] Все размеры и отступы пересчитаны через `px()`
- [ ] Цвета конвертированы через `color()`
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
- [ ] Размеры шрифтов пересчитаны через `font()`
- [ ] Y-отступы от верхних якорей — **отрицательные**
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
---
```
MyAddon/
├── MyAddon.toc ← обязателен, имя файла = имя папки
├── MyAddon.xml ← верстка фреймов (опционально)
└── MyAddon.lua ← логика
```
### TOC-файл (3.3.5a)
```toc
## Interface: 30300
## Title: My Addon
## Notes: Описание аддона
## Author: Ilyas
## Version: 1.0
## SavedVariables: MyAddonDB
MyAddon.xml
MyAddon.lua
```
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
---
## XML-верстка
### Минимальный шаблон XML-файла
```xml
<Ui xmlns="http://www.blizzard.com/wow/ui/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="MyAddon.lua"/>
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
<Size>
<AbsDimension x="300" y="200"/>
</Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentClose" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
</Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
<OnMouseDown>self:StartMoving()</OnMouseDown>
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
</Scripts>
</Frame>
</Ui>
```
---
## Точки якоря (FRAMEPOINT)
| Точка | Расположение |
|-------|-------------|
| `TOPLEFT` | Верхний левый угол |
| `TOP` | Верхний центр |
| `TOPRIGHT` | Верхний правый угол |
| `LEFT` | Левый центр |
| `CENTER` | Центр |
| `RIGHT` | Правый центр |
| `BOTTOMLEFT` | Нижний левый угол |
| `BOTTOM` | Нижний центр |
| `BOTTOMRIGHT` | Нижний правый угол |
### SetPoint через Lua
```lua
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
-- Сброс якорей перед переустановкой
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER")
```
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
---
## Слои и страты
### Draw Layers (внутри фрейма, сверху вниз по видимости)
```
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
```
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
- FontString всегда рисуется **поверх** Texture в том же слое
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
### Frame Strata (между фреймами)
```
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
```
```lua
frame:SetFrameStrata("DIALOG")
frame:SetFrameLevel(5) -- внутри страты (0–255)
```
---
## Виджеты, события, Blizzard API
> Полный справочник — в `references/widgets-and-api.md`.
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
---
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
**MyAddon.toc**
```toc
## Interface: 30300
## Title: MyAddon
## SavedVariables: MyAddonDB
MyAddon.lua
```
**MyAddon.lua**
```lua
local ADDON_NAME = "MyAddon"
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
f:SetWidth(250)
f:SetHeight(150)
f:SetPoint("CENTER")
f:SetFrameStrata("DIALOG")
f:SetMovable(true)
f:EnableMouse(true)
f:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = {left=11, right=12, top=12, bottom=11}
})
f:Hide()
-- Заголовок
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", f, "TOP", 0, -12)
title:SetText("My Addon")
-- Закрытие
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
close:SetScript("OnClick", function() f:Hide() end)
-- Перетаскивание
f:SetScript("OnMouseDown", function(self, btn)
if btn == "LeftButton" then self:StartMoving() end
end)
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
-- Инициализация
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(self, event, name)
if event == "ADDON_LOADED" and name == ADDON_NAME then
MyAddonDB = MyAddonDB or {}
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
end
end)
-- Слэш-команда
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
if f:IsShown() then f:Hide() else f:Show() end
end
```