Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f82bbf252c | |||
| a3fc195f14 | |||
| c1c2b4e8bb | |||
| 83f5e08689 | |||
| 76ca0cc851 | |||
| 398245d27d | |||
| 887ab7879b | |||
| e3b8d2649e | |||
| e916cdea19 | |||
| befdaa456d | |||
| 7291fff664 | |||
| 790506fc7a | |||
| 87329f2633 | |||
| 8375b95f7b | |||
| 01f476ca1b | |||
| 63c9af7846 | |||
| f02f4d73f0 | |||
| 721833bb50 | |||
| b30a3554cf | |||
| 595ed5091d | |||
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 | |||
| 2291ace9f7 | |||
| 26ff2b3b94 | |||
| d5e2dce359 | |||
| 22ae941c4a | |||
| 08c1b9da3f | |||
| b0da9ec10b | |||
| d8d94f10b0 | |||
| 255485daca |
+5
-1
@@ -5,4 +5,8 @@ AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=
|
||||
AWS_BUCKET=
|
||||
AWS_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
|
||||
PRODUCTION_REALMLIST=
|
||||
PTR_REALMLIST=
|
||||
LOCAL_REALMLIST=
|
||||
|
||||
+5
-1
@@ -3,4 +3,8 @@ __pycache__/
|
||||
manifest.json
|
||||
.claude
|
||||
.vscode
|
||||
dist
|
||||
dist
|
||||
build/
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
[submodule "vendor/warcraftxl"]
|
||||
path = vendor/warcraftxl
|
||||
url = https://github.com/WarcraftXL/wxl-core.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-assets"]
|
||||
path = vendor/modules/wxl-modern-assets
|
||||
url = https://github.com/WarcraftXL/wxl-modern-assets.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-render"]
|
||||
path = vendor/modules/wxl-modern-render
|
||||
url = https://github.com/WarcraftXL/wxl-modern-render.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-unit-outline"]
|
||||
path = vendor/modules/wxl-unit-outline
|
||||
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-adt"]
|
||||
path = vendor/modules/wxl-modern-adt
|
||||
url = https://github.com/WarcraftXL/wxl-modern-adt.git
|
||||
branch = main
|
||||
@@ -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.
|
||||
@@ -0,0 +1,74 @@
|
||||
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_MOONWELL_HOST
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/host/*.cpp")
|
||||
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_MOONWELL_HOST}
|
||||
${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()
|
||||
@@ -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 | 4–7 дней |
|
||||
| Независимые косметические каналы | 4–7 дней |
|
||||
| Asset pipeline | 3–5 дней |
|
||||
| Серверное хранение и протокол | 4–7 дней |
|
||||
| Текстурные слои и geoset | 5–10 дней |
|
||||
| Новый UI создания/изменения | 5–8 дней |
|
||||
| Полный вертикальный срез одной расы | 5–10 дней |
|
||||
| Каждая следующая раса | 2–5 дней |
|
||||
| Стабилизация и regression | 7–12 дней |
|
||||
|
||||
Ориентиры:
|
||||
|
||||
- технический прототип одной расы: 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.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Автоматический импорт mount и pet
|
||||
|
||||
Список современных моделей находится в `custom-creatures.json`. В нём есть две независимые
|
||||
секции:
|
||||
|
||||
```json
|
||||
{
|
||||
"mounts": [
|
||||
"assets/creatures/my_mount"
|
||||
],
|
||||
"pets": [
|
||||
"assets/creatures/my_pet"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Папка должна быть результатом **Raw M2 export** из wow.export и содержать:
|
||||
|
||||
- один основной `.m2`;
|
||||
- `*.manifest.json`;
|
||||
- все упомянутые в manifest текстуры, skin, skeleton, bone и animation-файлы.
|
||||
|
||||
Разрешены абсолютные пути, например `C:/Users/sindo/wow.export/creature/catslime`, но для
|
||||
воспроизводимой сборки папку лучше скопировать внутрь репозитория и указать относительный путь.
|
||||
|
||||
## Что делает сборщик
|
||||
|
||||
При обычном запуске `tool.exe`, `run.ps1` или `deploy.ps1` сборщик:
|
||||
|
||||
1. читает manifest каждой модели и размещает retail M2 со всеми зависимостями в loose-каталоге
|
||||
`Data/Patch-WXL.MPQ`, создавая рядом `.mdx`-алиас для загрузчика WoW 3.3.5, чтобы WarcraftXL всегда
|
||||
преобразовывал MD21 до загрузки клиентом;
|
||||
2. генерирует `WXLFileData.csv` для WarcraftXL;
|
||||
3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`;
|
||||
4. сохраняет стабильные ID в `custom-creatures.lock.json`;
|
||||
5. для `pets` создаёт companion summon spell и строку skill line `778`;
|
||||
6. кладёт серверные DBC, SQL и таблицу ID в `build/server-dbc`.
|
||||
|
||||
Полный `Spell.dbc` в патч не копируется. WarcraftXL читает компактный
|
||||
`WXLSpellOverrides.tsv` и при открытии DBC добавляет новые spell-записи к штатному файлу клиента.
|
||||
|
||||
Перестановка моделей в списке не меняет уже выданные ID. Lock-файл необходимо хранить в Git.
|
||||
|
||||
## Дополнительные параметры
|
||||
|
||||
Если значений по умолчанию недостаточно, вместо строки используется объект:
|
||||
|
||||
```json
|
||||
{
|
||||
"folder": "assets/creatures/my_mount",
|
||||
"key": "my_mount",
|
||||
"name": "My Mount",
|
||||
"name_ru": "Мой транспорт",
|
||||
"archive": "Creature/MyMount",
|
||||
"model_file": "my_mount.m2",
|
||||
"template_display_id": 2404,
|
||||
"display_scale": 1.0,
|
||||
"model_scale": 1.0,
|
||||
"collision_width": 0.6111,
|
||||
"collision_height": 2.031,
|
||||
"mount_height": 1.8657,
|
||||
"flags": 0
|
||||
}
|
||||
```
|
||||
|
||||
Для `mounts` по умолчанию клонируется верховая лошадь с display ID `2404`, для `pets` —
|
||||
компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC;
|
||||
параметры объекта позволяют их переопределить.
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
Для каждого элемента `pets` сборщик автоматически выдаёт `creatureEntry`, `summonSpellID` и
|
||||
`skillLineAbilityID`. Значения находятся в `build/server-dbc/custom-creatures.csv`.
|
||||
|
||||
1. Скопировать `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` из `build/server-dbc` в
|
||||
серверный каталог `data/dbc`.
|
||||
2. Применить `build/server-dbc/custom-creatures.sql` к `acore_world`.
|
||||
3. Перезапустить worldserver: DBC и таблицы `spell_dbc` загружаются только при старте.
|
||||
4. Для проверки изучить выданный spell командой `.learn <summonSpellID>`.
|
||||
|
||||
SQL создаёт `creature_template`, `creature_model_info`, `creature_template_model`, companion spell и строку skill line.
|
||||
Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует
|
||||
другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.64–1.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.0–1.0** (не 0–255).
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -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"` | Добавить атрибут |
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,209 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release',
|
||||
[string]$ClientPath = '',
|
||||
[string]$PackagePath = '',
|
||||
[switch]$Deploy,
|
||||
[switch]$NativeRenderer
|
||||
)
|
||||
|
||||
function Enable-LargeAddressAware {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
||||
|
||||
if ($bytes.Length -lt 64) {
|
||||
throw "File is too small to be a valid PE executable: $Path"
|
||||
}
|
||||
|
||||
# DOS header: MZ
|
||||
if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) {
|
||||
throw "DOS MZ signature was not found: $Path"
|
||||
}
|
||||
|
||||
# IMAGE_DOS_HEADER.e_lfanew
|
||||
$peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
|
||||
|
||||
if ($peOffset -lt 0 -or ($peOffset + 24) -gt $bytes.Length) {
|
||||
throw "Invalid PE header offset in: $Path"
|
||||
}
|
||||
|
||||
# PE\0\0 signature
|
||||
$peSignature = [BitConverter]::ToUInt32($bytes, $peOffset)
|
||||
|
||||
if ($peSignature -ne 0x00004550) {
|
||||
throw "PE signature was not found: $Path"
|
||||
}
|
||||
|
||||
# IMAGE_FILE_HEADER starts after the 4-byte PE signature.
|
||||
# Characteristics is at offset 18 inside IMAGE_FILE_HEADER.
|
||||
$characteristicsOffset = $peOffset + 4 + 18
|
||||
$characteristics = [BitConverter]::ToUInt16(
|
||||
$bytes,
|
||||
$characteristicsOffset
|
||||
)
|
||||
|
||||
$newCharacteristics = [uint16](
|
||||
$characteristics -bor 0x0020
|
||||
)
|
||||
|
||||
if ($newCharacteristics -ne $characteristics) {
|
||||
$encoded = [BitConverter]::GetBytes($newCharacteristics)
|
||||
|
||||
$bytes[$characteristicsOffset] = $encoded[0]
|
||||
$bytes[$characteristicsOffset + 1] = $encoded[1]
|
||||
|
||||
[System.IO.File]::WriteAllBytes($Path, $bytes)
|
||||
|
||||
Write-Host (
|
||||
"LAA enabled for {0}: 0x{1:X4} -> 0x{2:X4}" -f
|
||||
$Path,
|
||||
$characteristics,
|
||||
$newCharacteristics
|
||||
) -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host (
|
||||
"LAA is already enabled for {0}: 0x{1:X4}" -f
|
||||
$Path,
|
||||
$characteristics
|
||||
) -ForegroundColor DarkGreen
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
|
||||
$destinationExe = Join-Path $Destination 'Wow.exe'
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $stockExe `
|
||||
-Destination $destinationExe `
|
||||
-Force
|
||||
|
||||
Enable-LargeAddressAware -Path $destinationExe
|
||||
|
||||
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"
|
||||
@@ -20,6 +20,11 @@ IGNORED_TOP_LEVEL_DIRS = {
|
||||
IGNORED_DIRS_ANYWHERE = {
|
||||
".git",
|
||||
".moonwell_launcher",
|
||||
".vscode"
|
||||
}
|
||||
IGNORED_TOP_LEVEL_FILES = {
|
||||
"Wow.moonwell-patched.backup.exe",
|
||||
"WarcraftXL_on12.disable",
|
||||
}
|
||||
|
||||
ENV_VAR_NAME = "WOW_HOME"
|
||||
@@ -42,6 +47,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])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"base_dbc_dir": "assets/dbc/3.3.5a",
|
||||
"model_data_id_start": 50000,
|
||||
"display_info_id_start": 50000,
|
||||
"creature_entry_id_start": 5000000,
|
||||
"summon_spell_id_start": 110000,
|
||||
"skill_line_ability_id_start": 30000,
|
||||
"defaults": {
|
||||
"mount_template_display_id": 2404,
|
||||
"pet_template_display_id": 5448
|
||||
},
|
||||
"mounts": [],
|
||||
"pets": [
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/Catslime",
|
||||
"name": "Cat Slime",
|
||||
"name_ru": "Котослизень",
|
||||
"display_scale": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"models": {
|
||||
"catslime": {
|
||||
"model_data_id": 50000,
|
||||
"display_info_id": 50000,
|
||||
"creature_entry": 5000000,
|
||||
"summon_spell_id": 110000,
|
||||
"skill_line_ability_id": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
[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'
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if ($cargo) {
|
||||
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
|
||||
} elseif (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||
throw 'Cargo was not found and the MPQ builder is not compiled.'
|
||||
}
|
||||
|
||||
& $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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
LIBRARY d3d9
|
||||
EXPORTS
|
||||
Direct3DCreate9
|
||||
Direct3DCreate9Ex
|
||||
WxlD3D12Device
|
||||
WxlD3D12Queue
|
||||
WxlD3D12DrainDebug
|
||||
WxlSetSsaaFactor
|
||||
WxlGetSsaaFactor
|
||||
@@ -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()
|
||||
@@ -0,0 +1,264 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Appends compact MoonWell spell overrides to the stock 3.3.5 DBCs at open time.
|
||||
|
||||
#include "Host.hpp"
|
||||
#include "core/Logger.hpp"
|
||||
#include "mpq/MpqStore.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::host
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kOverridesPath = "WXLSpellOverrides.tsv";
|
||||
constexpr std::string_view kSpellPath = "DBFilesClient\\Spell.dbc";
|
||||
constexpr std::string_view kSkillPath = "DBFilesClient\\SkillLineAbility.dbc";
|
||||
constexpr uint32_t kSpellFields = 234;
|
||||
constexpr uint32_t kSpellRecordSize = kSpellFields * 4;
|
||||
constexpr uint32_t kSkillFields = 14;
|
||||
constexpr uint32_t kSkillRecordSize = kSkillFields * 4;
|
||||
|
||||
struct Override
|
||||
{
|
||||
uint32_t spellId = 0;
|
||||
uint32_t templateSpellId = 0;
|
||||
uint32_t creatureEntry = 0;
|
||||
uint32_t skillLineAbilityId = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
bool g_ready = false;
|
||||
std::vector<uint8_t> g_spellDbc;
|
||||
std::vector<uint8_t> g_skillDbc;
|
||||
|
||||
uint32_t ReadU32(const uint8_t* data)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
std::memcpy(&value, data, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
void WriteU32(uint8_t* data, uint32_t value)
|
||||
{
|
||||
std::memcpy(data, &value, sizeof(value));
|
||||
}
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front())))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back())))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ParseU32(std::string_view text, uint32_t& value)
|
||||
{
|
||||
text = Trim(text);
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size();
|
||||
}
|
||||
|
||||
bool SamePath(std::string_view left, std::string_view right)
|
||||
{
|
||||
if (left.size() != right.size()) return false;
|
||||
for (size_t i = 0; i < left.size(); ++i)
|
||||
{
|
||||
const unsigned char a = static_cast<unsigned char>(left[i] == '/' ? '\\' : left[i]);
|
||||
const unsigned char b = static_cast<unsigned char>(right[i] == '/' ? '\\' : right[i]);
|
||||
if (std::tolower(a) != std::tolower(b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseOverrides(const std::vector<uint8_t>& bytes, std::vector<Override>& out)
|
||||
{
|
||||
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < text.size())
|
||||
{
|
||||
size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string_view::npos) lineEnd = text.size();
|
||||
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
|
||||
lineStart = lineEnd + 1;
|
||||
if (line.empty() || line.front() == '#') continue;
|
||||
|
||||
std::string_view fields[5];
|
||||
size_t fieldStart = 0;
|
||||
bool valid = true;
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
const size_t tab = line.find('\t', fieldStart);
|
||||
if (tab == std::string_view::npos) { valid = false; break; }
|
||||
fields[i] = line.substr(fieldStart, tab - fieldStart);
|
||||
fieldStart = tab + 1;
|
||||
}
|
||||
if (!valid) return false;
|
||||
fields[4] = Trim(line.substr(fieldStart));
|
||||
|
||||
Override item;
|
||||
if (!ParseU32(fields[0], item.spellId) ||
|
||||
!ParseU32(fields[1], item.templateSpellId) ||
|
||||
!ParseU32(fields[2], item.creatureEntry) ||
|
||||
!ParseU32(fields[3], item.skillLineAbilityId) || fields[4].empty())
|
||||
return false;
|
||||
item.name.assign(fields[4]);
|
||||
out.push_back(std::move(item));
|
||||
}
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
bool ValidateWdbc(const std::vector<uint8_t>& bytes, uint32_t fields, uint32_t recordSize,
|
||||
uint32_t& count, uint32_t& stringSize, size_t& recordsEnd)
|
||||
{
|
||||
if (bytes.size() < 20 || std::memcmp(bytes.data(), "WDBC", 4) != 0) return false;
|
||||
count = ReadU32(bytes.data() + 4);
|
||||
if (ReadU32(bytes.data() + 8) != fields || ReadU32(bytes.data() + 12) != recordSize)
|
||||
return false;
|
||||
stringSize = ReadU32(bytes.data() + 16);
|
||||
recordsEnd = 20ull + static_cast<size_t>(count) * recordSize;
|
||||
return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size();
|
||||
}
|
||||
|
||||
const uint8_t* FindSpell(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t id)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSpellRecordSize;
|
||||
if (ReadU32(row) == id) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* FindSkillRow(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t spellId)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSkillRecordSize;
|
||||
if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BuildSpellDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSpellRecordSize);
|
||||
std::vector<uint8_t> strings;
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSpell(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSpellRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.spellId);
|
||||
WriteU32(row + 110 * 4, item.creatureEntry);
|
||||
|
||||
const uint32_t nameOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), item.name.begin(), item.name.end());
|
||||
strings.push_back(0);
|
||||
const std::string description = "Призывает или отпускает спутника: " + item.name + ".";
|
||||
const uint32_t descriptionOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), description.begin(), description.end());
|
||||
strings.push_back(0);
|
||||
for (uint32_t field = 136; field <= 151; ++field) WriteU32(row + field * 4, nameOffset);
|
||||
for (uint32_t field = 170; field <= 185; ++field) WriteU32(row + field * 4, descriptionOffset);
|
||||
for (uint32_t field = 187; field <= 202; ++field) WriteU32(row + field * 4, 0);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size() + strings.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
WriteU32(out.data() + 16, stringSize + static_cast<uint32_t>(strings.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
out.insert(out.end(), strings.begin(), strings.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildSkillDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSkillRecordSize);
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSkillRow(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSkillRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.skillLineAbilityId);
|
||||
WriteU32(row + 8, item.spellId);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
const std::string root = wxl::host::ClientRoot();
|
||||
wxl::host::mpq::MpqStore store;
|
||||
std::vector<uint8_t> overrideBytes, spellBase, skillBase;
|
||||
if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes))
|
||||
return;
|
||||
|
||||
std::vector<Override> overrides;
|
||||
if (!ParseOverrides(overrideBytes, overrides) ||
|
||||
!store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) ||
|
||||
!BuildSpellDbc(spellBase, overrides, g_spellDbc) ||
|
||||
!BuildSkillDbc(skillBase, overrides, g_skillDbc))
|
||||
{
|
||||
WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs");
|
||||
return;
|
||||
}
|
||||
g_ready = true;
|
||||
WLOG_INFO("moonwell-spells: appended %zu companion spell(s)", overrides.size());
|
||||
}
|
||||
|
||||
bool Provide(std::string_view name, std::vector<uint8_t>& out)
|
||||
{
|
||||
if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false;
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
if (!g_ready) return false;
|
||||
out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Registrar
|
||||
{
|
||||
Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); }
|
||||
};
|
||||
|
||||
Registrar g_registrar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// FileDataID resolver for selected retail assets shipped in MoonWell patches.
|
||||
|
||||
#include "Host.hpp"
|
||||
#include "core/Logger.hpp"
|
||||
#include "mpq/MpqStore.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::host
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
std::unordered_map<uint32_t, std::string> g_paths;
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
|
||||
value.front() == '\r' || value.front() == '\n'))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
|
||||
value.back() == '\r' || value.back() == '\n'))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
const std::string root = wxl::host::ClientRoot();
|
||||
wxl::host::mpq::MpqStore store;
|
||||
std::vector<uint8_t> bytes;
|
||||
if (root.empty() || !store.Mount(root) || !store.ReadAll(kFileDataMap, bytes))
|
||||
{
|
||||
WLOG_WARN("moonwell: FileDataID map '%.*s' was not found",
|
||||
int(kFileDataMap.size()), kFileDataMap.data());
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < text.size())
|
||||
{
|
||||
size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string_view::npos) lineEnd = text.size();
|
||||
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
|
||||
lineStart = lineEnd + 1;
|
||||
|
||||
if (line.empty() || line.front() == '#') continue;
|
||||
const size_t comma = line.find(',');
|
||||
if (comma == std::string_view::npos) continue;
|
||||
|
||||
const std::string_view idText = Trim(line.substr(0, comma));
|
||||
std::string_view pathText = Trim(line.substr(comma + 1));
|
||||
uint32_t fileDataId = 0;
|
||||
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
|
||||
fileDataId == 0 || pathText.empty())
|
||||
continue;
|
||||
|
||||
std::string path(pathText);
|
||||
for (char& c : path) if (c == '/') c = '\\';
|
||||
g_paths[fileDataId] = std::move(path);
|
||||
}
|
||||
|
||||
WLOG_INFO("moonwell: loaded %zu FileDataID path(s) from %.*s",
|
||||
g_paths.size(), int(kFileDataMap.size()), kFileDataMap.data());
|
||||
}
|
||||
|
||||
bool Resolve(uint32_t fileDataId, std::string& outPath)
|
||||
{
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
const auto found = g_paths.find(fileDataId);
|
||||
if (found == g_paths.end()) return false;
|
||||
outPath = found->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Registrar
|
||||
{
|
||||
Registrar() { wxl::host::RegisterResolver("moonwell-filedata", &Resolve); }
|
||||
};
|
||||
|
||||
Registrar g_registrar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
// 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/Hook.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;
|
||||
|
||||
// FrameXML's stock SetCreature only accepts a creature entry and waits
|
||||
// for its client cache record. Encounter Journal already has the exact
|
||||
// CreatureDisplayInfo ID, so accept it as an optional second argument.
|
||||
constexpr uintptr_t kSetCreature = 0x00597960;
|
||||
constexpr uintptr_t kGetCurrentModelFrame = 0x004A81B0;
|
||||
constexpr uintptr_t kModelFrameTypeToken = 0x00C0E4D4;
|
||||
constexpr uintptr_t kApplyCreatureCacheRecord = 0x00597700;
|
||||
|
||||
using SetCreatureFn = int(__cdecl*)(void* state);
|
||||
using ApplyCreatureCacheRecordFn = void(__fastcall*)(void* frame, void* edx,
|
||||
const void* cacheRecord);
|
||||
SetCreatureFn g_nextSetCreature = nullptr;
|
||||
alignas(4) std::array<uint32_t, 10> g_encounterJournalCreatureRecord{};
|
||||
|
||||
void* GetCurrentModelFrame(void* state, uint32_t typeToken)
|
||||
{
|
||||
// FrameScript_GetObject takes the type token on the stack, but the
|
||||
// 3.3.5 client also expects lua_State in ESI (an internal calling
|
||||
// convention not expressible with a regular C function pointer).
|
||||
void* frame = nullptr;
|
||||
__asm
|
||||
{
|
||||
mov esi, state
|
||||
push typeToken
|
||||
mov eax, kGetCurrentModelFrame
|
||||
call eax
|
||||
add esp, 4
|
||||
mov frame, eax
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
int __cdecl SetCreatureDisplayInfoHook(void* state)
|
||||
{
|
||||
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
|
||||
if (!state || !wxl::runtime::lua::IsNumber(state, 3))
|
||||
return result;
|
||||
|
||||
const double requestedDisplayInfo = wxl::runtime::lua::ToNumber(state, 3);
|
||||
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
|
||||
return result;
|
||||
|
||||
const auto displayInfo = static_cast<uint32_t>(requestedDisplayInfo);
|
||||
__try
|
||||
{
|
||||
const uint32_t typeToken = *reinterpret_cast<const uint32_t*>(kModelFrameTypeToken);
|
||||
if (!typeToken)
|
||||
return result;
|
||||
|
||||
void* frame = GetCurrentModelFrame(state, typeToken);
|
||||
if (!frame)
|
||||
return result;
|
||||
|
||||
// Both the initial loader and the later character-appearance
|
||||
// pass read displayInfo at +0x24. Keep this record alive and
|
||||
// attach it to the frame: character models use it asynchronously
|
||||
// to resolve CreatureDisplayInfoExtra, baked skin and equipment.
|
||||
g_encounterJournalCreatureRecord.fill(0);
|
||||
g_encounterJournalCreatureRecord[9] = displayInfo;
|
||||
*reinterpret_cast<const void**>(static_cast<uint8_t*>(frame) + 0x378) =
|
||||
g_encounterJournalCreatureRecord.data();
|
||||
reinterpret_cast<ApplyCreatureCacheRecordFn>(kApplyCreatureCacheRecord)(
|
||||
frame, nullptr, g_encounterJournalCreatureRecord.data());
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
WLOG_ERROR("moonwell: SetCreature displayInfo bridge failed for %u", displayInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InstallEncounterJournalModelPreview()
|
||||
{
|
||||
if (!wxl::core::hook::Install("MoonWellSetCreatureDisplayInfo", kSetCreature,
|
||||
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
|
||||
{
|
||||
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
|
||||
return;
|
||||
}
|
||||
WLOG_INFO("moonwell: encounter journal displayInfo model bridge installed");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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-encounter-journal-models", &InstallEncounterJournalModelPreview);
|
||||
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
||||
}
|
||||
} g_registration;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("production", "ptr", "local")]
|
||||
[string]$Env = "local"
|
||||
)
|
||||
|
||||
# Stop on errors
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
@@ -31,11 +37,78 @@ if (-not $env:WOW_HOME) {
|
||||
$WOW_HOME = $env:WOW_HOME
|
||||
Write-Host "WOW_HOME = $WOW_HOME"
|
||||
|
||||
function Test-ExistingFileWriteAccess {
|
||||
param([Parameter(Mandatory=$true)][string]$Path)
|
||||
|
||||
try {
|
||||
$stream = [System.IO.File]::Open(
|
||||
$Path,
|
||||
[System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::ReadWrite
|
||||
)
|
||||
$stream.Close()
|
||||
return $true
|
||||
} catch [System.UnauthorizedAccessException] {
|
||||
return $false
|
||||
} catch {
|
||||
# Sharing violations are handled later by Stop-WowRuntime. They do not
|
||||
# indicate that the whole script needs an elevated token.
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$installedWarcraftXL = Join-Path $WOW_HOME "WarcraftXL.dll"
|
||||
$isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||||
)
|
||||
|
||||
if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
|
||||
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
|
||||
-not $isAdministrator) {
|
||||
Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..."
|
||||
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
|
||||
-WorkingDirectory $ROOT `
|
||||
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
|
||||
exit $elevatedProcess.ExitCode
|
||||
}
|
||||
|
||||
# --- Paths
|
||||
$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"
|
||||
|
||||
function Stop-WowRuntime {
|
||||
param([Parameter(Mandatory=$true)][string]$ClientPath)
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\'
|
||||
$runtimeProcesses = @(Get-Process -Name "Wow", "WarcraftXLHost" -ErrorAction SilentlyContinue | Where-Object {
|
||||
try {
|
||||
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
$false
|
||||
}
|
||||
})
|
||||
|
||||
if (-not $runtimeProcesses.Count) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping the running WoW client and WarcraftXL host..."
|
||||
$runtimeProcesses | Stop-Process -Force
|
||||
|
||||
try {
|
||||
$runtimeProcesses | Wait-Process -Timeout 10 -ErrorAction Stop
|
||||
} catch {
|
||||
$stillRunning = @($runtimeProcesses | Where-Object { -not $_.HasExited })
|
||||
if ($stillRunning.Count) {
|
||||
$processNames = ($stillRunning | ForEach-Object { "$($_.ProcessName) (PID $($_.Id))" }) -join ", "
|
||||
throw "Could not stop processes that lock WarcraftXL files: $processNames"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Ensure tool exists
|
||||
if (!(Test-Path $TOOL)) {
|
||||
@@ -47,7 +120,7 @@ if (!(Test-Path $TOOL)) {
|
||||
|
||||
# --- Build MPQ archives into dist/
|
||||
Write-Host "Building MPQ archives from src/ -> dist/..."
|
||||
& $TOOL build-mpq $SRC_DIR $DIST_DIR
|
||||
& $TOOL $SRC_DIR $DIST_DIR
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "MPQ build failed!"
|
||||
exit 1
|
||||
@@ -59,6 +132,17 @@ 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.
|
||||
Stop-WowRuntime -ClientPath $WOW_HOME
|
||||
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
|
||||
@@ -70,6 +154,21 @@ if ($LASTEXITCODE -ge 8) {
|
||||
|
||||
Write-Host "MPQ archives deployed to $WOW_HOME"
|
||||
|
||||
# --- Write realmlist.wtf based on selected environment
|
||||
$realmlist = switch ($Env) {
|
||||
"production" { $env:PRODUCTION_REALMLIST }
|
||||
"ptr" { $env:PTR_REALMLIST }
|
||||
"local" { $env:LOCAL_REALMLIST }
|
||||
}
|
||||
|
||||
if ($realmlist) {
|
||||
$realmlistPath = Join-Path $WOW_HOME "Data\ruRU\realmlist.wtf"
|
||||
Set-Content -Path $realmlistPath -Value "set realmlist $realmlist" -Encoding ASCII
|
||||
Write-Host "Realmlist ($Env): $realmlist"
|
||||
} else {
|
||||
Write-Warning "REALMLIST for '$Env' is not set in .env -- skipping realmlist.wtf"
|
||||
}
|
||||
|
||||
# --- Run WoW reload script
|
||||
Write-Host "Launching WoW..."
|
||||
cmd /c $RELOAD_SCRIPT
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"fileDataID": 3730958,
|
||||
"textures": [
|
||||
{
|
||||
"fileDataID": 3732460,
|
||||
"file": "catslime_3732460.blp"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732461,
|
||||
"file": "catslime_3732461.blp"
|
||||
}
|
||||
],
|
||||
"skins": [
|
||||
{
|
||||
"fileDataID": 3732472,
|
||||
"file": "catslime00.skin"
|
||||
}
|
||||
],
|
||||
"lodSkins": [
|
||||
{
|
||||
"fileDataID": 3732473,
|
||||
"file": "catslime_lod01.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732474,
|
||||
"file": "catslime_lod02.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732475,
|
||||
"file": "catslime_lod03.skin"
|
||||
}
|
||||
],
|
||||
"anims": [
|
||||
{
|
||||
"fileDataID": 3732462,
|
||||
"file": "catslime0097-00.anim",
|
||||
"animID": 97,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732463,
|
||||
"file": "catslime0096-00.anim",
|
||||
"animID": 96,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732464,
|
||||
"file": "catslime0098-00.anim",
|
||||
"animID": 98,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732465,
|
||||
"file": "catslime0100-00.anim",
|
||||
"animID": 100,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732466,
|
||||
"file": "catslime0099-00.anim",
|
||||
"animID": 99,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732467,
|
||||
"file": "catslime0101-00.anim",
|
||||
"animID": 101,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732468,
|
||||
"file": "catslime0069-00.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732469,
|
||||
"file": "catslime0123-00.anim",
|
||||
"animID": 123,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732470,
|
||||
"file": "catslime0061-00.anim",
|
||||
"animID": 61,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732471,
|
||||
"file": "catslime0075-00.anim",
|
||||
"animID": 75,
|
||||
"subAnimID": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ FACTION_BACKDROP_COLOR_TABLE = {
|
||||
["Alliance"] = {0.5, 0.5, 0.5, 0.09, 0.09, 0.19},
|
||||
["Horde"] = {0.5, 0.2, 0.2, 0.19, 0.05, 0.05},
|
||||
};
|
||||
FRAMES_TO_BACKDROP_COLOR = {
|
||||
FRAMES_TO_BACKDROP_COLOR = {
|
||||
"CharacterCreateCharacterRace",
|
||||
"CharacterCreateCharacterClass",
|
||||
-- "CharacterCreateCharacterFaction",
|
||||
@@ -36,11 +36,11 @@ RACE_ICON_TCOORDS = {
|
||||
["NIGHTELF_MALE"] = {0.126953125, 0.189453125, 0.000000000, 0.125953125},
|
||||
["DRAENEI_MALE"] = {0.255859375, 0.318359375, 0.000000000, 0.125953125},
|
||||
|
||||
["HUMAN_FEMALE"] = {0.000000000, 0.062500000, 0.128906250, 0.253906250},
|
||||
["DWARF_FEMALE"] = {0.063476563, 0.125976563, 0.128906250, 0.253906250},
|
||||
["GNOME_FEMALE"] = {0.192382813, 0.254882813, 0.128906250, 0.253906250},
|
||||
["NIGHTELF_FEMALE"] = {0.127441406, 0.189941406, 0.128906250, 0.253906250},
|
||||
["DRAENEI_FEMALE"] = {0.256347656, 0.318359375, 0.128906250, 0.253906250},
|
||||
["HUMAN_FEMALE"] = {0.000000000, 0.062500000, 0.128906250, 0.253906250},
|
||||
["DWARF_FEMALE"] = {0.063476563, 0.125976563, 0.128906250, 0.253906250},
|
||||
["GNOME_FEMALE"] = {0.192382813, 0.254882813, 0.128906250, 0.253906250},
|
||||
["NIGHTELF_FEMALE"] = {0.127441406, 0.189941406, 0.128906250, 0.253906250},
|
||||
["DRAENEI_FEMALE"] = {0.256347656, 0.318359375, 0.128906250, 0.253906250},
|
||||
|
||||
["ORC_MALE"] = {0.062011719, 0.000000000, 0.262695313, 0.384765625},
|
||||
["SCOURGE_MALE"] = {0.125976563, 0.063964844, 0.262695313, 0.384765625},
|
||||
@@ -48,10 +48,10 @@ RACE_ICON_TCOORDS = {
|
||||
["TROLL_MALE"] = {0.253906250, 0.192382813, 0.262695313, 0.384765625},
|
||||
["BLOODELF_MALE"] = {0.317871094, 0.256835938, 0.262695313, 0.384765625},
|
||||
|
||||
["ORC_FEMALE"] = {0.060546875, 0.000000000, 0.391601563, 0.512695313},
|
||||
["SCOURGE_FEMALE"] = {0.124023438, 0.063476563, 0.391601563, 0.512695313},
|
||||
["TAUREN_FEMALE"] = {0.187988281, 0.127441406, 0.391601563, 0.512695313},
|
||||
["TROLL_FEMALE"] = {0.252441406, 0.191894531, 0.391601563, 0.512695313},
|
||||
["ORC_FEMALE"] = {0.060546875, 0.000000000, 0.391601563, 0.512695313},
|
||||
["SCOURGE_FEMALE"] = {0.124023438, 0.063476563, 0.391601563, 0.512695313},
|
||||
["TAUREN_FEMALE"] = {0.187988281, 0.127441406, 0.391601563, 0.512695313},
|
||||
["TROLL_FEMALE"] = {0.252441406, 0.191894531, 0.391601563, 0.512695313},
|
||||
["BLOODELF_FEMALE"] = {0.316894531, 0.256835938, 0.391601563, 0.512695313},
|
||||
};
|
||||
CLASS_ICON_TCOORDS = {
|
||||
@@ -137,23 +137,23 @@ local function HideAllTooltips()
|
||||
if AllianceTooltip then
|
||||
AllianceTooltip:Hide()
|
||||
end
|
||||
|
||||
|
||||
if HordeTooltip then
|
||||
HordeTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local backdrop = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
||||
};
|
||||
|
||||
local Backdrop2 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\ui-tooltip-border-maw",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\ui-tooltip-border-maw",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
||||
};
|
||||
|
||||
@@ -164,10 +164,10 @@ local function GetOrCreateRaceTooltip(button)
|
||||
tooltip:SetSize(280, 150)
|
||||
tooltip:SetFrameStrata("TOOLTIP")
|
||||
tooltip:SetBackdropColor(0, 0, 0, 1)
|
||||
|
||||
|
||||
local raceID = button:GetID()
|
||||
local faction = _G.GetFactionForRaceID(raceID)
|
||||
|
||||
|
||||
if faction == "Alliance" then
|
||||
tooltip:SetPoint("LEFT", CharacterCreateFrame, "LEFT", 160, 50)
|
||||
elseif faction == "Horde" then
|
||||
@@ -194,7 +194,7 @@ local function GetOrCreateRaceTooltip(button)
|
||||
tooltip.rightClickText:SetPoint("TOP", tooltip.detailsText, "BOTTOM", 0, -10)
|
||||
tooltip.rightClickText:SetTextColor(0.8, 0.8, 0.8)
|
||||
tooltip.rightClickText:SetJustifyH("CENTER")
|
||||
|
||||
|
||||
raceTooltips[button] = tooltip
|
||||
end
|
||||
return raceTooltips[button]
|
||||
@@ -217,7 +217,7 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
|
||||
tooltip.text:ClearAllPoints()
|
||||
tooltip.text:SetPoint("TOPLEFT", tooltip, "TOPLEFT", 10, -5)
|
||||
|
||||
|
||||
tooltip.detailsText:ClearAllPoints()
|
||||
tooltip.detailsText:SetPoint("TOPLEFT", tooltip.text, "BOTTOMLEFT", 0, -5)
|
||||
|
||||
@@ -271,7 +271,7 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
|
||||
local containerHeight = tooltip["spellIcon"..i]:GetHeight() + tooltip["spellDesc"..i]:GetHeight() + 3
|
||||
tooltip["spellContainer"..i]:SetHeight(containerHeight)
|
||||
|
||||
|
||||
lastElement = tooltip["spellContainer"..i]
|
||||
end
|
||||
end
|
||||
@@ -308,10 +308,10 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
end
|
||||
end
|
||||
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.rightClickText:GetHeight() + 25
|
||||
|
||||
|
||||
tooltip:SetHeight(baseHeight + heightToAdd)
|
||||
detailedRaceTooltips[button] = toggleDetails
|
||||
tooltip:ClearAllPoints()
|
||||
@@ -346,7 +346,7 @@ local function GetOrCreateClassTooltip(button)
|
||||
tooltip.Roles:SetWidth(284)
|
||||
tooltip.Roles:SetWordWrap(true)
|
||||
tooltip.Roles:SetJustifyH("LEFT")
|
||||
|
||||
|
||||
classTooltips[button] = tooltip
|
||||
end
|
||||
|
||||
@@ -370,7 +370,7 @@ local function GetValidRacesForClass(classID)
|
||||
table.insert(hordeRaces, raceName)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return allianceRaces, hordeRaces
|
||||
end
|
||||
local function UpdateClassTooltip(button)
|
||||
@@ -382,7 +382,7 @@ local function UpdateClassTooltip(button)
|
||||
|
||||
local buttonX, buttonY = button:GetCenter()
|
||||
local parentX, parentY = CharacterCreateFrame:GetCenter()
|
||||
|
||||
|
||||
if buttonX > parentX then
|
||||
tooltip:SetPoint("BOTTOMRIGHT", button, "TOPRIGHT", 0, 10)
|
||||
else
|
||||
@@ -394,7 +394,7 @@ local function UpdateClassTooltip(button)
|
||||
|
||||
tooltip.text:ClearAllPoints()
|
||||
tooltip.text:SetPoint("TOPLEFT", tooltip, "TOPLEFT", 8, -8)
|
||||
|
||||
|
||||
tooltip.detailsText:ClearAllPoints()
|
||||
tooltip.detailsText:SetPoint("TOPLEFT", tooltip.text, "BOTTOMLEFT", 0, -5)
|
||||
|
||||
@@ -405,7 +405,7 @@ local function UpdateClassTooltip(button)
|
||||
tooltip.RestrictionText = tooltip.RestrictionText or tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.RestrictionText:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
|
||||
tooltip.RestrictionText:SetJustifyH("LEFT")
|
||||
|
||||
|
||||
tooltip.FactionText = tooltip.FactionText or tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.FactionText:SetFont("Fonts\\FRIZQT__.TTF", 11)
|
||||
tooltip.FactionText:SetWordWrap(true)
|
||||
@@ -416,12 +416,12 @@ local function UpdateClassTooltip(button)
|
||||
coloredRoles = string.gsub(coloredRoles, "Дальний бой", "|cffff2020Дальний бой|r")
|
||||
coloredRoles = string.gsub(coloredRoles, "Танк", "|cff0070ddТанк|r")
|
||||
coloredRoles = string.gsub(coloredRoles, "Лекарь", "|cff20c000Лекарь|r")
|
||||
|
||||
|
||||
if not tooltip.Roles then
|
||||
tooltip.Roles = tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.Roles:SetFont("Fonts\\FRIZQT__.TTF", 11)
|
||||
end
|
||||
|
||||
|
||||
tooltip.Roles:SetText("|cFFFFFFFF"..FUNTION_INF.."|r\n\n "..coloredRoles)
|
||||
|
||||
if showRestriction then
|
||||
@@ -435,20 +435,20 @@ local function UpdateClassTooltip(button)
|
||||
tooltip.RestrictionText:Show()
|
||||
|
||||
local allianceRaces, hordeRaces = GetValidRacesForClass(classID)
|
||||
|
||||
|
||||
local factionText = ""
|
||||
|
||||
|
||||
if #allianceRaces > 0 then
|
||||
factionText = ALLIANCE_RACE .. " |cFFFFFFFF" .. table.concat(allianceRaces, ", ") .. "|r"
|
||||
end
|
||||
|
||||
|
||||
if #hordeRaces > 0 then
|
||||
if factionText ~= "" then
|
||||
factionText = factionText .. "\n\n"
|
||||
end
|
||||
factionText = factionText .. "\n" .. HORDE_RACE .. " |cFFFFFFFF" .. table.concat(hordeRaces, ", ") .. "|r"
|
||||
end
|
||||
|
||||
|
||||
tooltip.FactionText:SetPoint("TOPLEFT", tooltip.RestrictionText, "BOTTOMLEFT", 0, -15)
|
||||
tooltip.FactionText:SetTextColor(1, 0.82, 0)
|
||||
tooltip.FactionText:SetText(factionText)
|
||||
@@ -459,18 +459,18 @@ local function UpdateClassTooltip(button)
|
||||
maxWidth = math.max(maxWidth, tooltip.RestrictionText:GetWidth())
|
||||
maxWidth = math.max(maxWidth, tooltip.FactionText:GetWidth())
|
||||
maxWidth = math.max(280, math.min(maxWidth + 16, 400))
|
||||
|
||||
|
||||
tooltip:SetWidth(maxWidth)
|
||||
tooltip.FactionText:SetWidth(maxWidth - 16)
|
||||
|
||||
local restrictionHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
|
||||
local restrictionHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.Roles:GetHeight() +
|
||||
tooltip.RestrictionText:GetHeight() +
|
||||
tooltip.RestrictionText:GetHeight() +
|
||||
tooltip.FactionText:GetHeight() + 80
|
||||
|
||||
|
||||
tooltip:SetHeight(restrictionHeight)
|
||||
|
||||
|
||||
else
|
||||
tooltip.RestrictionText:Hide()
|
||||
tooltip.FactionText:Hide()
|
||||
@@ -484,10 +484,10 @@ local function UpdateClassTooltip(button)
|
||||
maxWidth = math.max(280, math.min(maxWidth + 16, 400))
|
||||
tooltip:SetWidth(maxWidth)
|
||||
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.Roles:GetHeight() + 30
|
||||
|
||||
|
||||
tooltip:SetHeight(baseHeight)
|
||||
end
|
||||
end
|
||||
@@ -498,7 +498,7 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
if button then
|
||||
local normalTex = _G[button:GetName().."NormalTexture"]
|
||||
local pushedTex = _G[button:GetName().."PushedTexture"]
|
||||
|
||||
|
||||
if normalTex then
|
||||
normalTex:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
@@ -513,7 +513,7 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
if button then
|
||||
local normalTex = _G[button:GetName().."NormalTexture"]
|
||||
local pushedTex = _G[button:GetName().."PushedTexture"]
|
||||
|
||||
|
||||
if normalTex then
|
||||
normalTex:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
@@ -525,12 +525,12 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale
|
||||
local femaleButton = CharacterCreateGenderButtonFemale
|
||||
|
||||
|
||||
if maleButton then
|
||||
_G[maleButton:GetName().."NormalTexture"]:SetDrawLayer("BACKGROUND")
|
||||
_G[maleButton:GetName().."PushedTexture"]:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
|
||||
|
||||
if femaleButton then
|
||||
_G[femaleButton:GetName().."NormalTexture"]:SetDrawLayer("BACKGROUND")
|
||||
_G[femaleButton:GetName().."PushedTexture"]:SetDrawLayer("BACKGROUND")
|
||||
@@ -558,7 +558,7 @@ local function CreateFactionTooltip(parent, factionName)
|
||||
tooltip.description:SetWordWrap(true)
|
||||
tooltip.description:SetJustifyH("LEFT")
|
||||
tooltip.description:SetTextColor(0.9, 0.9, 0.9)
|
||||
|
||||
|
||||
return tooltip
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ local function UpdateTooltipSize(tooltip)
|
||||
local titleHeight = tooltip.title:GetHeight()
|
||||
local descHeight = tooltip.description:GetHeight()
|
||||
local totalHeight = titleHeight + descHeight + 30
|
||||
|
||||
|
||||
tooltip:SetHeight(totalHeight)
|
||||
tooltip:SetWidth(300)
|
||||
end
|
||||
@@ -580,7 +580,7 @@ function CharacterCreate_PositionRaceButtons()
|
||||
local button = _G["CharacterCreateRaceButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 1 then
|
||||
button:SetPoint("TOP", AllianceLogoFrame, "BOTTOM", -horizontalOffset, verticalStart)
|
||||
else
|
||||
@@ -594,7 +594,7 @@ function CharacterCreate_PositionRaceButtons()
|
||||
local button = _G["CharacterCreateRaceButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 6 then
|
||||
button:SetPoint("TOP", HordeLogoFrame, "BOTTOM", horizontalOffset, verticalStart)
|
||||
else
|
||||
@@ -607,12 +607,12 @@ end
|
||||
|
||||
function CharacterCreate_PositionClassButtons()
|
||||
local buttonSpacing = 80
|
||||
|
||||
|
||||
for i = 1, MAX_CLASSES_PER_RACE do
|
||||
local button = _G["CharacterCreateClassButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 1 then
|
||||
button:SetPoint("CENTER", CharacterCreateFrame, "BOTTOM", -360, 80)
|
||||
else
|
||||
@@ -624,16 +624,16 @@ function CharacterCreate_PositionClassButtons()
|
||||
end
|
||||
|
||||
function CharacterCreate_PositionGenderButtons()
|
||||
local genderSpacing = 310
|
||||
|
||||
local genderSpacing = 310
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale
|
||||
local femaleButton = CharacterCreateGenderButtonFemale
|
||||
|
||||
|
||||
if maleButton then
|
||||
maleButton:ClearAllPoints()
|
||||
maleButton:SetPoint("CENTER", CharacterCreateFrame, "CENTER", -(genderSpacing/2), -250)
|
||||
end
|
||||
|
||||
|
||||
if femaleButton then
|
||||
femaleButton:ClearAllPoints()
|
||||
femaleButton:SetPoint("CENTER", CharacterCreateFrame, "CENTER", (genderSpacing/2), -250)
|
||||
@@ -676,6 +676,82 @@ 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
|
||||
|
||||
-- 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 frame.
|
||||
-- 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 +777,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
end
|
||||
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||
|
||||
if showGameModes then
|
||||
@@ -778,7 +856,7 @@ function CharacterCreate_OnLoad(self)
|
||||
self:SetSequence(0)
|
||||
self:SetCamera(0)
|
||||
SetCharCustomizeFrame("CharacterCreate")
|
||||
|
||||
|
||||
CharacterCreate.numRaces = 0
|
||||
CharacterCreate.selectedRace = 0
|
||||
CharacterCreate.numClasses = 0
|
||||
@@ -820,7 +898,7 @@ function CharacterCreate_OnLoad(self)
|
||||
AllianceLogoFrame:SetSize(100, 100)
|
||||
AllianceLogoFrame:SetPoint("TOPLEFT", -16, 16)
|
||||
AllianceLogoFrame:EnableMouse(true)
|
||||
|
||||
|
||||
AllianceLogoFrame:SetScript("OnEnter", function(self)
|
||||
AllianceTooltip.title:SetText(ALLIANCE)
|
||||
AllianceTooltip.description:SetText(FACTION_ALLIANCE_DESCRIPTION)
|
||||
@@ -829,7 +907,7 @@ function CharacterCreate_OnLoad(self)
|
||||
AllianceTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 90, 60)
|
||||
AllianceTooltip:Show()
|
||||
end)
|
||||
|
||||
|
||||
AllianceLogoFrame:SetScript("OnLeave", function(self)
|
||||
AllianceTooltip:Hide()
|
||||
end)
|
||||
@@ -850,7 +928,7 @@ function CharacterCreate_OnLoad(self)
|
||||
HordeLogoFrame:SetSize(100, 100)
|
||||
HordeLogoFrame:SetPoint("TOPRIGHT", 16, 16)
|
||||
HordeLogoFrame:EnableMouse(true)
|
||||
|
||||
|
||||
HordeLogoFrame:SetScript("OnEnter", function(self)
|
||||
HordeTooltip.title:SetText(HORDE)
|
||||
HordeTooltip.description:SetText(FACTION_HORDE_DESCRIPTION)
|
||||
@@ -859,7 +937,7 @@ function CharacterCreate_OnLoad(self)
|
||||
HordeTooltip:SetPoint("TOPRIGHT", self, "BOTTOMRIGHT", -90, 60)
|
||||
HordeTooltip:Show()
|
||||
end)
|
||||
|
||||
|
||||
HordeLogoFrame:SetScript("OnLeave", function(self)
|
||||
HordeTooltip:Hide()
|
||||
end)
|
||||
@@ -873,7 +951,7 @@ function CharacterCreate_SetupCustomButtons()
|
||||
{button = CharCreateRandomizeButton, width = 36, height = 36},
|
||||
{button = CharacterCreateRandomName, width = 30, height = 30}
|
||||
}
|
||||
|
||||
|
||||
local texturePath = "Interface\\Glues\\CharacterCreate\\charactercreate"
|
||||
|
||||
for _, btnInfo in ipairs(buttons) do
|
||||
@@ -943,6 +1021,7 @@ function CharacterCreate_TogglePersonalization()
|
||||
CharCreatePersonalizeButton:Show();
|
||||
CharCreateOkayButton:Hide();
|
||||
CharacterCreate_UpdateGameModeVisibility();
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
@@ -961,7 +1040,7 @@ end)
|
||||
|
||||
function CharacterCreate_OnShow()
|
||||
CharacterCreate.personalizationMode = false;
|
||||
|
||||
|
||||
for i=1, MAX_CLASSES_PER_RACE, 1 do
|
||||
local button = _G["CharacterCreateClassButton"..i];
|
||||
button:Enable();
|
||||
@@ -985,7 +1064,7 @@ function CharacterCreate_OnShow()
|
||||
CharacterCreate.personalizationMode = false;
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
CharCreateOkayButton:Hide();
|
||||
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
end
|
||||
@@ -1001,7 +1080,7 @@ function CharacterCreate_OnShow()
|
||||
CharacterChangeFixup();
|
||||
CharacterCreate_CreateGenderButtonTextures();
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
CharacterCreate_ResetState();
|
||||
CharacterCreate_ResetState(true);
|
||||
|
||||
hideScheduled = true
|
||||
HideNameEditFrame:Show()
|
||||
@@ -1013,7 +1092,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
|
||||
@@ -1072,11 +1151,11 @@ function CharacterCreateEnumerateRaces(...)
|
||||
local raceID = index
|
||||
local faction = _G.GetFactionForRaceID(raceID)
|
||||
local borderColor
|
||||
|
||||
|
||||
if faction == "Alliance" then
|
||||
borderColor = {0.0, 0.4, 1.0}
|
||||
else
|
||||
borderColor = {1.0, 0.0, 0.0}
|
||||
borderColor = {1.0, 0.0, 0.0}
|
||||
end
|
||||
|
||||
if not button.staticTexture then
|
||||
@@ -1114,10 +1193,10 @@ function CharacterCreateEnumerateRaces(...)
|
||||
if not button.texturesMoved then
|
||||
button:SetScript("OnMouseDown", nil);
|
||||
button:SetScript("OnMouseUp", nil);
|
||||
|
||||
|
||||
button.texturesMoved = true;
|
||||
end
|
||||
|
||||
|
||||
button:Show();
|
||||
if ( select(i+2, ...) == 1 ) then
|
||||
button.enable = true;
|
||||
@@ -1139,7 +1218,7 @@ function CharacterCreateEnumerateRaces(...)
|
||||
tooltip:Hide()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
button:SetScript("OnMouseDown", function(self, clickedButton)
|
||||
if self:IsEnabled() and clickedButton == "RightButton" then
|
||||
local tooltip = GetOrCreateRaceTooltip(self)
|
||||
@@ -1205,7 +1284,7 @@ function CharacterCreateEnumerateClasses(...)
|
||||
button.nameFrame = CreateFrame("Frame", nil, button);
|
||||
button.nameFrame:SetSize(112, 40);
|
||||
button.nameFrame:SetPoint("TOP", button, "BOTTOM", 0, -5);
|
||||
|
||||
|
||||
button.nameFrame.text = button.nameFrame:CreateFontString(nil, "OVERLAY");
|
||||
button.nameFrame.text:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE");
|
||||
button.nameFrame.text:SetPoint("CENTER", 0, 0);
|
||||
@@ -1220,10 +1299,10 @@ function CharacterCreateEnumerateClasses(...)
|
||||
if not button.texturesMoved then
|
||||
button:SetScript("OnMouseDown", nil);
|
||||
button:SetScript("OnMouseUp", nil);
|
||||
|
||||
|
||||
button.texturesMoved = true;
|
||||
end
|
||||
|
||||
|
||||
button:Show();
|
||||
|
||||
local className = select(i, ...);
|
||||
@@ -1270,7 +1349,7 @@ function CharacterCreateEnumerateClasses(...)
|
||||
tooltip:Hide()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
button:SetScript("OnMouseDown", function(self, clickedButton)
|
||||
if self:IsEnabled() and clickedButton == "RightButton" then
|
||||
local tooltip = GetOrCreateClassTooltip(self)
|
||||
@@ -1315,14 +1394,14 @@ function CharacterCreate_CreateGenderButtonTextures()
|
||||
maleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
maleButton.staticTexture:SetSize(86, 86);
|
||||
maleButton.staticTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
maleButton.highlightTexture = maleButton:CreateTexture(maleButton:GetName().."HighlightTexture", "HIGHLIGHT");
|
||||
maleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
maleButton.highlightTexture:SetAlpha(0.5);
|
||||
maleButton.highlightTexture:SetBlendMode("ADD");
|
||||
maleButton.highlightTexture:SetSize(86, 86);
|
||||
maleButton.highlightTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
maleButton.checkedTexture = maleButton:CreateTexture(maleButton:GetName().."CheckedTexture", "OVERLAY");
|
||||
maleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
|
||||
maleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
|
||||
@@ -1341,14 +1420,14 @@ function CharacterCreate_CreateGenderButtonTextures()
|
||||
femaleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
femaleButton.staticTexture:SetSize(86, 86);
|
||||
femaleButton.staticTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
femaleButton.highlightTexture = femaleButton:CreateTexture(femaleButton:GetName().."HighlightTexture", "HIGHLIGHT");
|
||||
femaleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
femaleButton.highlightTexture:SetAlpha(0.5);
|
||||
femaleButton.highlightTexture:SetBlendMode("ADD");
|
||||
femaleButton.highlightTexture:SetSize(86, 86);
|
||||
femaleButton.highlightTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
femaleButton.checkedTexture = femaleButton:CreateTexture(femaleButton:GetName().."CheckedTexture", "OVERLAY");
|
||||
femaleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
|
||||
femaleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
|
||||
@@ -1439,7 +1518,7 @@ function SetCharacterClass(id)
|
||||
end
|
||||
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
|
||||
|
||||
local className, classFileName, _, tank, healer, damage = GetSelectedClass();
|
||||
local abilityIndex = 0;
|
||||
local tempText = _G["CLASS_INFO_"..classFileName..abilityIndex];
|
||||
@@ -1452,7 +1531,7 @@ function SetCharacterClass(id)
|
||||
local coords = CLASS_ICON_TCOORDS[classFileName];
|
||||
CharacterCreateClassIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
|
||||
CharacterCreateClassLabel:SetText(className);
|
||||
CharacterCreateClassRolesText:SetText(abilityText);
|
||||
CharacterCreateClassRolesText:SetText(abilityText);
|
||||
CharacterCreateClassText:SetText(GetFlavorText("CLASS_"..strupper(classFileName), GetSelectedSex()).."|n|n");
|
||||
CharacterCreateClassScrollFrameScrollBar:SetValue(0);
|
||||
end
|
||||
@@ -1488,7 +1567,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale;
|
||||
local femaleButton = CharacterCreateGenderButtonFemale;
|
||||
|
||||
|
||||
if maleButton and maleButton.checkedTexture then
|
||||
if maleButton:GetChecked() then
|
||||
maleButton.checkedTexture:Show();
|
||||
@@ -1496,7 +1575,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
|
||||
maleButton.checkedTexture:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if femaleButton and femaleButton.checkedTexture then
|
||||
if femaleButton:GetChecked() then
|
||||
femaleButton.checkedTexture:Show();
|
||||
@@ -1518,6 +1597,7 @@ end
|
||||
|
||||
function CharacterCreate_UpdateModel(self)
|
||||
UpdateCustomizationScene();
|
||||
CharacterCreate_UpdateCamera();
|
||||
self:AdvanceTime();
|
||||
end
|
||||
|
||||
@@ -1599,7 +1679,7 @@ function CharacterRace_OnClick(self, id)
|
||||
SetCharacterClass(classIndex);
|
||||
|
||||
CharacterCreate_UpdateHairCustomization();
|
||||
|
||||
|
||||
CharacterChangeFixup();
|
||||
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
@@ -1624,7 +1704,7 @@ function SetCharacterGender(sex)
|
||||
CharacterCreateEnumerateRaces(GetAvailableRaces());
|
||||
CharacterCreateEnumerateClasses(GetAvailableClasses());
|
||||
SetCharacterRace(GetSelectedRace());
|
||||
|
||||
|
||||
local _,_,classIndex = GetSelectedClass();
|
||||
if ( PAID_SERVICE_TYPE ) then
|
||||
classIndex = PaidChange_GetCurrentClassIndex();
|
||||
@@ -1638,15 +1718,16 @@ function SetCharacterGender(sex)
|
||||
fileString = strupper(fileString);
|
||||
local coords = RACE_ICON_TCOORDS[fileString.."_"..gender];
|
||||
CharacterCreateRaceIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
|
||||
|
||||
|
||||
CharacterChangeFixup();
|
||||
|
||||
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();
|
||||
@@ -1664,7 +1745,7 @@ function CharacterCreate_ResetState()
|
||||
CharCreatePersonalizeButton:Show();
|
||||
CharCreateOkayButton:Hide();
|
||||
CharacterCreate_UpdateGameModeVisibility();
|
||||
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
end
|
||||
@@ -1710,7 +1791,7 @@ end
|
||||
function CharacterCreate_UpdateHairCustomization()
|
||||
CharacterCustomizationButtonFrame3Text:SetText(_G["HAIR_"..GetHairCustomization().."_STYLE"]);
|
||||
CharacterCustomizationButtonFrame4Text:SetText(_G["HAIR_"..GetHairCustomization().."_COLOR"]);
|
||||
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
|
||||
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
|
||||
end
|
||||
|
||||
function SetButtonDesaturated(button, desaturated, r, g, b)
|
||||
@@ -1732,7 +1813,7 @@ function SetButtonDesaturated(button, desaturated, r, g, b)
|
||||
g = 0.5;
|
||||
b = 0.5;
|
||||
end
|
||||
|
||||
|
||||
icon:SetVertexColor(r, g, b);
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -69,25 +69,26 @@ local MainMenuBarBackpackButton = _G.MainMenuBarBackpackButton;
|
||||
local HelpMicroButton = _G.HelpMicroButton;
|
||||
local KeyRingButton = _G.KeyRingButton;
|
||||
|
||||
-- Button collections (dynamically set based on server)
|
||||
local MICRO_BUTTONS
|
||||
-- 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
|
||||
|
||||
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 = {
|
||||
return {
|
||||
_G.CharacterMicroButton,
|
||||
_G.SpellbookMicroButton,
|
||||
_G.TalentMicroButton,
|
||||
@@ -95,6 +96,7 @@ else
|
||||
_G.QuestLogMicroButton,
|
||||
_G.SocialsMicroButton,
|
||||
_G.LFDMicroButton,
|
||||
_G.EncounterJournalMicroButton,
|
||||
_G.CollectionsMicroButton,
|
||||
_G.PVPMicroButton,
|
||||
_G.MainMenuMicroButton,
|
||||
@@ -102,6 +104,7 @@ else
|
||||
}
|
||||
end
|
||||
|
||||
local MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
local bagslots = {_G.CharacterBag0Slot, _G.CharacterBag1Slot, _G.CharacterBag2Slot, _G.CharacterBag3Slot};
|
||||
|
||||
@@ -783,6 +786,8 @@ local function ApplyMicromenuSystem()
|
||||
return
|
||||
end
|
||||
|
||||
MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
-- Store original states first
|
||||
StoreOriginalMicroButtonStates()
|
||||
|
||||
@@ -911,6 +916,8 @@ 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
|
||||
@@ -919,6 +926,152 @@ 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,
|
||||
@@ -1582,6 +1735,8 @@ local function ApplyMicromenuSystem()
|
||||
end
|
||||
|
||||
local function setupMicroButtons(xOffset)
|
||||
MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
local buttonxOffset = 0
|
||||
|
||||
local useGrayscale = addon.db.profile.micromenu.grayscale_icons
|
||||
@@ -1705,9 +1860,11 @@ local function ApplyMicromenuSystem()
|
||||
|
||||
local isCharacterButton = (buttonName == "Character")
|
||||
local isPVPButton = (buttonName == "PVP")
|
||||
local isEncounterJournalButton = (buttonName == "EncounterJournal")
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
if shouldUseGrayscale then
|
||||
-- Grayscale icons
|
||||
@@ -1741,6 +1898,8 @@ local function ApplyMicromenuSystem()
|
||||
end
|
||||
elseif isPVPButton then
|
||||
SetupPVPButton(button)
|
||||
elseif isEncounterJournalButton then
|
||||
SetupEncounterJournalButton(button)
|
||||
elseif isCharacterButton then
|
||||
SetupCharacterButton(button)
|
||||
else
|
||||
@@ -2245,6 +2404,8 @@ 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)
|
||||
+4172
File diff suppressed because it is too large
Load Diff
+3380
File diff suppressed because it is too large
Load Diff
+605
@@ -0,0 +1,605 @@
|
||||
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 scrollFrameOnMouseWheel(scrollFrame, delta)
|
||||
if not scrollFrame then return end
|
||||
|
||||
if ScrollFrameTemplate_OnMouseWheel then
|
||||
ScrollFrameTemplate_OnMouseWheel(scrollFrame, delta)
|
||||
return
|
||||
end
|
||||
|
||||
local scrollBar = getScrollBar(scrollFrame)
|
||||
if scrollBar then
|
||||
local minValue, maxValue = scrollBar:GetMinMaxValues()
|
||||
local step = scrollBar:GetValueStep() or 20
|
||||
scrollBar:SetValue(math.min(maxValue, math.max(minValue, scrollBar:GetValue() - delta * step)))
|
||||
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 resolveEncounterJournalInstanceID
|
||||
|
||||
local function getRecommendationArtwork(data)
|
||||
if type(data) ~= "table" then return nil end
|
||||
|
||||
local artwork = data.artwork or data.cover or data.coverImage or data.buttonImage or data.bgImage or data.icon
|
||||
if artwork and artwork ~= "" then return artwork end
|
||||
|
||||
if not resolveEncounterJournalInstanceID or not EJ_GetInstanceInfo then return nil end
|
||||
local instanceID = resolveEncounterJournalInstanceID(data)
|
||||
if not instanceID then return nil end
|
||||
|
||||
local ok, _, _, bgImage, buttonImage, loreImage, buttonSmallImage = pcall(EJ_GetInstanceInfo, instanceID)
|
||||
if ok then
|
||||
return buttonImage or bgImage or loreImage or buttonSmallImage
|
||||
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"
|
||||
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
|
||||
|
||||
local artwork = getRecommendationArtwork(r)
|
||||
card.Artwork:SetTexCoord(0, 0.68359375, 0, 0.7421875)
|
||||
card.Artwork:SetVertexColor(1, 1, 1, 1)
|
||||
if not artwork or not card.Artwork:SetTexture(artwork) then
|
||||
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
|
||||
end
|
||||
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
|
||||
self:EnableMouse(true)
|
||||
self:EnableMouseWheel(true)
|
||||
|
||||
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_OnMouseWheel(self, delta)
|
||||
if self.BodyScroll and self.BodyScroll:IsShown() then
|
||||
scrollFrameOnMouseWheel(self.BodyScroll, delta)
|
||||
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
|
||||
|
||||
local function validEncounterJournalInstanceID(value)
|
||||
local instanceID = tonumber(value)
|
||||
if not instanceID or instanceID <= 0 or not EJ_GetInstanceInfo then return nil end
|
||||
|
||||
local ok, instanceName = pcall(EJ_GetInstanceInfo, instanceID)
|
||||
if ok and instanceName then return instanceID end
|
||||
end
|
||||
|
||||
local function mapIDToEncounterJournalInstanceID(value)
|
||||
local mapID = tonumber(value)
|
||||
if not mapID or mapID <= 0 then return nil end
|
||||
if not C_EncounterJournal or not C_EncounterJournal.GetInstanceIDByMapID then return nil end
|
||||
|
||||
local ok, instanceID = pcall(C_EncounterJournal.GetInstanceIDByMapID, mapID)
|
||||
if ok then
|
||||
return validEncounterJournalInstanceID(instanceID)
|
||||
end
|
||||
end
|
||||
|
||||
function resolveEncounterJournalInstanceID(data)
|
||||
if type(data) == "number" or type(data) == "string" then
|
||||
return validEncounterJournalInstanceID(data) or mapIDToEncounterJournalInstanceID(data)
|
||||
end
|
||||
if type(data) ~= "table" then return nil end
|
||||
|
||||
local instanceID =
|
||||
validEncounterJournalInstanceID(data.ej_instanceID) or
|
||||
validEncounterJournalInstanceID(data.ejInstanceID) or
|
||||
validEncounterJournalInstanceID(data.journalInstanceID) or
|
||||
validEncounterJournalInstanceID(data.instanceID) or
|
||||
validEncounterJournalInstanceID(data.dungeonID) or
|
||||
validEncounterJournalInstanceID(data.targetID)
|
||||
if instanceID then return instanceID end
|
||||
|
||||
return mapIDToEncounterJournalInstanceID(data.mapID) or
|
||||
mapIDToEncounterJournalInstanceID(data.worldMapAreaID) or
|
||||
mapIDToEncounterJournalInstanceID(data.instanceID) or
|
||||
mapIDToEncounterJournalInstanceID(data.dungeonID) or
|
||||
mapIDToEncounterJournalInstanceID(data.targetID)
|
||||
end
|
||||
|
||||
local function requestOpenTarget(data)
|
||||
if type(data) == "table" and data.id and C_PlayerGuide and C_PlayerGuide.RequestOpenTarget then
|
||||
C_PlayerGuide.RequestOpenTarget(data.id)
|
||||
end
|
||||
end
|
||||
|
||||
local function openEncounterJournalInstance(data, encounterID)
|
||||
if not EncounterJournal then return nil end
|
||||
|
||||
local instanceID = resolveEncounterJournalInstanceID(data)
|
||||
if not instanceID then
|
||||
requestOpenTarget(data)
|
||||
return nil
|
||||
end
|
||||
|
||||
if type(data) == "table" and not encounterID then
|
||||
encounterID = data.encounterID
|
||||
end
|
||||
|
||||
if not EncounterJournal:IsShown() then
|
||||
ShowUIPanel(EncounterJournal)
|
||||
end
|
||||
|
||||
if EncounterJournal.encounter then
|
||||
EncounterJournal.encounter:Show()
|
||||
end
|
||||
if EncounterJournal.instanceSelect then
|
||||
EJ_ContentTab_SelectAppropriateInstanceTab(instanceID)
|
||||
end
|
||||
|
||||
if NavBar_Reset and EncounterJournal.navBar then
|
||||
NavBar_Reset(EncounterJournal.navBar)
|
||||
end
|
||||
|
||||
EncounterJournal_DisplayInstance(instanceID)
|
||||
|
||||
if encounterID and encounterID > 0 then
|
||||
EncounterJournal_DisplayEncounter(encounterID)
|
||||
end
|
||||
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
|
||||
openEncounterJournalInstance(o)
|
||||
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 then openEncounterJournalInstance(r) 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,
|
||||
}
|
||||
+490
@@ -0,0 +1,490 @@
|
||||
<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" setAllPoints="true">
|
||||
<Color r="1" g="1" b="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
|
||||
<Texture name="$parentTextBackdrop" parentKey="TextBackdrop" file="Interface\Buttons\WHITE8X8">
|
||||
<Size><AbsDimension x="176" y="42"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM"/>
|
||||
</Anchors>
|
||||
<Color r="0" g="0" b="0" a="0.72"/>
|
||||
</Texture>
|
||||
<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="-82"/></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="-98"/></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"/>
|
||||
<OnMouseWheel function="MW_PlayerGuide_OnMouseWheel"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
+996
@@ -0,0 +1,996 @@
|
||||
-- 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)
|
||||
|
||||
-- The imported Encounter Journal UI uses tooltip helpers introduced after
|
||||
-- 3.3.5. Keep their retail signatures, but implement them with the tooltip
|
||||
-- methods available in the 3.3.5 client.
|
||||
local function TooltipColorRGB(color, fallbackR, fallbackG, fallbackB)
|
||||
if type(color) == "table" then
|
||||
if color.GetRGB then
|
||||
return color:GetRGB()
|
||||
end
|
||||
|
||||
return color.r or fallbackR, color.g or fallbackG, color.b or fallbackB
|
||||
end
|
||||
|
||||
return fallbackR, fallbackG, fallbackB
|
||||
end
|
||||
|
||||
if not GameTooltip_AddColoredLine then
|
||||
function GameTooltip_AddColoredLine(tooltip, text, color, wrap)
|
||||
if not tooltip or not tooltip.AddLine then
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b = TooltipColorRGB(color, 1, 1, 1)
|
||||
tooltip:AddLine(text or "", r, g, b, wrap and true or false)
|
||||
end
|
||||
end
|
||||
|
||||
if not GameTooltip_AddNormalLine then
|
||||
function GameTooltip_AddNormalLine(tooltip, text, wrap)
|
||||
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap)
|
||||
end
|
||||
end
|
||||
|
||||
if not GameTooltip_AddHighlightLine then
|
||||
function GameTooltip_AddHighlightLine(tooltip, text, wrap)
|
||||
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap)
|
||||
end
|
||||
end
|
||||
|
||||
if not GameTooltip_AddBlankLineToTooltip then
|
||||
function GameTooltip_AddBlankLineToTooltip(tooltip)
|
||||
if tooltip and tooltip.AddLine then
|
||||
tooltip:AddLine(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not GameTooltip_SetTitle then
|
||||
function GameTooltip_SetTitle(tooltip, text, color)
|
||||
if not tooltip then
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b = TooltipColorRGB(color or NORMAL_FONT_COLOR, 1, 0.82, 0)
|
||||
if tooltip.SetText then
|
||||
tooltip:SetText(text or "", r, g, b)
|
||||
elseif tooltip.AddLine then
|
||||
tooltip:AddLine(text or "", r, g, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not GameTooltip_ShowDisabledTooltip then
|
||||
function GameTooltip_ShowDisabledTooltip(tooltip, owner, text, anchor, offsetX, offsetY)
|
||||
if not tooltip or not owner then
|
||||
return
|
||||
end
|
||||
|
||||
tooltip:SetOwner(owner, anchor or "ANCHOR_RIGHT", offsetX or 0, offsetY or 0)
|
||||
GameTooltip_AddNormalLine(tooltip, text, true)
|
||||
tooltip:Show()
|
||||
end
|
||||
end
|
||||
|
||||
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"
|
||||
|
||||
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 compactItemsCache = _G.MoonWellEncounterJournalItemCache
|
||||
if type(compactItemsCache) == "table" then
|
||||
itemsCache = itemsCache or {}
|
||||
for itemID, compactData in pairs(compactItemsCache) do
|
||||
if type(itemID) == "number" and type(compactData) == "table" and not itemsCache[itemID] then
|
||||
itemsCache[itemID] = {
|
||||
[ITEM_CACHE_FIELD.NAME_ENGB] = compactData[1],
|
||||
[ITEM_CACHE_FIELD.NAME_RURU] = compactData[2],
|
||||
[ITEM_CACHE_FIELD.RARITY] = compactData[3],
|
||||
[ITEM_CACHE_FIELD.TEXTURE] = compactData[4],
|
||||
}
|
||||
end
|
||||
end
|
||||
_G.ItemsCache = itemsCache
|
||||
_G.MoonWellEncounterJournalItemCache = nil
|
||||
end
|
||||
|
||||
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 = C_Item.GetItemInfoCache(item)
|
||||
if not name and 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
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
-- 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.
|
||||
|
||||
-- These entries belong to the Sirus-specific content pack that was mixed into
|
||||
-- the production export. They are not MoonWell content and must never reach
|
||||
-- the client, including when the journal is refreshed through AIO.
|
||||
local BLOCKED_INSTANCE_IDS = {
|
||||
[773] = true, -- Bronze Sanctum
|
||||
[774] = true, -- Tol'Garod Prison
|
||||
[776] = true, -- Sirus categories
|
||||
[777] = true, -- Sirus world bosses (Norigorn)
|
||||
}
|
||||
|
||||
local function RemoveBlockedInstances(data)
|
||||
local instances = data.JOURNALINSTANCE or data.instances
|
||||
local encounters = data.JOURNALENCOUNTER or data.encounters
|
||||
local creatures = data.JOURNALENCOUNTERCREATURE or data.creatures
|
||||
local items = data.JOURNALENCOUNTERITEM or data.items
|
||||
local sections = data.JOURNALENCOUNTERSECTION or data.sections
|
||||
local tierInstances = data.JOURNALTIERXINSTANCE or data.tierInstances
|
||||
local blockedEncounterIDs = {}
|
||||
|
||||
if type(encounters) == "table" then
|
||||
for instanceID in pairs(BLOCKED_INSTANCE_IDS) do
|
||||
local encounterList = encounters[instanceID]
|
||||
if type(encounterList) == "table" then
|
||||
for _, encounterInfo in ipairs(encounterList) do
|
||||
if type(encounterInfo) == "table" and type(encounterInfo[1]) == "number" then
|
||||
blockedEncounterIDs[encounterInfo[1]] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
encounters[instanceID] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if type(instances) == "table" then
|
||||
for instanceID in pairs(BLOCKED_INSTANCE_IDS) do
|
||||
instances[instanceID] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for encounterID in pairs(blockedEncounterIDs) do
|
||||
if type(creatures) == "table" then
|
||||
creatures[encounterID] = nil
|
||||
end
|
||||
if type(items) == "table" then
|
||||
items[encounterID] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if type(sections) == "table" then
|
||||
for sectionID, sectionInfo in pairs(sections) do
|
||||
if type(sectionInfo) == "table" and blockedEncounterIDs[sectionInfo[7]] then
|
||||
sections[sectionID] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if type(tierInstances) == "table" then
|
||||
for instanceID in pairs(BLOCKED_INSTANCE_IDS) do
|
||||
tierInstances[instanceID] = nil
|
||||
end
|
||||
|
||||
for _, instanceList in pairs(tierInstances) do
|
||||
if type(instanceList) == "table" then
|
||||
local writeIndex = 1
|
||||
for readIndex = 1, #instanceList do
|
||||
local instanceID = instanceList[readIndex]
|
||||
if not BLOCKED_INSTANCE_IDS[instanceID] then
|
||||
instanceList[writeIndex] = instanceID
|
||||
writeIndex = writeIndex + 1
|
||||
end
|
||||
end
|
||||
for index = writeIndex, #instanceList do
|
||||
instanceList[index] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ApplyEncounterJournalData(data)
|
||||
if type(data) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
RemoveBlockedInstances(data)
|
||||
|
||||
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
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
-- 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
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<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>
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
-- 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 portraitByDisplayInfo = {}
|
||||
local data = MoonWellEncounterJournalData
|
||||
if data and data.creatures then
|
||||
for _, creatureList in pairs(data.creatures) do
|
||||
for _, creature in ipairs(creatureList) do
|
||||
local displayInfo = creature[3]
|
||||
local icon = creature[4]
|
||||
if displayInfo and displayInfo > 0 and icon and icon ~= "" then
|
||||
portraitByDisplayInfo[displayInfo] = icon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Ogom is a nested encounter variant and has no top-level portrait row.
|
||||
portraitByDisplayInfo[6709] = [[Interface\EncounterJournal\UI-EJ-BOSS-Jammalan the Prophet]]
|
||||
MoonWellEncounterPortraitByDisplayInfo = portraitByDisplayInfo
|
||||
|
||||
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, displayInfo)
|
||||
self:SetTexture(portraitByDisplayInfo[displayInfo] or QUESTION_MARK_ICON)
|
||||
self:SetTexCoord(0, 1, 0, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
-- 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
|
||||
+10104
File diff suppressed because it is too large
Load Diff
+69
@@ -0,0 +1,69 @@
|
||||
-- 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()
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
-- 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
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-- 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
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- 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 {})
|
||||
|
||||
+23576
File diff suppressed because it is too large
Load Diff
+75
@@ -0,0 +1,75 @@
|
||||
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 = "ЗАДАЧА"
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
--[[-----------------------------------------------------------------------------------------------
|
||||
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
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<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>
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
|
||||
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
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
<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>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
local SetPortraitTexture = SetPortraitTexture
|
||||
local SetBagPortraitTexture = SetBagPortraitTexture
|
||||
|
||||
TitledPanelMixin = {};
|
||||
|
||||
function TitledPanelMixin:GetTitleText()
|
||||
return self.TitleContainer.TitleText;
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleColor(color)
|
||||
self:GetTitleText():SetTextColor(color:GetRGBA());
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitle(title)
|
||||
self:GetTitleText():SetText(title);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleFormatted(fmt, ...)
|
||||
self:GetTitleText():SetFormattedText(fmt, ...);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleMaxLinesAndHeight(maxLines, height)
|
||||
self:GetTitleText():SetMaxLines(maxLines);
|
||||
self:GetTitleText():SetHeight(height);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleOffsets(leftOffset, rightOffset)
|
||||
self.TitleContainer:SetPoint("TOPLEFT", self, "TOPLEFT", leftOffset or 58, -1);
|
||||
self.TitleContainer:SetPoint("TOPRIGHT", self, "TOPRIGHT", rightOffset or -24, -1);
|
||||
end
|
||||
|
||||
DefaultPanelMixin = CreateFromMixins(TitledPanelMixin);
|
||||
|
||||
PortraitFrameMixin = CreateFromMixins(TitledPanelMixin);
|
||||
|
||||
function PortraitFrameMixin:GetPortrait()
|
||||
return self.portrait;
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:GetTitleText()
|
||||
return self.TitleText;
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetBorder(layoutName)
|
||||
local layout = NineSliceUtil.GetLayout(layoutName);
|
||||
NineSliceUtil.ApplyLayout(self.NineSlice, layout);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToAsset(texture)
|
||||
SetPortraitToTexture(self:GetPortrait(), texture);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToUnit(unit)
|
||||
SetPortraitTexture(self:GetPortrait(), unit);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToBag(bagID)
|
||||
SetBagPortraitTexture(self:GetPortrait(), bagID);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTextureRaw(texture)
|
||||
self:GetPortrait():SetTexture(texture);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitAtlasRaw(atlas, ...)
|
||||
self:GetPortrait():SetAtlas(atlas, ...);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToClassIcon(classFilename)
|
||||
self:SetPortraitToAsset("Interface/TargetingFrame/UI-Classes-Circles");
|
||||
local left, right, bottom, top = unpack(CLASS_ICON_TCOORDS[string.upper(classFilename)]);
|
||||
self:SetPortraitTexCoord(left, right, bottom, top);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTexCoord(...)
|
||||
self:GetPortrait():SetTexCoord(...);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitShown(shown)
|
||||
self:GetPortrait():SetShown(shown);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTextureSizeAndOffset(size, offsetX, offsetY)
|
||||
local portrait = self:GetPortrait();
|
||||
portrait:SetSize(size, size);
|
||||
portrait:SetPoint("TOPLEFT", self, "TOPLEFT", offsetX, offsetY);
|
||||
end
|
||||
|
||||
do
|
||||
local function SetFrameLevelInternal(frame, level)
|
||||
if frame then
|
||||
-- assertsafe(level < 10000, "Base level needs to be low enough to accomodate the existing layering"); -- TODO: Should become a constant
|
||||
frame:SetFrameLevel(level);
|
||||
end
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetFrameLevelsFromBaseLevel(baseLevel)
|
||||
SetFrameLevelInternal(self.NineSlice, baseLevel + 2);
|
||||
SetFrameLevelInternal(self.PortraitContainer, baseLevel + 1);
|
||||
SetFrameLevelInternal(self.TitleContainer, baseLevel + 3);
|
||||
SetFrameLevelInternal(self.CloseButton, baseLevel + 3);
|
||||
end
|
||||
end
|
||||
|
||||
PortraitFrameFlatBaseMixin = {};
|
||||
|
||||
function PortraitFrameFlatBaseMixin:SetBackgroundColor(color)
|
||||
if self.Bg then
|
||||
local bg = self.Bg;
|
||||
color = color or PANEL_BACKGROUND_COLOR;
|
||||
local r, g, b, a = color:GetRGBA();
|
||||
bg.BottomLeft:SetVertexColor(r, g, b, a);
|
||||
bg.BottomRight:SetVertexColor(r, g, b, a);
|
||||
bg.BottomEdge:SetColorTexture(r, g, b, a);
|
||||
bg.TopSection:SetColorTexture(r, g, b, a);
|
||||
end
|
||||
end
|
||||
+1536
File diff suppressed because it is too large
Load Diff
+2443
File diff suppressed because it is too large
Load Diff
+365
@@ -0,0 +1,365 @@
|
||||
-- SOUND KITS
|
||||
SOUNDKIT = {
|
||||
LOOT_WINDOW_COIN_SOUND = 120,
|
||||
INTERFACE_SOUND_LOST_TARGET_UNIT = 684,
|
||||
GS_TITLE_OPTIONS = 778,
|
||||
GS_TITLE_CREDITS = 790,
|
||||
GS_TITLE_OPTION_OK = 798,
|
||||
GS_TITLE_OPTION_EXIT = 799,
|
||||
GS_LOGIN = 800,
|
||||
GS_LOGIN_NEW_ACCOUNT = 801,
|
||||
GS_LOGIN_CHANGE_REALM_OK = 805,
|
||||
GS_LOGIN_CHANGE_REALM_CANCEL = 807,
|
||||
GS_CHARACTER_SELECTION_ENTER_WORLD = 809,
|
||||
GS_CHARACTER_SELECTION_DEL_CHARACTER = 810,
|
||||
GS_CHARACTER_SELECTION_ACCT_OPTIONS = 811,
|
||||
GS_CHARACTER_SELECTION_EXIT = 812,
|
||||
GS_CHARACTER_SELECTION_CREATE_NEW = 813,
|
||||
GS_CHARACTER_CREATION_CLASS = 814,
|
||||
GS_CHARACTER_CREATION_LOOK = 817,
|
||||
GS_CHARACTER_CREATION_CREATE_CHAR = 818,
|
||||
GS_CHARACTER_CREATION_CANCEL = 819,
|
||||
IG_MINIMAP_OPEN = 821,
|
||||
IG_MINIMAP_CLOSE = 822,
|
||||
IG_MINIMAP_ZOOM_IN = 823,
|
||||
IG_MINIMAP_ZOOM_OUT = 824,
|
||||
IG_CHAT_EMOTE_BUTTON = 825,
|
||||
IG_CHAT_SCROLL_UP = 826,
|
||||
IG_CHAT_SCROLL_DOWN = 827,
|
||||
IG_CHAT_BOTTOM = 828,
|
||||
IG_SPELLBOOK_OPEN = 829,
|
||||
IG_SPELLBOOK_CLOSE = 830,
|
||||
IG_ABILITY_OPEN = 834,
|
||||
IG_ABILITY_CLOSE = 835,
|
||||
IG_ABILITY_PAGE_TURN = 836,
|
||||
IG_ABILITY_ICON_DROP = 838,
|
||||
IG_CHARACTER_INFO_OPEN = 839,
|
||||
IG_CHARACTER_INFO_CLOSE = 840,
|
||||
IG_CHARACTER_INFO_TAB = 841,
|
||||
IG_QUEST_LOG_OPEN = 844,
|
||||
IG_QUEST_LOG_CLOSE = 845,
|
||||
IG_QUEST_LOG_ABANDON_QUEST = 846,
|
||||
IG_MAINMENU_OPEN = 850,
|
||||
IG_MAINMENU_CLOSE = 851,
|
||||
IG_MAINMENU_OPTION = 852,
|
||||
IG_MAINMENU_LOGOUT = 853,
|
||||
IG_MAINMENU_QUIT = 854,
|
||||
IG_MAINMENU_CONTINUE = 855,
|
||||
IG_MAINMENU_OPTION_CHECKBOX_ON = 856,
|
||||
IG_MAINMENU_OPTION_CHECKBOX_OFF = 857,
|
||||
IG_MAINMENU_OPTION_FAER_TAB = 858,
|
||||
IG_INVENTORY_ROTATE_CHARACTER = 861,
|
||||
IG_BACKPACK_OPEN = 862,
|
||||
IG_BACKPACK_CLOSE = 863,
|
||||
IG_BACKPACK_COIN_SELECT = 864,
|
||||
IG_BACKPACK_COIN_OK = 865,
|
||||
IG_BACKPACK_COIN_CANCEL = 866,
|
||||
IG_CHARACTER_NPC_SELECT = 867,
|
||||
IG_CREATURE_NEUTRAL_SELECT = 871,
|
||||
IG_CREATURE_AGGRO_SELECT = 873,
|
||||
IG_QUEST_LIST_OPEN = 875,
|
||||
IG_QUEST_LIST_CLOSE = 876,
|
||||
IG_QUEST_LIST_SELECT = 877,
|
||||
IG_QUEST_LIST_COMPLETE = 878,
|
||||
IG_QUEST_CANCEL = 879,
|
||||
IG_PLAYER_INVITE = 880,
|
||||
MONEY_FRAME_OPEN = 891,
|
||||
MONEY_FRAME_CLOSE = 892,
|
||||
U_CHAT_SCROLL_BUTTON = 1115,
|
||||
PUT_DOWN_SMALL_CHAIN = 1212,
|
||||
LOOT_WINDOW_OPEN_EMPTY = 1264,
|
||||
TELL_MESSAGE = 3081,
|
||||
MAP_PING = 3175,
|
||||
FISHING_REEL_IN = 3407,
|
||||
IG_PVP_UPDATE = 4574,
|
||||
AUCTION_WINDOW_OPEN = 5274,
|
||||
AUCTION_WINDOW_CLOSE = 5275,
|
||||
TUTORIAL_POPUP = 7355,
|
||||
ITEM_REPAIR = 7994,
|
||||
PVP_ENTER_QUEUE = 8458,
|
||||
PVP_THROUGH_QUEUE = 8459,
|
||||
KEY_RING_OPEN = 8938,
|
||||
KEY_RING_CLOSE = 8939,
|
||||
RAID_WARNING = 8959,
|
||||
READY_CHECK = 8960,
|
||||
GLUESCREEN_INTRO = 9902,
|
||||
AMB_GLUESCREEN_HUMAN = 9903,
|
||||
AMB_GLUESCREEN_ORC = 9905,
|
||||
AMB_GLUESCREEN_TAUREN = 9906,
|
||||
AMB_GLUESCREEN_DWARF = 9907,
|
||||
AMB_GLUESCREEN_NIGHTELF = 9908,
|
||||
AMB_GLUESCREEN_UNDEAD = 9909,
|
||||
AMB_GLUESCREEN_BLOODELF = 9910,
|
||||
AMB_GLUESCREEN_DRAENEI = 9911,
|
||||
JEWEL_CRAFTING_FINALIZE = 10590,
|
||||
MENU_CREDITS01 = 10763,
|
||||
MENU_CREDITS02 = 10804,
|
||||
GUILD_VAULT_OPEN = 12188,
|
||||
GUILD_VAULT_CLOSE = 12189,
|
||||
RAID_BOSS_EMOTE_WARNING = 12197,
|
||||
GUILD_BANK_OPEN_BAG = 12206,
|
||||
GS_LICH_KING = 12765,
|
||||
ALARM_CLOCK_WARNING_2 = 12867,
|
||||
ALARM_CLOCK_WARNING_3 = 12889,
|
||||
MENU_CREDITS03 = 13822,
|
||||
ACHIEVEMENT_MENU_OPEN = 13832,
|
||||
ACHIEVEMENT_MENU_CLOSE = 13833,
|
||||
BARBERSHOP_HAIRCUT = 13873,
|
||||
BARBERSHOP_SIT = 14148,
|
||||
GM_CHAT_WARNING = 15273,
|
||||
LFG_REWARDS = 17316,
|
||||
LFG_ROLE_CHECK = 17317,
|
||||
LFG_DENIED = 17341,
|
||||
UI_BNET_TOAST = 18019,
|
||||
ALARM_CLOCK_WARNING_1 = 18871,
|
||||
AMB_GLUESCREEN_WORGEN = 20169,
|
||||
AMB_GLUESCREEN_GOBLIN = 20170,
|
||||
AMB_GLUESCREEN_TROLL = 21136,
|
||||
AMB_GLUESCREEN_GNOME = 21137,
|
||||
UI_POWER_AURA_GENERIC = 23287,
|
||||
UI_REFORGING_REFORGE = 23291,
|
||||
UI_AUTO_QUEST_COMPLETE = 23404,
|
||||
GS_CATACLYSM = 23640,
|
||||
MENU_CREDITS04 = 23812,
|
||||
UI_BATTLEGROUND_COUNTDOWN_TIMER = 25477,
|
||||
UI_BATTLEGROUND_COUNTDOWN_FINISHED = 25478,
|
||||
UI_VOID_STORAGE_UNLOCK = 25711,
|
||||
UI_VOID_STORAGE_DEPOSIT = 25712,
|
||||
UI_VOID_STORAGE_WITHDRAW = 25713,
|
||||
UI_TRANSMOGRIFY_UNDO = 25715,
|
||||
UI_ETHEREAL_WINDOW_OPEN = 25716,
|
||||
UI_ETHEREAL_WINDOW_CLOSE = 25717,
|
||||
UI_TRANSMOGRIFY_REDO = 25738,
|
||||
UI_VOID_STORAGE_BOTH = 25744,
|
||||
AMB_GLUESCREEN_PANDAREN = 25848,
|
||||
MUS_50_HEART_OF_PANDARIA_MAINTITLE = 28509,
|
||||
UI_PET_BATTLES_TRAP_READY = 28814,
|
||||
UI_EPICLOOT_TOAST = 31578,
|
||||
UI_BONUS_LOOT_ROLL_START = 31579,
|
||||
UI_BONUS_LOOT_ROLL_LOOP = 31580,
|
||||
UI_BONUS_LOOT_ROLL_END = 31581,
|
||||
UI_PET_BATTLE_START = 31584,
|
||||
UI_SCENARIO_ENDING = 31754,
|
||||
UI_SCENARIO_STAGE_END = 31757,
|
||||
MENU_CREDITS05 = 32015,
|
||||
UI_PET_BATTLE_CAMERA_MOVE_IN = 32047,
|
||||
UI_PET_BATTLE_CAMERA_MOVE_OUT = 32052,
|
||||
AMB_50_GLUESCREEN_ALLIANCE = 32412,
|
||||
AMB_50_GLUESCREEN_HORDE = 32413,
|
||||
AMB_50_GLUESCREEN_PANDAREN_NEUTRAL = 32414,
|
||||
UI_CHALLENGES_NEW_RECORD = 33338,
|
||||
MENU_CREDITS06 = 34020,
|
||||
UI_LOSS_OF_CONTROL_START = 34468,
|
||||
UI_PET_BATTLES_PVP_THROUGH_QUEUE = 36609,
|
||||
AMB_GLUESCREEN_DEATHKNIGHT = 37056,
|
||||
UI_RAID_BOSS_WHISPER_WARNING = 37666,
|
||||
UI_DIG_SITE_COMPLETION_TOAST = 38326,
|
||||
UI_IG_STORE_PAGE_NAV_BUTTON = 39511,
|
||||
UI_IG_STORE_WINDOW_OPEN_BUTTON = 39512,
|
||||
UI_IG_STORE_WINDOW_CLOSE_BUTTON = 39513,
|
||||
UI_IG_STORE_CANCEL_BUTTON = 39514,
|
||||
UI_IG_STORE_BUY_BUTTON = 39515,
|
||||
UI_IG_STORE_CONFIRM_PURCHASE_BUTTON = 39516,
|
||||
UI_IG_STORE_PURCHASE_DELIVERED_TOAST_01 = 39517,
|
||||
MUS_60_MAIN_TITLE = 40169,
|
||||
UI_GARRISON_MISSION_COMPLETE_ENCOUNTER_FAIL = 43501,
|
||||
UI_GARRISON_MISSION_COMPLETE_MISSION_SUCCESS = 43502,
|
||||
UI_GARRISON_MISSION_COMPLETE_MISSION_FAIL_STINGER = 43503,
|
||||
UI_GARRISON_MISSION_THREAT_COUNTERED = 43505,
|
||||
UI_GARRISON_MISSION_100_PERCENT_CHANCE_REACHED_NOT_USED = 43507,
|
||||
UI_QUEST_ROLLING_FORWARD_01 = 43936,
|
||||
UI_BAG_SORTING_01 = 43937,
|
||||
UI_TOYBOX_TABS = 43938,
|
||||
UI_GARRISON_TOAST_INVASION_ALERT = 44292,
|
||||
UI_GARRISON_TOAST_MISSION_COMPLETE = 44294,
|
||||
UI_GARRISON_TOAST_BUILDING_COMPLETE = 44295,
|
||||
UI_GARRISON_TOAST_FOLLOWER_GAINED = 44296,
|
||||
UI_GARRISON_NAV_TABS = 44297,
|
||||
UI_GARRISON_GARRISON_REPORT_OPEN = 44298,
|
||||
UI_GARRISON_GARRISON_REPORT_CLOSE = 44299,
|
||||
UI_GARRISON_ARCHITECT_TABLE_OPEN = 44300,
|
||||
UI_GARRISON_ARCHITECT_TABLE_CLOSE = 44301,
|
||||
UI_GARRISON_ARCHITECT_TABLE_UPGRADE = 44302,
|
||||
UI_GARRISON_ARCHITECT_TABLE_UPGRADE_CANCEL = 44304,
|
||||
UI_GARRISON_ARCHITECT_TABLE_UPGRADE_START = 44305,
|
||||
UI_GARRISON_ARCHITECT_TABLE_PLOT_SELECT = 44306,
|
||||
UI_GARRISON_ARCHITECT_TABLE_BUILDING_SELECT = 44307,
|
||||
UI_GARRISON_ARCHITECT_TABLE_BUILDING_PLACEMENT = 44308,
|
||||
UI_GARRISON_COMMAND_TABLE_OPEN = 44311,
|
||||
UI_GARRISON_COMMAND_TABLE_CLOSE = 44312,
|
||||
UI_GARRISON_COMMAND_TABLE_MISSION_CLOSE = 44313,
|
||||
UI_GARRISON_COMMAND_TABLE_NAV_NEXT = 44314,
|
||||
UI_GARRISON_COMMAND_TABLE_SELECT_MISSION = 44315,
|
||||
UI_GARRISON_COMMAND_TABLE_SELECT_FOLLOWER = 44316,
|
||||
UI_GARRISON_COMMAND_TABLE_FOLLOWER_ABILITY_OPEN = 44317,
|
||||
UI_GARRISON_COMMAND_TABLE_FOLLOWER_ABILITY_CLOSE = 44318,
|
||||
UI_GARRISON_COMMAND_TABLE_ASSIGN_FOLLOWER = 44319,
|
||||
UI_GARRISON_COMMAND_TABLE_UNASSIGN_FOLLOWER = 44320,
|
||||
UI_GARRISON_COMMAND_TABLE_SLOT_CHAMPION = 72546,
|
||||
UI_GARRISON_COMMAND_TABLE_SLOT_TROOP = 72547,
|
||||
UI_GARRISON_COMMAND_TABLE_REDUCED_SUCCESS_CHANCE = 44321,
|
||||
UI_GARRISON_COMMAND_TABLE_100_SUCCESS = 44322,
|
||||
UI_GARRISON_COMMAND_TABLE_MISSION_START = 44323,
|
||||
UI_GARRISON_COMMAND_TABLE_VIEW_MISSION_REPORT = 44324,
|
||||
UI_GARRISON_COMMAND_TABLE_MISSION_SUCCESS_STINGER = 44330,
|
||||
UI_GARRISON_COMMAND_TABLE_CHEST_UNLOCK = 44331,
|
||||
UI_GARRISON_COMMAND_TABLE_CHEST_UNLOCK_GOLD_SUCCESS = 44332,
|
||||
UI_GARRISON_MONUMENTS_OPEN = 44344,
|
||||
UI_BONUS_EVENT_SYSTEM_VIGNETTES = 45142,
|
||||
UI_GARRISON_COMMAND_TABLE_FOLLOWER_LEVEL_UP = 46893,
|
||||
UI_GARRISON_ARCHITECT_TABLE_BUILDING_PLACEMENT_ERROR = 47355,
|
||||
UI_GARRISON_MONUMENTS_CLOSE = 47373,
|
||||
AMB_GLUESCREEN_WARLORDS_OF_DRAENOR = 47544,
|
||||
MUS_1_0_MAINTITLE_ORIGINAL = 47598,
|
||||
UI_GROUP_FINDER_RECEIVE_APPLICATION = 47615,
|
||||
UI_GARRISON_MISSION_ENCOUNTER_ANIMATION_GENERIC = 47704,
|
||||
UI_GARRISON_START_WORK_ORDER = 47972,
|
||||
UI_GARRISON_SHIPMENTS_WINDOW_OPEN = 48191,
|
||||
UI_GARRISON_SHIPMENTS_WINDOW_CLOSE = 48192,
|
||||
UI_GARRISON_MONUMENTS_NAV = 48942,
|
||||
UI_RAID_BOSS_DEFEATED = 50111,
|
||||
UI_PERSONAL_LOOT_BANNER = 50893,
|
||||
UI_GARRISON_FOLLOWER_LEARN_TRAIT = 51324,
|
||||
UI_GARRISON_SHIPYARD_PLACE_CARRIER = 51385,
|
||||
UI_GARRISON_SHIPYARD_PLACE_GALLEON = 51387,
|
||||
UI_GARRISON_SHIPYARD_PLACE_DREADNOUGHT = 51388,
|
||||
UI_GARRISON_SHIPYARD_PLACE_SUBMARINE = 51389,
|
||||
UI_GARRISON_SHIPYARD_PLACE_LANDING_CRAFT = 51390,
|
||||
UI_GARRISON_SHIPYARD_START_MISSION = 51401,
|
||||
UI_RAID_LOOT_TOAST_LESSER_ITEM_WON = 51402,
|
||||
UI_WARFORGED_ITEM_LOOT_TOAST = 51561,
|
||||
UI_GARRISON_COMMAND_TABLE_INCREASED_SUCCESS_CHANCE = 51570,
|
||||
UI_GARRISON_SHIPYARD_DECOMISSION_SHIP = 51871,
|
||||
UI_70_ARTIFACT_FORGE_TRAIT_FIRST_TRAIT = 54126,
|
||||
UI_70_ARTIFACT_FORGE_RELIC_PLACE = 54128,
|
||||
UI_70_ARTIFACT_FORGE_APPEARANCE_COLOR_SELECT = 54130,
|
||||
UI_70_ARTIFACT_FORGE_APPEARANCE_LOCKED = 54131,
|
||||
UI_70_ARTIFACT_FORGE_APPEARANCE_APPEARANCE_CHANGE = 54132,
|
||||
UI_70_ARTIFACT_FORGE_TOAST_TRAIT_AVAILABLE = 54133,
|
||||
UI_70_ARTIFACT_FORGE_APPEARANCE_APPEARANCE_UNLOCK = 54139,
|
||||
UI_70_ARTIFACT_FORGE_TRAIT_GOLD_TRAIT = 54125,
|
||||
UI_72_ARTIFACT_FORGE_FINAL_TRAIT_UNLOCKED = 83682,
|
||||
UI_70_ARTIFACT_FORGE_TRAIT_FINALRANK = 54127,
|
||||
UI_70_ARTIFACT_FORGE_TRAIT_RANKUP = 54129,
|
||||
AMB_GLUESCREEN_DEMONHUNTER = 56352,
|
||||
MUS_70_MAIN_TITLE = 56353,
|
||||
MENU_CREDITS07 = 56354,
|
||||
UI_TRANSMOG_ITEM_CLICK = 62538,
|
||||
UI_TRANSMOG_PAGE_TURN = 62539,
|
||||
UI_TRANSMOG_GEAR_SLOT_CLICK = 62540,
|
||||
UI_TRANSMOG_REVERTING_GEAR_SLOT = 62541,
|
||||
UI_TRANSMOG_APPLY = 62542,
|
||||
UI_TRANSMOG_CLOSE_WINDOW = 62543,
|
||||
UI_TRANSMOG_OPEN_WINDOW = 62544,
|
||||
UI_LEGENDARY_LOOT_TOAST = 63971,
|
||||
UI_STORE_UNWRAP = 64329,
|
||||
AMB_GLUESCREEN_LEGION = 71535,
|
||||
UI_MISSION_200_PERCENT = 72548,
|
||||
UI_MISSION_MAP_ZOOM = 72549,
|
||||
UI_70_BOOST_THANKSFORPLAYING_SMALLER = 72978,
|
||||
UI_70_BOOST_THANKSFORPLAYING = 72977,
|
||||
UI_WORLDQUEST_START = 73275,
|
||||
UI_WORLDQUEST_MAP_SELECT = 73276,
|
||||
UI_WORLDQUEST_COMPLETE = 73277,
|
||||
UI_ORDERHALL_TALENT_SELECT = 73279,
|
||||
UI_ORDERHALL_TALENT_READY_TOAST = 73280,
|
||||
UI_ORDERHALL_TALENT_READY_CHECK = 73281,
|
||||
UI_ORDERHALL_TALENT_NUKE_FROM_ORBIT = 73282,
|
||||
UI_ORDERHALL_TALENT_WINDOW_OPEN = 73914,
|
||||
UI_ORDERHALL_TALENT_WINDOW_CLOSE = 73915,
|
||||
UI_PROFESSIONS_WINDOW_OPEN = 73917,
|
||||
UI_PROFESSIONS_WINDOW_CLOSE = 73918,
|
||||
UI_PROFESSIONS_NEW_RECIPE_LEARNED_TOAST = 73919,
|
||||
UI_70_CHALLENGE_MODE_SOCKET_PAGE_OPEN = 74421,
|
||||
UI_70_CHALLENGE_MODE_SOCKET_PAGE_CLOSE = 74423,
|
||||
UI_70_CHALLENGE_MODE_SOCKET_PAGE_SOCKET = 74431,
|
||||
UI_70_CHALLENGE_MODE_SOCKET_PAGE_ACTIVATE_BUTTON = 74432,
|
||||
UI_70_CHALLENGE_MODE_KEYSTONE_UPGRADE = 74437,
|
||||
UI_70_CHALLENGE_MODE_NEW_RECORD = 74438,
|
||||
UI_70_CHALLENGE_MODE_SOCKET_PAGE_REMOVE_KEYSTONE = 74525,
|
||||
UI_70_CHALLENGE_MODE_COMPLETE_NO_UPGRADE = 74526,
|
||||
UI_MISSION_SUCCESS_CHEERS = 74702,
|
||||
UI_PVP_HONOR_PRESTIGE_OPEN_WINDOW = 76995,
|
||||
UI_PVP_HONOR_PRESTIGE_WINDOW_CLOSE = 77002,
|
||||
UI_PVP_HONOR_PRESTIGE_RANK_UP = 77003,
|
||||
UI_71_SOCIAL_QUEUEING_TOAST = 79739,
|
||||
UI_72_ARTIFACT_FORGE_ACTIVATE_FINAL_TIER = 83681,
|
||||
UI_72_BUILDINGS_CONTRIBUTE_POWER_MENU_CLICK = 84240,
|
||||
UI_72_BUILDING_CONTRIBUTION_TABLE_OPEN = 84368,
|
||||
UI_72_BUILDINGS_CONTRIBUTION_TABLE_CLOSE = 84369,
|
||||
UI_72_BUILDINGS_CONTRIBUTE_RESOURCES = 84378,
|
||||
UI_72_ARTIFACT_FORGE_FINAL_TRAIT_REFUND_START = 83684,
|
||||
UI_72_ARTIFACT_FORGE_FINAL_TRAIT_REFUND_LOOP = 83685,
|
||||
UI_73_ARTIFACT_RELICS_TRAIT_SELECT_AND_REVEAL = 89685,
|
||||
UI_73_ARTIFACT_RELICS_TRAIT_SELECT_ONLY = 89686,
|
||||
UI_73_ARTIFACT_RELICS_TRAIT_REVEAL_ONLY = 90080,
|
||||
PUT_DOWN_GEMS = 1204,
|
||||
PICK_UP_GEMS = 1221,
|
||||
UI_73_ARTIFACT_OVERLOADED_HIGHEST = 97301,
|
||||
UI_73_ARTIFACT_OVERLOADED_HIGH = 97471,
|
||||
UI_73_ARTIFACT_OVERLOADED_MEDIUM = 97470,
|
||||
UI_73_ARTIFACT_OVERLOADED_LOW = 97469,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_HIGHEST = 97559,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_HIGH = 97558,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_MEDIUM = 97557,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_LOW = 97556,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_IMPACT_MEDIUM = 97597,
|
||||
UI_73_ARTIFACT_OVERLOADED_ORB_IMPACT_LOW = 97598,
|
||||
AMB_GLUESCREEN_VOIDELF = 97324,
|
||||
AMB_GLUESCREEN_LIGHTFORGEDDRAENEI = 97325,
|
||||
AMB_GLUESCREEN_NIGHTBORNE = 97326,
|
||||
AMB_GLUESCREEN_HIGHMOUNTAINTAUREN = 97327,
|
||||
UI_VOICECHAT_DEAFENOFF = 110990,
|
||||
UI_VOICECHAT_DEAFENON = 110989,
|
||||
UI_VOICECHAT_MUTEOFF = 110988,
|
||||
UI_VOICECHAT_MUTEON = 110987,
|
||||
UI_VOICECHAT_MUTEOTHEROFF = 111080,
|
||||
UI_VOICECHAT_MUTEOTHERON = 111081,
|
||||
UI_VOICECHAT_MEMBERLEAVECHANNEL = 110986,
|
||||
UI_VOICECHAT_MEMBERJOINCHANNEL = 110985,
|
||||
UI_VOICECHAT_STOPTALK = 110984,
|
||||
UI_VOICECHAT_TALKSTART = 110983,
|
||||
UI_VOICECHAT_LEAVECHANNEL = 110982,
|
||||
UI_VOICECHAT_JOINCHANNEL = 110981,
|
||||
AMB_GLUESCREEN_BATTLE_FOR_AZEROTH = 113894,
|
||||
AMB_GLUESCREEN_MAGHARORC = 113556,
|
||||
AMB_GLUESCREEN_DARKIRONDWARF = 113557,
|
||||
MUS_80_MAIN_TITLE = 113559,
|
||||
MENU_CREDITS08 = 113560,
|
||||
UI_AZERITE_EMPOWERED_ITEM_LOOT_TOAST = 118238,
|
||||
|
||||
UI_80_AZERITEARMOR_SELECTBUFF = 114993,
|
||||
UI_80_AZERITEARMOR_BUFFAVAILABLE = 116657,
|
||||
UI_80_AZERITEARMOR_ROTATION_LOOP = 114994,
|
||||
UI_80_AZERITEARMOR_ROTATIONENDS = 114995,
|
||||
UI_80_AZERITEARMOR_ROTATIONENDS_FINALTRAIT = 116233,
|
||||
UI_80_AZERITEARMOR_ROTATIONENDCLICKS = 115314,
|
||||
UI_80_AZERITEARMOR_ROTATIONSTARTCLICKS = 115358,
|
||||
UI_80_AZERITEARMOR_FIRSTTIMEFLOURISH = 118264,
|
||||
|
||||
UI_80_AZERITEARMOR_REFORGE_ETHEREALWINDOW_OPEN = 114990,
|
||||
UI_80_AZERITEARMOR_REFORGE_ETHEREALWINDOW_CLOSE = 114991,
|
||||
UI_80_AZERITEARMOR_REFORGE = 114992,
|
||||
UI_80_ISLANDS_AZERITECOLLECTION_START = 115003,
|
||||
UI_80_ISLANDS_AZERITECOLLECTION_LOOP = 115004,
|
||||
UI_80_ISLANDS_AZERITECOLLECTION_STOP = 115005,
|
||||
UI_80_SCRAPPING_WINDOW_CLOSE = 115487,
|
||||
UI_80_SCRAPPING_WINDOW_OPEN = 115488,
|
||||
UI_WARMODE_ACTIVATE = 118563,
|
||||
UI_WARMODE_DECTIVATE = 118564,
|
||||
|
||||
UI_80_ISLANDS_TABLE_OPEN = 118610,
|
||||
UI_80_ISLANDS_TABLE_CLOSE = 118611,
|
||||
UI_80_ISLANDS_TABLE_SELECT_DIFFICULTY = 118612,
|
||||
UI_80_ISLANDS_TABLE_FIND_GROUP = 118613,
|
||||
UI_80_ISLANDS_TABLE_FIND_GROUP_PVP = 118668,
|
||||
UI_80_ISLANDS_TUTORIAL_CLOSE = 119726,
|
||||
|
||||
UI_GARRISON_MISSION_COMPLETE_ENCOUNTER_CHANCE = 0, -- missing SoundKit entry!
|
||||
|
||||
UI_COUNTDOWN_BAR_STATE_STARTS = 158958,
|
||||
UI_COUNTDOWN_BAR_STATE_FINISHED = 158959,
|
||||
UI_COUNTDOWN_MEDIUM_NUMBER_FINISHED = 158960,
|
||||
UI_COUNTDOWN_TIMER = 191352,
|
||||
UI_COUNTDOWN_FINISHED = 158566,
|
||||
};
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user