6 Commits

Author SHA1 Message Date
sindoring 85f80445d1 план 2026-07-18 17:05:18 +04:00
sindoring 9fdb7ed718 фикс ачивок 2026-07-18 16:54:34 +04:00
sindoring 698de84762 зум при создании персонажа 2026-07-18 16:24:02 +04:00
sindoring cddaf2c515 получение режима внтури игры 2026-07-18 16:08:08 +04:00
sindoring 26ff2b3b94 полный переход на warcraft xl 2026-07-18 15:48:11 +04:00
sindoring 08c1b9da3f ai ready 2026-06-13 11:51:05 +04:00
1453 changed files with 2852 additions and 283835 deletions
+1 -1
View File
@@ -5,4 +5,4 @@ AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
AWS_BUCKET=
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
+5 -1
View File
@@ -3,4 +3,8 @@ __pycache__/
manifest.json
.claude
.vscode
dist
dist
build/
Wow*.exe
*.backup.exe
Logs/
+15
View File
@@ -0,0 +1,15 @@
[submodule "vendor/warcraftxl"]
path = vendor/warcraftxl
url = https://github.com/WarcraftXL/wxl-core.git
[submodule "vendor/modules/wxl-modern-assets"]
path = vendor/modules/wxl-modern-assets
url = https://github.com/WarcraftXL/wxl-modern-assets.git
[submodule "vendor/modules/wxl-modern-render"]
path = vendor/modules/wxl-modern-render
url = https://github.com/WarcraftXL/wxl-modern-render.git
[submodule "vendor/modules/wxl-unit-outline"]
path = vendor/modules/wxl-unit-outline
url = https://github.com/WarcraftXL/wxl-unit-outline.git
[submodule "vendor/modules/wxl-modern-adt"]
path = vendor/modules/wxl-modern-adt
url = https://github.com/WarcraftXL/wxl-modern-adt.git
+46
View File
@@ -0,0 +1,46 @@
# MoonWell Client Sources — Agent Guide
## Проект
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
кастомные интерфейсы для приватного сервера MoonWell.
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
---
## Навыки (Skills)
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
| Задача | Файл |
|--------|------|
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
и готовые шаблоны.
---
## Ключевые ограничения (всегда держи в голове)
- `## Interface: 30300` — никаких retail-API
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
- Кнопки и ScrollFrame в XML должны иметь `name`
---
## Структура аддона
```
AddonName/
├── AddonName.toc ← обязательно, имя файла = имя папки
├── AddonName.xml ← верстка (загружается первой)
└── AddonName.lua ← логика
```
Порядок файлов в TOC важен: XML раньше Lua.
+71
View File
@@ -0,0 +1,71 @@
cmake_minimum_required(VERSION 3.20)
project(MoonWellWarcraftXL LANGUAGES CXX)
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
# its aggregate DLL target here, so updating the framework does not mix our
# client-specific offsets and policy into the upstream checkout.
add_subdirectory(vendor/warcraftxl)
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
function(wxl_collect_sources output)
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
set(${output} "${collected}" PARENT_SCOPE)
endfunction()
if(TARGET WarcraftXL)
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
target_sources(WarcraftXL PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
${WXL_MODERN_ASSETS_RUNTIME}
${WXL_MODERN_RENDER_RUNTIME}
${WXL_UNIT_OUTLINE_RUNTIME}
${WXL_MODERN_ADT_RUNTIME})
# wxl-modern-render needs its module-root includes and D3D12 import library.
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
endif()
if(TARGET WarcraftXLHost)
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_ADT_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
target_sources(WarcraftXLHost PRIVATE
${WXL_MODERN_ASSETS_HOST}
${WXL_MODERN_ADT_HOST})
endif()
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
add_library(MoonWellLoader SHARED
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def")
set_target_properties(MoonWellLoader PROPERTIES
OUTPUT_NAME "d3d9-native"
PREFIX ""
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts")
target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
endif()
+788
View File
@@ -0,0 +1,788 @@
# План расширенной кастомизации персонажей MoonWell
## 1. Цель
Создать для клиента WoW 3.3.5a (build 12340) расширенную систему внешности, визуально и по удобству близкую к retail, сохранив оригинальный `Wow.exe` и используя WarcraftXL для всех клиентских расширений.
Система должна:
- поддерживать стандартные параметры 3.3.5a и дополнительные параметры MoonWell;
- показывать одинаковую внешность при создании, выборе персонажа, входе в мир и другим игрокам;
- хранить дополнительные параметры на сервере и считать сервер источником истины;
- поддерживать текстурные слои, geoset базовой модели и отдельные прикрепляемые M2-модели;
- работать без Eluna/AIO в качестве постоянного транспорта состояния;
- позволять добавлять новые варианты преимущественно данными и ассетами;
- иметь версионирование формата и безопасный откат.
## 2. Не входит в первую версию
- полное копирование внутренних `ChrCustomization*` DB2 и протокола актуального retail-клиента;
- поддержка всех retail-рас, которых нет в MoonWell;
- динамическое изменение скелета или пропорций тела;
- автоматический импорт всех retail-ассетов без ручной проверки;
- публикация полного дампа современных файлов Blizzard;
- изменение бинарника `Wow.exe` на диске.
## 3. Базовые ограничения
### Клиент
- WoW 3.3.5a, build 12340, Lua 5.1, `## Interface: 30300`.
- `Wow.exe` должен оставаться оригинальным с контролируемым SHA-256.
- Новое нативное поведение реализуется модулем MoonWell внутри WarcraftXL.
- GlueXML и FrameXML не используют retail API (`C_Timer`, `CreateFramePool`, `PixelUtil` и т. п.).
- Для анимаций интерфейса используется `OnUpdate`.
- UI-текстуры: `.blp`/`.tga`, размеры по степеням двойки, не более 512×512 на отдельный UI-тайл.
- В Lua/XML использовать `SetWidth`/`SetHeight`, а не `SetSize`.
### Ассеты и рендер
- Исходные современные M2/MD21 обрабатываются `wxl-modern-assets`.
- Каждая модель проверяется на поддерживаемую версию формата, лимит костей, материалы, анимации и attachment points.
- Не использовать OBJ/FBX/glTF как вход для игрового клиента: нужны исходные `.m2`, `.skin`, `.skel`, `.anim` и `.blp`.
- UI-превью и модель в мире должны использовать один каталог вариантов и одинаковые идентификаторы.
### Сервер
- Сервер валидирует расу, пол, доступность и совместимость каждого выбора.
- Клиент не может назначить себе недоступный вариант внешности.
- Дополнительные параметры хранятся отдельно от стандартных байтов внешности 3.3.5a.
- Изменения схемы БД поставляются миграциями и имеют обратимый путь.
## 4. Целевая архитектура
```text
Retail CDN (выбранные FileDataID)
Asset manifest + downloader + validation
Patch*.MPQ / loose Patch-WXL assets
├── wxl-modern-assets
├── адаптированный wxl-equip-extension
└── MoonWell customization runtime
Lua API / GlueXML UI
MoonWell customization protocol
AzerothCore storage + validation + broadcast
```
### Компоненты
1. **Asset pipeline** — получает только перечисленные зависимости по `FileDataID`, сохраняет манифест и проверяет комплектность.
2. **`wxl-modern-assets`** — загружает и приводит современные M2/MD21 и текстуры к контракту клиента 3.3.5a.
3. **Адаптированный `wxl-equip-extension`** — прикрепляет M2, поддерживает collection M2, два модельных канала, attachment points и фильтрацию geoset.
4. **MoonWell customization runtime** — управляет независимыми косметическими каналами, текстурными слоями, geoset базовой модели и Lua API.
5. **GlueXML UI** — категории, список вариантов, камера, случайный выбор, сброс и предварительный просмотр.
6. **Серверный модуль** — хранение, валидация, загрузка, изменение и распространение внешности.
## 5. Типы параметров внешности
### Стандартные параметры 3.3.5a
- пол;
- цвет кожи;
- лицо;
- причёска;
- цвет волос;
- стандартная особенность лица/борода.
Они продолжают использовать штатные поля персонажа и стандартный пакет создания.
### Дополнительные текстурные параметры
- шрамы;
- татуировки тела;
- татуировки лица;
- макияж;
- рисунок/отметины;
- дополнительные оттенки кожи;
- дополнительные слои лица.
Реализация: расширение `CharSections`, каталог слоёв и модификация/перехват клиентского композитора текстуры персонажа.
### Geoset базовой модели
- дополнительные причёски;
- части бороды и усов;
- форма ушей/бровей;
- варианты клыков;
- встроенные рога;
- скрытие частей базовой модели.
Реализация: таблица выбора geoset по расе, полу, категории и варианту.
### Прикрепляемые M2-элементы
- серьги и пирсинг;
- украшения для волос;
- объёмные бороды;
- рога и дополнительные клыки;
- протезы;
- венцы, диадемы и прочие аксессуары;
- независимые объёмные элементы тела.
Реализация: косметические каналы поверх механизма `wxl-equip-extension`.
## 6. Косметические каналы
Кастомизация не должна занимать реальные слоты экипировки. Нужен отдельный реестр каналов:
```text
hair_extra
beard_extra
horns
ears
tusks
piercing_1
piercing_2
accessory_head
accessory_face
accessory_body_1
accessory_body_2
```
Для каждого канала задаются:
- стабильный числовой `option_id`;
- допустимые расы и пол;
- `display_id` или прямое описание M2;
- Model1/Model2;
- Texture1/Texture2;
- attachment point для каждого модельного канала;
- список разрешённых geoset collection M2;
- флаги суффиксов расы/пола и структуры каталогов;
- правила конфликта со шлемом и настоящей экипировкой;
- приоритет и взаимоисключающие каналы;
- значение сброса (`choice_id = 0`).
## 7. Адаптация `wxl-equip-extension`
### Обязательные работы
- закрепить точную ревизию репозитория как submodule;
- адаптировать сборочную структуру к текущему WarcraftXL MoonWell;
- проверить пересечение хуков с `wxl-modern-assets` и существующими `OnItemSlotChange`, `OnItemSlotClear`, `OnM2PerFrameUpdate`;
- вынести разбор Model1/Model2, `Icon2`, attachment points и collection geoset в переиспользуемый слой;
- добавить API независимых косметических каналов, не привязанных к надетому предмету;
- устранить утечку/остаток collection M2 после снятия или смены варианта;
- обеспечить корректное уничтожение дочерних моделей при смене персонажа, карты, формы и пола;
- исключить двойное прикрепление модели при повторной синхронизации;
- очистить мусорные Model1/Model2 в старых строках `ItemDisplayInfo.dbc`;
- добавить диагностический лог разрешения пути, модели, текстуры, attachment point и geoset;
- реализовать безопасный fallback при отсутствии ассета.
### Проверки совместимости
- персонаж игрока;
- другие игроки;
- NPC с тем же `display_id`;
- экран создания;
- экран выбора;
- парикмахерская;
- смена экипировки;
- превращения и shapeshift;
- транспорт;
- телепорт и смена карты;
- повторный вход и `/reload`.
## 8. Каталог данных кастомизации
Нужен единый, версионируемый источник данных, из которого генерируются клиентское и серверное представления.
Предлагаемая запись:
```text
option_id
choice_id
race_mask
gender_mask
category
label_key
render_type
char_section_id
geoset_ids
model_1
model_2
texture_1
texture_2
attachment_1
attachment_2
flags
conflict_group
unlock_condition
asset_manifest_id
enabled
```
Требования:
- `(option_id, choice_id)` никогда не переиспользуется с другим смыслом;
- удалённый вариант остаётся распознаваемым для миграции старых персонажей;
- порядок отображения UI хранится отдельно от стабильного идентификатора;
- клиент и сервер собираются из одной версии каталога;
- версия каталога входит в сетевое рукопожатие.
## 9. Серверное хранение
### Предлагаемая таблица
```sql
character_customization (
guid INT UNSIGNED NOT NULL,
option_id SMALLINT UNSIGNED NOT NULL,
choice_id INT UNSIGNED NOT NULL,
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (guid, option_id)
)
```
Дополнительно:
- индекс по `guid`;
- внешний ключ или очистка записей при удалении персонажа;
- миграция с начальными значениями для существующих персонажей;
- сервис сброса неизвестных/отключённых вариантов;
- аудит изменения внешности при необходимости.
### Серверная логика
- загрузить набор при `Character::LoadFromDB` или эквивалентном этапе;
- валидировать выбор по расе, полу, режиму игры и доступности;
- сохранять транзакционно;
- включать состояние в данные экрана выбора персонажа;
- отправлять полный snapshot при входе в мир;
- отправлять delta при изменении;
- рассылать актуальное состояние игрокам в зоне видимости;
- отправлять snapshot при появлении объекта в зоне видимости нового клиента;
- ограничить частоту изменений;
- запретить недоступные комбинации независимо от клиентского UI.
## 10. Сетевой протокол MoonWell
### Общие требования
- собственный namespace/opcode или надёжный WarcraftXL transport;
- версия протокола и версия каталога;
- явная длина сообщения и лимит числа записей;
- little-endian поля с фиксированной шириной;
- сервер является источником истины;
- неизвестные поля пропускаются или отклоняются согласно версии;
- отсутствие расширения не ломает стандартный вход в игру.
### Сообщения
```text
CMSG_MOONWELL_CUSTOMIZATION_CREATE
protocol_version
catalog_version
option_count
repeated(option_id, choice_id)
SMSG_MOONWELL_CUSTOMIZATION_RESULT
result_code
normalized choices
SMSG_MOONWELL_CUSTOMIZATION_SNAPSHOT
player_guid
catalog_version
option_count
repeated(option_id, choice_id)
CMSG_MOONWELL_CUSTOMIZATION_UPDATE
option_count
repeated(option_id, choice_id)
SMSG_MOONWELL_CUSTOMIZATION_DELTA
player_guid
option_count
repeated(option_id, choice_id)
```
### Экран выбора персонажа
Предпочтительный вариант — отдельный snapshot после получения списка персонажей. Не расширять штатный пакет без строгой проверки длины и совместимости.
Клиент должен:
- сопоставить snapshot с GUID персонажа;
- применить его к модели выбранного персонажа;
- хранить данные только до обновления от сервера;
- показывать стандартную внешность, если snapshot отсутствует или версия несовместима.
## 11. Клиентский runtime API
Планируемый API WarcraftXL:
```text
MoonWellCustomizationGetCatalogVersion()
MoonWellCustomizationGetOptions(race, gender)
MoonWellCustomizationGetChoices(optionId)
MoonWellCustomizationGetCurrent(optionId)
MoonWellCustomizationPreview(optionId, choiceId)
MoonWellCustomizationResetPreview()
MoonWellCustomizationCommitPreview()
MoonWellCustomizationRandomize()
MoonWellCustomizationApplySnapshot(guid, data)
MoonWellCustomizationClear(guid)
```
Требования:
- Lua получает только безопасные операции высокого уровня;
- нативный слой владеет указателями моделей и их жизненным циклом;
- повторное применение snapshot идемпотентно;
- preview не меняет серверное состояние;
- при ошибке модель возвращается к последнему подтверждённому snapshot;
- смена расы или пола пересчитывает допустимость всех параметров;
- отсутствие M2/текстуры отключает только конкретный вариант, а не весь экран.
## 12. Применение внешности
Порядок применения должен быть фиксированным:
1. базовая раса и пол;
2. стандартные параметры 3.3.5a;
3. `CharSections` и текстурные слои;
4. geoset базовой модели;
5. скрытие/конфликты с экипировкой;
6. независимые косметические M2-каналы;
7. обновление материалов и композитной текстуры;
8. окончательное обновление позы и камеры превью.
При смене одного параметра обновляется только затронутый слой, если это безопасно. Полная пересборка модели используется как fallback.
## 13. Интерфейс создания персонажа
### Структура
- категории слева или справа от модели;
- прокручиваемая сетка вариантов;
- текстовое название активной категории;
- состояние выбранного варианта;
- кнопки случайного выбора и сброса;
- возврат к выбору расы/класса без потери валидных параметров;
- подтверждение создания только после серверной валидации;
- плавное приближение камеры к зоне редактирования.
### Категории первой версии
- кожа;
- лицо;
- причёска;
- цвет волос;
- борода/особенность лица;
- шрамы;
- татуировки;
- серьги/пирсинг;
- расовый аксессуар.
Категории скрываются, если для текущей расы/пола нет вариантов.
### Камера
- профиль камеры задаётся по расе, полу и категории;
- отдельно настраиваются distance, вертикальная цель, горизонтальный сдвиг и FOV;
- переход использует easing через `OnUpdate`;
- короткие и средние расы имеют отдельные профили;
- при выходе из кастомизации камера плавно возвращается к полному росту;
- вращение персонажа доступно во всех режимах;
- UI не перекрывает лицо и активный аксессуар на 4:3, 16:9 и 16:10.
### Ограничения UI 3.3.5a
- XML загружается раньше Lua;
- именованные Button и ScrollFrame;
- `relativeTo`, а не `relativeKey`;
- `$parentX`, а не `$parent.X`;
- `SetWidth`/`SetHeight` вместо `SetSize`;
- таймеры и анимации через `OnUpdate`;
- UI-атласы режутся на тайлы не более 512×512.
## 14. Парикмахерская и изменение существующего персонажа
- использовать тот же каталог и preview API;
- получать текущий серверный snapshot при открытии;
- считать стоимость на сервере;
- подтверждать изменение одним атомарным запросом;
- откатывать preview при закрытии или отказе сервера;
- блокировать недоступные варианты;
- поддерживать бесплатные/платные категории отдельными правилами;
- обновлять внешность окружающим только после подтверждения.
## 15. Получение retail-ассетов без установки клиента
### Инструменты
- `wow.export` в online-режиме — поиск и предварительный просмотр;
- Wago Tools/wow-listfile — поиск пути и `FileDataID`;
- TACTSharp/TACTTool — точечная загрузка исходных файлов с CDN Blizzard.
### Манифест ассета
```text
asset_id
source_product
source_build
file_data_id
original_path
target_path
sha256
kind
parent_asset_id
license_note
```
### Пайплайн
1. Выбрать модель и исходный retail build.
2. Зафиксировать корневой `FileDataID`.
3. Выгрузить M2 и определить зависимости.
4. Загрузить `.skin`, `.skel`, `.anim` и текстуры.
5. Проверить хэши и комплектность.
6. Проверить модель в изолированном preview.
7. Проверить downport через `wxl-modern-assets`.
8. Добавить запись в каталог кастомизации.
9. Упаковать только выбранные файлы.
10. Не помещать массовый дамп retail-клиента в Git.
### Автоматические проверки ассета
- поддерживаемая версия M2/MD21;
- наличие всех skin-профилей;
- разрешимые FileDataID и пути текстур;
- наличие внешнего скелета/анимаций;
- attachment points для целевой расы;
- лимит костей на draw после разбиения;
- валидные материалы и blend modes;
- отсутствие коллизий целевых путей;
- размер итогового MPQ и память после загрузки;
- корректное освобождение ресурсов.
## 16. Этапы реализации
### Этап 0. Исследование и фиксация контрактов
- [ ] Зафиксировать ревизии WarcraftXL и всех модулей.
- [ ] Зафиксировать исходный retail build для ассетов.
- [ ] Составить карту существующих хуков персонажа и DBC.
- [ ] Проверить структуру штатных пакетов создания/выбора персонажа.
- [ ] Зафиксировать каталог option/choice ID.
- [ ] Утвердить правила доступности и монетизации.
- [ ] Выбрать первую расу для вертикального среза: человек.
**Результат:** техническая спецификация без конфликтующих точек расширения.
### Этап 1. Интеграция equip-extension
- [ ] Добавить репозиторий как закреплённый submodule.
- [ ] Адаптировать CMake/module layout.
- [ ] Собрать Win32 runtime и x64 host.
- [ ] Устранить конфликт хуков с текущим WarcraftXL.
- [ ] Исправить снятие collection M2.
- [ ] Очистить мусорные поля `ItemDisplayInfo.dbc`.
- [ ] Добавить smoke-тест Model1/Model2 на каждом поддержанном слоте.
- [ ] Добавить диагностическое логирование.
**Результат:** современная объёмная экипировка работает и корректно снимается.
### Этап 2. Независимые косметические каналы
- [ ] Отделить attachment runtime от реальных слотов экипировки.
- [ ] Реализовать реестр косметических каналов.
- [ ] Реализовать add/replace/remove/clear.
- [ ] Реализовать conflict groups и приоритеты.
- [ ] Реализовать скрытие под шлемом.
- [ ] Добавить API preview.
- [ ] Проверить смену карты, формы и пола.
**Результат:** аксессуар можно показать без создания фиктивного предмета.
### Этап 3. Asset pipeline
- [ ] Добавить входной manifest с FileDataID.
- [ ] Реализовать пакетную загрузку через CDN.
- [ ] Добавить локальный кэш по хэшу.
- [ ] Автоматически проверять зависимости.
- [ ] Формировать отчёт о пропущенных файлах.
- [ ] Интегрировать выбранные ассеты в MPQ build.
- [ ] Добавить контроль размера и дубликатов.
**Результат:** воспроизводимая сборка выбранных retail-ассетов без полного клиента.
### Этап 4. Серверное хранение и протокол
- [ ] Добавить миграцию `character_customization`.
- [ ] Реализовать серверный каталог допустимых вариантов.
- [ ] Реализовать create/update validation.
- [ ] Реализовать snapshot и delta.
- [ ] Подключить данные к character enum/select flow.
- [ ] Рассылать состояние в visibility range.
- [ ] Добавить rate limit и журнал ошибок.
- [ ] Добавить fallback для клиента без совместимой версии.
**Результат:** параметры сохраняются и одинаково видны всем клиентам.
### Этап 5. Текстурные слои и geoset
- [ ] Расширить/переопределить `CharSections` данными MoonWell.
- [ ] Реализовать слои шрамов, татуировок и макияжа.
- [ ] Реализовать таблицу geoset базовой модели.
- [ ] Синхронизировать цветовые зависимости.
- [ ] Обеспечить совместимость с надетой бронёй.
- [ ] Добавить полную пересборку как fallback.
**Результат:** работают не только прикрепляемые M2, но и полноценные параметры тела/лица.
### Этап 6. Новый UI создания персонажа
- [ ] Создать адаптивную панель категорий.
- [ ] Создать прокручиваемую сетку choices.
- [ ] Подключить preview API.
- [ ] Добавить race/gender/category camera profiles.
- [ ] Добавить randomize и reset.
- [ ] Добавить обработку недоступного ассета.
- [ ] Добавить серверное подтверждение создания.
- [ ] Проверить 4:3, 16:9, 16:10 и UI scale.
**Результат:** законченный retail-подобный пользовательский сценарий.
### Этап 7. Вертикальный срез — человек
- [ ] Мужская и женская модели.
- [ ] Стандартные категории.
- [ ] Минимум одна новая текстурная категория.
- [ ] Минимум два M2-косметических канала.
- [ ] Сохранение, выбор персонажа и отображение в мире.
- [ ] Видимость другим игрокам.
- [ ] Парикмахерская/изменение существующего персонажа.
- [ ] Полный regression-тест экипировки.
**Результат:** одна раса полностью проходит production-сценарий.
### Этап 8. Масштабирование на все расы WotLK
- [ ] Дворфы.
- [ ] Ночные эльфы.
- [ ] Гномы.
- [ ] Дренеи.
- [ ] Орки.
- [ ] Нежить.
- [ ] Таурены.
- [ ] Тролли.
- [ ] Эльфы крови.
- [ ] Отдельная настройка камер для каждого пола.
- [ ] Матрица конфликтов расовых элементов с экипировкой.
**Результат:** единый уровень качества для всех доступных рас.
### Этап 9. Стабилизация и выпуск
- [ ] Нагрузочный тест большого скопления игроков.
- [ ] Проверка утечек модели/текстур при многократной смене вариантов.
- [ ] Проверка холодного кэша WarcraftXLHost.
- [ ] Проверка отсутствующих/повреждённых ассетов.
- [ ] Версионирование manifest, каталога и протокола.
- [ ] Миграция существующих персонажей.
- [ ] Rollback-пакет.
- [ ] Документация для добавления новой категории/расы.
- [ ] Обновление deploy-скрипта и launcher manifest.
**Результат:** воспроизводимый production-релиз.
## 17. Матрица тестирования
### Персонажи
- каждая раса × каждый пол;
- стандартный режим × предатель;
- новый персонаж × существующий персонаж;
- минимальное × максимальное число выбранных параметров;
- случайная комбинация × преднамеренно конфликтная комбинация.
### Состояния клиента
- создание;
- выбор;
- загрузочный экран;
- вход в мир;
- появление другого игрока;
- телепорт;
- смерть/дух;
- транспорт;
- shapeshift/превращение;
- парикмахерская;
- смена экипировки;
- скрытие шлема/плаща;
- `/reload`;
- переподключение;
- выход и повторный вход.
### Графика
- native renderer и основной D3D9On12 путь;
- разные уровни component texture quality;
- включённые/выключенные SSAA, SSAO и AA;
- дальние LOD;
- тени;
- прозрачные и emissive материалы;
- несколько одинаковых моделей одновременно;
- 4:3, 16:9, 16:10 и разные UI scale.
### Сеть и сервер
- потеря/повтор snapshot;
- snapshot до появления модели и после неё;
- неизвестная версия каталога;
- неизвестный option/choice;
- запрещённый выбор;
- слишком большое сообщение;
- быстрое повторное изменение;
- одновременное изменение и телепорт;
- сохранение при logout/crash;
- удаление и восстановление персонажа.
## 18. Производительность
Целевые ограничения:
- не выполнять разбор каталога каждый кадр;
- не пересобирать всю модель при изменении независимого аксессуара;
- кэшировать разрешённые пути и обработанные modern assets;
- не создавать дубликаты одной и той же дочерней M2-модели;
- освобождать attachment при удалении/скрытии объекта;
- ограничить число одновременных косметических M2 на персонажа;
- иметь настройку отключения дальних косметических attachment;
- измерять время первой загрузки, потребление RAM/VRAM и frame time;
- логирование hot path отключено в release-сборке.
Предварительный бюджет первой версии:
- до 6 независимых M2-каналов на персонажа;
- не более двух моделей на канал;
- отсутствие постоянной работы Lua `OnUpdate`, кроме активной UI-анимации;
- пересборка полной внешности только при snapshot, смене расы/пола или восстановлении после ошибки.
## 19. Безопасность и целостность
- сервер проверяет все ID и комбинации;
- клиент не передаёт пути файлов серверу;
- протокол содержит только числовые option/choice ID;
- сообщения имеют максимальный размер и максимальное число записей;
- сервер нормализует дубликаты option ID;
- неизвестные значения не записываются в БД;
- стоимость изменения рассчитывается сервером;
- unlock condition проверяется сервером;
- клиентский каталог не считается доказательством доступности;
- журналировать несовпадение версии и попытки недопустимых изменений.
## 20. Сборка, доставка и откат
### Сборка
- закреплённые submodule revisions;
- asset manifest с SHA-256;
- генерация клиентского и серверного каталога из одного источника;
- сборка WarcraftXL Win32 и Host x64;
- сборка MPQ;
- автоматическая проверка оригинального `Wow.exe`;
- формирование launcher manifest.
### Версионирование
- `customization_protocol_version`;
- `customization_catalog_version`;
- `customization_asset_version`;
- совместимая версия WarcraftXL runtime.
### Откат
- сервер умеет не отправлять расширенные сообщения;
- клиент при отсутствии данных показывает стандартную внешность;
- отключение модуля не блокирует вход персонажа;
- миграция БД не удаляет стандартные параметры;
- предыдущие MPQ/runtime хранятся как отдельный release artifact;
- неизвестные сохранённые choices не удаляются автоматически без миграции.
## 21. Оценка трудоёмкости
Для одного разработчика при наличии подготовленных ассетов:
| Работа | Оценка |
|---|---:|
| Адаптация и исправление equip-extension | 47 дней |
| Независимые косметические каналы | 4–7 дней |
| Asset pipeline | 35 дней |
| Серверное хранение и протокол | 4–7 дней |
| Текстурные слои и geoset | 5–10 дней |
| Новый UI создания/изменения | 5–8 дней |
| Полный вертикальный срез одной расы | 5–10 дней |
| Каждая следующая раса | 2–5 дней |
| Стабилизация и regression | 712 дней |
Ориентиры:
- технический прототип одной расы: 1–2 недели;
- production-вертикаль людей: 3–5 недель;
- все десять рас WotLK: 2–3 месяца;
- насыщенность категориями, близкая к retail: 3–6 месяцев.
Главный переменный фактор — подготовка и проверка ассетов, а не серверная схема.
## 22. Рекомендуемый первый milestone
Вертикальный срез для людей обоих полов:
- штатные пять категорий;
- одна текстурная категория: шрамы;
- два независимых M2-канала: серьги и аксессуар для волос;
- серверное сохранение;
- snapshot на экране выбора;
- видимость другим игрокам;
- изменение через парикмахерскую;
- камера по категориям;
- корректная работа со шлемом;
- один полный набор automated/manual acceptance checks.
Этот milestone должен подтвердить всю архитектуру до масштабирования на остальные расы.
## 23. Критерии готовности production-версии
- [ ] `Wow.exe` идентичен оригиналу по SHA-256.
- [ ] Все расширения поставляются через WarcraftXL и MPQ.
- [ ] Клиент и сервер используют одинаковую версию каталога.
- [ ] Внешность сохраняется после выхода и перезапуска клиента.
- [ ] Экран создания, экран выбора и модель в мире совпадают.
- [ ] Другие игроки видят подтверждённую сервером внешность.
- [ ] Недоступные варианты невозможно применить изменённым клиентом.
- [ ] Смена/снятие варианта не оставляет collection M2.
- [ ] Нет placeholder-моделей из мусорных строк `ItemDisplayInfo.dbc`.
- [ ] Нет заметных утечек памяти при многократном preview.
- [ ] Отсутствующий ассет не приводит к падению клиента.
- [ ] Все расы и оба пола проходят тестовую матрицу.
- [ ] Камера корректно кадрирует низких, средних и высоких персонажей.
- [ ] UI работает на 4:3, 16:9, 16:10 и разных UI scale.
- [ ] Реализован и проверен rollback.
- [ ] Описан процесс добавления новой категории, choice и ассета.
## 24. Открытые решения перед началом реализации
- точная ревизия `wxl-equip-extension` для закрепления;
- список категорий первого релиза;
- исходный retail build для моделей;
- набор моделей для вертикального среза людей;
- формат единого каталога: JSON, CSV или генератор из SQL/YAML;
- транспорт custom packets между WarcraftXL и AzerothCore;
- способ передачи snapshots на экране выбора;
- политика скрытия аксессуаров под шлемом;
- правила доступности и разблокировки вариантов;
- использование парикмахерской или отдельного UI изменения внешности;
- лимит косметических M2 на одного персонажа;
- политика хранения выбранных Blizzard-ассетов и доступа к ним;
- минимальная поддерживаемая версия launcher/runtime.
Binary file not shown.
+115 -62
View File
@@ -1,90 +1,143 @@
# MoonWell Client Sources
Репозиторий с ресурсами и исполняемым файлом модифицированного клиента World of Warcraft 3.3.5a.
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
Основная цель проекта: хранить и сопровождать клиентские изменения поверх стандартных MPQ-пакетов WoW, включая интерфейс, графику, звук и дополнительные данные для кастомных систем сервера.
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса.
## Что находится в репозитории
## Что перенесено из модифицированного Wow.exe
- `Wow.exe` - модифицированный клиентский исполняемый файл.
- Поддерживает использование до 4 ГБ ОЗУ.
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
- Содержит графические элементы внутриигрового магазина.
- Основная зона изменений: `Interface/Store_UI`.
- `patch-ruRU-4/` - наши изменения поверх стандартных `patch-ruRU-1.MPQ`, `patch-ruRU-2.MPQ` и `patch-ruRU-3.MPQ`.
- Содержит ресурсы локали `ruRU`, связанные с экраном входа, `GlueXML`, заставками и звуком.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/LoginScreen`, `Sound/Ambience`, `Sound/Music`.
- `patch-ruRU-5/` - пакет русской локализации и дополнительных интерфейсных ресурсов.
- Содержит перевод на русский язык для glue-интерфейса и связанных экранов клиента.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/GluesVideo`, `Interface/Loginscreen`, `Interface/tooltips`, `Interface/cinematics`.
- `patch-Z/` - кастомный пакет для модуля Mythic+.
- Содержит графические ресурсы Mythic+.
- Также включает связанные клиентские данные в `DBFielsClient/` и иконки в `Interface/Icons/`.
- разблокировка пользовательского `GlueXML`;
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
- одиннадцатое значение `GetCharacterInfo``charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector.
## Структура по назначению
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
### Внутриигровой магазин
## Структура
Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI).
```text
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
```
Примеры ресурсов:
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
Для обновления всех зависимостей WarcraftXL:
- `Currencies/Gold.blp`
- `Currencies/Token.blp`
- `Frames/StoreFrame_Main.blp`
```powershell
git submodule update --remote vendor/warcraftxl vendor/modules/*
```
### Экран входа и локализованные ресурсы
После обновления обязательно пересоберите и проверьте вход/создание персонажа: API WarcraftXL
пока развивается.
Файлы экрана входа и связанные ресурсы находятся в [`patch-ruRU-4`](./patch-ruRU-4).
## Требования
Основные каталоги:
- Visual Studio 2022 с C++ toolchain для Win32 и x64;
- CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика;
- локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
- `Interface/GlueXML` - Lua/XML-исходники интерфейса загрузки и логина
- `Interface/Glues` - графика и модели для glue-экрана
- `Interface/LoginScreen` - фон и связанные изображения
- `Sound/Ambience/GlueScreen` - фоновые звуки
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
После клонирования инициализируйте submodule:
### Русский перевод для `patch-ruRU-5`
```powershell
git submodule update --init --recursive
```
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
## Сборка WarcraftXL
Основные каталоги:
Быстрое полное развёртывание клиента одной командой:
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
- `Interface/Loginscreen` - фон и изображения экрана входа
- `Interface/tooltips` - локализованные рамки и оформление тултипов
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
```powershell
.\deploy.ps1
```
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
### Mythic+
Полезные варианты:
Файлы Mythic+ находятся в [`patch-Z`](./patch-Z).
```powershell
# Явный путь и запуск клиента после установки
.\deploy.ps1 -ClientPath 'D:\Games\World of Warcraft' -StopClient -Launch
Основные каталоги:
# Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild
- `Interface/MythicPlus/textures` - текстуры интерфейса Mythic+
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
- `Interface/Icons` - дополнительные иконки
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
# Аварийный native D3D9 вместо D3D9On12
.\deploy.ps1 -NativeRenderer
```
## Как воспринимать содержимое репозитория
Без установки в клиент:
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
```powershell
.\build-warcraftxl.ps1 -Configuration Release
```
## Для чего используется
С установкой:
Проект нужен для сопровождения клиентской части MoonWell:
```powershell
.\build-warcraftxl.ps1 -Configuration Release `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
- кастомизации GlueXML;
- поддержки русского перевода для `patch-ruRU-5`;
- поддержки внутриигрового магазина;
- добавления графики и данных для Mythic+;
- хранения пакетов графических улучшений;
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
При `-Deploy` скрипт:
1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
установить native-переходник без modern-render:
```powershell
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
Полная сборка MPQ, установка WarcraftXL и запуск:
```powershell
$env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1
```
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и
`Utils\WarcraftXLHost.log` внутри клиента.
## MPQ-пакеты
Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки
WarcraftXL не требуется и в объектное хранилище не загружается.
Основные пакеты:
- `patch-4` — ресурсы внутриигрового магазина;
- `patch-ruRU-4` — экран входа и GlueXML;
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль,
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
BIN
View File
Binary file not shown.
+535
View File
@@ -0,0 +1,535 @@
---
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
```
+256
View File
@@ -0,0 +1,256 @@
# WoW 3.3.5a — Виджеты, события и Blizzard API
## Создание фреймов через Lua
### CreateFrame
```lua
-- CreateFrame(frameType, name, parent, template)
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
f:SetWidth(300)
f:SetHeight(200)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
f:SetFrameStrata("DIALOG")
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:SetBackdropColor(0, 0, 0, 0.8)
```
### Типы фреймов (frameType)
| Тип | Назначение |
|-----|-----------|
| `Frame` | Базовый контейнер |
| `Button` | Кнопка |
| `EditBox` | Поле ввода текста |
| `ScrollFrame` | Прокручиваемая область |
| `StatusBar` | Полоска (HP, мана, XP) |
| `Cooldown` | Кулдаун-свип на иконке |
| `GameTooltip` | Тултип |
| `MessageFrame` | Прокручиваемые сообщения |
| `Model` | 3D-модель |
| `MovieFrame` | Видео |
---
## Текстуры
```lua
local tex = frame:CreateTexture(nil, "ARTWORK")
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
tex:SetAllPoints(frame)
tex:SetWidth(32)
tex:SetHeight(32)
tex:SetPoint("CENTER", frame, "CENTER")
tex:SetVertexColor(1, 0.5, 0, 1)
tex:SetAlpha(0.8)
-- Вырезка части текстуры
tex:SetTexCoord(0, 0.5, 0, 0.5)
```
## FontString (текст)
```lua
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
fs:SetPoint("CENTER", frame, "CENTER")
fs:SetText("Привет, Азерот!")
fs:SetTextColor(1, 1, 0, 1)
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
```
### Стандартные шаблоны шрифтов
| Шаблон | Применение |
|--------|-----------|
| `GameFontNormal` | Обычный текст интерфейса |
| `GameFontNormalLarge` | Заголовки |
| `GameFontNormalSmall` | Мелкий текст |
| `GameFontHighlight` | Подсвеченный |
| `GameFontDisable` | Неактивный (серый) |
| `NumberFontNormal` | Числа |
| `ChatFontNormal` | Чат |
---
## Button
```lua
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
btn:SetWidth(80)
btn:SetHeight(22)
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
btn:SetText("Нажми")
btn:SetScript("OnClick", function(self)
print("Нажато!")
end)
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
```
### Шаблоны кнопок
| Шаблон | Вид |
|--------|-----|
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
| `UIPanelCloseButton` | Крестик закрытия окна |
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
---
## ScrollFrame
```lua
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
child:SetWidth(scroll:GetWidth())
child:SetHeight(400)
scroll:SetScrollChild(child)
```
## EditBox
```lua
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
eb:SetWidth(200)
eb:SetHeight(20)
eb:SetPoint("CENTER", frame, "CENTER")
eb:SetAutoFocus(false)
eb:SetMaxLetters(255)
eb:SetScript("OnEnterPressed", function(self)
local text = self:GetText()
self:ClearFocus()
end)
```
## StatusBar
```lua
local bar = CreateFrame("StatusBar", nil, frame)
bar:SetWidth(200)
bar:SetHeight(16)
bar:SetPoint("CENTER", frame, "CENTER")
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(0, 1, 0, 1)
bar:SetMinMaxValues(0, 100)
bar:SetValue(75)
```
---
## Скрипты-события
```lua
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("CHAT_MSG_SAY")
frame:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_ENTERING_WORLD" then
-- вход в мир
elseif event == "CHAT_MSG_SAY" then
local msg, author = ...
end
end)
```
### Ключевые события
| Событие | Когда |
|---------|-------|
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
| `PLAYER_LOGIN` | После входа |
| `PLAYER_LOGOUT` | При выходе |
| `VARIABLES_LOADED` | SavedVariables загружены |
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
### OnUpdate — таймер
```lua
local elapsed_total = 0
frame:SetScript("OnUpdate", function(self, elapsed)
elapsed_total = elapsed_total + elapsed
if elapsed_total >= 1.0 then
elapsed_total = 0
-- действие каждую секунду
end
end)
```
---
## SavedVariables
```lua
-- В TOC: ## SavedVariables: MyAddonDB
frame:RegisterEvent("ADDON_LOADED")
frame:SetScript("OnEvent", function(self, event, addonName)
if event == "ADDON_LOADED" and addonName == "MyAddon" then
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
local p = MyAddonDB.position
self:ClearAllPoints()
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
end
end)
frame:SetScript("OnMouseUp", function(self)
self:StopMovingOrSizing()
local point, _, _, x, y = self:GetPoint()
MyAddonDB.position = { point, x, y }
end)
```
---
## Полезные Blizzard-утилиты (3.3.5a)
```lua
_G["MyAddon_Frame"]:Show()
IsAddOnLoaded("MyAddon")
UnitName("player")
UnitClass("target")
UnitHealth("player"), UnitHealthMax("player")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
print("Белый текст")
UnitAffectingCombat("player")
```
---
## Часто используемые пути к текстурам Blizzard
```
Interface\DialogFrame\UI-DialogBox-Background
Interface\DialogFrame\UI-DialogBox-Border
Interface\DialogFrame\UI-DialogBox-Gold-Background
Interface\Tooltips\UI-Tooltip-Background
Interface\Tooltips\UI-Tooltip-Border
Interface\Buttons\UI-Panel-Button-Up
Interface\Buttons\UI-Panel-Button-Down
Interface\Icons\INV_Misc_QuestionMark
Interface\TargetingFrame\UI-StatusBar
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
```
---
## Типичные ошибки и решения
| Ошибка | Причина | Решение |
|--------|---------|---------|
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
+106
View File
@@ -0,0 +1,106 @@
[CmdletBinding()]
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
[string]$ClientPath = '',
[string]$PackagePath = '',
[switch]$Deploy,
[switch]$NativeRenderer
)
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
if (-not (Test-Path -LiteralPath $cmake)) {
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' }
$cmake = $cmakeCommand.Source
}
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) {
throw "Stock executable was not found: $stockExe"
}
$expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8'
$stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash
if ($stockHash -ne $expectedStockHash) {
throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)."
}
}
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
function Install-WarcraftXLArtifacts {
param([Parameter(Mandatory)][string]$Destination)
$utils = Join-Path $Destination 'Utils'
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null
Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force
Copy-Item -LiteralPath $warcraftXL -Destination (Join-Path $Destination 'WarcraftXL.dll') -Force
Copy-Item -LiteralPath $selectedProxy -Destination (Join-Path $Destination 'd3d9.dll') -Force
Copy-Item -LiteralPath $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force
Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force
}
}
if ($Deploy) {
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
$ClientPath = 'C:\Program Files (x86)\World of Warcraft'
}
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
$clientExe = Join-Path $ClientPath 'Wow.exe'
if (Test-Path -LiteralPath $clientExe -PathType Leaf) {
$clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash
if ($clientHash -ne $stockHash) {
$legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe'
if (-not (Test-Path -LiteralPath $legacyBackup)) {
Copy-Item -LiteralPath $clientExe -Destination $legacyBackup
}
Write-Host "Legacy executable saved as $legacyBackup"
}
}
Install-WarcraftXLArtifacts -Destination $ClientPath
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
}
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
Install-WarcraftXLArtifacts -Destination $PackagePath
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
}
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
+6
View File
@@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = {
".git",
".moonwell_launcher",
}
IGNORED_TOP_LEVEL_FILES = {
"Wow.moonwell-patched.backup.exe",
"WarcraftXL_on12.disable",
}
ENV_VAR_NAME = "WOW_HOME"
@@ -42,6 +46,8 @@ def is_ignored(rel_path: Path) -> bool:
return False
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
-276
View File
@@ -1,276 +0,0 @@
--[[----------------------------------------------------------------------------
C_PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
Клиентское хранилище данных Путеводителя + UI-facing API.
Соответствует контракту из player-guide-requirements.md → "Client API".
Подключается до PlayerGuide.xml/.lua в .toc.
------------------------------------------------------------------------------]]
C_PlayerGuide = C_PlayerGuide or {}
local L = C_PlayerGuide
-- ──────────────────────────────────────────────────────────────────────────
-- Состояние
local currentData = nil -- последний валидный payload от сервера
local lastError = nil -- строка с описанием последней проблемы валидации
-- Состояние UI: "data" | "empty" | "loading"
local uiState = "loading"
-- Подписчики на обновления (PlayerGuide.lua вешает сюда свой Refresh)
local listeners = {}
-- ──────────────────────────────────────────────────────────────────────────
-- Утилиты
local function notify()
for _, fn in ipairs(listeners) do
-- pcall чтобы один сломанный listener не уронил остальные
pcall(fn, uiState, currentData)
end
end
local function isTable(v) return type(v) == "table" end
-- Валидация payload'а. Возвращает true либо false + строка ошибки.
local function validate(data)
if not isTable(data) then return false, "payload is not a table" end
if data.stageID and type(data.stageID) ~= "number" then
return false, "stageID is not a number"
end
if data.progress and type(data.progress) ~= "number" then
return false, "progress is not a number"
end
if data.objectives and not isTable(data.objectives) then
return false, "objectives is not a table"
end
-- Уникальность id целей
if isTable(data.objectives) then
local seen = {}
for i, o in ipairs(data.objectives) do
if not isTable(o) then
return false, "objective #" .. i .. " is not a table"
end
if o.id and seen[o.id] then
return false, "duplicate objective id: " .. tostring(o.id)
end
if o.id then seen[o.id] = true end
end
end
return true
end
-- ──────────────────────────────────────────────────────────────────────────
-- Подписки (для PlayerGuide.lua)
function L.RegisterListener(fn)
table.insert(listeners, fn)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичный API (см. требования → Client API)
function L.SetData(data)
local ok, err = validate(data)
if not ok then
lastError = err
uiState = "empty"
notify()
return false, err
end
currentData = data
lastError = nil
uiState = "data"
notify()
return true
end
function L.Clear()
currentData = nil
lastError = nil
uiState = "empty"
notify()
end
function L.SetLoading()
if uiState ~= "data" then
uiState = "loading"
notify()
end
end
function L.GetState()
return uiState
end
function L.GetLastError()
return lastError
end
function L.HasData()
return uiState == "data" and currentData ~= nil
end
function L.GetStageInfo()
if not L.HasData() then return nil end
return currentData.stageID,
currentData.stageName,
currentData.stageDescription,
currentData.progress or 0
end
-- Доп. метод: список всех этапов (для цепочки сверху)
function L.GetStages()
if not L.HasData() then return nil end
return currentData.stages or {} -- сервер опционально шлёт массив этапов
end
function L.GetNumObjectives()
if not L.HasData() or not currentData.objectives then return 0 end
return #currentData.objectives
end
function L.GetObjectiveInfo(index)
if not L.HasData() or not currentData.objectives then return nil end
local o = currentData.objectives[index]
if not o then return nil end
return {
id = o.id,
type = o.type or "custom",
title = o.title or "",
description = o.description or o.subtitle or "",
subtitle = o.subtitle, -- наше расширение поля для UI
completed = o.completed and true or false,
progress = o.progress or 0,
required = o.required or 1,
targetID = o.targetID,
questID = o.questID,
instanceID = o.instanceID,
encounterID = o.encounterID,
achievementID = o.achievementID,
factionID = o.factionID,
itemID = o.itemID,
currencyID = o.currencyID,
order = o.order or index,
}
end
function L.GetNumRecommendations()
if not L.HasData() or not currentData.recommendations then return 0 end
return #currentData.recommendations
end
function L.GetRecommendationInfo(index)
if not L.HasData() or not currentData.recommendations then return nil end
local r = currentData.recommendations[index]
if not r then return nil end
return {
id = r.id,
type = r.type or "dungeon",
title = r.title or r.name or "",
name = r.name or r.title or "",
description = r.description or r.loc or "",
loc = r.loc, -- локация/подзаголовок
level = r.level,
biome = r.biome, -- ключ биом-обложки
instanceID = r.instanceID,
encounterID = r.encounterID,
priority = r.priority or 0,
}
end
function L.GetNumRewards()
if not L.HasData() or not currentData.rewards then return 0 end
return #currentData.rewards
end
function L.GetRewardInfo(index)
if not L.HasData() or not currentData.rewards then return nil end
local r = currentData.rewards[index]
if not r then return nil end
return r
end
-- ──────────────────────────────────────────────────────────────────────────
-- Сетевой слой (AIO)
-- MoonWellPlayerGuide.lua перехватывает SetData() из serverToClient и вызывает
-- L.SetData(payload). RequestRefresh шлёт обратно в обратную сторону.
function L.RequestRefresh()
L.SetLoading()
if AIO and AIO.Handle then
-- name берётся из serverToClient на сервере. Тут — лёгкая обёртка.
AIO.Handle("MoonWellPlayerGuide", "RequestRefresh")
else
-- Без AIO (например, в режиме разработки) — отдаём заглушку через
-- 0.5 секунды, чтобы UI не висел в Loading навечно.
if C_Timer and C_Timer.After then
C_Timer.After(0.5, function() L.Clear() end)
end
end
end
function L.RequestTrackObjective(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestTrackObjective", objectiveID)
end
end
function L.RequestOpenTarget(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestOpenTarget", objectiveID)
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Статика для разработки (видна, пока сервер не прислал реальный payload).
-- Можно убрать перед релизом.
function L._InjectDevPayload()
L.SetData({
version = 1,
stageID = 5,
stageName = "Подготовка к Нордсколу",
stageDescription =
"Завершите ключевые цепочки заданий в Запределье и докажите готовность к походу на север. "..
"По прибытии в Нордскол вам понадобится снаряжение героического уровня.",
progress = 62,
stages = {
{ id = 1, name = "Начало пути", short = "Начало пути", era = "1—58", status = "done" },
{ id = 2, name = "Огненные Недра", short = "Огненные Недра", era = "60", status = "done" },
{ id = 3, name = "Сквозь Тёмный портал", short = "Тёмный портал", era = "58—60", status = "done" },
{ id = 4, name = "Покорение Запределья", short = "Запределье", era = "60—68", status = "done" },
{ id = 5, name = "Подготовка к Нордсколу", short = "К Нордсколу", era = "68—70", status = "active" },
{ id = 6, name = "Заря Нордскола", short = "Нордскол", era = "70—78", status = "locked" },
{ id = 7, name = "Цитадель Ледяной Короны",short = "Ледяная Корона", era = "78—80", status = "locked" },
},
objectives = {
{ id = 1, type = "level", title = "Достигнуть 68 уровня",
subtitle = "Текущий уровень: 68. Можно отправляться в Нордскол.",
completed = true, progress = 68, required = 68, order = 1 },
{ id = 2, type = "quest_chain", title = "Цепочка «Поиск виверны» в Награнде",
subtitle = "Кисть Шамана • Награнд • награда: «Привязка к виверне»",
completed = false, progress = 5, required = 8, order = 2 },
{ id = 3, type = "dungeon", title = "Пройти Кузню Крови",
subtitle = "Цитадель Адского Пламени • рек. уровень 61—64",
completed = false, progress = 0, required = 1, order = 3, instanceID = 256 },
{ id = 4, type = "dungeon", title = "Пройти Аукиндон: Подземелья Аукенай",
subtitle = "Награнд • рек. уровень 64—66",
completed = false, progress = 0, required = 1, order = 4, instanceID = 257 },
{ id = 5, type = "reputation", title = "Получить «Дружелюбие» с Кенарийской экспедицией",
subtitle = "Текущее положение: Нейтрально • Долина Призрачной Луны",
completed = false, progress = 1200, required = 3000, order = 5, factionID = 942 },
},
recommendations = {
{ id = 1, type = "dungeon", name = "Кузня Крови", loc = "Цитадель Адского Пламени", level = "61—64", biome = "fire" },
{ id = 2, type = "dungeon", name = "Подземелья Аукенай", loc = "Аукиндон", level = "64—66", biome = "arcane" },
{ id = 3, type = "dungeon", name = "Сетеккские залы", loc = "Награнд", level = "67—69", biome = "storm" },
{ id = 4, type = "dungeon", name = "Темный Лабиринт", loc = "Аукиндон", level = "68—70", biome = "shadow" },
{ id = 5, type = "dungeon", name = "Ботаника", loc = "Долина Призрачной Луны", level = "69—70", biome = "nature" },
{ id = 6, type = "dungeon", name = "Расколотые Залы", loc = "Цитадель Адского Пламени", level = "70 H", biome = "fire" },
},
rewards = {}, -- блок наград временно скрыт (см. SHOW_REWARDS в PlayerGuide.lua)
})
end
-78
View File
@@ -1,78 +0,0 @@
--[[----------------------------------------------------------------------------
AIO/MoonWellPlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
AIO-handler (клиентская часть) для namespace "MoonWellPlayerGuide".
Сервер отправляет SetData(payload), клиент кладёт его в C_PlayerGuide.
Клиент шлёт RequestRefresh обратно при нажатии «Обновить».
Контракт описан в player-guide-requirements.md → "AIO Contract".
------------------------------------------------------------------------------]]
local AIO = AIO or require("AIO")
if not AIO then return end -- безопасный no-op без AIO
local handlers = AIO.AddHandlers("MoonWellPlayerGuide", {})
-- ──────────────────────────────────────────────────────────────────────────
-- Server → Client
-- Полная замена payload
function handlers.SetData(player, payload)
if C_PlayerGuide and C_PlayerGuide.SetData then
local ok, err = C_PlayerGuide.SetData(payload)
if not ok and DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage(
"|cffff8080[Путеводитель]|r Ошибка payload: "..tostring(err))
end
end
end
-- Обновление одной цели (для частичных update'ов)
function handlers.UpdateObjective(player, objective)
if not (C_PlayerGuide and C_PlayerGuide.HasData()) then return end
-- Простая стратегия: пересобрать payload локально с заменой цели по id.
-- C_PlayerGuide хранит данные internally — даём прямой доступ через _patch:
local n = C_PlayerGuide.GetNumObjectives()
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if o and o.id == objective.id then
-- Грязный, но рабочий способ для MVP — отдать обратно весь массив.
-- Для прод-уровня лучше выставить C_PlayerGuide.PatchObjective(o).
o.progress = objective.progress or o.progress
o.required = objective.required or o.required
o.completed = objective.completed
if MW_PlayerGuide and MW_PlayerGuide.Refresh then
MW_PlayerGuide.Refresh()
end
break
end
end
end
-- Замена только инфы об этапе (без массива objectives)
function handlers.SetStage(player, stagePayload)
if not stagePayload then return end
-- Объединяем с текущим payload'ом
local _, name, desc, progress = C_PlayerGuide.GetStageInfo()
C_PlayerGuide.SetData({
stageID = stagePayload.stageID,
stageName = stagePayload.stageName or name,
stageDescription = stagePayload.stageDescription or desc,
progress = stagePayload.progress or progress,
stages = stagePayload.stages,
objectives = stagePayload.objectives or {},
recommendations = stagePayload.recommendations or {},
rewards = stagePayload.rewards or {},
})
end
-- Сообщения от сервера (баблы / chat-фрейм)
function handlers.Notify(player, message)
if DEFAULT_CHAT_FRAME and message then
DEFAULT_CHAT_FRAME:AddMessage("|cffffd76a[Путеводитель]|r "..tostring(message))
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Client → Server: ничего регистрировать не нужно — это просто AIO.Handle
-- внутри C_PlayerGuide.RequestRefresh / RequestTrackObjective / RequestOpenTarget.
-402
View File
@@ -1,402 +0,0 @@
--[[----------------------------------------------------------------------------
PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
UI-логика вкладки «Путеводитель». Парный файл PlayerGuide.xml.
Зависимости:
* C_PlayerGuide (Utils/C_PlayerGuide.lua) — данные и API
* MW_PLAYER_GUIDE* (Localization/ruRU.lua) — строки
* EncounterJournal (Custom_EncounterJournal) — родительский фрейм
------------------------------------------------------------------------------]]
-- ──────────────────────────────────────────────────────────────────────────
-- Конфигурация фичи
local SHOW_REWARDS = false -- блок наград временно отключён
local SHOW_RECOMMENDATIONS = true
local DENSITY = "compact" -- compact | regular | cozy
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 } -- золото
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
-- Биом → цвет «обложки» рекомендации (rgb 0..1).
-- В будущем заменить на текстуры Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*.
local BIOME_COLOR = {
fire = { 0.83, 0.29, 0.10 },
arcane = { 0.48, 0.31, 0.83 },
storm = { 0.24, 0.56, 0.77 },
shadow = { 0.35, 0.16, 0.54 },
nature = { 0.31, 0.63, 0.29 },
holy = { 0.83, 0.69, 0.24 },
}
-- Тип цели → глиф (заменить на иконки из BLP, когда будет атлас).
local TYPE_GLYPH = {
level = "",
quest = "!",
quest_chain = "!",
dungeon = "",
raid = "",
boss = "",
achievement = "",
reputation = "",
profession = "",
item = "",
currency = "",
custom = "",
}
-- Тип цели → плейсхолдер-иконка из встроенных текстур 3.3.5a.
local TYPE_ICON = {
level = "Interface\\Icons\\Ability_Warrior_Challange",
quest = "Interface\\Icons\\INV_Misc_Note_01",
quest_chain = "Interface\\Icons\\INV_Misc_Note_05",
dungeon = "Interface\\Icons\\INV_Misc_Key_06",
raid = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
boss = "Interface\\Icons\\Ability_Warrior_DecisiveStrike",
achievement = "Interface\\Icons\\Achievement_GuildPerk_HonorableMention",
reputation = "Interface\\Icons\\INV_Scroll_03",
profession = "Interface\\Icons\\Trade_Engineering",
item = "Interface\\Icons\\INV_Misc_Gem_Diamond_02",
currency = "Interface\\Icons\\INV_Misc_Coin_17",
custom = "Interface\\Icons\\INV_Misc_QuestionMark",
}
-- ──────────────────────────────────────────────────────────────────────────
-- Хелперы
local function pct(cur, req)
if not req or req <= 0 then return 0 end
return math.floor((cur / req) * 100 + 0.5)
end
local function setRGB(fs, c) fs:SetTextColor(c.r or c[1], c.g or c[2], c.b or c[3]) end
local function showOrHide(frame, show)
if not frame then return end
if show then frame:Show() else frame:Hide() end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Цепочка этапов
local stageBadges = {} -- кэш созданных бейджей по индексу
local function buildStageChain(stages)
if not PlayerGuideFrame then return end
local container = PlayerGuideFrame.StageChain
-- Прячем лишние бейджи, если этапов стало меньше
for i = (#stages + 1), #stageBadges do
stageBadges[i]:Hide()
end
if #stages == 0 then return end
local width = container:GetWidth()
if width == 0 then width = 600 end
local slot = width / #stages
for i, s in ipairs(stages) do
local badge = stageBadges[i]
if not badge then
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
stageBadges[i] = badge
end
badge:ClearAllPoints()
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
badge:Show()
-- Текст
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
badge.Label:SetText(s.short or s.name or "")
-- Цветовая схема по статусу
local status = s.status or "locked"
if status == "active" then
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
badge.Label:SetTextColor(1.0, 0.84, 0.42)
badge.BadgeBG:SetVertexColor(1.0, 0.84, 0.42, 1)
elseif status == "done" then
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
badge.Label:SetTextColor(0.79, 0.65, 0.38)
badge.BadgeBG:SetVertexColor(0.55, 0.40, 0.18, 1)
-- Можно заменить римскую цифру на «✓» для завершённых:
badge.Roman:SetText("")
else
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
badge.Label:SetTextColor(0.40, 0.34, 0.22)
badge.BadgeBG:SetVertexColor(0.20, 0.15, 0.08, 1)
end
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки целей
local objectiveCards = {}
local CARD_HEIGHT = { compact = 44, regular = 56, cozy = 72 }
local CARD_GAP = { compact = 4, regular = 6, cozy = 8 }
local function renderObjectives()
local section = PlayerGuideFrame.ObjectivesSection
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumObjectives()
-- Скрыть лишние
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
local h = CARD_HEIGHT[DENSITY] or 44
local gap = CARD_GAP[DENSITY] or 4
local done = 0
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if not o then break end
local card = objectiveCards[i]
if not card then
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
objectiveCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
card:SetHeight(h)
card.objectiveData = o
card:Show()
-- Иконка типа
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
card.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) -- crop borders Blizz-стиль
-- Тип-плашка caps
card.TypeLabel:SetText(_G["MW_PG_TYPE_"..string.upper(o.type)] or string.upper(o.type))
-- Заголовок и подзаголовок
card.Title:SetText(o.title or "")
card.Subtitle:SetText(o.subtitle or o.description or "")
-- Прогресс
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
card.ProgressBar:SetValue(o.progress or 0)
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
-- Цвет по статусу
if o.completed then
done = done + 1
card.Title:SetTextColor(0.62, 0.84, 0.47)
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
card.Count:SetTextColor(0.62, 0.84, 0.47)
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
else
card.Title:SetTextColor(1.0, 0.91, 0.63)
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
card.Count:SetTextColor(0.79, 0.65, 0.38)
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
end
end
parent:SetHeight(math.max(1, n * (h + gap)))
section.Count:SetText(string.format("· %d / %d", done, n))
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки рекомендаций
local recCards = {}
local function renderRecommendations()
local section = PlayerGuideFrame.RecommendationsSection
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumRecommendations()
if n == 0 then section:Hide(); return end
section:Show()
for i = (n + 1), #recCards do recCards[i]:Hide() end
local cardW = 148
local gap = 8
for i = 1, n do
local r = C_PlayerGuide.GetRecommendationInfo(i)
if not r then break end
local card = recCards[i]
if not card then
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
recCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
card.recData = r
card:Show()
local biome = BIOME_COLOR[r.biome] or BIOME_COLOR.fire
card.Artwork:SetVertexColor(biome[1], biome[2], biome[3], 1)
card.Name:SetText(r.name or "")
card.Loc:SetText(r.loc or "")
card.Level:SetText(r.level or "")
end
parent:SetWidth(math.max(1, n * (cardW + gap)))
section.Count:SetText("· "..n)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Главный Refresh
local function showState(state)
local F = PlayerGuideFrame
local hasData = (state == "data")
showOrHide(F.StageChain, hasData)
showOrHide(F.Header, hasData)
showOrHide(F.ObjectivesSection, hasData)
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
showOrHide(F.EmptyState, state == "empty")
showOrHide(F.LoadingState, state == "loading")
end
local function refresh()
if not PlayerGuideFrame then return end
local state = C_PlayerGuide.GetState()
showState(state)
if state ~= "data" then return end
-- Заголовок этапа
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
local stages = C_PlayerGuide.GetStages() or {}
PlayerGuideFrame.Header.Pretitle:SetText(
string.format(MW_PLAYER_GUIDE_STAGE_OF or "Этап %d из %d", id or 0, #stages))
PlayerGuideFrame.Header.Title:SetText(name or "")
PlayerGuideFrame.Header.Description:SetText(desc or "")
PlayerGuideFrame.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
PlayerGuideFrame.Header.ProgressPercent:SetText((progress or 0) .. "%")
PlayerGuideFrame.Header.ProgressBar:SetMinMaxValues(0, 100)
PlayerGuideFrame.Header.ProgressBar:SetValue(progress or 0)
buildStageChain(stages)
renderObjectives()
renderRecommendations()
end
-- ──────────────────────────────────────────────────────────────────────────
-- XML script handlers
function MW_PlayerGuide_OnLoad(self)
-- Подписываемся на изменения C_PlayerGuide
C_PlayerGuide.RegisterListener(function() refresh() end)
end
function MW_PlayerGuide_OnShow(self)
-- Если данных ещё нет — попросим сервер
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
refresh()
end
function MW_PlayerGuide_RefreshButton_OnClick(self)
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
-- Клик по карточке цели — открывает связанный таргет (см. требования)
function MW_PlayerGuide_Objective_OnClick(self)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID)
end
elseif o.type == "boss" then
if o.encounterID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID, o.encounterID)
end
elseif o.type == "achievement" and o.achievementID then
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
if AchievementFrame_SelectAchievement then
AchievementFrame_SelectAchievement(o.achievementID)
end
elseif o.type == "item" and o.itemID then
-- Запрос подсказки предмета
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemByID(o.itemID)
GameTooltip:Show()
end
else
-- Любой нестандартный тип — пытаемся попросить сервер открыть таргет
C_PlayerGuide.RequestOpenTarget(o.id)
end
end
function MW_PlayerGuide_Objective_OnEnter(self)
local o = self.objectiveData
if not o then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
if o.subtitle and o.subtitle ~= "" then
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
end
if not o.completed then
GameTooltip:AddLine(string.format("Прогресс: %d / %d", o.progress, o.required),
0.79, 0.65, 0.38)
end
GameTooltip:Show()
end
function MW_PlayerGuide_Objective_OnLeave(self) GameTooltip:Hide() end
function MW_PlayerGuide_Rec_OnClick(self)
local r = self.recData
if r and r.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(r.instanceID)
end
end
function MW_PlayerGuide_Rec_OnEnter(self)
local r = self.recData
if not r then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(r.name or "", 1, 0.91, 0.63)
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
if r.level then GameTooltip:AddLine("Уровень: "..r.level, 0.79, 0.65, 0.38) end
GameTooltip:Show()
end
function MW_PlayerGuide_Rec_OnLeave(self) GameTooltip:Hide() end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичные хелперы для Custom_EncounterJournal.lua (routing вкладки)
function PlayerGuide_Show()
if PlayerGuideFrame then
PlayerGuideFrame:Show()
end
end
function PlayerGuide_Hide()
if PlayerGuideFrame then
PlayerGuideFrame:Hide()
end
end
-- Глобальный экспорт для отладки
MW_PlayerGuide = {
Refresh = refresh,
Show = PlayerGuide_Show,
Hide = PlayerGuide_Hide,
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
}
-498
View File
@@ -1,498 +0,0 @@
<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">
<!-- ============================================================
PlayerGuide.xml — вкладка «Путеводитель» в Encounter Journal
Канон WotLK: тёмно-синий фон, золотые акценты, Friz Quadrata.
Подключается ПОСЛЕ Custom_EncounterJournal.xml в .toc, чтобы
PlayerGuideFrame мог анкориться к EncounterJournal как parent.
============================================================ -->
<!-- ───────── Шрифты (Friz Quadrata из клиента 3.3.5a) ───────── -->
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
<Color r="0.85" g="0.78" b="0.63"/>
</Font>
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
<Color r="0.63" g="0.46" b="0.19"/>
</Font>
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
<Color r="0.66" g="0.59" b="0.47"/>
</Font>
<!-- ───────── Backdrop'ы ───────── -->
<!-- Карточка цели (полупрозрачная пергамент-плашка) -->
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
<Size><AbsDimension x="0" y="44"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border"
tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.22" g="0.15" b="0.07" a="0.65"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Иконка типа цели (32×32, золотая рамка применяется отдельной текстурой) -->
<Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
<Texture parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="42" y="42"/></Size>
<Anchors><Anchor point="CENTER" relativeKey="$parent.Icon"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="10" y="-1"/></Offset>
</Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="6" y="0"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-10" y="-6"/></Offset></Anchor>
</Anchors>
<Color r="0.79" g="0.65" b="0.38"/>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="0" y="6"/></Size>
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Icon" relativePoint="RIGHT">
<Offset><AbsDimension x="10" y="-12"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="-12"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
</Frames>
<Scripts>
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
</Scripts>
</Frame>
<!-- Карточка рекомендованного подземелья -->
<Button name="MW_PG_RecCardTemplate" virtual="true">
<Size><AbsDimension x="148" y="106"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.10" g="0.07" b="0.03" a="0.85"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Биом-плашка (плейсхолдер: цветной градиент) -->
<Texture parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="138" y="58"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Level" inherits="GameFontNormalSmall" justifyH="CENTER">
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.Artwork" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-4" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="130" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Artwork" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="130" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Name" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
</Scripts>
</Button>
<!-- Бейдж этапа в цепочке (круглая золотая монета с цифрой) -->
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
<Size><AbsDimension x="38" y="38"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BadgeBG" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- ───────── Корневой фрейм ───────── -->
<!-- Парент: EncounterJournal.encounter (см. PlayerGuide.lua → Init) -->
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-58"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<!-- «Волна» сверху, как в оригинальном EJ — добавляется через
родительский фон. Здесь — тонкая золотая внутренняя рамка. -->
<Layer level="BORDER">
<Texture parentKey="TopGlow" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.84" b="0.42" a="0.35"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- 1) Цепочка этапов сверху -->
<Frame parentKey="StageChain">
<Size><AbsDimension x="0" y="62"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-4"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-8" y="-4"/></Offset></Anchor>
</Anchors>
<!-- Бейджи создаются программно в Lua (PlayerGuide_BuildStageChain) -->
</Frame>
<!-- 2) Заголовок текущего этапа -->
<Frame parentKey="Header">
<Size><AbsDimension x="0" y="78"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.StageChain" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="12" y="-12"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.StageChain" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-12" y="-12"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Pretitle" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Title" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-6"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
<Size><AbsDimension x="240" y="28"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressLabel" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="240" y="10"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressPercent" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="100" y="22"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressBar" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 3) Секция «Задачи этапа» — заголовок + список карточек -->
<Frame parentKey="ObjectivesSection">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Header" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="-2" y="-10"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.Header" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="2" y="-10"/></Offset>
</Anchor>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="142"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="600" y="200"/></Size>
<!-- Карточки целей вставляются программно через ObjectiveCardTemplate -->
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 4) Секция «Подходящие подземелья» — горизонтальная карусель -->
<Frame parentKey="RecommendationsSection">
<Size><AbsDimension x="0" y="132"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="6" y="6"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="900" y="110"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 5) Пустое состояние -->
<Frame parentKey="EmptyState" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="200"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="28"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="420" y="60"/></Size>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="120" y="24"/></Size>
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Body" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-14"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 6) Состояние загрузки -->
<Frame parentKey="LoadingState" hidden="true">
<Anchors><Anchor point="CENTER"/></Anchors>
<Size><AbsDimension x="300" y="80"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
<Anchors><Anchor point="TOP"/></Anchors>
<Size><AbsDimension x="300" y="20"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="300" y="16"/></Size>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
</Scripts>
</Frame>
</Ui>
-64
View File
@@ -1,64 +0,0 @@
# PlayerGuide — клиентский пакет (3.3.5a / MoonWellClient)
Это материалы для интеграции вкладки **«Путеводитель»** в существующий
ретропортированный Encounter Journal. Реализация соответствует требованиям
из `uploads/player-guide-requirements.md` и опирается на канон-визуал
WotLK (Friz Quadrata, тёмно-синий фон EJ, золотые акценты).
## Структура
```
MoonWellClient/
├── EncounterJournal/
│ ├── Utils/
│ │ └── C_PlayerGuide.lua ← хранилище + API
│ ├── Custom_EncounterJournal/
│ │ ├── Custom_EncounterJournal.xml ← патч: новая вкладка
│ │ ├── Custom_EncounterJournal.lua ← патч: routing вкладки
│ │ ├── PlayerGuide.xml ← фреймы и шаблоны (НОВОЕ)
│ │ └── PlayerGuide.lua ← рендер и логика (НОВОЕ)
│ └── Localization/
│ └── ruRU.lua ← патч: MW_PLAYER_GUIDE и др.
├── AIO/
│ └── MoonWellPlayerGuide.lua ← AIO-handler (клиентская часть)
└── MoonWellClient.toc ← патч: подключение файлов
```
## Порядок интеграции
1. **Скопируйте новые файлы** в проект:
* `client-export/PlayerGuide.xml``EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml`
* `client-export/PlayerGuide.lua``EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua`
* `client-export/C_PlayerGuide.lua``EncounterJournal/Utils/C_PlayerGuide.lua`
* `client-export/MoonWellPlayerGuide.lua``AIO/MoonWellPlayerGuide.lua`
2. **Примените патчи** из `client-export/patches/` к существующим файлам
(см. ниже — `Custom_EncounterJournal.xml`, `Custom_EncounterJournal.lua`,
`MoonWellClient.toc`, `Localization/ruRU.lua`).
3. **Перезагрузите интерфейс** (`/reload`). Если AIO ещё не передал
payload — увидите «пустое» состояние с кнопкой «Обновить».
## Что вошло в MVP (по требованиям)
| MVP-пункт | Где |
| ----------------------------- | ------------------------------------------- |
| вкладка `Путеводитель` | `Custom_EncounterJournal.xml` патч |
| пустой `PlayerGuideFrame` | `PlayerGuide.xml` |
| `C_PlayerGuide.lua` + статика | `C_PlayerGuide.lua` |
| заголовок этапа + цели | `PlayerGuide.lua → PlayerGuide_Refresh` |
| tab switching / hide-show | `Custom_EncounterJournal.lua → EJ_ContentTab_OnClick` хук |
| AIO `SetData` | `MoonWellPlayerGuide.lua → handlers.SetData` |
| Обновить (RequestRefresh) | `PlayerGuide.lua → PlayerGuide_RefreshButton_OnClick` |
## Что отложено (см. «Later UI» в требованиях)
* Фильтры (все / активные / завершённые)
* Pinning отслеживаемой цели
* **Награды** — блок временно отключён (`SHOW_REWARDS = false`)
* Серверные уведомления (`Notify`)
## Сноска по визуалу
Текстуры в XML — это встроенные ресурсы 3.3.5a-клиента. Если у вас
есть атлас MoonWell — замените пути в блоке `-- TEXTURE_ATLAS` на свои.
Биом-обложки рекомендаций (`Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*`)
надо создать отдельно — это плейсхолдеры (см. таблицу в `PlayerGuide.lua`).
@@ -1,120 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Custom_EncounterJournal.lua.patch
─────────────────────────────────────────────────────────────────────────────
Патч-сниппеты для существующего Custom_EncounterJournal.lua. Эти куски
добавляют routing новой вкладки и hide-show поведение по требованиям:
«при выборе guide tab:
- hide instance cards
- hide Encounter Journal detail panel
- hide suggested content panel
- hide loot journal panel
- hide or disable the expansion/tier dropdown unless explicitly needed
- show PlayerGuideFrame»
------------------------------------------------------------------------------]]
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
-- НАЙТИ функцию EJ_ContentTab_Select (или ту, что переключает вкладки в
-- Encounter Journal). Обычно она выглядит как switch по id вкладки.
-- ДОБАВИТЬ ветку для нашей вкладки. Пример полностью переписанной функции:
local PLAYERGUIDE_TAB_ID = 1 -- должен совпадать с id="1" в XML патче
function EJ_ContentTab_Select(id)
EncounterJournal.suggestTab:UnlockHighlight()
EncounterJournal.dungeonsTab:UnlockHighlight()
EncounterJournal.raidsTab:UnlockHighlight()
if EncounterJournal.lootJournalTab then
EncounterJournal.lootJournalTab:UnlockHighlight()
end
if EncounterJournal.guideTab then
EncounterJournal.guideTab:UnlockHighlight()
end
-- Прячем «обычные» панели EJ. Этот хелпер уже есть в Custom_EncounterJournal:
-- EncounterJournal_HideAllPanels()
-- Если его нет — выньте тело сюда (Hide instance list, encounter, loot).
EncounterJournal_HideAllPanels()
-- Прячем PlayerGuideFrame — он показывается только на guide-вкладке.
if PlayerGuideFrame then PlayerGuideFrame:Hide() end
if id == PLAYERGUIDE_TAB_ID then
-- НАША ВКЛАДКА -----------------------------------------------------
EncounterJournal.guideTab:LockHighlight()
EncounterJournal.instanceSelect:Hide()
EncounterJournal.encounter:Hide()
if EncounterJournalSuggestFrame then EncounterJournalSuggestFrame:Hide() end
if LootJournal then LootJournal:Hide() end
-- Скрыть/задисейблить дропдаун расширений, как просят требования:
if EJTierDropDown then EJTierDropDown:Hide() end
if PlayerGuideFrame then PlayerGuideFrame:Show() end
return
end
-- ── Остальные вкладки — оригинальная логика ──
if EJTierDropDown then EJTierDropDown:Show() end
if id == 2 then -- SuggestTab (было 1)
EncounterJournal.suggestTab:LockHighlight()
EncounterJournal_ListInstances() -- или ваш Suggest-Show
elseif id == 3 then -- DungeonTab
EncounterJournal.dungeonsTab:LockHighlight()
EJ_HideNonInstancePanels()
EncounterJournal_ListInstances()
elseif id == 4 then -- RaidTab
EncounterJournal.raidsTab:LockHighlight()
EJ_HideNonInstancePanels()
EncounterJournal_ListInstances()
elseif id == 5 then -- LootJournalTab
if EncounterJournal.lootJournalTab then
EncounterJournal.lootJournalTab:LockHighlight()
end
if LootJournal_Show then LootJournal_Show() end
end
end
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
-- В EJ_ContentTab_OnClick добавить вызов нашего Select для tab id=1
-- (если у вас один централизованный _OnClick — он уже передаёт id и
-- ничего дополнительно делать не нужно):
function EJ_ContentTab_OnClick(self)
EJ_ContentTab_Select(self:GetID())
PlaySound("igCharacterInfoTab")
end
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
-- Защита: при открытии EJ через EncounterJournal_OpenJournal — переключиться
-- НА dungeon/raid таб, а не оставлять guide-таб выбранным.
-- Найдите вашу EncounterJournal_OpenJournal и убедитесь, что в начале:
local _orig_EncounterJournal_OpenJournal = EncounterJournal_OpenJournal
function EncounterJournal_OpenJournal(instanceID, encounterID, ...)
-- При прямом открытии конкретного instance — переключиться на Dungeon/Raid
if instanceID then
EJ_ContentTab_Select(3) -- Dungeon по умолчанию; ваша логика может выбрать Raid
end
return _orig_EncounterJournal_OpenJournal(instanceID, encounterID, ...)
end
-- ─── ПАТЧ #4 ───────────────────────────────────────────────────────────────
-- Init: статика для разработки (можно убрать перед релизом). Если AIO
-- ещё не успел прислать payload — покажем dev-payload через 1с после
-- первого открытия EJ.
local _devInjected = false
EncounterJournal:HookScript("OnShow", function()
if not _devInjected and not C_PlayerGuide.HasData() then
_devInjected = true
C_Timer.After(1.0, function()
if not C_PlayerGuide.HasData() then
C_PlayerGuide._InjectDevPayload()
end
end)
end
end)
@@ -1,57 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Custom_EncounterJournal.xml.patch
─────────────────────────────────────────────────────────────────────────────
Это НЕ замена файла — это патч-сниппеты для существующего
Custom_EncounterJournal.xml. Вставьте обозначенные блоки.
В оригинальном Custom_EncounterJournal.xml внутри EncounterJournal.bg или
аналогичного контейнера уже есть вкладки SuggestTab / DungeonTab / RaidTab /
LootJournalTab. Нужно вставить GuideTab ПЕРЕД ними (id=1) и сдвинуть
анкоры остальных.
------------------------------------------------------------------------------]]
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
-- НАЙТИ блок вкладки SuggestTab (или то, что у вас сейчас называется
-- «$parentSuggestTab» / «$parentDungeonTab») и ДОБАВИТЬ перед ним
-- следующую кнопку. id=1, чтобы EJ_ContentTab_OnClick знал об этой вкладке.
<Button name="$parentGuideTab"
inherits="EncounterTierTabTemplate"
text="MW_PLAYER_GUIDE"
parentKey="guideTab"
id="1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
<Offset><AbsDimension x="40" y="2"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="EJ_ContentTab_OnClick"/>
</Scripts>
</Button>
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
-- ИЗМЕНИТЬ id существующих вкладок (теперь Путеводитель = 1):
--
-- SuggestTab id="2" (было 1)
-- DungeonTab id="3" (было 2)
-- RaidTab id="4" (было 3)
-- LootJournalTab id="5" (было 4)
--
-- И СДВИНУТЬ анкор первой следующей вкладки (SuggestTab), чтобы она
-- цеплялась за GuideTab:
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentGuideTab" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-15" y="0"/></Offset>
</Anchor>
</Anchors>
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
-- В конце <Ui>... </Ui>, ПЕРЕД закрывающим тегом, добавить include
-- для нашего PlayerGuide.xml, чтобы PlayerGuideFrame создавался ВНУТРИ
-- родителя EncounterJournal:
<Include file="PlayerGuide.xml"/>
@@ -1,43 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Localization-ruRU.lua.patch
─────────────────────────────────────────────────────────────────────────────
Строки локализации для вкладки «Путеводитель».
Вставьте этот блок в ваш существующий Localization/ruRU.lua (или
ruRU.lua в корне), либо подключите как отдельный файл и добавьте в .toc.
------------------------------------------------------------------------------]]
-- Имя вкладки
MW_PLAYER_GUIDE = "Путеводитель"
-- Заголовки секций
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
-- Шапка
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
MW_PLAYER_GUIDE_REFRESH = "Обновить"
-- Пустое состояние
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. "..
"Это может занять несколько секунд после входа в игру."
-- Загрузка
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
-- Caps-плашки типов целей (используются в карточке цели)
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
MW_PG_TYPE_RAID = "РЕЙД"
MW_PG_TYPE_BOSS = "БОСС"
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
@@ -1,28 +0,0 @@
# patches/MoonWellClient.toc.patch
#
# Сниппет для добавления в существующий MoonWellClient.toc. ВАЖНО: порядок
# подключения файлов критичен — C_PlayerGuide.lua должен идти ДО PlayerGuide.lua,
# а PlayerGuide.xml — ПОСЛЕ Custom_EncounterJournal.xml (PlayerGuideFrame
# анкорится к EncounterJournal как parent).
#
# Вставьте строки ниже в соответствующие места вашего .toc.
# ─── Локализация ────────────────────────────────────────────────────────────
# В блок Localization, рядом с остальными ruRU.lua:
Localization\ruRU.lua
# ─── Хранилище и API ────────────────────────────────────────────────────────
# В блок EncounterJournal\Utils\, до подключения чего-либо из
# Custom_EncounterJournal\:
EncounterJournal\Utils\C_PlayerGuide.lua
# ─── Сам Encounter Journal (ОБНОВЛЁННЫЙ ПОРЯДОК) ────────────────────────────
# Эти строки заменяют ваш существующий блок Custom_EncounterJournal:
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.lua
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
# ─── AIO-handler ────────────────────────────────────────────────────────────
# Должен подключаться ПОСЛЕ C_PlayerGuide.lua и AIO/AIO_Client.lua:
AIO\MoonWellPlayerGuide.lua
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

-125
View File
@@ -1,125 +0,0 @@
# PlayerGuide — текстуры
Все файлы — **PNG с альфа-каналом**, размеры — **степень двойки**
(требование BLP-конвертера). Палитра канон-WotLK: золото (`#ffd76a`)
на тёмно-коричневом (`#1a1208`).
## Что есть
| Файл | Размер | Куда |
|---|---|---|
| `StageBadge-Active.png` | 64×64 | Бейдж текущего этапа в цепочке. |
| `StageBadge-Done.png` | 64×64 | Бейдж завершённого этапа. |
| `StageBadge-Locked.png` | 64×64 | Бейдж недоступного этапа. |
| `ProgressBar-Fill.png` | 128×16 | Заливка прогресс-бара (`BarTexture`). |
| `ProgressBar-BG.png` | 128×16 | Подложка прогресс-бара. |
| `Card-Background.png` | 256×256 | Тайл-фон карточек (`bgFile`, tileable). |
| `Card-Border.png` | 256×32 | Рамка карточки (`edgeFile`, 8 сегментов 32×32: up/down/left/right/TL/TR/BL/BR). |
| `Section-Divider.png` | 256×16 | Декоративный разделитель секций. |
| `Corner-Ornament.png` | 64×64 | Угол декоративной рамки фрейма. |
| `Top-Wave-Glow.png` | 512×64 | «Волна» свечения сверху (как в оригинальном EJ). |
| `Biome-Fire.png` | 256×128 | Обложка рекомендации — огненные локации. |
| `Biome-Arcane.png` | 256×128 | Тайная магия. |
| `Biome-Storm.png` | 256×128 | Буря/вода. |
| `Biome-Shadow.png` | 256×128 | Тьма. |
| `Biome-Nature.png` | 256×128 | Природа. |
| `Biome-Holy.png` | 256×128 | Свет/нежить. |
| `TypeIcon-Level.png` | 64×64 | Иконка цели «уровень». |
| `TypeIcon-Quest.png` | 64×64 | «задание». |
| `TypeIcon-QuestChain.png` | 64×64 | «цепочка». |
| `TypeIcon-Dungeon.png` | 64×64 | «подземелье». |
| `TypeIcon-Raid.png` | 64×64 | «рейд». |
| `TypeIcon-Boss.png` | 64×64 | «босс». |
| `TypeIcon-Achievement.png` | 64×64 | «достижение». |
| `TypeIcon-Reputation.png` | 64×64 | «репутация». |
| `TypeIcon-Profession.png` | 64×64 | «профессия». |
| `TypeIcon-Item.png` | 64×64 | «предмет». |
| `TypeIcon-Currency.png` | 64×64 | «валюта». |
| `TypeIcon-Custom.png` | 64×64 | прочее. |
| `TypeIcon-Done.png` | 64×64 | Цель завершена (зелёная галочка). |
| `TypeIcon-Locked.png` | 64×64 | Цель заблокирована (замок). |
| `Button-Up.png` | 128×32 | Кнопка «Обновить», нормальное. |
| `Button-Down.png` | 128×32 | Кнопка нажата. |
| `Button-Highlight.png` | 128×32 | Hover. |
## Куда положить в проекте
```
MoonWellClient/EncounterJournal/Textures/PlayerGuide/
├── StageBadge-Active.blp
├── StageBadge-Done.blp
├── ...
└── Button-Highlight.blp
```
И в XML использовать пути:
```
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active"
```
(WoW сам подставит `.blp`.)
## Конвертация PNG → BLP
Любой из:
- **BLPNG Converter** (Windows, GUI) — самый простой, drag-and-drop;
- **BLPLab** — с предпросмотром;
- **CLI**: `blpconverter input.png output.blp` (есть пайплайны на GitHub).
Параметры:
- **Compression:** `DXT5` для всего с альфой (всё, кроме `Card-Background`
для него `DXT1` или `Uncompressed`, если хочется лучше);
- **Mipmaps:** включить (`Generate Mipmaps`), кроме `ProgressBar-Fill/BG`
(тонкие полоски ломаются на мипмапах — там лучше off);
- **Sampling:** `Bicubic` или `Lanczos`.
## Замена биом-обложек
Сейчас биом-обложки — стилизованные плашки с аркой и светящимся центром.
Их можно полностью заменить вашими собственными — главное, оставить размер
**256×128** и имена файлов. Если поменяете список биомов, поправьте таблицу
`BIOME_COLOR` в `PlayerGuide.lua`.
## Замена иконок типов целей
Если у вас есть атлас иконок WoW (например, из `Interface/Icons/`) — карта
`TYPE_ICON` в `PlayerGuide.lua` уже использует встроенные текстуры
клиента. Текстуры `TypeIcon-*` нужны, только если хочется единого
визуального словаря (рамка + глиф).
## Применение в XML (примеры)
```xml
<!-- Бейдж в цепочке этапов: меняем встроенный UI-Quickslot2 на наш -->
<Texture parentKey="BadgeBG"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active">
<Size><AbsDimension x="44" y="44"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
<!-- Прогресс-бар: BarTexture указывает на Fill, BG ставится в Layer BACKGROUND -->
<StatusBar parentKey="ProgressBar">
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
<!-- Карточка цели: bgFile = Card-Background, edgeFile = Card-Border -->
<Backdrop bgFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Background"
edgeFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Border"
tile="true">
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
<TileSize><AbsValue val="64"/></TileSize>
<EdgeSize><AbsValue val="16"/></EdgeSize>
</Backdrop>
```
## Сноска
Это плейсхолдеры на уровне «лучше, чем встроенные UI-Quickslot2». Если в
проекте есть художник — финальные текстуры стоит делать в этом же
визуальном словаре, чтобы Lua/XML менять не пришлось (только перерисовать
PNG того же размера и сконвертировать).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

-263
View File
@@ -1,263 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>PlayerGuide — Texture Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet" />
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
font-family: 'EB Garamond', Georgia, serif;
background:
radial-gradient(ellipse at 50% 0%, #1c2c48 0%, transparent 60%),
radial-gradient(ellipse at 100% 100%, #0d1422 0%, transparent 70%),
linear-gradient(180deg, #0a1018 0%, #050810 100%);
color: #d8c8a0;
padding: 60px 50px 80px;
}
h1 {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 36px; margin: 0 0 6px;
color: #ffd76a;
text-shadow: 0 0 14px rgba(197,151,46,0.5), 0 2px 0 #000;
letter-spacing: 0.02em;
}
.sub {
font-style: italic; color: #a89878;
margin: 0 0 36px; font-size: 16px;
}
.group {
margin: 0 0 44px;
}
.group-title {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
color: #ffd76a; text-shadow: 0 1px 0 #000;
display: flex; align-items: center; gap: 12px;
margin: 0 0 18px;
}
.group-title::before {
content: ''; width: 3px; height: 14px;
background: #ffd76a; box-shadow: 0 0 6px #ffd76a88;
}
.group-title::after {
content: ''; flex: 1; height: 1px;
background: linear-gradient(90deg, rgba(138,106,38,0.5), transparent);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
}
.item {
background: linear-gradient(180deg, rgba(28,44,72,0.4), rgba(10,16,24,0.4));
border: 1px solid rgba(255,215,106,0.18);
border-radius: 3px;
padding: 12px 12px 10px;
display: flex; flex-direction: column; align-items: center;
}
.preview {
width: 100%; aspect-ratio: 1 / 1;
display: flex; align-items: center; justify-content: center;
background:
linear-gradient(45deg, #0d0905 25%, transparent 25%),
linear-gradient(-45deg, #0d0905 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #0d0905 75%),
linear-gradient(-45deg, transparent 75%, #0d0905 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
background-color: #18120a;
border: 1px solid rgba(125,90,42,0.4);
margin-bottom: 8px;
overflow: hidden;
}
.preview.wide { aspect-ratio: 2 / 1; }
.preview.bar { aspect-ratio: 4 / 1; }
.preview img {
max-width: 100%; max-height: 100%;
image-rendering: pixelated;
}
.name {
font-family: 'Cinzel', serif; font-weight: 600;
font-size: 11px; color: #ffe9a0;
text-shadow: 0 1px 0 #000;
letter-spacing: 0.04em; text-align: center;
line-height: 1.2;
}
.size {
font-size: 10px; color: #a89878;
font-style: italic; margin-top: 3px;
font-variant-numeric: tabular-nums;
}
/* Showcase row */
.showcase {
margin: 24px 0 40px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.showcase-card {
background: linear-gradient(180deg, rgba(60,40,18,0.35), rgba(25,15,8,0.55));
border: 1px solid #5a4220;
box-shadow: inset 0 1px 0 rgba(255,200,100,0.18), 0 4px 12px rgba(0,0,0,0.5);
border-radius: 2px;
padding: 18px;
display: flex; gap: 14px; align-items: stretch;
}
.showcase-card h3 {
margin: 0 0 4px;
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 14px; color: #ffe9a0; text-shadow: 0 1px 0 #000;
}
.showcase-card p {
margin: 0; font-style: italic; font-size: 12px; color: #a89878;
}
.progress-demo {
margin-top: 10px;
height: 14px; width: 100%; position: relative;
background: url('Card-Background.png');
}
</style>
</head>
<body>
<h1>PlayerGuide — лист текстур</h1>
<p class="sub">33 PNG-файла, готовы к конвертации в BLP. Шахматный фон под превью = прозрачность.</p>
<!-- Demo: показываем как они стыкуются вместе -->
<div class="group">
<div class="group-title">Демонстрация (как это смотрится в карточке)</div>
<div class="showcase">
<div class="showcase-card">
<img src="TypeIcon-Dungeon.png" width="44" height="44" alt="" />
<div style="flex:1;">
<h3>Пройти Кузню Крови</h3>
<p>Цитадель Адского Пламени • рек. уровень 61—64</p>
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<div style="flex:1; height:8px; background: url('ProgressBar-BG.png'); position:relative;">
<div style="width:35%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
<span style="font-family:Cinzel,serif; font-size:11px; color:#caa760;">0 / 1</span>
</div>
</div>
</div>
<div class="showcase-card" style="background: url('Card-Background.png'); border-color: #5a4220;">
<img src="StageBadge-Active.png" width="48" height="48" alt="" />
<div style="flex:1;">
<h3 style="color:#ffd76a;">Подготовка к Нордсколу</h3>
<p>Этап V из VII · 62% завершено</p>
<div style="margin-top:10px; height:14px; position:relative; background: url('ProgressBar-BG.png');">
<div style="width:62%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Группы текстур -->
<div class="group">
<div class="group-title">Бейджи этапов</div>
<div class="grid" id="stage"></div>
</div>
<div class="group">
<div class="group-title">Прогресс-бар</div>
<div class="grid" id="progress"></div>
</div>
<div class="group">
<div class="group-title">Карточка</div>
<div class="grid" id="card"></div>
</div>
<div class="group">
<div class="group-title">Декор</div>
<div class="grid" id="decor"></div>
</div>
<div class="group">
<div class="group-title">Обложки рекомендаций (биомы)</div>
<div class="grid" id="biome"></div>
</div>
<div class="group">
<div class="group-title">Иконки типов целей</div>
<div class="grid" id="types"></div>
</div>
<div class="group">
<div class="group-title">Иконки статусов</div>
<div class="grid" id="status"></div>
</div>
<div class="group">
<div class="group-title">Кнопки</div>
<div class="grid" id="buttons"></div>
</div>
<script>
const groups = {
stage: [
['StageBadge-Active', '64×64'],
['StageBadge-Done', '64×64'],
['StageBadge-Locked', '64×64'],
],
progress: [
['ProgressBar-Fill', '128×16', 'bar'],
['ProgressBar-BG', '128×16', 'bar'],
],
card: [
['Card-Background', '256×256'],
['Card-Border', '256×32', 'bar'],
],
decor: [
['Section-Divider', '256×16', 'bar'],
['Corner-Ornament', '64×64'],
['Top-Wave-Glow', '512×64', 'bar'],
],
biome: [
['Biome-Fire', '256×128', 'wide'],
['Biome-Arcane', '256×128', 'wide'],
['Biome-Storm', '256×128', 'wide'],
['Biome-Shadow', '256×128', 'wide'],
['Biome-Nature', '256×128', 'wide'],
['Biome-Holy', '256×128', 'wide'],
],
types: [
['TypeIcon-Level', '64×64'],
['TypeIcon-Quest', '64×64'],
['TypeIcon-QuestChain', '64×64'],
['TypeIcon-Dungeon', '64×64'],
['TypeIcon-Raid', '64×64'],
['TypeIcon-Boss', '64×64'],
['TypeIcon-Achievement', '64×64'],
['TypeIcon-Reputation', '64×64'],
['TypeIcon-Profession', '64×64'],
['TypeIcon-Item', '64×64'],
['TypeIcon-Currency', '64×64'],
['TypeIcon-Custom', '64×64'],
],
status: [
['TypeIcon-Done', '64×64'],
['TypeIcon-Locked', '64×64'],
],
buttons: [
['Button-Up', '128×32', 'bar'],
['Button-Down', '128×32', 'bar'],
['Button-Highlight', '128×32', 'bar'],
],
};
for (const [groupId, items] of Object.entries(groups)) {
const el = document.getElementById(groupId);
for (const [name, size, shape] of items) {
const item = document.createElement('div');
item.className = 'item';
item.innerHTML = `
<div class="preview ${shape || ''}">
<img src="${name}.png" alt="${name}" />
</div>
<div class="name">${name}</div>
<div class="size">${size}</div>
`;
el.appendChild(item);
}
}
</script>
</body>
</html>
+135
View File
@@ -0,0 +1,135 @@
[CmdletBinding()]
param(
[string]$ClientPath = '',
[string]$PackagePath = '',
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
[switch]$SkipDataBuild,
[switch]$SkipManifest,
[switch]$NativeRenderer,
[switch]$StopClient,
[switch]$Launch
)
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
function Import-ProjectEnvironment {
$envFile = Join-Path $repoRoot '.env'
if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) { return }
foreach ($line in Get-Content -LiteralPath $envFile -Encoding UTF8) {
if ($line -notmatch '^\s*([^#][^=]+)=(.*)$') { continue }
$name = $matches[1].Trim()
$value = $matches[2].Trim().Trim('"').Trim("'")
if ($name -and -not (Test-Path "Env:$name")) {
Set-Item -Path "Env:$name" -Value $value
}
}
}
Import-ProjectEnvironment
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
$ClientPath = if ($env:WOW_HOME) {
$env:WOW_HOME
} else {
'C:\Program Files (x86)\World of Warcraft'
}
}
if ([string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = Join-Path $repoRoot 'dist'
}
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
})
if ($runningClients.Count) {
if (-not $StopClient) {
throw 'Wow.exe is running. Close the client or repeat deployment with -StopClient.'
}
Write-Host 'Stopping the running WoW client...'
$runningClients | Stop-Process -Force
Start-Sleep -Seconds 2
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
} | Stop-Process -Force
}
$env:WOW_HOME = $ClientPath
Write-Host "MoonWell client: $ClientPath"
Write-Host "Package staging: $PackagePath"
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
if (-not $SkipDataBuild) {
Write-Host '[2/5] Building MPQ patches...'
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' }
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
}
& $tool (Join-Path $repoRoot 'src') $PackagePath
if ($LASTEXITCODE -ne 0) { throw 'MPQ patch build failed.' }
} else {
Write-Host '[2/5] MPQ build skipped.'
}
Write-Host '[3/5] Building and installing WarcraftXL...'
$buildArguments = @{
Configuration = $Configuration
ClientPath = $ClientPath
PackagePath = $PackagePath
Deploy = $true
}
if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
& (Join-Path $repoRoot 'build-warcraftxl.ps1') @buildArguments
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
Write-Host '[4/5] Synchronizing package files...'
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
$robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -ge 8) {
throw "Package synchronization failed with robocopy exit code $robocopyExitCode."
}
if (-not $SkipManifest) {
Write-Host '[5/5] Building launcher manifest...'
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' }
& $python.Source (Join-Path $repoRoot 'build_manifest.py') `
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' }
} else {
Write-Host '[5/5] Manifest generation skipped.'
}
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
Write-Host ''
Write-Host 'Deployment complete.'
Write-Host " Renderer: $renderer"
Write-Host " Wow.exe SHA-256: $wowHash"
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
if ($Launch) {
Write-Host 'Launching Wow.exe...'
Start-Process -FilePath (Join-Path $ClientPath 'Wow.exe') -WorkingDirectory $ClientPath
}
# robocopy uses successful non-zero exit codes; do not leak one to callers.
$global:LASTEXITCODE = 0
-448
View File
@@ -1,448 +0,0 @@
# Player Guide Requirements
## Goal
Create a `Путеводитель` tab inside the existing Encounter Journal UI.
The guide must show personal character progress calculated by `mod-individual-progress` and recommend the next relevant content: quests, dungeons, raids, bosses, requirements, activities, and rewards.
This feature is not a third Encounter Journal instance type. It is a separate panel inside the same interface.
## High-Level Architecture
```text
AzerothCore / mod-individual-progress
-> calculates player progress
-> builds a guide payload
AIO / MoonWell bridge
-> sends guide data to the client
MoonWellClient / EncounterJournal
-> receives the payload
-> stores it in C_PlayerGuide
-> renders the Player Guide tab
```
## Client Components
Add separate client-side files:
```text
EncounterJournal/Utils/C_PlayerGuide.lua
EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua
EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml
```
Responsibilities:
```text
C_PlayerGuide.lua
- stores current guide data
- exposes a small UI-facing API
- receives server payloads
- requests refreshes from the server
PlayerGuide.lua
- renders guide data
- refreshes objective rows
- handles clicks on objectives/recommendations
PlayerGuide.xml
- defines the guide panel
- defines static frames, headers, scroll area, row templates
```
Register these files in `MoonWellClient.toc` before `Custom_EncounterJournal.lua`.
## Encounter Journal Tab
Add a new top-level tab near `Подземелья` and `Рейды`:
```text
Путеводитель | Подземелья | Рейды | Комплекты
```
The tab must live in:
```text
EncounterJournal/Custom_EncounterJournal/Custom_EncounterJournal.xml
```
Expected XML shape:
```xml
<Button name="$parentGuideTab"
inherits="EncounterTierTabTemplate"
text="MW_PLAYER_GUIDE"
parentKey="guideTab"
id="5">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentSuggestTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/>
</Anchors>
<Scripts>
<OnClick function="EJ_ContentTab_OnClick"/>
</Scripts>
</Button>
```
If the guide tab is inserted before existing tabs, anchors for `DungeonTab`, `RaidTab`, and `LootJournalTab` must be adjusted.
Add a localized string:
```lua
MW_PLAYER_GUIDE = "Путеводитель"
```
## Tab Selection Behavior
When the guide tab is selected:
```text
- hide instance cards
- hide Encounter Journal detail panel
- hide suggested content panel
- hide loot journal panel
- hide or disable the expansion/tier dropdown unless explicitly needed
- show PlayerGuideFrame
```
When any other top-level tab is selected:
```text
- hide PlayerGuideFrame
```
Do not route this tab through:
```lua
EJ_GetInstanceByIndex(index, isRaid)
```
That API is only for dungeon/raid instance lists.
## Client API
Create:
```lua
C_PlayerGuide = C_PlayerGuide or {}
```
Required methods:
```lua
C_PlayerGuide.SetData(data)
C_PlayerGuide.GetStageInfo()
C_PlayerGuide.GetNumObjectives()
C_PlayerGuide.GetObjectiveInfo(index)
C_PlayerGuide.GetNumRecommendations()
C_PlayerGuide.GetRecommendationInfo(index)
C_PlayerGuide.GetNumRewards()
C_PlayerGuide.GetRewardInfo(index)
C_PlayerGuide.RequestRefresh()
```
Minimum behavior:
```text
SetData(data)
- validate payload is a table
- replace current data
- fire/trigger a UI refresh if the guide panel is visible
GetStageInfo()
- returns stageID, stageName, stageDescription, progress
GetObjectiveInfo(index)
- returns a normalized objective record or unpacked objective fields
RequestRefresh()
- asks the server to resend the current payload
```
## Server Payload
The server should send one normalized payload per character.
Minimum shape:
```lua
{
version = 1,
characterGuid = 123,
stageID = 10,
stageName = "Подготовка к Нордсколу",
stageDescription = "Завершите ключевые шаги перед переходом дальше.",
progress = 40,
objectives = {
{
id = 1,
type = "level",
title = "Достигнуть 68 уровня",
description = "",
completed = true,
progress = 68,
required = 68,
targetID = 68,
order = 1,
},
{
id = 2,
type = "quest",
title = "Завершить цепочку Темного портала",
description = "",
completed = false,
progress = 3,
required = 8,
targetID = 10119,
order = 2,
},
{
id = 3,
type = "dungeon",
title = "Пройти Кузню Крови",
description = "",
completed = false,
progress = 0,
required = 1,
targetID = 256,
instanceID = 256,
order = 3,
},
},
recommendations = {
{
id = 1,
type = "dungeon",
title = "Кузня Крови",
description = "Подходит для текущего этапа.",
instanceID = 256,
priority = 100,
},
},
rewards = {
{
type = "item",
itemID = 12345,
count = 1,
},
},
}
```
## Objective Types
Support at least:
```text
level
quest
quest_chain
dungeon
raid
boss
achievement
reputation
profession
item
currency
custom
```
Every objective should use the common fields:
```text
id
type
title
description
completed
progress
required
targetID
order
```
Optional type-specific fields:
```text
questID
questChainID
instanceID
encounterID
achievementID
factionID
professionID
itemID
currencyID
```
## Objective Click Behavior
Recommended behavior:
```text
quest
- show quest link or quest hint if available
dungeon
- open Encounter Journal on instanceID
raid
- open Encounter Journal on instanceID
boss
- open Encounter Journal on encounterID
item
- show item tooltip
achievement
- show achievement UI/link
custom
- show text-only detail or request a server action
```
If a target cannot be opened, the UI should fail quietly and keep the guide visible.
## AIO Contract
Use a separate handler namespace:
```text
MoonWellPlayerGuide
```
Server to client:
```lua
SetData(payload)
UpdateObjective(objective)
SetStage(stagePayload)
Notify(message)
```
Client to server:
```lua
RequestRefresh()
RequestTrackObjective(objectiveID)
RequestOpenTarget(objectiveID)
```
Minimum MVP only needs:
```lua
SetData(payload)
RequestRefresh()
```
## AzerothCore / mod-individual-progress Requirements
`mod-individual-progress` must:
```text
- determine the player's current progression stage
- calculate objective completion state
- calculate objective progress values
- build the guide payload
- send the payload on login
- send updates when progress changes
- respond to explicit client refresh requests
```
Recommended update events:
```text
login
level up
quest accepted
quest completed
quest rewarded
achievement earned
boss kill
dungeon completed
reputation changed
profession changed
item acquired, if item objectives are used
```
## UI Requirements
MVP UI:
```text
- guide tab
- stage title
- stage description
- total progress percentage
- objective list
- completed/incomplete visual state
- recommendation list
- manual refresh button
```
Later UI:
```text
- categories
- filters: all / active / completed
- tracked objective pinning
- reward preview
- requirement tooltips
- direct "Open in Journal" buttons
- server notifications
```
## Non-Goals
Do not:
```text
- store guide progress in JOURNALINSTANCE
- make the guide a third dungeon/raid instance category
- overload EJ_GetInstanceByIndex(index, isRaid)
- depend on Sirus-only globals or realm constants
- block the old dungeon/raid Encounter Journal if guide data is missing
```
## MVP Implementation Order
1. Add the `Путеводитель` tab.
2. Add an empty `PlayerGuideFrame`.
3. Add `C_PlayerGuide.lua` with static test data.
4. Render the stage title and three test objectives.
5. Wire tab switching and hide/show behavior.
6. Add AIO `MoonWellPlayerGuide.SetData`.
7. Send test payload from the server.
8. Replace static data with real `mod-individual-progress` data.
9. Add click behavior for dungeon/raid/boss objectives.
10. Add polish: progress bar, icons, rewards, filters.
## Validation
Client should tolerate missing or malformed fields:
```text
- missing payload -> show empty state
- missing objectives -> show "no active objectives"
- missing recommendations -> hide recommendations block
- unknown objective type -> render as text-only custom objective
- invalid instanceID/encounterID -> do not throw Lua errors
```
Server should validate:
```text
- objective ids are unique within the payload
- order is stable
- type is known or custom
- progress <= required when required is present
- dungeon/raid/boss targets exist in Encounter Journal data when used
```
+65
View File
@@ -0,0 +1,65 @@
// Minimal D3D9 forwarding proxy for loading WarcraftXL without editing Wow.exe.
#include <windows.h>
#include <d3d9.h>
namespace
{
using Create9Fn = IDirect3D9* (WINAPI*)(UINT);
using Create9ExFn = HRESULT (WINAPI*)(UINT, IDirect3D9Ex**);
HMODULE g_systemD3D9 = nullptr;
Create9Fn g_create9 = nullptr;
Create9ExFn g_create9Ex = nullptr;
void ResolveSystemD3D9()
{
if (g_systemD3D9) return;
char path[MAX_PATH] = {};
const UINT length = GetSystemDirectoryA(path, MAX_PATH);
if (!length || length >= MAX_PATH - 10) return;
lstrcatA(path, "\\d3d9.dll");
g_systemD3D9 = LoadLibraryA(path);
if (!g_systemD3D9) return;
g_create9 = reinterpret_cast<Create9Fn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9"));
g_create9Ex = reinterpret_cast<Create9ExFn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9Ex"));
}
void LoadWarcraftXL()
{
static bool attempted = false;
if (attempted) return;
attempted = true;
LoadLibraryA("WarcraftXL.dll");
}
}
extern "C" IDirect3D9* WINAPI Direct3DCreate9(UINT sdkVersion)
{
ResolveSystemD3D9();
LoadWarcraftXL();
return g_create9 ? g_create9(sdkVersion) : nullptr;
}
extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** output)
{
ResolveSystemD3D9();
LoadWarcraftXL();
if (g_create9Ex) return g_create9Ex(sdkVersion, output);
if (output) *output = nullptr;
return E_NOINTERFACE;
}
// Compatibility exports referenced by the current WarcraftXL core. MoonWell
// does not enable its D3D9On12 backend, so these intentionally report no D3D12
// device and a neutral supersampling factor.
extern "C" void* WxlD3D12Device() { return nullptr; }
extern "C" void* WxlD3D12Queue() { return nullptr; }
extern "C" void WxlD3D12DrainDebug() {}
extern "C" void WxlSetSsaaFactor(float) {}
extern "C" float WxlGetSsaaFactor() { return 1.0f; }
BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
{
return TRUE;
}
+9
View File
@@ -0,0 +1,9 @@
LIBRARY d3d9
EXPORTS
Direct3DCreate9
Direct3DCreate9Ex
WxlD3D12Device
WxlD3D12Queue
WxlD3D12DrainDebug
WxlSetSsaaFactor
WxlGetSsaaFactor
-339
View File
@@ -1,339 +0,0 @@
"""
Wow.exe binary patcher for MoonWell custom features.
Patches:
1. Script_CreateCharacter: reads lua_tonumber(L, 2) as outfitId,
stores to global, passes to CMSG_CHAR_CREATE packet instead of 0.
2. GetCharacterInfo: adds 11th return value — charFlags as a Lua number,
read from struct_offset +0x170 (same field as ghost flag 0x2000).
Lua checks bit 0x40000000 (CHARACTER_FLAG_UNK31) for traitor status.
Usage:
python make_modes_in_exe.py <path_to_Wow.exe>
A backup (Wow.exe.bak) is created before patching.
"""
import struct
import sys
import shutil
from pathlib import Path
# ---------------------------------------------------------------------------
# PE helpers
# ---------------------------------------------------------------------------
def _u16(data, off):
return struct.unpack_from("<H", data, off)[0]
def _u32(data, off):
return struct.unpack_from("<I", data, off)[0]
def parse_pe_sections(data):
if data[0:2] != b"MZ":
raise ValueError("Not a valid PE file (missing MZ)")
pe_off = _u32(data, 0x3C)
if data[pe_off:pe_off + 4] != b"PE\x00\x00":
raise ValueError("Not a valid PE file (missing PE signature)")
coff = pe_off + 4
num_secs = _u16(data, coff + 2)
opt_size = _u16(data, coff + 16)
opt = coff + 20
magic = _u16(data, opt)
if magic == 0x10b:
image_base = _u32(data, opt + 28)
elif magic == 0x20b:
image_base = struct.unpack_from("<Q", data, opt + 24)[0]
else:
raise ValueError(f"Unknown PE magic: 0x{magic:04x}")
sec_table = opt + opt_size
sections = []
for i in range(num_secs):
o = sec_table + i * 40
name = data[o:o + 8].rstrip(b"\x00").decode("ascii", errors="replace")
virt_size = _u32(data, o + 8)
virt_addr = _u32(data, o + 12)
raw_size = _u32(data, o + 16)
raw_off = _u32(data, o + 20)
sections.append((name, virt_addr, virt_size, raw_off, raw_size))
return image_base, sections
def va_to_offset(va, sections, image_base):
rva = va - image_base
for name, va0, vs, ro, _ in sections:
if va0 <= rva < va0 + vs:
return ro + (rva - va0)
raise ValueError(f"VA 0x{va:08x} not in any section")
def offset_to_va(off, sections, image_base):
for name, va0, _, ro, rs in sections:
if ro <= off < ro + rs:
return image_base + va0 + (off - ro)
raise ValueError(f"File offset 0x{off:08x} not in any section")
def find_cave(data, sections, image_base, size, search_from_va):
"""Return VA of the first run of >= size consecutive CC bytes after search_from_va."""
text = next((s for s in sections if s[0] == ".text"), None)
if not text is None:
_, va0, vs, ro, rs = text
else:
raise ValueError("No .text section")
rva_start = max(search_from_va - image_base, va0)
off_start = ro + (rva_start - va0)
off_end = ro + min(vs, rs)
i = off_start
while i <= off_end - size:
if data[i] != 0xcc:
i += 1
continue
j = i
while j < off_end and data[j] == 0xcc:
j += 1
if j - i >= size:
return offset_to_va(i, sections, image_base)
i = j
raise ValueError(f"No {size}-byte code cave found from VA 0x{search_from_va:08x}")
# ---------------------------------------------------------------------------
# Patch 1 & 2: outfitId in CMSG_CHAR_CREATE (fixed addresses)
# ---------------------------------------------------------------------------
_FIXED_PATCHES = [
(
0x004e0c60,
bytes.fromhex(
"558bec568b75086a0156e8f1d23600"
"83c40885c0740d6a006a0156e860d4"
"360083c40c6a006a0156e853d43600"
"50e8edf6ffff83c41033c05e5dc3cc"
"cccccccc"
),
bytes.fromhex(
"558bec568b75086a0256e8c1d33600"
"db1c245883c404a221 42ac006a006a"
"0156e85bd4360050e8f5f6ffff83c4"
"1033c05e5dc38855f08a1521 42ac00"
"8855f4c3"
).replace(b" ", b""), # spaces stripped for readability
"Script_CreateCharacter: read outfitId from Lua arg 2 -> global",
),
(
0x004e042f,
bytes.fromhex("8855f0c645f400"),
bytes.fromhex("e85f08000090 90").replace(b" ", b""),
"Packet builder: CALL helper -> load face + outfitId from global",
),
]
# ---------------------------------------------------------------------------
# Patch 3: GetCharacterInfo — add 11th return value (charFlags as number)
# ---------------------------------------------------------------------------
#
# Assembly context (FUN_004e3170 epilogue, valid branch):
#
# 004e31cd 56 PUSH ESI ← saves caller's ESI
# 004e31ce 8b f0 MOV ESI, EAX ← ESI = char struct ptr
# ... [function body — ESI used throughout for char struct] ...
# 004e32e5 8b 8e 70010000 MOV ECX, [ESI+0x170] ← charFlags (ghost flag check)
# ...
# 004e332d 83 c4 2c ADD ESP, 0x2c ← clean accumulated args ← PATCH HERE
# 004e3330 5e POP ESI ← restores caller's ESI (ESI valid until here)
# 004e3331 b8 0a 00 00 00 MOV EAX, 0xa (dead after patch)
# 004e3336 5f POP EDI ← JMP back target
# 004e3337 8b e5 MOV ESP, EBP
# 004e3339 5d POP EBP
# 004e333a c3 RET
#
# KEY: ESI = char struct ptr up to and including ADD ESP,0x2c (004e332d).
# After POP ESI (004e3330) it is caller's ESI — NOT char struct.
# Previous approach: CALL cave at 004e3331 (after POP ESI) -> ESI already wrong.
# Fix: JMP cave at 004e332d (before POP ESI) -> ESI still valid in cave.
#
# New approach: patch 5 bytes at 004e332d with JMP cave.
# Old bytes: 83 c4 2c (ADD ESP,0x2c) + 5e (POP ESI) + b8 (MOV EAX first byte)
# New bytes: e9 XX XX XX XX JMP cave
# The 4 dead bytes at 004e3332-004e3335 are unreachable.
#
# Cave (30 bytes, found automatically via CC scan over entire .text):
# 83 ec 08 SUB ESP, 8 ; make room for double
# db 86 70 01 00 00 FILD [ESI+0x170] ; load charFlags into FPU (ESI valid here!)
# dd 1c 24 FSTP [ESP] ; store as double
# 57 PUSH EDI ; lua_State* L
# e8 XX XX XX XX CALL FUN_0084e2a0 ; lua_pushnumber(L, charFlags)
# 83 c4 38 ADD ESP, 0x38 ; merged: own 0xc + stolen 0x2c
# 5e POP ESI ; stolen: restore caller's ESI
# 6a 0b PUSH 0xb ; \ EAX = 11 in 3 bytes
# 58 POP EAX ; / (vs MOV EAX,0xb = 5 bytes)
# e9 XX XX XX XX JMP 004e3336 ; continue epilogue (POP EDI ...)
#
# Lua code reads info[11] and checks bit 0x40000000 (CHARACTER_FLAG_UNK31 = traitor).
# charFlags confirmed at [ESI+0x170]: same field used for ghost flag (AND ECX,0x2000).
_CHARINFO_PATCH_VA = 0x004e332d # ADD ESP,0x2c (5 bytes including POP ESI + MOV EAX[0])
_CHARINFO_JMPBACK_VA = 0x004e3336 # POP EDI — resume epilogue after cave
_CHARINFO_CAVE_SIZE = 30
_CHARINFO_CAVE_SEARCH_FROM = 0x00401000 # search entire .text for first 35-byte CC block
_LUA_PUSHNUMBER_VA = 0x0084e2a0 # lua_pushnumber
def _build_charinfo_patches(data, sections, image_base):
"""
Build the dynamic GetCharacterInfo patches.
Returns list of (va, old_bytes, new_bytes, description), [] if already applied,
or None on unrecoverable error.
"""
patch_off = va_to_offset(_CHARINFO_PATCH_VA, sections, image_base)
current = bytes(data[patch_off:patch_off + 5])
# Already patched with new JMP approach?
if current[0:1] == b"\xe9":
return []
# Verify first 4 bytes look like the epilogue (ADD ESP,0x2c + POP ESI).
# Accept both unpatched (b8 at [4]) and old-CALL-patched (e8 at [4]) states.
if current[0:4] != bytes.fromhex("83c42c5e"):
print(f"[WARN] 0x{_CHARINFO_PATCH_VA:08x}: unexpected bytes {current.hex(' ')}")
print(" Expected ADD ESP,0x2c + POP ESI at this address.")
print(" Restore Wow.exe.bak and re-run.")
return None
old_b = current # exact 5 bytes currently in file (b8 or e8 at index 4)
# Find a 35-byte code cave in .text
cave_va = find_cave(data, sections, image_base,
_CHARINFO_CAVE_SIZE, _CHARINFO_CAVE_SEARCH_FROM)
# Cave layout offsets (30 bytes total):
# 0 SUB ESP,8 (3)
# 3 FILD [ESI+0x170] (6)
# 9 FSTP [ESP] (3)
# 12 PUSH EDI (1)
# 13 CALL lua_pushnumber (5) — next instr at 18
# 18 ADD ESP, 0x38 (3) merged: own 0xc + stolen 0x2c
# 21 POP ESI (1) stolen
# 22 PUSH 0xb (2) \ shorter than MOV EAX,0xb (5 bytes)
# 24 POP EAX (1) /
# 25 JMP 004e3336 (5) — next instr at cave_va+30
call_rel = _LUA_PUSHNUMBER_VA - (cave_va + 18)
jmp_back_rel = _CHARINFO_JMPBACK_VA - (cave_va + 30)
cave_bytes = (
bytes.fromhex("83ec08") # SUB ESP, 8 (3)
+ bytes.fromhex("db8670010000") # FILD [ESI+0x170] (6)
+ bytes.fromhex("dd1c24") # FSTP [ESP] (3)
+ bytes.fromhex("57") # PUSH EDI (1)
+ b"\xe8" + struct.pack("<i", call_rel) # CALL lua_pushnumber (5)
+ bytes.fromhex("83c438") # ADD ESP, 0x38 (0xc+0x2c) (3)
+ bytes.fromhex("5e") # POP ESI [stolen] (1)
+ bytes.fromhex("6a0b") # PUSH 0xb (2)
+ bytes.fromhex("58") # POP EAX -> EAX=11 (1)
+ b"\xe9" + struct.pack("<i", jmp_back_rel) # JMP 004e3336 (5)
)
assert len(cave_bytes) == _CHARINFO_CAVE_SIZE, \
f"Cave size {len(cave_bytes)} != {_CHARINFO_CAVE_SIZE}"
# Patch: JMP cave (5 bytes at 004e332d)
jmp_rel = cave_va - (_CHARINFO_PATCH_VA + 5)
patch_new = b"\xe9" + struct.pack("<i", jmp_rel)
return [
(
cave_va,
bytes([0xcc] * _CHARINFO_CAVE_SIZE),
cave_bytes,
f"GetCharacterInfo cave @ 0x{cave_va:08x}: push charFlags, stolen epilogue",
),
(
_CHARINFO_PATCH_VA,
old_b,
patch_new,
"GetCharacterInfo: JMP to cave before POP ESI (ESI=char struct), return 11",
),
]
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) != 2:
print(f"Usage: python {Path(sys.argv[0]).name} <path_to_Wow.exe>")
sys.exit(1)
exe_path = Path(sys.argv[1])
if not exe_path.exists():
print(f"Error: file not found: {exe_path}")
sys.exit(1)
data = bytearray(exe_path.read_bytes())
image_base, sections = parse_pe_sections(data)
print(f"Image base: 0x{image_base:08x}")
print(f"Sections: {len(sections)}")
for n, va, vs, ro, rs in sections:
print(f" {n:8s} VA=0x{va:08x} VSize=0x{vs:08x} Raw=0x{ro:08x} RSize=0x{rs:08x}")
print()
charinfo_patches = _build_charinfo_patches(data, sections, image_base)
if charinfo_patches is None:
sys.exit(1)
all_patches = list(_FIXED_PATCHES) + charinfo_patches
# Verify all first
ok = True
for va, old_b, new_b, desc in all_patches:
off = va_to_offset(va, sections, image_base)
cur = bytes(data[off:off + len(old_b)])
if cur == new_b:
print(f"[SKIP] 0x{va:08x}: already patched — {desc}")
elif cur == old_b:
print(f"[ OK ] 0x{va:08x}: verified — {desc}")
else:
print(f"[FAIL] 0x{va:08x}: {desc}")
print(f" Expected: {old_b.hex(' ')}")
print(f" Found: {cur.hex(' ')}")
ok = False
if not ok:
print("\nByte mismatch — aborting. Is this the correct Wow.exe (3.3.5a build 12340)?")
sys.exit(1)
# Backup
bak = exe_path.with_suffix(".exe.bak")
if not bak.exists():
shutil.copy2(exe_path, bak)
print(f"\nBackup: {bak}")
else:
print(f"\nBackup already exists: {bak}")
# Apply
applied = 0
for va, old_b, new_b, desc in all_patches:
off = va_to_offset(va, sections, image_base)
cur = bytes(data[off:off + len(old_b)])
if cur == new_b:
continue
data[off:off + len(old_b)] = new_b
applied += 1
print(f"[PATCH] 0x{va:08x}: {desc}")
if applied == 0:
print("\nNothing to patch.")
else:
exe_path.write_bytes(data)
print(f"\n{applied} patch(es) written to {exe_path}")
print("Done!")
if __name__ == "__main__":
main()
+345
View File
@@ -0,0 +1,345 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MoonWell compatibility module for WarcraftXL.
//
// The stock 3.3.5a executable stays untouched on disk. These narrowly scoped
// changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded.
#include "core/Logger.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include <windows.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace moonwell
{
namespace
{
constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false;
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
GxSetProjectionFn g_nextSetProjection = nullptr;
struct CameraTransition
{
float zoom = 1.0f;
float verticalOffset = 0.0f;
float startZoom = 1.0f;
float startVerticalOffset = 0.0f;
float targetZoom = 1.0f;
float targetVerticalOffset = 0.0f;
DWORD startedAt = 0;
DWORD duration = 0;
} g_characterCreateCamera;
void UpdateCharacterCreateCamera(DWORD now)
{
auto& camera = g_characterCreateCamera;
if (!camera.duration)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
return;
}
const DWORD elapsed = now - camera.startedAt;
float t = static_cast<float>(elapsed) / static_cast<float>(camera.duration);
if (t >= 1.0f)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
camera.duration = 0;
return;
}
// Smoothstep avoids a visible jerk at both ends of the move.
t = t * t * (3.0f - 2.0f * t);
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
camera.verticalOffset = camera.startVerticalOffset
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
}
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
const DWORD now = GetTickCount();
UpdateCharacterCreateCamera(now);
auto& camera = g_characterCreateCamera;
camera.startZoom = camera.zoom;
camera.startVerticalOffset = camera.verticalOffset;
camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f;
camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f;
camera.startedAt = now;
camera.duration = static_cast<DWORD>(requestedDuration);
UpdateCharacterCreateCamera(now);
return 0;
}
void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection)
{
if (!g_nextSetProjection)
return;
UpdateCharacterCreateCamera(GetTickCount());
const auto& camera = g_characterCreateCamera;
if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f))
{
const float* source = static_cast<const float*>(projection);
if (source[11] > 0.5f)
{
float adjusted[16];
std::memcpy(adjusted, source, sizeof(adjusted));
adjusted[0] *= camera.zoom;
adjusted[5] *= camera.zoom;
adjusted[9] += camera.verticalOffset;
g_nextSetProjection(self, edx, adjusted);
return;
}
}
g_nextSetProjection(self, edx, projection);
}
void InstallCharacterCreateCamera()
{
namespace gx = wxl::offsets::engine::gx;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
void** slot = &vtable[gx::kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
return;
}
WLOG_INFO("moonwell: smooth character-create camera installed");
}
int __cdecl SetLoginCharacterFlags(void* state)
{
uint32_t flags = 0;
if (state && wxl::runtime::lua::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
return 1;
}
int __cdecl IsTraitor(void* state)
{
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
return 1;
}
struct Patch
{
const char* name;
uintptr_t address;
const uint8_t* expected;
const uint8_t* replacement;
size_t size;
};
template <size_t N>
bool Apply(const char* name, uintptr_t address,
const std::array<uint8_t, N>& expected,
const std::array<uint8_t, N>& replacement)
{
const auto* current = reinterpret_cast<const uint8_t*>(address);
if (std::memcmp(current, replacement.data(), N) == 0)
return true;
if (std::memcmp(current, expected.data(), N) != 0)
{
WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required",
name, reinterpret_cast<void*>(address));
return false;
}
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
return false;
}
WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(address));
return true;
}
bool InstallGlueUnlock()
{
// Same stock-client checks formerly changed in the distributed Wow.exe.
// They allow custom GlueXML while keeping WarcraftXL's callback validator intact.
bool ok = true;
ok &= Apply("glue archive override", 0x005F4DBF,
std::array<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback gate", 0x00816625,
std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback result A", 0x0081663F,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback result B", 0x00816695,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback compare", 0x00816746,
std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback return", 0x0081675F,
std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
std::array<uint8_t, 7>{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED});
return ok;
}
bool InstallCharacterModePacket()
{
// CreateCharacter(name, modeId): capture Lua argument 2 and place it in
// the final CMSG_CHAR_CREATE byte that is zero in the stock client.
constexpr std::array<uint8_t, 64> expectedCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00,
0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4,
0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00,
0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3,
0xCC,0xCC,0xCC,0xCC,0xCC
};
constexpr std::array<uint8_t, 64> replacementCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00,
0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A,
0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4,
0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC,
0x00,0x88,0x55,0xF4,0xC3
};
constexpr std::array<uint8_t, 7> expectedPacket =
{0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
constexpr std::array<uint8_t, 7> replacementPacket =
{0xE8,0x5F,0x08,0x00,0x00,0x90,0x90};
return Apply("CreateCharacter mode argument", 0x004E0C60,
expectedCreate, replacementCreate)
&& Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F,
expectedPacket, replacementPacket);
}
bool InstallCharacterFlags()
{
// GetCharacterInfo(index) gains return value 11: the character flags
// from CharacterInfo+0x170. The glue code uses bit 0x40000000.
constexpr uintptr_t patchAddress = 0x004E332D;
constexpr uintptr_t returnAddress = 0x004E3336;
constexpr uintptr_t luaPushNumber = 0x0084E2A0;
constexpr std::array<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};
if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
return true;
if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
{
WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
reinterpret_cast<void*>(patchAddress));
return false;
}
auto* cave = static_cast<uint8_t*>(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!cave)
{
WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError());
return false;
}
const uint8_t prefix[] = {
0x83,0xEC,0x08, // sub esp, 8
0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h]
0xDD,0x1C,0x24, // fstp qword ptr [esp]
0x57, // push edi (lua_State*)
0xE8,0,0,0,0, // call lua_pushnumber
0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch
0x5E, // stolen pop esi
0x6A,0x0B,0x58, // eax = 11
0xE9,0,0,0,0 // jump to stock epilogue
};
static_assert(sizeof(prefix) == 30);
std::memcpy(cave, prefix, sizeof(prefix));
const auto callRel = static_cast<int32_t>(luaPushNumber -
(reinterpret_cast<uintptr_t>(cave) + 18));
const auto backRel = static_cast<int32_t>(returnAddress -
(reinterpret_cast<uintptr_t>(cave) + 30));
std::memcpy(cave + 14, &callRel, sizeof(callRel));
std::memcpy(cave + 26, &backRel, sizeof(backRel));
FlushInstructionCache(GetCurrentProcess(), cave, 30);
std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
{
VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
return false;
}
WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11");
return true;
}
void InstallBoot()
{
const bool glue = InstallGlueUnlock();
const bool create = InstallCharacterModePacket();
const bool flags = InstallCharacterFlags();
if (!glue || !create || !flags)
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
wxl::core::log::Flush();
}
DWORD WINAPI FlushRuntimeLog(LPVOID)
{
// Let the remainder of RunAll() and the core-ready line reach the
// shared buffered logger, then make startup diagnostics durable.
Sleep(1000);
wxl::core::log::Flush();
return 0;
}
void InstallRuntimeLogFlush()
{
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
if (thread) CloseHandle(thread);
}
struct Registration
{
Registration()
{
wxl::runtime::lua::RegisterFunction(
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
wxl::runtime::lua::RegisterFunction(
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
wxl::runtime::modules::Register(
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
}
} g_registration;
}
}
+11
View File
@@ -36,6 +36,7 @@ $SRC_DIR = Join-Path $ROOT "src"
$DIST_DIR = Join-Path $ROOT "dist"
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
# --- Ensure tool exists
if (!(Test-Path $TOOL)) {
@@ -59,6 +60,16 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
exit 1
}
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Write-Host "Building and packaging WarcraftXL..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy
if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!"
exit 1
}
# --- Sync dist/ -> WOW_HOME
Write-Host "Syncing dist/ -> WOW_HOME..."
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
end
end
-- The stock creation scene frames each race at a different height. Projection
-- zoom therefore needs a per-race vertical target to keep the face in the same
-- part of the screen. Values are indexed by GetSelectedSex(): 2 male, 3 female.
local CHARACTER_CREATE_CAMERA_PROFILES = {
HUMAN = { [2] = -0.28, [3] = -0.20 },
ORC = { [2] = -0.50, [3] = -0.30 },
DWARF = { [2] = 0.30, [3] = 0.38 },
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
SCOURGE = { [2] = 0.18, [3] = 0.27 },
TAUREN = { [2] = -0.78, [3] = -0.62 },
GNOME = { [2] = 0.64, [3] = 0.72 },
TROLL = { [2] = -0.70, [3] = -0.55 },
BLOODELF = { [2] = -0.30, [3] = -0.22 },
DRAENEI = { [2] = -0.62, [3] = -0.55 },
};
local function CharacterCreate_GetCameraProfile()
local _, raceFile = GetNameForRace();
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
local sex = GetSelectedSex();
return 2.0, raceProfile and raceProfile[sex] or -0.20;
end
local function CharacterCreate_UpdateCamera(immediate)
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
local mode = showFace and 1 or 0;
if CharacterCreate.cameraMode == mode and not immediate then
return;
end
CharacterCreate.cameraMode = mode;
CharacterCreate:SetCamera(0);
if MoonWellSetCharacterCreateCamera then
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
end
end
local function CharacterCreate_GetEnteredName()
local text = "";
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
CharacterCreate.gameModeSelectionMode = false;
end
CharacterCreate_UpdateCamera();
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
if showGameModes then
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
CharacterCreate_UpdateCamera();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
CharacterChangeFixup();
CharacterCreate_CreateGenderButtonTextures();
CharacterCreate_UpdateButtonCheckedStates();
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
hideScheduled = true
HideNameEditFrame:Show()
@@ -1013,7 +1054,7 @@ end
function CharacterCreate_OnHide()
PAID_SERVICE_CHARACTER_ID = nil;
PAID_SERVICE_TYPE = nil;
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
for button, tooltip in pairs(raceTooltips) do
if tooltip then tooltip:Hide() end
@@ -1518,6 +1559,7 @@ end
function CharacterCreate_UpdateModel(self)
UpdateCustomizationScene();
CharacterCreate_UpdateCamera();
self:AdvanceTime();
end
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
CharacterCreate_UpdateButtonCheckedStates();
end
function CharacterCreate_ResetState()
function CharacterCreate_ResetState(immediate)
CharacterCreate.personalizationMode = false;
CharacterCreate.gameModeSelectionMode = false;
CharacterCreate_UpdateCamera(immediate);
CharacterCreateRaceButtonsContainer:Show();
CharacterCreateClassButtonsContainer:Show();
CharacterCreateGenderButtonsContainer:Show();
@@ -1139,6 +1139,12 @@ end
function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld");
StopGlueAmbience();
-- Persist the server-provided game-mode bit across the GlueXML -> FrameXML
-- Lua-state transition. WarcraftXL owns the value for this Wow.exe process.
if ( type(MoonWellSetLoginCharacterFlags) == "function" ) then
local info = { GetCharacterInfo(CharacterSelect.selectedIndex) };
MoonWellSetLoginCharacterFlags(info[11] or 0);
end
EnterWorld();
end
@@ -69,26 +69,25 @@ local MainMenuBarBackpackButton = _G.MainMenuBarBackpackButton;
local HelpMicroButton = _G.HelpMicroButton;
local KeyRingButton = _G.KeyRingButton;
-- Button collections (rebuilt after addons create optional custom buttons).
local function BuildMicroButtonList()
if isAscensionServer then
return {
_G.CharacterMicroButton,
_G.SpellbookMicroButton,
_G.TalentMicroButton,
_G.AchievementMicroButton,
_G.QuestLogMicroButton,
_G.SocialsMicroButton,
_G.LFDMicroButton,
_G.EncounterJournalMicroButton,
_G.PathToAscensionMicroButton,
_G.ChallengesMicroButton,
_G.MainMenuMicroButton,
_G.HelpMicroButton
}
end
-- Button collections (dynamically set based on server)
local MICRO_BUTTONS
return {
if isAscensionServer then
MICRO_BUTTONS = {
_G.CharacterMicroButton,
_G.SpellbookMicroButton,
_G.TalentMicroButton,
_G.AchievementMicroButton,
_G.QuestLogMicroButton,
_G.SocialsMicroButton,
_G.LFDMicroButton,
_G.PathToAscensionMicroButton,
_G.ChallengesMicroButton,
_G.MainMenuMicroButton,
_G.HelpMicroButton
}
else
MICRO_BUTTONS = {
_G.CharacterMicroButton,
_G.SpellbookMicroButton,
_G.TalentMicroButton,
@@ -96,7 +95,6 @@ local function BuildMicroButtonList()
_G.QuestLogMicroButton,
_G.SocialsMicroButton,
_G.LFDMicroButton,
_G.EncounterJournalMicroButton,
_G.CollectionsMicroButton,
_G.PVPMicroButton,
_G.MainMenuMicroButton,
@@ -104,7 +102,6 @@ local function BuildMicroButtonList()
}
end
local MICRO_BUTTONS = BuildMicroButtonList()
local bagslots = {_G.CharacterBag0Slot, _G.CharacterBag1Slot, _G.CharacterBag2Slot, _G.CharacterBag3Slot};
@@ -786,8 +783,6 @@ local function ApplyMicromenuSystem()
return
end
MICRO_BUTTONS = BuildMicroButtonList()
-- Store original states first
StoreOriginalMicroButtonStates()
@@ -916,8 +911,6 @@ local function ApplyMicromenuSystem()
or (_G.PVPParentFrame and _G.PVPParentFrame:IsVisible() and true or false)
or (_G.BattlefieldFrame and _G.BattlefieldFrame:IsVisible() and true or false)
or (_G.HonorFrame and _G.HonorFrame:IsVisible() and true or false)
elseif buttonName == "EncounterJournal" then
return (_G.EncounterJournal and _G.EncounterJournal:IsVisible() and true or false)
end
return pressed
@@ -926,152 +919,6 @@ local function ApplyMicromenuSystem()
-- ============================================================================
-- SECTION 5: SPECIALIZED BUTTON SETUP
-- ============================================================================
local function SetupEncounterJournalButton(button)
local iconTexture = 'Interface\\EncounterJournal\\UI-EJ-PortraitIcon'
local backgroundTexture = 'Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\uimicromenu2x'
local buttonWidth, buttonHeight = button:GetSize()
local dx, dy = -1, 1
local offX, offY = button:GetPushedTextOffset()
if not button.DragonUIEncounterJournalIcon then
button.DragonUIEncounterJournalIcon = button:CreateTexture(nil, 'ARTWORK')
end
local icon = button.DragonUIEncounterJournalIcon
icon:SetTexture(iconTexture)
icon:SetTexCoord(0, 1, 0, 1)
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
icon:SetSize(buttonWidth - 8, buttonWidth - 8)
icon:SetAlpha(1)
icon:Show()
local highlightTexture = button:GetHighlightTexture()
if highlightTexture then
highlightTexture:SetTexture(iconTexture)
highlightTexture:SetTexCoord(0, 1, 0, 1)
highlightTexture:ClearAllPoints()
highlightTexture:SetAllPoints(icon)
highlightTexture:SetBlendMode('ADD')
highlightTexture:SetAlpha(0.55)
end
if not button.DragonUIBackground then
local bg = button:CreateTexture(nil, 'BACKGROUND')
bg:SetTexture(backgroundTexture)
bg:SetSize(buttonWidth, buttonHeight + 1)
bg:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
bg:SetPoint('CENTER', dx, dy)
button.DragonUIBackground = bg
local bgPushed = button:CreateTexture(nil, 'BACKGROUND')
bgPushed:SetTexture(backgroundTexture)
bgPushed:SetSize(buttonWidth, buttonHeight + 1)
bgPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
bgPushed:SetPoint('CENTER', dx + offX, dy + offY)
bgPushed:Hide()
button.DragonUIBackgroundPushed = bgPushed
else
button.DragonUIBackground:SetTexture(backgroundTexture)
button.DragonUIBackground:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
button.DragonUIBackground:ClearAllPoints()
button.DragonUIBackground:SetPoint('CENTER', dx, dy)
button.DragonUIBackground:SetSize(buttonWidth, buttonHeight + 1)
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:SetTexture(backgroundTexture)
button.DragonUIBackgroundPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
button.DragonUIBackgroundPushed:ClearAllPoints()
button.DragonUIBackgroundPushed:SetPoint('CENTER', dx + offX, dy + offY)
button.DragonUIBackgroundPushed:SetSize(buttonWidth, buttonHeight + 1)
end
end
button.dragonUIState = button.dragonUIState or {}
button.dragonUIState.pushed = IsSpecialMicroButtonActive(button, "EncounterJournal")
button.dragonUILastState = button.dragonUIState.pushed
button.dragonUITimer = button.dragonUITimer or 0
button.HandleDragonUIState = function()
local state = button.dragonUIState
local hlTex = button:GetHighlightTexture()
if state and state.pushed then
if icon then
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', offX, offY)
icon:SetAlpha(0.7)
end
if button.DragonUIBackground then
button.DragonUIBackground:Hide()
end
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:Show()
end
if hlTex then
hlTex:ClearAllPoints()
hlTex:SetPoint('TOPLEFT', icon, 'TOPLEFT', 0, 0)
hlTex:SetPoint('BOTTOMRIGHT', icon, 'BOTTOMRIGHT', 0, 0)
end
else
if icon then
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
icon:SetAlpha(1)
end
if button.DragonUIBackground then
button.DragonUIBackground:Show()
end
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:Hide()
end
if hlTex then
hlTex:ClearAllPoints()
hlTex:SetAllPoints(icon)
end
end
end
button:SetScript('OnUpdate', function(self, elapsed)
self.dragonUITimer = (self.dragonUITimer or 0) + elapsed
if self.dragonUITimer >= 0.1 then
self.dragonUITimer = 0
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
if currentState ~= self.dragonUILastState then
self.dragonUILastState = currentState
if self.dragonUIState then
self.dragonUIState.pushed = currentState
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end
end
end)
if not button.DragonUIStateHooks then
button:HookScript('OnMouseDown', function(self)
if self.dragonUIState then
self.dragonUIState.pushed = true
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end)
button:HookScript('OnMouseUp', function(self)
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
if self.dragonUIState then
self.dragonUIState.pushed = currentState
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end)
button.DragonUIStateHooks = true
end
button.HandleDragonUIState()
end
local function SetupPVPButton(button)
-- Mirror the Character button pattern:
-- Instead of fighting WoW's internal NormalTexture alpha management,
@@ -1735,8 +1582,6 @@ local function ApplyMicromenuSystem()
end
local function setupMicroButtons(xOffset)
MICRO_BUTTONS = BuildMicroButtonList()
local buttonxOffset = 0
local useGrayscale = addon.db.profile.micromenu.grayscale_icons
@@ -1860,11 +1705,9 @@ local function ApplyMicromenuSystem()
local isCharacterButton = (buttonName == "Character")
local isPVPButton = (buttonName == "PVP")
local isEncounterJournalButton = (buttonName == "EncounterJournal")
local upCoords = not isCharacterButton and not isPVPButton and not isEncounterJournalButton and GetColoredTextureCoords(name, "Up") or nil
local shouldUseGrayscale = (useGrayscale and not isEncounterJournalButton) or
(not isPVPButton and not isEncounterJournalButton and not upCoords and not isCharacterButton)
local upCoords = not isCharacterButton and not isPVPButton and GetColoredTextureCoords(name, "Up") or nil
local shouldUseGrayscale = useGrayscale or (not isPVPButton and not upCoords and not isCharacterButton)
if shouldUseGrayscale then
-- Grayscale icons
@@ -1898,8 +1741,6 @@ local function ApplyMicromenuSystem()
end
elseif isPVPButton then
SetupPVPButton(button)
elseif isEncounterJournalButton then
SetupEncounterJournalButton(button)
elseif isCharacterButton then
SetupCharacterButton(button)
else
@@ -2404,8 +2245,6 @@ end
return
end
MICRO_BUTTONS = BuildMicroButtonList()
local useGrayscale = addon.db.profile.micromenu.grayscale_icons
local configMode = useGrayscale and "grayscale" or "normal"
local config = addon.db.profile.micromenu[configMode]
@@ -0,0 +1,202 @@
-- The MoonWell Achievement.dbc intentionally exposes both faction variants.
-- Restore the 3.3.5a client-side faction filter and make traitors use the
-- opposite faction. IDs come from the stock ruRU build 12340 DBC.
local achievementFaction = {}
local function AddFaction(ids, faction)
for id in string.gmatch(ids, "%d+") do
achievementFaction[tonumber(id)] = faction
end
end
AddFaction([[
33 34 35 37 41 58 202 203 206 220 225 230 246 388 433 434 435 436 437 438
439 440 441 442 470 471 472 473 604 610 611 612 613 614 701 707 709 711 713
764 899 907 908 942 948 963 966 969 970 1012 1022 1023 1024 1028 1029 1030
1034 1035 1038 1040 1151 1167 1169 1172 1184 1189 1191 1192 1255 1262 1279
1466 1563 1656 1676 1678 1681 1684 1686 1692 1697 1707 1737 1752 1757 1762
1782 2016 2144 2194 2419 2421 2536 2760 2761 2762 2763 2764 2770 2777 2778
2779 2780 2781 2782 2797 2817 3356 3478 3556 3576 3580 3596 3676 3846 3851
3856 3857 4156 4296 4298 4436 4784 4786
]], "Alliance")
AddFaction([[
224 259 443 444 445 446 447 448 449 450 451 452 453 454 468 469 593 603 615
616 617 618 619 700 706 708 710 712 714 762 763 873 901 909 926 943 965 967
968 971 1005 1006 1011 1025 1026 1027 1031 1032 1033 1036 1037 1039 1041
1164 1168 1170 1173 1175 1203 1251 1252 1271 1272 1273 1274 1280 1356 1357
1358 1359 1360 1502 1657 1677 1680 1682 1683 1685 1691 1693 1698 1783 1784
2017 2145 2192 2195 2200 2420 2476 2497 2537 2765 2766 2767 2768 2769 2771
2776 2783 2784 2785 2786 2787 2788 2798 2816 3357 3557 3577 3581 3597 3656
3677 3778 3957 4079 4176 4177 4256 4297 4437 4785 4790
]], "Horde")
local NativeGetAchievementInfo = GetAchievementInfo
local NativeGetCategoryNumAchievements = GetCategoryNumAchievements
local NativeGetLatestCompletedAchievements = GetLatestCompletedAchievements
local NativeGetTrackedAchievements = GetTrackedAchievements
local NativeGetNumCompletedAchievements = GetNumCompletedAchievements
local NativeGetTotalAchievementPoints = GetTotalAchievementPoints
local categoryCache = {}
local summaryCache = {}
local function GetDisplayFaction()
local faction = UnitFactionGroup("player")
if MoonWell_IsTraitor then
if faction == "Alliance" then
return "Horde"
elseif faction == "Horde" then
return "Alliance"
end
end
return faction
end
local function IsAchievementVisible(id, faction)
local requiredFaction = achievementFaction[id]
return not requiredFaction or not faction or requiredFaction == faction
end
local function BuildCategory(categoryID, showAll)
local faction = GetDisplayFaction()
local key = tostring(categoryID) .. ":" .. tostring(showAll and 1 or 0)
.. ":" .. tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
local cached = categoryCache[key]
if cached then
return cached
end
local nativeCount = NativeGetCategoryNumAchievements(categoryID, showAll)
local indices = {}
local completed = 0
for nativeIndex = 1, nativeCount do
local id, _, _, isCompleted = NativeGetAchievementInfo(categoryID, nativeIndex)
if id and IsAchievementVisible(id, faction) then
indices[#indices + 1] = nativeIndex
if isCompleted then
completed = completed + 1
end
end
end
cached = { indices = indices, completed = completed }
categoryCache[key] = cached
return cached
end
function GetCategoryNumAchievements(categoryID, showAll)
local category = BuildCategory(categoryID, showAll)
return #category.indices, category.completed
end
function GetAchievementInfo(idOrCategory, achievementIndex)
if achievementIndex == nil then
return NativeGetAchievementInfo(idOrCategory)
end
local category = BuildCategory(idOrCategory, false)
local nativeIndex = category.indices[achievementIndex]
if not nativeIndex then
return nil
end
return NativeGetAchievementInfo(idOrCategory, nativeIndex)
end
local function BuildSummary()
local faction = GetDisplayFaction()
local key = tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
local cached = summaryCache[key]
if cached then
return cached
end
local total, completed = NativeGetNumCompletedAchievements()
local points = NativeGetTotalAchievementPoints()
local statisticFlag = ACHIEVEMENT_FLAGS_STATISTIC or 0x00000001
local hiddenFlag = ACHIEVEMENT_FLAGS_HIDDEN or 0x00000002
for id, requiredFaction in pairs(achievementFaction) do
if faction and requiredFaction ~= faction then
local _, _, achievementPoints, isCompleted, _, _, _, _, flags =
NativeGetAchievementInfo(id)
if flags and bit.band(flags, statisticFlag) == 0 then
local isHidden = bit.band(flags, hiddenFlag) ~= 0
if not isHidden or isCompleted then
total = total - 1
end
if isCompleted then
completed = completed - 1
points = points - (achievementPoints or 0)
end
end
end
end
cached = { total = total, completed = completed, points = points }
summaryCache[key] = cached
return cached
end
function GetNumCompletedAchievements()
local summary = BuildSummary()
return summary.total, summary.completed
end
function GetTotalAchievementPoints()
return BuildSummary().points
end
local function FilterAchievementIDs(getter)
local faction = GetDisplayFaction()
local source = { getter() }
local filtered = {}
for i = 1, #source do
if IsAchievementVisible(source[i], faction) then
filtered[#filtered + 1] = source[i]
end
end
return unpack(filtered)
end
if NativeGetLatestCompletedAchievements then
function GetLatestCompletedAchievements()
return FilterAchievementIDs(NativeGetLatestCompletedAchievements)
end
end
if NativeGetTrackedAchievements then
function GetTrackedAchievements()
return FilterAchievementIDs(NativeGetTrackedAchievements)
end
end
function MoonWellAchievementFaction_Refresh()
categoryCache = {}
summaryCache = {}
if AchievementFrame and AchievementFrame:IsShown() then
if AchievementFrameCategories_Update then
AchievementFrameCategories_Update()
end
if AchievementFrameAchievements_ForceUpdate then
AchievementFrameAchievements_ForceUpdate()
elseif AchievementFrameAchievements_Update then
AchievementFrameAchievements_Update()
end
if AchievementFrameSummary_Update then
AchievementFrameSummary_Update()
end
end
end
local events = CreateFrame("Frame")
events:RegisterEvent("ACHIEVEMENT_EARNED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_FACTION")
events:SetScript("OnEvent", function(self, event, unit)
if event ~= "UNIT_FACTION" or unit == "player" then
MoonWellAchievementFaction_Refresh()
end
end)
@@ -1,544 +0,0 @@
UIPanelWindows["ItemBrowser"] = { area = "left", pushable = 0, whileDead = 1, width = 830, xOffset = "15", yOffset = "-10"}
local type = type
local mathmax = math.max
local strfind, strlower, strtrim = string.find, string.lower, string.trim
local tinsert, twipe = table.insert, table.wipe
local debugprofilestop = debugprofilestop
local COLUMNS = {
{
name = "Name",
dataProviderKey = "name",
template = "ItemBrowserListCellNameIconTemplate",
},
{
name = "ItemLevel",
dataProviderKey = "itemLevel",
width = 70,
template = "ItemBrowserListCellItemLevelTemplate",
},
{
name = "ID",
dataProviderKey = "itemID",
width = 90,
template = "ItemBrowserListCellItemIDTemplate",
},
}
ItemBrowserMixin = {}
function ItemBrowserMixin:OnLoad()
self.TitleText:SetText(ITEM_BROWSER)
SetPortraitToTexture(self.portrait, [[Interface\Icons\Achievement_General_ClassicBattles_64]])
self.inset.Bgs:SetAtlas("UI-EJ-BattleforAzeroth")
do -- tabs
self:RegisterCustomEvent("SERVICE_DATA_UPDATE")
self:RegisterCustomEvent("CUSTOM_CHALLENGE_DEACTIVATED")
self.tab1:SetFrameLevel(1)
self.tab2:SetFrameLevel(1)
self.tab3:SetFrameLevel(1)
self.tab4:SetFrameLevel(1)
self.maxTabWidth = (self:GetWidth() - 19) / 4
PanelTemplates_SetNumTabs(self, 4)
end
do -- coroutine
self.COROUTINE_HANDLER = CreateFrame("Frame")
self.COROUTINE_HANDLER:Hide()
self.COROUTINE_HANDLER.FRAMETIME_TARGET = 1 / 55
self.COROUTINE_HANDLER.FRAMETIME_AVAILABLE = 8
self.COROUTINE_HANDLER.FRAMETIME_RESERVE = 4
self.COROUTINE_HANDLER.DEBUG = false
self.COROUTINE_HANDLER:SetScript("OnUpdate", function(this, elapsed)
local frametimeStep = this.FRAMETIME_TARGET - elapsed
if frametimeStep ~= 0 then
frametimeStep = frametimeStep * 1000
this.FRAMETIME_AVAILABLE = mathmax(5, this.FRAMETIME_AVAILABLE + frametimeStep)
end
if this.COROUTINE then
this.COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(this.COROUTINE)
if not status then
this.COROUTINE = nil
this:Hide()
self.PROGRESS = 1
error(result, 2)
elseif not result then
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
self.PROGRESS = progress
self:OnSearchResultUpdate()
else
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", 1) end
this.COROUTINE = nil
this:Hide()
self.PROGRESS = 1
self:OnSearchResultUpdate()
end
else
this:Hide()
end
end)
end
self.BUTTON_OFFSET_Y = 0
self.PROGRESS = 1
self.results = {}
self.Scroll.ScrollBar:SetBackgroundShown(false)
self.Scroll.update = function(scrollFrame)
self:UpdateResultList()
end
self.Scroll.ScrollBar.doNotHide = true
self.Scroll.scrollBar = self.Scroll.ScrollBar
HybridScrollFrame_CreateButtons(self.Scroll, "ItemBrowserListRowButtonTemplate", 0, 0, nil, nil, nil, -self.BUTTON_OFFSET_Y)
self.SearchBox:ToggleOnCharFilter(true, 0.3)
self.tableBuilder = CreateTableBuilder(HybridScrollFrame_GetButtons(self.Scroll))
self.tableBuilder:SetHeaderContainer(self.HeaderHolder)
self:ConstructTable()
end
function ItemBrowserMixin:OnShow()
SetParentFrameLevel(self.inset)
PanelTemplates_SetTab(self, 4)
self.dirty = true
self.Scroll.ScrollBar:SetValue(0)
self:UpdateResultList()
self.SearchBox:SetFocus()
end
function ItemBrowserMixin:OnEvent(event, ...)
if event == "SERVICE_DATA_UPDATE"
or (event == "CUSTOM_CHALLENGE_DEACTIVATED" and select(2, ...) ~= Enum.HardcoreDeathReason.FAILED_DEATH)
then
if not C_Service.IsRenegadeRealm() then
PanelTemplates_HideTab(self, 2)
self.tab3:SetPoint("LEFT", self.tab1, "RIGHT", -16, 0);
end
if not C_Service.IsHardcoreEnabledOnRealm() then
PanelTemplates_HideTab(self, 3)
end
if not C_Service.IsGMAccount() and not IsInterfaceDevClient() then
PanelTemplates_HideTab(self, 4)
end
end
end
function ItemBrowserMixin:GetRowProvider()
return function(itemIndex)
if itemIndex > #self.results then
return nil
end
local itemID = self.results[itemIndex]
local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(itemID, false, nil, true, true)
return {
itemID = itemID,
name = itemName,
itemLink = itemLink,
itemLevel = itemLevel,
itemType = itemType,
itemSubType = itemSubType,
icon = itemTexture,
}
end
end
function ItemBrowserMixin:ConstructTable()
local textPadding = 1
local cellPadding = 10
self.tableBuilder:Reset()
self.tableBuilder:SetDataProvider(self:GetRowProvider())
self.tableBuilder:SetTableMargins(5)
self.tableBuilder:SetTableWidth(self.Scroll:GetWidth() or select(3, self.Scroll:GetRect()))
for index, columnInfo in ipairs(COLUMNS) do
local column = self.tableBuilder:AddColumn()
column:ConstructHeader("Button", "ItemBrowserListHeaderButtonTemplate", columnInfo.name)
column:ConstrainToHeader(textPadding)
column:ConstructCells("Frame", columnInfo.template)
if columnInfo.width then
column:SetFixedConstraints(columnInfo.width, cellPadding)
else
column:SetFillConstraints(1, cellPadding)
end
end
self.tableBuilder:Arrange()
end
function ItemBrowserMixin:GetNumItems()
if not self.NUM_ITEMS then
self.NUM_ITEMS = tCount(ItemsCache)
end
return self.NUM_ITEMS
end
function ItemBrowserMixin:SearchItem(item)
item = strtrim(item)
if item == "" then
self:ClearResults()
return
end
local itemID = C_Item.GetItemIDFromString(item)
local searchField, searchValue, useStringFind
if itemID then
searchField = Enum.ItemCacheField.ITEM_ID
searchValue = itemID
elseif item ~= "" then
searchField = GetLocale() == "ruRU" and Enum.ItemCacheField.NAME_RURU or Enum.ItemCacheField.NAME_ENGB
searchValue = strlower(item)
useStringFind = true
end
if not searchField then
self:ClearResults()
return
end
self:StartSearch(searchField, searchValue, useStringFind)
end
function ItemBrowserMixin:StartSearch(searchField, searchValue, useStringFind)
if self.SEARCH_FIELD == searchField and self.SEARCH_VALUE == searchValue then
return
end
if self.COROUTINE then
coroutine.close(self.COROUTINE)
self.COROUTINE = nil
self:ClearResults()
end
self.SEARCH_FIELD = searchField
self.SEARCH_VALUE = searchValue
self.USE_STRING_FIND = useStringFind
local framerate = GetFramerate()
self.COROUTINE_HANDLER.FRAMETIME_TARGET = framerate > 63 and (1 / 60) or (1 / 55)
self.COROUTINE_HANDLER.FRAMETIME_BUDGET = 1000 / framerate - self.COROUTINE_HANDLER.FRAMETIME_RESERVE
self.COROUTINE_HANDLER.COROUTINE = coroutine.create(self.SearchProcess)
self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(self.COROUTINE_HANDLER.COROUTINE, self)
if not status then
self.COROUTINE_HANDLER.COROUTINE = nil
self.COROUTINE_HANDLER:Hide()
self.PROGRESS = 1
self:ClearResults()
geterrorhandler()(result)
elseif coroutine.status(self.COROUTINE_HANDLER.COROUTINE) == "dead" then
self.COROUTINE_HANDLER.COROUTINE = nil
self.PROGRESS = 1
else
if self.COROUTINE_HANDLER.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
self.PROGRESS = progress
self.COROUTINE_HANDLER:Show()
end
self:OnSearchResultUpdate()
end
function ItemBrowserMixin:SearchProcess()
twipe(self.results)
if not ItemsCache then
return true, 1
end
local processedEntries = 0
local fieldValue
for itemID, itemInfo in pairs(ItemsCache) do
processedEntries = processedEntries + 1
fieldValue = itemInfo[self.SEARCH_FIELD]
if type(fieldValue) == "string" then
fieldValue = strlower(fieldValue)
end
if fieldValue == self.SEARCH_VALUE
or (self.USE_STRING_FIND and strfind(fieldValue, self.SEARCH_VALUE, 1, true))
then
tinsert(self.results, itemInfo[Enum.ItemCacheField.ITEM_ID])
end
if (debugprofilestop() - self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP) > self.COROUTINE_HANDLER.FRAMETIME_BUDGET then
coroutine.yield(false, processedEntries / self:GetNumItems())
end
end
return true, 1
end
function ItemBrowserMixin:OnSearchResultUpdate()
self.dirty = true
self:UpdateSearchProgress()
self:UpdateResultList()
end
function ItemBrowserMixin:UpdateSearchProgress()
local progress = Clamp(self.PROGRESS or 1, 0, 1)
self.SearchProgressBar:SetValue(progress)
self.SearchProgressBar:SetAlpha(progress < 1 and 1 or 0)
end
function ItemBrowserMixin:ClearResults()
twipe(self.results)
self.SEARCH_FIELD = nil
self.SEARCH_VALUE = nil
self.USE_STRING_FIND = nil
self.PROGRESS = 1
self.dirty = true
self:UpdateSearchProgress()
self:UpdateResultList()
end
function ItemBrowserMixin:UpdateResultList()
local scrollFrame = self.Scroll
local offset = HybridScrollFrame_GetOffset(scrollFrame)
local numResults = self.results and #self.results or 0
if self.numItems ~= numResults then
self.numItems = numResults
self.dirty = true
end
local populateCount = math.min(#scrollFrame.buttons, numResults)
self.tableBuilder:Populate(offset, populateCount)
local mouseFocus = GetMouseFocus()
for index, button in ipairs(scrollFrame.buttons) do
button:SetOwner(self)
button:SetShown(index <= numResults)
if button == mouseFocus then
button:OnEnter()
end
end
if self.dirty then
local buttonHeight = scrollFrame.buttons[1] and scrollFrame.buttons[1]:GetHeight() or 0
local scrollHeight = buttonHeight * numResults + self.BUTTON_OFFSET_Y * (numResults - 1) + self.BUTTON_OFFSET_Y / 3
HybridScrollFrame_Update(scrollFrame, scrollHeight, scrollFrame:GetHeight())
self.dirty = nil
end
end
ItemBrowserSearchBoxMixin = CreateFromMixins(PKBT_EditBoxMixin)
function ItemBrowserSearchBoxMixin:OnLoad()
PKBT_EditBoxMixin.OnLoad(self)
local l, r, t, b = self:GetTextInsets()
self:SetTextInsets(l + 10, r + 10, t - 1, b)
self:SetInstructions(SEARCH)
self.Instructions:ClearAllPoints()
self.Instructions:SetPoint("LEFT", 19, -2)
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6)
self:SetCustomCharFilter(function(text)
text = string.gsub(text, "%s%s+", " ")
text = utf8.gsub(text, "[^A-zА-я0-9':%- ]+", "")
return text
end)
self.value = ""
end
function ItemBrowserSearchBoxMixin:OnHide()
self:StopOnCharSearchDelay()
end
function ItemBrowserSearchBoxMixin:OnEnterPressed()
self:ClearFocus()
self:DoSearch()
end
function ItemBrowserSearchBoxMixin:OnEditFocusLost()
SearchBoxTemplate_OnEditFocusLost(self)
self:HighlightText(0, 0)
self:DoSearch()
end
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
self.searchDelay = self.searchDelay - elapsed
if self.searchDelay <= 0 then
self:SetScript("OnUpdate", nil)
self.searchDelay = nil
self:DoSearch()
end
end
function ItemBrowserSearchBoxMixin:DoSearch()
local newValue = self:GetText()
if self.value ~= newValue and not (self.value == nil and newValue == "") then
self.value = newValue
self:GetParent():SearchItem(newValue)
end
end
function ItemBrowserSearchBoxMixin:ToggleOnCharFilter(state, searchUpdateDelay)
if state then
self.searchUpdateDelay = searchUpdateDelay
self.searchDelay = searchUpdateDelay
else
self:SetScript("OnUpdate", nil)
self.searchUpdateDelay = nil
self.searchDelay = nil
end
end
function ItemBrowserSearchBoxMixin:OnTextChanged(userInput)
PKBT_EditBoxMixin.OnTextChanged(self, userInput)
SearchBoxTemplate_OnTextChanged(self)
end
function ItemBrowserSearchBoxMixin:OnTextValueChanged(value, userInput)
if userInput then
if self.searchUpdateDelay then
self.searchDelay = self.searchUpdateDelay
self:SetScript("OnUpdate", self.OnUpdate)
end
else
self:StopOnCharSearchDelay()
end
end
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
self.searchDelay = self.searchDelay - elapsed
if self.searchDelay <= 0 then
self:SetScript("OnUpdate", nil)
self.searchDelay = nil
self:DoSearch()
end
end
function ItemBrowserSearchBoxMixin:StopOnCharSearchDelay()
self:SetScript("OnUpdate", nil)
if self.searchUpdateDelay then
self.searchDelay = self.searchUpdateDelay
end
end
function ItemBrowserSearchBoxMixin:OnClearClick(button)
SearchBoxTemplateClearButton_OnClick(self.clearButton, button)
self:StopOnCharSearchDelay()
self:DoSearch()
end
ItemBrowserListHeaderButtonMixin = CreateFromMixins(TableBuilderElementMixin)
function ItemBrowserListHeaderButtonMixin:Init(name)
self:SetText(name)
end
function ItemBrowserListHeaderButtonMixin:OnClick(button)
end
ItemBrowserListRowButton = CreateFromMixins(PKBT_OwnerMixin, TableBuilderRowMixin)
function ItemBrowserListRowButton:OnLoad()
self.UpdateTooltip = self.OnEnter
end
function ItemBrowserListRowButton:OnEnter()
if self.itemLink then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetHyperlink(self.itemLink)
GameTooltip:Show()
end
end
function ItemBrowserListRowButton:OnLeave()
GameTooltip:Hide()
end
function ItemBrowserListRowButton:OnClick(button)
if self.itemLink then
HandleModifiedItemClick(self.itemLink)
end
end
function ItemBrowserListRowButton:Populate(rowData, dataIndex)
self:SetID(dataIndex)
self.itemID = rowData.itemID
self.itemLink = rowData.itemLink
end
ItemBrowserListCellMixin = {}
function ItemBrowserListCellMixin:OnLoad()
end
function ItemBrowserListCellMixin:Init(dataProviderKey)
self.dataProviderKey = dataProviderKey
end
function ItemBrowserListCellMixin:Populate(rowData, dataIndex)
local value = rowData[self.dataProviderKey]
self.Text:SetJustifyH(self.textAlignment or "CENTER")
self.Text:SetText(value or "?")
end
ItemBrowserListCellNameIconMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellNameIconMixin:Populate(rowData, dataIndex)
local textWidth = self:GetWidth() - 10
self.Text:SetText(rowData.name or UNKNOWN)
self.Text:SetWidth(textWidth)
self.SubText:SetWidth(textWidth)
if rowData.itemType and rowData.itemSubType then
self.SubText:SetFormattedText("%s - %s", rowData.itemType, rowData.itemSubType)
elseif rowData.itemType or rowData.itemSubType then
self.SubText:SetText(rowData.itemType or rowData.itemSubType)
else
self.SubText:SetText("")
end
self.Icon:SetTexture(rowData.icon or QUESTION_MARK_ICON)
local r, g, b = GetItemQualityColor(rowData.rarity or 1)
self.Border:SetVertexColor(r, g, b)
end
ItemBrowserListCellItemLevelMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellItemLevelMixin:Populate(rowData, dataIndex)
self.Text:SetText(rowData.itemLevel and rowData.itemLevel ~= 0 and rowData.itemLevel or "")
end
ItemBrowserListCellItemIDMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellItemIDMixin:Populate(rowData, dataIndex)
self.Text:SetText(rowData.itemID)
end
@@ -1,331 +0,0 @@
<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="Custom_ItemBrowser.lua"/>
<Button name="ItemBrowserListHeaderButtonTemplate" virtual="true">
<Size x="50" y="30"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true">
<Color r="0.2" g="0.2" b="0.2"/>
</Texture>
</Layer>
<!--
<Layer level="HIGHLIGHT">
<Texture name="$parentHighlight" parentKey="Highlight" setAllPoints="true" alpha="0.2" alphaMode="ADD">
<Color r="1" g="1" b="1"/>
</Texture>
</Layer>
-->
</Layers>
<NormalFont style="SystemFont_Shadow_Small"/>
<PushedTextOffset x="0" y="0"/>
<!--
<PushedTextOffset x="1" y="-1"/>
<ButtonText name="$parentButtonText" parentKey="ButtonText">
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
</ButtonText>
-->
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListHeaderButtonMixin)
</OnLoad>
<!--
<OnClick>
self:OnClick(button)
</OnClick>
-->
</Scripts>
</Button>
<Button name="ItemBrowserListRowButtonTemplate" virtual="true">
<Size x="0" y="46"/>
<Anchors>
<Anchor point="LEFT" x="1" y="0"/>
<Anchor point="RIGHT" x="-1" y="0"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true" inherits="_SearchBarLg"/>
</Layer>
</Layers>
<HighlightTexture file="Interface\Common\Search">
<TexCoords left="0.001953" right="0.501953" top="0.234375" bottom="0.601562"/>
</HighlightTexture>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListRowButton)
self:OnLoad()
</OnLoad>
<OnEvent>
self:OnEvent(event, ...)
</OnEvent>
<OnEnter>
self:OnEnter()
</OnEnter>
<OnLeave>
self:OnLeave()
</OnLeave>
<OnClick>
self:OnClick()
</OnClick>
</Scripts>
</Button>
<Frame name="ItemBrowserListCellNameIconTemplate" virtual="true">
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentBorder" parentKey="Border" file="Interface\Common\Search">
<Size x="40" y="40"/>
<Anchors>
<Anchor point="LEFT" x="10" y="0"/>
</Anchors>
<TexCoords left="0.755859" right="0.830078" top="0.234375" bottom="0.531250"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentIcon" parentKey="Icon">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBorder" x="1" y="-2"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorder" x="-1" y="1"/>
</Anchors>
</Texture>
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" justifyH="LEFT" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT" x="10" y="8"/>
</Anchors>
<Color r="0.96875" g="0.8984375" b="0.578125" a="1"/>
</FontString>
<FontString name="$parentSubText" parentKey="SubText" inherits="GameFontNormal" justifyH="LEFT">
<Size x="400" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentText" relativePoint="BOTTOMLEFT" x="0" y="-5"/>
</Anchors>
<Color r="0.66796875" g="0.51171875" b="0.3359375" a="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellNameIconMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowserListCellItemLevelTemplate" virtual="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Color r="1" g="1" b="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellItemLevelMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowserListCellItemIDTemplate" virtual="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Color r="1" g="1" b="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellItemIDMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowser" inherits="PortraitFrameTemplate" toplevel="true" enableMouse="true" parent="UIParent" hidden="true">
<Size x="800" y="496"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Frames>
<Frame name="$parentInset" inherits="InsetFrameTemplate" parentKey="inset">
<Anchors>
<Anchor point="TOPRIGHT" x="-4" y="-60"/>
<Anchor point="BOTTOMLEFT" x="4" y="5"/>
</Anchors>
</Frame>
<EditBox name="$parentSearchBox" parentKey="SearchBox" inherits="PKBT_EditBoxTemplate" letters="64">
<Size x="130" y="36"/>
<Anchors>
<Anchor point="TOPLEFT" x="60" y="-22"/>
<Anchor point="TOPRIGHT" x="-10" y="-22"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentSearchIcon" parentKey="searchIcon" file="Interface\Common\UI-Searchbox-Icon">
<Size x="14" y="14"/>
<Anchors>
<Anchor point="LEFT" x="5" y="-2"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentClearButton" parentKey="clearButton" hidden="true">
<Size x="22" y="22"/>
<Anchors>
<Anchor point="RIGHT" x="-5" y="-1"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture parentKey="texture" file="Interface\FriendsFrame\ClearBroadcastIcon" alpha="0.5" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>
self.texture:SetAlpha(1.0)
</OnEnter>
<OnLeave>
self.texture:SetAlpha(0.5)
</OnLeave>
<OnMouseDown>
if self:IsEnabled() == 1 then
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1)
end
</OnMouseDown>
<OnMouseUp>
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
</OnMouseUp>
<OnClick>
self:GetParent():OnClearClick(button)
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserSearchBoxMixin)
self:OnLoad()
</OnLoad>
<OnHide>
self:OnHide()
</OnHide>
<OnTextChanged>
self:OnTextChanged(userInput)
</OnTextChanged>
<OnEnterPressed>
self:OnEnterPressed()
</OnEnterPressed>
<OnEditFocusLost>
self:OnEditFocusLost()
</OnEditFocusLost>
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
</Scripts>
</EditBox>
<StatusBar name="$parentSearchProgressBar" parentKey="SearchProgressBar" minValue="0" maxValue="1" alpha="0">
<Size x="0" y="6"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentSearchBox" relativePoint="BOTTOMLEFT" x="7" y="1"/>
<Anchor point="TOPRIGHT" relativeTo="$parentSearchBox" relativePoint="BOTTOMRIGHT" x="-7" y="1"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Color r="0" g="0" b="0" a="0.3"/>
</Texture>
</Layer>
</Layers>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="1" b="0"/>
</StatusBar>
<ScrollFrame name="$parentScroll" parentKey="Scroll" inherits="HybridScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentInset" relativePoint="TOPLEFT" x="10" y="-50"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" relativePoint="BOTTOMRIGHT" x="-36" y="10"/>
</Anchors>
<Frames>
<Slider name="$parentScrollBar" parentKey="ScrollBar" inherits="PKBT_HybridScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="5" y="-25"/>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="5" y="27"/>
</Anchors>
</Slider>
</Frames>
</ScrollFrame>
<Frame name="$parentHeaderHolder" parentKey="HeaderHolder">
<Size x="0" y="30"/>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentScroll" relativePoint="TOPLEFT" x="0" y="12"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentScroll" relativePoint="TOPRIGHT" x="0" y="12"/>
</Anchors>
</Frame>
<Button name="$parentTab1" inherits="CharacterFrameTabButtonTemplate" text="ADVENTURE_JOURNAL" id="1" parentKey="tab1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="BOTTOMLEFT" x="19" y="-30"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab2" inherits="CharacterFrameTabButtonTemplate" text="HEADHUNTING" id="2" parentKey="tab2">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab1" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab3" inherits="CharacterFrameTabButtonTemplate" text="HARDCORE_CHALLENGES_TITLE" id="3" parentKey="tab3">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab2" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab4" inherits="CharacterFrameTabButtonTemplate" text="ITEM_BROWSER" id="4" parentKey="tab4">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab3" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserMixin)
self:OnLoad()
</OnLoad>
<OnShow>
self:OnShow()
</OnShow>
<OnEvent>
self:OnEvent(event, ...)
</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,483 +0,0 @@
do
local tonumber = tonumber
local unpack = unpack
local bitband = bit.band
local strsub = string.sub
local LJ_BUFFER = {}
local NO_CLASS_FILTER = 0
local NO_SPEC_FILTER = 0
local function getPlayerFactionID()
local factionGroup = UnitFactionGroup("player") or "NEUTRAL"
if factionGroup == "Renegade" then
return (C_FactionManager.GetFactionInfoOriginal())
end
return PLAYER_FACTION_GROUP[factionGroup]
end
local function filterByFaction(factionID, playerFactionID)
if factionID == PLAYER_FACTION_GROUP.Neutral then
return true
elseif factionID == playerFactionID then
return true
end
return false
end
local function filterByClassAndSpecFilters(classID, specID, classFilter, specFilter)
if classFilter and specFilter then
if specFilter == 3 then
specFilter = 4
end
if classFilter == NO_CLASS_FILTER and specFilter == NO_SPEC_FILTER then
return true
elseif classFilter == classID and specFilter == NO_SPEC_FILTER then
return true
elseif classFilter == classID and bitband(specFilter, specID) == specFilter then
return true
end
end
return false
end
local function sortItemSets(a, b)
if a[4] == b[4] then
if a[6] ~= b[6] then
return a[6]
end
return (tonumber(strsub(a[3], 3, -1) or 0)) > (tonumber(strsub(b[3], 3, -1) or 0))
end
return a[4] > b[4]
end
C_LootJournal = {}
function C_LootJournal.GenerateLootData(classFilter, specFilter)
table.wipe(LJ_BUFFER)
local serverID = C_Service and C_Service.GetRealmID and C_Service.GetRealmID() or 0
local playerFactionID = getPlayerFactionID()
local itemSetSourceRealm = EJ_ITEMSET_SOURCE_REALM and EJ_ITEMSET_SOURCE_REALM[serverID]
for setIndex, itemSetData in pairs(EJ_ITEMSET_DATA) do
local name, itemlevel, tierName, sourceID, classID, specID, isPVP, itemList, factionID, realmFlag = unpack(itemSetData, 1, 10)
if not factionID then
factionID = 0
end
if filterByClassAndSpecFilters(classID, specID, classFilter or 0, specFilter or 0)
and filterByFaction(factionID, playerFactionID)
then
if itemSetSourceRealm and itemSetSourceRealm[setIndex] then
sourceID = itemSetSourceRealm[setIndex]
end
local sourceText = EJ_ITEMSET_SOURCE[sourceID] or ""
local numItems = type(itemList) == "table" and #itemList or 0
local items = {}
for i = 1, numItems do
local itemID = itemList[i]
items[i] = {
itemID = itemID,
icon = GetItemIcon(itemID) or [[Interface\Icons\INV_Misc_QuestionMark]],
}
end
LJ_BUFFER[#LJ_BUFFER + 1] = {setIndex, name, tierName or "", itemlevel, sourceText, isPVP == 1, items, numItems}
end
end
table.sort(LJ_BUFFER, sortItemSets)
return LJ_BUFFER
end
function C_LootJournal.GetNumItemSets()
return #LJ_BUFFER
end
function C_LootJournal.GetItemSetInfo(index)
if index and LJ_BUFFER[index] then
return unpack(LJ_BUFFER[index])
end
end
end
local NO_SPEC_FILTER = 0;
local NO_CLASS_FILTER = 0;
local NO_INV_TYPE_FILTER = 0;
--===================================================================================================================================
LootJournalItemsMixin = { };
function LootJournalItemsMixin:OnLoad()
self.Background:SetAtlas("loottab-background", true)
self:SetView(LOOT_JOURNAL_ITEM_SETS);
local _, _, classID = UnitClass("player");
local specIndex = C_Talent.GetCurrentSpecTabIndex()
self:SetClassAndSpecFilters(classID, specIndex);
end
function LootJournalItemsMixin:SetClassAndSpecFilters(classID, specID)
self.classFilter = classID;
self.specFilter = specID;
end
function LootJournalItemsMixin:GetClassAndSpecFilters()
return self.classFilter or NO_CLASS_FILTER, self.specFilter or NO_SPEC_FILTER;
end
function LootJournalItemsMixin:SetView(view)
if ( self.view == view ) then
return;
end
self.view = view;
self.ItemSetsFrame:Show();
end
function LootJournalItemsMixin:GetActiveList()
return self.ItemSetsFrame;
end
function LootJournalItemsMixin:Refresh()
self:GetActiveList():Refresh();
end
function LootJournalItemButtonTemplate_OnEnter(self)
local listFrame = self:GetParent();
while ( listFrame and not listFrame.ShowItemTooltip ) do
listFrame = listFrame:GetParent();
end
if ( listFrame ) then
listFrame:ShowItemTooltip(self);
self:SetScript("OnUpdate", LootJournalItemButton_OnUpdate);
self.UpdateTooltip = LootJournalItemButtonTemplate_OnEnter;
end
end
function LootJournalItemButtonTemplate_OnLeave(self)
self.UpdateTooltip = nil;
GameTooltip:Hide();
self:SetScript("OnUpdate", nil);
ResetCursor();
end
function LootJournalItemButton_OnClick(self, button)
if HandleModifiedItemClick(self.itemLink) then
return
end
LootJournal_OpenItemByEntry(self.itemID)
end
--===================================================================================================================================
LootJournalItemSetsMixin = {}
local LJ_ITEMSET_X_OFFSET = 10;
local LJ_ITEMSET_Y_OFFSET = 10;
local LJ_ITEMSET_BUTTON_SPACING = 13;
local LJ_ITEMSET_BOTTOM_BUFFER = 4;
function LootJournalItemSetsMixin:Refresh()
self.dirty = true;
local offset = self.scrollBar:GetValue();
if ( offset == 0 ) then
self:UpdateList();
else
self.scrollBar:SetValue(0);
end
if ( self.ClassButton ) then
self:UpdateClassButtonText();
end
end
function LootJournalItemSetsMixin:GetClassAndSpecFilters()
return self:GetParent():GetClassAndSpecFilters();
end
function LootJournalItemSetsMixin:UpdateClassButtonText()
end
function LootJournalItemSetsMixin:ShowItemTooltip(button)
-- itemLink may not be available
if not button.itemLink then
GameTooltip:Hide()
return
end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT");
GameTooltip:SetHyperlink(button.itemLink);
if LootJournal_CanOpenItemByEntry(button.itemID) then
GameTooltip:AddLine(LOOTJOURNAL_ITEM_CLICK_TO_OPEN_LOOT, 0, 0.8, 1)
end
self.tooltipItemID = button.itemID;
GameTooltip:Show();
end
function LootJournalItemSetsMixin:CheckItemButtonTooltip(button)
if ( GameTooltip:GetOwner() == button and self.tooltipItemID ~= button.itemID ) then
self:ShowItemTooltip(button);
end
end
function LootJournalItemSetsMixin:GetClassFilter()
local classFilter, specFilter = self:GetClassAndSpecFilters();
return classFilter;
end
function LootJournalItemSetsMixin:GetSpecFilter()
local classFilter, specFilter = self:GetClassAndSpecFilters();
return specFilter;
end
function LootJournalItemSetsMixin:SetClassAndSpecFilters(newClassFilter, newSpecFilter)
local classFilter, specFilter = self:GetClassAndSpecFilters();
if not self.classAndSpecFiltersSet or classFilter ~= newClassFilter or specFilter ~= newSpecFilter then
--[[
-- if choosing just a class without a spec, pick a spec
if newClassFilter ~= NO_CLASS_FILTER and newSpecFilter == NO_SPEC_FILTER then
local _, _, classID = UnitClass("player");
-- if player's class, choose active spec
-- otherwise use 1st spec
if classID == newClassFilter then
local specIndex = C_Talent.GetCurrentSpecTabIndex()
newSpecFilter = specIndex
else
newSpecFilter = 1
end
end
--]]
self:GetParent():SetClassAndSpecFilters(newClassFilter, newSpecFilter);
self.scrollBar:SetValue(0);
self:Refresh();
end
CloseDropDownMenus(1);
self.classAndSpecFiltersSet = true;
end
do
function LootJournalItemButton_OnUpdate(self)
if GameTooltip:IsOwned(self) then
if IsModifiedClick("DRESSUP") then
ShowInspectCursor();
else
ResetCursor();
end
end
end
local function OpenClassFilterDropDown(self, level)
if level then
self:GetParent():GetActiveList():OpenClassFilterDropDown(level);
end
end
function LootJournalItemsClassDropDown_OnLoad(self)
UIDropDownMenu_Initialize(self, OpenClassFilterDropDown, "MENU");
end
local CLASS_DROPDOWN = 1;
function LootJournalItemSetsMixin:OpenClassFilterDropDown(level)
local filterClassID = self:GetClassFilter();
local filterSpecID = self:GetSpecFilter();
local classDisplayName, classTag, classID;
local function SetClassAndSpecFilters(_, classFilter, specFilter)
self:SetClassAndSpecFilters(classFilter, specFilter);
end
local info = UIDropDownMenu_CreateInfo();
if UIDROPDOWNMENU_MENU_VALUE == CLASS_DROPDOWN then
info.text = ALL_CLASSES;
info.checked = filterClassID == NO_CLASS_FILTER;
info.arg1 = NO_CLASS_FILTER;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
local numClasses = GetNumClasses();
for i = 1, numClasses do
classDisplayName, classTag, classID = GetClassInfo(i);
if classID ~= 10 then
info.text = classDisplayName;
info.checked = filterClassID == classID;
info.arg1 = classID;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
end
end
if level == 1 then
info.text = CLASS;
info.func = nil;
info.notCheckable = true;
info.hasArrow = true;
info.value = CLASS_DROPDOWN;
UIDropDownMenu_AddButton(info, level);
local classDisplayName, classTag, classID;
if filterClassID ~= NO_CLASS_FILTER then
classID = filterClassID;
classDisplayName = GetClassInfo(classID)
else
classDisplayName, classTag, classID = UnitClass("player");
end
info.text = classDisplayName;
info.notCheckable = true;
info.arg1 = nil;
info.arg2 = nil;
info.func = nil;
info.hasArrow = false;
UIDropDownMenu_AddButton(info, level);
info.notCheckable = nil;
for i = 1, GetNumSpecializationsForClassID(classID) do
local _, specName = GetSpecializationInfoForClassID(classID, i)
info.leftPadding = 10;
info.text = specName;
info.checked = filterSpecID == i;
info.arg1 = classID;
info.arg2 = i;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
info.text = ALL_SPECS;
info.leftPadding = 10;
info.checked = (classID == filterClassID and filterSpecID == NO_SPEC_FILTER) or (filterClassID == NO_CLASS_FILTER and filterSpecID == NO_SPEC_FILTER);
info.arg1 = classID;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
end
end
function LootJournalItemSetsMixin:OnLoad()
self.scrollBar.trackBG:Hide();
self.update = LootJournalItemSetsMixin.UpdateList;
HybridScrollFrame_CreateButtons(self, "LootJournalItemSetButtonTemplate", LJ_ITEMSET_X_OFFSET, -LJ_ITEMSET_Y_OFFSET, "TOPLEFT", nil, nil, -LJ_ITEMSET_BUTTON_SPACING);
end
function LootJournalItemSetsMixin:OnShow()
if not self.init then
self.init = true;
self:Refresh();
end
end
local function onItemLoaded(itemID)
local self = EncounterJournal.LootJournalItems.ItemSetsFrame
for i = 1, #self.buttons do
local itemButtons = self.buttons[i].ItemButtons;
for j = 1, #itemButtons do
if ( itemButtons[j].itemID == itemID ) then
self:ConfigureItemButton(itemButtons[j]);
return;
end
end
end
end
function LootJournalItemSetsMixin:ConfigureItemButton(button)
local _, itemLink, itemQuality, _, _, _, _, _, _, icon = C_Item.GetItemInfo(button.itemID, false, onItemLoaded, true);
button.itemLink = itemLink;
itemQuality = itemQuality or Enum.ItemQuality.Epic; -- sets are most likely rare
if ( itemQuality == Enum.ItemQuality.Uncommon ) then
button.Border:SetAtlas("loottab-set-itemborder-green", true);
elseif ( itemQuality == Enum.ItemQuality.Rare ) then
button.Border:SetAtlas("loottab-set-itemborder-blue", true);
elseif ( itemQuality == Enum.ItemQuality.Epic ) then
button.Border:SetAtlas("loottab-set-itemborder-purple", true);
end
button.Icon:SetTexture(icon or "Interface\\ICONS\\temp")
button:GetParent().SetName:SetTextColor(GetItemQualityColor(itemQuality));
self:CheckItemButtonTooltip(button);
end
function LootJournalItemSetsMixin:UpdateList()
if ( self.dirty ) then
C_LootJournal.GenerateLootData(self:GetClassAndSpecFilters())
self.dirty = nil;
end
local buttons = self.buttons;
local offset = HybridScrollFrame_GetOffset(self);
local numSets = C_LootJournal.GetNumItemSets()
for i = 1, #buttons do
local button = buttons[i];
local index = offset + i;
if ( index <= numSets ) then
local setIndex, name, tierName, itemlevel, sourceText, isPVP, items, numItems = C_LootJournal.GetItemSetInfo(index)
button:Show();
button.SetName:SetText(name);
button.ItemLevel:SetFormattedText(EJ_SET_ITEM_LEVEL, tierName, itemlevel);
button.PVPIcon:SetShown(isPVP);
button.setIndex = setIndex
button.tooltipText = sourceText
button:CheckItemButtonTooltip()
for j = 1, numItems do
local itemButton = button.ItemButtons[j];
if ( not itemButton ) then
itemButton = CreateFrame("BUTTON", string.format("$parentItemButton%i", j), button, "LootJournalItemSetItemButtonTemplate");
button[string.format("$parentItemButton%i", j)] = itemButton
itemButton:SetPoint("LEFT", button.ItemButtons[j-1], "RIGHT", 5, 0);
end
itemButton.Icon:SetTexture(items[j].icon);
itemButton.itemID = items[j].itemID;
itemButton:Show();
self:ConfigureItemButton(itemButton);
end
for j = numItems + 1, #button.ItemButtons do
button.ItemButtons[j].itemID = nil;
button.ItemButtons[j]:Hide();
end
else
button:Hide();
end
end
local totalHeight = numSets * buttons[1]:GetHeight() + (numSets - 1) * LJ_ITEMSET_BUTTON_SPACING + LJ_ITEMSET_Y_OFFSET + LJ_ITEMSET_BOTTOM_BUFFER
HybridScrollFrame_Update(self, totalHeight, self:GetHeight());
end
LootJournalItemSetButtonMixin = {}
function LootJournalItemSetButtonMixin:OnEnter()
if self.tooltipText then
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText(LOOTJOURNAL_SOURCE_TOOLTIP_HEAD, 1, 1, 1)
GameTooltip:AddLine(self.tooltipText, nil, nil, nil, 1)
self.tooltipSetIndex = self.setIndex
GameTooltip:Show()
else
GameTooltip:Hide()
end
end
function LootJournalItemSetButtonMixin:CheckItemButtonTooltip()
if ( GameTooltip:GetOwner() == self and self.tooltipSetIndex ~= self.setIndex ) then
self:OnEnter();
end
end
@@ -1,164 +0,0 @@
<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="Custom_LootJournalItems.lua"/>
<Button name="LootJournalItemButtonTemplate" virtual="true">
<Scripts>
<OnEnter function="LootJournalItemButtonTemplate_OnEnter"/>
<OnLeave function="LootJournalItemButtonTemplate_OnLeave"/>
<OnClick function="LootJournalItemButton_OnClick"/>
</Scripts>
</Button>
<Button name="LootJournalItemSetItemButtonTemplate" inherits="LootJournalItemButtonTemplate" virtual="true" hidden="true">
<Size x="32" y="32"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon" parentKey="Icon">
<Size x="28" y="28"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</Texture>
</Layer>
<Layer level="BORDER">
<Texture parentKey="Border">
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentIcon" relativePoint="CENTER" x="20" y="1"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
self:SetParentArray("ItemButtons")
self.Border:SetAtlas("loottab-set-itemborder-green", true)
</OnLoad>
</Scripts>
</Button>
<Button name="LootJournalItemSetButtonTemplate" virtual="true">
<Size x="744" y="41"/>
<HitRectInsets left="34" right="390" top="1" bottom="1"/>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="0"/>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentSetName" parentKey="SetName" inherits="GameFontNormalMed2" maxLines="1" justifyH="LEFT">
<Size x="320" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" x="38" y="-6"/>
</Anchors>
</FontString>
<FontString name="$parentItemLevel" parentKey="ItemLevel" inherits="GameFontNormal" text="Item Level" maxLines="1" justifyH="LEFT">
<Size x="320" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentSetName" relativePoint="BOTTOMLEFT" x="0" y="-2"/>
</Anchors>
<Color r="0.718" g="0.561" b="0.416"/>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentPVPIcon" parentKey="PVPIcon" hidden="true">
<Size x="30" y="30"/>
<Anchors>
<Anchor point="LEFT" relativePoint="LEFT" x="0" y="2"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon" parentKey="Icon" file="Interface\PVPFrame\Icons\prestige-icon-4"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP_HEAD, 1, 1, 1)
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP, nil, nil, nil, 1)
GameTooltip:Show()
</OnEnter>
<OnLeave function="GameTooltip_Hide"/>
</Scripts>
</Frame>
<Button name="$parentItemButton1" parentKey="ItemButton1" inherits="LootJournalItemSetItemButtonTemplate">
<Anchors>
<Anchor point="LEFT" x="397" y="0"/>
</Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemSetButtonMixin)
self.Background:SetAtlas("loottab-set-background", true)
</OnLoad>
<OnEnter>
self:OnEnter()
</OnEnter>
<OnLeave function="GameTooltip_Hide"/>
</Scripts>
</Button>
<Frame parent="EncounterJournal" name="$parentLootJournalItems" parentKey="LootJournalItems" hidden="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentInset" x="4" y="-51" />
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" x="-4" y="0"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOP" x="0" y="4"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="LootJournalItemsClassDropDown" parentKey="ClassDropDown" inherits="UIDropDownMenuTemplate" hidden="true">
<Scripts>
<OnLoad function="LootJournalItemsClassDropDown_OnLoad"/>
</Scripts>
</Frame>
<ScrollFrame name="$parentItemSetsFrame" parentKey="ItemSetsFrame" inherits="MinimalHybridScrollFrameTemplate" hidden="true">
<Size x="762" y="332"/>
<Anchors>
<Anchor point="BOTTOMLEFT" x="0" y="3"/>
</Anchors>
<Frames>
<Button parentKey="ClassButton" inherits="EJButtonTemplate" text="FILTER">
<Size x="114" y="26"/>
<Anchors>
<Anchor point="TOPRIGHT" x="-12" y="35"/>
</Anchors>
<Scripts>
<OnMouseDown>
EJButtonMixin.OnMouseDown(self, button);
ToggleDropDownMenu(1, nil, self:GetParent():GetParent().ClassDropDown, self, 5, 0);
PlaySound("igMainMenuOptionCheckBoxOn");
</OnMouseDown>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemSetsMixin)
self:OnLoad()
</OnLoad>
<OnShow>
self:OnShow()
</OnShow>
</Scripts>
</ScrollFrame>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemsMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
</Ui>
@@ -1,484 +0,0 @@
local SHOW_RECOMMENDATIONS = true
local DENSITY = "regular"
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 }
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
local BIOME_TEX = {
fire = T .. "Biome-Fire",
arcane = T .. "Biome-Arcane",
storm = T .. "Biome-Storm",
shadow = T .. "Biome-Shadow",
nature = T .. "Biome-Nature",
holy = T .. "Biome-Holy",
}
local BADGE_TEX = {
active = T .. "StageBadge-Active",
done = T .. "StageBadge-Done",
locked = T .. "StageBadge-Locked",
}
local TYPE_ICON = {
level = T .. "TypeIcon-Level",
quest = T .. "TypeIcon-Quest",
quest_chain = T .. "TypeIcon-QuestChain",
dungeon = T .. "TypeIcon-Dungeon",
raid = T .. "TypeIcon-Raid",
boss = T .. "TypeIcon-Boss",
achievement = T .. "TypeIcon-Achievement",
reputation = T .. "TypeIcon-Reputation",
profession = T .. "TypeIcon-Profession",
item = T .. "TypeIcon-Item",
currency = T .. "TypeIcon-Currency",
custom = T .. "TypeIcon-Custom",
done = T .. "TypeIcon-Done",
locked = T .. "TypeIcon-Locked",
}
local function showOrHide(frame, show)
if not frame then return end
if show then frame:Show() else frame:Hide() end
end
local function getScrollBar(scrollFrame)
if not scrollFrame then return nil end
return scrollFrame.ScrollBar or _G[scrollFrame:GetName() .. "ScrollBar"]
end
local function updateScrollBar(scrollFrame, contentHeight, contentWidth)
if not scrollFrame then return end
local viewHeight = scrollFrame:GetHeight() or 0
local viewWidth = scrollFrame:GetWidth() or 0
local scrollBar = getScrollBar(scrollFrame)
if scrollFrame.ScrollChild and viewWidth > 0 then
scrollFrame.ScrollChild:SetWidth(math.max(1, contentWidth or viewWidth))
end
if scrollBar then
if contentHeight and viewHeight > 0 and contentHeight > viewHeight + 2 then
scrollBar:Show()
else
scrollBar:Hide()
end
end
end
local function hideScrollBar(scrollFrame)
local scrollBar = getScrollBar(scrollFrame)
if scrollBar then scrollBar:Hide() end
end
local function getBodyChild()
local body = PlayerGuideFrame and PlayerGuideFrame.BodyScroll
return body and body.ScrollChild
end
-- ─────────────────────────────────── Цепочка этапов ──
local stageBadges = {}
local function buildStageChain(stages)
if not PlayerGuideFrame then return end
local container = PlayerGuideFrame.StageChain
for i = (#stages + 1), #stageBadges do stageBadges[i]:Hide() end
if #stages == 0 then return end
local width = container:GetWidth()
if width == 0 then width = 600 end
local slot = width / #stages
for i, s in ipairs(stages) do
local badge = stageBadges[i]
if not badge then
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
stageBadges[i] = badge
end
badge:ClearAllPoints()
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
badge:Show()
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
badge.Label:SetText(s.short or s.name or "")
local status = s.status or "locked"
badge.BadgeBG:SetTexture(BADGE_TEX[status] or BADGE_TEX.locked)
if status == "active" then
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
badge.Label:SetTextColor(1.0, 0.84, 0.42)
elseif status == "done" then
badge.Roman:SetText("v")
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
badge.Label:SetTextColor(0.79, 0.65, 0.38)
else
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
badge.Label:SetTextColor(0.40, 0.34, 0.22)
end
end
end
-- ─────────────────────────────────── Карточки целей ──
local objectiveCards = {}
local CARD_HEIGHT = { compact = 54, regular = 70, cozy = 82 }
local CARD_GAP = { compact = 6, regular = 8, cozy = 10 }
local function renderObjectives()
local section = PlayerGuideFrame.ObjectivesSection
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumObjectives()
local width = 0
if PlayerGuideFrame.BodyScroll then
width = PlayerGuideFrame.BodyScroll:GetWidth()
end
if width == 0 then width = section:GetWidth() end
if width == 0 then width = section.ScrollFrame:GetWidth() end
if width == 0 then width = 860 end
-- Body child right inset 8, section right inset 0, scrollframe left/right insets 6/8.
-- The card then keeps 8px equal visual padding on both sides of the objective row.
parent:SetWidth(math.max(1, width - 30))
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
local h = CARD_HEIGHT[DENSITY] or 44
local gap = CARD_GAP[DENSITY] or 4
local done = 0
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if not o then break end
local card = objectiveCards[i]
if not card then
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
objectiveCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
card:SetHeight(h)
card.objectiveData = o
card:Show()
if card.SetBackdropColor then
card:SetBackdropColor(0, 0, 0, 0.72)
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
end
card.TypeLabel:SetText(_G["MW_PG_TYPE_" .. string.upper(o.type or "custom")] or string.upper(o.type or ""))
card.Title:SetText(o.title or "")
card.Subtitle:SetText(o.subtitle or o.description or "")
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
card.ProgressBar:SetValue(o.progress or 0)
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
if o.completed then
done = done + 1
card.Icon:SetTexture(TYPE_ICON.done)
card.Icon:SetTexCoord(0, 1, 0, 1)
card.Title:SetTextColor(0.62, 0.84, 0.47)
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
card.Count:SetTextColor(0.62, 0.84, 0.47)
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
if card.SetBackdropBorderColor then
card:SetBackdropBorderColor(0.26, 0.52, 0.18, 0.9)
end
else
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
card.Icon:SetTexCoord(0, 1, 0, 1)
card.Title:SetTextColor(1.0, 0.91, 0.63)
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
card.Count:SetTextColor(0.79, 0.65, 0.38)
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
end
if o.required == 1 and (o.progress or 0) == 0 then
card.ProgressBar:Hide()
else
card.ProgressBar:Show()
end
end
parent:SetHeight(math.max(1, n * h + math.max(0, n - 1) * gap))
section:SetHeight(48 + parent:GetHeight())
hideScrollBar(section.ScrollFrame)
section.Count:SetText(string.format(" %d / %d", done, n))
end
-- ─────────────────────────────── Карточки рекомендаций ──
local recCards = {}
local function renderRecommendations()
local section = PlayerGuideFrame.RecommendationsSection
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumRecommendations()
if n == 0 then section:Hide(); return end
section:Show()
for i = (n + 1), #recCards do recCards[i]:Hide() end
local cardW = 176
local gap = 16
for i = 1, n do
local r = C_PlayerGuide.GetRecommendationInfo(i)
if not r then break end
local card = recCards[i]
if not card then
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
recCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
card.recData = r
card:Show()
if card.SetBackdropColor then
card:SetBackdropColor(0, 0, 0, 0.88)
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
end
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
card.Name:SetText(r.name or r.title or "")
card.Loc:SetText(r.loc or r.description or "")
card.Level:SetText(r.level or "")
end
parent:SetWidth(math.max(1, n * cardW + math.max(0, n - 1) * gap))
parent:SetHeight(116)
section:SetHeight(162)
hideScrollBar(section.ScrollFrame)
section.Count:SetText(" " .. n)
end
local function updateBodyContent()
local F = PlayerGuideFrame
local child = getBodyChild()
if not F or not child then return end
local width = F.BodyScroll:GetWidth()
if width == 0 then width = 860 end
child:SetWidth(math.max(1, width - 8))
local y = 0
F.ObjectivesSection:ClearAllPoints()
F.ObjectivesSection:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
F.ObjectivesSection:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, y)
y = y - F.ObjectivesSection:GetHeight() - 16
if F.RecommendationsSection:IsShown() then
F.RecommendationsSection:ClearAllPoints()
F.RecommendationsSection:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
F.RecommendationsSection:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, y)
y = y - F.RecommendationsSection:GetHeight()
end
local contentHeight = math.max(1, -y + 8)
child:SetHeight(contentHeight)
updateScrollBar(F.BodyScroll, contentHeight, child:GetWidth())
end
-- ─────────────────────────────────────── Layout ──
local function applyLayout(self, hasChain)
local chain = self.StageChain
local hdr = self.Header
local body = self.BodyScroll
chain:ClearAllPoints()
chain:SetPoint("TOPLEFT", self, "TOPLEFT", 28, -8)
chain:SetPoint("TOPRIGHT", self, "TOPRIGHT", -28, -8)
hdr:ClearAllPoints()
if hasChain then
hdr:SetPoint("TOPLEFT", chain, "BOTTOMLEFT", 0, -6)
hdr:SetPoint("TOPRIGHT", chain, "BOTTOMRIGHT", 0, -6)
else
hdr:SetPoint("TOPLEFT", self, "TOPLEFT", 30, -68)
hdr:SetPoint("TOPRIGHT", self, "TOPRIGHT", -30, -68)
end
body:ClearAllPoints()
body:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", -4, -8)
body:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -30, 16)
end
-- ─────────────────────────────────────── Show/hide ──
local function showState(state)
local F = PlayerGuideFrame
local hasData = (state == "data")
-- StageChain visibility controlled in refresh() based on #stages
showOrHide(F.Header, hasData)
showOrHide(F.BodyScroll, hasData)
showOrHide(F.ObjectivesSection, hasData)
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
showOrHide(F.EmptyState, state == "empty")
showOrHide(F.LoadingState, state == "loading")
if not hasData then showOrHide(F.StageChain, false) end
end
-- ─────────────────────────────────────── Render ──
local function refresh()
if not PlayerGuideFrame then return end
local state = C_PlayerGuide.GetState()
showState(state)
if state ~= "data" then return end
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
local stages = C_PlayerGuide.GetStages() or {}
local F = PlayerGuideFrame
if #stages > 0 then
F.Header.Pretitle:SetText(string.format(
MW_PLAYER_GUIDE_STAGE_OF or "ЭТАП %d ИЗ %d", id or 0, #stages))
else
F.Header.Pretitle:SetText(string.format("ЭТАП %d", id or 0))
end
F.Header.Title:SetText(name or "")
F.Header.Description:SetText(desc or "")
F.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
F.Header.ProgressPercent:SetText((progress or 0) .. "%")
F.Header.ProgressBar:SetMinMaxValues(0, 100)
F.Header.ProgressBar:SetValue(progress or 0)
local hasChain = #stages > 0
showOrHide(F.StageChain, hasChain)
applyLayout(F, hasChain)
buildStageChain(stages)
updateBodyContent()
renderObjectives()
renderRecommendations()
updateBodyContent()
end
-- ─────────────────────────────────── XML callbacks ──
function MW_PlayerGuide_OnLoad(self)
EncounterJournal.playerGuideFrame = self
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
if bodyChild then
self.ObjectivesSection:SetParent(bodyChild)
self.RecommendationsSection:SetParent(bodyChild)
end
applyLayout(self, false)
local objScrollBar = getScrollBar(self.ObjectivesSection.ScrollFrame)
if objScrollBar then objScrollBar:Hide() end
local recScrollBar = getScrollBar(self.RecommendationsSection.ScrollFrame)
if recScrollBar then recScrollBar:Hide() end
hideScrollBar(self.BodyScroll)
C_PlayerGuide.RegisterListener(function() refresh() end)
end
function MW_PlayerGuide_OnShow()
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
refresh()
end
function MW_PlayerGuide_RefreshButton_OnClick()
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
function MW_PlayerGuide_Objective_OnClick(self)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and o.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(o.instanceID)
EncounterJournal_DisplayInstance(o.instanceID)
end
elseif o.type == "boss" then
if o.encounterID and o.encounterID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EncounterJournal_DisplayEncounter(o.encounterID)
end
elseif o.type == "achievement" then
if o.achievementID then
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
if AchievementFrame_SelectAchievement then
AchievementFrame_SelectAchievement(o.achievementID)
end
end
elseif o.type == "item" then
if o.itemID and o.itemID > 0 then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemByID(o.itemID)
GameTooltip:Show()
end
elseif o.type == "quest" or o.type == "quest_chain" then
local qid = o.questID or o.questChainID or o.targetID
if qid and qid > 0 then
pcall(SetItemRef, "quest:" .. qid .. ":-1", "", "LeftButton")
end
else
C_PlayerGuide.RequestOpenTarget(o.id)
end
end
function MW_PlayerGuide_Objective_OnEnter(self)
local o = self.objectiveData
if not o then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
if o.subtitle and o.subtitle ~= "" then
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
end
if not o.completed then
GameTooltip:AddLine(
string.format("Прогресс: %d / %d", o.progress or 0, o.required or 1),
0.79, 0.65, 0.38)
end
GameTooltip:Show()
end
function MW_PlayerGuide_Objective_OnLeave() GameTooltip:Hide() end
function MW_PlayerGuide_Rec_OnClick(self)
local r = self.recData
if r and r.instanceID and r.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(r.instanceID)
EncounterJournal_DisplayInstance(r.instanceID)
end
end
function MW_PlayerGuide_Rec_OnEnter(self)
local r = self.recData
if not r then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(r.name or r.title or "", 1, 0.91, 0.63)
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
if r.level then GameTooltip:AddLine("Уровень: " .. r.level, 0.79, 0.65, 0.38) end
GameTooltip:Show()
end
function MW_PlayerGuide_Rec_OnLeave() GameTooltip:Hide() end
-- ─────────────────────────────── Экспорт для отладки ──
MW_PlayerGuide = {
Refresh = refresh,
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
}
@@ -1,486 +0,0 @@
<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="PlayerGuide.lua"/>
<!-- ───────── Шрифты ───────── -->
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
<Color r="0.85" g="0.78" b="0.63"/>
</Font>
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
<Color r="0.63" g="0.46" b="0.19"/>
</Font>
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
<Color r="0.66" g="0.59" b="0.47"/>
</Font>
<!-- ───────── Карточка цели ───────── -->
<!-- Высота = 60px (DENSITY "regular"). Позиции: TypeLabel+Title строка 1, Subtitle строка 2, бар внизу. -->
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
<Size><AbsDimension x="0" y="70"/></Size>
<Backdrop bgFile="Interface\Buttons\WHITE8X8"
edgeFile="Interface\Buttons\WHITE8X8"
tile="true">
<BackgroundInsets><AbsInset left="0" right="0" top="0" bottom="0"/></BackgroundInsets>
<EdgeSize><AbsValue val="1"/></EdgeSize>
<TileSize><AbsValue val="64"/></TileSize>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<!-- Рамка иконки — фоновый слой, центрируется по LEFT -->
<Texture name="$parentIconBorder" parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="50" y="50"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentIcon" parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
<Size><AbsDimension x="34" y="34"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Строка 1: TypeLabel (префикс) + Title (основной текст) на одной строке -->
<FontString name="$parentTypeLabel" parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
<Size><AbsDimension x="90" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="72" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentTitle" parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="185" y="-16"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-118" y="-16"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Строка 2: Subtitle (описание/прогресс) -->
<FontString name="$parentSubtitle" parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="72" y="-36"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-118" y="-36"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Count: мелко снизу справа -->
<FontString name="$parentCount" parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="110" y="10"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-18" y="10"/></Offset></Anchor>
</Anchors>
<Color r="0.63" g="0.46" b="0.19"/>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Прогресс-бар: тонкая полоска у самого низа карточки -->
<StatusBar name="$parentProgressBar" parentKey="ProgressBar">
<Size><AbsDimension x="0" y="4"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="72" y="8"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-118" y="8"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
</Frames>
<Scripts>
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
</Scripts>
</Frame>
<!-- ───────── Карточка рекомендации ───────── -->
<Button name="MW_PG_RecCardTemplate" virtual="true">
<!-- Height 96: fits in the 100px visible area of RecommendationsSection scrollframe -->
<Size><AbsDimension x="176" y="116"/></Size>
<Backdrop bgFile="Interface\Buttons\WHITE8X8"
edgeFile="Interface\Buttons\WHITE8X8"
tile="true">
<BackgroundInsets><AbsInset left="0" right="0" top="0" bottom="0"/></BackgroundInsets>
<EdgeSize><AbsValue val="1"/></EdgeSize>
<TileSize><AbsValue val="64"/></TileSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="168" y="72"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
<FontString name="$parentLevel" parentKey="Level" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-9"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Name: 4px below artwork (artwork bottom y=61, name at y=65) -->
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-80"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Loc: 3px below name (name bottom y=79, loc at y=82) -->
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-96"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
</Scripts>
</Button>
<!-- ───────── Бейдж этапа ───────── -->
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
<Size><AbsDimension x="38" y="38"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBadgeBG" parentKey="BadgeBG"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Locked">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentRoman" parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
<Anchors><Anchor point="CENTER"/></Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString name="$parentLabel" parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- ───────── Корневой фрейм ───────── -->
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="EncounterJournalInset" relativePoint="TOPLEFT">
<Offset><AbsDimension x="5" y="-4"/></Offset>
</Anchor>
<Anchor point="BOTTOMRIGHT" relativeTo="EncounterJournalInset" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-3" y="0"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BackgroundPage"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Background-Page"
setAllPoints="true">
<TexCoords left="0" right="1" top="0" bottom="1"/>
</Texture>
</Layer>
<Layer level="BORDER">
<Texture parentKey="TabDivider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="28" y="-52"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-28" y="-52"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- 1) Цепочка этапов — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideStageChain" parentKey="StageChain">
<Size><AbsDimension x="0" y="62"/></Size>
</Frame>
<!-- 2) Заголовок этапа — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideHeader" parentKey="Header">
<Size><AbsDimension x="0" y="82"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideHeaderPretitle" parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderTitle" parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-18"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderDesc" parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-48"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderProgLabel" parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderProgPct" parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
<Size><AbsDimension x="240" y="28"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-14"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar name="PlayerGuideHeaderProgBar" parentKey="ProgressBar">
<Size><AbsDimension x="240" y="10"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-52"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
<Button name="PlayerGuideHeaderRefresh" parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="100" y="22"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-72"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
self:SetNormalTexture(T.."Button-Up")
self:SetPushedTexture(T.."Button-Down")
self:SetHighlightTexture(T.."Button-Highlight")
</OnLoad>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 3) Задачи этапа — позиция задаётся Lua в OnLoad -->
<ScrollFrame name="PlayerGuideBodyScroll" parentKey="BodyScroll" inherits="UIPanelScrollFrameTemplate">
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="860" y="1"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
<Frame name="PlayerGuideObjSection" parentKey="ObjectivesSection">
<Layers>
<Layer level="OVERLAY">
<Texture name="PlayerGuideObjOrnament" parentKey="Ornament"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Corner-Ornament">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString name="PlayerGuideObjHeading" parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideObjCount" parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="PlayerGuideObjHeading" relativePoint="RIGHT">
<Offset><AbsDimension x="6" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="PlayerGuideObjDivider" parentKey="Divider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-24"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-24"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame name="PlayerGuideObjScroll" parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-48"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="600" y="200"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 4) Подходящие подземелья — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideRecsSection" parentKey="RecommendationsSection">
<Size><AbsDimension x="0" y="150"/></Size>
<Layers>
<Layer level="OVERLAY">
<Texture name="PlayerGuideRecsOrnament" parentKey="Ornament"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Corner-Ornament">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString name="PlayerGuideRecsHeading" parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideRecsCount" parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="PlayerGuideRecsHeading" relativePoint="RIGHT">
<Offset><AbsDimension x="6" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="PlayerGuideRecsDivider" parentKey="Divider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-24"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-24"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame name="PlayerGuideRecScroll" parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-46"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-36" y="0"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="900" y="96"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 5) Пустое состояние -->
<Frame name="PlayerGuideEmptyState" parentKey="EmptyState" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="200"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideEmptyTitle" parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="28"/></Size>
</FontString>
<FontString name="PlayerGuideEmptyBody" parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideEmptyTitle" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="420" y="60"/></Size>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="PlayerGuideEmptyRefresh" parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="120" y="24"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideEmptyBody" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-14"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
self:SetNormalTexture(T.."Button-Up")
self:SetPushedTexture(T.."Button-Down")
self:SetHighlightTexture(T.."Button-Highlight")
</OnLoad>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 6) Загрузка -->
<Frame name="PlayerGuideLoadingState" parentKey="LoadingState" hidden="true">
<Anchors><Anchor point="CENTER"/></Anchors>
<Size><AbsDimension x="300" y="80"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideLoadingTitle" parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
<Anchors><Anchor point="TOP"/></Anchors>
<Size><AbsDimension x="300" y="20"/></Size>
</FontString>
<FontString name="PlayerGuideLoadingBody" parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideLoadingTitle" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="300" y="16"/></Size>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
</Scripts>
</Frame>
</Ui>
@@ -1,920 +0,0 @@
-- Compatibility shims for the Sirus Encounter Journal backport on 3.3.5a.
if not tIndexOf then
function tIndexOf(tbl, value)
if not tbl then
return nil
end
for index, entry in ipairs(tbl) do
if entry == value then
return index
end
end
return nil
end
end
if not CopyTable then
function CopyTable(tbl)
if type(tbl) ~= "table" then
return tbl
end
local copy = {}
for key, value in pairs(tbl) do
copy[key] = CopyTable(value)
end
return copy
end
end
if not table.wipe then
function table.wipe(tbl)
for key in pairs(tbl) do
tbl[key] = nil
end
return tbl
end
end
if not tCompare then
function tCompare(lhsTable, rhsTable, depth)
depth = depth or 1
for key, value in pairs(lhsTable) do
if type(value) == "table" then
local rhsValue = rhsTable[key]
if type(rhsValue) ~= "table" then
return false
end
if depth > 1 then
if not tCompare(value, rhsValue, depth - 1) then
return false
end
end
elseif value ~= rhsTable[key] then
return false
end
end
for key in pairs(rhsTable) do
if lhsTable[key] == nil then
return false
end
end
return true
end
end
tsort = tsort or table.sort
bit = bit or {}
if not bit.band then
function bit.band(a, b)
local result = 0
local bitValue = 1
a = a or 0
b = b or 0
while a > 0 or b > 0 do
local aBit = a % 2
local bBit = b % 2
if aBit == 1 and bBit == 1 then
result = result + bitValue
end
a = math.floor(a / 2)
b = math.floor(b / 2)
bitValue = bitValue * 2
end
return result
end
end
bitband = bitband or bit.band
if not string.trim then
function string.trim(value)
return value and value:match("^%s*(.-)%s*$") or value
end
end
utf8 = utf8 or {}
utf8.find = utf8.find or string.find
utf8.upper = utf8.upper or string.upper
LE_EXPANSION_CLASSIC = LE_EXPANSION_CLASSIC or 0
LE_EXPANSION_BURNING_CRUSADE = LE_EXPANSION_BURNING_CRUSADE or 1
LE_EXPANSION_WRATH_OF_THE_LICH_KING = LE_EXPANSION_WRATH_OF_THE_LICH_KING or 2
LE_EXPANSION_CATACLYSM = LE_EXPANSION_CATACLYSM or 3
LE_EXPANSION_MISTS_OF_PANDARIA = LE_EXPANSION_MISTS_OF_PANDARIA or 4
LE_EXPANSION_WARLORDS_OF_DRAENOR = LE_EXPANSION_WARLORDS_OF_DRAENOR or 5
LE_EXPANSION_LEGION = LE_EXPANSION_LEGION or 6
LE_EXPANSION_BATTLE_FOR_AZEROTH = LE_EXPANSION_BATTLE_FOR_AZEROTH or 7
LE_EXPANSION_SHADOWLANDS = LE_EXPANSION_SHADOWLANDS or 8
LE_EXPANSION_DRAGONFLIGHT = LE_EXPANSION_DRAGONFLIGHT or 9
LE_EXPANSION_WAR_WITHIN = LE_EXPANSION_WAR_WITHIN or 10
LE_EXPANSION_MIDNIGHT = LE_EXPANSION_MIDNIGHT or 11
LE_EXPANSION_LEVEL_CURRENT = LE_EXPANSION_LEVEL_CURRENT or LE_EXPANSION_WRATH_OF_THE_LICH_KING
MAX_PLAYER_LEVEL_TABLE = MAX_PLAYER_LEVEL_TABLE or {}
MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_CLASSIC] = MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_CLASSIC] or 60
MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_BURNING_CRUSADE] = MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_BURNING_CRUSADE] or 70
MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_WRATH_OF_THE_LICH_KING] = MAX_PLAYER_LEVEL_TABLE[LE_EXPANSION_WRATH_OF_THE_LICH_KING] or 80
PLAYER_FACTION_GROUP = PLAYER_FACTION_GROUP or {}
PLAYER_FACTION_GROUP.Alliance = PLAYER_FACTION_GROUP.Alliance or 1
PLAYER_FACTION_GROUP.Horde = PLAYER_FACTION_GROUP.Horde or 2
PLAYER_FACTION_GROUP.Neutral = PLAYER_FACTION_GROUP.Neutral or 3
PLAYER_FACTION_GROUP.Renegade = PLAYER_FACTION_GROUP.Renegade or 4
QUESTION_MARK_ICON = QUESTION_MARK_ICON or "Interface\\Icons\\INV_Misc_QuestionMark"
CLASS_ID_WARRIOR = CLASS_ID_WARRIOR or 1
CLASS_ID_PALADIN = CLASS_ID_PALADIN or 2
CLASS_ID_HUNTER = CLASS_ID_HUNTER or 3
CLASS_ID_ROGUE = CLASS_ID_ROGUE or 4
CLASS_ID_PRIEST = CLASS_ID_PRIEST or 5
CLASS_ID_DEATHKNIGHT = CLASS_ID_DEATHKNIGHT or 6
CLASS_ID_SHAMAN = CLASS_ID_SHAMAN or 7
CLASS_ID_MAGE = CLASS_ID_MAGE or 8
CLASS_ID_WARLOCK = CLASS_ID_WARLOCK or 9
CLASS_ID_DRUID = CLASS_ID_DRUID or 11
if not SetParentFrameLevel then
function SetParentFrameLevel(frame, offset)
frame:SetFrameLevel(frame:GetParent():GetFrameLevel() + (offset or 0))
end
end
if not IsOnGlueScreen then
function IsOnGlueScreen() return false end
end
if not RegisterCustomEvent then
function RegisterCustomEvent() end
end
if not UnregisterCustomEvent then
function UnregisterCustomEvent() end
end
C_Timer = C_Timer or {}
if not C_Timer.NewTimer then
local timerFrame
local timers = {}
local function EnsureTimerFrame()
if timerFrame or not CreateFrame then
return
end
timerFrame = CreateFrame("Frame")
timerFrame:SetScript("OnUpdate", function(_, elapsed)
for timer in pairs(timers) do
timer.remaining = timer.remaining - elapsed
if timer.remaining <= 0 then
timers[timer] = nil
if not timer.cancelled then
timer.callback()
end
end
end
end)
end
function C_Timer.NewTimer(seconds, callback)
local timer = {
remaining = seconds or 0,
callback = callback or function() end,
}
function timer:Cancel()
self.cancelled = true
timers[self] = nil
end
EnsureTimerFrame()
timers[timer] = true
return timer
end
end
C_Timer.After = C_Timer.After or function(seconds, callback)
return C_Timer.NewTimer(seconds, callback)
end
if not IsInterfaceDevClient then
function IsInterfaceDevClient()
return false
end
end
if not IsGMAccount then
function IsGMAccount()
return false
end
end
if not FireCustomClientEvent then
function FireCustomClientEvent(event, ...)
if EventRegistry and EventRegistry.TriggerEvent then
EventRegistry:TriggerEvent(event, ...)
end
end
end
EventRegistry = EventRegistry or {}
EventRegistry.registry = EventRegistry.registry or {}
EventRegistry.TriggerEvent = EventRegistry.TriggerEvent or function(self, event, ...)
local callbacks = self.registry[event]
if callbacks then
for _, callback in ipairs(callbacks) do
callback(...)
end
end
end
EventRegistry.RegisterCallback = EventRegistry.RegisterCallback or function(self, event, callback)
self.registry[event] = self.registry[event] or {}
table.insert(self.registry[event], callback)
end
C_Service = C_Service or {}
C_Service.GetRealmID = function() return 0 end
C_Service.GetRealmStage = C_Service.GetRealmStage or function() return 0 end
C_Service.GetCustomValue = C_Service.GetCustomValue or function() return 0 end
C_Service.IsInGMMode = C_Service.IsInGMMode or IsGMAccount
C_Service.IsGMAccount = C_Service.IsGMAccount or IsGMAccount
C_Service.IsRenegadeRealm = C_Service.IsRenegadeRealm or function() return false end
C_Service.IsHardcoreEnabledOnRealm = C_Service.IsHardcoreEnabledOnRealm or function() return false end
C_MythicPlus = C_MythicPlus or {}
C_MythicPlus.IsMythicPlusActive = C_MythicPlus.IsMythicPlusActive or function() return false end
C_PvP = C_PvP or {}
C_LFGList = C_LFGList or {}
C_LFGList.IsPremadeGroupFinderEnabled = C_LFGList.IsPremadeGroupFinderEnabled or function() return false end
if not MicroButtonPulseStop then
function MicroButtonPulseStop() end
end
if not EJSuggestFrame_OpenFrame then
function EJSuggestFrame_OpenFrame() end
end
if not EJSuggestTab_GetPlayerTierIndex then
function EJSuggestTab_GetPlayerTierIndex()
if EJ_GetCurrentTier then
return EJ_GetCurrentTier()
end
return 1
end
end
if not ChatEdit_TryInsertChatLink then
function ChatEdit_TryInsertChatLink()
return false
end
end
if not SharedTooltip_SetBackdropStyle then
function SharedTooltip_SetBackdropStyle(tooltip)
if not tooltip then
return
end
if tooltip.SetBackdropBorderColor and TOOLTIP_DEFAULT_COLOR then
tooltip:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
end
if tooltip.SetBackdropColor and TOOLTIP_DEFAULT_BACKGROUND_COLOR then
tooltip:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
end
end
end
if not HelpPlate_Hide then
function HelpPlate_Hide() end
end
if not HelpPlate_Show then
function HelpPlate_Show() end
end
-- 3.3.5 has no GetNumClasses/GetClassInfo (added in Cataclysm). Sirus EJ uses
-- them to populate the loot class filter dropdown.
local MOONWELL_CLASS_INFO = {
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.WARRIOR or "Warrior", "WARRIOR", CLASS_ID_WARRIOR},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.PALADIN or "Paladin", "PALADIN", CLASS_ID_PALADIN},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.HUNTER or "Hunter", "HUNTER", CLASS_ID_HUNTER},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.ROGUE or "Rogue", "ROGUE", CLASS_ID_ROGUE},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.PRIEST or "Priest", "PRIEST", CLASS_ID_PRIEST},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.DEATHKNIGHT or "Death Knight", "DEATHKNIGHT", CLASS_ID_DEATHKNIGHT},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.SHAMAN or "Shaman", "SHAMAN", CLASS_ID_SHAMAN},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.MAGE or "Mage", "MAGE", CLASS_ID_MAGE},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.WARLOCK or "Warlock", "WARLOCK", CLASS_ID_WARLOCK},
{LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE.DRUID or "Druid", "DRUID", CLASS_ID_DRUID},
}
if not GetNumClasses then
function GetNumClasses()
return #MOONWELL_CLASS_INFO
end
end
if not GetClassInfo then
function GetClassInfo(index)
local info = MOONWELL_CLASS_INFO[index]
if info then
return info[1], info[2], info[3]
end
end
end
CLASS_ID_DEMONHUNTER = CLASS_ID_DEMONHUNTER or 12
local function EnsureColorMethods(color)
if type(color) ~= "table" then
return color
end
color.GetRGB = color.GetRGB or function(self)
return self.r or 1, self.g or 1, self.b or 1
end
color.GetRGBA = color.GetRGBA or function(self)
return self.r or 1, self.g or 1, self.b or 1, self.a or 1
end
color.WrapTextInColorCode = color.WrapTextInColorCode or function(self, text)
if self.colorStr then
return "|c" .. self.colorStr .. text .. "|r"
end
return text
end
return color
end
EnsureColorMethods(NORMAL_FONT_COLOR)
EnsureColorMethods(HIGHLIGHT_FONT_COLOR)
EnsureColorMethods(RED_FONT_COLOR)
EnsureColorMethods(GREEN_FONT_COLOR)
EnsureColorMethods(DISABLED_FONT_COLOR)
LOOTJOURNAL_SOURCE_TOOLTIP_HEAD = LOOTJOURNAL_SOURCE_TOOLTIP_HEAD or "Способ получения:"
RETURN_TO_DEFAULT = RETURN_TO_DEFAULT or "Настройки по умолчанию"
GUIDEBOOK = GUIDEBOOK or "Путеводитель"
INSTANCES = INSTANCES or "Подземелья"
RAIDS = RAIDS or "Рейды"
LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты"
-- Format string used by EncounterJournal_SetLootButton to label the loot
-- source ("Босс: <name>"). Sirus ships it via locale globals; default to a
-- compact form on 3.3.5.
BOSS_INFO_STRING = BOSS_INFO_STRING or "Босс: %s"
-- Sirus ships a PKBT model mixin in SharedXML/SharedUIPanelPKBTTemplates;
-- we don't carry it. The EJ DressUpModel OnShow does `self:OnShow()` and
-- OnEvent does `self:OnEvent(...)`, expecting the mixin to provide them.
PKBT_ModelMixin = PKBT_ModelMixin or {}
PKBT_ModelMixin.OnShow = PKBT_ModelMixin.OnShow or function() end
PKBT_ModelMixin.OnEvent = PKBT_ModelMixin.OnEvent or function() end
PKBT_ModelMixin.OnLoad = PKBT_ModelMixin.OnLoad or function() end
if not SharedXML_Model_OnLoad then
function SharedXML_Model_OnLoad() end
end
if not SharedXML_Model_OnEvent then
function SharedXML_Model_OnEvent() end
end
ITEM_CLASS_2 = ITEM_CLASS_2 or 2
ITEM_CLASS_4 = ITEM_CLASS_4 or 4
for _, classID in ipairs({0, 2, 4, 5, 7, 9, 11, 12, 13, 14, 15}) do
for subclassID = 0, 20 do
local key = string.format("ITEM_SUB_CLASS_%d_%d", classID, subclassID)
if _G[key] == nil then
_G[key] = subclassID
end
end
end
LE_ITEM_FILTER_TYPE_HEAD = LE_ITEM_FILTER_TYPE_HEAD or 1
LE_ITEM_FILTER_TYPE_NECK = LE_ITEM_FILTER_TYPE_NECK or 2
LE_ITEM_FILTER_TYPE_SHOULDER = LE_ITEM_FILTER_TYPE_SHOULDER or 3
LE_ITEM_FILTER_TYPE_CLOAK = LE_ITEM_FILTER_TYPE_CLOAK or 16
LE_ITEM_FILTER_TYPE_CHEST = LE_ITEM_FILTER_TYPE_CHEST or 5
LE_ITEM_FILTER_TYPE_WRIST = LE_ITEM_FILTER_TYPE_WRIST or 9
LE_ITEM_FILTER_TYPE_HAND = LE_ITEM_FILTER_TYPE_HAND or 10
LE_ITEM_FILTER_TYPE_WAIST = LE_ITEM_FILTER_TYPE_WAIST or 6
LE_ITEM_FILTER_TYPE_LEGS = LE_ITEM_FILTER_TYPE_LEGS or 7
LE_ITEM_FILTER_TYPE_FEET = LE_ITEM_FILTER_TYPE_FEET or 8
LE_ITEM_FILTER_TYPE_MAIN_HAND = LE_ITEM_FILTER_TYPE_MAIN_HAND or 21
LE_ITEM_FILTER_TYPE_OFF_HAND = LE_ITEM_FILTER_TYPE_OFF_HAND or 22
LE_ITEM_FILTER_TYPE_2HWEAPON = LE_ITEM_FILTER_TYPE_2HWEAPON or 17
LE_ITEM_FILTER_TYPE_HWEAPON = LE_ITEM_FILTER_TYPE_HWEAPON or 13
LE_ITEM_FILTER_TYPE_RANGED = LE_ITEM_FILTER_TYPE_RANGED or 15
LE_ITEM_FILTER_TYPE_FINGER = LE_ITEM_FILTER_TYPE_FINGER or 11
LE_ITEM_FILTER_TYPE_TRINKET = LE_ITEM_FILTER_TYPE_TRINKET or 12
LE_ITEM_QUALITY_COMMON = LE_ITEM_QUALITY_COMMON or 1
BAG_ITEM_QUALITY_COLORS = BAG_ITEM_QUALITY_COLORS or {}
local itemQualityColors = ITEM_QUALITY_COLORS or {}
for quality = 0, 7 do
local color = BAG_ITEM_QUALITY_COLORS[quality] or itemQualityColors[quality] or NORMAL_FONT_COLOR
BAG_ITEM_QUALITY_COLORS[quality] = EnsureColorMethods(color or {})
end
if not SetItemButtonQuality then
function SetItemButtonQuality(button, quality)
if not button or not button.IconBorder then
return
end
local color = quality and BAG_ITEM_QUALITY_COLORS[quality]
if color then
button.IconBorder:Show()
button.IconBorder:SetVertexColor(color.r or 1, color.g or 1, color.b or 1)
else
button.IconBorder:Hide()
end
end
end
Enum = Enum or {}
Enum.ItemQuality = Enum.ItemQuality or {
Poor = 0,
Common = 1,
Uncommon = 2,
Rare = 3,
Epic = 4,
Legendary = 5,
Artifact = 6,
Heirloom = 7,
}
Enum.ModelType = Enum.ModelType or {
M2 = 0,
Unit = 1,
Creature = 2,
Item = 3,
ItemSet = 4,
Illusion = 5,
ItemTransmog = 6,
Customization = 7,
}
Enum.HardcoreDeathReason = Enum.HardcoreDeathReason or {
FAILED_DEATH = 1,
RESTORE = 2,
}
local ITEM_CACHE_FIELD = {
NAME_ENGB = 1,
NAME_RURU = 2,
RARITY = 3,
ILEVEL = 4,
MINLEVEL = 5,
TYPE = 6,
SUBTYPE = 7,
STACKCOUNT = 8,
EQUIPLOC = 9,
TEXTURE = 10,
VENDORPRICE = 11,
VENDORPRIC = 11,
}
local ITEM_ID_FIELD = 0
local ITEM_LINK_FIELD = -1
Enum.ItemCacheField = Enum.ItemCacheField or {}
for key, value in pairs(ITEM_CACHE_FIELD) do
Enum.ItemCacheField[key] = value
end
Enum.ItemCacheField.ITEM_ID = ITEM_ID_FIELD
SHARED_INVTYPE_BY_ID = SHARED_INVTYPE_BY_ID or {
[0] = "",
[1] = "INVTYPE_HEAD",
[2] = "INVTYPE_NECK",
[3] = "INVTYPE_SHOULDER",
[4] = "INVTYPE_BODY",
[5] = "INVTYPE_CHEST",
[6] = "INVTYPE_WAIST",
[7] = "INVTYPE_LEGS",
[8] = "INVTYPE_FEET",
[9] = "INVTYPE_WRIST",
[10] = "INVTYPE_HAND",
[11] = "INVTYPE_FINGER",
[12] = "INVTYPE_TRINKET",
[13] = "INVTYPE_WEAPON",
[14] = "INVTYPE_SHIELD",
[15] = "INVTYPE_RANGED",
[16] = "INVTYPE_CLOAK",
[17] = "INVTYPE_2HWEAPON",
[18] = "INVTYPE_BAG",
[19] = "INVTYPE_TABARD",
[20] = "INVTYPE_ROBE",
[21] = "INVTYPE_WEAPONMAINHAND",
[22] = "INVTYPE_WEAPONOFFHAND",
[23] = "INVTYPE_HOLDABLE",
[24] = "INVTYPE_AMMO",
[25] = "INVTYPE_THROWN",
[26] = "INVTYPE_RANGEDRIGHT",
[27] = "INVTYPE_QUIVER",
[28] = "INVTYPE_RELIC",
}
C_FactionManager = C_FactionManager or {}
C_FactionManager.GetFactionInfoOriginal = C_FactionManager.GetFactionInfoOriginal or function()
local faction = UnitFactionGroup and UnitFactionGroup("player")
return PLAYER_FACTION_GROUP[faction] or PLAYER_FACTION_GROUP.Alliance
end
C_FactionManager.RegisterCallback = C_FactionManager.RegisterCallback or function(callback)
if callback then
callback()
end
end
C_Item = C_Item or {}
local ExistingCItemGetItemInfo = C_Item.GetItemInfo
local ExistingCItemRequestServerCache = C_Item.RequestServerCache
local ExistingCItemGetItemIDFromString = C_Item.GetItemIDFromString
local ITEM_CACHE_CALLBACKS = {}
local ITEM_QUALITY_HEX = {
[0] = "ff9d9d9d",
[1] = "ffffffff",
[2] = "ff1eff00",
[3] = "ff0070dd",
[4] = "ffa335ee",
[5] = "ffff8000",
[6] = "ffe6cc80",
[7] = "ffe6cc80",
}
local ITEMS_CACHE_INITIALIZED
local function GetItemIDFromAny(item)
if type(item) == "number" then
return item
end
if type(item) == "string" then
local itemsCache = _G.ItemsCache
if type(itemsCache) == "table" and type(itemsCache[item]) == "table" then
return itemsCache[item][ITEM_ID_FIELD]
end
return tonumber(item:match("item:(%d+)") or item:match("^(%d+)$"))
end
end
local function CreateItemCacheLink(itemName, itemID, itemRarity, itemMinLevel)
return string.format("|c%s|Hitem:%d:0:0:0:0:0:0:0:%d|h[%s]|h|r",
ITEM_QUALITY_HEX[itemRarity] or "ffffffff",
itemID or 0,
itemMinLevel or 0,
itemName or UNKNOWN or ""
)
end
local function EnsureItemsCache()
if ITEMS_CACHE_INITIALIZED then
return _G.ItemsCache
end
local itemsCache = type(_G.ItemsCache) == "table" and _G.ItemsCache or nil
local fileIndex = 1
local itemCacheTable = _G["ItemsCache" .. fileIndex]
while type(itemCacheTable) == "table" do
if not itemsCache then
itemsCache = itemCacheTable
_G.ItemsCache = itemsCache
elseif itemCacheTable ~= itemsCache then
for itemID, itemData in pairs(itemCacheTable) do
itemsCache[itemID] = itemData
end
table.wipe(itemCacheTable)
end
_G["ItemsCache" .. fileIndex] = nil
fileIndex = fileIndex + 1
itemCacheTable = _G["ItemsCache" .. fileIndex]
end
if itemsCache then
local localeIndex = GetLocale and GetLocale() == "ruRU" and ITEM_CACHE_FIELD.NAME_RURU or ITEM_CACHE_FIELD.NAME_ENGB
local namedItems = {}
for itemID, itemData in pairs(itemsCache) do
if type(itemID) == "number" and type(itemData) == "table" then
itemData[ITEM_ID_FIELD] = itemID
itemData[ITEM_LINK_FIELD] = nil
local itemName = itemData[localeIndex]
if itemName and itemName ~= "" then
namedItems[itemName] = itemData
end
end
end
setmetatable(itemsCache, {__index = namedItems})
end
ITEMS_CACHE_INITIALIZED = true
return itemsCache
end
function C_Item.GetItemInfoCache(item)
local itemsCache = EnsureItemsCache()
if not itemsCache then
return
end
local itemID = GetItemIDFromAny(item)
if not itemID then
return
end
local cacheData = itemsCache[itemID]
if type(cacheData) ~= "table" then
return
end
local localeIndex = GetLocale and GetLocale() == "ruRU" and ITEM_CACHE_FIELD.NAME_RURU or ITEM_CACHE_FIELD.NAME_ENGB
local itemName = cacheData[localeIndex] or cacheData[ITEM_CACHE_FIELD.NAME_ENGB]
if not itemName or itemName == "" then
return
end
local itemRarity = cacheData[ITEM_CACHE_FIELD.RARITY] or LE_ITEM_QUALITY_COMMON
local itemMinLevel = cacheData[ITEM_CACHE_FIELD.MINLEVEL] or 0
local classID = cacheData[ITEM_CACHE_FIELD.TYPE]
local subclassID = cacheData[ITEM_CACHE_FIELD.SUBTYPE]
local equipLocID = cacheData[ITEM_CACHE_FIELD.EQUIPLOC] or 0
if not cacheData[ITEM_LINK_FIELD] then
cacheData[ITEM_LINK_FIELD] = CreateItemCacheLink(itemName, itemID, itemRarity, itemMinLevel)
end
return itemName,
cacheData[ITEM_LINK_FIELD],
itemRarity,
cacheData[ITEM_CACHE_FIELD.ILEVEL],
itemMinLevel,
_G["ITEM_CLASS_" .. tostring(classID)] or classID,
_G["ITEM_SUB_CLASS_" .. tostring(classID) .. "_" .. tostring(subclassID)] or subclassID,
cacheData[ITEM_CACHE_FIELD.STACKCOUNT],
SHARED_INVTYPE_BY_ID[equipLocID],
"Interface\\Icons\\" .. tostring(cacheData[ITEM_CACHE_FIELD.TEXTURE] or "INV_Misc_QuestionMark"),
cacheData[ITEM_CACHE_FIELD.VENDORPRICE],
itemID,
classID,
subclassID,
equipLocID
end
local function GetItemInfoCompat(item)
local name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice = GetItemInfo(item)
if name then
return name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice
end
return C_Item.GetItemInfoCache(item)
end
local function FireItemCacheCallbacks(itemID)
local callbacks = ITEM_CACHE_CALLBACKS[itemID]
if not callbacks then
return
end
local name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice = GetItemInfoCompat(itemID)
if not name then
return
end
ITEM_CACHE_CALLBACKS[itemID] = nil
for _, callback in ipairs(callbacks) do
callback(itemID, name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice)
end
end
local function QueueItemCacheCallback(item, callback)
if not callback then
return
end
local itemID = tonumber(item)
if not itemID and type(item) == "string" then
itemID = tonumber(item:match("item:(%d+)") or item:match("^(%d+)$"))
end
if not itemID then
return
end
ITEM_CACHE_CALLBACKS[itemID] = ITEM_CACHE_CALLBACKS[itemID] or {}
table.insert(ITEM_CACHE_CALLBACKS[itemID], callback)
end
local itemCacheFrame
local function EnsureItemCacheFrame()
if itemCacheFrame or not CreateFrame then
return
end
itemCacheFrame = CreateFrame("Frame")
itemCacheFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
itemCacheFrame:SetScript("OnEvent", function(_, _, itemID)
if itemID then
FireItemCacheCallbacks(itemID)
else
for queuedItemID in pairs(ITEM_CACHE_CALLBACKS) do
FireItemCacheCallbacks(queuedItemID)
end
end
end)
end
C_Item.GetItemInfo = function(item, skipClientCache, callback, ...)
local name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice
if ExistingCItemGetItemInfo then
name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice = ExistingCItemGetItemInfo(item, skipClientCache, nil, ...)
end
if not name then
name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice = GetItemInfoCompat(item)
end
if callback then
if name then
callback(GetItemIDFromAny(item) or item, name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice)
else
QueueItemCacheCallback(item, callback)
EnsureItemCacheFrame()
end
end
return name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice
end
C_Item.RequestServerCache = function(item, callback)
local name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice = GetItemInfoCompat(item)
if callback then
if name then
callback(GetItemIDFromAny(item) or item, name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice)
elseif ExistingCItemRequestServerCache then
return ExistingCItemRequestServerCache(item, callback)
else
QueueItemCacheCallback(item, callback)
EnsureItemCacheFrame()
end
end
return name, link, quality, itemLevel, reqLevel, itemType, itemSubType, maxStack, equipSlot, icon, vendorPrice
end
C_Item.GetItemIDFromString = function(item)
local itemID = ExistingCItemGetItemIDFromString and ExistingCItemGetItemIDFromString(item)
if itemID then
return itemID
end
EnsureItemsCache()
return GetItemIDFromAny(item)
end
EnsureItemsCache()
if not Mixin then
function Mixin(object, ...)
for i = 1, select("#", ...) do
local mixin = select(i, ...)
for key, value in pairs(mixin) do
object[key] = value
end
end
return object
end
end
if not CreateFromMixins then
function CreateFromMixins(...)
return Mixin({}, ...)
end
end
if not CreateAndInitFromMixin then
function CreateAndInitFromMixin(mixin, ...)
local object = CreateFromMixins(mixin)
object:Init(...)
return object
end
end
if not InlineHyperlinkFrame_SimpleHTMLAsFontString_OnLoad then
function InlineHyperlinkFrame_SimpleHTMLAsFontString_OnLoad(self)
if PKBT_SimpleHTMLAsFontStringMixin then
Mixin(self, PKBT_SimpleHTMLAsFontStringMixin)
if self.OnLoadSimpleHTMLAsFontString then
self:OnLoadSimpleHTMLAsFontString()
end
elseif self.GetRegions and not self:GetRegions() then
local meta = getmetatable(self)
local methods = meta and meta.__index
if methods and methods.SetText then
methods.SetText(self, "")
end
end
end
end
local function InstallTextureAtlasShim()
if not CreateFrame then
return
end
local texture = CreateFrame("Frame"):CreateTexture(nil, "ARTWORK")
local meta = getmetatable(texture)
local methods = meta and meta.__index
local atlasToTexture = {
["adventureguide-pane-large"] = {335, 337, 0.001953, 0.656250, 0.320312, 0.978516, "Interface\\EncounterJournal\\AdventureGuide"},
["adventureguide-pane-small"] = {344, 161, 0.001953, 0.673828, 0.001953, 0.316406, "Interface\\EncounterJournal\\AdventureGuide"},
["adventureguide-ring"] = {94, 95, 0.775391, 0.958984, 0.001953, 0.187500, "Interface\\EncounterJournal\\AdventureGuide"},
["adventureguide-rewardring"] = {48, 48, 0.677734, 0.771484, 0.001953, 0.095703, "Interface\\EncounterJournal\\AdventureGuide"},
["adventureguide-redx"] = {35, 34, 0.677734, 0.746094, 0.175781, 0.242188, "Interface\\EncounterJournal\\AdventureGuide"},
["UI-EJ-Classic"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds1"},
["UI-EJ-BurningCrusade"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds1"},
["UI-EJ-WrathoftheLichKing"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds2"},
["UI-EJ-Cataclysm"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds2"},
["UI-EJ-MistsofPandaria"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds3"},
["UI-EJ-WarlordsofDraenor"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds3"},
["UI-EJ-BattleforAzeroth"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds4"},
["UI-EJ-Legion"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds4"},
["UI-EJ-Dragonflight"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds5"},
["UI-EJ-Shadowlands"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds5"},
["UI-EJ-TheWarWithin"] = {786, 425, 0.000977, 0.768555, 0.000977, 0.416016, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds6"},
["UI-EJ-Midnight"] = {786, 425, 0.000977, 0.768555, 0.417969, 0.833008, "Interface\\EncounterJournal\\DungeonJournalTierBackgrounds6"},
}
if type(methods) == "table" and not methods.SetAtlas then
methods.SetAtlas = function(self, atlas, useAtlasSize)
local atlasInfo = atlasToTexture[atlas]
if atlasInfo then
self:SetTexture(atlasInfo[7])
self:SetTexCoord(atlasInfo[3], atlasInfo[4], atlasInfo[5], atlasInfo[6])
if useAtlasSize then
self:SetSize(atlasInfo[1], atlasInfo[2])
end
elseif SetAtlasTexture then
local ok = pcall(SetAtlasTexture, self, atlas)
if not ok then
self:SetTexture(nil)
end
else
self:SetTexture(nil)
end
end
end
end
InstallTextureAtlasShim()
local function InstallFrameMethodShims()
if not CreateFrame then
return
end
local frame = CreateFrame("Frame")
local meta = getmetatable(frame)
local methods = meta and meta.__index
if type(methods) ~= "table" or methods.SetParentArray then
return
end
methods.SetParentArray = function(self, arrayName)
local parent = self:GetParent()
if not parent or not arrayName then
return
end
parent[arrayName] = parent[arrayName] or {}
table.insert(parent[arrayName], self)
end
end
InstallFrameMethodShims()
if not EncounterJournal_SetSearchPreviewSelection then
function EncounterJournal_SetSearchPreviewSelection(selectedIndex)
if not EncounterJournal or not EncounterJournal.searchBox then
return
end
local searchBox = EncounterJournal.searchBox
searchBox.selectedIndex = selectedIndex or 1
if searchBox.searchPreview then
for index, button in ipairs(searchBox.searchPreview) do
if button.selectedTexture then
if index == searchBox.selectedIndex then
button.selectedTexture:Show()
else
button.selectedTexture:Hide()
end
end
end
end
if searchBox.showAllResults and searchBox.showAllResults.selectedTexture then
searchBox.showAllResults.selectedTexture:Hide()
end
end
end
@@ -1,79 +0,0 @@
-- Optional AIO bridge: lets the server push fresh Encounter Journal data at runtime.
-- The static fallback is loaded from Generated_EncounterJournal.lua, so the journal works
-- without ever calling ApplyEncounterJournalData.
local function ApplyEncounterJournalData(data)
if type(data) ~= "table" then
return
end
local function NormalizeTierInstances(tiers, tierInstances)
if type(tiers) ~= "table" or type(tierInstances) ~= "table" then
return tierInstances
end
local tierIDs = {}
for _, tierInfo in ipairs(tiers) do
if type(tierInfo) == "table" then
tierIDs[tierInfo[1]] = true
end
end
local hasTierKey = false
for tierID in pairs(tierIDs) do
if tierInstances[tierID] then
hasTierKey = true
break
end
end
if not hasTierKey then
return tierInstances
end
local normalized = {}
for tierID, instanceList in pairs(tierInstances) do
if tierIDs[tierID] and type(instanceList) == "table" then
for _, instanceID in ipairs(instanceList) do
normalized[instanceID] = normalized[instanceID] or {}
table.insert(normalized[instanceID], tierID)
end
else
normalized[tierID] = instanceList
end
end
return normalized
end
local tiers = data.JOURNALTIER or data.tiers or JOURNALTIER
local tierInstances = data.JOURNALTIERXINSTANCE or data.tierInstances or JOURNALTIERXINSTANCE
JOURNALINSTANCE = data.JOURNALINSTANCE or data.instances or JOURNALINSTANCE
JOURNALENCOUNTER = data.JOURNALENCOUNTER or data.encounters or JOURNALENCOUNTER
JOURNALENCOUNTERCREATURE = data.JOURNALENCOUNTERCREATURE or data.creatures or JOURNALENCOUNTERCREATURE
JOURNALENCOUNTERITEM = data.JOURNALENCOUNTERITEM or data.items or JOURNALENCOUNTERITEM
JOURNALENCOUNTERSECTION = data.JOURNALENCOUNTERSECTION or data.sections or JOURNALENCOUNTERSECTION
JOURNALTIER = tiers
JOURNALTIERXINSTANCE = NormalizeTierInstances(tiers, tierInstances)
JOURNAACTUALRAIDS = data.JOURNAACTUALRAIDS or data.actualRaids or JOURNAACTUALRAIDS
if C_EncounterJournal and C_EncounterJournal.ReloadData then
C_EncounterJournal.ReloadData()
end
end
MoonWell_ApplyEncounterJournalData = ApplyEncounterJournalData
local staticData = MoonWellEncounterJournalData or EncounterJournalData
if staticData then
ApplyEncounterJournalData(staticData)
end
if AIO then
local Handler = AIO.AddHandlers("MoonWellEncounterJournal", {})
function Handler.SetData(_, data)
ApplyEncounterJournalData(data)
end
end
@@ -1,157 +0,0 @@
-- Hide the Sirus Encounter Journal panels we don't ship: suggested content,
-- LootJournal/ItemBrowser, search box, retail-era top tabs. Dungeons + Raids stay.
-- The shipped Sirus tab layout assumes all four content tabs are visible; this
-- skin repositions the two tabs we keep.
local function HideFrame(frame)
if frame then
frame:Hide()
end
end
local function HideTabKeepEnabled(tab)
if not tab then
return
end
tab:Hide()
-- Keep the tab enabled so Sirus EJ_OnShow does not fall back to the
-- suggest-tier code path (EJSuggestTab_GetPlayerTierIndex etc).
end
local function ConfigureTierTab(tab, text)
if not tab then
return
end
tab:SetText(text or "")
if tab.SetNormalFontObject and GameFontNormal then
tab:SetNormalFontObject(GameFontNormal)
end
if tab.SetHighlightFontObject and GameFontHighlight then
tab:SetHighlightFontObject(GameFontHighlight)
end
if tab.SetDisabledFontObject and GameFontNormal then
tab:SetDisabledFontObject(GameFontNormal)
end
local fontString = tab:GetFontString()
if fontString then
fontString:SetText(text or "")
fontString:ClearAllPoints()
fontString:SetPoint("CENTER", tab, "CENTER", 0, -3)
if fontString.SetDrawLayer then
fontString:SetDrawLayer("OVERLAY", 7)
end
end
tab:SetWidth(math.max((tab:GetTextWidth() or 0) + 28, 92))
end
local function PositionVisibleTierTabs(instanceSelect)
local guidebookTab = instanceSelect and instanceSelect.guidebookTab
local dungeonsTab = instanceSelect and instanceSelect.dungeonsTab
local raidsTab = instanceSelect and instanceSelect.raidsTab
if not dungeonsTab or not raidsTab then
return
end
ConfigureTierTab(guidebookTab, GUIDEBOOK or "Guidebook")
ConfigureTierTab(dungeonsTab, INSTANCES or "Instances")
ConfigureTierTab(raidsTab, RAIDS or "Raids")
dungeonsTab:ClearAllPoints()
dungeonsTab:SetPoint("BOTTOMLEFT", instanceSelect, "TOPLEFT", 150, -45)
raidsTab:ClearAllPoints()
raidsTab:SetPoint("BOTTOMLEFT", dungeonsTab, "BOTTOMRIGHT", 22, 0)
end
local function GetVisibleSelectedTabID(instanceSelect)
if not instanceSelect then
return
end
local selectedTab = instanceSelect.selectedTab
local dungeonsTab = instanceSelect.dungeonsTab
local raidsTab = instanceSelect.raidsTab
if raidsTab and selectedTab == raidsTab.id then
return selectedTab
end
if dungeonsTab then
dungeonsTab.id = dungeonsTab.id or dungeonsTab:GetID()
return dungeonsTab.id
end
end
local function SelectVisibleInstanceTab(journal)
local instanceSelect = journal and journal.instanceSelect
if not instanceSelect then
return
end
local selectedTab = GetVisibleSelectedTabID(instanceSelect)
if selectedTab then
instanceSelect.selectedTab = selectedTab
end
if journal:IsShown()
and instanceSelect:IsShown()
and (not journal.encounter or not journal.encounter:IsShown())
and selectedTab
and EJ_ContentTab_Select
then
EJ_ContentTab_Select(selectedTab)
end
end
local function ApplyDungeonAndRaidOnlySkin()
local journal = EncounterJournal
if not journal then
return
end
-- Sirus EJ_OnLoad sets numTabs at the very end; if anything earlier in
-- OnLoad bails out, PanelTemplates_UpdateTabs blows up later with a nil
-- for-limit when OnShow calls PanelTemplates_SetTab. Guard it.
if journal.numTabs == nil then
journal.numTabs = 4
end
HideFrame(journal.searchBox)
HideFrame(journal.searchResults)
HideFrame(journal.suggestFrame)
HideFrame(journal.LootJournal)
HideFrame(journal.LootJournalItems)
HideFrame(journal.TutorialButton)
for index = 1, 4 do
HideFrame(_G["EncounterJournalTab" .. index])
end
local instanceSelect = journal.instanceSelect
if instanceSelect then
HideTabKeepEnabled(instanceSelect.suggestTab)
HideTabKeepEnabled(instanceSelect.LootJournalTab)
-- Sirus EJ_OnShow forwards instanceSelect.selectedTab into
-- EJ_ContentTab_Select; if it's nil the tab loop matches nothing and
-- selectedTab.selectedGlow blows up. Default to the dungeons tab.
local dungeonsTab = instanceSelect.dungeonsTab
if dungeonsTab then
dungeonsTab.id = dungeonsTab.id or dungeonsTab:GetID()
end
PositionVisibleTierTabs(instanceSelect)
SelectVisibleInstanceTab(journal)
end
end
MoonWell_EncounterJournalApplyDungeonMode = ApplyDungeonAndRaidOnlySkin
if EncounterJournal then
ApplyDungeonAndRaidOnlySkin()
EncounterJournal:HookScript("OnShow", ApplyDungeonAndRaidOnlySkin)
end
@@ -1,34 +0,0 @@
<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">
<Font name="QuestFont_Super_Huge" font="Fonts\MORPHEUS.ttf" virtual="true">
<FontHeight val="24"/>
<Color r="1" g=".82" b="0"/>
</Font>
<Font name="QuestFont_Super_Huge2" font="Fonts\MORPHEUS.ttf" virtual="true">
<FontHeight val="26"/>
<Shadow>
<Offset x="2" y="-2"/>
<Color r="0" g="0" b="0"/>
</Shadow>
</Font>
<Font name="GameFontNormalMed1" inherits="GameFontNormal" virtual="true">
<FontHeight val="12"/>
<Shadow>
<Offset x="1" y="-1"/>
<Color r="0" g="0" b="0"/>
</Shadow>
<Color r="1" g=".82" b="0"/>
</Font>
<Font name="QuestFont22" font="Fonts\MORPHEUS.ttf" virtual="true">
<FontHeight val="22"/>
<Color r="1" g=".82" b="0"/>
</Font>
<Font name="QuestFont_Enormous" font="Fonts\MORPHEUS.ttf" virtual="true">
<FontHeight val="30"/>
<Color r="1" g=".82" b="0"/>
</Font>
<Font name="DestinyFontHuge" font="Fonts\MORPHEUS.ttf" virtual="true">
<FontHeight val="32"/>
<Color r="1" g=".82" b="0"/>
</Font>
</Ui>
@@ -1,131 +0,0 @@
-- Minimal Frame metatable extensions used by Sirus EJ. Replaces Sirus
-- SharedExtendedMethods.lua, which clobbers SetUnit/PlayerModel methods with
-- retail-only API (ResetAllSpells, SetSpell, SetUnitRaw, GetDisplayIDs,
-- C_CustomizationsCollection.*) and breaks TabardFrame/DressUpFrame on 3.3.5.
local function getMeta(objType)
local frame = CreateFrame(objType)
frame:Hide()
return getmetatable(frame).__index
end
local function RegisterCustomEventNoop() end
local function UnregisterCustomEventNoop() end
local function SetShown(self, shown)
if shown then
self:Show()
else
self:Hide()
end
end
local function SetParentArray(self, arrayName)
local parent = self:GetParent()
if not parent or not arrayName then
return
end
parent[arrayName] = parent[arrayName] or {}
table.insert(parent[arrayName], self)
end
local function SetEnabled(self, enabled)
if enabled then
self:Enable()
else
self:Disable()
end
end
-- Sirus EJ calls Frame/Button:IsMouseOverEx() (Cataclysm+ retail-only).
-- Map to the 3.3.5 IsMouseOver().
local function IsMouseOverEx(self)
return self:IsMouseOver()
end
local metas = {
getMeta("Frame"),
getMeta("Button"),
getMeta("CheckButton"),
getMeta("EditBox"),
getMeta("Slider"),
getMeta("StatusBar"),
getMeta("ScrollFrame"),
getMeta("MessageFrame"),
getMeta("ScrollingMessageFrame"),
}
for _, meta in ipairs(metas) do
if type(meta) == "table" then
if not meta.RegisterCustomEvent then
meta.RegisterCustomEvent = RegisterCustomEventNoop
end
if not meta.UnregisterCustomEvent then
meta.UnregisterCustomEvent = UnregisterCustomEventNoop
end
if not meta.SetShown then
meta.SetShown = SetShown
end
if not meta.SetParentArray then
meta.SetParentArray = SetParentArray
end
if not meta.SetEnabled then
meta.SetEnabled = SetEnabled
end
if not meta.IsMouseOverEx then
meta.IsMouseOverEx = IsMouseOverEx
end
end
end
-- Sirus EJ calls FontString:IsTruncated() (Cataclysm+) to detect overflow for
-- header tooltips. Always return false on 3.3.5 — we just don't show the
-- "expand" tooltip.
do
local frame = CreateFrame("Frame")
local fs = frame:CreateFontString()
local meta = getmetatable(fs)
local methods = meta and meta.__index
if type(methods) == "table" and not methods.IsTruncated then
methods.IsTruncated = function() return false end
end
end
-- Sirus EJ description fields are SimpleHTML frames; the EJ logic does
-- `self.description:GetText()` (Cataclysm+ on SimpleHTML). 3.3.5 SimpleHTML
-- only has SetText, no GetText. Hook SetText to remember the last text and
-- expose GetText that returns it.
do
local ok, simple = pcall(CreateFrame, "SimpleHTML")
if ok and simple then
local meta = getmetatable(simple)
local methods = meta and meta.__index
if type(methods) == "table" then
local origSetText = methods.SetText
if origSetText and not methods.GetText then
methods.SetText = function(self, text, ...)
self.__moonwellText = text
return origSetText(self, text, ...)
end
methods.GetText = function(self)
return self.__moonwellText
end
end
end
end
end
-- Sirus EJ calls Texture:SetPortrait(displayID) for boss/creature portraits.
-- 3.3.5 has no SetPortraitTextureFromCreatureDisplayID equivalent, so leave
-- the texture blank — the static UI-EJ-BOSS-* icon set elsewhere is enough.
do
local frame = CreateFrame("Frame")
local texture = frame:CreateTexture()
local meta = getmetatable(texture)
local methods = meta and meta.__index
if type(methods) == "table" and not methods.SetPortrait then
methods.SetPortrait = function(self)
self:SetTexture(nil)
end
end
end
@@ -1,41 +0,0 @@
-- Force C_EncounterJournal to copy the freshly loaded JOURNAL* globals into its
-- internal locals. Without this the EJ data API returns nil and the instance
-- list comes up empty.
if C_EncounterJournal and C_EncounterJournal.ReloadData then
C_EncounterJournal.ReloadData()
end
-- EJ_GetLootFilter returns nil until a class filter is set; Sirus
-- EncounterJournal_UpdateFilterString does `if classID > 0 then`, which blows
-- up when classID is nil. Coerce to 0.
if EJ_GetLootFilter then
local _GetLootFilter = EJ_GetLootFilter
function EJ_GetLootFilter()
return _GetLootFilter() or 0
end
end
-- Sirus C_AdventureJournal sometimes fails to fully initialize on 3.3.5
-- because PRIVATE.Initialize() inside Utils/C_AdventureJournal.lua hits an
-- unsupported API mid-way and aborts before `C_AdventureJournal = {}` is
-- reached. Provide a no-op stub so suggestFrame OnShow doesn't blow up.
C_AdventureJournal = C_AdventureJournal or {}
local function noop() end
C_AdventureJournal.UpdateSuggestions = C_AdventureJournal.UpdateSuggestions or noop
C_AdventureJournal.GetPrimaryOffset = C_AdventureJournal.GetPrimaryOffset or function() return 0 end
C_AdventureJournal.SetPrimaryOffset = C_AdventureJournal.SetPrimaryOffset or noop
C_AdventureJournal.GetNumAvailableSuggestions = C_AdventureJournal.GetNumAvailableSuggestions or function() return 0 end
C_AdventureJournal.GetSuggestion = C_AdventureJournal.GetSuggestion or function() end
C_AdventureJournal.CanBeShown = C_AdventureJournal.CanBeShown or function() return true end
C_AdventureJournal.ReloadData = C_AdventureJournal.ReloadData or noop
C_AdventureJournal.ActivateEntry = C_AdventureJournal.ActivateEntry or noop
C_AdventureJournal.GetSuggestions = C_AdventureJournal.GetSuggestions or function(out)
if type(out) == "table" then
for k in pairs(out) do out[k] = nil end
end
return out or {}
end
C_AdventureJournal.GetReward = C_AdventureJournal.GetReward or function() end
C_AdventureJournal.GetPrimaryReward = C_AdventureJournal.GetPrimaryReward or function() end
C_AdventureJournal.GetSecondaryReward = C_AdventureJournal.GetSecondaryReward or function() end
@@ -1,69 +0,0 @@
-- Explicit entry point for 3.3.5 clients, which do not have the retail
-- EncounterJournal micro button.
local function MoonWell_ToggleEncounterJournal()
if not EncounterJournal then
return
end
if EncounterJournal:IsShown() then
HideUIPanel(EncounterJournal)
return
end
if MoonWell_EncounterJournalApplyDungeonMode then
MoonWell_EncounterJournalApplyDungeonMode()
end
ShowUIPanel(EncounterJournal)
if EJPlayerGuide_OpenFrame then
EJPlayerGuide_OpenFrame()
elseif EncounterJournal.instanceSelect and EncounterJournal.instanceSelect.guideTab and EJ_ContentTab_Select then
EJ_ContentTab_Select(EncounterJournal.instanceSelect.guideTab:GetID())
end
end
SLASH_MOONWELLENCOUNTERJOURNAL1 = "/ej"
SLASH_MOONWELLENCOUNTERJOURNAL2 = "/journal"
SLASH_MOONWELLENCOUNTERJOURNAL3 = "/encounterjournal"
SlashCmdList.MOONWELLENCOUNTERJOURNAL = MoonWell_ToggleEncounterJournal
MoonWell_ToggleEncounterJournal = MoonWell_ToggleEncounterJournal
local function MoonWell_EncounterJournalMicroButton_OnEnter(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine("Путеводитель по приключениям", 1, 1, 1)
GameTooltip:Show()
end
local function MoonWell_CreateEncounterJournalMicroButton()
local button = EncounterJournalMicroButton
if not button then
local iconTexture = "Interface\\EncounterJournal\\UI-EJ-PortraitIcon"
button = CreateFrame("Button", "EncounterJournalMicroButton", MainMenuBarArtFrame or UIParent)
button:SetSize(32, 40)
button:SetPoint("BOTTOMLEFT", LFDMicroButton or MainMenuMicroButton or HelpMicroButton or UIParent, "BOTTOMRIGHT", 0, 0)
button:RegisterForClicks("LeftButtonUp")
button:SetNormalTexture(iconTexture)
button:SetPushedTexture(iconTexture)
button:SetDisabledTexture(iconTexture)
button:SetHighlightTexture(iconTexture)
button:SetPushedTextOffset(1, -1)
local highlight = button:GetHighlightTexture()
if highlight then
highlight:SetBlendMode("ADD")
highlight:SetAlpha(0.55)
end
end
button:SetScript("OnClick", MoonWell_ToggleEncounterJournal)
button:SetScript("OnEnter", MoonWell_EncounterJournalMicroButton_OnEnter)
button:SetScript("OnLeave", GameTooltip_Hide)
button:Show()
return button
end
MoonWell_CreateEncounterJournalMicroButton()
@@ -1,24 +0,0 @@
<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">
<!--
Sirus EncounterJournalScrollBarTemplate inherits MinimalScrollBarTemplate
and references self.trackBG inside its inline OnLoad. On 3.3.5 the
parentKey="trackBG" attribute on the inherited template does not
propagate, leaving self.trackBG nil and crashing OnLoad. Define the
template here ourselves with trackBG as a direct child texture, before
Sirus Custom_EncounterJournal.xml gets a chance to register its own.
-->
<Slider name="EncounterJournalScrollBarTemplate" inherits="MinimalScrollBarTemplate" parentKey="ScrollBar" virtual="true">
<Anchors>
<Anchor point="TOPRIGHT" x="0" y="-17"/>
<Anchor point="BOTTOMRIGHT" x="0" y="17"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture parentKey="trackBG" setAllPoints="true">
<Color r="0.2" g="0.13" b="0.08" a="0.25"/>
</Texture>
</Layer>
</Layers>
</Slider>
</Ui>
@@ -1,40 +0,0 @@
-- Sirus EJ XML for lootScroll/searchResults.scrollFrame omits the scrollUp /
-- scrollDown buttons that the stock 3.3.5 HybridScrollFrame implementation
-- requires (HybridScrollFrame_Update / HybridScrollFrame_UpdateButtonStates
-- index them directly). Wire up no-op buttons so the stock helpers don't
-- index nil and the rest of the EJ flow works.
local function AttachFakeScrollButtons(scrollFrame)
if not scrollFrame then
return
end
if not scrollFrame.scrollUp then
local btn = CreateFrame("Button", nil, scrollFrame)
btn:Hide()
btn:SetSize(1, 1)
btn:SetPoint("TOPLEFT")
scrollFrame.scrollUp = btn
end
if not scrollFrame.scrollDown then
local btn = CreateFrame("Button", nil, scrollFrame)
btn:Hide()
btn:SetSize(1, 1)
btn:SetPoint("BOTTOMLEFT")
scrollFrame.scrollDown = btn
end
if scrollFrame.scrollBar and not scrollFrame.scrollBar.thumbTexture then
scrollFrame.scrollBar.thumbTexture = scrollFrame.scrollBar:GetThumbTexture()
or scrollFrame.scrollBar:CreateTexture(nil, "ARTWORK")
end
if scrollFrame.scrollBar and not scrollFrame.scrollBar.trackBG then
scrollFrame.scrollBar.trackBG = scrollFrame.scrollBar:CreateTexture(nil, "BACKGROUND")
end
end
if EncounterJournal then
AttachFakeScrollButtons(EncounterJournal.encounter
and EncounterJournal.encounter.info
and EncounterJournal.encounter.info.lootScroll)
AttachFakeScrollButtons(EncounterJournal.searchResults
and EncounterJournal.searchResults.scrollFrame)
end
@@ -1,38 +0,0 @@
-- Restrict Sirus EJ data tables to expansions that exist on 3.3.5a (Classic, TBC, WotLK).
-- Must run AFTER Generated_EncounterJournal.lua (where the globals are populated) and BEFORE
-- Utils/C_EncounterJournal.lua reads them into locals on first ReloadData.
local ALLOWED_TIER_IDS = {
[68] = true, -- Classic
[70] = true, -- Burning Crusade
[72] = true, -- Wrath of the Lich King
}
local TIER_FIELD_ID = 1
if type(JOURNALTIER) == "table" then
local filtered = {}
for _, tierInfo in ipairs(JOURNALTIER) do
if ALLOWED_TIER_IDS[tierInfo[TIER_FIELD_ID]] then
filtered[#filtered + 1] = tierInfo
end
end
JOURNALTIER = filtered
end
if type(JOURNALTIERXINSTANCE) == "table" then
for instanceID, tierList in pairs(JOURNALTIERXINSTANCE) do
local kept
for _, tierID in ipairs(tierList) do
if ALLOWED_TIER_IDS[tierID] then
kept = kept or {}
kept[#kept + 1] = tierID
end
end
if kept then
JOURNALTIERXINSTANCE[instanceID] = kept
else
JOURNALTIERXINSTANCE[instanceID] = nil
end
end
end
@@ -1,6 +0,0 @@
-- Keep tier tables available for the 3.3.5a dungeon-list shim.
-- C_EncounterJournal imports Sirus globals into locals and clears the globals.
MoonWell_EncounterJournalTiers = CopyTable(JOURNALTIER or {})
MoonWell_EncounterJournalTierInstances = CopyTable(JOURNALTIERXINSTANCE or {})
@@ -1,75 +0,0 @@
ADVENTURE_JOURNAL = "Путеводитель по приключениям"
AJ_ACTION_TEXT_JOIN_BATTLE = "Вступить в бой"
AJ_ACTION_TEXT_JOIN_GROUP = "Вступить в группу"
AJ_ACTION_TEXT_OPEN_EJ = "Открыть журнал"
AJ_ACTION_TEXT_SHOW_QUEST = "Открыть задания"
AJ_ACTION_TEXT_START_HUNT = "Открыть охоту"
AJ_ACTION_TEXT_START_QUEST = "Начать задание"
AJ_LFG_REWARD_DEFAULT_IRANGE_TEXT = "%s: здесь можно получить предметы экипировки %i - %i уровней."
AJ_LFG_REWARD_DEFAULT_TEXT = "%s: здесь можно получить предметы экипировки %i-го уровня."
AJ_LFG_REWARD_DIFFICULTY_IRANGE_TEXT = "%s (%s): здесь можно получить предметы экипировки %i - %i уровней."
AJ_LFG_REWARD_DIFFICULTY_TEXT = "%s (%s): здесь можно получить предметы экипировки %i-го уровня."
AJ_PRIMARY_REWARD_TEXT = "Награда:"
AJ_REWARD_CLICK_TEXT = "Щелкните для просмотра всех наград."
AJ_SAMPLE_REWARD_TEXT = "\n\nПример награды:"
AJ_SUGGESTED_CONTENT_TAB = "Рекомендуемый контент"
EJ_CLASS_FILTER = "Фильтр по классу: %s"
EJ_FILTER_ALL_CLASS = "Все классы"
EJ_INSTANCE_REQUIREMENT_ACHIEVEMENTS = "Достижения:"
EJ_INSTANCE_REQUIREMENT_DIFFICULTY = "Сложность: %s"
EJ_INSTANCE_REQUIREMENT_ITEM_LEVEL = "Средний уровень предметов: %s"
EJ_INSTANCE_REQUIREMENT_LABLE = "Требования:"
EJ_INSTANCE_REQUIREMENT_LEVEL = "Уровень: %s"
EJ_INSTANCE_REQUIREMENT_LEVEL_RANGE = "Уровень: %s-%s"
EJ_INSTANCE_REQUIREMENT_NONE = "Вход свободный"
EJ_INSTANCE_REQUIREMENT_QUESTS = "Задания:"
EJ_INSTANCE_REQUIREMENT_RAID_GROUP = "Для входа требуется быть участником рейдовой группы"
EJ_SET_ITEM_LEVEL = "|cffcc4040[%s]|r Уровень предмета: %d"
ENCOUNTER_JOURNAL_ABILITY = "Способность"
ENCOUNTER_JOURNAL_ENCOUNTER = "Босс"
ENCOUNTER_JOURNAL_ENCOUNTER_ADD = "Помощник"
ENCOUNTER_JOURNAL_FIND_GROUP = "Найти\nгруппу"
ENCOUNTER_JOURNAL_FIND_RAID = "Найти\nрейд"
ENCOUNTER_JOURNAL_INSTANCE = "Подземелье"
ENCOUNTER_JOURNAL_ITEM = "Предмет"
ENCOUNTER_JOURNAL_SEARCH_RESULTS = "Результаты поиска для \"%s\" (%d)"
ENCOUNTER_JOURNAL_SHOW_MAP = "Показать\nкарту"
ENCOUNTER_JOURNAL_SHOW_SEARCH_RESULTS = "Показать все результаты: %d"
LOOT_JOURNAL_ITEM_SETS = "Комплекты"
ENCOUNTER_JOURNAL_NAVIGATION_HOME = "Главная"
-- Путеводитель — вкладка
MW_PLAYER_GUIDE = "Путеводитель"
MW_PLAYER_GUIDE_REFRESH = "Обновить"
-- Заголовки секций
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
-- Шапка этапа
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
-- Пустое состояние
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. " ..
"Это может занять несколько секунд после входа в игру."
-- Загрузка
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
-- Caps-плашки типов целей
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
MW_PG_TYPE_RAID = "РЕЙД"
MW_PG_TYPE_BOSS = "БОСС"
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
@@ -1,128 +0,0 @@
local securecall = securecall
local ipairs = ipairs
local next = next
local type = type
local strsplit = string.split
local ExecuteFrameScript = ExecuteFrameScript
local GetFramesRegisteredForEvent = GetFramesRegisteredForEvent
local UnitName = UnitName
local UnitTokenFromGUID = UnitTokenFromGUID
local REGISTERED_CUSTOM_EVENTS = {}
local REGISTERED_CUSTOM_EVENTS_ALL
local function SecureNext(elements, key)
return securecall(next, elements, key)
end
function FireClientEvent(event, ...)
for _, frame in SecureNext, {GetFramesRegisteredForEvent(event)} do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
function FireCustomClientEvent(event, ...)
if REGISTERED_CUSTOM_EVENTS[event] then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
if REGISTERED_CUSTOM_EVENTS_ALL then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS_ALL do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
end
local eventRegistrar = CreateFrame("Frame")
eventRegistrar:SetScript("OnAttributeChanged", function(this, name, value)
if name == "1" then
if not REGISTERED_CUSTOM_EVENTS[value] then
REGISTERED_CUSTOM_EVENTS[value] = {}
end
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = true
elseif name == "0" then
if REGISTERED_CUSTOM_EVENTS[value] then
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = nil
end
elseif name == "2" then
REGISTERED_CUSTOM_EVENTS_ALL[value] = true
elseif name == "3" then
REGISTERED_CUSTOM_EVENTS_ALL[value] = nil
end
end)
function RegisterCustomEvent(self, event)
eventRegistrar:SetAttribute("f", self)
eventRegistrar:SetAttribute("1", event)
end
function UnregisterCustomEvent(self, event)
eventRegistrar:SetAttribute("f", self)
eventRegistrar:SetAttribute("0", event)
end
function RegisterAllCustomEvents(self)
if not REGISTERED_CUSTOM_EVENTS_ALL then
REGISTERED_CUSTOM_EVENTS_ALL = {}
end
eventRegistrar:SetAttribute("2", self)
end
function UnregisterAllCustomEvents(self)
if not REGISTERED_CUSTOM_EVENTS_ALL then
return
end
eventRegistrar:SetAttribute("3", self)
end
function FireCustomClientUnitGroupEvent(event, unitGUID, ...)
if event and REGISTERED_CUSTOM_EVENTS[event] and unitGUID then
local units = {UnitTokenFromGUID(unitGUID)}
if #units > 0 then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
for _, unit in ipairs(units) do
securecall(ExecuteFrameScript, frame, "OnEvent", event, unit, ...)
end
end
end
end
end
EventHandler = setmetatable(
{
events = {}, -- Original events
listeners = {}, -- New tables for handling events outside og EventHandler
RegisterListener = function(self, listener)
self.listeners[listener] = true
end,
Handle = function(self, opcode, message, unk, sender)
if sender == UnitName("player") then
for listener in SecureNext, self.listeners do
if listener[opcode] then
securecall(listener[opcode], listener, message)
end
end
if self.events[opcode] then
securecall(self.events[opcode], self, message)
end
end
end
},
{
__newindex = function(self, key, value)
if type(value) == "function" then
self.events[key] = value
end
rawset(self, key, value)
end
}
)
local EventHandlerFrame = CreateFrame("Frame")
EventHandlerFrame:RegisterEvent("CHAT_MSG_ADDON")
EventHandlerFrame:SetScript("OnEvent", function(self, event, opcode, message, unk, sender)
EventHandler:Handle(opcode, message, unk, sender)
end)
@@ -1,427 +0,0 @@
--[[-----------------------------------------------------------------------------------------------
For a hybrid scroll frame with buttons of varying size, set .dynamic on the scroll frame
to be a function which will take the offset and return:
1. how many buttons the offset is completely past
2. how many pixels the offset is into the topmost button
So with buttons of size 20, .dynamic(0) should return 0,0 and .dynamic(34) should return 1,14
-----------------------------------------------------------------------------------------------]]--
local round = function (num) return math.floor(num + .5); end
local function HybridScrollFrame_SetVerticalScrollOffset(self, offset)
if self.SetVerticalScroll then
self:SetVerticalScroll(offset);
end
end
local function HybridScrollFrame_UpdateScrollChildRectSafe(self)
if self.UpdateScrollChildRect then
self:UpdateScrollChildRect();
end
end
local function HybridScrollFrame_GetScrollFrame(self)
if not self then
return self;
end
if self.SetVerticalScroll then
return self;
end
local name = self.GetName and self:GetName();
local scrollFrame = self.Container or (name and _G[name.."Container"]);
return scrollFrame or self;
end
local function HybridScrollFrame_GetScrollBar(self)
if self.scrollBar then
return self.scrollBar;
end
local name = self.GetName and self:GetName();
local scrollBar = self.ScrollBar or (name and (_G[name.."ScrollBar"] or _G[name.."ContainerScrollBar"]));
if scrollBar then
self.scrollBar = scrollBar;
end
return scrollBar;
end
function HybridScrollFrame_OnLoad (self)
self:EnableMouse(true);
end
function HybridScrollFrameScrollUp_OnLoad (self)
self:GetParent():GetParent().scrollUp = self;
self:Disable();
self:RegisterForClicks("LeftButtonUp", "LeftButtonDown");
self.direction = 1;
end
function HybridScrollFrameScrollDown_OnLoad (self)
self:GetParent():GetParent().scrollDown = self;
self:Disable();
self:RegisterForClicks("LeftButtonUp", "LeftButtonDown");
self.direction = -1;
end
function HybridScrollFrame_OnValueChanged (self, value)
local scrollFrame = self;
if self.SetMinMaxValues and self.GetParent then
scrollFrame = self:GetParent();
end
scrollFrame = HybridScrollFrame_GetScrollFrame(scrollFrame);
HybridScrollFrame_SetOffset(scrollFrame, value);
HybridScrollFrame_UpdateButtonStates(scrollFrame, value);
end
function HybridScrollFrame_UpdateButtonStates (self, currValue)
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if ( not scrollBar ) then
return;
end
local scrollBarName = scrollBar.GetName and scrollBar:GetName();
local scrollUp = self.scrollUp or scrollBar.ScrollUpButton or (scrollBarName and _G[scrollBarName.."ScrollUpButton"]);
local scrollDown = self.scrollDown or scrollBar.ScrollDownButton or (scrollBarName and _G[scrollBarName.."ScrollDownButton"]);
if ( not currValue ) then
currValue = scrollBar:GetValue();
end
if ( scrollUp ) then
scrollUp:Enable();
end
if ( scrollDown ) then
scrollDown:Enable();
end
local minVal, maxVal = scrollBar:GetMinMaxValues();
if ( currValue >= maxVal ) then
if ( scrollBar.thumbTexture ) then
scrollBar.thumbTexture:Show();
end
if ( scrollDown ) then
scrollDown:Disable()
end
end
if ( currValue <= minVal ) then
if ( scrollBar.thumbTexture ) then
scrollBar.thumbTexture:Show();
end
if ( scrollUp ) then
scrollUp:Disable();
end
end
end
function HybridScrollFrame_OnMouseWheel (self, delta, stepSize)
self = HybridScrollFrame_GetScrollFrame(self);
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if ( not scrollBar or not scrollBar:IsVisible() ) then
return;
end
local isEnabled = not scrollBar.IsEnabled or scrollBar:IsEnabled();
if ( not isEnabled or isEnabled == 0 ) then
return;
end
local minVal, maxVal = scrollBar:GetMinMaxValues();
minVal = minVal or 0;
maxVal = maxVal or self.range or 0;
stepSize = stepSize or self.stepSize or self.buttonHeight or 1;
if ( delta == 1 ) then
scrollBar:SetValue(max(minVal, scrollBar:GetValue() - stepSize));
else
scrollBar:SetValue(min(maxVal, scrollBar:GetValue() + stepSize));
end
end
function HybridScrollFrameScrollButton_OnUpdate (self, elapsed)
self.timeSinceLast = self.timeSinceLast + elapsed;
if ( self.timeSinceLast >= ( self.updateInterval or 0.08 ) ) then
if ( not IsMouseButtonDown("LeftButton") ) then
self:SetScript("OnUpdate", nil);
elseif ( self:IsMouseOver() ) then
local parent = self.parent or self:GetParent():GetParent();
HybridScrollFrame_OnMouseWheel (parent, self.direction, (self.stepSize or parent.buttonHeight/3));
self.timeSinceLast = 0;
end
end
end
function HybridScrollFrameScrollButton_OnClick (self, button, down)
local parent = self.parent or self:GetParent():GetParent();
if ( down ) then
if IsMouseButtonDown then
self.timeSinceLast = (self.timeToStart or -0.2);
self:SetScript("OnUpdate", HybridScrollFrameScrollButton_OnUpdate);
end
HybridScrollFrame_OnMouseWheel (parent, self.direction);
PlaySound("UChatScrollButton");
else
self:SetScript("OnUpdate", nil);
end
end
function HybridScrollFrame_SetPercentageHeight(self, percentageHeight)
local offset = percentageHeight * (self.totalHeight or 0);
HybridScrollFrame_SetOffset(self, offset);
end
function HybridScrollFrame_GetVisiblePercentage(self)
local totalHeight = (self.totalHeight or 0);
if totalHeight == 0 then
return 0;
end
return self.scrollChild:GetHeight() / totalHeight;
end
function HybridScrollFrame_GetScrollPercentage(self)
local scrollableHeight = (self.totalHeight or 0) - self.scrollChild:GetHeight();
if scrollableHeight == 0 then
return 0;
end
return (self.offset or 0) / scrollableHeight;
end
function HybridScrollFrame_Update (self, totalHeight, displayedHeight)
local range = floor(totalHeight - self:GetHeight() + 0.5);
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if ( range > 0 and scrollBar ) then
local minVal, maxVal = scrollBar:GetMinMaxValues();
if ( math.floor(scrollBar:GetValue()) >= math.floor(maxVal) ) then
scrollBar:SetMinMaxValues(0, range);
if ( math.floor(scrollBar:GetValue()) ~= math.floor(range) ) then
scrollBar:SetValue(range);
else
HybridScrollFrame_SetOffset(self, range); -- If we've scrolled to the bottom, we need to recalculate the offset.
end
else
scrollBar:SetMinMaxValues(0, range)
end
scrollBar:Enable();
HybridScrollFrame_UpdateButtonStates(self);
scrollBar:Show();
elseif ( scrollBar ) then
scrollBar:SetValue(0);
if ( scrollBar.doNotHide ) then
scrollBar:Disable();
if ( self.scrollUp ) then
self.scrollUp:Disable();
end
if ( self.scrollDown ) then
self.scrollDown:Disable();
end
if ( scrollBar.thumbTexture ) then
scrollBar.thumbTexture:Hide();
end
else
scrollBar:Hide();
end
end
self.range = range;
self.totalHeight = totalHeight;
self.scrollChild:SetHeight(displayedHeight);
HybridScrollFrame_UpdateScrollChildRectSafe(self);
end
function HybridScrollFrame_GetOffset (self)
return math.floor(self.offset or 0), (self.offset or 0);
end
function HybridScrollFrameScrollChild_OnLoad (self)
self:GetParent().scrollChild = self;
end
function HybridScrollFrame_ExpandButton (self, offset, height)
self.largeButtonTop = round(offset);
self.largeButtonHeight = round(height)
local scrollBar = HybridScrollFrame_GetScrollBar(self);
HybridScrollFrame_SetOffset(self, scrollBar and scrollBar:GetValue() or 0);
end
function HybridScrollFrame_CollapseButton (self)
self.largeButtonTop = nil;
self.largeButtonHeight = nil;
end
function HybridScrollFrame_SetOffset (self, offset)
local buttons = self.buttons
local buttonHeight = self.buttonHeight;
local element, overflow;
if ( not buttonHeight and buttons and buttons[1] ) then
buttonHeight = round(buttons[1]:GetHeight());
self.buttonHeight = buttonHeight;
end
if ( not buttonHeight or buttonHeight == 0 ) then
self.offset = 0;
HybridScrollFrame_SetVerticalScrollOffset(self, 0);
return;
end
local scrollHeight = 0;
local largeButtonTop = self.largeButtonTop
if ( self.dynamic ) then --This is for frames where buttons will have different heights
if ( offset < buttonHeight ) then
-- a little optimization
element = 0;
scrollHeight = offset;
else
element, scrollHeight = self.dynamic(offset);
end
elseif ( largeButtonTop and offset >= largeButtonTop ) then
local largeButtonHeight = self.largeButtonHeight;
-- Initial offset...
element = largeButtonTop / buttonHeight;
if ( offset >= (largeButtonTop + largeButtonHeight) ) then
element = element + 1;
local leftovers = (offset - (largeButtonTop + largeButtonHeight) );
element = element + ( leftovers / buttonHeight );
overflow = element - math.floor(element);
scrollHeight = overflow * buttonHeight;
else
scrollHeight = math.abs(offset - largeButtonTop);
end
else
element = offset / buttonHeight;
overflow = element - math.floor(element);
scrollHeight = overflow * buttonHeight;
end
if ( math.floor(self.offset or 0) ~= math.floor(element) and self.update ) then
self.offset = element;
self:update();
else
self.offset = element;
end
HybridScrollFrame_SetVerticalScrollOffset(self, scrollHeight);
end
function HybridScrollFrame_CreateButtons (self, buttonTemplate, initialOffsetX, initialOffsetY, initialPoint, initialRelative, offsetX, offsetY, point, relativePoint)
local scrollChild = self.scrollChild;
local button, buttonHeight, buttons, numButtons;
local parentName = self:GetName();
local buttonName = parentName and (parentName .. "Button") or nil;
initialPoint = initialPoint or "TOPLEFT";
initialRelative = initialRelative or "TOPLEFT";
point = point or "TOPLEFT";
relativePoint = relativePoint or "BOTTOMLEFT";
offsetX = offsetX or 0;
offsetY = offsetY or 0;
if ( self.buttons ) then
buttons = self.buttons;
buttonHeight = buttons[1]:GetHeight();
else
button = CreateFrame("BUTTON", buttonName and (buttonName .. 1) or nil, scrollChild, buttonTemplate);
buttonHeight = button:GetHeight();
button:SetPoint(initialPoint, scrollChild, initialRelative, initialOffsetX, initialOffsetY);
buttons = {}
tinsert(buttons, button);
end
self.buttonHeight = round(buttonHeight) - offsetY;
numButtons = math.ceil(self:GetHeight() / buttonHeight) + 1;
for i = #buttons + 1, numButtons do
button = CreateFrame("BUTTON", buttonName and (buttonName .. i) or nil, scrollChild, buttonTemplate);
button:SetPoint(point, buttons[i-1], relativePoint, offsetX, offsetY);
tinsert(buttons, button);
end
scrollChild:SetWidth(self:GetWidth())
scrollChild:SetHeight(numButtons * buttonHeight);
HybridScrollFrame_SetVerticalScrollOffset(self, 0);
HybridScrollFrame_UpdateScrollChildRectSafe(self);
self.buttons = buttons;
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if scrollBar then
scrollBar:SetMinMaxValues(0, numButtons * buttonHeight)
scrollBar.buttonHeight = buttonHeight;
scrollBar:SetValueStep(.005);
scrollBar:SetValue(0);
end
end
function HybridScrollFrame_GetButtonIndex(self, button)
return tIndexOf(self.buttons, button);
end
function HybridScrollFrame_GetButtons (self)
return self.buttons;
end
function HybridScrollFrame_SetDoNotHideScrollBar (self, doNotHide)
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if not scrollBar or scrollBar.doNotHide == doNotHide then
return;
end
scrollBar.doNotHide = doNotHide;
HybridScrollFrame_Update(self, self.totalHeight or 0, self.scrollChild:GetHeight());
end
function HybridScrollFrame_ScrollToIndex(self, index, getHeightFunc)
local scrollBar = HybridScrollFrame_GetScrollBar(self);
if not scrollBar then
return;
end
local totalHeight = 0;
local scrollFrameHeight = self:GetHeight();
for i = 1, index do
local entryHeight = getHeightFunc(i);
if i == index then
local offset = 0;
-- we don't need to do anything if the entry is fully displayed with the scroll all the way up
if ( totalHeight + entryHeight > scrollFrameHeight ) then
if ( entryHeight > scrollFrameHeight ) then
-- this entry is larger than the entire scrollframe, put it at the top
offset = totalHeight;
else
-- otherwise place it in the center
local diff = scrollFrameHeight - entryHeight;
offset = totalHeight - diff / 2;
end
-- because of valuestep our positioning might change
-- we'll do the adjustment ourselves to make sure the entry ends up above the center rather than below
local valueStep = scrollBar:GetValueStep();
offset = offset + valueStep - mod(offset, valueStep);
-- but if we ended up moving the entry so high up that its top is not visible, move it back down
if ( offset > totalHeight ) then
offset = offset - valueStep;
end
end
scrollBar:SetValue(offset);
break;
end
totalHeight = totalHeight + entryHeight;
end
end
function HybridScrollBar_Disable(scrollBar)
scrollBar:Disable();
local scrollDownButton = scrollBar.ScrollDownButton or _G[scrollBar:GetName().."ScrollDownButton"];
if scrollDownButton then
scrollDownButton:Disable();
end
local scrollUpButton = scrollBar.ScrollUpButton or _G[scrollBar:GetName().."ScrollUpButton"];
if scrollUpButton then
scrollUpButton:Disable();
end
end
@@ -1,220 +0,0 @@
<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="HybridScrollFrame.lua"/>
<Texture name="HybridScrollBarButton" virtual="true">
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</Texture>
<Slider name="HybridScrollBarBackgroundTemplate" parentKey="scrollBar" virtual="true">
<Size x="20" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="0" y="-18"/>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="0" y="16"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBG" setAllPoints="true" hidden="true" parentKey="trackBG">
<Color r="0" g="0" b="0" a=".85"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentTop" parentKey="ScrollBarTop" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size x="27" y="48"/>
<Anchors>
<Anchor point="TOPLEFT" x="-4" y="17"/>
</Anchors>
<TexCoords left="0" right="0.484375" top="0" bottom=".20"/>
</Texture>
<Texture name="$parentBottom" parentKey="ScrollBarBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size x="27" y="64"/>
<Anchors>
<Anchor point="BOTTOMLEFT" x="-4" y="-15"/>
</Anchors>
<TexCoords left="0.515625" right="1.0" top="0.1440625" bottom="0.4140625"/>
</Texture>
<Texture name="$parentMiddle" parentKey="ScrollBarMiddle" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTop" relativePoint="BOTTOMLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBottom" relativePoint="TOPRIGHT"/>
</Anchors>
<TexCoords left="0" right="0.484375" top="0.1640625" bottom="1"/>
</Texture>
</Layer>
</Layers>
<ThumbTexture name="$parentThumbTexture" file="Interface\Buttons\UI-ScrollBar-Knob" parentKey="thumbTexture">
<Size x="18" y="24"/>
<TexCoords left="0.20" right="0.80" top="0.125" bottom="0.875"/>
</ThumbTexture>
</Slider>
<Slider name="HybridScrollBarTemplate" inherits="HybridScrollBarBackgroundTemplate" virtual="true">
<Frames>
<Button name="$parentScrollUpButton" parentKey="ScrollUpButton" inherits="UIPanelScrollUpButtonTemplate">
<Anchors>
<Anchor point="BOTTOM" relativePoint="TOP" x="0" y="-2"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollUp_OnLoad"/>
<OnClick function ="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
<Button name="$parentScrollDownButton" parentKey="ScrollDownButton" inherits="UIPanelScrollDownButtonTemplate">
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM" x="0" y="2"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollDown_OnLoad"/>
<OnClick function="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged function="HybridScrollFrame_OnValueChanged"/>
</Scripts>
</Slider>
<Slider name="HybridScrollBarTrimTemplate" parentKey="scrollBar" virtual="true">
<Size x="20" y="0"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBG" setAllPoints="true" hidden="true" parentKey="trackBG">
<Color r="0" g="0" b="0" a=".85"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentTop" parentKey="Top" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size x="24" y="48"/>
<Anchors>
<Anchor point="TOPLEFT" x="-4" y="17"/>
</Anchors>
<TexCoords left="0" right="0.45" top="0" bottom=".20"/>
</Texture>
<Texture name="$parentBottom" parentKey="Bottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size x="24" y="64"/>
<Anchors>
<Anchor point="BOTTOMLEFT" x="-4" y="-15"/>
</Anchors>
<TexCoords left="0.515625" right="0.97" top="0.1440625" bottom="0.4140625"/>
</Texture>
<Texture name="$parentMiddle" parentKey="Middle" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTop" relativePoint="BOTTOMLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBottom" relativePoint="TOPRIGHT"/>
</Anchors>
<TexCoords left="0" right="0.45" top="0.1640625" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentScrollUpButton" parentKey="UpButton" inherits="UIPanelScrollUpButtonTemplate">
<Anchors>
<Anchor point="BOTTOM" relativePoint="TOP" x="0" y="-2"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollUp_OnLoad"/>
<OnClick function="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
<Button name="$parentScrollDownButton" parentKey="DownButton" inherits="UIPanelScrollDownButtonTemplate">
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM" x="0" y="2"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollDown_OnLoad"/>
<OnClick function="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged function="HybridScrollFrame_OnValueChanged"/>
</Scripts>
<ThumbTexture name="$parentThumbTexture" inherits="HybridScrollBarButton" file="Interface\Buttons\UI-ScrollBar-Knob" parentKey="thumbTexture">
<Size x="18" y="24"/>
<TexCoords left="0.20" right="0.80" top="0.125" bottom="0.875"/>
</ThumbTexture>
</Slider>
<Slider name="MinimalHybridScrollBarTemplate" parentKey="scrollBar" virtual="true">
<Size x="22" y="0"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTrack" parentKey="trackBG">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="17"/>
<Anchor point="BOTTOMRIGHT" x="0" y="-17"/>
</Anchors>
<Color r="0" g="0" b="0" a="1"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentScrollUpButton" inherits="UIPanelScrollUpButtonTemplate">
<Anchors>
<Anchor point="TOP" x="0" y="15"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollUp_OnLoad"/>
<OnClick function="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
<Button name="$parentScrollDownButton" inherits="UIPanelScrollDownButtonTemplate">
<Anchors>
<Anchor point="BOTTOM" x="0" y="-15"/>
</Anchors>
<Scripts>
<OnLoad function="HybridScrollFrameScrollDown_OnLoad"/>
<OnClick function="HybridScrollFrameScrollButton_OnClick"/>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged function="HybridScrollFrame_OnValueChanged"/>
</Scripts>
<ThumbTexture name="$parentThumbTexture" inherits="HybridScrollBarButton" file="Interface\Buttons\UI-ScrollBar-Knob" parentKey="thumbTexture">
<Size x="18" y="24"/>
<TexCoords left="0.20" right="0.80" top="0.125" bottom="0.875"/>
</ThumbTexture>
</Slider>
<ScrollFrame name="HybridScrollFrameTemplate" virtual="true">
<Scripts>
<OnLoad function="HybridScrollFrame_OnLoad"/>
<OnMouseWheel function="HybridScrollFrame_OnMouseWheel"/>
</Scripts>
<ScrollChild>
<Frame name="$parentScrollChild" parentKey="ScrollChild">
<Scripts>
<OnLoad function="HybridScrollFrameScrollChild_OnLoad"/>
</Scripts>
</Frame>
</ScrollChild>
</ScrollFrame>
<ScrollFrame name="BasicHybridScrollFrameTemplate" inherits="HybridScrollFrameTemplate" virtual="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Slider name="$parentScrollBar" parentKey="ScrollBar" inherits="HybridScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="0" y="-17"/>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="0" y="12"/>
</Anchors>
</Slider>
</Frames>
</ScrollFrame>
<!-- This is a scrollframe with no border and a black texture for a track -->
<ScrollFrame name="MinimalHybridScrollFrameTemplate" inherits="HybridScrollFrameTemplate" virtual="true">
<Frames>
<Slider name="$parentScrollBar" inherits="MinimalHybridScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="0" y="-17"/>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="0" y="17"/>
</Anchors>
</Slider>
</Frames>
</ScrollFrame>
</Ui>
@@ -1,355 +0,0 @@
NAVBAR_WIDTHBUFFER = 20;
function NavBar_Initialize(self, template, homeData, homeButton, overflowButton)
self.template = template;
self.freeButtons = {};
self.navList = {};
self.widthBuffer = NAVBAR_WIDTHBUFFER;
local name = self:GetName();
if not self.dropDown then
local dropDownName = name and name.."DropDown" or nil;
self.dropDown = CreateFrame("Frame", dropDownName, self, "UIDropDownMenuTemplate");
UIDropDownMenu_Initialize(self.dropDown, NavBar_DropDown_Initialize, "MENU");
end
if not homeButton then
local homeButtonName = name and name.."HomeButton" or nil;
homeButton = CreateFrame("BUTTON", homeButtonName, self, self.template);
homeButton:SetWidth(homeButton.text:GetStringWidth()+30);
end
homeButton:SetText(homeData.name or HOME);
if not overflowButton then
local overflowButtonName = name and name.."OverflowButton" or nil;
overflowButton = CreateFrame("BUTTON", overflowButtonName, self, self.template);
overflowButton:SetWidth(30);
-- LOOK AT CLICK
end
if not homeButton:GetScript("OnEnter") then
homeButton:SetScript("OnEnter", NavBar_ButtonOnEnter);
end
if not homeButton:GetScript("OnLeave") then
homeButton:SetScript("OnLeave", NavBar_ButtonOnLeave);
end
if ( self.oldStyle ) then
homeButton:RegisterForClicks("LeftButtonUp", "RightButtonUp");
overflowButton:RegisterForClicks("LeftButtonUp", "RightButtonUp");
else
homeButton:RegisterForClicks("LeftButtonUp");
end
overflowButton.listFunc = NavBar_ListOverFlowButtons;
homeButton:ClearAllPoints();
overflowButton:ClearAllPoints();
homeButton:SetPoint("LEFT", self, "LEFT", 0, 0);
overflowButton:SetPoint("LEFT", self, "LEFT", 0, 0);
overflowButton:Hide();
homeButton.oldClick = homeButton:GetScript("OnClick");
homeButton:SetScript("OnClick", NavBar_ButtonOnClick);
overflowButton:SetScript("OnMouseDown", NavBar_ToggleMenu);
self.homeButton = homeButton;
self.overflowButton = overflowButton;
self.navList[#self.navList+1] = homeButton;
homeButton.myclick = homeData.OnClick;
homeButton.listFunc = homeData.listFunc;
homeButton.data = homeData;
homeButton:Show();
end
function NavBar_Reset(self)
for index=2,#self.navList do
self.navList[index]:Hide();
tinsert(self.freeButtons, self.navList[index])
self.navList[index] = nil;
end
NavBar_CheckLength(self);
end
function NavBar_AddButton(self, buttonData)
local navButton = self.freeButtons[#self.freeButtons];
if navButton then
self.freeButtons[#self.freeButtons] = nil;
end
if not navButton then
local name = self:GetName();
local buttonName = name and name.."Button"..(#self.navList+1);
navButton = CreateFrame("BUTTON", buttonName, self, self.template);
navButton.oldClick = navButton:GetScript("OnClick");
navButton:SetScript("OnClick", NavBar_ButtonOnClick);
if ( self.oldStyle ) then
navButton:RegisterForClicks("LeftButtonUp", "RightButtonUp");
else
navButton:RegisterForClicks("LeftButtonUp");
end
if not navButton:GetScript("OnEnter") then
navButton:SetScript("OnEnter", NavBar_ButtonOnEnter);
end
if not navButton:GetScript("OnLeave") then
navButton:SetScript("OnLeave", NavBar_ButtonOnLeave);
end
end
--Set up the button
local navParent = self.navList[#self.navList];
self.navList[#self.navList+1] = navButton;
navButton.navParent = navParent;
navButton:SetText(buttonData.name);
local buttonExtraWidth;
navButton.MenuArrowButton:Hide();
buttonExtraWidth = 30;
navButton.text:SetWidth(self.textMaxWidth or 0);
navButton:SetWidth(navButton.text:GetStringWidth() + buttonExtraWidth);
navButton.myclick = buttonData.OnClick;
navButton.listFunc = buttonData.listFunc;
navButton.id = buttonData.id;
navButton.data = buttonData;
navButton:Show();
NavBar_CheckLength(self);
navButton.MenuArrowButton:SetEnabled(not buttonData.isDisabled)
end
function NavBar_ClearTrailingButtons(list, freeList, button)
for index=#list,1,-1 do
if not list[index] or button == list[index] then
break
end
list[index]:Hide();
tinsert(freeList, list[index])
list[index] = nil;
end
NavBar_CheckLength(button:GetParent());
end
function NavBar_OpenTo(self, id)
local button;
local found = false;
for i=1, #self.navList do
button = self.navList[i];
if (button.id and button.id == id) then
found = true;
break;
end
end
if (found) then
NavBar_ClearTrailingButtons(self.navList, self.freeButtons, button)
end
end
function NavBar_ButtonOnClick(self, button)
local parent = self:GetParent()
CloseDropDownMenus();
if button == "LeftButton" then
NavBar_ClearTrailingButtons(parent.navList, parent.freeButtons, self);
if self.oldClick then
self:oldClick(button);
end
if self.myclick then
self:myclick(button);
end
elseif button == "RightButton" then
NavBar_ToggleMenu(self);
end
end
function NavBar_ButtonOnEnter(self)
if self.text:IsTruncated() then
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT");
GameTooltip:AddLine(self.text:GetText(), NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, true);
GameTooltip:Show();
end
end
function NavBar_ButtonOnLeave(self)
if self.text:IsTruncated() then
GameTooltip:Hide();
end
end
function NavBar_CheckLength(self)
local width = 0;
local collapsedWidth;
local maxWidth = self:GetWidth() - self.widthBuffer;
local xoffset;
local lastShown;
local collapsed = false;
for i=#self.navList,1,-1 do
local currentWidth = width;
width = width + self.navList[i]:GetWidth();
if width > maxWidth then
self.navList[i]:Hide();
collapsed = true;
if not collapsedWidth then -- store the width for adding the offset button
collapsedWidth = currentWidth;
end
else
self.navList[i]:Show();
if lastShown then
local lastButton = self.navList[lastShown];
xoffset = self.navList[i].xoffset or 0
lastButton:SetPoint("LEFT", self.navList[i], "RIGHT", xoffset, 0);
self.navList[i]:SetFrameLevel(lastButton:GetFrameLevel()+1);
else
self.navList[i]:SetFrameLevel(self:GetFrameLevel()+1);
end
lastShown = i;
end
if i<#self.navList then
if self.navList[i].selected then
self.navList[i].selected:Hide();
end
self.navList[i]:Enable();
else
if self.navList[i].selected then
self.navList[i].selected:Show();
end
self.navList[i]:SetButtonState("NORMAL");
self.navList[i]:Disable();
end
end
if self.navList[1] then
local frameLevel = self.navList[1]:GetFrameLevel()
if frameLevel >= self.overlay:GetFrameLevel() then
self.overlay:SetFrameLevel(frameLevel + 1)
end
end
if collapsed then
if collapsedWidth + self.overflowButton:GetWidth() > maxWidth then
--No room for the overflow button
self.navList[lastShown]:Hide();
lastShown = lastShown + 1;
end
self.overflowButton:Show();
--There should only ever be no lastShown if a single button is longer than maxWidth by itself.
if ( lastShown ) then
local lastButton = self.navList[lastShown];
--There should only ever be no lastButton when there is lastShown if a single button is less than maxWidth
--but it's width plus the width of the overflow button is longer than maxWidth. In this case the single button
--is hidden to make room for the overflowButton.
if ( lastButton ) then
xoffset = self.overflowButton.xoffset or 0
lastButton:SetPoint("LEFT", self.overflowButton, "RIGHT", xoffset, 0);
self.overflowButton:SetFrameLevel(lastButton:GetFrameLevel()+1);
end
end
else
self.overflowButton:Hide();
end
end
function NavBar_ListOverFlowButtons(self, index)
local navBar = self:GetParent();
local button = navBar.navList[index];
if not button:IsShown() then
return button:GetText(), NavBar_OverflowItemOnClick;
end
end
function NavBar_ToggleMenu(self, button)
if ( self:GetParent().dropDown.buttonOwner ~= self ) then
CloseDropDownMenus();
end
self:GetParent().dropDown.buttonOwner = self;
ToggleDropDownMenu(nil, nil, self:GetParent().dropDown, self, 20, 3);
end
function NavBar_DropDown_Initialize(self, level)
local navButton = self.buttonOwner;
if not navButton or not navButton.listFunc then
return;
end
local info = UIDropDownMenu_CreateInfo();
info.func = NavBar_DropDown_Click;
info.owner = navButton;
info.notCheckable = true;
local index = 1;
local text, func = navButton:listFunc(index);
while text do
info.text = text;
info.arg1 = index;
info.arg2 = func;
UIDropDownMenu_AddButton(info, level);
index = index + 1;
text, func = navButton:listFunc(index);
end
end
function NavBar_DropDown_Click(self, index, func)
local navButton = self.owner;
local navBar = navButton:GetParent();
if func ~= NavBar_OverflowItemOnClick then
NavBar_ClearTrailingButtons(navBar.navList, navBar.freeButtons, navButton);
end
if type(func) == "function" then
func(self, index, navBar);
end
end
function NavBar_OverflowItemOnClick(junk, index, navBar)
local button = navBar.navList[index];
if button then
button:Click();
end
end
function NavBar_ReconsultationSize( self )
self.xoffset = -15
local newWidth = min(128, self.text:GetStringWidth()+50)
local texCoordoffsetX = (newWidth/128)*0.25
self:GetNormalTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.70312500, 0.00781250, 0.24218750)
self:GetPushedTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.70312500, 0.25781250, 0.49218750)
self:GetHighlightTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.71312500, 0.50781250, 0.74218750)
self:SetWidth(newWidth)
end
@@ -1,241 +0,0 @@
<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="NavigationBar.lua"/>
<Button name="NavButtonTemplate" motionScriptsWhileDisabled="true" virtual="true">
<Size x="80" y="30"/>
<Layers>
<Layer level="OVERLAY">
<Texture file="Interface\HelpFrame\CS_HelpTextures" parentKey="arrowUp">
<Size x="21" y="30"/>
<Anchors>
<Anchor point="LEFT" relativePoint="RIGHT" x="0" y="0"/>
</Anchors>
<TexCoords left="0.88867188" right="0.92968750" top="0.29687500" bottom="0.53125000"/>
</Texture>
<Texture file="Interface\HelpFrame\CS_HelpTextures" parentKey="arrowDown">
<Size x="21" y="30"/>
<Anchors>
<Anchor point="LEFT" relativePoint="RIGHT" x="0" y="0"/>
</Anchors>
<TexCoords left="0.63281250" right="0.67382813" top="0.75781250" bottom="0.99218750"/>
</Texture>
<Texture file="Interface\HelpFrame\CS_HelpTextures" parentKey="selected" hidden="true">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="0"/>
<Anchor point="BOTTOMRIGHT" x="0" y="0"/>
</Anchors>
<TexCoords left="0.00195313" right="0.25195313" top="0.37500000" bottom="0.64062500"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Button parentKey="MenuArrowButton" hidden="true">
<Size x="27" y="31"/>
<Anchors>
<Anchor point="RIGHT" relativePoint="TOPRIGHT" x="-2" y="-15"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture parentKey="Art" file="Interface\Buttons\SquareButtonTextures">
<Size x="12" y="12"/>
<Anchors>
<Anchor point="CENTER" x="0" y="-1"/>
</Anchors>
<TexCoords left="0.45312500" right="0.64062500" bottom="0.01562500" top="0.20312500"/>
</Texture>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-SquareButton-Up" parentKey="NormalTexture" alpha="0">
<Size x="32" y="32"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</NormalTexture>
<PushedTexture file="Interface\Buttons\UI-SquareButton-Down" parentKey="PushedTexture" alpha="0">
<Size x="32" y="32"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD">
<Size x="32" y="32"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</HighlightTexture>
<Scripts>
<OnMouseDown>
if self:IsEnabled() == 1 then
self.Art:SetPoint("CENTER", -1, -2);
end
</OnMouseDown>
<OnMouseUp>
if self:IsEnabled() == 1 then
self.Art:SetPoint("CENTER", 0, -1);
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
NavBar_ToggleMenu(self:GetParent());
end
</OnMouseUp>
<OnEnter>
self.NormalTexture:SetAlpha(1);
self.PushedTexture:SetAlpha(1);
</OnEnter>
<OnLeave>
self.NormalTexture:SetAlpha(0);
self.PushedTexture:SetAlpha(0);
</OnLeave>
<OnEnable>
self.NormalTexture:SetDesaturated(false)
self.PushedTexture:SetDesaturated(false)
self.Art:SetDesaturated(false)
</OnEnable>
<OnDisable>
self.NormalTexture:SetDesaturated(true)
self.PushedTexture:SetDesaturated(true)
self.Art:SetDesaturated(true)
</OnDisable>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnMouseDown>
if self:IsEnabled() == 1 then
self.arrowUp:Hide();
self.arrowDown:Show();
end
</OnMouseDown>
<OnMouseUp>
self.arrowDown:Hide();
self.arrowUp:Show();
</OnMouseUp>
<OnClick>
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
</OnClick>
<OnEnable>
self.arrowDown:Hide();
self.arrowUp:Show();
</OnEnable>
<OnDisable>
self.arrowDown:Hide();
self.arrowUp:Show();
</OnDisable>
</Scripts>
<NormalTexture file="Interface\HelpFrame\CS_HelpTextures_Tile" parentKey="NormalTexture" horizTile="true">
<TexCoords top="0.06250000" bottom="0.12109375"/>
</NormalTexture>
<PushedTexture file="Interface\HelpFrame\CS_HelpTextures_Tile" parentKey="PushedTexture" horizTile="true">
<TexCoords top="0.12500000" bottom="0.18359375"/>
</PushedTexture>
<HighlightTexture file="Interface\HelpFrame\CS_HelpTextures" parentKey="HighlightTexture" alphaMode="ADD">
<TexCoords left="0.00195313" right="0.25195313" top="0.65625000" bottom="0.92187500"/>
</HighlightTexture>
<ButtonText name="$parentText" inherits="GameFontNormal" justifyH="LEFT" parentKey="text">
<Size x="0" y="12"/>
<Anchors>
<Anchor point="LEFT" x="20" y="0"/>
</Anchors>
</ButtonText>
</Button>
<Frame name="NavBarTemplate" parentKey="navBar" virtual="true">
<Size x="300" y="34"/>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\HelpFrame\CS_HelpTextures_Tile" horizTile="true">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT" x="0" y="0"/>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT" x="0" y="0"/>
</Anchors>
<TexCoords top="0.18750000" bottom="0.25390625"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="$parentOverlay" parentKey="overlay">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="0"/>
<Anchor point="BOTTOMRIGHT" x="0" y="0"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture file="Interface\HelpFrame\CS_HelpTextures_Tile" horizTile="true">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="0"/>
<Anchor point="BOTTOMRIGHT" x="0" y="0"/>
</Anchors>
<TexCoords top="0.25781250" bottom="0.32421875"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
self:SetFrameLevel(50);
</OnLoad>
</Scripts>
</Frame>
<Button name="$parentOverflowButton" parentKey="overflow">
<Size x="44" y="30"/>
<Scripts>
<OnLoad>
self.xoffset = -18
</OnLoad>
<OnClick>
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
</OnClick>
</Scripts>
<NormalTexture file="Interface\HelpFrame\CS_HelpTextures">
<TexCoords left="0.54296875" right="0.62890625" top="0.75781250" bottom="0.99218750"/>
</NormalTexture>
<PushedTexture file="Interface\HelpFrame\CS_HelpTextures">
<TexCoords left="0.45312500" right="0.53906250" top="0.75781250" bottom="0.99218750"/>
</PushedTexture>
<HighlightTexture file="Interface\HelpFrame\CS_HelpTextures" alphaMode="ADD" alpha="0.4">
<TexCoords left="0.54296875" right="0.62890625" top="0.75781250" bottom="0.99218750"/>
</HighlightTexture>
</Button>
<Button name="$parentHomeButton" parentKey="home" text="NAVIGATIONBAR_HOME">
<Size x="128" y="30"/>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentLeft" file="Interface\Common\ShadowOverlay-Left">
<Size x="30" y="30"/>
<Anchors>
<Anchor point="LEFT" x="0" y="0"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
self.xoffset = -15;
local newWidth = min(128, self.text:GetStringWidth()+50)
local texCoordoffsetX = (newWidth/128)*0.25;
self:GetNormalTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.70312500, 0.00781250, 0.24218750);
self:GetPushedTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.70312500, 0.25781250, 0.49218750);
self:GetHighlightTexture():SetTexCoord(0.70312500-texCoordoffsetX, 0.71312500, 0.50781250, 0.74218750);
self:SetWidth(newWidth);
</OnLoad>
<OnClick>
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
</OnClick>
</Scripts>
<NormalTexture file="Interface\HelpFrame\CS_HelpTextures">
</NormalTexture>
<PushedTexture file="Interface\HelpFrame\CS_HelpTextures">
</PushedTexture>
<HighlightTexture file="Interface\HelpFrame\CS_HelpTextures" alphaMode="ADD">
</HighlightTexture>
<ButtonText name="$parentText" inherits="GameFontNormal" justifyH="LEFT" parentKey="text">
<Size x="0" y="12"/>
<Anchors>
<Anchor point="LEFT" x="10" y="0"/>
<Anchor point="RIGHT" x="-30" y="0"/>
</Anchors>
</ButtonText>
</Button>
</Frames>
</Frame>
</Ui>
@@ -1,422 +0,0 @@
local pairs = pairs
local ipairs = ipairs
local next = next
local CreateFrame = CreateFrame
ObjectPoolMixin = {};
function ObjectPoolMixin:OnLoad(creationFunc, resetterFunc)
self.creationFunc = creationFunc;
self.resetterFunc = resetterFunc;
self.activeObjects = {};
self.inactiveObjects = {};
self.numActiveObjects = 0;
end
function ObjectPoolMixin:Acquire()
local numInactiveObjects = #self.inactiveObjects;
if numInactiveObjects > 0 then
local obj = self.inactiveObjects[numInactiveObjects];
self.activeObjects[obj] = true;
self.numActiveObjects = self.numActiveObjects + 1;
self.inactiveObjects[numInactiveObjects] = nil;
return obj, false;
end
local newObj = self.creationFunc(self);
if self.resetterFunc and not self.disallowResetIfNew then
self.resetterFunc(self, newObj);
end
self.activeObjects[newObj] = true;
self.numActiveObjects = self.numActiveObjects + 1;
return newObj, true;
end
function ObjectPoolMixin:Release(obj)
if self:IsActive(obj) then
self.inactiveObjects[#self.inactiveObjects + 1] = obj;
self.activeObjects[obj] = nil;
self.numActiveObjects = self.numActiveObjects - 1;
if self.resetterFunc then
self.resetterFunc(self, obj);
end
return true;
end
return false;
end
function ObjectPoolMixin:ReleaseAll()
for obj in pairs(self.activeObjects) do
self:Release(obj);
end
end
function ObjectPoolMixin:SetResetDisallowedIfNew(disallowed)
self.disallowResetIfNew = disallowed;
end
function ObjectPoolMixin:EnumerateActive()
return pairs(self.activeObjects);
end
function ObjectPoolMixin:GetNextActive(current)
return (next(self.activeObjects, current));
end
function ObjectPoolMixin:GetNextInactive(current)
return (next(self.inactiveObjects, current));
end
function ObjectPoolMixin:IsActive(object)
return (self.activeObjects[object] ~= nil);
end
function ObjectPoolMixin:GetNumActive()
return self.numActiveObjects;
end
function ObjectPoolMixin:EnumerateInactive()
return ipairs(self.inactiveObjects);
end
function CreateObjectPool(creationFunc, resetterFunc)
local objectPool = CreateFromMixins(ObjectPoolMixin);
objectPool:OnLoad(creationFunc, resetterFunc);
return objectPool;
end
FramePoolMixin = CreateFromMixins(ObjectPoolMixin);
local pollFrameIndex = 0
local function FramePoolFactory(framePool)
framePool.createdFrames = framePool.createdFrames + 1
pollFrameIndex = pollFrameIndex + 1
local parentName, frameName = framePool.parent and framePool.parent:GetName()
if parentName then
frameName = string.format("%sPoolFrame%s%i_%i", parentName, framePool.frameTemplate or "NoTemplate", framePool.createdFrames, pollFrameIndex)
elseif framePool.frameTemplate then
frameName = string.format("UnnamedPoolFrame%s%i_%i", framePool.frameTemplate, framePool.createdFrames, pollFrameIndex)
else
frameName = string.format("UnnamedPoolFrameNoTemplate%i_%i", framePool.createdFrames, pollFrameIndex)
end
return CreateFrame(framePool.frameType, frameName, framePool.parent, framePool.frameTemplate);
end
local function ForbiddenFramePoolFactory(framePool)
return FramePoolFactory(framePool)
end
function FramePoolMixin:OnLoad(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
if forbidden then
local creationFunc = ForbiddenFramePoolFactory;
if frameInitFunc ~= nil then
creationFunc = function(framePool)
local frame = ForbiddenFramePoolFactory(framePool);
frameInitFunc(frame);
return frame;
end
end
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
else
local creationFunc = FramePoolFactory;
if frameInitFunc ~= nil then
creationFunc = function(framePool)
local frame = FramePoolFactory(framePool);
frameInitFunc(frame);
return frame;
end
end
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
end
self.createdFrames = 0;
self.frameType = frameType;
self.parent = parent;
self.frameTemplate = frameTemplate;
end
function FramePoolMixin:GetTemplate()
return self.frameTemplate;
end
function FramePool_Hide(framePool, frame)
frame:Hide();
end
function FramePool_HideAndClearAnchors(framePool, frame)
frame:Hide();
frame:ClearAllPoints();
end
function CreateFramePool(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
local framePool = CreateFromMixins(FramePoolMixin);
framePool:OnLoad(frameType, parent, frameTemplate, resetterFunc or FramePool_HideAndClearAnchors, forbidden, frameInitFunc);
return framePool;
end
TexturePoolMixin = CreateFromMixins(ObjectPoolMixin);
local function TexturePoolFactory(texturePool)
return texturePool.parent:CreateTexture(nil, texturePool.layer, texturePool.textureTemplate, texturePool.subLayer);
end
function TexturePoolMixin:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, TexturePoolFactory, resetterFunc);
self.parent = parent;
self.layer = layer;
self.subLayer = subLayer;
self.textureTemplate = textureTemplate;
end
TexturePool_Hide = FramePool_Hide;
TexturePool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
function CreateTexturePool(parent, layer, subLayer, textureTemplate, resetterFunc)
local texturePool = CreateFromMixins(TexturePoolMixin);
texturePool:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc or TexturePool_HideAndClearAnchors);
return texturePool;
end
FontStringPoolMixin = CreateFromMixins(ObjectPoolMixin);
local function FontStringPoolFactory(fontStringPool)
return fontStringPool.parent:CreateFontString(nil, fontStringPool.layer, fontStringPool.fontStringTemplate, fontStringPool.subLayer);
end
function FontStringPoolMixin:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, FontStringPoolFactory, resetterFunc);
self.parent = parent;
self.layer = layer;
self.subLayer = subLayer;
self.fontStringTemplate = fontStringTemplate;
end
FontStringPool_Hide = FramePool_Hide;
FontStringPool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
function CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
local fontStringPool = CreateFromMixins(FontStringPoolMixin);
fontStringPool:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc or FontStringPool_HideAndClearAnchors);
return fontStringPool;
end
ActorPoolMixin = CreateFromMixins(ObjectPoolMixin);
local function ActorPoolFactory(actorPool)
return actorPool.parent:CreateActor(nil, actorPool.actorTemplate);
end
function ActorPoolMixin:OnLoad(parent, actorTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, ActorPoolFactory, resetterFunc);
self.parent = parent;
self.actorTemplate = actorTemplate;
end
ActorPool_Hide = FramePool_Hide;
function ActorPool_HideAndClearModel(actorPool, actor)
actor:ClearModel();
actor:Hide();
end
function CreateActorPool(parent, actorTemplate, resetterFunc)
local actorPool = CreateFromMixins(ActorPoolMixin);
actorPool:OnLoad(parent, actorTemplate, resetterFunc or ActorPool_HideAndClearModel);
return actorPool;
end
FramePoolCollectionMixin = {};
function CreateFramePoolCollection()
local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FramePoolCollectionMixin:OnLoad()
self.pools = {};
end
function FramePoolCollectionMixin:GetNumActive()
local numTotalActive = 0;
for _, pool in pairs(self.pools) do
numTotalActive = numTotalActive + pool:GetNumActive();
end
return numTotalActive;
end
-- Returns the pool, and whether or not the pool needed to be created.
function FramePoolCollectionMixin:GetOrCreatePool(frameType, parent, template, resetterFunc, forbidden)
local pool = self:GetPool(template);
if not pool then
return self:CreatePool(frameType, parent, template, resetterFunc, forbidden), true;
end
return pool, false;
end
function FramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc)
assert(self:GetPool(template) == nil);
local pool = CreateFramePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc);
self.pools[template] = pool;
return pool;
end
function FramePoolCollectionMixin:GetPool(template)
return self.pools[template];
end
function FramePoolCollectionMixin:Acquire(template)
local pool = self:GetPool(template);
assert(pool);
return pool:Acquire();
end
function FramePoolCollectionMixin:Release(object)
for _, pool in pairs(self.pools) do
if pool:Release(object) then
-- Found it! Just return
return;
end
end
-- Huh, we didn't find that object
assert(false);
end
function FramePoolCollectionMixin:ReleaseAllByTemplate(template)
local pool = self:GetPool(template);
if pool then
pool:ReleaseAll();
end
end
function FramePoolCollectionMixin:ReleaseAll()
for key, pool in pairs(self.pools) do
pool:ReleaseAll();
end
end
function FramePoolCollectionMixin:EnumerateActiveByTemplate(template)
local pool = self:GetPool(template);
if pool then
return pool:EnumerateActive();
end
return nop;
end
function FramePoolCollectionMixin:EnumerateActive()
local currentPoolKey, currentPool = next(self.pools, nil);
local currentObject = nil;
return function()
if currentPool then
currentObject = currentPool:GetNextActive(currentObject);
while not currentObject do
currentPoolKey, currentPool = next(self.pools, currentPoolKey);
if currentPool then
currentObject = currentPool:GetNextActive();
else
break;
end
end
end
return currentObject;
end, nil;
end
function FramePoolCollectionMixin:EnumerateInactiveByTemplate(template)
local pool = self:GetPool(template);
if pool then
return pool:EnumerateInactive();
end
return nop;
end
FixedSizeFramePoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
function CreateFixedSizeFramePoolCollection()
local poolCollection = CreateFromMixins(FixedSizeFramePoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FixedSizeFramePoolCollectionMixin:OnLoad()
FramePoolCollectionMixin.OnLoad(self);
self.sizes = {};
end
function FixedSizeFramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, maxPoolSize, preallocate)
local pool = FramePoolCollectionMixin.CreatePool(self, frameType, parent, template, resetterFunc, forbidden);
if preallocate then
for i = 1, maxPoolSize do
pool:Acquire();
end
pool:ReleaseAll();
end
self.sizes[template] = maxPoolSize;
return pool;
end
function FixedSizeFramePoolCollectionMixin:Acquire(template)
local pool = self:GetPool(template);
assert(pool);
if pool:GetNumActive() < self.sizes[template] then
return pool:Acquire();
end
return nil;
end
FontStringPoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
function CreateFontStringPoolCollection()
local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FontStringPoolCollectionMixin:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
local pool = self:GetPool(fontStringTemplate);
if not pool then
pool = self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
end
return pool;
end
function FontStringPoolCollectionMixin:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
assert(self:GetPool(fontStringTemplate) == nil);
local pool = CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
self.pools[fontStringTemplate] = pool;
return pool;
end
function FontStringPoolCollectionMixin:CreatePoolIfNeeded(parent, layer, subLayer, fontStringTemplate, resetterFunc)
if not self:GetPool(fontStringTemplate) then
self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
end
end
function FontStringPoolCollectionMixin:Acquire(fontStringTemplate, parent, layer, subLayer, resetterFunc)
local pool = self:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
local newString = pool:Acquire();
if parent then
newString:SetParent(parent);
end
if layer then
newString:SetDrawLayer(layer, subLayer);
end
return newString;
end

Some files were not shown because too many files have changed in this diff Show More