Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b30a3554cf | |||
| 595ed5091d | |||
| 2291ace9f7 | |||
| 26ff2b3b94 | |||
| d5e2dce359 |
@@ -6,3 +6,7 @@ AWS_DEFAULT_REGION=
|
|||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_ENDPOINT=
|
AWS_ENDPOINT=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||||
|
|
||||||
|
PRODUCTION_REALMLIST=
|
||||||
|
PTR_REALMLIST=
|
||||||
|
LOCAL_REALMLIST=
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ manifest.json
|
|||||||
.claude
|
.claude
|
||||||
.vscode
|
.vscode
|
||||||
dist
|
dist
|
||||||
|
build/
|
||||||
|
Wow*.exe
|
||||||
|
*.backup.exe
|
||||||
|
Logs/
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[submodule "vendor/warcraftxl"]
|
||||||
|
path = vendor/warcraftxl
|
||||||
|
url = https://github.com/WarcraftXL/wxl-core.git
|
||||||
|
[submodule "vendor/modules/wxl-modern-assets"]
|
||||||
|
path = vendor/modules/wxl-modern-assets
|
||||||
|
url = https://github.com/WarcraftXL/wxl-modern-assets.git
|
||||||
|
[submodule "vendor/modules/wxl-modern-render"]
|
||||||
|
path = vendor/modules/wxl-modern-render
|
||||||
|
url = https://github.com/WarcraftXL/wxl-modern-render.git
|
||||||
|
[submodule "vendor/modules/wxl-unit-outline"]
|
||||||
|
path = vendor/modules/wxl-unit-outline
|
||||||
|
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||||
|
[submodule "vendor/modules/wxl-modern-adt"]
|
||||||
|
path = vendor/modules/wxl-modern-adt
|
||||||
|
url = https://github.com/WarcraftXL/wxl-modern-adt.git
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MoonWellWarcraftXL LANGUAGES CXX)
|
||||||
|
|
||||||
|
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
|
||||||
|
# its aggregate DLL target here, so updating the framework does not mix our
|
||||||
|
# client-specific offsets and policy into the upstream checkout.
|
||||||
|
add_subdirectory(vendor/warcraftxl)
|
||||||
|
|
||||||
|
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
|
||||||
|
|
||||||
|
function(wxl_collect_sources output)
|
||||||
|
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
|
||||||
|
set(${output} "${collected}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if(TARGET WarcraftXL)
|
||||||
|
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||||
|
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||||
|
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
|
||||||
|
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||||
|
endif()
|
||||||
|
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
|
||||||
|
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
|
||||||
|
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||||
|
|
||||||
|
target_sources(WarcraftXL PRIVATE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
|
||||||
|
${WXL_MODERN_ASSETS_RUNTIME}
|
||||||
|
${WXL_MODERN_RENDER_RUNTIME}
|
||||||
|
${WXL_UNIT_OUTLINE_RUNTIME}
|
||||||
|
${WXL_MODERN_ADT_RUNTIME})
|
||||||
|
|
||||||
|
# wxl-modern-render needs its module-root includes and D3D12 import library.
|
||||||
|
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET WarcraftXLHost)
|
||||||
|
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||||
|
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||||
|
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
|
||||||
|
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||||
|
endif()
|
||||||
|
wxl_collect_sources(WXL_MODERN_ADT_HOST
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||||
|
|
||||||
|
target_sources(WarcraftXLHost PRIVATE
|
||||||
|
${WXL_MODERN_ASSETS_HOST}
|
||||||
|
${WXL_MODERN_ADT_HOST})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
|
||||||
|
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
|
||||||
|
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||||
|
add_library(MoonWellLoader SHARED
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def")
|
||||||
|
set_target_properties(MoonWellLoader PROPERTIES
|
||||||
|
OUTPUT_NAME "d3d9-native"
|
||||||
|
PREFIX ""
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts")
|
||||||
|
target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
|
endif()
|
||||||
@@ -1,90 +1,143 @@
|
|||||||
# MoonWell Client Sources
|
# 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` - модифицированный клиентский исполняемый файл.
|
- разблокировка пользовательского `GlueXML`;
|
||||||
- Поддерживает использование до 4 ГБ ОЗУ.
|
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
|
||||||
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
|
- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`;
|
||||||
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
|
- загрузка WarcraftXL без import-table patch и без отдельного injector.
|
||||||
- Содержит графические элементы внутриигрового магазина.
|
|
||||||
- Основная зона изменений: `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/`.
|
|
||||||
|
|
||||||
## Структура по назначению
|
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`
|
```powershell
|
||||||
- `Currencies/Token.blp`
|
git submodule update --remote vendor/warcraftxl vendor/modules/*
|
||||||
- `Frames/StoreFrame_Main.blp`
|
```
|
||||||
|
|
||||||
### Экран входа и локализованные ресурсы
|
После обновления обязательно пересоберите и проверьте вход/создание персонажа: 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-исходники интерфейса загрузки и логина
|
После клонирования инициализируйте submodule:
|
||||||
- `Interface/Glues` - графика и модели для glue-экрана
|
|
||||||
- `Interface/LoginScreen` - фон и связанные изображения
|
|
||||||
- `Sound/Ambience/GlueScreen` - фоновые звуки
|
|
||||||
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
|
|
||||||
|
|
||||||
### Русский перевод для `patch-ruRU-5`
|
```powershell
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
|
## Сборка WarcraftXL
|
||||||
|
|
||||||
Основные каталоги:
|
Быстрое полное развёртывание клиента одной командой:
|
||||||
|
|
||||||
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
|
```powershell
|
||||||
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
|
.\deploy.ps1
|
||||||
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
|
```
|
||||||
- `Interface/Loginscreen` - фон и изображения экрана входа
|
|
||||||
- `Interface/tooltips` - локализованные рамки и оформление тултипов
|
|
||||||
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
|
|
||||||
|
|
||||||
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
|
Скрипт инициализирует зависимости, собирает 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+
|
# Аварийный native D3D9 вместо D3D9On12
|
||||||
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
|
.\deploy.ps1 -NativeRenderer
|
||||||
- `Interface/Icons` - дополнительные иконки
|
```
|
||||||
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
|
|
||||||
|
|
||||||
## Как воспринимать содержимое репозитория
|
Без установки в клиент:
|
||||||
|
|
||||||
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
|
```powershell
|
||||||
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
|
.\build-warcraftxl.ps1 -Configuration Release
|
||||||
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
|
```
|
||||||
|
|
||||||
## Для чего используется
|
С установкой:
|
||||||
|
|
||||||
Проект нужен для сопровождения клиентской части MoonWell:
|
```powershell
|
||||||
|
.\build-warcraftxl.ps1 -Configuration Release `
|
||||||
|
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
|
||||||
|
```
|
||||||
|
|
||||||
- кастомизации GlueXML;
|
При `-Deploy` скрипт:
|
||||||
- поддержки русского перевода для `patch-ruRU-5`;
|
|
||||||
- поддержки внутриигрового магазина;
|
1. проверяет хеш `Wow_Original.exe`;
|
||||||
- добавления графики и данных для Mythic+;
|
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
|
||||||
- хранения пакетов графических улучшений;
|
3. восстанавливает оригинальный `Wow.exe`;
|
||||||
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
|
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,106 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('Debug', 'Release')]
|
||||||
|
[string]$Configuration = 'Release',
|
||||||
|
[string]$ClientPath = '',
|
||||||
|
[string]$PackagePath = '',
|
||||||
|
[switch]$Deploy,
|
||||||
|
[switch]$NativeRenderer
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repoRoot = $PSScriptRoot
|
||||||
|
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $cmake)) {
|
||||||
|
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
|
||||||
|
if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' }
|
||||||
|
$cmake = $cmakeCommand.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
& git -C $repoRoot submodule update --init --recursive
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
|
||||||
|
|
||||||
|
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
|
||||||
|
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
|
||||||
|
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
|
||||||
|
|
||||||
|
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) {
|
||||||
|
throw "Stock executable was not found: $stockExe"
|
||||||
|
}
|
||||||
|
$expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8'
|
||||||
|
$stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash
|
||||||
|
if ($stockHash -ne $expectedStockHash) {
|
||||||
|
throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
|
||||||
|
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
|
||||||
|
|
||||||
|
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
|
||||||
|
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
|
||||||
|
|
||||||
|
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||||
|
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
|
||||||
|
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||||
|
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||||
|
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||||
|
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
|
||||||
|
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
|
||||||
|
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
|
||||||
|
|
||||||
|
function Install-WarcraftXLArtifacts {
|
||||||
|
param([Parameter(Mandatory)][string]$Destination)
|
||||||
|
|
||||||
|
$utils = Join-Path $Destination 'Utils'
|
||||||
|
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
|
||||||
|
New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force
|
||||||
|
Copy-Item -LiteralPath $warcraftXL -Destination (Join-Path $Destination 'WarcraftXL.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $selectedProxy -Destination (Join-Path $Destination 'd3d9.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force
|
||||||
|
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
|
||||||
|
Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Deploy) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||||
|
$ClientPath = 'C:\Program Files (x86)\World of Warcraft'
|
||||||
|
}
|
||||||
|
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
|
||||||
|
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||||
|
throw "Client directory was not found: $ClientPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientExe = Join-Path $ClientPath 'Wow.exe'
|
||||||
|
if (Test-Path -LiteralPath $clientExe -PathType Leaf) {
|
||||||
|
$clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash
|
||||||
|
if ($clientHash -ne $stockHash) {
|
||||||
|
$legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $legacyBackup)) {
|
||||||
|
Copy-Item -LiteralPath $clientExe -Destination $legacyBackup
|
||||||
|
}
|
||||||
|
Write-Host "Legacy executable saved as $legacyBackup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Install-WarcraftXLArtifacts -Destination $ClientPath
|
||||||
|
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||||
|
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||||
|
Install-WarcraftXLArtifacts -Destination $PackagePath
|
||||||
|
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
|
||||||
|
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
|
||||||
@@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = {
|
|||||||
".git",
|
".git",
|
||||||
".moonwell_launcher",
|
".moonwell_launcher",
|
||||||
}
|
}
|
||||||
|
IGNORED_TOP_LEVEL_FILES = {
|
||||||
|
"Wow.moonwell-patched.backup.exe",
|
||||||
|
"WarcraftXL_on12.disable",
|
||||||
|
}
|
||||||
|
|
||||||
ENV_VAR_NAME = "WOW_HOME"
|
ENV_VAR_NAME = "WOW_HOME"
|
||||||
|
|
||||||
@@ -42,6 +46,8 @@ def is_ignored(rel_path: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
|
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
|
||||||
return True
|
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])
|
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
C_PlayerGuide.lua
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
Клиентское хранилище данных Путеводителя + UI-facing API.
|
|
||||||
Соответствует контракту из player-guide-requirements.md → "Client API".
|
|
||||||
|
|
||||||
Подключается до PlayerGuide.xml/.lua в .toc.
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
C_PlayerGuide = C_PlayerGuide or {}
|
|
||||||
local L = C_PlayerGuide
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Состояние
|
|
||||||
|
|
||||||
local currentData = nil -- последний валидный payload от сервера
|
|
||||||
local lastError = nil -- строка с описанием последней проблемы валидации
|
|
||||||
|
|
||||||
-- Состояние UI: "data" | "empty" | "loading"
|
|
||||||
local uiState = "loading"
|
|
||||||
|
|
||||||
-- Подписчики на обновления (PlayerGuide.lua вешает сюда свой Refresh)
|
|
||||||
local listeners = {}
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Утилиты
|
|
||||||
|
|
||||||
local function notify()
|
|
||||||
for _, fn in ipairs(listeners) do
|
|
||||||
-- pcall чтобы один сломанный listener не уронил остальные
|
|
||||||
pcall(fn, uiState, currentData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isTable(v) return type(v) == "table" end
|
|
||||||
|
|
||||||
-- Валидация payload'а. Возвращает true либо false + строка ошибки.
|
|
||||||
local function validate(data)
|
|
||||||
if not isTable(data) then return false, "payload is not a table" end
|
|
||||||
if data.stageID and type(data.stageID) ~= "number" then
|
|
||||||
return false, "stageID is not a number"
|
|
||||||
end
|
|
||||||
if data.progress and type(data.progress) ~= "number" then
|
|
||||||
return false, "progress is not a number"
|
|
||||||
end
|
|
||||||
if data.objectives and not isTable(data.objectives) then
|
|
||||||
return false, "objectives is not a table"
|
|
||||||
end
|
|
||||||
-- Уникальность id целей
|
|
||||||
if isTable(data.objectives) then
|
|
||||||
local seen = {}
|
|
||||||
for i, o in ipairs(data.objectives) do
|
|
||||||
if not isTable(o) then
|
|
||||||
return false, "objective #" .. i .. " is not a table"
|
|
||||||
end
|
|
||||||
if o.id and seen[o.id] then
|
|
||||||
return false, "duplicate objective id: " .. tostring(o.id)
|
|
||||||
end
|
|
||||||
if o.id then seen[o.id] = true end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Подписки (для PlayerGuide.lua)
|
|
||||||
|
|
||||||
function L.RegisterListener(fn)
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Публичный API (см. требования → Client API)
|
|
||||||
|
|
||||||
function L.SetData(data)
|
|
||||||
local ok, err = validate(data)
|
|
||||||
if not ok then
|
|
||||||
lastError = err
|
|
||||||
uiState = "empty"
|
|
||||||
notify()
|
|
||||||
return false, err
|
|
||||||
end
|
|
||||||
currentData = data
|
|
||||||
lastError = nil
|
|
||||||
uiState = "data"
|
|
||||||
notify()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.Clear()
|
|
||||||
currentData = nil
|
|
||||||
lastError = nil
|
|
||||||
uiState = "empty"
|
|
||||||
notify()
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.SetLoading()
|
|
||||||
if uiState ~= "data" then
|
|
||||||
uiState = "loading"
|
|
||||||
notify()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetState()
|
|
||||||
return uiState
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetLastError()
|
|
||||||
return lastError
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.HasData()
|
|
||||||
return uiState == "data" and currentData ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetStageInfo()
|
|
||||||
if not L.HasData() then return nil end
|
|
||||||
return currentData.stageID,
|
|
||||||
currentData.stageName,
|
|
||||||
currentData.stageDescription,
|
|
||||||
currentData.progress or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Доп. метод: список всех этапов (для цепочки сверху)
|
|
||||||
function L.GetStages()
|
|
||||||
if not L.HasData() then return nil end
|
|
||||||
return currentData.stages or {} -- сервер опционально шлёт массив этапов
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetNumObjectives()
|
|
||||||
if not L.HasData() or not currentData.objectives then return 0 end
|
|
||||||
return #currentData.objectives
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetObjectiveInfo(index)
|
|
||||||
if not L.HasData() or not currentData.objectives then return nil end
|
|
||||||
local o = currentData.objectives[index]
|
|
||||||
if not o then return nil end
|
|
||||||
return {
|
|
||||||
id = o.id,
|
|
||||||
type = o.type or "custom",
|
|
||||||
title = o.title or "",
|
|
||||||
description = o.description or o.subtitle or "",
|
|
||||||
subtitle = o.subtitle, -- наше расширение поля для UI
|
|
||||||
completed = o.completed and true or false,
|
|
||||||
progress = o.progress or 0,
|
|
||||||
required = o.required or 1,
|
|
||||||
targetID = o.targetID,
|
|
||||||
questID = o.questID,
|
|
||||||
instanceID = o.instanceID,
|
|
||||||
encounterID = o.encounterID,
|
|
||||||
achievementID = o.achievementID,
|
|
||||||
factionID = o.factionID,
|
|
||||||
itemID = o.itemID,
|
|
||||||
currencyID = o.currencyID,
|
|
||||||
order = o.order or index,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetNumRecommendations()
|
|
||||||
if not L.HasData() or not currentData.recommendations then return 0 end
|
|
||||||
return #currentData.recommendations
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetRecommendationInfo(index)
|
|
||||||
if not L.HasData() or not currentData.recommendations then return nil end
|
|
||||||
local r = currentData.recommendations[index]
|
|
||||||
if not r then return nil end
|
|
||||||
return {
|
|
||||||
id = r.id,
|
|
||||||
type = r.type or "dungeon",
|
|
||||||
title = r.title or r.name or "",
|
|
||||||
name = r.name or r.title or "",
|
|
||||||
description = r.description or r.loc or "",
|
|
||||||
loc = r.loc, -- локация/подзаголовок
|
|
||||||
level = r.level,
|
|
||||||
biome = r.biome, -- ключ биом-обложки
|
|
||||||
instanceID = r.instanceID,
|
|
||||||
encounterID = r.encounterID,
|
|
||||||
priority = r.priority or 0,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetNumRewards()
|
|
||||||
if not L.HasData() or not currentData.rewards then return 0 end
|
|
||||||
return #currentData.rewards
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.GetRewardInfo(index)
|
|
||||||
if not L.HasData() or not currentData.rewards then return nil end
|
|
||||||
local r = currentData.rewards[index]
|
|
||||||
if not r then return nil end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Сетевой слой (AIO)
|
|
||||||
-- MoonWellPlayerGuide.lua перехватывает SetData() из serverToClient и вызывает
|
|
||||||
-- L.SetData(payload). RequestRefresh шлёт обратно в обратную сторону.
|
|
||||||
|
|
||||||
function L.RequestRefresh()
|
|
||||||
L.SetLoading()
|
|
||||||
if AIO and AIO.Handle then
|
|
||||||
-- name берётся из serverToClient на сервере. Тут — лёгкая обёртка.
|
|
||||||
AIO.Handle("MoonWellPlayerGuide", "RequestRefresh")
|
|
||||||
else
|
|
||||||
-- Без AIO (например, в режиме разработки) — отдаём заглушку через
|
|
||||||
-- 0.5 секунды, чтобы UI не висел в Loading навечно.
|
|
||||||
if C_Timer and C_Timer.After then
|
|
||||||
C_Timer.After(0.5, function() L.Clear() end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.RequestTrackObjective(objectiveID)
|
|
||||||
if AIO and AIO.Handle then
|
|
||||||
AIO.Handle("MoonWellPlayerGuide", "RequestTrackObjective", objectiveID)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function L.RequestOpenTarget(objectiveID)
|
|
||||||
if AIO and AIO.Handle then
|
|
||||||
AIO.Handle("MoonWellPlayerGuide", "RequestOpenTarget", objectiveID)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Статика для разработки (видна, пока сервер не прислал реальный payload).
|
|
||||||
-- Можно убрать перед релизом.
|
|
||||||
|
|
||||||
function L._InjectDevPayload()
|
|
||||||
L.SetData({
|
|
||||||
version = 1,
|
|
||||||
stageID = 5,
|
|
||||||
stageName = "Подготовка к Нордсколу",
|
|
||||||
stageDescription =
|
|
||||||
"Завершите ключевые цепочки заданий в Запределье и докажите готовность к походу на север. "..
|
|
||||||
"По прибытии в Нордскол вам понадобится снаряжение героического уровня.",
|
|
||||||
progress = 62,
|
|
||||||
stages = {
|
|
||||||
{ id = 1, name = "Начало пути", short = "Начало пути", era = "1—58", status = "done" },
|
|
||||||
{ id = 2, name = "Огненные Недра", short = "Огненные Недра", era = "60", status = "done" },
|
|
||||||
{ id = 3, name = "Сквозь Тёмный портал", short = "Тёмный портал", era = "58—60", status = "done" },
|
|
||||||
{ id = 4, name = "Покорение Запределья", short = "Запределье", era = "60—68", status = "done" },
|
|
||||||
{ id = 5, name = "Подготовка к Нордсколу", short = "К Нордсколу", era = "68—70", status = "active" },
|
|
||||||
{ id = 6, name = "Заря Нордскола", short = "Нордскол", era = "70—78", status = "locked" },
|
|
||||||
{ id = 7, name = "Цитадель Ледяной Короны",short = "Ледяная Корона", era = "78—80", status = "locked" },
|
|
||||||
},
|
|
||||||
objectives = {
|
|
||||||
{ id = 1, type = "level", title = "Достигнуть 68 уровня",
|
|
||||||
subtitle = "Текущий уровень: 68. Можно отправляться в Нордскол.",
|
|
||||||
completed = true, progress = 68, required = 68, order = 1 },
|
|
||||||
{ id = 2, type = "quest_chain", title = "Цепочка «Поиск виверны» в Награнде",
|
|
||||||
subtitle = "Кисть Шамана • Награнд • награда: «Привязка к виверне»",
|
|
||||||
completed = false, progress = 5, required = 8, order = 2 },
|
|
||||||
{ id = 3, type = "dungeon", title = "Пройти Кузню Крови",
|
|
||||||
subtitle = "Цитадель Адского Пламени • рек. уровень 61—64",
|
|
||||||
completed = false, progress = 0, required = 1, order = 3, instanceID = 256 },
|
|
||||||
{ id = 4, type = "dungeon", title = "Пройти Аукиндон: Подземелья Аукенай",
|
|
||||||
subtitle = "Награнд • рек. уровень 64—66",
|
|
||||||
completed = false, progress = 0, required = 1, order = 4, instanceID = 257 },
|
|
||||||
{ id = 5, type = "reputation", title = "Получить «Дружелюбие» с Кенарийской экспедицией",
|
|
||||||
subtitle = "Текущее положение: Нейтрально • Долина Призрачной Луны",
|
|
||||||
completed = false, progress = 1200, required = 3000, order = 5, factionID = 942 },
|
|
||||||
},
|
|
||||||
recommendations = {
|
|
||||||
{ id = 1, type = "dungeon", name = "Кузня Крови", loc = "Цитадель Адского Пламени", level = "61—64", biome = "fire" },
|
|
||||||
{ id = 2, type = "dungeon", name = "Подземелья Аукенай", loc = "Аукиндон", level = "64—66", biome = "arcane" },
|
|
||||||
{ id = 3, type = "dungeon", name = "Сетеккские залы", loc = "Награнд", level = "67—69", biome = "storm" },
|
|
||||||
{ id = 4, type = "dungeon", name = "Темный Лабиринт", loc = "Аукиндон", level = "68—70", biome = "shadow" },
|
|
||||||
{ id = 5, type = "dungeon", name = "Ботаника", loc = "Долина Призрачной Луны", level = "69—70", biome = "nature" },
|
|
||||||
{ id = 6, type = "dungeon", name = "Расколотые Залы", loc = "Цитадель Адского Пламени", level = "70 H", biome = "fire" },
|
|
||||||
},
|
|
||||||
rewards = {}, -- блок наград временно скрыт (см. SHOW_REWARDS в PlayerGuide.lua)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
AIO/MoonWellPlayerGuide.lua
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
AIO-handler (клиентская часть) для namespace "MoonWellPlayerGuide".
|
|
||||||
Сервер отправляет SetData(payload), клиент кладёт его в C_PlayerGuide.
|
|
||||||
Клиент шлёт RequestRefresh обратно при нажатии «Обновить».
|
|
||||||
|
|
||||||
Контракт описан в player-guide-requirements.md → "AIO Contract".
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
local AIO = AIO or require("AIO")
|
|
||||||
if not AIO then return end -- безопасный no-op без AIO
|
|
||||||
|
|
||||||
local handlers = AIO.AddHandlers("MoonWellPlayerGuide", {})
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Server → Client
|
|
||||||
|
|
||||||
-- Полная замена payload
|
|
||||||
function handlers.SetData(player, payload)
|
|
||||||
if C_PlayerGuide and C_PlayerGuide.SetData then
|
|
||||||
local ok, err = C_PlayerGuide.SetData(payload)
|
|
||||||
if not ok and DEFAULT_CHAT_FRAME then
|
|
||||||
DEFAULT_CHAT_FRAME:AddMessage(
|
|
||||||
"|cffff8080[Путеводитель]|r Ошибка payload: "..tostring(err))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Обновление одной цели (для частичных update'ов)
|
|
||||||
function handlers.UpdateObjective(player, objective)
|
|
||||||
if not (C_PlayerGuide and C_PlayerGuide.HasData()) then return end
|
|
||||||
-- Простая стратегия: пересобрать payload локально с заменой цели по id.
|
|
||||||
-- C_PlayerGuide хранит данные internally — даём прямой доступ через _patch:
|
|
||||||
local n = C_PlayerGuide.GetNumObjectives()
|
|
||||||
for i = 1, n do
|
|
||||||
local o = C_PlayerGuide.GetObjectiveInfo(i)
|
|
||||||
if o and o.id == objective.id then
|
|
||||||
-- Грязный, но рабочий способ для MVP — отдать обратно весь массив.
|
|
||||||
-- Для прод-уровня лучше выставить C_PlayerGuide.PatchObjective(o).
|
|
||||||
o.progress = objective.progress or o.progress
|
|
||||||
o.required = objective.required or o.required
|
|
||||||
o.completed = objective.completed
|
|
||||||
if MW_PlayerGuide and MW_PlayerGuide.Refresh then
|
|
||||||
MW_PlayerGuide.Refresh()
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Замена только инфы об этапе (без массива objectives)
|
|
||||||
function handlers.SetStage(player, stagePayload)
|
|
||||||
if not stagePayload then return end
|
|
||||||
-- Объединяем с текущим payload'ом
|
|
||||||
local _, name, desc, progress = C_PlayerGuide.GetStageInfo()
|
|
||||||
C_PlayerGuide.SetData({
|
|
||||||
stageID = stagePayload.stageID,
|
|
||||||
stageName = stagePayload.stageName or name,
|
|
||||||
stageDescription = stagePayload.stageDescription or desc,
|
|
||||||
progress = stagePayload.progress or progress,
|
|
||||||
stages = stagePayload.stages,
|
|
||||||
objectives = stagePayload.objectives or {},
|
|
||||||
recommendations = stagePayload.recommendations or {},
|
|
||||||
rewards = stagePayload.rewards or {},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Сообщения от сервера (баблы / chat-фрейм)
|
|
||||||
function handlers.Notify(player, message)
|
|
||||||
if DEFAULT_CHAT_FRAME and message then
|
|
||||||
DEFAULT_CHAT_FRAME:AddMessage("|cffffd76a[Путеводитель]|r "..tostring(message))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Client → Server: ничего регистрировать не нужно — это просто AIO.Handle
|
|
||||||
-- внутри C_PlayerGuide.RequestRefresh / RequestTrackObjective / RequestOpenTarget.
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
PlayerGuide.lua
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
UI-логика вкладки «Путеводитель». Парный файл PlayerGuide.xml.
|
|
||||||
|
|
||||||
Зависимости:
|
|
||||||
* C_PlayerGuide (Utils/C_PlayerGuide.lua) — данные и API
|
|
||||||
* MW_PLAYER_GUIDE* (Localization/ruRU.lua) — строки
|
|
||||||
* EncounterJournal (Custom_EncounterJournal) — родительский фрейм
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Конфигурация фичи
|
|
||||||
|
|
||||||
local SHOW_REWARDS = false -- блок наград временно отключён
|
|
||||||
local SHOW_RECOMMENDATIONS = true
|
|
||||||
local DENSITY = "compact" -- compact | regular | cozy
|
|
||||||
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 } -- золото
|
|
||||||
|
|
||||||
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
|
|
||||||
|
|
||||||
-- Биом → цвет «обложки» рекомендации (rgb 0..1).
|
|
||||||
-- В будущем заменить на текстуры Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*.
|
|
||||||
local BIOME_COLOR = {
|
|
||||||
fire = { 0.83, 0.29, 0.10 },
|
|
||||||
arcane = { 0.48, 0.31, 0.83 },
|
|
||||||
storm = { 0.24, 0.56, 0.77 },
|
|
||||||
shadow = { 0.35, 0.16, 0.54 },
|
|
||||||
nature = { 0.31, 0.63, 0.29 },
|
|
||||||
holy = { 0.83, 0.69, 0.24 },
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Тип цели → глиф (заменить на иконки из BLP, когда будет атлас).
|
|
||||||
local TYPE_GLYPH = {
|
|
||||||
level = "⚔",
|
|
||||||
quest = "!",
|
|
||||||
quest_chain = "!",
|
|
||||||
dungeon = "⛨",
|
|
||||||
raid = "☠",
|
|
||||||
boss = "☠",
|
|
||||||
achievement = "★",
|
|
||||||
reputation = "◈",
|
|
||||||
profession = "⚒",
|
|
||||||
item = "◆",
|
|
||||||
currency = "✦",
|
|
||||||
custom = "∗",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Тип цели → плейсхолдер-иконка из встроенных текстур 3.3.5a.
|
|
||||||
local TYPE_ICON = {
|
|
||||||
level = "Interface\\Icons\\Ability_Warrior_Challange",
|
|
||||||
quest = "Interface\\Icons\\INV_Misc_Note_01",
|
|
||||||
quest_chain = "Interface\\Icons\\INV_Misc_Note_05",
|
|
||||||
dungeon = "Interface\\Icons\\INV_Misc_Key_06",
|
|
||||||
raid = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
|
|
||||||
boss = "Interface\\Icons\\Ability_Warrior_DecisiveStrike",
|
|
||||||
achievement = "Interface\\Icons\\Achievement_GuildPerk_HonorableMention",
|
|
||||||
reputation = "Interface\\Icons\\INV_Scroll_03",
|
|
||||||
profession = "Interface\\Icons\\Trade_Engineering",
|
|
||||||
item = "Interface\\Icons\\INV_Misc_Gem_Diamond_02",
|
|
||||||
currency = "Interface\\Icons\\INV_Misc_Coin_17",
|
|
||||||
custom = "Interface\\Icons\\INV_Misc_QuestionMark",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Хелперы
|
|
||||||
|
|
||||||
local function pct(cur, req)
|
|
||||||
if not req or req <= 0 then return 0 end
|
|
||||||
return math.floor((cur / req) * 100 + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setRGB(fs, c) fs:SetTextColor(c.r or c[1], c.g or c[2], c.b or c[3]) end
|
|
||||||
|
|
||||||
local function showOrHide(frame, show)
|
|
||||||
if not frame then return end
|
|
||||||
if show then frame:Show() else frame:Hide() end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Цепочка этапов
|
|
||||||
|
|
||||||
local stageBadges = {} -- кэш созданных бейджей по индексу
|
|
||||||
|
|
||||||
local function buildStageChain(stages)
|
|
||||||
if not PlayerGuideFrame then return end
|
|
||||||
local container = PlayerGuideFrame.StageChain
|
|
||||||
|
|
||||||
-- Прячем лишние бейджи, если этапов стало меньше
|
|
||||||
for i = (#stages + 1), #stageBadges do
|
|
||||||
stageBadges[i]:Hide()
|
|
||||||
end
|
|
||||||
|
|
||||||
if #stages == 0 then return end
|
|
||||||
|
|
||||||
local width = container:GetWidth()
|
|
||||||
if width == 0 then width = 600 end
|
|
||||||
local slot = width / #stages
|
|
||||||
|
|
||||||
for i, s in ipairs(stages) do
|
|
||||||
local badge = stageBadges[i]
|
|
||||||
if not badge then
|
|
||||||
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
|
|
||||||
stageBadges[i] = badge
|
|
||||||
end
|
|
||||||
badge:ClearAllPoints()
|
|
||||||
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
|
|
||||||
badge:Show()
|
|
||||||
|
|
||||||
-- Текст
|
|
||||||
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
|
|
||||||
badge.Label:SetText(s.short or s.name or "")
|
|
||||||
|
|
||||||
-- Цветовая схема по статусу
|
|
||||||
local status = s.status or "locked"
|
|
||||||
if status == "active" then
|
|
||||||
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
|
|
||||||
badge.Label:SetTextColor(1.0, 0.84, 0.42)
|
|
||||||
badge.BadgeBG:SetVertexColor(1.0, 0.84, 0.42, 1)
|
|
||||||
elseif status == "done" then
|
|
||||||
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
|
|
||||||
badge.Label:SetTextColor(0.79, 0.65, 0.38)
|
|
||||||
badge.BadgeBG:SetVertexColor(0.55, 0.40, 0.18, 1)
|
|
||||||
-- Можно заменить римскую цифру на «✓» для завершённых:
|
|
||||||
badge.Roman:SetText("✓")
|
|
||||||
else
|
|
||||||
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
|
|
||||||
badge.Label:SetTextColor(0.40, 0.34, 0.22)
|
|
||||||
badge.BadgeBG:SetVertexColor(0.20, 0.15, 0.08, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Карточки целей
|
|
||||||
|
|
||||||
local objectiveCards = {}
|
|
||||||
|
|
||||||
local CARD_HEIGHT = { compact = 44, regular = 56, cozy = 72 }
|
|
||||||
local CARD_GAP = { compact = 4, regular = 6, cozy = 8 }
|
|
||||||
|
|
||||||
local function renderObjectives()
|
|
||||||
local section = PlayerGuideFrame.ObjectivesSection
|
|
||||||
local parent = section.ScrollFrame.ScrollChild
|
|
||||||
local n = C_PlayerGuide.GetNumObjectives()
|
|
||||||
|
|
||||||
-- Скрыть лишние
|
|
||||||
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
|
|
||||||
|
|
||||||
local h = CARD_HEIGHT[DENSITY] or 44
|
|
||||||
local gap = CARD_GAP[DENSITY] or 4
|
|
||||||
local done = 0
|
|
||||||
|
|
||||||
for i = 1, n do
|
|
||||||
local o = C_PlayerGuide.GetObjectiveInfo(i)
|
|
||||||
if not o then break end
|
|
||||||
|
|
||||||
local card = objectiveCards[i]
|
|
||||||
if not card then
|
|
||||||
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
|
|
||||||
objectiveCards[i] = card
|
|
||||||
end
|
|
||||||
card:ClearAllPoints()
|
|
||||||
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
|
|
||||||
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
|
|
||||||
card:SetHeight(h)
|
|
||||||
card.objectiveData = o
|
|
||||||
card:Show()
|
|
||||||
|
|
||||||
-- Иконка типа
|
|
||||||
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
|
|
||||||
card.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) -- crop borders Blizz-стиль
|
|
||||||
|
|
||||||
-- Тип-плашка caps
|
|
||||||
card.TypeLabel:SetText(_G["MW_PG_TYPE_"..string.upper(o.type)] or string.upper(o.type))
|
|
||||||
|
|
||||||
-- Заголовок и подзаголовок
|
|
||||||
card.Title:SetText(o.title or "")
|
|
||||||
card.Subtitle:SetText(o.subtitle or o.description or "")
|
|
||||||
|
|
||||||
-- Прогресс
|
|
||||||
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
|
|
||||||
card.ProgressBar:SetValue(o.progress or 0)
|
|
||||||
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
|
|
||||||
|
|
||||||
-- Цвет по статусу
|
|
||||||
if o.completed then
|
|
||||||
done = done + 1
|
|
||||||
card.Title:SetTextColor(0.62, 0.84, 0.47)
|
|
||||||
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
|
|
||||||
card.Count:SetTextColor(0.62, 0.84, 0.47)
|
|
||||||
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
|
|
||||||
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
|
|
||||||
else
|
|
||||||
card.Title:SetTextColor(1.0, 0.91, 0.63)
|
|
||||||
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
|
|
||||||
card.Count:SetTextColor(0.79, 0.65, 0.38)
|
|
||||||
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
|
|
||||||
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
parent:SetHeight(math.max(1, n * (h + gap)))
|
|
||||||
section.Count:SetText(string.format("· %d / %d", done, n))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Карточки рекомендаций
|
|
||||||
|
|
||||||
local recCards = {}
|
|
||||||
|
|
||||||
local function renderRecommendations()
|
|
||||||
local section = PlayerGuideFrame.RecommendationsSection
|
|
||||||
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
|
|
||||||
|
|
||||||
local parent = section.ScrollFrame.ScrollChild
|
|
||||||
local n = C_PlayerGuide.GetNumRecommendations()
|
|
||||||
if n == 0 then section:Hide(); return end
|
|
||||||
section:Show()
|
|
||||||
|
|
||||||
for i = (n + 1), #recCards do recCards[i]:Hide() end
|
|
||||||
|
|
||||||
local cardW = 148
|
|
||||||
local gap = 8
|
|
||||||
|
|
||||||
for i = 1, n do
|
|
||||||
local r = C_PlayerGuide.GetRecommendationInfo(i)
|
|
||||||
if not r then break end
|
|
||||||
|
|
||||||
local card = recCards[i]
|
|
||||||
if not card then
|
|
||||||
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
|
|
||||||
recCards[i] = card
|
|
||||||
end
|
|
||||||
card:ClearAllPoints()
|
|
||||||
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
|
|
||||||
card.recData = r
|
|
||||||
card:Show()
|
|
||||||
|
|
||||||
local biome = BIOME_COLOR[r.biome] or BIOME_COLOR.fire
|
|
||||||
card.Artwork:SetVertexColor(biome[1], biome[2], biome[3], 1)
|
|
||||||
card.Name:SetText(r.name or "")
|
|
||||||
card.Loc:SetText(r.loc or "")
|
|
||||||
card.Level:SetText(r.level or "")
|
|
||||||
end
|
|
||||||
|
|
||||||
parent:SetWidth(math.max(1, n * (cardW + gap)))
|
|
||||||
section.Count:SetText("· "..n)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Главный Refresh
|
|
||||||
|
|
||||||
local function showState(state)
|
|
||||||
local F = PlayerGuideFrame
|
|
||||||
local hasData = (state == "data")
|
|
||||||
|
|
||||||
showOrHide(F.StageChain, hasData)
|
|
||||||
showOrHide(F.Header, hasData)
|
|
||||||
showOrHide(F.ObjectivesSection, hasData)
|
|
||||||
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
|
|
||||||
showOrHide(F.EmptyState, state == "empty")
|
|
||||||
showOrHide(F.LoadingState, state == "loading")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function refresh()
|
|
||||||
if not PlayerGuideFrame then return end
|
|
||||||
|
|
||||||
local state = C_PlayerGuide.GetState()
|
|
||||||
showState(state)
|
|
||||||
|
|
||||||
if state ~= "data" then return end
|
|
||||||
|
|
||||||
-- Заголовок этапа
|
|
||||||
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
|
|
||||||
local stages = C_PlayerGuide.GetStages() or {}
|
|
||||||
|
|
||||||
PlayerGuideFrame.Header.Pretitle:SetText(
|
|
||||||
string.format(MW_PLAYER_GUIDE_STAGE_OF or "Этап %d из %d", id or 0, #stages))
|
|
||||||
PlayerGuideFrame.Header.Title:SetText(name or "")
|
|
||||||
PlayerGuideFrame.Header.Description:SetText(desc or "")
|
|
||||||
PlayerGuideFrame.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
|
|
||||||
PlayerGuideFrame.Header.ProgressPercent:SetText((progress or 0) .. "%")
|
|
||||||
PlayerGuideFrame.Header.ProgressBar:SetMinMaxValues(0, 100)
|
|
||||||
PlayerGuideFrame.Header.ProgressBar:SetValue(progress or 0)
|
|
||||||
|
|
||||||
buildStageChain(stages)
|
|
||||||
renderObjectives()
|
|
||||||
renderRecommendations()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- XML script handlers
|
|
||||||
|
|
||||||
function MW_PlayerGuide_OnLoad(self)
|
|
||||||
-- Подписываемся на изменения C_PlayerGuide
|
|
||||||
C_PlayerGuide.RegisterListener(function() refresh() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_OnShow(self)
|
|
||||||
-- Если данных ещё нет — попросим сервер
|
|
||||||
if not C_PlayerGuide.HasData() then
|
|
||||||
C_PlayerGuide.SetLoading()
|
|
||||||
C_PlayerGuide.RequestRefresh()
|
|
||||||
end
|
|
||||||
refresh()
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_RefreshButton_OnClick(self)
|
|
||||||
C_PlayerGuide.SetLoading()
|
|
||||||
C_PlayerGuide.RequestRefresh()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Клик по карточке цели — открывает связанный таргет (см. требования)
|
|
||||||
function MW_PlayerGuide_Objective_OnClick(self)
|
|
||||||
local o = self.objectiveData
|
|
||||||
if not o then return end
|
|
||||||
|
|
||||||
if o.type == "dungeon" or o.type == "raid" then
|
|
||||||
if o.instanceID and EncounterJournal_OpenJournal then
|
|
||||||
EncounterJournal_OpenJournal(o.instanceID)
|
|
||||||
end
|
|
||||||
elseif o.type == "boss" then
|
|
||||||
if o.encounterID and EncounterJournal_OpenJournal then
|
|
||||||
EncounterJournal_OpenJournal(o.instanceID, o.encounterID)
|
|
||||||
end
|
|
||||||
elseif o.type == "achievement" and o.achievementID then
|
|
||||||
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
|
|
||||||
if AchievementFrame_SelectAchievement then
|
|
||||||
AchievementFrame_SelectAchievement(o.achievementID)
|
|
||||||
end
|
|
||||||
elseif o.type == "item" and o.itemID then
|
|
||||||
-- Запрос подсказки предмета
|
|
||||||
if GameTooltip then
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
||||||
GameTooltip:SetItemByID(o.itemID)
|
|
||||||
GameTooltip:Show()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Любой нестандартный тип — пытаемся попросить сервер открыть таргет
|
|
||||||
C_PlayerGuide.RequestOpenTarget(o.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_Objective_OnEnter(self)
|
|
||||||
local o = self.objectiveData
|
|
||||||
if not o then return end
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
||||||
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
|
|
||||||
if o.subtitle and o.subtitle ~= "" then
|
|
||||||
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
|
|
||||||
end
|
|
||||||
if not o.completed then
|
|
||||||
GameTooltip:AddLine(string.format("Прогресс: %d / %d", o.progress, o.required),
|
|
||||||
0.79, 0.65, 0.38)
|
|
||||||
end
|
|
||||||
GameTooltip:Show()
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_Objective_OnLeave(self) GameTooltip:Hide() end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnClick(self)
|
|
||||||
local r = self.recData
|
|
||||||
if r and r.instanceID and EncounterJournal_OpenJournal then
|
|
||||||
EncounterJournal_OpenJournal(r.instanceID)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnEnter(self)
|
|
||||||
local r = self.recData
|
|
||||||
if not r then return end
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
||||||
GameTooltip:AddLine(r.name or "", 1, 0.91, 0.63)
|
|
||||||
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
|
|
||||||
if r.level then GameTooltip:AddLine("Уровень: "..r.level, 0.79, 0.65, 0.38) end
|
|
||||||
GameTooltip:Show()
|
|
||||||
end
|
|
||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnLeave(self) GameTooltip:Hide() end
|
|
||||||
|
|
||||||
-- ──────────────────────────────────────────────────────────────────────────
|
|
||||||
-- Публичные хелперы для Custom_EncounterJournal.lua (routing вкладки)
|
|
||||||
|
|
||||||
function PlayerGuide_Show()
|
|
||||||
if PlayerGuideFrame then
|
|
||||||
PlayerGuideFrame:Show()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PlayerGuide_Hide()
|
|
||||||
if PlayerGuideFrame then
|
|
||||||
PlayerGuideFrame:Hide()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Глобальный экспорт для отладки
|
|
||||||
MW_PlayerGuide = {
|
|
||||||
Refresh = refresh,
|
|
||||||
Show = PlayerGuide_Show,
|
|
||||||
Hide = PlayerGuide_Hide,
|
|
||||||
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
|
|
||||||
}
|
|
||||||
@@ -1,498 +0,0 @@
|
|||||||
<Ui xmlns="http://www.blizzard.com/wow/ui/"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
|
||||||
..\FrameXML\UI.xsd">
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
PlayerGuide.xml — вкладка «Путеводитель» в Encounter Journal
|
|
||||||
Канон WotLK: тёмно-синий фон, золотые акценты, Friz Quadrata.
|
|
||||||
Подключается ПОСЛЕ Custom_EncounterJournal.xml в .toc, чтобы
|
|
||||||
PlayerGuideFrame мог анкориться к EncounterJournal как parent.
|
|
||||||
============================================================ -->
|
|
||||||
|
|
||||||
<!-- ───────── Шрифты (Friz Quadrata из клиента 3.3.5a) ───────── -->
|
|
||||||
|
|
||||||
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
|
|
||||||
<Color r="1.0" g="0.84" b="0.42"/>
|
|
||||||
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
|
|
||||||
</Font>
|
|
||||||
|
|
||||||
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
|
|
||||||
<Color r="1.0" g="0.84" b="0.42"/>
|
|
||||||
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
|
|
||||||
</Font>
|
|
||||||
|
|
||||||
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
|
|
||||||
<Color r="0.85" g="0.78" b="0.63"/>
|
|
||||||
</Font>
|
|
||||||
|
|
||||||
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
|
|
||||||
<Color r="0.63" g="0.46" b="0.19"/>
|
|
||||||
</Font>
|
|
||||||
|
|
||||||
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
|
|
||||||
<Color r="0.66" g="0.59" b="0.47"/>
|
|
||||||
</Font>
|
|
||||||
|
|
||||||
<!-- ───────── Backdrop'ы ───────── -->
|
|
||||||
|
|
||||||
<!-- Карточка цели (полупрозрачная пергамент-плашка) -->
|
|
||||||
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
|
|
||||||
<Size><AbsDimension x="0" y="44"/></Size>
|
|
||||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
|
|
||||||
edgeFile="Interface\Tooltips\UI-Tooltip-Border"
|
|
||||||
tile="true">
|
|
||||||
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
|
|
||||||
<TileSize><AbsValue val="16"/></TileSize>
|
|
||||||
<EdgeSize><AbsValue val="12"/></EdgeSize>
|
|
||||||
<Color r="0.22" g="0.15" b="0.07" a="0.65"/>
|
|
||||||
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
|
|
||||||
</Backdrop>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<!-- Иконка типа цели (32×32, золотая рамка применяется отдельной текстурой) -->
|
|
||||||
<Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
|
|
||||||
<Size><AbsDimension x="32" y="32"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
|
|
||||||
<Size><AbsDimension x="42" y="42"/></Size>
|
|
||||||
<Anchors><Anchor point="CENTER" relativeKey="$parent.Icon"/></Anchors>
|
|
||||||
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
|
|
||||||
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
|
|
||||||
<Size><AbsDimension x="100" y="10"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPRIGHT">
|
|
||||||
<Offset><AbsDimension x="10" y="-1"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="TOPRIGHT">
|
|
||||||
<Offset><AbsDimension x="6" y="0"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="0" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
|
|
||||||
<Size><AbsDimension x="80" y="12"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-10" y="-6"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.79" g="0.65" b="0.38"/>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
|
|
||||||
<Frames>
|
|
||||||
<StatusBar parentKey="ProgressBar">
|
|
||||||
<Size><AbsDimension x="0" y="6"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeKey="$parent.Icon" relativePoint="RIGHT">
|
|
||||||
<Offset><AbsDimension x="10" y="-12"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="-12"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
|
||||||
<BarColor r="1.0" g="0.79" b="0.29"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
|
|
||||||
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</StatusBar>
|
|
||||||
</Frames>
|
|
||||||
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
|
|
||||||
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
|
|
||||||
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- Карточка рекомендованного подземелья -->
|
|
||||||
<Button name="MW_PG_RecCardTemplate" virtual="true">
|
|
||||||
<Size><AbsDimension x="148" y="106"/></Size>
|
|
||||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
|
|
||||||
edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
|
||||||
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
|
|
||||||
<TileSize><AbsValue val="16"/></TileSize>
|
|
||||||
<EdgeSize><AbsValue val="12"/></EdgeSize>
|
|
||||||
<Color r="0.10" g="0.07" b="0.03" a="0.85"/>
|
|
||||||
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
|
|
||||||
</Backdrop>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<!-- Биом-плашка (плейсхолдер: цветной градиент) -->
|
|
||||||
<Texture parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
|
|
||||||
<Size><AbsDimension x="138" y="58"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.55" g="0.16" b="0.07"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Level" inherits="GameFontNormalSmall" justifyH="CENTER">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.Artwork" relativePoint="TOPRIGHT">
|
|
||||||
<Offset><AbsDimension x="-4" y="-4"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
|
|
||||||
<Size><AbsDimension x="130" y="12"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Artwork" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="2" y="-4"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
|
|
||||||
<Size><AbsDimension x="130" y="10"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Name" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="0" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
|
|
||||||
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
|
|
||||||
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Бейдж этапа в цепочке (круглая золотая монета с цифрой) -->
|
|
||||||
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
|
|
||||||
<Size><AbsDimension x="38" y="38"/></Size>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture parentKey="BadgeBG" file="Interface\Buttons\UI-Quickslot2">
|
|
||||||
<Size><AbsDimension x="60" y="60"/></Size>
|
|
||||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
|
||||||
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
|
|
||||||
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
|
|
||||||
<Size><AbsDimension x="100" y="10"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- ───────── Корневой фрейм ───────── -->
|
|
||||||
<!-- Парент: EncounterJournal.encounter (см. PlayerGuide.lua → Init) -->
|
|
||||||
|
|
||||||
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-58"/></Offset></Anchor>
|
|
||||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
|
|
||||||
<Layers>
|
|
||||||
<!-- «Волна» сверху, как в оригинальном EJ — добавляется через
|
|
||||||
родительский фон. Здесь — тонкая золотая внутренняя рамка. -->
|
|
||||||
<Layer level="BORDER">
|
|
||||||
<Texture parentKey="TopGlow" file="Interface\Common\UI-TooltipDivider-Transparent">
|
|
||||||
<Size><AbsDimension x="0" y="16"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
|
|
||||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1.0" g="0.84" b="0.42" a="0.35"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
|
|
||||||
<Frames>
|
|
||||||
|
|
||||||
<!-- 1) Цепочка этапов сверху -->
|
|
||||||
<Frame parentKey="StageChain">
|
|
||||||
<Size><AbsDimension x="0" y="62"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-4"/></Offset></Anchor>
|
|
||||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-8" y="-4"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<!-- Бейджи создаются программно в Lua (PlayerGuide_BuildStageChain) -->
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- 2) Заголовок текущего этапа -->
|
|
||||||
<Frame parentKey="Header">
|
|
||||||
<Size><AbsDimension x="0" y="78"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.StageChain" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="12" y="-12"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.StageChain" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="-12" y="-12"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Pretitle" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="0" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Title" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="2" y="-6"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
|
|
||||||
<FontString parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
|
|
||||||
<Size><AbsDimension x="240" y="28"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressLabel" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="0" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<StatusBar parentKey="ProgressBar">
|
|
||||||
<Size><AbsDimension x="240" y="10"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressPercent" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="0" y="-4"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
|
||||||
<BarColor r="1.0" g="0.79" b="0.29"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
|
|
||||||
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</StatusBar>
|
|
||||||
|
|
||||||
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
|
|
||||||
<Size><AbsDimension x="100" y="22"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressBar" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="0" y="-6"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- 3) Секция «Задачи этапа» — заголовок + список карточек -->
|
|
||||||
<Frame parentKey="ObjectivesSection">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeKey="$parent.Header" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="-2" y="-10"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="TOPRIGHT" relativeKey="$parent.Header" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="2" y="-10"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="142"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
|
|
||||||
<Offset><AbsDimension x="8" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
|
|
||||||
<Size><AbsDimension x="0" y="2"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="0" y="-4"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
|
|
||||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<ScrollChild>
|
|
||||||
<Frame parentKey="ScrollChild">
|
|
||||||
<Size><AbsDimension x="600" y="200"/></Size>
|
|
||||||
<!-- Карточки целей вставляются программно через ObjectiveCardTemplate -->
|
|
||||||
</Frame>
|
|
||||||
</ScrollChild>
|
|
||||||
</ScrollFrame>
|
|
||||||
</Frames>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- 4) Секция «Подходящие подземелья» — горизонтальная карусель -->
|
|
||||||
<Frame parentKey="RecommendationsSection">
|
|
||||||
<Size><AbsDimension x="0" y="132"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="6" y="6"/></Offset></Anchor>
|
|
||||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
|
|
||||||
<Offset><AbsDimension x="8" y="-2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
|
|
||||||
<Size><AbsDimension x="0" y="2"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
|
|
||||||
<Offset><AbsDimension x="0" y="-4"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
|
|
||||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<ScrollChild>
|
|
||||||
<Frame parentKey="ScrollChild">
|
|
||||||
<Size><AbsDimension x="900" y="110"/></Size>
|
|
||||||
</Frame>
|
|
||||||
</ScrollChild>
|
|
||||||
</ScrollFrame>
|
|
||||||
</Frames>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- 5) Пустое состояние -->
|
|
||||||
<Frame parentKey="EmptyState" hidden="true">
|
|
||||||
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
|
|
||||||
<Size><AbsDimension x="460" y="200"/></Size>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
|
|
||||||
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
|
|
||||||
<Size><AbsDimension x="460" y="28"/></Size>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
|
|
||||||
<Offset><AbsDimension x="0" y="-10"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Size><AbsDimension x="420" y="60"/></Size>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
|
|
||||||
<Size><AbsDimension x="120" y="24"/></Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP" relativeKey="$parent.Body" relativePoint="BOTTOM">
|
|
||||||
<Offset><AbsDimension x="0" y="-14"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<!-- 6) Состояние загрузки -->
|
|
||||||
<Frame parentKey="LoadingState" hidden="true">
|
|
||||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
|
||||||
<Size><AbsDimension x="300" y="80"/></Size>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<FontString parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
|
|
||||||
<Anchors><Anchor point="TOP"/></Anchors>
|
|
||||||
<Size><AbsDimension x="300" y="20"/></Size>
|
|
||||||
</FontString>
|
|
||||||
<FontString parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
|
|
||||||
<Offset><AbsDimension x="0" y="-6"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Size><AbsDimension x="300" y="16"/></Size>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
</Frames>
|
|
||||||
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad function="MW_PlayerGuide_OnLoad"/>
|
|
||||||
<OnShow function="MW_PlayerGuide_OnShow"/>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
</Ui>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# PlayerGuide — клиентский пакет (3.3.5a / MoonWellClient)
|
|
||||||
|
|
||||||
Это материалы для интеграции вкладки **«Путеводитель»** в существующий
|
|
||||||
ретропортированный Encounter Journal. Реализация соответствует требованиям
|
|
||||||
из `uploads/player-guide-requirements.md` и опирается на канон-визуал
|
|
||||||
WotLK (Friz Quadrata, тёмно-синий фон EJ, золотые акценты).
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
|
|
||||||
```
|
|
||||||
MoonWellClient/
|
|
||||||
├── EncounterJournal/
|
|
||||||
│ ├── Utils/
|
|
||||||
│ │ └── C_PlayerGuide.lua ← хранилище + API
|
|
||||||
│ ├── Custom_EncounterJournal/
|
|
||||||
│ │ ├── Custom_EncounterJournal.xml ← патч: новая вкладка
|
|
||||||
│ │ ├── Custom_EncounterJournal.lua ← патч: routing вкладки
|
|
||||||
│ │ ├── PlayerGuide.xml ← фреймы и шаблоны (НОВОЕ)
|
|
||||||
│ │ └── PlayerGuide.lua ← рендер и логика (НОВОЕ)
|
|
||||||
│ └── Localization/
|
|
||||||
│ └── ruRU.lua ← патч: MW_PLAYER_GUIDE и др.
|
|
||||||
├── AIO/
|
|
||||||
│ └── MoonWellPlayerGuide.lua ← AIO-handler (клиентская часть)
|
|
||||||
└── MoonWellClient.toc ← патч: подключение файлов
|
|
||||||
```
|
|
||||||
|
|
||||||
## Порядок интеграции
|
|
||||||
|
|
||||||
1. **Скопируйте новые файлы** в проект:
|
|
||||||
* `client-export/PlayerGuide.xml` → `EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml`
|
|
||||||
* `client-export/PlayerGuide.lua` → `EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua`
|
|
||||||
* `client-export/C_PlayerGuide.lua` → `EncounterJournal/Utils/C_PlayerGuide.lua`
|
|
||||||
* `client-export/MoonWellPlayerGuide.lua` → `AIO/MoonWellPlayerGuide.lua`
|
|
||||||
2. **Примените патчи** из `client-export/patches/` к существующим файлам
|
|
||||||
(см. ниже — `Custom_EncounterJournal.xml`, `Custom_EncounterJournal.lua`,
|
|
||||||
`MoonWellClient.toc`, `Localization/ruRU.lua`).
|
|
||||||
3. **Перезагрузите интерфейс** (`/reload`). Если AIO ещё не передал
|
|
||||||
payload — увидите «пустое» состояние с кнопкой «Обновить».
|
|
||||||
|
|
||||||
## Что вошло в MVP (по требованиям)
|
|
||||||
|
|
||||||
| MVP-пункт | Где |
|
|
||||||
| ----------------------------- | ------------------------------------------- |
|
|
||||||
| вкладка `Путеводитель` | `Custom_EncounterJournal.xml` патч |
|
|
||||||
| пустой `PlayerGuideFrame` | `PlayerGuide.xml` |
|
|
||||||
| `C_PlayerGuide.lua` + статика | `C_PlayerGuide.lua` |
|
|
||||||
| заголовок этапа + цели | `PlayerGuide.lua → PlayerGuide_Refresh` |
|
|
||||||
| tab switching / hide-show | `Custom_EncounterJournal.lua → EJ_ContentTab_OnClick` хук |
|
|
||||||
| AIO `SetData` | `MoonWellPlayerGuide.lua → handlers.SetData` |
|
|
||||||
| Обновить (RequestRefresh) | `PlayerGuide.lua → PlayerGuide_RefreshButton_OnClick` |
|
|
||||||
|
|
||||||
## Что отложено (см. «Later UI» в требованиях)
|
|
||||||
|
|
||||||
* Фильтры (все / активные / завершённые)
|
|
||||||
* Pinning отслеживаемой цели
|
|
||||||
* **Награды** — блок временно отключён (`SHOW_REWARDS = false`)
|
|
||||||
* Серверные уведомления (`Notify`)
|
|
||||||
|
|
||||||
## Сноска по визуалу
|
|
||||||
|
|
||||||
Текстуры в XML — это встроенные ресурсы 3.3.5a-клиента. Если у вас
|
|
||||||
есть атлас MoonWell — замените пути в блоке `-- TEXTURE_ATLAS` на свои.
|
|
||||||
Биом-обложки рекомендаций (`Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*`)
|
|
||||||
надо создать отдельно — это плейсхолдеры (см. таблицу в `PlayerGuide.lua`).
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
patches/Custom_EncounterJournal.lua.patch
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
Патч-сниппеты для существующего Custom_EncounterJournal.lua. Эти куски
|
|
||||||
добавляют routing новой вкладки и hide-show поведение по требованиям:
|
|
||||||
|
|
||||||
«при выборе guide tab:
|
|
||||||
- hide instance cards
|
|
||||||
- hide Encounter Journal detail panel
|
|
||||||
- hide suggested content panel
|
|
||||||
- hide loot journal panel
|
|
||||||
- hide or disable the expansion/tier dropdown unless explicitly needed
|
|
||||||
- show PlayerGuideFrame»
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
|
|
||||||
-- НАЙТИ функцию EJ_ContentTab_Select (или ту, что переключает вкладки в
|
|
||||||
-- Encounter Journal). Обычно она выглядит как switch по id вкладки.
|
|
||||||
-- ДОБАВИТЬ ветку для нашей вкладки. Пример полностью переписанной функции:
|
|
||||||
|
|
||||||
local PLAYERGUIDE_TAB_ID = 1 -- должен совпадать с id="1" в XML патче
|
|
||||||
|
|
||||||
function EJ_ContentTab_Select(id)
|
|
||||||
EncounterJournal.suggestTab:UnlockHighlight()
|
|
||||||
EncounterJournal.dungeonsTab:UnlockHighlight()
|
|
||||||
EncounterJournal.raidsTab:UnlockHighlight()
|
|
||||||
if EncounterJournal.lootJournalTab then
|
|
||||||
EncounterJournal.lootJournalTab:UnlockHighlight()
|
|
||||||
end
|
|
||||||
if EncounterJournal.guideTab then
|
|
||||||
EncounterJournal.guideTab:UnlockHighlight()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Прячем «обычные» панели EJ. Этот хелпер уже есть в Custom_EncounterJournal:
|
|
||||||
-- EncounterJournal_HideAllPanels()
|
|
||||||
-- Если его нет — выньте тело сюда (Hide instance list, encounter, loot).
|
|
||||||
EncounterJournal_HideAllPanels()
|
|
||||||
|
|
||||||
-- Прячем PlayerGuideFrame — он показывается только на guide-вкладке.
|
|
||||||
if PlayerGuideFrame then PlayerGuideFrame:Hide() end
|
|
||||||
|
|
||||||
if id == PLAYERGUIDE_TAB_ID then
|
|
||||||
-- НАША ВКЛАДКА -----------------------------------------------------
|
|
||||||
EncounterJournal.guideTab:LockHighlight()
|
|
||||||
EncounterJournal.instanceSelect:Hide()
|
|
||||||
EncounterJournal.encounter:Hide()
|
|
||||||
if EncounterJournalSuggestFrame then EncounterJournalSuggestFrame:Hide() end
|
|
||||||
if LootJournal then LootJournal:Hide() end
|
|
||||||
-- Скрыть/задисейблить дропдаун расширений, как просят требования:
|
|
||||||
if EJTierDropDown then EJTierDropDown:Hide() end
|
|
||||||
|
|
||||||
if PlayerGuideFrame then PlayerGuideFrame:Show() end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ── Остальные вкладки — оригинальная логика ──
|
|
||||||
if EJTierDropDown then EJTierDropDown:Show() end
|
|
||||||
|
|
||||||
if id == 2 then -- SuggestTab (было 1)
|
|
||||||
EncounterJournal.suggestTab:LockHighlight()
|
|
||||||
EncounterJournal_ListInstances() -- или ваш Suggest-Show
|
|
||||||
elseif id == 3 then -- DungeonTab
|
|
||||||
EncounterJournal.dungeonsTab:LockHighlight()
|
|
||||||
EJ_HideNonInstancePanels()
|
|
||||||
EncounterJournal_ListInstances()
|
|
||||||
elseif id == 4 then -- RaidTab
|
|
||||||
EncounterJournal.raidsTab:LockHighlight()
|
|
||||||
EJ_HideNonInstancePanels()
|
|
||||||
EncounterJournal_ListInstances()
|
|
||||||
elseif id == 5 then -- LootJournalTab
|
|
||||||
if EncounterJournal.lootJournalTab then
|
|
||||||
EncounterJournal.lootJournalTab:LockHighlight()
|
|
||||||
end
|
|
||||||
if LootJournal_Show then LootJournal_Show() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
|
|
||||||
-- В EJ_ContentTab_OnClick добавить вызов нашего Select для tab id=1
|
|
||||||
-- (если у вас один централизованный _OnClick — он уже передаёт id и
|
|
||||||
-- ничего дополнительно делать не нужно):
|
|
||||||
|
|
||||||
function EJ_ContentTab_OnClick(self)
|
|
||||||
EJ_ContentTab_Select(self:GetID())
|
|
||||||
PlaySound("igCharacterInfoTab")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
|
|
||||||
-- Защита: при открытии EJ через EncounterJournal_OpenJournal — переключиться
|
|
||||||
-- НА dungeon/raid таб, а не оставлять guide-таб выбранным.
|
|
||||||
-- Найдите вашу EncounterJournal_OpenJournal и убедитесь, что в начале:
|
|
||||||
|
|
||||||
local _orig_EncounterJournal_OpenJournal = EncounterJournal_OpenJournal
|
|
||||||
function EncounterJournal_OpenJournal(instanceID, encounterID, ...)
|
|
||||||
-- При прямом открытии конкретного instance — переключиться на Dungeon/Raid
|
|
||||||
if instanceID then
|
|
||||||
EJ_ContentTab_Select(3) -- Dungeon по умолчанию; ваша логика может выбрать Raid
|
|
||||||
end
|
|
||||||
return _orig_EncounterJournal_OpenJournal(instanceID, encounterID, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #4 ───────────────────────────────────────────────────────────────
|
|
||||||
-- Init: статика для разработки (можно убрать перед релизом). Если AIO
|
|
||||||
-- ещё не успел прислать payload — покажем dev-payload через 1с после
|
|
||||||
-- первого открытия EJ.
|
|
||||||
|
|
||||||
local _devInjected = false
|
|
||||||
EncounterJournal:HookScript("OnShow", function()
|
|
||||||
if not _devInjected and not C_PlayerGuide.HasData() then
|
|
||||||
_devInjected = true
|
|
||||||
C_Timer.After(1.0, function()
|
|
||||||
if not C_PlayerGuide.HasData() then
|
|
||||||
C_PlayerGuide._InjectDevPayload()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
patches/Custom_EncounterJournal.xml.patch
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
Это НЕ замена файла — это патч-сниппеты для существующего
|
|
||||||
Custom_EncounterJournal.xml. Вставьте обозначенные блоки.
|
|
||||||
|
|
||||||
В оригинальном Custom_EncounterJournal.xml внутри EncounterJournal.bg или
|
|
||||||
аналогичного контейнера уже есть вкладки SuggestTab / DungeonTab / RaidTab /
|
|
||||||
LootJournalTab. Нужно вставить GuideTab ПЕРЕД ними (id=1) и сдвинуть
|
|
||||||
анкоры остальных.
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
|
|
||||||
-- НАЙТИ блок вкладки SuggestTab (или то, что у вас сейчас называется
|
|
||||||
-- «$parentSuggestTab» / «$parentDungeonTab») и ДОБАВИТЬ перед ним
|
|
||||||
-- следующую кнопку. id=1, чтобы EJ_ContentTab_OnClick знал об этой вкладке.
|
|
||||||
|
|
||||||
<Button name="$parentGuideTab"
|
|
||||||
inherits="EncounterTierTabTemplate"
|
|
||||||
text="MW_PLAYER_GUIDE"
|
|
||||||
parentKey="guideTab"
|
|
||||||
id="1">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
|
|
||||||
<Offset><AbsDimension x="40" y="2"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="EJ_ContentTab_OnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
|
|
||||||
-- ИЗМЕНИТЬ id существующих вкладок (теперь Путеводитель = 1):
|
|
||||||
--
|
|
||||||
-- SuggestTab id="2" (было 1)
|
|
||||||
-- DungeonTab id="3" (было 2)
|
|
||||||
-- RaidTab id="4" (было 3)
|
|
||||||
-- LootJournalTab id="5" (было 4)
|
|
||||||
--
|
|
||||||
-- И СДВИНУТЬ анкор первой следующей вкладки (SuggestTab), чтобы она
|
|
||||||
-- цеплялась за GuideTab:
|
|
||||||
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentGuideTab" relativePoint="BOTTOMRIGHT">
|
|
||||||
<Offset><AbsDimension x="-15" y="0"/></Offset>
|
|
||||||
</Anchor>
|
|
||||||
</Anchors>
|
|
||||||
|
|
||||||
|
|
||||||
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
|
|
||||||
-- В конце <Ui>... </Ui>, ПЕРЕД закрывающим тегом, добавить include
|
|
||||||
-- для нашего PlayerGuide.xml, чтобы PlayerGuideFrame создавался ВНУТРИ
|
|
||||||
-- родителя EncounterJournal:
|
|
||||||
|
|
||||||
<Include file="PlayerGuide.xml"/>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
--[[----------------------------------------------------------------------------
|
|
||||||
patches/Localization-ruRU.lua.patch
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
|
||||||
Строки локализации для вкладки «Путеводитель».
|
|
||||||
Вставьте этот блок в ваш существующий Localization/ruRU.lua (или
|
|
||||||
ruRU.lua в корне), либо подключите как отдельный файл и добавьте в .toc.
|
|
||||||
------------------------------------------------------------------------------]]
|
|
||||||
|
|
||||||
-- Имя вкладки
|
|
||||||
MW_PLAYER_GUIDE = "Путеводитель"
|
|
||||||
|
|
||||||
-- Заголовки секций
|
|
||||||
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
|
|
||||||
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
|
|
||||||
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
|
|
||||||
|
|
||||||
-- Шапка
|
|
||||||
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
|
|
||||||
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
|
|
||||||
MW_PLAYER_GUIDE_REFRESH = "Обновить"
|
|
||||||
|
|
||||||
-- Пустое состояние
|
|
||||||
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
|
|
||||||
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. "..
|
|
||||||
"Это может занять несколько секунд после входа в игру."
|
|
||||||
|
|
||||||
-- Загрузка
|
|
||||||
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
|
|
||||||
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
|
|
||||||
|
|
||||||
-- Caps-плашки типов целей (используются в карточке цели)
|
|
||||||
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
|
|
||||||
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
|
|
||||||
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
|
|
||||||
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
|
|
||||||
MW_PG_TYPE_RAID = "РЕЙД"
|
|
||||||
MW_PG_TYPE_BOSS = "БОСС"
|
|
||||||
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
|
|
||||||
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
|
|
||||||
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
|
|
||||||
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
|
|
||||||
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
|
|
||||||
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# patches/MoonWellClient.toc.patch
|
|
||||||
#
|
|
||||||
# Сниппет для добавления в существующий MoonWellClient.toc. ВАЖНО: порядок
|
|
||||||
# подключения файлов критичен — C_PlayerGuide.lua должен идти ДО PlayerGuide.lua,
|
|
||||||
# а PlayerGuide.xml — ПОСЛЕ Custom_EncounterJournal.xml (PlayerGuideFrame
|
|
||||||
# анкорится к EncounterJournal как parent).
|
|
||||||
#
|
|
||||||
# Вставьте строки ниже в соответствующие места вашего .toc.
|
|
||||||
|
|
||||||
# ─── Локализация ────────────────────────────────────────────────────────────
|
|
||||||
# В блок Localization, рядом с остальными ruRU.lua:
|
|
||||||
Localization\ruRU.lua
|
|
||||||
|
|
||||||
# ─── Хранилище и API ────────────────────────────────────────────────────────
|
|
||||||
# В блок EncounterJournal\Utils\, до подключения чего-либо из
|
|
||||||
# Custom_EncounterJournal\:
|
|
||||||
EncounterJournal\Utils\C_PlayerGuide.lua
|
|
||||||
|
|
||||||
# ─── Сам Encounter Journal (ОБНОВЛЁННЫЙ ПОРЯДОК) ────────────────────────────
|
|
||||||
# Эти строки заменяют ваш существующий блок Custom_EncounterJournal:
|
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
|
|
||||||
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
|
|
||||||
EncounterJournal\Custom_EncounterJournal\PlayerGuide.lua
|
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
|
|
||||||
|
|
||||||
# ─── AIO-handler ────────────────────────────────────────────────────────────
|
|
||||||
# Должен подключаться ПОСЛЕ C_PlayerGuide.lua и AIO/AIO_Client.lua:
|
|
||||||
AIO\MoonWellPlayerGuide.lua
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 307 B |
@@ -1,125 +0,0 @@
|
|||||||
# PlayerGuide — текстуры
|
|
||||||
|
|
||||||
Все файлы — **PNG с альфа-каналом**, размеры — **степень двойки**
|
|
||||||
(требование BLP-конвертера). Палитра канон-WotLK: золото (`#ffd76a`)
|
|
||||||
на тёмно-коричневом (`#1a1208`).
|
|
||||||
|
|
||||||
## Что есть
|
|
||||||
|
|
||||||
| Файл | Размер | Куда |
|
|
||||||
|---|---|---|
|
|
||||||
| `StageBadge-Active.png` | 64×64 | Бейдж текущего этапа в цепочке. |
|
|
||||||
| `StageBadge-Done.png` | 64×64 | Бейдж завершённого этапа. |
|
|
||||||
| `StageBadge-Locked.png` | 64×64 | Бейдж недоступного этапа. |
|
|
||||||
| `ProgressBar-Fill.png` | 128×16 | Заливка прогресс-бара (`BarTexture`). |
|
|
||||||
| `ProgressBar-BG.png` | 128×16 | Подложка прогресс-бара. |
|
|
||||||
| `Card-Background.png` | 256×256 | Тайл-фон карточек (`bgFile`, tileable). |
|
|
||||||
| `Card-Border.png` | 256×32 | Рамка карточки (`edgeFile`, 8 сегментов 32×32: up/down/left/right/TL/TR/BL/BR). |
|
|
||||||
| `Section-Divider.png` | 256×16 | Декоративный разделитель секций. |
|
|
||||||
| `Corner-Ornament.png` | 64×64 | Угол декоративной рамки фрейма. |
|
|
||||||
| `Top-Wave-Glow.png` | 512×64 | «Волна» свечения сверху (как в оригинальном EJ). |
|
|
||||||
| `Biome-Fire.png` | 256×128 | Обложка рекомендации — огненные локации. |
|
|
||||||
| `Biome-Arcane.png` | 256×128 | Тайная магия. |
|
|
||||||
| `Biome-Storm.png` | 256×128 | Буря/вода. |
|
|
||||||
| `Biome-Shadow.png` | 256×128 | Тьма. |
|
|
||||||
| `Biome-Nature.png` | 256×128 | Природа. |
|
|
||||||
| `Biome-Holy.png` | 256×128 | Свет/нежить. |
|
|
||||||
| `TypeIcon-Level.png` | 64×64 | Иконка цели «уровень». |
|
|
||||||
| `TypeIcon-Quest.png` | 64×64 | «задание». |
|
|
||||||
| `TypeIcon-QuestChain.png` | 64×64 | «цепочка». |
|
|
||||||
| `TypeIcon-Dungeon.png` | 64×64 | «подземелье». |
|
|
||||||
| `TypeIcon-Raid.png` | 64×64 | «рейд». |
|
|
||||||
| `TypeIcon-Boss.png` | 64×64 | «босс». |
|
|
||||||
| `TypeIcon-Achievement.png` | 64×64 | «достижение». |
|
|
||||||
| `TypeIcon-Reputation.png` | 64×64 | «репутация». |
|
|
||||||
| `TypeIcon-Profession.png` | 64×64 | «профессия». |
|
|
||||||
| `TypeIcon-Item.png` | 64×64 | «предмет». |
|
|
||||||
| `TypeIcon-Currency.png` | 64×64 | «валюта». |
|
|
||||||
| `TypeIcon-Custom.png` | 64×64 | прочее. |
|
|
||||||
| `TypeIcon-Done.png` | 64×64 | Цель завершена (зелёная галочка). |
|
|
||||||
| `TypeIcon-Locked.png` | 64×64 | Цель заблокирована (замок). |
|
|
||||||
| `Button-Up.png` | 128×32 | Кнопка «Обновить», нормальное. |
|
|
||||||
| `Button-Down.png` | 128×32 | Кнопка нажата. |
|
|
||||||
| `Button-Highlight.png` | 128×32 | Hover. |
|
|
||||||
|
|
||||||
## Куда положить в проекте
|
|
||||||
|
|
||||||
```
|
|
||||||
MoonWellClient/EncounterJournal/Textures/PlayerGuide/
|
|
||||||
├── StageBadge-Active.blp
|
|
||||||
├── StageBadge-Done.blp
|
|
||||||
├── ...
|
|
||||||
└── Button-Highlight.blp
|
|
||||||
```
|
|
||||||
|
|
||||||
И в XML использовать пути:
|
|
||||||
```
|
|
||||||
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active"
|
|
||||||
```
|
|
||||||
(WoW сам подставит `.blp`.)
|
|
||||||
|
|
||||||
## Конвертация PNG → BLP
|
|
||||||
|
|
||||||
Любой из:
|
|
||||||
- **BLPNG Converter** (Windows, GUI) — самый простой, drag-and-drop;
|
|
||||||
- **BLPLab** — с предпросмотром;
|
|
||||||
- **CLI**: `blpconverter input.png output.blp` (есть пайплайны на GitHub).
|
|
||||||
|
|
||||||
Параметры:
|
|
||||||
- **Compression:** `DXT5` для всего с альфой (всё, кроме `Card-Background` —
|
|
||||||
для него `DXT1` или `Uncompressed`, если хочется лучше);
|
|
||||||
- **Mipmaps:** включить (`Generate Mipmaps`), кроме `ProgressBar-Fill/BG`
|
|
||||||
(тонкие полоски ломаются на мипмапах — там лучше off);
|
|
||||||
- **Sampling:** `Bicubic` или `Lanczos`.
|
|
||||||
|
|
||||||
## Замена биом-обложек
|
|
||||||
|
|
||||||
Сейчас биом-обложки — стилизованные плашки с аркой и светящимся центром.
|
|
||||||
Их можно полностью заменить вашими собственными — главное, оставить размер
|
|
||||||
**256×128** и имена файлов. Если поменяете список биомов, поправьте таблицу
|
|
||||||
`BIOME_COLOR` в `PlayerGuide.lua`.
|
|
||||||
|
|
||||||
## Замена иконок типов целей
|
|
||||||
|
|
||||||
Если у вас есть атлас иконок WoW (например, из `Interface/Icons/`) — карта
|
|
||||||
`TYPE_ICON` в `PlayerGuide.lua` уже использует встроенные текстуры
|
|
||||||
клиента. Текстуры `TypeIcon-*` нужны, только если хочется единого
|
|
||||||
визуального словаря (рамка + глиф).
|
|
||||||
|
|
||||||
## Применение в XML (примеры)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Бейдж в цепочке этапов: меняем встроенный UI-Quickslot2 на наш -->
|
|
||||||
<Texture parentKey="BadgeBG"
|
|
||||||
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active">
|
|
||||||
<Size><AbsDimension x="44" y="44"/></Size>
|
|
||||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
|
||||||
</Texture>
|
|
||||||
|
|
||||||
<!-- Прогресс-бар: BarTexture указывает на Fill, BG ставится в Layer BACKGROUND -->
|
|
||||||
<StatusBar parentKey="ProgressBar">
|
|
||||||
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture setAllPoints="true"
|
|
||||||
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</StatusBar>
|
|
||||||
|
|
||||||
<!-- Карточка цели: bgFile = Card-Background, edgeFile = Card-Border -->
|
|
||||||
<Backdrop bgFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Background"
|
|
||||||
edgeFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Border"
|
|
||||||
tile="true">
|
|
||||||
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
|
|
||||||
<TileSize><AbsValue val="64"/></TileSize>
|
|
||||||
<EdgeSize><AbsValue val="16"/></EdgeSize>
|
|
||||||
</Backdrop>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Сноска
|
|
||||||
|
|
||||||
Это плейсхолдеры на уровне «лучше, чем встроенные UI-Quickslot2». Если в
|
|
||||||
проекте есть художник — финальные текстуры стоит делать в этом же
|
|
||||||
визуальном словаре, чтобы Lua/XML менять не пришлось (только перерисовать
|
|
||||||
PNG того же размера и сконвертировать).
|
|
||||||
|
Before Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1,263 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>PlayerGuide — Texture Sheet</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet" />
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
html, body { margin: 0; padding: 0; min-height: 100%; }
|
|
||||||
body {
|
|
||||||
font-family: 'EB Garamond', Georgia, serif;
|
|
||||||
background:
|
|
||||||
radial-gradient(ellipse at 50% 0%, #1c2c48 0%, transparent 60%),
|
|
||||||
radial-gradient(ellipse at 100% 100%, #0d1422 0%, transparent 70%),
|
|
||||||
linear-gradient(180deg, #0a1018 0%, #050810 100%);
|
|
||||||
color: #d8c8a0;
|
|
||||||
padding: 60px 50px 80px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family: 'Cinzel', serif; font-weight: 700;
|
|
||||||
font-size: 36px; margin: 0 0 6px;
|
|
||||||
color: #ffd76a;
|
|
||||||
text-shadow: 0 0 14px rgba(197,151,46,0.5), 0 2px 0 #000;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
.sub {
|
|
||||||
font-style: italic; color: #a89878;
|
|
||||||
margin: 0 0 36px; font-size: 16px;
|
|
||||||
}
|
|
||||||
.group {
|
|
||||||
margin: 0 0 44px;
|
|
||||||
}
|
|
||||||
.group-title {
|
|
||||||
font-family: 'Cinzel', serif; font-weight: 700;
|
|
||||||
font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
|
|
||||||
color: #ffd76a; text-shadow: 0 1px 0 #000;
|
|
||||||
display: flex; align-items: center; gap: 12px;
|
|
||||||
margin: 0 0 18px;
|
|
||||||
}
|
|
||||||
.group-title::before {
|
|
||||||
content: ''; width: 3px; height: 14px;
|
|
||||||
background: #ffd76a; box-shadow: 0 0 6px #ffd76a88;
|
|
||||||
}
|
|
||||||
.group-title::after {
|
|
||||||
content: ''; flex: 1; height: 1px;
|
|
||||||
background: linear-gradient(90deg, rgba(138,106,38,0.5), transparent);
|
|
||||||
}
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
background: linear-gradient(180deg, rgba(28,44,72,0.4), rgba(10,16,24,0.4));
|
|
||||||
border: 1px solid rgba(255,215,106,0.18);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 12px 12px 10px;
|
|
||||||
display: flex; flex-direction: column; align-items: center;
|
|
||||||
}
|
|
||||||
.preview {
|
|
||||||
width: 100%; aspect-ratio: 1 / 1;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
background:
|
|
||||||
linear-gradient(45deg, #0d0905 25%, transparent 25%),
|
|
||||||
linear-gradient(-45deg, #0d0905 25%, transparent 25%),
|
|
||||||
linear-gradient(45deg, transparent 75%, #0d0905 75%),
|
|
||||||
linear-gradient(-45deg, transparent 75%, #0d0905 75%);
|
|
||||||
background-size: 16px 16px;
|
|
||||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
|
|
||||||
background-color: #18120a;
|
|
||||||
border: 1px solid rgba(125,90,42,0.4);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.preview.wide { aspect-ratio: 2 / 1; }
|
|
||||||
.preview.bar { aspect-ratio: 4 / 1; }
|
|
||||||
.preview img {
|
|
||||||
max-width: 100%; max-height: 100%;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
.name {
|
|
||||||
font-family: 'Cinzel', serif; font-weight: 600;
|
|
||||||
font-size: 11px; color: #ffe9a0;
|
|
||||||
text-shadow: 0 1px 0 #000;
|
|
||||||
letter-spacing: 0.04em; text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.size {
|
|
||||||
font-size: 10px; color: #a89878;
|
|
||||||
font-style: italic; margin-top: 3px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
/* Showcase row */
|
|
||||||
.showcase {
|
|
||||||
margin: 24px 0 40px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
.showcase-card {
|
|
||||||
background: linear-gradient(180deg, rgba(60,40,18,0.35), rgba(25,15,8,0.55));
|
|
||||||
border: 1px solid #5a4220;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255,200,100,0.18), 0 4px 12px rgba(0,0,0,0.5);
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 18px;
|
|
||||||
display: flex; gap: 14px; align-items: stretch;
|
|
||||||
}
|
|
||||||
.showcase-card h3 {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
font-family: 'Cinzel', serif; font-weight: 700;
|
|
||||||
font-size: 14px; color: #ffe9a0; text-shadow: 0 1px 0 #000;
|
|
||||||
}
|
|
||||||
.showcase-card p {
|
|
||||||
margin: 0; font-style: italic; font-size: 12px; color: #a89878;
|
|
||||||
}
|
|
||||||
.progress-demo {
|
|
||||||
margin-top: 10px;
|
|
||||||
height: 14px; width: 100%; position: relative;
|
|
||||||
background: url('Card-Background.png');
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>PlayerGuide — лист текстур</h1>
|
|
||||||
<p class="sub">33 PNG-файла, готовы к конвертации в BLP. Шахматный фон под превью = прозрачность.</p>
|
|
||||||
|
|
||||||
<!-- Demo: показываем как они стыкуются вместе -->
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Демонстрация (как это смотрится в карточке)</div>
|
|
||||||
<div class="showcase">
|
|
||||||
<div class="showcase-card">
|
|
||||||
<img src="TypeIcon-Dungeon.png" width="44" height="44" alt="" />
|
|
||||||
<div style="flex:1;">
|
|
||||||
<h3>Пройти Кузню Крови</h3>
|
|
||||||
<p>Цитадель Адского Пламени • рек. уровень 61—64</p>
|
|
||||||
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
|
|
||||||
<div style="flex:1; height:8px; background: url('ProgressBar-BG.png'); position:relative;">
|
|
||||||
<div style="width:35%; height:100%; background: url('ProgressBar-Fill.png');"></div>
|
|
||||||
</div>
|
|
||||||
<span style="font-family:Cinzel,serif; font-size:11px; color:#caa760;">0 / 1</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="showcase-card" style="background: url('Card-Background.png'); border-color: #5a4220;">
|
|
||||||
<img src="StageBadge-Active.png" width="48" height="48" alt="" />
|
|
||||||
<div style="flex:1;">
|
|
||||||
<h3 style="color:#ffd76a;">Подготовка к Нордсколу</h3>
|
|
||||||
<p>Этап V из VII · 62% завершено</p>
|
|
||||||
<div style="margin-top:10px; height:14px; position:relative; background: url('ProgressBar-BG.png');">
|
|
||||||
<div style="width:62%; height:100%; background: url('ProgressBar-Fill.png');"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Группы текстур -->
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Бейджи этапов</div>
|
|
||||||
<div class="grid" id="stage"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Прогресс-бар</div>
|
|
||||||
<div class="grid" id="progress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Карточка</div>
|
|
||||||
<div class="grid" id="card"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Декор</div>
|
|
||||||
<div class="grid" id="decor"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Обложки рекомендаций (биомы)</div>
|
|
||||||
<div class="grid" id="biome"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Иконки типов целей</div>
|
|
||||||
<div class="grid" id="types"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Иконки статусов</div>
|
|
||||||
<div class="grid" id="status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="group">
|
|
||||||
<div class="group-title">Кнопки</div>
|
|
||||||
<div class="grid" id="buttons"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const groups = {
|
|
||||||
stage: [
|
|
||||||
['StageBadge-Active', '64×64'],
|
|
||||||
['StageBadge-Done', '64×64'],
|
|
||||||
['StageBadge-Locked', '64×64'],
|
|
||||||
],
|
|
||||||
progress: [
|
|
||||||
['ProgressBar-Fill', '128×16', 'bar'],
|
|
||||||
['ProgressBar-BG', '128×16', 'bar'],
|
|
||||||
],
|
|
||||||
card: [
|
|
||||||
['Card-Background', '256×256'],
|
|
||||||
['Card-Border', '256×32', 'bar'],
|
|
||||||
],
|
|
||||||
decor: [
|
|
||||||
['Section-Divider', '256×16', 'bar'],
|
|
||||||
['Corner-Ornament', '64×64'],
|
|
||||||
['Top-Wave-Glow', '512×64', 'bar'],
|
|
||||||
],
|
|
||||||
biome: [
|
|
||||||
['Biome-Fire', '256×128', 'wide'],
|
|
||||||
['Biome-Arcane', '256×128', 'wide'],
|
|
||||||
['Biome-Storm', '256×128', 'wide'],
|
|
||||||
['Biome-Shadow', '256×128', 'wide'],
|
|
||||||
['Biome-Nature', '256×128', 'wide'],
|
|
||||||
['Biome-Holy', '256×128', 'wide'],
|
|
||||||
],
|
|
||||||
types: [
|
|
||||||
['TypeIcon-Level', '64×64'],
|
|
||||||
['TypeIcon-Quest', '64×64'],
|
|
||||||
['TypeIcon-QuestChain', '64×64'],
|
|
||||||
['TypeIcon-Dungeon', '64×64'],
|
|
||||||
['TypeIcon-Raid', '64×64'],
|
|
||||||
['TypeIcon-Boss', '64×64'],
|
|
||||||
['TypeIcon-Achievement', '64×64'],
|
|
||||||
['TypeIcon-Reputation', '64×64'],
|
|
||||||
['TypeIcon-Profession', '64×64'],
|
|
||||||
['TypeIcon-Item', '64×64'],
|
|
||||||
['TypeIcon-Currency', '64×64'],
|
|
||||||
['TypeIcon-Custom', '64×64'],
|
|
||||||
],
|
|
||||||
status: [
|
|
||||||
['TypeIcon-Done', '64×64'],
|
|
||||||
['TypeIcon-Locked', '64×64'],
|
|
||||||
],
|
|
||||||
buttons: [
|
|
||||||
['Button-Up', '128×32', 'bar'],
|
|
||||||
['Button-Down', '128×32', 'bar'],
|
|
||||||
['Button-Highlight', '128×32', 'bar'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [groupId, items] of Object.entries(groups)) {
|
|
||||||
const el = document.getElementById(groupId);
|
|
||||||
for (const [name, size, shape] of items) {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'item';
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="preview ${shape || ''}">
|
|
||||||
<img src="${name}.png" alt="${name}" />
|
|
||||||
</div>
|
|
||||||
<div class="name">${name}</div>
|
|
||||||
<div class="size">${size}</div>
|
|
||||||
`;
|
|
||||||
el.appendChild(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ClientPath = '',
|
||||||
|
[string]$PackagePath = '',
|
||||||
|
[ValidateSet('Debug', 'Release')]
|
||||||
|
[string]$Configuration = 'Release',
|
||||||
|
[switch]$SkipDataBuild,
|
||||||
|
[switch]$SkipManifest,
|
||||||
|
[switch]$NativeRenderer,
|
||||||
|
[switch]$StopClient,
|
||||||
|
[switch]$Launch
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repoRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
function Import-ProjectEnvironment {
|
||||||
|
$envFile = Join-Path $repoRoot '.env'
|
||||||
|
if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) { return }
|
||||||
|
|
||||||
|
foreach ($line in Get-Content -LiteralPath $envFile -Encoding UTF8) {
|
||||||
|
if ($line -notmatch '^\s*([^#][^=]+)=(.*)$') { continue }
|
||||||
|
$name = $matches[1].Trim()
|
||||||
|
$value = $matches[2].Trim().Trim('"').Trim("'")
|
||||||
|
if ($name -and -not (Test-Path "Env:$name")) {
|
||||||
|
Set-Item -Path "Env:$name" -Value $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-ProjectEnvironment
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||||
|
$ClientPath = if ($env:WOW_HOME) {
|
||||||
|
$env:WOW_HOME
|
||||||
|
} else {
|
||||||
|
'C:\Program Files (x86)\World of Warcraft'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||||
|
$PackagePath = Join-Path $repoRoot 'dist'
|
||||||
|
}
|
||||||
|
|
||||||
|
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
|
||||||
|
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||||
|
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||||
|
throw "Client directory was not found: $ClientPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
|
||||||
|
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
})
|
||||||
|
if ($runningClients.Count) {
|
||||||
|
if (-not $StopClient) {
|
||||||
|
throw 'Wow.exe is running. Close the client or repeat deployment with -StopClient.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Stopping the running WoW client...'
|
||||||
|
$runningClients | Stop-Process -Force
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
|
||||||
|
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
} | Stop-Process -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$env:WOW_HOME = $ClientPath
|
||||||
|
Write-Host "MoonWell client: $ClientPath"
|
||||||
|
Write-Host "Package staging: $PackagePath"
|
||||||
|
|
||||||
|
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
|
||||||
|
& git -C $repoRoot submodule update --init --recursive
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||||
|
|
||||||
|
if (-not $SkipDataBuild) {
|
||||||
|
Write-Host '[2/5] Building MPQ patches...'
|
||||||
|
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||||
|
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||||
|
if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' }
|
||||||
|
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
& $tool (Join-Path $repoRoot 'src') $PackagePath
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'MPQ patch build failed.' }
|
||||||
|
} else {
|
||||||
|
Write-Host '[2/5] MPQ build skipped.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '[3/5] Building and installing WarcraftXL...'
|
||||||
|
$buildArguments = @{
|
||||||
|
Configuration = $Configuration
|
||||||
|
ClientPath = $ClientPath
|
||||||
|
PackagePath = $PackagePath
|
||||||
|
Deploy = $true
|
||||||
|
}
|
||||||
|
if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
|
||||||
|
& (Join-Path $repoRoot 'build-warcraftxl.ps1') @buildArguments
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
|
||||||
|
|
||||||
|
Write-Host '[4/5] Synchronizing package files...'
|
||||||
|
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
|
||||||
|
$robocopyExitCode = $LASTEXITCODE
|
||||||
|
if ($robocopyExitCode -ge 8) {
|
||||||
|
throw "Package synchronization failed with robocopy exit code $robocopyExitCode."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipManifest) {
|
||||||
|
Write-Host '[5/5] Building launcher manifest...'
|
||||||
|
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||||
|
if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' }
|
||||||
|
& $python.Source (Join-Path $repoRoot 'build_manifest.py') `
|
||||||
|
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' }
|
||||||
|
} else {
|
||||||
|
Write-Host '[5/5] Manifest generation skipped.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
|
||||||
|
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Deployment complete.'
|
||||||
|
Write-Host " Renderer: $renderer"
|
||||||
|
Write-Host " Wow.exe SHA-256: $wowHash"
|
||||||
|
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
|
||||||
|
|
||||||
|
if ($Launch) {
|
||||||
|
Write-Host 'Launching Wow.exe...'
|
||||||
|
Start-Process -FilePath (Join-Path $ClientPath 'Wow.exe') -WorkingDirectory $ClientPath
|
||||||
|
}
|
||||||
|
|
||||||
|
# robocopy uses successful non-zero exit codes; do not leak one to callers.
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
# Player Guide Requirements
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Create a `Путеводитель` tab inside the existing Encounter Journal UI.
|
|
||||||
|
|
||||||
The guide must show personal character progress calculated by `mod-individual-progress` and recommend the next relevant content: quests, dungeons, raids, bosses, requirements, activities, and rewards.
|
|
||||||
|
|
||||||
This feature is not a third Encounter Journal instance type. It is a separate panel inside the same interface.
|
|
||||||
|
|
||||||
## High-Level Architecture
|
|
||||||
|
|
||||||
```text
|
|
||||||
AzerothCore / mod-individual-progress
|
|
||||||
-> calculates player progress
|
|
||||||
-> builds a guide payload
|
|
||||||
|
|
||||||
AIO / MoonWell bridge
|
|
||||||
-> sends guide data to the client
|
|
||||||
|
|
||||||
MoonWellClient / EncounterJournal
|
|
||||||
-> receives the payload
|
|
||||||
-> stores it in C_PlayerGuide
|
|
||||||
-> renders the Player Guide tab
|
|
||||||
```
|
|
||||||
|
|
||||||
## Client Components
|
|
||||||
|
|
||||||
Add separate client-side files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
EncounterJournal/Utils/C_PlayerGuide.lua
|
|
||||||
EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua
|
|
||||||
EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
```text
|
|
||||||
C_PlayerGuide.lua
|
|
||||||
- stores current guide data
|
|
||||||
- exposes a small UI-facing API
|
|
||||||
- receives server payloads
|
|
||||||
- requests refreshes from the server
|
|
||||||
|
|
||||||
PlayerGuide.lua
|
|
||||||
- renders guide data
|
|
||||||
- refreshes objective rows
|
|
||||||
- handles clicks on objectives/recommendations
|
|
||||||
|
|
||||||
PlayerGuide.xml
|
|
||||||
- defines the guide panel
|
|
||||||
- defines static frames, headers, scroll area, row templates
|
|
||||||
```
|
|
||||||
|
|
||||||
Register these files in `MoonWellClient.toc` before `Custom_EncounterJournal.lua`.
|
|
||||||
|
|
||||||
## Encounter Journal Tab
|
|
||||||
|
|
||||||
Add a new top-level tab near `Подземелья` and `Рейды`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Путеводитель | Подземелья | Рейды | Комплекты
|
|
||||||
```
|
|
||||||
|
|
||||||
The tab must live in:
|
|
||||||
|
|
||||||
```text
|
|
||||||
EncounterJournal/Custom_EncounterJournal/Custom_EncounterJournal.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected XML shape:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Button name="$parentGuideTab"
|
|
||||||
inherits="EncounterTierTabTemplate"
|
|
||||||
text="MW_PLAYER_GUIDE"
|
|
||||||
parentKey="guideTab"
|
|
||||||
id="5">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentSuggestTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="EJ_ContentTab_OnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
```
|
|
||||||
|
|
||||||
If the guide tab is inserted before existing tabs, anchors for `DungeonTab`, `RaidTab`, and `LootJournalTab` must be adjusted.
|
|
||||||
|
|
||||||
Add a localized string:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
MW_PLAYER_GUIDE = "Путеводитель"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tab Selection Behavior
|
|
||||||
|
|
||||||
When the guide tab is selected:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- hide instance cards
|
|
||||||
- hide Encounter Journal detail panel
|
|
||||||
- hide suggested content panel
|
|
||||||
- hide loot journal panel
|
|
||||||
- hide or disable the expansion/tier dropdown unless explicitly needed
|
|
||||||
- show PlayerGuideFrame
|
|
||||||
```
|
|
||||||
|
|
||||||
When any other top-level tab is selected:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- hide PlayerGuideFrame
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not route this tab through:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
EJ_GetInstanceByIndex(index, isRaid)
|
|
||||||
```
|
|
||||||
|
|
||||||
That API is only for dungeon/raid instance lists.
|
|
||||||
|
|
||||||
## Client API
|
|
||||||
|
|
||||||
Create:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
C_PlayerGuide = C_PlayerGuide or {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Required methods:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
C_PlayerGuide.SetData(data)
|
|
||||||
C_PlayerGuide.GetStageInfo()
|
|
||||||
C_PlayerGuide.GetNumObjectives()
|
|
||||||
C_PlayerGuide.GetObjectiveInfo(index)
|
|
||||||
C_PlayerGuide.GetNumRecommendations()
|
|
||||||
C_PlayerGuide.GetRecommendationInfo(index)
|
|
||||||
C_PlayerGuide.GetNumRewards()
|
|
||||||
C_PlayerGuide.GetRewardInfo(index)
|
|
||||||
C_PlayerGuide.RequestRefresh()
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimum behavior:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SetData(data)
|
|
||||||
- validate payload is a table
|
|
||||||
- replace current data
|
|
||||||
- fire/trigger a UI refresh if the guide panel is visible
|
|
||||||
|
|
||||||
GetStageInfo()
|
|
||||||
- returns stageID, stageName, stageDescription, progress
|
|
||||||
|
|
||||||
GetObjectiveInfo(index)
|
|
||||||
- returns a normalized objective record or unpacked objective fields
|
|
||||||
|
|
||||||
RequestRefresh()
|
|
||||||
- asks the server to resend the current payload
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Payload
|
|
||||||
|
|
||||||
The server should send one normalized payload per character.
|
|
||||||
|
|
||||||
Minimum shape:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
{
|
|
||||||
version = 1,
|
|
||||||
characterGuid = 123,
|
|
||||||
|
|
||||||
stageID = 10,
|
|
||||||
stageName = "Подготовка к Нордсколу",
|
|
||||||
stageDescription = "Завершите ключевые шаги перед переходом дальше.",
|
|
||||||
progress = 40,
|
|
||||||
|
|
||||||
objectives = {
|
|
||||||
{
|
|
||||||
id = 1,
|
|
||||||
type = "level",
|
|
||||||
title = "Достигнуть 68 уровня",
|
|
||||||
description = "",
|
|
||||||
completed = true,
|
|
||||||
progress = 68,
|
|
||||||
required = 68,
|
|
||||||
targetID = 68,
|
|
||||||
order = 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id = 2,
|
|
||||||
type = "quest",
|
|
||||||
title = "Завершить цепочку Темного портала",
|
|
||||||
description = "",
|
|
||||||
completed = false,
|
|
||||||
progress = 3,
|
|
||||||
required = 8,
|
|
||||||
targetID = 10119,
|
|
||||||
order = 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id = 3,
|
|
||||||
type = "dungeon",
|
|
||||||
title = "Пройти Кузню Крови",
|
|
||||||
description = "",
|
|
||||||
completed = false,
|
|
||||||
progress = 0,
|
|
||||||
required = 1,
|
|
||||||
targetID = 256,
|
|
||||||
instanceID = 256,
|
|
||||||
order = 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
recommendations = {
|
|
||||||
{
|
|
||||||
id = 1,
|
|
||||||
type = "dungeon",
|
|
||||||
title = "Кузня Крови",
|
|
||||||
description = "Подходит для текущего этапа.",
|
|
||||||
instanceID = 256,
|
|
||||||
priority = 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
rewards = {
|
|
||||||
{
|
|
||||||
type = "item",
|
|
||||||
itemID = 12345,
|
|
||||||
count = 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Objective Types
|
|
||||||
|
|
||||||
Support at least:
|
|
||||||
|
|
||||||
```text
|
|
||||||
level
|
|
||||||
quest
|
|
||||||
quest_chain
|
|
||||||
dungeon
|
|
||||||
raid
|
|
||||||
boss
|
|
||||||
achievement
|
|
||||||
reputation
|
|
||||||
profession
|
|
||||||
item
|
|
||||||
currency
|
|
||||||
custom
|
|
||||||
```
|
|
||||||
|
|
||||||
Every objective should use the common fields:
|
|
||||||
|
|
||||||
```text
|
|
||||||
id
|
|
||||||
type
|
|
||||||
title
|
|
||||||
description
|
|
||||||
completed
|
|
||||||
progress
|
|
||||||
required
|
|
||||||
targetID
|
|
||||||
order
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional type-specific fields:
|
|
||||||
|
|
||||||
```text
|
|
||||||
questID
|
|
||||||
questChainID
|
|
||||||
instanceID
|
|
||||||
encounterID
|
|
||||||
achievementID
|
|
||||||
factionID
|
|
||||||
professionID
|
|
||||||
itemID
|
|
||||||
currencyID
|
|
||||||
```
|
|
||||||
|
|
||||||
## Objective Click Behavior
|
|
||||||
|
|
||||||
Recommended behavior:
|
|
||||||
|
|
||||||
```text
|
|
||||||
quest
|
|
||||||
- show quest link or quest hint if available
|
|
||||||
|
|
||||||
dungeon
|
|
||||||
- open Encounter Journal on instanceID
|
|
||||||
|
|
||||||
raid
|
|
||||||
- open Encounter Journal on instanceID
|
|
||||||
|
|
||||||
boss
|
|
||||||
- open Encounter Journal on encounterID
|
|
||||||
|
|
||||||
item
|
|
||||||
- show item tooltip
|
|
||||||
|
|
||||||
achievement
|
|
||||||
- show achievement UI/link
|
|
||||||
|
|
||||||
custom
|
|
||||||
- show text-only detail or request a server action
|
|
||||||
```
|
|
||||||
|
|
||||||
If a target cannot be opened, the UI should fail quietly and keep the guide visible.
|
|
||||||
|
|
||||||
## AIO Contract
|
|
||||||
|
|
||||||
Use a separate handler namespace:
|
|
||||||
|
|
||||||
```text
|
|
||||||
MoonWellPlayerGuide
|
|
||||||
```
|
|
||||||
|
|
||||||
Server to client:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
SetData(payload)
|
|
||||||
UpdateObjective(objective)
|
|
||||||
SetStage(stagePayload)
|
|
||||||
Notify(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
Client to server:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
RequestRefresh()
|
|
||||||
RequestTrackObjective(objectiveID)
|
|
||||||
RequestOpenTarget(objectiveID)
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimum MVP only needs:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
SetData(payload)
|
|
||||||
RequestRefresh()
|
|
||||||
```
|
|
||||||
|
|
||||||
## AzerothCore / mod-individual-progress Requirements
|
|
||||||
|
|
||||||
`mod-individual-progress` must:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- determine the player's current progression stage
|
|
||||||
- calculate objective completion state
|
|
||||||
- calculate objective progress values
|
|
||||||
- build the guide payload
|
|
||||||
- send the payload on login
|
|
||||||
- send updates when progress changes
|
|
||||||
- respond to explicit client refresh requests
|
|
||||||
```
|
|
||||||
|
|
||||||
Recommended update events:
|
|
||||||
|
|
||||||
```text
|
|
||||||
login
|
|
||||||
level up
|
|
||||||
quest accepted
|
|
||||||
quest completed
|
|
||||||
quest rewarded
|
|
||||||
achievement earned
|
|
||||||
boss kill
|
|
||||||
dungeon completed
|
|
||||||
reputation changed
|
|
||||||
profession changed
|
|
||||||
item acquired, if item objectives are used
|
|
||||||
```
|
|
||||||
|
|
||||||
## UI Requirements
|
|
||||||
|
|
||||||
MVP UI:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- guide tab
|
|
||||||
- stage title
|
|
||||||
- stage description
|
|
||||||
- total progress percentage
|
|
||||||
- objective list
|
|
||||||
- completed/incomplete visual state
|
|
||||||
- recommendation list
|
|
||||||
- manual refresh button
|
|
||||||
```
|
|
||||||
|
|
||||||
Later UI:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- categories
|
|
||||||
- filters: all / active / completed
|
|
||||||
- tracked objective pinning
|
|
||||||
- reward preview
|
|
||||||
- requirement tooltips
|
|
||||||
- direct "Open in Journal" buttons
|
|
||||||
- server notifications
|
|
||||||
```
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
Do not:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- store guide progress in JOURNALINSTANCE
|
|
||||||
- make the guide a third dungeon/raid instance category
|
|
||||||
- overload EJ_GetInstanceByIndex(index, isRaid)
|
|
||||||
- depend on Sirus-only globals or realm constants
|
|
||||||
- block the old dungeon/raid Encounter Journal if guide data is missing
|
|
||||||
```
|
|
||||||
|
|
||||||
## MVP Implementation Order
|
|
||||||
|
|
||||||
1. Add the `Путеводитель` tab.
|
|
||||||
2. Add an empty `PlayerGuideFrame`.
|
|
||||||
3. Add `C_PlayerGuide.lua` with static test data.
|
|
||||||
4. Render the stage title and three test objectives.
|
|
||||||
5. Wire tab switching and hide/show behavior.
|
|
||||||
6. Add AIO `MoonWellPlayerGuide.SetData`.
|
|
||||||
7. Send test payload from the server.
|
|
||||||
8. Replace static data with real `mod-individual-progress` data.
|
|
||||||
9. Add click behavior for dungeon/raid/boss objectives.
|
|
||||||
10. Add polish: progress bar, icons, rewards, filters.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Client should tolerate missing or malformed fields:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- missing payload -> show empty state
|
|
||||||
- missing objectives -> show "no active objectives"
|
|
||||||
- missing recommendations -> hide recommendations block
|
|
||||||
- unknown objective type -> render as text-only custom objective
|
|
||||||
- invalid instanceID/encounterID -> do not throw Lua errors
|
|
||||||
```
|
|
||||||
|
|
||||||
Server should validate:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- objective ids are unique within the payload
|
|
||||||
- order is stable
|
|
||||||
- type is known or custom
|
|
||||||
- progress <= required when required is present
|
|
||||||
- dungeon/raid/boss targets exist in Encounter Journal data when used
|
|
||||||
```
|
|
||||||
@@ -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,406 @@
|
|||||||
|
// 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
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"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
|
# Stop on errors
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
@@ -36,6 +42,7 @@ $SRC_DIR = Join-Path $ROOT "src"
|
|||||||
$DIST_DIR = Join-Path $ROOT "dist"
|
$DIST_DIR = Join-Path $ROOT "dist"
|
||||||
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
||||||
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
||||||
|
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
|
||||||
|
|
||||||
# --- Ensure tool exists
|
# --- Ensure tool exists
|
||||||
if (!(Test-Path $TOOL)) {
|
if (!(Test-Path $TOOL)) {
|
||||||
@@ -59,6 +66,16 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
|
||||||
|
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
|
||||||
|
Write-Host "Building and packaging WarcraftXL..."
|
||||||
|
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
|
||||||
|
-PackagePath $DIST_DIR -Deploy
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "WarcraftXL build/deploy failed!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# --- Sync dist/ -> WOW_HOME
|
# --- Sync dist/ -> WOW_HOME
|
||||||
Write-Host "Syncing dist/ -> WOW_HOME..."
|
Write-Host "Syncing dist/ -> WOW_HOME..."
|
||||||
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
||||||
@@ -70,6 +87,21 @@ if ($LASTEXITCODE -ge 8) {
|
|||||||
|
|
||||||
Write-Host "MPQ archives deployed to $WOW_HOME"
|
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
|
# --- Run WoW reload script
|
||||||
Write-Host "Launching WoW..."
|
Write-Host "Launching WoW..."
|
||||||
cmd /c $RELOAD_SCRIPT
|
cmd /c $RELOAD_SCRIPT
|
||||||
|
|||||||
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
|
|||||||
end
|
end
|
||||||
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 function CharacterCreate_GetEnteredName()
|
||||||
local text = "";
|
local text = "";
|
||||||
|
|
||||||
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
|||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||||
|
|
||||||
if showGameModes then
|
if showGameModes then
|
||||||
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
|
|||||||
CharCreatePersonalizeButton:Show();
|
CharCreatePersonalizeButton:Show();
|
||||||
CharCreateOkayButton:Hide();
|
CharCreateOkayButton:Hide();
|
||||||
CharacterCreate_UpdateGameModeVisibility();
|
CharacterCreate_UpdateGameModeVisibility();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||||
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
|
|||||||
CharacterChangeFixup();
|
CharacterChangeFixup();
|
||||||
CharacterCreate_CreateGenderButtonTextures();
|
CharacterCreate_CreateGenderButtonTextures();
|
||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
hideScheduled = true
|
hideScheduled = true
|
||||||
HideNameEditFrame:Show()
|
HideNameEditFrame:Show()
|
||||||
@@ -1013,7 +1054,7 @@ end
|
|||||||
function CharacterCreate_OnHide()
|
function CharacterCreate_OnHide()
|
||||||
PAID_SERVICE_CHARACTER_ID = nil;
|
PAID_SERVICE_CHARACTER_ID = nil;
|
||||||
PAID_SERVICE_TYPE = nil;
|
PAID_SERVICE_TYPE = nil;
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
for button, tooltip in pairs(raceTooltips) do
|
for button, tooltip in pairs(raceTooltips) do
|
||||||
if tooltip then tooltip:Hide() end
|
if tooltip then tooltip:Hide() end
|
||||||
@@ -1518,6 +1559,7 @@ end
|
|||||||
|
|
||||||
function CharacterCreate_UpdateModel(self)
|
function CharacterCreate_UpdateModel(self)
|
||||||
UpdateCustomizationScene();
|
UpdateCustomizationScene();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
self:AdvanceTime();
|
self:AdvanceTime();
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
|
|||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
end
|
end
|
||||||
|
|
||||||
function CharacterCreate_ResetState()
|
function CharacterCreate_ResetState(immediate)
|
||||||
CharacterCreate.personalizationMode = false;
|
CharacterCreate.personalizationMode = false;
|
||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
|
CharacterCreate_UpdateCamera(immediate);
|
||||||
CharacterCreateRaceButtonsContainer:Show();
|
CharacterCreateRaceButtonsContainer:Show();
|
||||||
CharacterCreateClassButtonsContainer:Show();
|
CharacterCreateClassButtonsContainer:Show();
|
||||||
CharacterCreateGenderButtonsContainer:Show();
|
CharacterCreateGenderButtonsContainer:Show();
|
||||||
|
|||||||
@@ -49,10 +49,19 @@ local rolesByFlag = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local EJ_Tabs = {};
|
local EJ_Tabs = {};
|
||||||
|
local EJ_BROKEN_BAKED_MODEL = {};
|
||||||
|
local EJ_MODEL_CAMERA = {
|
||||||
|
-- Avatar of Hakkar is unusually small and anchored too low.
|
||||||
|
[8053] = { scale = 2.0, x = 0, y = 0.16, z = 0 },
|
||||||
|
-- Gahz'rilla's native UI camera is an extreme facial close-up.
|
||||||
|
[7271] = { scale = 0.42, x = 0, y = 0.04, z = 0 },
|
||||||
|
};
|
||||||
|
local EJ_MODEL_USER_SCALE = {};
|
||||||
|
|
||||||
EJ_Tabs[1] = { frame = "overviewScroll", button = "overviewTab" };
|
EJ_Tabs[1] = { frame = "overviewScroll", button = "overviewTab" };
|
||||||
EJ_Tabs[2] = { frame = "lootScroll", button = "lootTab" };
|
EJ_Tabs[2] = { frame = "lootScroll", button = "lootTab" };
|
||||||
EJ_Tabs[3] = { frame = "detailsScroll", button = "bossTab" };
|
EJ_Tabs[3] = { frame = "detailsScroll", button = "bossTab" };
|
||||||
|
EJ_Tabs[4] = { frame = "model", button = "modelTab" };
|
||||||
|
|
||||||
|
|
||||||
local EJ_section_openTable = {};
|
local EJ_section_openTable = {};
|
||||||
@@ -177,12 +186,75 @@ function EncounterJournal_InitTab(self)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function EncounterJournal_ScrollFrame_OnMouseWheel(scrollFrame, delta, isHybrid)
|
||||||
|
if not scrollFrame then
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
if isHybrid and HybridScrollFrame_OnMouseWheel then
|
||||||
|
HybridScrollFrame_OnMouseWheel(scrollFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
if ScrollFrameTemplate_OnMouseWheel then
|
||||||
|
ScrollFrameTemplate_OnMouseWheel(scrollFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local scrollBar = scrollFrame.ScrollBar or scrollFrame.scrollBar or _G[scrollFrame:GetName() .. "ScrollBar"];
|
||||||
|
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)));
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_OnMouseWheel(self, delta)
|
||||||
|
if PlayerGuideFrame and PlayerGuideFrame:IsVisible() and PlayerGuideFrame.BodyScroll and PlayerGuideFrame.BodyScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(PlayerGuideFrame.BodyScroll, delta);
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.searchResults and self.searchResults:IsVisible() and self.searchResults.scrollFrame and self.searchResults.scrollFrame:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(self.searchResults.scrollFrame, delta, true);
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.suggestFrame and self.suggestFrame:IsVisible() then
|
||||||
|
EJSuggestFrame_OnMouseWheel(self.suggestFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local instanceSelect = self.instanceSelect;
|
||||||
|
if instanceSelect and instanceSelect:IsVisible() and instanceSelect.scroll and instanceSelect.scroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(instanceSelect.scroll, delta);
|
||||||
|
end
|
||||||
|
|
||||||
|
local info = self.encounter and self.encounter.info;
|
||||||
|
if info and info:IsVisible() then
|
||||||
|
if info.lootScroll and info.lootScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.lootScroll, delta, true);
|
||||||
|
elseif info.detailsScroll and info.detailsScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.detailsScroll, delta);
|
||||||
|
elseif info.overviewScroll and info.overviewScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.overviewScroll, delta);
|
||||||
|
elseif info.bossesScroll and info.bossesScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.bossesScroll, delta);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_OnLoad(self)
|
function EncounterJournal_OnLoad(self)
|
||||||
EncounterJournalTitleText:SetText(ADVENTURE_JOURNAL);
|
EncounterJournalTitleText:SetText(ADVENTURE_JOURNAL);
|
||||||
SetPortraitToTexture(EncounterJournalPortrait, "Interface\\EncounterJournal\\UI-EJ-PortraitIcon");
|
SetPortraitToTexture(EncounterJournalPortrait, "Interface\\EncounterJournal\\UI-EJ-PortraitIcon");
|
||||||
self:RegisterCustomEvent("EJ_LOOT_DATA_RECIEVED");
|
self:RegisterCustomEvent("EJ_LOOT_DATA_RECIEVED");
|
||||||
self:RegisterCustomEvent("EJ_DIFFICULTY_UPDATE");
|
self:RegisterCustomEvent("EJ_DIFFICULTY_UPDATE");
|
||||||
self:RegisterCustomEvent("SEARCH_DB_LOADED");
|
self:RegisterCustomEvent("SEARCH_DB_LOADED");
|
||||||
|
self:EnableMouseWheel(true);
|
||||||
|
self:SetScript("OnMouseWheel", EncounterJournal_OnMouseWheel);
|
||||||
|
|
||||||
do
|
do
|
||||||
SetParentFrameLevel(self.inset)
|
SetParentFrameLevel(self.inset)
|
||||||
@@ -1061,6 +1133,7 @@ function EncounterJournal_DisplayEncounter(encounterID, noButton, scrollToEncoun
|
|||||||
|
|
||||||
-- Setup Creatures
|
-- Setup Creatures
|
||||||
local id, name, description, displayInfo, iconImage, creatureID;
|
local id, name, description, displayInfo, iconImage, creatureID;
|
||||||
|
local hasCreatureModel = false;
|
||||||
for i = 1, MAX_CREATURES_PER_ENCOUNTER do
|
for i = 1, MAX_CREATURES_PER_ENCOUNTER do
|
||||||
id, name, description, displayInfo, iconImage, creatureID = EJ_GetCreatureInfo(i);
|
id, name, description, displayInfo, iconImage, creatureID = EJ_GetCreatureInfo(i);
|
||||||
if id then
|
if id then
|
||||||
@@ -1071,12 +1144,17 @@ function EncounterJournal_DisplayEncounter(encounterID, noButton, scrollToEncoun
|
|||||||
button.id = id;
|
button.id = id;
|
||||||
button.description = description;
|
button.description = description;
|
||||||
button.displayInfo = displayInfo;
|
button.displayInfo = displayInfo;
|
||||||
button.creatureID = creatureID or displayInfo;
|
button.iconImage = iconImage;
|
||||||
|
button.creatureID = creatureID;
|
||||||
|
if creatureID and creatureID > 0 then
|
||||||
|
hasCreatureModel = true;
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--enable abilities tab
|
--enable abilities tab
|
||||||
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.bossTab, true);
|
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.bossTab, true);
|
||||||
|
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.modelTab, hasCreatureModel);
|
||||||
|
|
||||||
if (overviewFound) then
|
if (overviewFound) then
|
||||||
EncounterJournal_ToggleHeaders(self.overviewFrame);
|
EncounterJournal_ToggleHeaders(self.overviewFrame);
|
||||||
@@ -1110,12 +1188,30 @@ function EncounterJournal_DisplayCreature(self)
|
|||||||
|
|
||||||
local model = EncounterJournal.encounter.info.model;
|
local model = EncounterJournal.encounter.info.model;
|
||||||
local displayInfo = self.displayInfo;
|
local displayInfo = self.displayInfo;
|
||||||
if model.ResetFull then
|
if model.ClearModel then
|
||||||
model:ResetFull();
|
|
||||||
elseif model.ClearModel then
|
|
||||||
model:ClearModel();
|
model:ClearModel();
|
||||||
end
|
end
|
||||||
EncounterJournal.creatureDisplayID = displayInfo;
|
EncounterJournal.creatureDisplayID = displayInfo;
|
||||||
|
model.displayInfo = displayInfo;
|
||||||
|
local useStaticPortrait = EJ_BROKEN_BAKED_MODEL[displayInfo];
|
||||||
|
if model.fallbackPortrait then
|
||||||
|
if useStaticPortrait then
|
||||||
|
model.fallbackPortrait:SetTexture(self.iconImage or
|
||||||
|
(MoonWellEncounterPortraitByDisplayInfo and MoonWellEncounterPortraitByDisplayInfo[displayInfo]) or
|
||||||
|
QUESTION_MARK_ICON);
|
||||||
|
model.fallbackPortrait:Show();
|
||||||
|
else
|
||||||
|
model.fallbackPortrait:Hide();
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- WarcraftXL accepts displayInfo as an optional second argument and can
|
||||||
|
-- therefore render entries which are not present in the creature cache.
|
||||||
|
-- Stock 3.3.5 safely ignores the extra argument and keeps its old fallback.
|
||||||
|
if not useStaticPortrait and self.creatureID and self.creatureID > 0 and model.SetCreature then
|
||||||
|
model:SetCreature(self.creatureID, displayInfo);
|
||||||
|
EncounterJournal_Model_ApplyCamera(model);
|
||||||
|
end
|
||||||
|
|
||||||
model.imageTitle:SetText(self.name)
|
model.imageTitle:SetText(self.name)
|
||||||
|
|
||||||
@@ -1134,6 +1230,74 @@ function EncounterJournal_ShowCreatures()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnLoad(self)
|
||||||
|
self.rotation = 0;
|
||||||
|
self:SetRotation(self.rotation);
|
||||||
|
self:EnableMouse(true);
|
||||||
|
self:EnableMouseWheel(true);
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_ApplyCamera(self)
|
||||||
|
if not self or not self.displayInfo or EJ_BROKEN_BAKED_MODEL[self.displayInfo] then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local camera = EJ_MODEL_CAMERA[self.displayInfo];
|
||||||
|
local scale = EJ_MODEL_USER_SCALE[self.displayInfo] or (camera and camera.scale) or 1;
|
||||||
|
if self.SetModelScale then
|
||||||
|
self:SetModelScale(scale);
|
||||||
|
end
|
||||||
|
if self.SetPosition then
|
||||||
|
self:SetPosition(camera and camera.x or 0, camera and camera.y or 0, camera and camera.z or 0);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnMouseWheel(self, delta)
|
||||||
|
if not self.displayInfo or EJ_BROKEN_BAKED_MODEL[self.displayInfo] then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local camera = EJ_MODEL_CAMERA[self.displayInfo];
|
||||||
|
local scale = EJ_MODEL_USER_SCALE[self.displayInfo] or (camera and camera.scale) or 1;
|
||||||
|
if delta > 0 then
|
||||||
|
scale = scale * 1.12;
|
||||||
|
else
|
||||||
|
scale = scale / 1.12;
|
||||||
|
end
|
||||||
|
scale = math.max(0.15, math.min(4, scale));
|
||||||
|
EJ_MODEL_USER_SCALE[self.displayInfo] = scale;
|
||||||
|
EncounterJournal_Model_ApplyCamera(self);
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnUpdateModel(self)
|
||||||
|
EncounterJournal_Model_ApplyCamera(self);
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnMouseDown(self, button)
|
||||||
|
if button == "LeftButton" then
|
||||||
|
self.dragCursorX = GetCursorPosition();
|
||||||
|
elseif button == "RightButton" and self.displayInfo then
|
||||||
|
EJ_MODEL_USER_SCALE[self.displayInfo] = nil;
|
||||||
|
EncounterJournal_Model_ApplyCamera(self);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnMouseUp(self)
|
||||||
|
self.dragCursorX = nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_Model_OnUpdate(self)
|
||||||
|
if not self.dragCursorX then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local cursorX = GetCursorPosition();
|
||||||
|
local delta = cursorX - self.dragCursorX;
|
||||||
|
self.dragCursorX = cursorX;
|
||||||
|
self.rotation = (self.rotation or 0) + delta * 0.01;
|
||||||
|
self:SetRotation(self.rotation);
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_HideCreatures(clearDisplayInfo)
|
function EncounterJournal_HideCreatures(clearDisplayInfo)
|
||||||
for index, creatureButton in ipairs(EncounterJournal.encounter.info.creatureButtons) do
|
for index, creatureButton in ipairs(EncounterJournal.encounter.info.creatureButtons) do
|
||||||
creatureButton:Hide();
|
creatureButton:Hide();
|
||||||
@@ -1149,6 +1313,30 @@ local toggleTempList = {};
|
|||||||
local headerCount = 0;
|
local headerCount = 0;
|
||||||
local loopedSections = {};
|
local loopedSections = {};
|
||||||
|
|
||||||
|
local function EncounterJournal_GetHeaderWidth(sourceFrame)
|
||||||
|
local width = sourceFrame and sourceFrame:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return width;
|
||||||
|
end
|
||||||
|
|
||||||
|
local info = EncounterJournal and EncounterJournal.encounter and EncounterJournal.encounter.info;
|
||||||
|
local scrollFrame = info and ((info.detailsScroll and info.detailsScroll:IsShown() and info.detailsScroll) or
|
||||||
|
(info.overviewScroll and info.overviewScroll:IsShown() and info.overviewScroll) or info.detailsScroll or info.overviewScroll);
|
||||||
|
local child = scrollFrame and (scrollFrame.child or scrollFrame.ScrollChild);
|
||||||
|
|
||||||
|
width = child and child:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return width;
|
||||||
|
end
|
||||||
|
|
||||||
|
width = scrollFrame and scrollFrame:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return math.max(1, width - 30);
|
||||||
|
end
|
||||||
|
|
||||||
|
return 320;
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_UpdateButtonState(self)
|
function EncounterJournal_UpdateButtonState(self)
|
||||||
local oldtex = self.textures.expanded;
|
local oldtex = self.textures.expanded;
|
||||||
if self:GetParent().expanded then
|
if self:GetParent().expanded then
|
||||||
@@ -1178,6 +1366,22 @@ function EncounterJournal_UpdateButtonState(self)
|
|||||||
self.tex.down[3]:Hide();
|
self.tex.down[3]:Hide();
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function EncounterJournal_NormalizeHeaderButton(infoHeader, width)
|
||||||
|
if not infoHeader or not infoHeader.button then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
width = width or EncounterJournal_GetHeaderWidth(infoHeader);
|
||||||
|
infoHeader:SetWidth(width);
|
||||||
|
infoHeader.button:SetWidth(width);
|
||||||
|
|
||||||
|
if infoHeader.button:GetButtonState() ~= "NORMAL" then
|
||||||
|
infoHeader.button:SetButtonState("NORMAL");
|
||||||
|
end
|
||||||
|
|
||||||
|
EncounterJournal_UpdateButtonState(infoHeader.button);
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_OnClick(self)
|
function EncounterJournal_OnClick(self)
|
||||||
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
|
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
|
||||||
if self.link then
|
if self.link then
|
||||||
@@ -1471,6 +1675,7 @@ function EncounterJournal_SetUpOverview(self, role, index)
|
|||||||
infoHeader.button.link = link;
|
infoHeader.button.link = link;
|
||||||
infoHeader.sectionID = nextSectionID;
|
infoHeader.sectionID = nextSectionID;
|
||||||
|
|
||||||
|
EncounterJournal_NormalizeHeaderButton(infoHeader, EncounterJournal_GetHeaderWidth(self));
|
||||||
infoHeader.overviewDescription:SetWidth(infoHeader:GetWidth() - 20);
|
infoHeader.overviewDescription:SetWidth(infoHeader:GetWidth() - 20);
|
||||||
EncounterJournal_SetDescriptionWithBullets(infoHeader, description);
|
EncounterJournal_SetDescriptionWithBullets(infoHeader, description);
|
||||||
infoHeader:Show();
|
infoHeader:Show();
|
||||||
@@ -1479,7 +1684,7 @@ end
|
|||||||
function EncounterJournal_ToggleHeaders(self, doNotShift)
|
function EncounterJournal_ToggleHeaders(self, doNotShift)
|
||||||
local numAdded = 0;
|
local numAdded = 0;
|
||||||
local infoHeader, parentID, _;
|
local infoHeader, parentID, _;
|
||||||
local hWidth = self:GetWidth();
|
local hWidth = EncounterJournal_GetHeaderWidth(self);
|
||||||
local nextSectionID;
|
local nextSectionID;
|
||||||
local topLevelSection = false;
|
local topLevelSection = false;
|
||||||
|
|
||||||
@@ -1703,7 +1908,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
|
|||||||
end
|
end
|
||||||
|
|
||||||
infoHeader.index = nil;
|
infoHeader.index = nil;
|
||||||
infoHeader:SetWidth(hWidth);
|
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
|
||||||
EncounterJournal_SetHeaderDescription(infoHeader, description);
|
EncounterJournal_SetHeaderDescription(infoHeader, description);
|
||||||
|
|
||||||
-- If this section has not be seen and should start open
|
-- If this section has not be seen and should start open
|
||||||
@@ -1717,6 +1922,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
|
|||||||
numAdded = numAdded + EncounterJournal_ToggleHeaders(infoHeader, true);
|
numAdded = numAdded + EncounterJournal_ToggleHeaders(infoHeader, true);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
|
||||||
infoHeader:Show();
|
infoHeader:Show();
|
||||||
end -- if not filteredByDifficulty
|
end -- if not filteredByDifficulty
|
||||||
|
|
||||||
@@ -1961,6 +2167,7 @@ function EncounterJournal_ClearDetails()
|
|||||||
|
|
||||||
local clearDisplayInfo = true;
|
local clearDisplayInfo = true;
|
||||||
EncounterJournal_HideCreatures(clearDisplayInfo);
|
EncounterJournal_HideCreatures(clearDisplayInfo);
|
||||||
|
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.modelTab, false);
|
||||||
|
|
||||||
local bossIndex = 1
|
local bossIndex = 1
|
||||||
local bossButton = _G["EncounterJournalBossButton" .. bossIndex];
|
local bossButton = _G["EncounterJournalBossButton" .. bossIndex];
|
||||||
@@ -1988,12 +2195,15 @@ function EncounterJournal_SetTab(tabType)
|
|||||||
|
|
||||||
local info = EncounterJournal.encounter.info;
|
local info = EncounterJournal.encounter.info;
|
||||||
info.tab = tabType;
|
info.tab = tabType;
|
||||||
if info.model then
|
if info.model and tabType ~= 4 then
|
||||||
info.model:Hide();
|
info.model:Hide();
|
||||||
end
|
end
|
||||||
for key, data in pairs(EJ_Tabs) do
|
for key, data in pairs(EJ_Tabs) do
|
||||||
if key == tabType then
|
if key == tabType then
|
||||||
info[data.frame]:Show();
|
info[data.frame]:Show();
|
||||||
|
if key == 4 then
|
||||||
|
EncounterJournal_ShowCreatures();
|
||||||
|
end
|
||||||
info[data.button].selected:Show();
|
info[data.button].selected:Show();
|
||||||
info[data.button].unselected:Hide();
|
info[data.button].unselected:Hide();
|
||||||
info[data.button]:LockHighlight();
|
info[data.button]:LockHighlight();
|
||||||
|
|||||||
@@ -467,7 +467,7 @@
|
|||||||
</Texture>
|
</Texture>
|
||||||
</Layer>
|
</Layer>
|
||||||
<Layer level="ARTWORK">
|
<Layer level="ARTWORK">
|
||||||
<FontString name="$parentText" parentKey="Text" inherits="PKBT_Font_14" text="UNAVAILABLE">
|
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormal" text="UNAVAILABLE">
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="CENTER" x="0" y="0"/>
|
<Anchor point="CENTER" x="0" y="0"/>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
@@ -2510,7 +2510,7 @@
|
|||||||
<OnClick function="EncounterTabTemplate_OnClick"/>
|
<OnClick function="EncounterTabTemplate_OnClick"/>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Button>
|
</Button>
|
||||||
<Button name="$parentModelTab" inherits="EncounterTabTemplate" parentKey="modelTab" id="4" hidden="true">
|
<Button name="$parentModelTab" inherits="EncounterTabTemplate" parentKey="modelTab" id="4">
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="TOP" relativeTo="$parentBossTab" relativePoint="BOTTOM" x="0" y="2"/>
|
<Anchor point="TOP" relativeTo="$parentBossTab" relativePoint="BOTTOM" x="0" y="2"/>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
@@ -2532,8 +2532,8 @@
|
|||||||
<OnLoad>
|
<OnLoad>
|
||||||
self.tooltip = MODEL;
|
self.tooltip = MODEL;
|
||||||
self:Disable();
|
self:Disable();
|
||||||
self:Hide();
|
|
||||||
</OnLoad>
|
</OnLoad>
|
||||||
|
<OnClick function="EncounterTabTemplate_OnClick"/>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Button>
|
</Button>
|
||||||
<ScrollFrame name="$parentBossesScrollFrame" parentKey="bossesScroll" inherits="UIPanelScrollFrameCodeTemplate">
|
<ScrollFrame name="$parentBossesScrollFrame" parentKey="bossesScroll" inherits="UIPanelScrollFrameCodeTemplate">
|
||||||
@@ -2844,7 +2844,7 @@
|
|||||||
</OnShow>
|
</OnShow>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</ScrollFrame>
|
</ScrollFrame>
|
||||||
<DressUpModel name="$parentModelFrame" parentKey="model" inherits="PKBT_ModelTemplate,ModelTemplate">
|
<DressUpModel name="$parentModelFrame" parentKey="model" hidden="true" enableMouse="true">
|
||||||
<Size x="390" y="423"/>
|
<Size x="390" y="423"/>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="BOTTOMRIGHT" x="-3" y="1"/>
|
<Anchor point="BOTTOMRIGHT" x="-3" y="1"/>
|
||||||
@@ -2859,6 +2859,14 @@
|
|||||||
<TexCoords left="0.76953125" right="0" top="0" bottom="0.830078125"/>
|
<TexCoords left="0.76953125" right="0" top="0" bottom="0.830078125"/>
|
||||||
</Texture>
|
</Texture>
|
||||||
</Layer>
|
</Layer>
|
||||||
|
<Layer level="ARTWORK">
|
||||||
|
<Texture name="$parentFallbackPortrait" parentKey="fallbackPortrait" hidden="true">
|
||||||
|
<Size x="256" y="256"/>
|
||||||
|
<Anchors>
|
||||||
|
<Anchor point="CENTER" x="0" y="8"/>
|
||||||
|
</Anchors>
|
||||||
|
</Texture>
|
||||||
|
</Layer>
|
||||||
<Layer level="OVERLAY">
|
<Layer level="OVERLAY">
|
||||||
<Texture name="$parentShadow" file="Interface\EncounterJournal\UI-EJ-BossModelPaperFrame">
|
<Texture name="$parentShadow" file="Interface\EncounterJournal\UI-EJ-BossModelPaperFrame">
|
||||||
<Size x="393" y="424"/>
|
<Size x="393" y="424"/>
|
||||||
@@ -2892,20 +2900,20 @@
|
|||||||
</Frames>
|
</Frames>
|
||||||
<Scripts>
|
<Scripts>
|
||||||
<OnLoad>
|
<OnLoad>
|
||||||
Mixin(self, PKBT_ModelMixin)
|
EncounterJournal_Model_OnLoad(self);
|
||||||
SharedXML_Model_OnLoad(self);
|
|
||||||
self.imageTitle = self.overlayFrame.imageTitle;
|
self.imageTitle = self.overlayFrame.imageTitle;
|
||||||
</OnLoad>
|
</OnLoad>
|
||||||
<OnShow>
|
<OnShow>
|
||||||
self:OnShow()
|
EncounterJournal_ShowCreatures();
|
||||||
EncounterJournal.encounter.info.TitleFrame.encounterTitle:Hide();
|
EncounterJournal.encounter.info.TitleFrame.encounterTitle:Hide();
|
||||||
EncounterJournal.encounter.info.rightShadow:Hide();
|
EncounterJournal.encounter.info.rightShadow:Hide();
|
||||||
EncounterJournal.encounter.info.difficulty:Hide();
|
EncounterJournal.encounter.info.difficulty:Hide();
|
||||||
</OnShow>
|
</OnShow>
|
||||||
<OnEvent>
|
<OnMouseDown function="EncounterJournal_Model_OnMouseDown"/>
|
||||||
self:OnEvent(event, ...)
|
<OnMouseUp function="EncounterJournal_Model_OnMouseUp"/>
|
||||||
SharedXML_Model_OnEvent(event, ...)
|
<OnMouseWheel function="EncounterJournal_Model_OnMouseWheel"/>
|
||||||
</OnEvent>
|
<OnUpdateModel function="EncounterJournal_Model_OnUpdateModel"/>
|
||||||
|
<OnUpdate function="EncounterJournal_Model_OnUpdate"/>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</DressUpModel>
|
</DressUpModel>
|
||||||
<Button name="$parentCreatureButton1" inherits="EncounterCreatureButtonTemplate">
|
<Button name="$parentCreatureButton1" inherits="EncounterCreatureButtonTemplate">
|
||||||
@@ -2919,8 +2927,7 @@
|
|||||||
SetParentFrameLevel(self.overviewTab, 10)
|
SetParentFrameLevel(self.overviewTab, 10)
|
||||||
SetParentFrameLevel(self.lootTab, 10)
|
SetParentFrameLevel(self.lootTab, 10)
|
||||||
SetParentFrameLevel(self.bossTab, 10)
|
SetParentFrameLevel(self.bossTab, 10)
|
||||||
self.modelTab:Hide()
|
SetParentFrameLevel(self.modelTab, 10)
|
||||||
self.model:Hide()
|
|
||||||
</OnLoad>
|
</OnLoad>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|||||||
@@ -1,544 +0,0 @@
|
|||||||
UIPanelWindows["ItemBrowser"] = { area = "left", pushable = 0, whileDead = 1, width = 830, xOffset = "15", yOffset = "-10"}
|
|
||||||
|
|
||||||
local type = type
|
|
||||||
local mathmax = math.max
|
|
||||||
local strfind, strlower, strtrim = string.find, string.lower, string.trim
|
|
||||||
local tinsert, twipe = table.insert, table.wipe
|
|
||||||
local debugprofilestop = debugprofilestop
|
|
||||||
|
|
||||||
local COLUMNS = {
|
|
||||||
{
|
|
||||||
name = "Name",
|
|
||||||
dataProviderKey = "name",
|
|
||||||
template = "ItemBrowserListCellNameIconTemplate",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = "ItemLevel",
|
|
||||||
dataProviderKey = "itemLevel",
|
|
||||||
width = 70,
|
|
||||||
template = "ItemBrowserListCellItemLevelTemplate",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = "ID",
|
|
||||||
dataProviderKey = "itemID",
|
|
||||||
width = 90,
|
|
||||||
template = "ItemBrowserListCellItemIDTemplate",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemBrowserMixin = {}
|
|
||||||
|
|
||||||
function ItemBrowserMixin:OnLoad()
|
|
||||||
self.TitleText:SetText(ITEM_BROWSER)
|
|
||||||
SetPortraitToTexture(self.portrait, [[Interface\Icons\Achievement_General_ClassicBattles_64]])
|
|
||||||
|
|
||||||
self.inset.Bgs:SetAtlas("UI-EJ-BattleforAzeroth")
|
|
||||||
|
|
||||||
do -- tabs
|
|
||||||
self:RegisterCustomEvent("SERVICE_DATA_UPDATE")
|
|
||||||
self:RegisterCustomEvent("CUSTOM_CHALLENGE_DEACTIVATED")
|
|
||||||
|
|
||||||
self.tab1:SetFrameLevel(1)
|
|
||||||
self.tab2:SetFrameLevel(1)
|
|
||||||
self.tab3:SetFrameLevel(1)
|
|
||||||
self.tab4:SetFrameLevel(1)
|
|
||||||
|
|
||||||
self.maxTabWidth = (self:GetWidth() - 19) / 4
|
|
||||||
|
|
||||||
PanelTemplates_SetNumTabs(self, 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- coroutine
|
|
||||||
self.COROUTINE_HANDLER = CreateFrame("Frame")
|
|
||||||
self.COROUTINE_HANDLER:Hide()
|
|
||||||
|
|
||||||
self.COROUTINE_HANDLER.FRAMETIME_TARGET = 1 / 55
|
|
||||||
self.COROUTINE_HANDLER.FRAMETIME_AVAILABLE = 8
|
|
||||||
self.COROUTINE_HANDLER.FRAMETIME_RESERVE = 4
|
|
||||||
self.COROUTINE_HANDLER.DEBUG = false
|
|
||||||
|
|
||||||
self.COROUTINE_HANDLER:SetScript("OnUpdate", function(this, elapsed)
|
|
||||||
local frametimeStep = this.FRAMETIME_TARGET - elapsed
|
|
||||||
|
|
||||||
if frametimeStep ~= 0 then
|
|
||||||
frametimeStep = frametimeStep * 1000
|
|
||||||
this.FRAMETIME_AVAILABLE = mathmax(5, this.FRAMETIME_AVAILABLE + frametimeStep)
|
|
||||||
end
|
|
||||||
|
|
||||||
if this.COROUTINE then
|
|
||||||
this.COROUTINE_TIMESTAMP = debugprofilestop()
|
|
||||||
local status, result, progress = coroutine.resume(this.COROUTINE)
|
|
||||||
if not status then
|
|
||||||
this.COROUTINE = nil
|
|
||||||
this:Hide()
|
|
||||||
self.PROGRESS = 1
|
|
||||||
error(result, 2)
|
|
||||||
elseif not result then
|
|
||||||
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
|
|
||||||
self.PROGRESS = progress
|
|
||||||
self:OnSearchResultUpdate()
|
|
||||||
else
|
|
||||||
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", 1) end
|
|
||||||
this.COROUTINE = nil
|
|
||||||
this:Hide()
|
|
||||||
self.PROGRESS = 1
|
|
||||||
self:OnSearchResultUpdate()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
this:Hide()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.BUTTON_OFFSET_Y = 0
|
|
||||||
self.PROGRESS = 1
|
|
||||||
|
|
||||||
self.results = {}
|
|
||||||
|
|
||||||
self.Scroll.ScrollBar:SetBackgroundShown(false)
|
|
||||||
self.Scroll.update = function(scrollFrame)
|
|
||||||
self:UpdateResultList()
|
|
||||||
end
|
|
||||||
self.Scroll.ScrollBar.doNotHide = true
|
|
||||||
self.Scroll.scrollBar = self.Scroll.ScrollBar
|
|
||||||
HybridScrollFrame_CreateButtons(self.Scroll, "ItemBrowserListRowButtonTemplate", 0, 0, nil, nil, nil, -self.BUTTON_OFFSET_Y)
|
|
||||||
|
|
||||||
self.SearchBox:ToggleOnCharFilter(true, 0.3)
|
|
||||||
|
|
||||||
self.tableBuilder = CreateTableBuilder(HybridScrollFrame_GetButtons(self.Scroll))
|
|
||||||
self.tableBuilder:SetHeaderContainer(self.HeaderHolder)
|
|
||||||
self:ConstructTable()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:OnShow()
|
|
||||||
SetParentFrameLevel(self.inset)
|
|
||||||
PanelTemplates_SetTab(self, 4)
|
|
||||||
|
|
||||||
self.dirty = true
|
|
||||||
self.Scroll.ScrollBar:SetValue(0)
|
|
||||||
self:UpdateResultList()
|
|
||||||
|
|
||||||
self.SearchBox:SetFocus()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:OnEvent(event, ...)
|
|
||||||
if event == "SERVICE_DATA_UPDATE"
|
|
||||||
or (event == "CUSTOM_CHALLENGE_DEACTIVATED" and select(2, ...) ~= Enum.HardcoreDeathReason.FAILED_DEATH)
|
|
||||||
then
|
|
||||||
if not C_Service.IsRenegadeRealm() then
|
|
||||||
PanelTemplates_HideTab(self, 2)
|
|
||||||
self.tab3:SetPoint("LEFT", self.tab1, "RIGHT", -16, 0);
|
|
||||||
end
|
|
||||||
if not C_Service.IsHardcoreEnabledOnRealm() then
|
|
||||||
PanelTemplates_HideTab(self, 3)
|
|
||||||
end
|
|
||||||
if not C_Service.IsGMAccount() and not IsInterfaceDevClient() then
|
|
||||||
PanelTemplates_HideTab(self, 4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:GetRowProvider()
|
|
||||||
return function(itemIndex)
|
|
||||||
if itemIndex > #self.results then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local itemID = self.results[itemIndex]
|
|
||||||
local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(itemID, false, nil, true, true)
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemID = itemID,
|
|
||||||
name = itemName,
|
|
||||||
itemLink = itemLink,
|
|
||||||
itemLevel = itemLevel,
|
|
||||||
itemType = itemType,
|
|
||||||
itemSubType = itemSubType,
|
|
||||||
icon = itemTexture,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:ConstructTable()
|
|
||||||
local textPadding = 1
|
|
||||||
local cellPadding = 10
|
|
||||||
|
|
||||||
self.tableBuilder:Reset()
|
|
||||||
self.tableBuilder:SetDataProvider(self:GetRowProvider())
|
|
||||||
self.tableBuilder:SetTableMargins(5)
|
|
||||||
self.tableBuilder:SetTableWidth(self.Scroll:GetWidth() or select(3, self.Scroll:GetRect()))
|
|
||||||
|
|
||||||
for index, columnInfo in ipairs(COLUMNS) do
|
|
||||||
local column = self.tableBuilder:AddColumn()
|
|
||||||
column:ConstructHeader("Button", "ItemBrowserListHeaderButtonTemplate", columnInfo.name)
|
|
||||||
column:ConstrainToHeader(textPadding)
|
|
||||||
column:ConstructCells("Frame", columnInfo.template)
|
|
||||||
|
|
||||||
if columnInfo.width then
|
|
||||||
column:SetFixedConstraints(columnInfo.width, cellPadding)
|
|
||||||
else
|
|
||||||
column:SetFillConstraints(1, cellPadding)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.tableBuilder:Arrange()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:GetNumItems()
|
|
||||||
if not self.NUM_ITEMS then
|
|
||||||
self.NUM_ITEMS = tCount(ItemsCache)
|
|
||||||
end
|
|
||||||
return self.NUM_ITEMS
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:SearchItem(item)
|
|
||||||
item = strtrim(item)
|
|
||||||
if item == "" then
|
|
||||||
self:ClearResults()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local itemID = C_Item.GetItemIDFromString(item)
|
|
||||||
local searchField, searchValue, useStringFind
|
|
||||||
|
|
||||||
if itemID then
|
|
||||||
searchField = Enum.ItemCacheField.ITEM_ID
|
|
||||||
searchValue = itemID
|
|
||||||
elseif item ~= "" then
|
|
||||||
searchField = GetLocale() == "ruRU" and Enum.ItemCacheField.NAME_RURU or Enum.ItemCacheField.NAME_ENGB
|
|
||||||
searchValue = strlower(item)
|
|
||||||
useStringFind = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if not searchField then
|
|
||||||
self:ClearResults()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self:StartSearch(searchField, searchValue, useStringFind)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:StartSearch(searchField, searchValue, useStringFind)
|
|
||||||
if self.SEARCH_FIELD == searchField and self.SEARCH_VALUE == searchValue then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.COROUTINE then
|
|
||||||
coroutine.close(self.COROUTINE)
|
|
||||||
self.COROUTINE = nil
|
|
||||||
self:ClearResults()
|
|
||||||
end
|
|
||||||
|
|
||||||
self.SEARCH_FIELD = searchField
|
|
||||||
self.SEARCH_VALUE = searchValue
|
|
||||||
self.USE_STRING_FIND = useStringFind
|
|
||||||
|
|
||||||
local framerate = GetFramerate()
|
|
||||||
self.COROUTINE_HANDLER.FRAMETIME_TARGET = framerate > 63 and (1 / 60) or (1 / 55)
|
|
||||||
self.COROUTINE_HANDLER.FRAMETIME_BUDGET = 1000 / framerate - self.COROUTINE_HANDLER.FRAMETIME_RESERVE
|
|
||||||
|
|
||||||
self.COROUTINE_HANDLER.COROUTINE = coroutine.create(self.SearchProcess)
|
|
||||||
self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP = debugprofilestop()
|
|
||||||
|
|
||||||
local status, result, progress = coroutine.resume(self.COROUTINE_HANDLER.COROUTINE, self)
|
|
||||||
if not status then
|
|
||||||
self.COROUTINE_HANDLER.COROUTINE = nil
|
|
||||||
self.COROUTINE_HANDLER:Hide()
|
|
||||||
self.PROGRESS = 1
|
|
||||||
self:ClearResults()
|
|
||||||
geterrorhandler()(result)
|
|
||||||
elseif coroutine.status(self.COROUTINE_HANDLER.COROUTINE) == "dead" then
|
|
||||||
self.COROUTINE_HANDLER.COROUTINE = nil
|
|
||||||
self.PROGRESS = 1
|
|
||||||
else
|
|
||||||
if self.COROUTINE_HANDLER.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
|
|
||||||
self.PROGRESS = progress
|
|
||||||
self.COROUTINE_HANDLER:Show()
|
|
||||||
end
|
|
||||||
|
|
||||||
self:OnSearchResultUpdate()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:SearchProcess()
|
|
||||||
twipe(self.results)
|
|
||||||
|
|
||||||
if not ItemsCache then
|
|
||||||
return true, 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local processedEntries = 0
|
|
||||||
local fieldValue
|
|
||||||
|
|
||||||
for itemID, itemInfo in pairs(ItemsCache) do
|
|
||||||
processedEntries = processedEntries + 1
|
|
||||||
|
|
||||||
fieldValue = itemInfo[self.SEARCH_FIELD]
|
|
||||||
|
|
||||||
if type(fieldValue) == "string" then
|
|
||||||
fieldValue = strlower(fieldValue)
|
|
||||||
end
|
|
||||||
|
|
||||||
if fieldValue == self.SEARCH_VALUE
|
|
||||||
or (self.USE_STRING_FIND and strfind(fieldValue, self.SEARCH_VALUE, 1, true))
|
|
||||||
then
|
|
||||||
tinsert(self.results, itemInfo[Enum.ItemCacheField.ITEM_ID])
|
|
||||||
end
|
|
||||||
|
|
||||||
if (debugprofilestop() - self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP) > self.COROUTINE_HANDLER.FRAMETIME_BUDGET then
|
|
||||||
coroutine.yield(false, processedEntries / self:GetNumItems())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:OnSearchResultUpdate()
|
|
||||||
self.dirty = true
|
|
||||||
self:UpdateSearchProgress()
|
|
||||||
self:UpdateResultList()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:UpdateSearchProgress()
|
|
||||||
local progress = Clamp(self.PROGRESS or 1, 0, 1)
|
|
||||||
self.SearchProgressBar:SetValue(progress)
|
|
||||||
self.SearchProgressBar:SetAlpha(progress < 1 and 1 or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:ClearResults()
|
|
||||||
twipe(self.results)
|
|
||||||
|
|
||||||
self.SEARCH_FIELD = nil
|
|
||||||
self.SEARCH_VALUE = nil
|
|
||||||
self.USE_STRING_FIND = nil
|
|
||||||
|
|
||||||
self.PROGRESS = 1
|
|
||||||
self.dirty = true
|
|
||||||
|
|
||||||
self:UpdateSearchProgress()
|
|
||||||
self:UpdateResultList()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserMixin:UpdateResultList()
|
|
||||||
local scrollFrame = self.Scroll
|
|
||||||
local offset = HybridScrollFrame_GetOffset(scrollFrame)
|
|
||||||
local numResults = self.results and #self.results or 0
|
|
||||||
|
|
||||||
if self.numItems ~= numResults then
|
|
||||||
self.numItems = numResults
|
|
||||||
self.dirty = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local populateCount = math.min(#scrollFrame.buttons, numResults)
|
|
||||||
self.tableBuilder:Populate(offset, populateCount)
|
|
||||||
|
|
||||||
local mouseFocus = GetMouseFocus()
|
|
||||||
for index, button in ipairs(scrollFrame.buttons) do
|
|
||||||
button:SetOwner(self)
|
|
||||||
button:SetShown(index <= numResults)
|
|
||||||
if button == mouseFocus then
|
|
||||||
button:OnEnter()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.dirty then
|
|
||||||
local buttonHeight = scrollFrame.buttons[1] and scrollFrame.buttons[1]:GetHeight() or 0
|
|
||||||
local scrollHeight = buttonHeight * numResults + self.BUTTON_OFFSET_Y * (numResults - 1) + self.BUTTON_OFFSET_Y / 3
|
|
||||||
HybridScrollFrame_Update(scrollFrame, scrollHeight, scrollFrame:GetHeight())
|
|
||||||
self.dirty = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserSearchBoxMixin = CreateFromMixins(PKBT_EditBoxMixin)
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnLoad()
|
|
||||||
PKBT_EditBoxMixin.OnLoad(self)
|
|
||||||
|
|
||||||
local l, r, t, b = self:GetTextInsets()
|
|
||||||
self:SetTextInsets(l + 10, r + 10, t - 1, b)
|
|
||||||
|
|
||||||
self:SetInstructions(SEARCH)
|
|
||||||
self.Instructions:ClearAllPoints()
|
|
||||||
self.Instructions:SetPoint("LEFT", 19, -2)
|
|
||||||
|
|
||||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6)
|
|
||||||
|
|
||||||
self:SetCustomCharFilter(function(text)
|
|
||||||
text = string.gsub(text, "%s%s+", " ")
|
|
||||||
text = utf8.gsub(text, "[^A-zА-я0-9':%- ]+", "")
|
|
||||||
return text
|
|
||||||
end)
|
|
||||||
|
|
||||||
self.value = ""
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnHide()
|
|
||||||
self:StopOnCharSearchDelay()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnEnterPressed()
|
|
||||||
self:ClearFocus()
|
|
||||||
self:DoSearch()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnEditFocusLost()
|
|
||||||
SearchBoxTemplate_OnEditFocusLost(self)
|
|
||||||
self:HighlightText(0, 0)
|
|
||||||
self:DoSearch()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
|
|
||||||
self.searchDelay = self.searchDelay - elapsed
|
|
||||||
|
|
||||||
if self.searchDelay <= 0 then
|
|
||||||
self:SetScript("OnUpdate", nil)
|
|
||||||
self.searchDelay = nil
|
|
||||||
self:DoSearch()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:DoSearch()
|
|
||||||
local newValue = self:GetText()
|
|
||||||
if self.value ~= newValue and not (self.value == nil and newValue == "") then
|
|
||||||
self.value = newValue
|
|
||||||
self:GetParent():SearchItem(newValue)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:ToggleOnCharFilter(state, searchUpdateDelay)
|
|
||||||
if state then
|
|
||||||
self.searchUpdateDelay = searchUpdateDelay
|
|
||||||
self.searchDelay = searchUpdateDelay
|
|
||||||
else
|
|
||||||
self:SetScript("OnUpdate", nil)
|
|
||||||
self.searchUpdateDelay = nil
|
|
||||||
self.searchDelay = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnTextChanged(userInput)
|
|
||||||
PKBT_EditBoxMixin.OnTextChanged(self, userInput)
|
|
||||||
SearchBoxTemplate_OnTextChanged(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnTextValueChanged(value, userInput)
|
|
||||||
if userInput then
|
|
||||||
if self.searchUpdateDelay then
|
|
||||||
self.searchDelay = self.searchUpdateDelay
|
|
||||||
self:SetScript("OnUpdate", self.OnUpdate)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
self:StopOnCharSearchDelay()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
|
|
||||||
self.searchDelay = self.searchDelay - elapsed
|
|
||||||
|
|
||||||
if self.searchDelay <= 0 then
|
|
||||||
self:SetScript("OnUpdate", nil)
|
|
||||||
self.searchDelay = nil
|
|
||||||
self:DoSearch()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:StopOnCharSearchDelay()
|
|
||||||
self:SetScript("OnUpdate", nil)
|
|
||||||
|
|
||||||
if self.searchUpdateDelay then
|
|
||||||
self.searchDelay = self.searchUpdateDelay
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserSearchBoxMixin:OnClearClick(button)
|
|
||||||
SearchBoxTemplateClearButton_OnClick(self.clearButton, button)
|
|
||||||
self:StopOnCharSearchDelay()
|
|
||||||
self:DoSearch()
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListHeaderButtonMixin = CreateFromMixins(TableBuilderElementMixin)
|
|
||||||
|
|
||||||
function ItemBrowserListHeaderButtonMixin:Init(name)
|
|
||||||
self:SetText(name)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListHeaderButtonMixin:OnClick(button)
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListRowButton = CreateFromMixins(PKBT_OwnerMixin, TableBuilderRowMixin)
|
|
||||||
|
|
||||||
function ItemBrowserListRowButton:OnLoad()
|
|
||||||
self.UpdateTooltip = self.OnEnter
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListRowButton:OnEnter()
|
|
||||||
if self.itemLink then
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
||||||
GameTooltip:SetHyperlink(self.itemLink)
|
|
||||||
GameTooltip:Show()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListRowButton:OnLeave()
|
|
||||||
GameTooltip:Hide()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListRowButton:OnClick(button)
|
|
||||||
if self.itemLink then
|
|
||||||
HandleModifiedItemClick(self.itemLink)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListRowButton:Populate(rowData, dataIndex)
|
|
||||||
self:SetID(dataIndex)
|
|
||||||
self.itemID = rowData.itemID
|
|
||||||
self.itemLink = rowData.itemLink
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListCellMixin = {}
|
|
||||||
|
|
||||||
function ItemBrowserListCellMixin:OnLoad()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListCellMixin:Init(dataProviderKey)
|
|
||||||
self.dataProviderKey = dataProviderKey
|
|
||||||
end
|
|
||||||
|
|
||||||
function ItemBrowserListCellMixin:Populate(rowData, dataIndex)
|
|
||||||
local value = rowData[self.dataProviderKey]
|
|
||||||
self.Text:SetJustifyH(self.textAlignment or "CENTER")
|
|
||||||
self.Text:SetText(value or "?")
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListCellNameIconMixin = CreateFromMixins(ItemBrowserListCellMixin)
|
|
||||||
|
|
||||||
function ItemBrowserListCellNameIconMixin:Populate(rowData, dataIndex)
|
|
||||||
local textWidth = self:GetWidth() - 10
|
|
||||||
|
|
||||||
self.Text:SetText(rowData.name or UNKNOWN)
|
|
||||||
self.Text:SetWidth(textWidth)
|
|
||||||
self.SubText:SetWidth(textWidth)
|
|
||||||
|
|
||||||
if rowData.itemType and rowData.itemSubType then
|
|
||||||
self.SubText:SetFormattedText("%s - %s", rowData.itemType, rowData.itemSubType)
|
|
||||||
elseif rowData.itemType or rowData.itemSubType then
|
|
||||||
self.SubText:SetText(rowData.itemType or rowData.itemSubType)
|
|
||||||
else
|
|
||||||
self.SubText:SetText("")
|
|
||||||
end
|
|
||||||
|
|
||||||
self.Icon:SetTexture(rowData.icon or QUESTION_MARK_ICON)
|
|
||||||
local r, g, b = GetItemQualityColor(rowData.rarity or 1)
|
|
||||||
self.Border:SetVertexColor(r, g, b)
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListCellItemLevelMixin = CreateFromMixins(ItemBrowserListCellMixin)
|
|
||||||
|
|
||||||
function ItemBrowserListCellItemLevelMixin:Populate(rowData, dataIndex)
|
|
||||||
self.Text:SetText(rowData.itemLevel and rowData.itemLevel ~= 0 and rowData.itemLevel or "")
|
|
||||||
end
|
|
||||||
|
|
||||||
ItemBrowserListCellItemIDMixin = CreateFromMixins(ItemBrowserListCellMixin)
|
|
||||||
|
|
||||||
function ItemBrowserListCellItemIDMixin:Populate(rowData, dataIndex)
|
|
||||||
self.Text:SetText(rowData.itemID)
|
|
||||||
end
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
|
||||||
..\..\FrameXML\UI.xsd">
|
|
||||||
<Script file="Custom_ItemBrowser.lua"/>
|
|
||||||
|
|
||||||
<Button name="ItemBrowserListHeaderButtonTemplate" virtual="true">
|
|
||||||
<Size x="50" y="30"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true">
|
|
||||||
<Color r="0.2" g="0.2" b="0.2"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<!--
|
|
||||||
<Layer level="HIGHLIGHT">
|
|
||||||
<Texture name="$parentHighlight" parentKey="Highlight" setAllPoints="true" alpha="0.2" alphaMode="ADD">
|
|
||||||
<Color r="1" g="1" b="1"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
-->
|
|
||||||
</Layers>
|
|
||||||
|
|
||||||
<NormalFont style="SystemFont_Shadow_Small"/>
|
|
||||||
<PushedTextOffset x="0" y="0"/>
|
|
||||||
<!--
|
|
||||||
<PushedTextOffset x="1" y="-1"/>
|
|
||||||
<ButtonText name="$parentButtonText" parentKey="ButtonText">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" x="0" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
</ButtonText>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserListHeaderButtonMixin)
|
|
||||||
</OnLoad>
|
|
||||||
<!--
|
|
||||||
<OnClick>
|
|
||||||
self:OnClick(button)
|
|
||||||
</OnClick>
|
|
||||||
-->
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
<Button name="ItemBrowserListRowButtonTemplate" virtual="true">
|
|
||||||
<Size x="0" y="46"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" x="1" y="0"/>
|
|
||||||
<Anchor point="RIGHT" x="-1" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true" inherits="_SearchBarLg"/>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<HighlightTexture file="Interface\Common\Search">
|
|
||||||
<TexCoords left="0.001953" right="0.501953" top="0.234375" bottom="0.601562"/>
|
|
||||||
</HighlightTexture>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserListRowButton)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
<OnEvent>
|
|
||||||
self:OnEvent(event, ...)
|
|
||||||
</OnEvent>
|
|
||||||
<OnEnter>
|
|
||||||
self:OnEnter()
|
|
||||||
</OnEnter>
|
|
||||||
<OnLeave>
|
|
||||||
self:OnLeave()
|
|
||||||
</OnLeave>
|
|
||||||
<OnClick>
|
|
||||||
self:OnClick()
|
|
||||||
</OnClick>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Frame name="ItemBrowserListCellNameIconTemplate" virtual="true">
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<Texture name="$parentBorder" parentKey="Border" file="Interface\Common\Search">
|
|
||||||
<Size x="40" y="40"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" x="10" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<TexCoords left="0.755859" right="0.830078" top="0.234375" bottom="0.531250"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<Texture name="$parentIcon" parentKey="Icon">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentBorder" x="1" y="-2"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorder" x="-1" y="1"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" justifyH="LEFT" maxLines="1">
|
|
||||||
<Size x="0" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT" x="10" y="8"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.96875" g="0.8984375" b="0.578125" a="1"/>
|
|
||||||
</FontString>
|
|
||||||
<FontString name="$parentSubText" parentKey="SubText" inherits="GameFontNormal" justifyH="LEFT">
|
|
||||||
<Size x="400" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentText" relativePoint="BOTTOMLEFT" x="0" y="-5"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.66796875" g="0.51171875" b="0.3359375" a="1"/>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserListCellNameIconMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
<Frame name="ItemBrowserListCellItemLevelTemplate" virtual="true">
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
|
|
||||||
<Size x="0" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" x="0" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1" g="1" b="1"/>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserListCellItemLevelMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
<Frame name="ItemBrowserListCellItemIDTemplate" virtual="true">
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
|
|
||||||
<Size x="0" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" x="0" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="1" g="1" b="1"/>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserListCellItemIDMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<Frame name="ItemBrowser" inherits="PortraitFrameTemplate" toplevel="true" enableMouse="true" parent="UIParent" hidden="true">
|
|
||||||
<Size x="800" y="496"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" x="0" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
|
|
||||||
<Frames>
|
|
||||||
<Frame name="$parentInset" inherits="InsetFrameTemplate" parentKey="inset">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" x="-4" y="-60"/>
|
|
||||||
<Anchor point="BOTTOMLEFT" x="4" y="5"/>
|
|
||||||
</Anchors>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<EditBox name="$parentSearchBox" parentKey="SearchBox" inherits="PKBT_EditBoxTemplate" letters="64">
|
|
||||||
<Size x="130" y="36"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" x="60" y="-22"/>
|
|
||||||
<Anchor point="TOPRIGHT" x="-10" y="-22"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<Texture name="$parentSearchIcon" parentKey="searchIcon" file="Interface\Common\UI-Searchbox-Icon">
|
|
||||||
<Size x="14" y="14"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" x="5" y="-2"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<Button name="$parentClearButton" parentKey="clearButton" hidden="true">
|
|
||||||
<Size x="22" y="22"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="RIGHT" x="-5" y="-1"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<Texture parentKey="texture" file="Interface\FriendsFrame\ClearBroadcastIcon" alpha="0.5" setAllPoints="true"/>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnEnter>
|
|
||||||
self.texture:SetAlpha(1.0)
|
|
||||||
</OnEnter>
|
|
||||||
<OnLeave>
|
|
||||||
self.texture:SetAlpha(0.5)
|
|
||||||
</OnLeave>
|
|
||||||
<OnMouseDown>
|
|
||||||
if self:IsEnabled() == 1 then
|
|
||||||
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1)
|
|
||||||
end
|
|
||||||
</OnMouseDown>
|
|
||||||
<OnMouseUp>
|
|
||||||
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
|
|
||||||
</OnMouseUp>
|
|
||||||
<OnClick>
|
|
||||||
self:GetParent():OnClearClick(button)
|
|
||||||
</OnClick>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserSearchBoxMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
<OnHide>
|
|
||||||
self:OnHide()
|
|
||||||
</OnHide>
|
|
||||||
<OnTextChanged>
|
|
||||||
self:OnTextChanged(userInput)
|
|
||||||
</OnTextChanged>
|
|
||||||
<OnEnterPressed>
|
|
||||||
self:OnEnterPressed()
|
|
||||||
</OnEnterPressed>
|
|
||||||
<OnEditFocusLost>
|
|
||||||
self:OnEditFocusLost()
|
|
||||||
</OnEditFocusLost>
|
|
||||||
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
|
|
||||||
</Scripts>
|
|
||||||
</EditBox>
|
|
||||||
|
|
||||||
<StatusBar name="$parentSearchProgressBar" parentKey="SearchProgressBar" minValue="0" maxValue="1" alpha="0">
|
|
||||||
<Size x="0" y="6"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentSearchBox" relativePoint="BOTTOMLEFT" x="7" y="1"/>
|
|
||||||
<Anchor point="TOPRIGHT" relativeTo="$parentSearchBox" relativePoint="BOTTOMRIGHT" x="-7" y="1"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture parentKey="Background">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0" g="0" b="0" a="0.3"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
|
||||||
<BarColor r="0" g="1" b="0"/>
|
|
||||||
</StatusBar>
|
|
||||||
|
|
||||||
<ScrollFrame name="$parentScroll" parentKey="Scroll" inherits="HybridScrollFrameTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentInset" relativePoint="TOPLEFT" x="10" y="-50"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" relativePoint="BOTTOMRIGHT" x="-36" y="10"/>
|
|
||||||
</Anchors>
|
|
||||||
<Frames>
|
|
||||||
<Slider name="$parentScrollBar" parentKey="ScrollBar" inherits="PKBT_HybridScrollBarTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="5" y="-25"/>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="5" y="27"/>
|
|
||||||
</Anchors>
|
|
||||||
</Slider>
|
|
||||||
</Frames>
|
|
||||||
</ScrollFrame>
|
|
||||||
|
|
||||||
<Frame name="$parentHeaderHolder" parentKey="HeaderHolder">
|
|
||||||
<Size x="0" y="30"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentScroll" relativePoint="TOPLEFT" x="0" y="12"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentScroll" relativePoint="TOPRIGHT" x="0" y="12"/>
|
|
||||||
</Anchors>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<Button name="$parentTab1" inherits="CharacterFrameTabButtonTemplate" text="ADVENTURE_JOURNAL" id="1" parentKey="tab1">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="BOTTOMLEFT" x="19" y="-30"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="Adventure_TabOnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
<Button name="$parentTab2" inherits="CharacterFrameTabButtonTemplate" text="HEADHUNTING" id="2" parentKey="tab2">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeTo="$parentTab1" relativePoint="RIGHT" x="-16" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="Adventure_TabOnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
<Button name="$parentTab3" inherits="CharacterFrameTabButtonTemplate" text="HARDCORE_CHALLENGES_TITLE" id="3" parentKey="tab3">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeTo="$parentTab2" relativePoint="RIGHT" x="-16" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="Adventure_TabOnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
<Button name="$parentTab4" inherits="CharacterFrameTabButtonTemplate" text="ITEM_BROWSER" id="4" parentKey="tab4">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativeTo="$parentTab3" relativePoint="RIGHT" x="-16" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnClick function="Adventure_TabOnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, ItemBrowserMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
<OnShow>
|
|
||||||
self:OnShow()
|
|
||||||
</OnShow>
|
|
||||||
<OnEvent>
|
|
||||||
self:OnEvent(event, ...)
|
|
||||||
</OnEvent>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
</Ui>
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
do
|
|
||||||
local tonumber = tonumber
|
|
||||||
local unpack = unpack
|
|
||||||
local bitband = bit.band
|
|
||||||
local strsub = string.sub
|
|
||||||
|
|
||||||
local LJ_BUFFER = {}
|
|
||||||
|
|
||||||
local NO_CLASS_FILTER = 0
|
|
||||||
local NO_SPEC_FILTER = 0
|
|
||||||
|
|
||||||
local function getPlayerFactionID()
|
|
||||||
local factionGroup = UnitFactionGroup("player") or "NEUTRAL"
|
|
||||||
if factionGroup == "Renegade" then
|
|
||||||
return (C_FactionManager.GetFactionInfoOriginal())
|
|
||||||
end
|
|
||||||
return PLAYER_FACTION_GROUP[factionGroup]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function filterByFaction(factionID, playerFactionID)
|
|
||||||
if factionID == PLAYER_FACTION_GROUP.Neutral then
|
|
||||||
return true
|
|
||||||
elseif factionID == playerFactionID then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function filterByClassAndSpecFilters(classID, specID, classFilter, specFilter)
|
|
||||||
if classFilter and specFilter then
|
|
||||||
if specFilter == 3 then
|
|
||||||
specFilter = 4
|
|
||||||
end
|
|
||||||
|
|
||||||
if classFilter == NO_CLASS_FILTER and specFilter == NO_SPEC_FILTER then
|
|
||||||
return true
|
|
||||||
elseif classFilter == classID and specFilter == NO_SPEC_FILTER then
|
|
||||||
return true
|
|
||||||
elseif classFilter == classID and bitband(specFilter, specID) == specFilter then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sortItemSets(a, b)
|
|
||||||
if a[4] == b[4] then
|
|
||||||
if a[6] ~= b[6] then
|
|
||||||
return a[6]
|
|
||||||
end
|
|
||||||
return (tonumber(strsub(a[3], 3, -1) or 0)) > (tonumber(strsub(b[3], 3, -1) or 0))
|
|
||||||
end
|
|
||||||
return a[4] > b[4]
|
|
||||||
end
|
|
||||||
|
|
||||||
C_LootJournal = {}
|
|
||||||
|
|
||||||
function C_LootJournal.GenerateLootData(classFilter, specFilter)
|
|
||||||
table.wipe(LJ_BUFFER)
|
|
||||||
|
|
||||||
local serverID = C_Service and C_Service.GetRealmID and C_Service.GetRealmID() or 0
|
|
||||||
local playerFactionID = getPlayerFactionID()
|
|
||||||
local itemSetSourceRealm = EJ_ITEMSET_SOURCE_REALM and EJ_ITEMSET_SOURCE_REALM[serverID]
|
|
||||||
|
|
||||||
for setIndex, itemSetData in pairs(EJ_ITEMSET_DATA) do
|
|
||||||
local name, itemlevel, tierName, sourceID, classID, specID, isPVP, itemList, factionID, realmFlag = unpack(itemSetData, 1, 10)
|
|
||||||
if not factionID then
|
|
||||||
factionID = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if filterByClassAndSpecFilters(classID, specID, classFilter or 0, specFilter or 0)
|
|
||||||
and filterByFaction(factionID, playerFactionID)
|
|
||||||
then
|
|
||||||
if itemSetSourceRealm and itemSetSourceRealm[setIndex] then
|
|
||||||
sourceID = itemSetSourceRealm[setIndex]
|
|
||||||
end
|
|
||||||
local sourceText = EJ_ITEMSET_SOURCE[sourceID] or ""
|
|
||||||
|
|
||||||
local numItems = type(itemList) == "table" and #itemList or 0
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
for i = 1, numItems do
|
|
||||||
local itemID = itemList[i]
|
|
||||||
items[i] = {
|
|
||||||
itemID = itemID,
|
|
||||||
icon = GetItemIcon(itemID) or [[Interface\Icons\INV_Misc_QuestionMark]],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
LJ_BUFFER[#LJ_BUFFER + 1] = {setIndex, name, tierName or "", itemlevel, sourceText, isPVP == 1, items, numItems}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
table.sort(LJ_BUFFER, sortItemSets)
|
|
||||||
|
|
||||||
return LJ_BUFFER
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_LootJournal.GetNumItemSets()
|
|
||||||
return #LJ_BUFFER
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_LootJournal.GetItemSetInfo(index)
|
|
||||||
if index and LJ_BUFFER[index] then
|
|
||||||
return unpack(LJ_BUFFER[index])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local NO_SPEC_FILTER = 0;
|
|
||||||
local NO_CLASS_FILTER = 0;
|
|
||||||
local NO_INV_TYPE_FILTER = 0;
|
|
||||||
|
|
||||||
--===================================================================================================================================
|
|
||||||
LootJournalItemsMixin = { };
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:OnLoad()
|
|
||||||
self.Background:SetAtlas("loottab-background", true)
|
|
||||||
self:SetView(LOOT_JOURNAL_ITEM_SETS);
|
|
||||||
local _, _, classID = UnitClass("player");
|
|
||||||
local specIndex = C_Talent.GetCurrentSpecTabIndex()
|
|
||||||
self:SetClassAndSpecFilters(classID, specIndex);
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:SetClassAndSpecFilters(classID, specID)
|
|
||||||
self.classFilter = classID;
|
|
||||||
self.specFilter = specID;
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:GetClassAndSpecFilters()
|
|
||||||
return self.classFilter or NO_CLASS_FILTER, self.specFilter or NO_SPEC_FILTER;
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:SetView(view)
|
|
||||||
if ( self.view == view ) then
|
|
||||||
return;
|
|
||||||
end
|
|
||||||
|
|
||||||
self.view = view;
|
|
||||||
self.ItemSetsFrame:Show();
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:GetActiveList()
|
|
||||||
return self.ItemSetsFrame;
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsMixin:Refresh()
|
|
||||||
self:GetActiveList():Refresh();
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemButtonTemplate_OnEnter(self)
|
|
||||||
local listFrame = self:GetParent();
|
|
||||||
while ( listFrame and not listFrame.ShowItemTooltip ) do
|
|
||||||
listFrame = listFrame:GetParent();
|
|
||||||
end
|
|
||||||
if ( listFrame ) then
|
|
||||||
listFrame:ShowItemTooltip(self);
|
|
||||||
self:SetScript("OnUpdate", LootJournalItemButton_OnUpdate);
|
|
||||||
self.UpdateTooltip = LootJournalItemButtonTemplate_OnEnter;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemButtonTemplate_OnLeave(self)
|
|
||||||
self.UpdateTooltip = nil;
|
|
||||||
GameTooltip:Hide();
|
|
||||||
self:SetScript("OnUpdate", nil);
|
|
||||||
ResetCursor();
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemButton_OnClick(self, button)
|
|
||||||
if HandleModifiedItemClick(self.itemLink) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
LootJournal_OpenItemByEntry(self.itemID)
|
|
||||||
end
|
|
||||||
|
|
||||||
--===================================================================================================================================
|
|
||||||
LootJournalItemSetsMixin = {}
|
|
||||||
|
|
||||||
local LJ_ITEMSET_X_OFFSET = 10;
|
|
||||||
local LJ_ITEMSET_Y_OFFSET = 10;
|
|
||||||
local LJ_ITEMSET_BUTTON_SPACING = 13;
|
|
||||||
local LJ_ITEMSET_BOTTOM_BUFFER = 4;
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:Refresh()
|
|
||||||
self.dirty = true;
|
|
||||||
local offset = self.scrollBar:GetValue();
|
|
||||||
if ( offset == 0 ) then
|
|
||||||
self:UpdateList();
|
|
||||||
else
|
|
||||||
self.scrollBar:SetValue(0);
|
|
||||||
end
|
|
||||||
if ( self.ClassButton ) then
|
|
||||||
self:UpdateClassButtonText();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:GetClassAndSpecFilters()
|
|
||||||
return self:GetParent():GetClassAndSpecFilters();
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:UpdateClassButtonText()
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:ShowItemTooltip(button)
|
|
||||||
-- itemLink may not be available
|
|
||||||
if not button.itemLink then
|
|
||||||
GameTooltip:Hide()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT");
|
|
||||||
GameTooltip:SetHyperlink(button.itemLink);
|
|
||||||
|
|
||||||
if LootJournal_CanOpenItemByEntry(button.itemID) then
|
|
||||||
GameTooltip:AddLine(LOOTJOURNAL_ITEM_CLICK_TO_OPEN_LOOT, 0, 0.8, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.tooltipItemID = button.itemID;
|
|
||||||
GameTooltip:Show();
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:CheckItemButtonTooltip(button)
|
|
||||||
if ( GameTooltip:GetOwner() == button and self.tooltipItemID ~= button.itemID ) then
|
|
||||||
self:ShowItemTooltip(button);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:GetClassFilter()
|
|
||||||
local classFilter, specFilter = self:GetClassAndSpecFilters();
|
|
||||||
return classFilter;
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:GetSpecFilter()
|
|
||||||
local classFilter, specFilter = self:GetClassAndSpecFilters();
|
|
||||||
return specFilter;
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:SetClassAndSpecFilters(newClassFilter, newSpecFilter)
|
|
||||||
local classFilter, specFilter = self:GetClassAndSpecFilters();
|
|
||||||
if not self.classAndSpecFiltersSet or classFilter ~= newClassFilter or specFilter ~= newSpecFilter then
|
|
||||||
--[[
|
|
||||||
-- if choosing just a class without a spec, pick a spec
|
|
||||||
if newClassFilter ~= NO_CLASS_FILTER and newSpecFilter == NO_SPEC_FILTER then
|
|
||||||
local _, _, classID = UnitClass("player");
|
|
||||||
-- if player's class, choose active spec
|
|
||||||
-- otherwise use 1st spec
|
|
||||||
if classID == newClassFilter then
|
|
||||||
local specIndex = C_Talent.GetCurrentSpecTabIndex()
|
|
||||||
newSpecFilter = specIndex
|
|
||||||
else
|
|
||||||
newSpecFilter = 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--]]
|
|
||||||
self:GetParent():SetClassAndSpecFilters(newClassFilter, newSpecFilter);
|
|
||||||
self.scrollBar:SetValue(0);
|
|
||||||
self:Refresh();
|
|
||||||
end
|
|
||||||
|
|
||||||
CloseDropDownMenus(1);
|
|
||||||
self.classAndSpecFiltersSet = true;
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
function LootJournalItemButton_OnUpdate(self)
|
|
||||||
if GameTooltip:IsOwned(self) then
|
|
||||||
if IsModifiedClick("DRESSUP") then
|
|
||||||
ShowInspectCursor();
|
|
||||||
else
|
|
||||||
ResetCursor();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function OpenClassFilterDropDown(self, level)
|
|
||||||
if level then
|
|
||||||
self:GetParent():GetActiveList():OpenClassFilterDropDown(level);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemsClassDropDown_OnLoad(self)
|
|
||||||
UIDropDownMenu_Initialize(self, OpenClassFilterDropDown, "MENU");
|
|
||||||
end
|
|
||||||
|
|
||||||
local CLASS_DROPDOWN = 1;
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:OpenClassFilterDropDown(level)
|
|
||||||
local filterClassID = self:GetClassFilter();
|
|
||||||
local filterSpecID = self:GetSpecFilter();
|
|
||||||
local classDisplayName, classTag, classID;
|
|
||||||
|
|
||||||
local function SetClassAndSpecFilters(_, classFilter, specFilter)
|
|
||||||
self:SetClassAndSpecFilters(classFilter, specFilter);
|
|
||||||
end
|
|
||||||
|
|
||||||
local info = UIDropDownMenu_CreateInfo();
|
|
||||||
|
|
||||||
if UIDROPDOWNMENU_MENU_VALUE == CLASS_DROPDOWN then
|
|
||||||
info.text = ALL_CLASSES;
|
|
||||||
info.checked = filterClassID == NO_CLASS_FILTER;
|
|
||||||
info.arg1 = NO_CLASS_FILTER;
|
|
||||||
info.arg2 = NO_SPEC_FILTER;
|
|
||||||
info.func = SetClassAndSpecFilters;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
|
|
||||||
local numClasses = GetNumClasses();
|
|
||||||
for i = 1, numClasses do
|
|
||||||
classDisplayName, classTag, classID = GetClassInfo(i);
|
|
||||||
if classID ~= 10 then
|
|
||||||
info.text = classDisplayName;
|
|
||||||
info.checked = filterClassID == classID;
|
|
||||||
info.arg1 = classID;
|
|
||||||
info.arg2 = NO_SPEC_FILTER;
|
|
||||||
info.func = SetClassAndSpecFilters;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if level == 1 then
|
|
||||||
info.text = CLASS;
|
|
||||||
info.func = nil;
|
|
||||||
info.notCheckable = true;
|
|
||||||
info.hasArrow = true;
|
|
||||||
info.value = CLASS_DROPDOWN;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
|
|
||||||
local classDisplayName, classTag, classID;
|
|
||||||
if filterClassID ~= NO_CLASS_FILTER then
|
|
||||||
classID = filterClassID;
|
|
||||||
classDisplayName = GetClassInfo(classID)
|
|
||||||
else
|
|
||||||
classDisplayName, classTag, classID = UnitClass("player");
|
|
||||||
end
|
|
||||||
|
|
||||||
info.text = classDisplayName;
|
|
||||||
info.notCheckable = true;
|
|
||||||
info.arg1 = nil;
|
|
||||||
info.arg2 = nil;
|
|
||||||
info.func = nil;
|
|
||||||
info.hasArrow = false;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
|
|
||||||
info.notCheckable = nil;
|
|
||||||
for i = 1, GetNumSpecializationsForClassID(classID) do
|
|
||||||
local _, specName = GetSpecializationInfoForClassID(classID, i)
|
|
||||||
info.leftPadding = 10;
|
|
||||||
info.text = specName;
|
|
||||||
info.checked = filterSpecID == i;
|
|
||||||
info.arg1 = classID;
|
|
||||||
info.arg2 = i;
|
|
||||||
info.func = SetClassAndSpecFilters;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
end
|
|
||||||
|
|
||||||
info.text = ALL_SPECS;
|
|
||||||
info.leftPadding = 10;
|
|
||||||
info.checked = (classID == filterClassID and filterSpecID == NO_SPEC_FILTER) or (filterClassID == NO_CLASS_FILTER and filterSpecID == NO_SPEC_FILTER);
|
|
||||||
info.arg1 = classID;
|
|
||||||
info.arg2 = NO_SPEC_FILTER;
|
|
||||||
info.func = SetClassAndSpecFilters;
|
|
||||||
UIDropDownMenu_AddButton(info, level);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:OnLoad()
|
|
||||||
self.scrollBar.trackBG:Hide();
|
|
||||||
self.update = LootJournalItemSetsMixin.UpdateList;
|
|
||||||
HybridScrollFrame_CreateButtons(self, "LootJournalItemSetButtonTemplate", LJ_ITEMSET_X_OFFSET, -LJ_ITEMSET_Y_OFFSET, "TOPLEFT", nil, nil, -LJ_ITEMSET_BUTTON_SPACING);
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:OnShow()
|
|
||||||
if not self.init then
|
|
||||||
self.init = true;
|
|
||||||
self:Refresh();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function onItemLoaded(itemID)
|
|
||||||
local self = EncounterJournal.LootJournalItems.ItemSetsFrame
|
|
||||||
for i = 1, #self.buttons do
|
|
||||||
local itemButtons = self.buttons[i].ItemButtons;
|
|
||||||
for j = 1, #itemButtons do
|
|
||||||
if ( itemButtons[j].itemID == itemID ) then
|
|
||||||
self:ConfigureItemButton(itemButtons[j]);
|
|
||||||
return;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:ConfigureItemButton(button)
|
|
||||||
local _, itemLink, itemQuality, _, _, _, _, _, _, icon = C_Item.GetItemInfo(button.itemID, false, onItemLoaded, true);
|
|
||||||
button.itemLink = itemLink;
|
|
||||||
itemQuality = itemQuality or Enum.ItemQuality.Epic; -- sets are most likely rare
|
|
||||||
if ( itemQuality == Enum.ItemQuality.Uncommon ) then
|
|
||||||
button.Border:SetAtlas("loottab-set-itemborder-green", true);
|
|
||||||
elseif ( itemQuality == Enum.ItemQuality.Rare ) then
|
|
||||||
button.Border:SetAtlas("loottab-set-itemborder-blue", true);
|
|
||||||
elseif ( itemQuality == Enum.ItemQuality.Epic ) then
|
|
||||||
button.Border:SetAtlas("loottab-set-itemborder-purple", true);
|
|
||||||
end
|
|
||||||
|
|
||||||
button.Icon:SetTexture(icon or "Interface\\ICONS\\temp")
|
|
||||||
|
|
||||||
button:GetParent().SetName:SetTextColor(GetItemQualityColor(itemQuality));
|
|
||||||
self:CheckItemButtonTooltip(button);
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetsMixin:UpdateList()
|
|
||||||
if ( self.dirty ) then
|
|
||||||
C_LootJournal.GenerateLootData(self:GetClassAndSpecFilters())
|
|
||||||
self.dirty = nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
local buttons = self.buttons;
|
|
||||||
local offset = HybridScrollFrame_GetOffset(self);
|
|
||||||
|
|
||||||
local numSets = C_LootJournal.GetNumItemSets()
|
|
||||||
|
|
||||||
for i = 1, #buttons do
|
|
||||||
local button = buttons[i];
|
|
||||||
local index = offset + i;
|
|
||||||
if ( index <= numSets ) then
|
|
||||||
local setIndex, name, tierName, itemlevel, sourceText, isPVP, items, numItems = C_LootJournal.GetItemSetInfo(index)
|
|
||||||
|
|
||||||
button:Show();
|
|
||||||
button.SetName:SetText(name);
|
|
||||||
button.ItemLevel:SetFormattedText(EJ_SET_ITEM_LEVEL, tierName, itemlevel);
|
|
||||||
button.PVPIcon:SetShown(isPVP);
|
|
||||||
|
|
||||||
button.setIndex = setIndex
|
|
||||||
button.tooltipText = sourceText
|
|
||||||
button:CheckItemButtonTooltip()
|
|
||||||
|
|
||||||
for j = 1, numItems do
|
|
||||||
local itemButton = button.ItemButtons[j];
|
|
||||||
if ( not itemButton ) then
|
|
||||||
itemButton = CreateFrame("BUTTON", string.format("$parentItemButton%i", j), button, "LootJournalItemSetItemButtonTemplate");
|
|
||||||
button[string.format("$parentItemButton%i", j)] = itemButton
|
|
||||||
itemButton:SetPoint("LEFT", button.ItemButtons[j-1], "RIGHT", 5, 0);
|
|
||||||
end
|
|
||||||
itemButton.Icon:SetTexture(items[j].icon);
|
|
||||||
itemButton.itemID = items[j].itemID;
|
|
||||||
itemButton:Show();
|
|
||||||
self:ConfigureItemButton(itemButton);
|
|
||||||
end
|
|
||||||
for j = numItems + 1, #button.ItemButtons do
|
|
||||||
button.ItemButtons[j].itemID = nil;
|
|
||||||
button.ItemButtons[j]:Hide();
|
|
||||||
end
|
|
||||||
else
|
|
||||||
button:Hide();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local totalHeight = numSets * buttons[1]:GetHeight() + (numSets - 1) * LJ_ITEMSET_BUTTON_SPACING + LJ_ITEMSET_Y_OFFSET + LJ_ITEMSET_BOTTOM_BUFFER
|
|
||||||
HybridScrollFrame_Update(self, totalHeight, self:GetHeight());
|
|
||||||
end
|
|
||||||
|
|
||||||
LootJournalItemSetButtonMixin = {}
|
|
||||||
|
|
||||||
function LootJournalItemSetButtonMixin:OnEnter()
|
|
||||||
if self.tooltipText then
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_TOP")
|
|
||||||
GameTooltip:SetText(LOOTJOURNAL_SOURCE_TOOLTIP_HEAD, 1, 1, 1)
|
|
||||||
GameTooltip:AddLine(self.tooltipText, nil, nil, nil, 1)
|
|
||||||
self.tooltipSetIndex = self.setIndex
|
|
||||||
GameTooltip:Show()
|
|
||||||
else
|
|
||||||
GameTooltip:Hide()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function LootJournalItemSetButtonMixin:CheckItemButtonTooltip()
|
|
||||||
if ( GameTooltip:GetOwner() == self and self.tooltipSetIndex ~= self.setIndex ) then
|
|
||||||
self:OnEnter();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
|
||||||
..\..\FrameXML\UI.xsd">
|
|
||||||
<Script file="Custom_LootJournalItems.lua"/>
|
|
||||||
|
|
||||||
<Button name="LootJournalItemButtonTemplate" virtual="true">
|
|
||||||
<Scripts>
|
|
||||||
<OnEnter function="LootJournalItemButtonTemplate_OnEnter"/>
|
|
||||||
<OnLeave function="LootJournalItemButtonTemplate_OnLeave"/>
|
|
||||||
<OnClick function="LootJournalItemButton_OnClick"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button name="LootJournalItemSetItemButtonTemplate" inherits="LootJournalItemButtonTemplate" virtual="true" hidden="true">
|
|
||||||
<Size x="32" y="32"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture name="$parentIcon" parentKey="Icon">
|
|
||||||
<Size x="28" y="28"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="BORDER">
|
|
||||||
<Texture parentKey="Border">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="RIGHT" relativeTo="$parentIcon" relativePoint="CENTER" x="20" y="1"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
self:SetParentArray("ItemButtons")
|
|
||||||
self.Border:SetAtlas("loottab-set-itemborder-green", true)
|
|
||||||
</OnLoad>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button name="LootJournalItemSetButtonTemplate" virtual="true">
|
|
||||||
<Size x="744" y="41"/>
|
|
||||||
<HitRectInsets left="34" right="390" top="1" bottom="1"/>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture parentKey="Background">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" x="0" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<FontString name="$parentSetName" parentKey="SetName" inherits="GameFontNormalMed2" maxLines="1" justifyH="LEFT">
|
|
||||||
<Size x="320" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" x="38" y="-6"/>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString name="$parentItemLevel" parentKey="ItemLevel" inherits="GameFontNormal" text="Item Level" maxLines="1" justifyH="LEFT">
|
|
||||||
<Size x="320" y="0"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentSetName" relativePoint="BOTTOMLEFT" x="0" y="-2"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.718" g="0.561" b="0.416"/>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<Frame name="$parentPVPIcon" parentKey="PVPIcon" hidden="true">
|
|
||||||
<Size x="30" y="30"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" relativePoint="LEFT" x="0" y="2"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture name="$parentIcon" parentKey="Icon" file="Interface\PVPFrame\Icons\prestige-icon-4"/>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnEnter>
|
|
||||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
||||||
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP_HEAD, 1, 1, 1)
|
|
||||||
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP, nil, nil, nil, 1)
|
|
||||||
GameTooltip:Show()
|
|
||||||
</OnEnter>
|
|
||||||
<OnLeave function="GameTooltip_Hide"/>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
<Button name="$parentItemButton1" parentKey="ItemButton1" inherits="LootJournalItemSetItemButtonTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="LEFT" x="397" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, LootJournalItemSetButtonMixin)
|
|
||||||
self.Background:SetAtlas("loottab-set-background", true)
|
|
||||||
</OnLoad>
|
|
||||||
<OnEnter>
|
|
||||||
self:OnEnter()
|
|
||||||
</OnEnter>
|
|
||||||
<OnLeave function="GameTooltip_Hide"/>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Frame parent="EncounterJournal" name="$parentLootJournalItems" parentKey="LootJournalItems" hidden="true">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentInset" x="4" y="-51" />
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" x="-4" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture parentKey="Background">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP" x="0" y="4"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<Frame name="LootJournalItemsClassDropDown" parentKey="ClassDropDown" inherits="UIDropDownMenuTemplate" hidden="true">
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad function="LootJournalItemsClassDropDown_OnLoad"/>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
<ScrollFrame name="$parentItemSetsFrame" parentKey="ItemSetsFrame" inherits="MinimalHybridScrollFrameTemplate" hidden="true">
|
|
||||||
<Size x="762" y="332"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" x="0" y="3"/>
|
|
||||||
</Anchors>
|
|
||||||
<Frames>
|
|
||||||
<Button parentKey="ClassButton" inherits="EJButtonTemplate" text="FILTER">
|
|
||||||
<Size x="114" y="26"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" x="-12" y="35"/>
|
|
||||||
</Anchors>
|
|
||||||
<Scripts>
|
|
||||||
<OnMouseDown>
|
|
||||||
EJButtonMixin.OnMouseDown(self, button);
|
|
||||||
ToggleDropDownMenu(1, nil, self:GetParent():GetParent().ClassDropDown, self, 5, 0);
|
|
||||||
PlaySound("igMainMenuOptionCheckBoxOn");
|
|
||||||
</OnMouseDown>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, LootJournalItemSetsMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
<OnShow>
|
|
||||||
self:OnShow()
|
|
||||||
</OnShow>
|
|
||||||
</Scripts>
|
|
||||||
</ScrollFrame>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, LootJournalItemsMixin)
|
|
||||||
self:OnLoad()
|
|
||||||
</OnLoad>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
</Ui>
|
|
||||||
@@ -68,6 +68,22 @@ local function updateScrollBar(scrollFrame, contentHeight, contentWidth)
|
|||||||
end
|
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 function hideScrollBar(scrollFrame)
|
||||||
local scrollBar = getScrollBar(scrollFrame)
|
local scrollBar = getScrollBar(scrollFrame)
|
||||||
if scrollBar then scrollBar:Hide() end
|
if scrollBar then scrollBar:Hide() end
|
||||||
@@ -78,6 +94,24 @@ local function getBodyChild()
|
|||||||
return body and body.ScrollChild
|
return body and body.ScrollChild
|
||||||
end
|
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 stageBadges = {}
|
||||||
@@ -248,7 +282,12 @@ local function renderRecommendations()
|
|||||||
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
|
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
|
||||||
end
|
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)
|
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
|
||||||
|
end
|
||||||
card.Name:SetText(r.name or r.title or "")
|
card.Name:SetText(r.name or r.title or "")
|
||||||
card.Loc:SetText(r.loc or r.description or "")
|
card.Loc:SetText(r.loc or r.description or "")
|
||||||
card.Level:SetText(r.level or "")
|
card.Level:SetText(r.level or "")
|
||||||
@@ -368,6 +407,8 @@ end
|
|||||||
|
|
||||||
function MW_PlayerGuide_OnLoad(self)
|
function MW_PlayerGuide_OnLoad(self)
|
||||||
EncounterJournal.playerGuideFrame = self
|
EncounterJournal.playerGuideFrame = self
|
||||||
|
self:EnableMouse(true)
|
||||||
|
self:EnableMouseWheel(true)
|
||||||
|
|
||||||
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
|
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
|
||||||
if bodyChild then
|
if bodyChild then
|
||||||
@@ -386,6 +427,12 @@ function MW_PlayerGuide_OnLoad(self)
|
|||||||
C_PlayerGuide.RegisterListener(function() refresh() end)
|
C_PlayerGuide.RegisterListener(function() refresh() end)
|
||||||
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()
|
function MW_PlayerGuide_OnShow()
|
||||||
if not C_PlayerGuide.HasData() then
|
if not C_PlayerGuide.HasData() then
|
||||||
C_PlayerGuide.SetLoading()
|
C_PlayerGuide.SetLoading()
|
||||||
@@ -399,16 +446,94 @@ function MW_PlayerGuide_RefreshButton_OnClick()
|
|||||||
C_PlayerGuide.RequestRefresh()
|
C_PlayerGuide.RequestRefresh()
|
||||||
end
|
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)
|
function MW_PlayerGuide_Objective_OnClick(self)
|
||||||
local o = self.objectiveData
|
local o = self.objectiveData
|
||||||
if not o then return end
|
if not o then return end
|
||||||
|
|
||||||
if o.type == "dungeon" or o.type == "raid" then
|
if o.type == "dungeon" or o.type == "raid" then
|
||||||
if o.instanceID and o.instanceID > 0 then
|
openEncounterJournalInstance(o)
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
|
||||||
EJ_ContentTab_SelectAppropriateInstanceTab(o.instanceID)
|
|
||||||
EncounterJournal_DisplayInstance(o.instanceID)
|
|
||||||
end
|
|
||||||
elseif o.type == "boss" then
|
elseif o.type == "boss" then
|
||||||
if o.encounterID and o.encounterID > 0 then
|
if o.encounterID and o.encounterID > 0 then
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
||||||
@@ -457,11 +582,7 @@ function MW_PlayerGuide_Objective_OnLeave() GameTooltip:Hide() end
|
|||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnClick(self)
|
function MW_PlayerGuide_Rec_OnClick(self)
|
||||||
local r = self.recData
|
local r = self.recData
|
||||||
if r and r.instanceID and r.instanceID > 0 then
|
if r then openEncounterJournalInstance(r) end
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
|
||||||
EJ_ContentTab_SelectAppropriateInstanceTab(r.instanceID)
|
|
||||||
EncounterJournal_DisplayInstance(r.instanceID)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnEnter(self)
|
function MW_PlayerGuide_Rec_OnEnter(self)
|
||||||
|
|||||||
@@ -133,16 +133,19 @@
|
|||||||
<Layers>
|
<Layers>
|
||||||
<Layer level="ARTWORK">
|
<Layer level="ARTWORK">
|
||||||
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
|
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
|
||||||
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
|
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8" setAllPoints="true">
|
||||||
<Size><AbsDimension x="168" y="72"/></Size>
|
<Color r="1" g="1" b="1"/>
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.55" g="0.16" b="0.07"/>
|
|
||||||
</Texture>
|
</Texture>
|
||||||
</Layer>
|
</Layer>
|
||||||
<Layer level="OVERLAY">
|
<Layer level="OVERLAY">
|
||||||
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
|
<!-- 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">
|
<FontString name="$parentLevel" parentKey="Level" inherits="GameFontNormalSmall" justifyH="RIGHT">
|
||||||
<Size><AbsDimension x="80" y="12"/></Size>
|
<Size><AbsDimension x="80" y="12"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
|
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
|
||||||
<Size><AbsDimension x="162" y="16"/></Size>
|
<Size><AbsDimension x="162" y="16"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-80"/></Offset></Anchor>
|
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-82"/></Offset></Anchor>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
<Color r="1.0" g="0.91" b="0.63"/>
|
||||||
</FontString>
|
</FontString>
|
||||||
@@ -162,7 +165,7 @@
|
|||||||
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
|
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
|
||||||
<Size><AbsDimension x="162" y="16"/></Size>
|
<Size><AbsDimension x="162" y="16"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-96"/></Offset></Anchor>
|
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-98"/></Offset></Anchor>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
</FontString>
|
</FontString>
|
||||||
</Layer>
|
</Layer>
|
||||||
@@ -480,6 +483,7 @@
|
|||||||
<Scripts>
|
<Scripts>
|
||||||
<OnLoad function="MW_PlayerGuide_OnLoad"/>
|
<OnLoad function="MW_PlayerGuide_OnLoad"/>
|
||||||
<OnShow function="MW_PlayerGuide_OnShow"/>
|
<OnShow function="MW_PlayerGuide_OnShow"/>
|
||||||
|
<OnMouseWheel function="MW_PlayerGuide_OnMouseWheel"/>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
|
|||||||
@@ -365,21 +365,6 @@ LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты"
|
|||||||
-- compact form on 3.3.5.
|
-- compact form on 3.3.5.
|
||||||
BOSS_INFO_STRING = BOSS_INFO_STRING or "Босс: %s"
|
BOSS_INFO_STRING = BOSS_INFO_STRING or "Босс: %s"
|
||||||
|
|
||||||
-- Sirus ships a PKBT model mixin in SharedXML/SharedUIPanelPKBTTemplates;
|
|
||||||
-- we don't carry it. The EJ DressUpModel OnShow does `self:OnShow()` and
|
|
||||||
-- OnEvent does `self:OnEvent(...)`, expecting the mixin to provide them.
|
|
||||||
PKBT_ModelMixin = PKBT_ModelMixin or {}
|
|
||||||
PKBT_ModelMixin.OnShow = PKBT_ModelMixin.OnShow or function() end
|
|
||||||
PKBT_ModelMixin.OnEvent = PKBT_ModelMixin.OnEvent or function() end
|
|
||||||
PKBT_ModelMixin.OnLoad = PKBT_ModelMixin.OnLoad or function() end
|
|
||||||
|
|
||||||
if not SharedXML_Model_OnLoad then
|
|
||||||
function SharedXML_Model_OnLoad() end
|
|
||||||
end
|
|
||||||
if not SharedXML_Model_OnEvent then
|
|
||||||
function SharedXML_Model_OnEvent() end
|
|
||||||
end
|
|
||||||
|
|
||||||
ITEM_CLASS_2 = ITEM_CLASS_2 or 2
|
ITEM_CLASS_2 = ITEM_CLASS_2 or 2
|
||||||
ITEM_CLASS_4 = ITEM_CLASS_4 or 4
|
ITEM_CLASS_4 = ITEM_CLASS_4 or 4
|
||||||
|
|
||||||
|
|||||||
@@ -119,13 +119,32 @@ end
|
|||||||
-- 3.3.5 has no SetPortraitTextureFromCreatureDisplayID equivalent, so leave
|
-- 3.3.5 has no SetPortraitTextureFromCreatureDisplayID equivalent, so leave
|
||||||
-- the texture blank — the static UI-EJ-BOSS-* icon set elsewhere is enough.
|
-- the texture blank — the static UI-EJ-BOSS-* icon set elsewhere is enough.
|
||||||
do
|
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 frame = CreateFrame("Frame")
|
||||||
local texture = frame:CreateTexture()
|
local texture = frame:CreateTexture()
|
||||||
local meta = getmetatable(texture)
|
local meta = getmetatable(texture)
|
||||||
local methods = meta and meta.__index
|
local methods = meta and meta.__index
|
||||||
if type(methods) == "table" and not methods.SetPortrait then
|
if type(methods) == "table" and not methods.SetPortrait then
|
||||||
methods.SetPortrait = function(self)
|
methods.SetPortrait = function(self, displayInfo)
|
||||||
self:SetTexture(nil)
|
self:SetTexture(portraitByDisplayInfo[displayInfo] or QUESTION_MARK_ICON)
|
||||||
|
self:SetTexCoord(0, 1, 0, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ MoonWellEncounterJournalData = {
|
|||||||
[281] = { 'Нексус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-thenexus.blp', 'interface\\lfgframe\\lfgicon-thenexus.blp', 'interface\\encounterjournal\\ui-ej-background-thenexus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-thenexus.blp', 576, 0, 11, 1, 281, 520 },
|
[281] = { 'Нексус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-thenexus.blp', 'interface\\lfgframe\\lfgicon-thenexus.blp', 'interface\\encounterjournal\\ui-ej-background-thenexus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-thenexus.blp', 576, 0, 11, 1, 281, 520 },
|
||||||
[282] = { 'Окулус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-theoculus.blp', 'interface\\lfgframe\\lfgicon-theoculus.blp', 'interface\\encounterjournal\\ui-ej-background-theoculus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-theoculus.blp', 578, 0, 12, 0, 282, 528 },
|
[282] = { 'Окулус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-theoculus.blp', 'interface\\lfgframe\\lfgicon-theoculus.blp', 'interface\\encounterjournal\\ui-ej-background-theoculus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-theoculus.blp', 578, 0, 12, 0, 282, 528 },
|
||||||
[761] = { 'Рубиновое святилище', 'После обнаружения кладки яиц сумеречных драконов в Обсидиановом святилище, Кориалстраз попросил Совет Шести отправить искателей приключений уничтожить их. Это было сделано быстро и без лишнего шума. И хотя все со временем успокоилось, по необъяснимым причинам связь со стражами Рубинового святилища была утеряна. Привратники, стоявшие у портала, оказались убитыми, а магические камни – разрушены. Нынешнее положение Рубинового святилища неизвестно, и ещё меньше известно о напавших на него, так как число чёрных драконов в Нордсколе слишком мало для подобного нападения. Что же за сила посмела напасть на оплот красных драконов, и как они смогли проскользнуть мимо стражников незамеченными?', 'interface\\encounterjournal\\ui-ej-dungeonbutton-rubysanctum.blp', 'interface\\lfgframe\\lfgicon-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-background-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-lorebg-rubysanctum.blp', 724, 0, 12, 16, 761, 609 },
|
[761] = { 'Рубиновое святилище', 'После обнаружения кладки яиц сумеречных драконов в Обсидиановом святилище, Кориалстраз попросил Совет Шести отправить искателей приключений уничтожить их. Это было сделано быстро и без лишнего шума. И хотя все со временем успокоилось, по необъяснимым причинам связь со стражами Рубинового святилища была утеряна. Привратники, стоявшие у портала, оказались убитыми, а магические камни – разрушены. Нынешнее положение Рубинового святилища неизвестно, и ещё меньше известно о напавших на него, так как число чёрных драконов в Нордсколе слишком мало для подобного нападения. Что же за сила посмела напасть на оплот красных драконов, и как они смогли проскользнуть мимо стражников незамеченными?', 'interface\\encounterjournal\\ui-ej-dungeonbutton-rubysanctum.blp', 'interface\\lfgframe\\lfgicon-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-background-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-lorebg-rubysanctum.blp', 724, 0, 12, 16, 761, 609 },
|
||||||
[772] = { 'Старые предгорья Хилсбрада', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blpp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 560, 0, 12, 1024, 772, 845 },
|
[772] = { 'Старые предгорья Хилсбрада', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 560, 0, 12, 1024, 772, 845 },
|
||||||
[253] = { 'Темный лабиринт', 'Дренорским изгнанникам смерть казалась неприятным и неблаговидным последствием жизни. Поэтому дренеи скрывали своих мертвых в подземном некрополе Аукиндоне. Этот чудо-лабиринт раскинулся под самым лесом Тероккар.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-auchindoun.blp', 'interface\\lfgframe\\lfgicon-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-background-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-lorebg-auchindoun.blp', 555, 0, 13, 0, 253, 834 },
|
[253] = { 'Темный лабиринт', 'Дренорским изгнанникам смерть казалась неприятным и неблаговидным последствием жизни. Поэтому дренеи скрывали своих мертвых в подземном некрополе Аукиндоне. Этот чудо-лабиринт раскинулся под самым лесом Тероккар.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-auchindoun.blp', 'interface\\lfgframe\\lfgicon-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-background-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-lorebg-auchindoun.blp', 555, 0, 13, 0, 253, 834 },
|
||||||
[769] = { 'Очищение Стратхольма', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-TheCullingOfStratholme.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-TheCullingOfStratholme.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-TheCullingOfStratholme.blp', 595, 0, 13, 1024, 769, 521 },
|
[769] = { 'Очищение Стратхольма', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-TheCullingOfStratholme.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-TheCullingOfStratholme.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-TheCullingOfStratholme.blp', 595, 0, 13, 1024, 769, 521 },
|
||||||
[277] = { 'Чертоги Камня', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsofstone.blp', 'interface\\lfgframe\\lfgicon-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-background-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsofstone.blp', 599, 0, 14, 0, 277, 526 },
|
[277] = { 'Чертоги Камня', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsofstone.blp', 'interface\\lfgframe\\lfgicon-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-background-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsofstone.blp', 599, 0, 14, 0, 277, 526 },
|
||||||
@@ -72,7 +72,7 @@ MoonWellEncounterJournalData = {
|
|||||||
[275] = { 'Чертоги Молний', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsoflightning.blp', 'interface\\lfgframe\\lfgicon-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-background-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsoflightning.blp', 602, 0, 15, 1, 275, 525 },
|
[275] = { 'Чертоги Молний', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsoflightning.blp', 'interface\\lfgframe\\lfgicon-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-background-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsoflightning.blp', 602, 0, 15, 1, 275, 525 },
|
||||||
[229] = { 'Нижняя часть пика Черной горы', 'Потрясающая крепость, вырезанная в огненном сердце Черной горы, веками служила образцом мощи клана Черного Железа. Но не так давно черный дракон Нефариан и его род захватили верхние ярусы крепости и развязали жестокую войну с дворфами. Армии драконов нашли союзника в лице вождя Ренда Чернорука и его \'Орды\', и теперь их объединенные силы хозяйничают на Пике Черной горы, проводя эксперименты, призванные расширить их ряды, и строя планы по уничтожению дворфов Черного Железа.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-blackrockspire.blp', 'interface\\lfgframe\\lfgicon-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-background-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-lorebg-blackrockspire.blp', 229, 0, 16, 0, 229, 882 },
|
[229] = { 'Нижняя часть пика Черной горы', 'Потрясающая крепость, вырезанная в огненном сердце Черной горы, веками служила образцом мощи клана Черного Железа. Но не так давно черный дракон Нефариан и его род захватили верхние ярусы крепости и развязали жестокую войну с дворфами. Армии драконов нашли союзника в лице вождя Ренда Чернорука и его \'Орды\', и теперь их объединенные силы хозяйничают на Пике Черной горы, проводя эксперименты, призванные расширить их ряды, и строя планы по уничтожению дворфов Черного Железа.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-blackrockspire.blp', 'interface\\lfgframe\\lfgicon-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-background-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-lorebg-blackrockspire.blp', 229, 0, 16, 0, 229, 882 },
|
||||||
[278] = { 'Яма Сарона', 'Противостояние защитников Азерота и армии Короля-лича продолжалось долгие годы. За это время пало множество героев, а после смерти они были вынуждены присоединиться к чудовищной армии нежити. В ходе этого безустанного противостояния Тирион Фордринг из Серебряного Авангарда объединил силы с рыцарями Черного Клинка под командованием Дариона Могрейна. Теперь эта объединенная армия, получившая название Пепельный союз, собирается взять штурмом цитадель Ледяной Короны.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-pitofsaron.blp', 'interface\\lfgframe\\lfgicon-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-background-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-lorebg-pitofsaron.blp', 658, 0, 16, 1, 278, 602 },
|
[278] = { 'Яма Сарона', 'Противостояние защитников Азерота и армии Короля-лича продолжалось долгие годы. За это время пало множество героев, а после смерти они были вынуждены присоединиться к чудовищной армии нежити. В ходе этого безустанного противостояния Тирион Фордринг из Серебряного Авангарда объединил силы с рыцарями Черного Клинка под командованием Дариона Могрейна. Теперь эта объединенная армия, получившая название Пепельный союз, собирается взять штурмом цитадель Ледяной Короны.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-pitofsaron.blp', 'interface\\lfgframe\\lfgicon-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-background-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-lorebg-pitofsaron.blp', 658, 0, 16, 1, 278, 602 },
|
||||||
[768] = { 'Черные топи ', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blpp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 269, 0, 16, 1024, 768, 844 },
|
[768] = { 'Черные топи ', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 269, 0, 16, 1024, 768, 844 },
|
||||||
[762] = { 'Огненная Пропасть', 'Огненная Пропасть залегает глубоко под Оргриммаром. Ee вулканическими пещерами некогда владели дикие трогги, пока туда не пришли жуткие сектанты из клана Пламенеющего Клинка. Сектанты вызвали в этот мир жуткое создание, имя которому Тарагаман Ненасытный. С помощью его мощи, ходят слухи, что они хотят выбраться из подземелья и захватить Оргриммар. В помощь себе они также призвали к себе в ряды сатира Баззалана, который будет руководить армией сектантов.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-RagefireChasm.blp', 'interface\\lfgframe\\lfgicon-RAGEFIRECHASM.blp', 'interface\\encounterjournal\\ui-ej-background-ragefirechasm.blp', 'interface\\encounterjournal\\ui-ej-lorebg-ragefirechasm.blp', 389, 0, 17, 0, 762, 680 },
|
[762] = { 'Огненная Пропасть', 'Огненная Пропасть залегает глубоко под Оргриммаром. Ee вулканическими пещерами некогда владели дикие трогги, пока туда не пришли жуткие сектанты из клана Пламенеющего Клинка. Сектанты вызвали в этот мир жуткое создание, имя которому Тарагаман Ненасытный. С помощью его мощи, ходят слухи, что они хотят выбраться из подземелья и захватить Оргриммар. В помощь себе они также призвали к себе в ряды сатира Баззалана, который будет руководить армией сектантов.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-RagefireChasm.blp', 'interface\\lfgframe\\lfgicon-RAGEFIRECHASM.blp', 'interface\\encounterjournal\\ui-ej-background-ragefirechasm.blp', 'interface\\encounterjournal\\ui-ej-lorebg-ragefirechasm.blp', 389, 0, 17, 0, 762, 680 },
|
||||||
[240] = { 'Пещеры Стенаний', 'Много лет назад прославленный друид ночных эльфов Наралекс спустился в темные Пещеры Стенаний со своими спутниками. Своим названием пещеры обязаны похожим на плач звукам, сопровождающим вырывающийся на свободу пар. Он намеревался использовать подземные источники, чтобы вернуть Степям их былое плодородие, но стоило ему окунуться в Изумрудный Сон, как его прекрасное видение обернулось жутким кошмаром. С тех самых пор над этим местом нависло страшное проклятие.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-wailingcaverns.blp', 'interface\\lfgframe\\lfgicon-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-background-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-lorebg-wailingcaverns.blp', 43, 0, 18, 0, 240, 749 },
|
[240] = { 'Пещеры Стенаний', 'Много лет назад прославленный друид ночных эльфов Наралекс спустился в темные Пещеры Стенаний со своими спутниками. Своим названием пещеры обязаны похожим на плач звукам, сопровождающим вырывающийся на свободу пар. Он намеревался использовать подземные источники, чтобы вернуть Степям их былое плодородие, но стоило ему окунуться в Изумрудный Сон, как его прекрасное видение обернулось жутким кошмаром. С тех самых пор над этим местом нависло страшное проклятие.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-wailingcaverns.blp', 'interface\\lfgframe\\lfgicon-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-background-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-lorebg-wailingcaverns.blp', 43, 0, 18, 0, 240, 749 },
|
||||||
[236] = { 'Стратхольм', 'Стратхольм был настоящей жемчужиной северного Лордерона, но сейчас все помнят лишь его сокрушительное падение. Именно здесь принц Артас когда-то ослушался своего наставника, благородного паладина Утера Светоносного, и вырезал сотни горожан, якобы зараженных чумой нежити. С тех пор в городе царят смерть, предательство и отчаяние.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-stratholme.blp', 'interface\\lfgframe\\lfgicon-stratholme.blp', 'interface\\encounterjournal\\ui-ej-background-stratholme.blp', 'interface\\encounterjournal\\ui-ej-lorebg-stratholme.blp', 329, 0, 19, 0, 236, 892 },
|
[236] = { 'Стратхольм', 'Стратхольм был настоящей жемчужиной северного Лордерона, но сейчас все помнят лишь его сокрушительное падение. Именно здесь принц Артас когда-то ослушался своего наставника, благородного паладина Утера Светоносного, и вырезал сотни горожан, якобы зараженных чумой нежити. С тех пор в городе царят смерть, предательство и отчаяние.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-stratholme.blp', 'interface\\lfgframe\\lfgicon-stratholme.blp', 'interface\\encounterjournal\\ui-ej-background-stratholme.blp', 'interface\\encounterjournal\\ui-ej-lorebg-stratholme.blp', 329, 0, 19, 0, 236, 892 },
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
local securecall = securecall
|
|
||||||
local ipairs = ipairs
|
|
||||||
local next = next
|
|
||||||
local type = type
|
|
||||||
local strsplit = string.split
|
|
||||||
|
|
||||||
local ExecuteFrameScript = ExecuteFrameScript
|
|
||||||
local GetFramesRegisteredForEvent = GetFramesRegisteredForEvent
|
|
||||||
local UnitName = UnitName
|
|
||||||
local UnitTokenFromGUID = UnitTokenFromGUID
|
|
||||||
|
|
||||||
local REGISTERED_CUSTOM_EVENTS = {}
|
|
||||||
local REGISTERED_CUSTOM_EVENTS_ALL
|
|
||||||
|
|
||||||
local function SecureNext(elements, key)
|
|
||||||
return securecall(next, elements, key)
|
|
||||||
end
|
|
||||||
|
|
||||||
function FireClientEvent(event, ...)
|
|
||||||
for _, frame in SecureNext, {GetFramesRegisteredForEvent(event)} do
|
|
||||||
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function FireCustomClientEvent(event, ...)
|
|
||||||
if REGISTERED_CUSTOM_EVENTS[event] then
|
|
||||||
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
|
|
||||||
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if REGISTERED_CUSTOM_EVENTS_ALL then
|
|
||||||
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS_ALL do
|
|
||||||
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local eventRegistrar = CreateFrame("Frame")
|
|
||||||
eventRegistrar:SetScript("OnAttributeChanged", function(this, name, value)
|
|
||||||
if name == "1" then
|
|
||||||
if not REGISTERED_CUSTOM_EVENTS[value] then
|
|
||||||
REGISTERED_CUSTOM_EVENTS[value] = {}
|
|
||||||
end
|
|
||||||
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = true
|
|
||||||
elseif name == "0" then
|
|
||||||
if REGISTERED_CUSTOM_EVENTS[value] then
|
|
||||||
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = nil
|
|
||||||
end
|
|
||||||
elseif name == "2" then
|
|
||||||
REGISTERED_CUSTOM_EVENTS_ALL[value] = true
|
|
||||||
elseif name == "3" then
|
|
||||||
REGISTERED_CUSTOM_EVENTS_ALL[value] = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
function RegisterCustomEvent(self, event)
|
|
||||||
eventRegistrar:SetAttribute("f", self)
|
|
||||||
eventRegistrar:SetAttribute("1", event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function UnregisterCustomEvent(self, event)
|
|
||||||
eventRegistrar:SetAttribute("f", self)
|
|
||||||
eventRegistrar:SetAttribute("0", event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function RegisterAllCustomEvents(self)
|
|
||||||
if not REGISTERED_CUSTOM_EVENTS_ALL then
|
|
||||||
REGISTERED_CUSTOM_EVENTS_ALL = {}
|
|
||||||
end
|
|
||||||
eventRegistrar:SetAttribute("2", self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function UnregisterAllCustomEvents(self)
|
|
||||||
if not REGISTERED_CUSTOM_EVENTS_ALL then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
eventRegistrar:SetAttribute("3", self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function FireCustomClientUnitGroupEvent(event, unitGUID, ...)
|
|
||||||
if event and REGISTERED_CUSTOM_EVENTS[event] and unitGUID then
|
|
||||||
local units = {UnitTokenFromGUID(unitGUID)}
|
|
||||||
if #units > 0 then
|
|
||||||
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
|
|
||||||
for _, unit in ipairs(units) do
|
|
||||||
securecall(ExecuteFrameScript, frame, "OnEvent", event, unit, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
EventHandler = setmetatable(
|
|
||||||
{
|
|
||||||
events = {}, -- Original events
|
|
||||||
listeners = {}, -- New tables for handling events outside og EventHandler
|
|
||||||
RegisterListener = function(self, listener)
|
|
||||||
self.listeners[listener] = true
|
|
||||||
end,
|
|
||||||
Handle = function(self, opcode, message, unk, sender)
|
|
||||||
if sender == UnitName("player") then
|
|
||||||
for listener in SecureNext, self.listeners do
|
|
||||||
if listener[opcode] then
|
|
||||||
securecall(listener[opcode], listener, message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.events[opcode] then
|
|
||||||
securecall(self.events[opcode], self, message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__newindex = function(self, key, value)
|
|
||||||
if type(value) == "function" then
|
|
||||||
self.events[key] = value
|
|
||||||
end
|
|
||||||
rawset(self, key, value)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
local EventHandlerFrame = CreateFrame("Frame")
|
|
||||||
EventHandlerFrame:RegisterEvent("CHAT_MSG_ADDON")
|
|
||||||
EventHandlerFrame:SetScript("OnEvent", function(self, event, opcode, message, unk, sender)
|
|
||||||
EventHandler:Handle(opcode, message, unk, sender)
|
|
||||||
end)
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
local pairs = pairs
|
|
||||||
local ipairs = ipairs
|
|
||||||
local next = next
|
|
||||||
local CreateFrame = CreateFrame
|
|
||||||
|
|
||||||
ObjectPoolMixin = {};
|
|
||||||
|
|
||||||
function ObjectPoolMixin:OnLoad(creationFunc, resetterFunc)
|
|
||||||
self.creationFunc = creationFunc;
|
|
||||||
self.resetterFunc = resetterFunc;
|
|
||||||
|
|
||||||
self.activeObjects = {};
|
|
||||||
self.inactiveObjects = {};
|
|
||||||
|
|
||||||
self.numActiveObjects = 0;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:Acquire()
|
|
||||||
local numInactiveObjects = #self.inactiveObjects;
|
|
||||||
if numInactiveObjects > 0 then
|
|
||||||
local obj = self.inactiveObjects[numInactiveObjects];
|
|
||||||
self.activeObjects[obj] = true;
|
|
||||||
self.numActiveObjects = self.numActiveObjects + 1;
|
|
||||||
self.inactiveObjects[numInactiveObjects] = nil;
|
|
||||||
return obj, false;
|
|
||||||
end
|
|
||||||
|
|
||||||
local newObj = self.creationFunc(self);
|
|
||||||
if self.resetterFunc and not self.disallowResetIfNew then
|
|
||||||
self.resetterFunc(self, newObj);
|
|
||||||
end
|
|
||||||
self.activeObjects[newObj] = true;
|
|
||||||
self.numActiveObjects = self.numActiveObjects + 1;
|
|
||||||
return newObj, true;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:Release(obj)
|
|
||||||
if self:IsActive(obj) then
|
|
||||||
self.inactiveObjects[#self.inactiveObjects + 1] = obj;
|
|
||||||
self.activeObjects[obj] = nil;
|
|
||||||
self.numActiveObjects = self.numActiveObjects - 1;
|
|
||||||
if self.resetterFunc then
|
|
||||||
self.resetterFunc(self, obj);
|
|
||||||
end
|
|
||||||
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:ReleaseAll()
|
|
||||||
for obj in pairs(self.activeObjects) do
|
|
||||||
self:Release(obj);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:SetResetDisallowedIfNew(disallowed)
|
|
||||||
self.disallowResetIfNew = disallowed;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:EnumerateActive()
|
|
||||||
return pairs(self.activeObjects);
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:GetNextActive(current)
|
|
||||||
return (next(self.activeObjects, current));
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:GetNextInactive(current)
|
|
||||||
return (next(self.inactiveObjects, current));
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:IsActive(object)
|
|
||||||
return (self.activeObjects[object] ~= nil);
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:GetNumActive()
|
|
||||||
return self.numActiveObjects;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ObjectPoolMixin:EnumerateInactive()
|
|
||||||
return ipairs(self.inactiveObjects);
|
|
||||||
end
|
|
||||||
|
|
||||||
function CreateObjectPool(creationFunc, resetterFunc)
|
|
||||||
local objectPool = CreateFromMixins(ObjectPoolMixin);
|
|
||||||
objectPool:OnLoad(creationFunc, resetterFunc);
|
|
||||||
return objectPool;
|
|
||||||
end
|
|
||||||
|
|
||||||
FramePoolMixin = CreateFromMixins(ObjectPoolMixin);
|
|
||||||
|
|
||||||
local pollFrameIndex = 0
|
|
||||||
local function FramePoolFactory(framePool)
|
|
||||||
framePool.createdFrames = framePool.createdFrames + 1
|
|
||||||
pollFrameIndex = pollFrameIndex + 1
|
|
||||||
local parentName, frameName = framePool.parent and framePool.parent:GetName()
|
|
||||||
if parentName then
|
|
||||||
frameName = string.format("%sPoolFrame%s%i_%i", parentName, framePool.frameTemplate or "NoTemplate", framePool.createdFrames, pollFrameIndex)
|
|
||||||
elseif framePool.frameTemplate then
|
|
||||||
frameName = string.format("UnnamedPoolFrame%s%i_%i", framePool.frameTemplate, framePool.createdFrames, pollFrameIndex)
|
|
||||||
else
|
|
||||||
frameName = string.format("UnnamedPoolFrameNoTemplate%i_%i", framePool.createdFrames, pollFrameIndex)
|
|
||||||
end
|
|
||||||
return CreateFrame(framePool.frameType, frameName, framePool.parent, framePool.frameTemplate);
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ForbiddenFramePoolFactory(framePool)
|
|
||||||
return FramePoolFactory(framePool)
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolMixin:OnLoad(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
|
|
||||||
if forbidden then
|
|
||||||
local creationFunc = ForbiddenFramePoolFactory;
|
|
||||||
if frameInitFunc ~= nil then
|
|
||||||
creationFunc = function(framePool)
|
|
||||||
local frame = ForbiddenFramePoolFactory(framePool);
|
|
||||||
frameInitFunc(frame);
|
|
||||||
return frame;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
|
|
||||||
else
|
|
||||||
local creationFunc = FramePoolFactory;
|
|
||||||
if frameInitFunc ~= nil then
|
|
||||||
creationFunc = function(framePool)
|
|
||||||
local frame = FramePoolFactory(framePool);
|
|
||||||
frameInitFunc(frame);
|
|
||||||
return frame;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
|
|
||||||
end
|
|
||||||
self.createdFrames = 0;
|
|
||||||
self.frameType = frameType;
|
|
||||||
self.parent = parent;
|
|
||||||
self.frameTemplate = frameTemplate;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolMixin:GetTemplate()
|
|
||||||
return self.frameTemplate;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePool_Hide(framePool, frame)
|
|
||||||
frame:Hide();
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePool_HideAndClearAnchors(framePool, frame)
|
|
||||||
frame:Hide();
|
|
||||||
frame:ClearAllPoints();
|
|
||||||
end
|
|
||||||
|
|
||||||
function CreateFramePool(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
|
|
||||||
local framePool = CreateFromMixins(FramePoolMixin);
|
|
||||||
framePool:OnLoad(frameType, parent, frameTemplate, resetterFunc or FramePool_HideAndClearAnchors, forbidden, frameInitFunc);
|
|
||||||
return framePool;
|
|
||||||
end
|
|
||||||
|
|
||||||
TexturePoolMixin = CreateFromMixins(ObjectPoolMixin);
|
|
||||||
|
|
||||||
local function TexturePoolFactory(texturePool)
|
|
||||||
return texturePool.parent:CreateTexture(nil, texturePool.layer, texturePool.textureTemplate, texturePool.subLayer);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TexturePoolMixin:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc)
|
|
||||||
ObjectPoolMixin.OnLoad(self, TexturePoolFactory, resetterFunc);
|
|
||||||
self.parent = parent;
|
|
||||||
self.layer = layer;
|
|
||||||
self.subLayer = subLayer;
|
|
||||||
self.textureTemplate = textureTemplate;
|
|
||||||
end
|
|
||||||
|
|
||||||
TexturePool_Hide = FramePool_Hide;
|
|
||||||
TexturePool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
|
|
||||||
|
|
||||||
function CreateTexturePool(parent, layer, subLayer, textureTemplate, resetterFunc)
|
|
||||||
local texturePool = CreateFromMixins(TexturePoolMixin);
|
|
||||||
texturePool:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc or TexturePool_HideAndClearAnchors);
|
|
||||||
return texturePool;
|
|
||||||
end
|
|
||||||
|
|
||||||
FontStringPoolMixin = CreateFromMixins(ObjectPoolMixin);
|
|
||||||
|
|
||||||
local function FontStringPoolFactory(fontStringPool)
|
|
||||||
return fontStringPool.parent:CreateFontString(nil, fontStringPool.layer, fontStringPool.fontStringTemplate, fontStringPool.subLayer);
|
|
||||||
end
|
|
||||||
|
|
||||||
function FontStringPoolMixin:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc)
|
|
||||||
ObjectPoolMixin.OnLoad(self, FontStringPoolFactory, resetterFunc);
|
|
||||||
self.parent = parent;
|
|
||||||
self.layer = layer;
|
|
||||||
self.subLayer = subLayer;
|
|
||||||
self.fontStringTemplate = fontStringTemplate;
|
|
||||||
end
|
|
||||||
|
|
||||||
FontStringPool_Hide = FramePool_Hide;
|
|
||||||
FontStringPool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
|
|
||||||
|
|
||||||
function CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
|
|
||||||
local fontStringPool = CreateFromMixins(FontStringPoolMixin);
|
|
||||||
fontStringPool:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc or FontStringPool_HideAndClearAnchors);
|
|
||||||
return fontStringPool;
|
|
||||||
end
|
|
||||||
|
|
||||||
ActorPoolMixin = CreateFromMixins(ObjectPoolMixin);
|
|
||||||
|
|
||||||
local function ActorPoolFactory(actorPool)
|
|
||||||
return actorPool.parent:CreateActor(nil, actorPool.actorTemplate);
|
|
||||||
end
|
|
||||||
|
|
||||||
function ActorPoolMixin:OnLoad(parent, actorTemplate, resetterFunc)
|
|
||||||
ObjectPoolMixin.OnLoad(self, ActorPoolFactory, resetterFunc);
|
|
||||||
self.parent = parent;
|
|
||||||
self.actorTemplate = actorTemplate;
|
|
||||||
end
|
|
||||||
|
|
||||||
ActorPool_Hide = FramePool_Hide;
|
|
||||||
function ActorPool_HideAndClearModel(actorPool, actor)
|
|
||||||
actor:ClearModel();
|
|
||||||
actor:Hide();
|
|
||||||
end
|
|
||||||
|
|
||||||
function CreateActorPool(parent, actorTemplate, resetterFunc)
|
|
||||||
local actorPool = CreateFromMixins(ActorPoolMixin);
|
|
||||||
actorPool:OnLoad(parent, actorTemplate, resetterFunc or ActorPool_HideAndClearModel);
|
|
||||||
return actorPool;
|
|
||||||
end
|
|
||||||
|
|
||||||
FramePoolCollectionMixin = {};
|
|
||||||
|
|
||||||
function CreateFramePoolCollection()
|
|
||||||
local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
|
|
||||||
poolCollection:OnLoad();
|
|
||||||
return poolCollection;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:OnLoad()
|
|
||||||
self.pools = {};
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:GetNumActive()
|
|
||||||
local numTotalActive = 0;
|
|
||||||
for _, pool in pairs(self.pools) do
|
|
||||||
numTotalActive = numTotalActive + pool:GetNumActive();
|
|
||||||
end
|
|
||||||
return numTotalActive;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Returns the pool, and whether or not the pool needed to be created.
|
|
||||||
function FramePoolCollectionMixin:GetOrCreatePool(frameType, parent, template, resetterFunc, forbidden)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
if not pool then
|
|
||||||
return self:CreatePool(frameType, parent, template, resetterFunc, forbidden), true;
|
|
||||||
end
|
|
||||||
return pool, false;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc)
|
|
||||||
assert(self:GetPool(template) == nil);
|
|
||||||
local pool = CreateFramePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc);
|
|
||||||
self.pools[template] = pool;
|
|
||||||
return pool;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:GetPool(template)
|
|
||||||
return self.pools[template];
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:Acquire(template)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
assert(pool);
|
|
||||||
return pool:Acquire();
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:Release(object)
|
|
||||||
for _, pool in pairs(self.pools) do
|
|
||||||
if pool:Release(object) then
|
|
||||||
-- Found it! Just return
|
|
||||||
return;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Huh, we didn't find that object
|
|
||||||
assert(false);
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:ReleaseAllByTemplate(template)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
if pool then
|
|
||||||
pool:ReleaseAll();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:ReleaseAll()
|
|
||||||
for key, pool in pairs(self.pools) do
|
|
||||||
pool:ReleaseAll();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:EnumerateActiveByTemplate(template)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
if pool then
|
|
||||||
return pool:EnumerateActive();
|
|
||||||
end
|
|
||||||
|
|
||||||
return nop;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:EnumerateActive()
|
|
||||||
local currentPoolKey, currentPool = next(self.pools, nil);
|
|
||||||
local currentObject = nil;
|
|
||||||
return function()
|
|
||||||
if currentPool then
|
|
||||||
currentObject = currentPool:GetNextActive(currentObject);
|
|
||||||
while not currentObject do
|
|
||||||
currentPoolKey, currentPool = next(self.pools, currentPoolKey);
|
|
||||||
if currentPool then
|
|
||||||
currentObject = currentPool:GetNextActive();
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return currentObject;
|
|
||||||
end, nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FramePoolCollectionMixin:EnumerateInactiveByTemplate(template)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
if pool then
|
|
||||||
return pool:EnumerateInactive();
|
|
||||||
end
|
|
||||||
|
|
||||||
return nop;
|
|
||||||
end
|
|
||||||
|
|
||||||
FixedSizeFramePoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
|
|
||||||
|
|
||||||
function CreateFixedSizeFramePoolCollection()
|
|
||||||
local poolCollection = CreateFromMixins(FixedSizeFramePoolCollectionMixin);
|
|
||||||
poolCollection:OnLoad();
|
|
||||||
return poolCollection;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FixedSizeFramePoolCollectionMixin:OnLoad()
|
|
||||||
FramePoolCollectionMixin.OnLoad(self);
|
|
||||||
self.sizes = {};
|
|
||||||
end
|
|
||||||
|
|
||||||
function FixedSizeFramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, maxPoolSize, preallocate)
|
|
||||||
local pool = FramePoolCollectionMixin.CreatePool(self, frameType, parent, template, resetterFunc, forbidden);
|
|
||||||
|
|
||||||
if preallocate then
|
|
||||||
for i = 1, maxPoolSize do
|
|
||||||
pool:Acquire();
|
|
||||||
end
|
|
||||||
pool:ReleaseAll();
|
|
||||||
end
|
|
||||||
|
|
||||||
self.sizes[template] = maxPoolSize;
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FixedSizeFramePoolCollectionMixin:Acquire(template)
|
|
||||||
local pool = self:GetPool(template);
|
|
||||||
assert(pool);
|
|
||||||
|
|
||||||
if pool:GetNumActive() < self.sizes[template] then
|
|
||||||
return pool:Acquire();
|
|
||||||
end
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
FontStringPoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
|
|
||||||
|
|
||||||
function CreateFontStringPoolCollection()
|
|
||||||
local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
|
|
||||||
poolCollection:OnLoad();
|
|
||||||
return poolCollection;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FontStringPoolCollectionMixin:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
|
|
||||||
local pool = self:GetPool(fontStringTemplate);
|
|
||||||
if not pool then
|
|
||||||
pool = self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
|
|
||||||
end
|
|
||||||
return pool;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FontStringPoolCollectionMixin:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
|
|
||||||
assert(self:GetPool(fontStringTemplate) == nil);
|
|
||||||
local pool = CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
|
|
||||||
self.pools[fontStringTemplate] = pool;
|
|
||||||
return pool;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FontStringPoolCollectionMixin:CreatePoolIfNeeded(parent, layer, subLayer, fontStringTemplate, resetterFunc)
|
|
||||||
if not self:GetPool(fontStringTemplate) then
|
|
||||||
self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function FontStringPoolCollectionMixin:Acquire(fontStringTemplate, parent, layer, subLayer, resetterFunc)
|
|
||||||
local pool = self:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
|
|
||||||
local newString = pool:Acquire();
|
|
||||||
|
|
||||||
if parent then
|
|
||||||
newString:SetParent(parent);
|
|
||||||
end
|
|
||||||
|
|
||||||
if layer then
|
|
||||||
newString:SetDrawLayer(layer, subLayer);
|
|
||||||
end
|
|
||||||
|
|
||||||
return newString;
|
|
||||||
end
|
|
||||||
@@ -1,660 +0,0 @@
|
|||||||
local function getObjTypeMeta(objType)
|
|
||||||
local obj = CreateFrame(objType)
|
|
||||||
obj:Hide()
|
|
||||||
return getmetatable(obj).__index, obj
|
|
||||||
end
|
|
||||||
|
|
||||||
local FrameMeta, FrameObj = getObjTypeMeta("Frame")
|
|
||||||
local FontStringMeta = getmetatable(FrameObj:CreateFontString()).__index
|
|
||||||
local TextureMeta = getmetatable(FrameObj:CreateTexture()).__index
|
|
||||||
|
|
||||||
local ButtonMeta = getObjTypeMeta("Button")
|
|
||||||
local CheckButtonMeta = getObjTypeMeta("CheckButton")
|
|
||||||
local EditBoxMeta = getObjTypeMeta("EditBox")
|
|
||||||
local SimpleHTMLMeta = getObjTypeMeta("SimpleHTML")
|
|
||||||
local SliderMeta = getObjTypeMeta("Slider")
|
|
||||||
local StatusBarMeta = getObjTypeMeta("StatusBar")
|
|
||||||
local ScrollFrameMeta = getObjTypeMeta("ScrollFrame")
|
|
||||||
local MessageFrameMeta = getObjTypeMeta("MessageFrame")
|
|
||||||
local ScrollingMessageFrameMeta = getObjTypeMeta("ScrollingMessageFrame")
|
|
||||||
local ModelMeta = getObjTypeMeta("Model")
|
|
||||||
--local ColorSelectMeta = createAndGetMeta("ColorSelect")
|
|
||||||
--local MovieFrameMeta = createAndGetMeta("MovieFrame")
|
|
||||||
|
|
||||||
local AnimationGroup = FrameObj:CreateAnimationGroup()
|
|
||||||
local AnimationGroupMeta = getmetatable(AnimationGroup).__index
|
|
||||||
local AnimationAnimationMeta = getmetatable(AnimationGroup:CreateAnimation("Animation")).__index
|
|
||||||
local AnimationAlphaMeta = getmetatable(AnimationGroup:CreateAnimation("Alpha")).__index
|
|
||||||
local AnimationRotationMeta = getmetatable(AnimationGroup:CreateAnimation("Rotation")).__index
|
|
||||||
local AnimationPathMeta = getmetatable(AnimationGroup:CreateAnimation("Path")).__index
|
|
||||||
local AnimationScaleMeta = getmetatable(AnimationGroup:CreateAnimation("Scale")).__index
|
|
||||||
local AnimationTranslationMeta = getmetatable(AnimationGroup:CreateAnimation("Translation")).__index
|
|
||||||
|
|
||||||
local GameTooltipMeta, PlayerModelMeta, DressUpModelMeta, TabardModelMeta, ModelFFXMeta
|
|
||||||
|
|
||||||
if IsOnGlueScreen() then
|
|
||||||
ModelFFXMeta = getObjTypeMeta("ModelFFX")
|
|
||||||
else
|
|
||||||
GameTooltipMeta = getObjTypeMeta("GameTooltip")
|
|
||||||
PlayerModelMeta = getObjTypeMeta("PlayerModel")
|
|
||||||
DressUpModelMeta = getObjTypeMeta("DressUpModel")
|
|
||||||
TabardModelMeta = getObjTypeMeta("TabardModel")
|
|
||||||
|
|
||||||
-- local CooldownMeta = createAndGetMeta("Cooldown")
|
|
||||||
-- local MinimapMeta = createAndGetMeta("Minimap")
|
|
||||||
-- local TaxiRouteFrameMeta = createAndGetMeta("TaxiRouteFrame")
|
|
||||||
-- local QuestPOIFrameMeta = createAndGetMeta("QuestPOIFrame")
|
|
||||||
-- local WorldFrameMeta = createAndGetMeta("WorldFrame")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nop() end
|
|
||||||
|
|
||||||
local function ScriptRegion_SetShown(self, show)
|
|
||||||
if show then
|
|
||||||
self:Show()
|
|
||||||
else
|
|
||||||
self:Hide()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ScriptRegion_GetScaledRect(self)
|
|
||||||
local left, bottom, width, height = self:GetRect()
|
|
||||||
if left and bottom and width and height then
|
|
||||||
local scale = self:GetEffectiveScale()
|
|
||||||
return left * scale, bottom * scale, width * scale, height * scale
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ScriptRegion_IsMouseOverEx(self, ignoreVisibility, checkMouseFocus)
|
|
||||||
if ignoreVisibility or self:IsVisible() then
|
|
||||||
local l, r, t, b = self:GetHitRectInsets()
|
|
||||||
return self:IsMouseOver(-t, b, l, -r) and (not checkMouseFocus or self == GetMouseFocus())
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ScriptRegionResizing_ClearAndSetPoint(self, ...)
|
|
||||||
self:ClearAllPoints()
|
|
||||||
self:SetPoint(...)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Region_GetEffectiveScale(self)
|
|
||||||
return self:GetParent():GetEffectiveScale()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function LayoutFrame_SetParentArray(self, arrayName, element, setInSelf)
|
|
||||||
local parent = not setInSelf and self:GetParent() or self
|
|
||||||
|
|
||||||
if not parent[arrayName] then
|
|
||||||
parent[arrayName] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(parent[arrayName], element or self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local RegisterCustomEvent = RegisterCustomEvent
|
|
||||||
local UnregisterCustomEvent = UnregisterCustomEvent
|
|
||||||
|
|
||||||
local function Frame_RegisterCustomEvent(self, event)
|
|
||||||
RegisterCustomEvent(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Frame_UnregisterCustomEvent(self, event)
|
|
||||||
UnregisterCustomEvent(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function FontString_SetDesaturated(self, toggle, color)
|
|
||||||
if toggle then
|
|
||||||
self:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
|
|
||||||
else
|
|
||||||
if type(color) == "table" then
|
|
||||||
self:SetTextColor(color.r, color.g, color.b)
|
|
||||||
else
|
|
||||||
self:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local SECONDS_PER_DAY = 86400
|
|
||||||
local function FontString_SetRemainingTime(self, seconds, daysformat)
|
|
||||||
if type(seconds) ~= "number" then
|
|
||||||
self:SetText("")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if daysformat then
|
|
||||||
if seconds >= SECONDS_PER_DAY then
|
|
||||||
self:SetFormattedText(D_DAYS_FULL, math.floor(seconds / SECONDS_PER_DAY))
|
|
||||||
else
|
|
||||||
self:SetText(date("!%X", seconds))
|
|
||||||
end
|
|
||||||
elseif seconds >= SECONDS_PER_DAY then
|
|
||||||
local days = string.format(DAY_ONELETTER_ABBR_SHORT, math.floor(seconds / SECONDS_PER_DAY))
|
|
||||||
self:SetFormattedText("%s %s", days, date("!%X", seconds % SECONDS_PER_DAY))
|
|
||||||
elseif seconds >= 0 then
|
|
||||||
self:SetText(date("!%X", seconds))
|
|
||||||
else
|
|
||||||
self:SetText("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Texture_SetSubTexCoord(self, left, right, top, bottom)
|
|
||||||
local ULx, ULy, LLx, LLy, URx, URy, LRx, LRy = self:GetTexCoord()
|
|
||||||
|
|
||||||
local leftedge = ULx
|
|
||||||
local rightedge = URx
|
|
||||||
local topedge = ULy
|
|
||||||
local bottomedge = LLy
|
|
||||||
|
|
||||||
local width = rightedge - leftedge
|
|
||||||
local height = bottomedge - topedge
|
|
||||||
|
|
||||||
leftedge = ULx + width * left
|
|
||||||
topedge = ULy + height * top
|
|
||||||
rightedge = math.max(rightedge * right, ULx)
|
|
||||||
bottomedge = math.max(bottomedge * bottom, ULy)
|
|
||||||
|
|
||||||
ULx = leftedge
|
|
||||||
ULy = topedge
|
|
||||||
LLx = leftedge
|
|
||||||
LLy = bottomedge
|
|
||||||
URx = rightedge
|
|
||||||
URy = topedge
|
|
||||||
LRx = rightedge
|
|
||||||
LRy = bottomedge
|
|
||||||
|
|
||||||
self:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ATLAS_FIELD_WIDTH = 1
|
|
||||||
local ATLAS_FIELD_HEIGHT = 2
|
|
||||||
local ATLAS_FIELD_LEFT = 3
|
|
||||||
local ATLAS_FIELD_RIGHT = 4
|
|
||||||
local ATLAS_FIELD_TOP = 5
|
|
||||||
local ATLAS_FIELD_BOTTOM = 6
|
|
||||||
local ATLAS_FIELD_TILESHORIZ = 7
|
|
||||||
local ATLAS_FIELD_TILESVERT = 8
|
|
||||||
local ATLAS_FIELD_TEXTUREPATH = 9
|
|
||||||
|
|
||||||
local S_ATLAS_STORAGE = S_ATLAS_STORAGE
|
|
||||||
|
|
||||||
local function Texture_SetAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
if type(self) ~= "table" then
|
|
||||||
error("Attempt to find 'this' in non-table object (used '.' instead of ':' ?)", 3)
|
|
||||||
elseif type(atlasName) ~= "string" then
|
|
||||||
error(string.format("Usage: %s:SetAtlas(\"atlasName\"[, useAtlasSize])", self:GetName() or tostring(self)), 3)
|
|
||||||
end
|
|
||||||
|
|
||||||
local atlas = S_ATLAS_STORAGE[atlasName]
|
|
||||||
if not atlas then
|
|
||||||
error(string.format("%s:SetAtlas: Atlas %s does not exist", self:GetName() or tostring(self), atlasName), 3)
|
|
||||||
end
|
|
||||||
|
|
||||||
local success = self:SetTexture(atlas[ATLAS_FIELD_TEXTUREPATH] or "", atlas[ATLAS_FIELD_TILESHORIZ], atlas[ATLAS_FIELD_TILESVERT])
|
|
||||||
if success then
|
|
||||||
if useAtlasSize then
|
|
||||||
self:SetWidth(atlas[ATLAS_FIELD_WIDTH])
|
|
||||||
self:SetHeight(atlas[ATLAS_FIELD_HEIGHT])
|
|
||||||
end
|
|
||||||
|
|
||||||
self:SetTexCoord(atlas[ATLAS_FIELD_LEFT], atlas[ATLAS_FIELD_RIGHT], atlas[ATLAS_FIELD_TOP], atlas[ATLAS_FIELD_BOTTOM])
|
|
||||||
|
|
||||||
self:SetHorizTile(atlas[ATLAS_FIELD_TILESHORIZ])
|
|
||||||
self:SetVertTile(atlas[ATLAS_FIELD_TILESVERT])
|
|
||||||
end
|
|
||||||
|
|
||||||
return success
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Texture_SetPortrait(self, displayID)
|
|
||||||
local portrait = strconcat("Interface\\PORTRAITS\\Portrait_model_", tonumber(displayID))
|
|
||||||
local success = self:SetTexture(portrait)
|
|
||||||
return success
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_SetEnabled(self, enabled)
|
|
||||||
if enabled then
|
|
||||||
self:Enable()
|
|
||||||
else
|
|
||||||
self:Disable()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_SetNormalAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetNormalTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetNormalTexture("")
|
|
||||||
texture = self:GetNormalTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_SetPushedAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetPushedTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetPushedTexture("")
|
|
||||||
texture = self:GetPushedTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_SetDisabledAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetDisabledTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetDisabledTexture("")
|
|
||||||
texture = self:GetDisabledTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_SetHighlightAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetHighlightTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetHighlightTexture("")
|
|
||||||
texture = self:GetHighlightTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_ClearNormalTexture(self)
|
|
||||||
self:SetNormalTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_ClearPushedTexture(self)
|
|
||||||
self:SetPushedTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_ClearDisabledTexture(self)
|
|
||||||
self:SetDisabledTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Button_ClearHighlightTexture(self)
|
|
||||||
self:SetHighlightTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function CheckButton_SetCheckedAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetCheckedTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetCheckedTexture("")
|
|
||||||
texture = self:GetCheckedTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function CheckButton_SetDisabledCheckedAtlas(self, atlasName, useAtlasSize, filterMode)
|
|
||||||
local texture = self:GetDisabledCheckedTexture()
|
|
||||||
if not texture then
|
|
||||||
self:SetDisabledCheckedTexture("")
|
|
||||||
texture = self:GetDisabledCheckedTexture()
|
|
||||||
end
|
|
||||||
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function CheckButton_ClearCheckedTexture(self)
|
|
||||||
self:SetCheckedTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function CheckButton_ClearDisabledCheckedTexture(self)
|
|
||||||
self:SetDisabledCheckedTexture("")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function EditBox_IsEnabled(self)
|
|
||||||
return self.__ext_Disabled ~= true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function EditBox_Enabled(self)
|
|
||||||
if self.__ext_mouseEnabled then
|
|
||||||
self:EnableMouse(true)
|
|
||||||
end
|
|
||||||
if self.__ext_autoFocus then
|
|
||||||
self:SetAutoFocus(true)
|
|
||||||
self:SetFocus()
|
|
||||||
self:HighlightText(0, 0)
|
|
||||||
end
|
|
||||||
self.SetFocus = nil
|
|
||||||
self.__ext_Disabled = nil
|
|
||||||
self.__ext_autoFocus = nil
|
|
||||||
self.__ext_mouseEnabled = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function EditBox_Disable(self)
|
|
||||||
if not self.__ext_Disabled then
|
|
||||||
self.__ext_autoFocus = self:IsAutoFocus() == 1
|
|
||||||
self.__ext_mouseEnabled = self:IsMouseEnabled() == 1
|
|
||||||
end
|
|
||||||
self:SetAutoFocus(false)
|
|
||||||
self:ClearFocus()
|
|
||||||
self:EnableMouse(false)
|
|
||||||
self.SetFocus = nop
|
|
||||||
self.__ext_Disabled = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function PlayerModel_UpdateModelEffects(self)
|
|
||||||
self:ResetAllSpells()
|
|
||||||
|
|
||||||
do -- 3D armor
|
|
||||||
local displayIDs = self:GetDisplayIDs()
|
|
||||||
if displayIDs then
|
|
||||||
for index, displayID in ipairs(displayIDs) do
|
|
||||||
if displayID ~= 0 then
|
|
||||||
local spellID = ITEMS_WITH_3D[displayID]
|
|
||||||
if spellID then
|
|
||||||
self:SetSpell(spellID)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- customizations
|
|
||||||
if not self.__hideCustomizations then
|
|
||||||
local guid = self.__unitGUID
|
|
||||||
if guid and guid == UnitGUID("player") then
|
|
||||||
local activeCustomizationItems = C_CustomizationsCollection.GetActiveCustomizationItems(1)
|
|
||||||
if activeCustomizationItems then
|
|
||||||
for index, customizationItemID in ipairs(activeCustomizationItems) do
|
|
||||||
local spellID = C_CustomizationsCollection.GetCustomizationSpellByItem(customizationItemID)
|
|
||||||
if spellID then
|
|
||||||
if spellID == 320799 then
|
|
||||||
local SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS = {313589, 313590, 313591, 313592, 313593, 313594}
|
|
||||||
spellID = SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS[math.random(1, #SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS)]
|
|
||||||
end
|
|
||||||
self:SetSpell(spellID)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function PlayerModel_SetUnit(self, unit, isCustomPosition, positionData)
|
|
||||||
if type(unit) ~= "string" then
|
|
||||||
error("Usage: SetUnit(\"unit\")", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local objectType = self:GetObjectType()
|
|
||||||
if objectType == "PlayerModel" then
|
|
||||||
self:SetUnitRaw(unit)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if isCustomPosition then
|
|
||||||
if objectType == "TabardModel" then
|
|
||||||
self:SetPosition(0, 0, 0)
|
|
||||||
else
|
|
||||||
self:SetPosition(1, 1, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self:SetUnitRaw(unit)
|
|
||||||
|
|
||||||
self.__hideCustomizations = nil
|
|
||||||
self.__unitGUID = UnitGUID(unit)
|
|
||||||
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
|
|
||||||
if isCustomPosition then
|
|
||||||
local unitSex = UnitSex(unit) or 2
|
|
||||||
local _, unitRace = UnitRace(unit)
|
|
||||||
local positionStorage = positionData or (objectType == "TabardModel" and TABARDMODEL_CAMERA_POSITION or DRESSUPMODEL_CAMERA_POSITION)
|
|
||||||
local data = positionStorage[string.format("%s%d", unitRace or "Human", unitSex)]
|
|
||||||
|
|
||||||
if data then
|
|
||||||
local positionX, positionY, positionZ, facing = unpack(data, 1, 4)
|
|
||||||
self.basePositionOverrideX = positionX
|
|
||||||
self.basePositionOverrideY = positionY
|
|
||||||
self.basePositionOverrideZ = positionZ
|
|
||||||
self.basePositionOverrideXasZ = true
|
|
||||||
self:SetPosition(positionX or 0, positionY or 0, positionZ or 0)
|
|
||||||
|
|
||||||
if facing then
|
|
||||||
self:SetFacing(math.rad(facing))
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.basePositionOverrideX = nil
|
|
||||||
self.basePositionOverrideY = nil
|
|
||||||
self.basePositionOverrideZ = nil
|
|
||||||
self.basePositionOverrideXasZ = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function PlayerModel_RefreshUnit(self)
|
|
||||||
self:RefreshUnitRaw()
|
|
||||||
|
|
||||||
local objectType = self:GetObjectType()
|
|
||||||
if objectType == "DressUpModel" or objectType == "TabardModel" then
|
|
||||||
self.__hideCustomizations = nil
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DressUpModel_Dress(self)
|
|
||||||
self:DressRaw()
|
|
||||||
self.__hideCustomizations = nil
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DressUpModel_Undress(self)
|
|
||||||
self:UndressRaw()
|
|
||||||
self.__hideCustomizations = true
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DressUpModel_TryOn(self, item)
|
|
||||||
if C_CustomizationsCollection.CanPreviewCustomizationInDressUp(item) then
|
|
||||||
local spellID, category, subcategory = C_CustomizationsCollection.GetCustomizationSpellByItem(item)
|
|
||||||
if spellID then
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
|
|
||||||
for index, activeSpellID in ipairs({self:GetAllSpells()}) do
|
|
||||||
local isCustomizationSpell, activeSpellCategory, activeSpellSubCategory = C_CustomizationsCollection.IsCustomizationSpell(activeSpellID)
|
|
||||||
if isCustomizationSpell and category == activeSpellCategory and subcategory == activeSpellSubCategory then
|
|
||||||
self:ResetSpell(activeSpellID)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if spellID == 320799 then
|
|
||||||
local SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS = {313589, 313590, 313591, 313592, 313593, 313594}
|
|
||||||
spellID = SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS[math.random(1, #SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS)]
|
|
||||||
end
|
|
||||||
|
|
||||||
self:SetSpell(spellID)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self:TryOnRaw(item)
|
|
||||||
PlayerModel_UpdateModelEffects(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Animation_Restart(self)
|
|
||||||
if self:IsPlaying() then
|
|
||||||
self:Stop()
|
|
||||||
end
|
|
||||||
self:Play()
|
|
||||||
end
|
|
||||||
|
|
||||||
FrameMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
FrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
FrameMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
|
|
||||||
FrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
FrameMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
FrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
FrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
FontStringMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
FontStringMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
FontStringMeta.GetEffectiveScale = Region_GetEffectiveScale
|
|
||||||
FontStringMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
FontStringMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
FontStringMeta.SetDesaturated = FontString_SetDesaturated
|
|
||||||
FontStringMeta.SetRemainingTime = FontString_SetRemainingTime
|
|
||||||
|
|
||||||
TextureMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
TextureMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
TextureMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
TextureMeta.GetEffectiveScale = Region_GetEffectiveScale
|
|
||||||
TextureMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
TextureMeta.SetSubTexCoord = Texture_SetSubTexCoord
|
|
||||||
TextureMeta.SetAtlas = Texture_SetAtlas
|
|
||||||
TextureMeta.SetPortrait = Texture_SetPortrait
|
|
||||||
|
|
||||||
ButtonMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
ButtonMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
ButtonMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
|
|
||||||
ButtonMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
ButtonMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
ButtonMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
ButtonMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
ButtonMeta.SetEnabled = Button_SetEnabled
|
|
||||||
ButtonMeta.SetNormalAtlas = Button_SetNormalAtlas
|
|
||||||
ButtonMeta.SetPushedAtlas = Button_SetPushedAtlas
|
|
||||||
ButtonMeta.SetDisabledAtlas = Button_SetDisabledAtlas
|
|
||||||
ButtonMeta.SetHighlightAtlas = Button_SetHighlightAtlas
|
|
||||||
ButtonMeta.ClearClearNormalTexture = Button_ClearNormalTexture
|
|
||||||
ButtonMeta.ClearPushedTexture = Button_ClearPushedTexture
|
|
||||||
ButtonMeta.ClearDisabledTexture = Button_ClearDisabledTexture
|
|
||||||
ButtonMeta.ClearHighlightTexture = Button_ClearHighlightTexture
|
|
||||||
|
|
||||||
CheckButtonMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
CheckButtonMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
|
|
||||||
CheckButtonMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
CheckButtonMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
CheckButtonMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
CheckButtonMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
CheckButtonMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
CheckButtonMeta.SetEnabled = Button_SetEnabled
|
|
||||||
CheckButtonMeta.SetNormalAtlas = Button_SetNormalAtlas
|
|
||||||
CheckButtonMeta.SetPushedAtlas = Button_SetPushedAtlas
|
|
||||||
CheckButtonMeta.SetDisabledAtlas = Button_SetDisabledAtlas
|
|
||||||
CheckButtonMeta.SetHighlightAtlas = Button_SetHighlightAtlas
|
|
||||||
CheckButtonMeta.SetCheckedAtlas = CheckButton_SetCheckedAtlas
|
|
||||||
CheckButtonMeta.SetDisabledCheckedAtlas = CheckButton_SetDisabledCheckedAtlas
|
|
||||||
CheckButtonMeta.ClearClearNormalTexture = Button_ClearNormalTexture
|
|
||||||
CheckButtonMeta.ClearPushedTexture = Button_ClearPushedTexture
|
|
||||||
CheckButtonMeta.ClearDisabledTexture = Button_ClearDisabledTexture
|
|
||||||
CheckButtonMeta.ClearHighlightTexture = Button_ClearHighlightTexture
|
|
||||||
CheckButtonMeta.ClearDisabledCheckedTexture = CheckButton_ClearCheckedTexture
|
|
||||||
CheckButtonMeta.ClearDisabledCheckedTexture = CheckButton_ClearDisabledCheckedTexture
|
|
||||||
|
|
||||||
EditBoxMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
EditBoxMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
EditBoxMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
EditBoxMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
EditBoxMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
EditBoxMeta.SetEnabled = Button_SetEnabled
|
|
||||||
EditBoxMeta.IsEnabled = EditBox_IsEnabled
|
|
||||||
EditBoxMeta.Enable = EditBox_Enabled
|
|
||||||
EditBoxMeta.Disable = EditBox_Disable
|
|
||||||
|
|
||||||
SimpleHTMLMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
SimpleHTMLMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
SimpleHTMLMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
SimpleHTMLMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
SimpleHTMLMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
SimpleHTMLMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
SliderMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
SliderMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
SliderMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
|
|
||||||
SliderMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
SliderMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
SliderMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
SliderMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
SliderMeta.SetEnabled = Button_SetEnabled
|
|
||||||
|
|
||||||
StatusBarMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
StatusBarMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
StatusBarMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
StatusBarMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
StatusBarMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
StatusBarMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
ScrollFrameMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
ScrollFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
ScrollFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
ScrollFrameMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
ScrollFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
ScrollFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
MessageFrameMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
MessageFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
MessageFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
MessageFrameMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
MessageFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
MessageFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
ScrollingMessageFrameMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
ScrollingMessageFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
ScrollingMessageFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
ScrollingMessageFrameMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
ScrollingMessageFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
ScrollingMessageFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
ModelMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
ModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
ModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
ModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
ModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
AnimationGroupMeta.Restart = Animation_Restart
|
|
||||||
AnimationAnimationMeta.Restart = Animation_Restart
|
|
||||||
AnimationAlphaMeta.Restart = Animation_Restart
|
|
||||||
AnimationRotationMeta.Restart = Animation_Restart
|
|
||||||
AnimationPathMeta.Restart = Animation_Restart
|
|
||||||
AnimationScaleMeta.Restart = Animation_Restart
|
|
||||||
AnimationTranslationMeta.Restart = Animation_Restart
|
|
||||||
|
|
||||||
if IsOnGlueScreen() then
|
|
||||||
ModelFFXMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
ModelFFXMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
ModelFFXMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
else
|
|
||||||
GameTooltipMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
GameTooltipMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
GameTooltipMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
|
|
||||||
PlayerModelMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
PlayerModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
PlayerModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
PlayerModelMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
PlayerModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
PlayerModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
PlayerModelMeta.SetUnitRaw = PlayerModelMeta.SetUnit
|
|
||||||
PlayerModelMeta.SetUnit = PlayerModel_SetUnit
|
|
||||||
PlayerModelMeta.RefreshUnitRaw = PlayerModelMeta.RefreshUnit
|
|
||||||
PlayerModelMeta.RefreshUnit = PlayerModel_RefreshUnit
|
|
||||||
|
|
||||||
DressUpModelMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
DressUpModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
DressUpModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
DressUpModelMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
DressUpModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
DressUpModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
DressUpModelMeta.SetUnitRaw = DressUpModelMeta.SetUnit
|
|
||||||
DressUpModelMeta.SetUnit = PlayerModel_SetUnit
|
|
||||||
DressUpModelMeta.RefreshUnitRaw = DressUpModelMeta.RefreshUnit
|
|
||||||
DressUpModelMeta.RefreshUnit = PlayerModel_RefreshUnit
|
|
||||||
DressUpModelMeta.TryOnRaw = DressUpModelMeta.TryOn
|
|
||||||
DressUpModelMeta.TryOn = DressUpModel_TryOn
|
|
||||||
DressUpModelMeta.DressRaw = DressUpModelMeta.Dress
|
|
||||||
DressUpModelMeta.Dress = DressUpModel_Dress
|
|
||||||
DressUpModelMeta.UndressRaw = DressUpModelMeta.Undress
|
|
||||||
DressUpModelMeta.Undress = DressUpModel_Undress
|
|
||||||
|
|
||||||
TabardModelMeta.SetShown = ScriptRegion_SetShown
|
|
||||||
TabardModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
|
|
||||||
TabardModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
|
|
||||||
TabardModelMeta.SetParentArray = LayoutFrame_SetParentArray
|
|
||||||
TabardModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
|
|
||||||
TabardModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
|
|
||||||
TabardModelMeta.SetUnitRaw = TabardModelMeta.SetUnit
|
|
||||||
TabardModelMeta.SetUnit = PlayerModel_SetUnit
|
|
||||||
TabardModelMeta.RefreshUnitRaw = TabardModelMeta.RefreshUnit
|
|
||||||
TabardModelMeta.RefreshUnit = PlayerModel_RefreshUnit
|
|
||||||
TabardModelMeta.SetSpell = function() end
|
|
||||||
end
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
local function SetupTextFont(fontString, fontObject)
|
|
||||||
if fontString and fontObject then
|
|
||||||
fontString:SetFontObject(fontObject);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function SharedTooltip_OnLoad(self)
|
|
||||||
local style = nil;
|
|
||||||
local isEmbedded = false;
|
|
||||||
SharedTooltip_SetBackdropStyle(self, style, isEmbedded);
|
|
||||||
self:SetClampRectInsets(0, 0, 15, 0);
|
|
||||||
|
|
||||||
SetupTextFont(self.TextLeft1, self.textLeft1Font);
|
|
||||||
SetupTextFont(self.TextRight1, self.textRight1Font);
|
|
||||||
SetupTextFont(self.TextLeft2, self.textLeft2Font);
|
|
||||||
SetupTextFont(self.TextRight2, self.textRight2Font);
|
|
||||||
end
|
|
||||||
|
|
||||||
function SharedTooltip_OnHide(self)
|
|
||||||
self:SetPadding(0, 0, 0, 0);
|
|
||||||
end
|
|
||||||
|
|
||||||
local DEFAULT_TOOLTIP_OFFSET_X = -17;
|
|
||||||
local DEFAULT_TOOLTIP_OFFSET_Y = 70;
|
|
||||||
|
|
||||||
function SharedTooltip_SetDefaultAnchor(tooltip, parent)
|
|
||||||
tooltip:SetOwner(parent or GetAppropriateTopLevelParent(), "ANCHOR_NONE");
|
|
||||||
tooltip:SetPoint("BOTTOMRIGHT", GetAppropriateTopLevelParent(), "BOTTOMRIGHT", DEFAULT_TOOLTIP_OFFSET_X, DEFAULT_TOOLTIP_OFFSET_Y);
|
|
||||||
end
|
|
||||||
|
|
||||||
function SharedTooltip_ClearInsertedFrames(self)
|
|
||||||
if self.insertedFrames then
|
|
||||||
for i = 1, #self.insertedFrames do
|
|
||||||
self.insertedFrames[i]:Hide();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.insertedFrames = nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function SharedTooltip_SetBackdropStyle(self, style, embedded)
|
|
||||||
if embedded or self.IsEmbedded then
|
|
||||||
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b, 0)
|
|
||||||
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b, 0);
|
|
||||||
else
|
|
||||||
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b, 1);
|
|
||||||
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b, 1);
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.TopOverlay then
|
|
||||||
if style and style.overlayAtlasTop then
|
|
||||||
self.TopOverlay:SetAtlas(style.overlayAtlasTop, true);
|
|
||||||
self.TopOverlay:SetScale(style.overlayAtlasTopScale or 1.0);
|
|
||||||
self.TopOverlay:SetPoint("CENTER", self, "TOP", style.overlayAtlasTopXOffset or 0, style.overlayAtlasTopYOffset or 0);
|
|
||||||
self.TopOverlay:Show();
|
|
||||||
else
|
|
||||||
self.TopOverlay:Hide();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.BottomOverlay then
|
|
||||||
if style and style.overlayAtlasBottom then
|
|
||||||
self.BottomOverlay:SetAtlas(style.overlayAtlasBottom, true);
|
|
||||||
self.BottomOverlay:SetScale(style.overlayAtlasBottomScale or 1.0);
|
|
||||||
self.BottomOverlay:SetPoint("CENTER", self, "BOTTOM", style.overlayAtlasBottomXOffset or 0, style.overlayAtlasBottomYOffset or 0);
|
|
||||||
self.BottomOverlay:Show();
|
|
||||||
else
|
|
||||||
self.BottomOverlay:Hide();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if style and style.padding then
|
|
||||||
self:SetPadding(style.padding.right, style.padding.bottom, style.padding.left, style.padding.top);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddBlankLinesToTooltip(tooltip, numLines)
|
|
||||||
if numLines ~= nil then
|
|
||||||
for i = 1, numLines do
|
|
||||||
tooltip:AddLine(" ");
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddBlankLineToTooltip(tooltip)
|
|
||||||
GameTooltip_AddBlankLinesToTooltip(tooltip, 1);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_SetTitle(tooltip, text, overrideColor, wrap)
|
|
||||||
local titleColor = overrideColor or HIGHLIGHT_FONT_COLOR;
|
|
||||||
local r, g, b, a = titleColor:GetRGBA();
|
|
||||||
if wrap == nil then
|
|
||||||
wrap = 1;
|
|
||||||
end
|
|
||||||
tooltip:SetText(text, r, g, b, a, wrap);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_ShowDisabledTooltip(tooltip, owner, text, tooltipAnchor)
|
|
||||||
tooltip:SetOwner(owner, tooltipAnchor);
|
|
||||||
|
|
||||||
local wrap = 1;
|
|
||||||
GameTooltip_SetTitle(tooltip, text, RED_FONT_COLOR, wrap);
|
|
||||||
|
|
||||||
tooltip:Show();
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
|
|
||||||
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
|
|
||||||
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddInstructionLine(tooltip, text, wrap, leftOffset)
|
|
||||||
GameTooltip_AddColoredLine(tooltip, text, GREEN_FONT_COLOR, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddErrorLine(tooltip, text, wrap, leftOffset)
|
|
||||||
GameTooltip_AddColoredLine(tooltip, text, RED_FONT_COLOR, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddDisabledLine(tooltip, text, wrap, leftOffset)
|
|
||||||
GameTooltip_AddColoredLine(tooltip, text, DISABLED_FONT_COLOR, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
|
|
||||||
local r, g, b = color:GetRGB();
|
|
||||||
if wrap == nil then
|
|
||||||
wrap = 1;
|
|
||||||
end
|
|
||||||
tooltip:AddLine(text, r, g, b, wrap, leftOffset);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrap)
|
|
||||||
local leftR, leftG, leftB = leftColor:GetRGB();
|
|
||||||
local rightR, rightG, rightB = rightColor:GetRGB();
|
|
||||||
if wrap == nil then
|
|
||||||
wrap = 1;
|
|
||||||
end
|
|
||||||
tooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB, wrap);
|
|
||||||
end
|
|
||||||
|
|
||||||
function GameTooltip_InsertFrame(tooltipFrame, frame, verticalPadding)
|
|
||||||
verticalPadding = verticalPadding or 0;
|
|
||||||
|
|
||||||
local textSpacing = 2;
|
|
||||||
local textHeight = Round(_G[tooltipFrame:GetName().."TextLeft2"]:GetLineHeight());
|
|
||||||
local neededHeight = Round(frame:GetHeight() + verticalPadding);
|
|
||||||
local numLinesNeeded = math.ceil(neededHeight / (textHeight + textSpacing));
|
|
||||||
local currentLine = tooltipFrame:NumLines();
|
|
||||||
GameTooltip_AddBlankLinesToTooltip(tooltipFrame, numLinesNeeded);
|
|
||||||
frame:SetParent(tooltipFrame);
|
|
||||||
frame:ClearAllPoints();
|
|
||||||
frame:SetPoint("TOPLEFT", tooltipFrame:GetName().."TextLeft"..(currentLine + 1), "TOPLEFT", 0, -verticalPadding);
|
|
||||||
if not tooltipFrame.insertedFrames then
|
|
||||||
tooltipFrame.insertedFrames = { };
|
|
||||||
end
|
|
||||||
local frameWidth = frame:GetWidth();
|
|
||||||
if tooltipFrame:GetMinimumWidth() < frameWidth then
|
|
||||||
tooltipFrame:SetMinimumWidth(frameWidth);
|
|
||||||
end
|
|
||||||
frame:Show();
|
|
||||||
tinsert(tooltipFrame.insertedFrames, frame);
|
|
||||||
-- return space taken so inserted frame can resize if needed
|
|
||||||
return (numLinesNeeded * textHeight) + (numLinesNeeded - 1) * textSpacing;
|
|
||||||
end
|
|
||||||
|
|
||||||
DisabledTooltipButtonMixin = {};
|
|
||||||
|
|
||||||
function DisabledTooltipButtonMixin:OnEnter()
|
|
||||||
if not self:IsEnabled() then
|
|
||||||
local disabledTooltip, disabledTooltipAnchor = self:GetDisabledTooltip();
|
|
||||||
if disabledTooltip ~= nil then
|
|
||||||
GameTooltip_ShowDisabledTooltip(GetAppropriateTooltip(), self, disabledTooltip, disabledTooltipAnchor);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function DisabledTooltipButtonMixin:OnLeave()
|
|
||||||
local tooltip = GetAppropriateTooltip();
|
|
||||||
tooltip:Hide();
|
|
||||||
end
|
|
||||||
|
|
||||||
function DisabledTooltipButtonMixin:SetDisabledTooltip(disabledTooltip, disabledTooltipAnchor)
|
|
||||||
self.disabledTooltip = disabledTooltip;
|
|
||||||
self.disabledTooltipAnchor = disabledTooltipAnchor;
|
|
||||||
end
|
|
||||||
|
|
||||||
function DisabledTooltipButtonMixin:GetDisabledTooltip()
|
|
||||||
return self.disabledTooltip, self.disabledTooltipAnchor;
|
|
||||||
end
|
|
||||||
|
|
||||||
function DisabledTooltipButtonMixin:SetDisabledState(disabled, disabledTooltip, disabledTooltipAnchor)
|
|
||||||
self:SetEnabled(not disabled);
|
|
||||||
self:SetDisabledTooltip(disabledTooltip, disabledTooltipAnchor);
|
|
||||||
end
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
|
||||||
..\FrameXML\UI.xsd">
|
|
||||||
<Script file="SharedTooltipTemplates.lua"/>
|
|
||||||
|
|
||||||
<FontString name="TooltipTextLeftTemplate" inherits="GameTooltipText" justifyH="LEFT" hidden="true" virtual="true"/>
|
|
||||||
<FontString name="TooltipTextRightTemplate" inherits="GameTooltipText" justifyH="RIGHT" hidden="true" virtual="true"/>
|
|
||||||
|
|
||||||
<Texture name="TooltipTextureTemplate" hidden="true" virtual="true">
|
|
||||||
<Size x="12" y="12"/>
|
|
||||||
</Texture>
|
|
||||||
|
|
||||||
<GameTooltip name="SharedTooltipTemplate" clampedToScreen="true" frameStrata="TOOLTIP" hidden="true" virtual="true">
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="textLeft1Font" value="GameTooltipHeaderText" type="string"/>
|
|
||||||
<Attribute name="textRight1Font" value="GameTooltipHeaderText" type="string"/>
|
|
||||||
<Attribute name="textLeft2Font" value="GameTooltipText" type="string"/>
|
|
||||||
<Attribute name="textRight2Font" value="GameTooltipText" type="string"/>
|
|
||||||
<Attribute name="layoutType" value="TooltipDefaultLayout" type="string"/>
|
|
||||||
</Attributes>
|
|
||||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
|
||||||
<EdgeSize>
|
|
||||||
<AbsValue val="16"/>
|
|
||||||
</EdgeSize>
|
|
||||||
<TileSize>
|
|
||||||
<AbsValue val="16"/>
|
|
||||||
</TileSize>
|
|
||||||
<BackgroundInsets>
|
|
||||||
<AbsInset left="5" right="5" top="5" bottom="5"/>
|
|
||||||
</BackgroundInsets>
|
|
||||||
</Backdrop>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="OVERLAY">
|
|
||||||
<Texture parentKey="TopOverlay" hidden="true">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" relativePoint="TOP" />
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BottomOverlay" hidden="true">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER" relativePoint="BOTTOM" />
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<FontString name="$parentTextLeft1" parentKey="TextLeft1" inherits="TooltipTextLeftTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" x="10" y="-10"/>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString name="$parentTextRight1" parentKey="TextRight1" inherits="TooltipTextRightTemplate" justifyH="LEFT">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="RIGHT" relativeTo="$parentTextLeft1" relativePoint="LEFT" x="40" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString name="$parentTextLeft2" parentKey="TextLeft2" inherits="TooltipTextLeftTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentTextLeft1" relativePoint="BOTTOMLEFT" x="0" y="-2"/>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<FontString name="$parentTextRight2" parentKey="TextRight2" inherits="TooltipTextRightTemplate">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="RIGHT" relativeTo="$parentTextLeft2" relativePoint="LEFT" x="40" y="0"/>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
<Texture name="$parentTexture1" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture2" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture3" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture4" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture5" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture6" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture7" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture8" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture9" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture10" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture11" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture12" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture13" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture14" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture15" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture16" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture17" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture18" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture19" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture20" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture21" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture22" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture23" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture24" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture25" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture26" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture27" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture28" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture29" inherits="TooltipTextureTemplate"/>
|
|
||||||
<Texture name="$parentTexture30" inherits="TooltipTextureTemplate"/>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad function="SharedTooltip_OnLoad"/>
|
|
||||||
<OnHide function="SharedTooltip_OnHide"/>
|
|
||||||
<OnTooltipSetDefaultAnchor function="SharedTooltip_SetDefaultAnchor"/>
|
|
||||||
<OnTooltipCleared function="SharedTooltip_ClearInsertedFrames"/>
|
|
||||||
</Scripts>
|
|
||||||
</GameTooltip>
|
|
||||||
|
|
||||||
<GameTooltip name="SharedNoHeaderTooltipTemplate" inherits="SharedTooltipTemplate" virtual="true">
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="textLeft1Font" value="GameTooltipText" type="string"/>
|
|
||||||
<Attribute name="textRight1Font" value="GameTooltipText" type="string"/>
|
|
||||||
</Attributes>
|
|
||||||
</GameTooltip>
|
|
||||||
|
|
||||||
<Frame name="TooltipBorderedFrameTemplate" virtual="true">
|
|
||||||
<Layers>
|
|
||||||
<Layer level="BACKGROUND">
|
|
||||||
<Texture parentKey="BorderTopLeft" name="$parentBorderTopLeft" file="Interface\Tooltips\UI-Tooltip-TL">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderTopRight" name="$parentBorderTopRight" file="Interface\Tooltips\UI-Tooltip-TR">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderBottomRight" name="$parentBorderBottomRight" file="Interface\Tooltips\UI-Tooltip-BR">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMRIGHT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderBottomLeft" name="$parentBorderBottomLeft" file="Interface\Tooltips\UI-Tooltip-BL">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderTop" file="Interface\Tooltips\UI-Tooltip-T">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="TOPRIGHT"/>
|
|
||||||
<Anchor point="TOPRIGHT" relativeTo="$parentBorderTopRight" relativePoint="TOPLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderRight" file="Interface\Tooltips\UI-Tooltip-R">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT" relativeTo="$parentBorderTopRight" relativePoint="BOTTOMRIGHT"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="TOPRIGHT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderBottom" file="Interface\Tooltips\UI-Tooltip-B">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentBorderBottomLeft" relativePoint="BOTTOMRIGHT"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="BOTTOMLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="BorderLeft" file="Interface\Tooltips\UI-Tooltip-L">
|
|
||||||
<Size x="8" y="8"/>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOMLEFT"/>
|
|
||||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentBorderBottomLeft" relativePoint="TOPLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
</Texture>
|
|
||||||
<Texture parentKey="Background">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOMRIGHT"/>
|
|
||||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="TOPLEFT"/>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0" g="0" b="0" a="0.8"/>
|
|
||||||
</Texture>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<Button name="DisabledTooltipButtonTemplate" motionScriptsWhileDisabled="true" virtual="true">
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>
|
|
||||||
Mixin(self, DisabledTooltipButtonMixin)
|
|
||||||
</OnLoad>
|
|
||||||
<OnEnter>
|
|
||||||
self:OnEnter()
|
|
||||||
</OnEnter>
|
|
||||||
<OnLeave>
|
|
||||||
self:OnLeave()
|
|
||||||
</OnLeave>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
|
||||||
</Ui>
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
local ColumnWidthConstraints = {
|
|
||||||
Fill = 1, -- Width will be distributed by available space.
|
|
||||||
Fixed = 2, -- Width is specified when initializing the column.
|
|
||||||
};
|
|
||||||
|
|
||||||
-- Any row or cell is expected to initialize itself in terms of the row data. The dataIndex is provided
|
|
||||||
-- in case the derived mixin needs to make additional CAPI calls involving it's relative index. The row
|
|
||||||
-- data may also be needed for a tooltip, so it will be assigned to the row and cells on update.
|
|
||||||
TableBuilderElementMixin = {};
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderElementMixin:Init(...)
|
|
||||||
end
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderElementMixin:Populate(rowData, dataIndex)
|
|
||||||
end
|
|
||||||
|
|
||||||
TableBuilderCellMixin = CreateFromMixins(TableBuilderElementMixin);
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderCellMixin:OnLineEnter()
|
|
||||||
end
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderCellMixin:OnLineLeave()
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
TableBuilderRowMixin = CreateFromMixins(TableBuilderElementMixin);
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderRowMixin:OnLineEnter()
|
|
||||||
end
|
|
||||||
|
|
||||||
--Derive
|
|
||||||
function TableBuilderRowMixin:OnLineLeave()
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function TableBuilderRowMixin:OnEnter()
|
|
||||||
self:OnLineEnter();
|
|
||||||
for i, cell in ipairs(self.cells) do
|
|
||||||
cell:OnLineEnter();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderRowMixin:OnLeave()
|
|
||||||
self:OnLineLeave();
|
|
||||||
for i, cell in ipairs(self.cells) do
|
|
||||||
cell:OnLineLeave();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Defines an entire column within the table builder, by default a column's sizing constraints are set to fill.
|
|
||||||
TableBuilderColumnMixin = {};
|
|
||||||
function TableBuilderColumnMixin:Init(table)
|
|
||||||
self.cells = {};
|
|
||||||
self.table = table;
|
|
||||||
|
|
||||||
local fillCoefficient = 1.0;
|
|
||||||
local padding = 0;
|
|
||||||
self:SetFillConstraints(fillCoefficient, padding);
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Constructs the header frame with an optional initializer.
|
|
||||||
function TableBuilderColumnMixin:ConstructHeader(templateType, template, ...)
|
|
||||||
local frame = self.table:ConstructHeader(templateType, template);
|
|
||||||
self.headerFrame = frame;
|
|
||||||
if frame.Init then
|
|
||||||
frame:Init(...);
|
|
||||||
end
|
|
||||||
frame:Show();
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Constructs cells corresponding to each row with an optional initializer.
|
|
||||||
function TableBuilderColumnMixin:ConstructCells(templateType, template, ...)
|
|
||||||
local cells = self.table:ConstructCells(templateType, template);
|
|
||||||
self.cells = cells;
|
|
||||||
for k, cell in pairs(cells) do
|
|
||||||
if cell.Init then
|
|
||||||
cell:Init(...);
|
|
||||||
end
|
|
||||||
cell:Show();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetCellByRowIndex(rowIndex)
|
|
||||||
return self.cells[rowIndex];
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetFillCoefficient()
|
|
||||||
return self.fillCoefficient;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetFillCoefficient(fillCoefficient)
|
|
||||||
self.fillCoefficient = fillCoefficient;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetPadding()
|
|
||||||
return self.padding;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetPadding(padding)
|
|
||||||
self.padding = padding;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetCellPadding()
|
|
||||||
return self.leftCellPadding or 0, self.rightCellPadding or 0;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetCellPadding(leftCellPadding, rightCellPadding)
|
|
||||||
self.leftCellPadding = leftCellPadding;
|
|
||||||
self.rightCellPadding = rightCellPadding;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetHeaderFrame()
|
|
||||||
return self.headerFrame;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetHeaderFrame(headerFrame)
|
|
||||||
self.headerFrame = headerFrame;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetWidthConstraints()
|
|
||||||
return self.widthConstraints;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetFixedWidth()
|
|
||||||
return self.fixedWidth;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A header frame for the column is expected to be constructed or assigned prior to calling this.
|
|
||||||
-- See ConstructHeader() or SetHeaderFrame().
|
|
||||||
function TableBuilderColumnMixin:ConstrainToHeader(padding)
|
|
||||||
local header = self:GetHeaderFrame();
|
|
||||||
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
|
||||||
self:SetFixedConstraints(header:GetWidth(), padding or 0);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
|
|
||||||
self.widthConstraints = ColumnWidthConstraints.Fixed;
|
|
||||||
self.fixedWidth = fixedWidth;
|
|
||||||
self:SetFillCoefficient(0);
|
|
||||||
self:SetPadding(padding or 0);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetFillConstraints(fillCoefficient, padding)
|
|
||||||
self.widthConstraints = ColumnWidthConstraints.Fill;
|
|
||||||
self.fixedWidth = 0;
|
|
||||||
self:SetFillCoefficient(fillCoefficient);
|
|
||||||
self:SetPadding(padding or 0);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetCalculatedWidth(calculatedWidth)
|
|
||||||
self.calculatedWidth = calculatedWidth;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetCalculatedWidth()
|
|
||||||
return self.calculatedWidth;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetCellWidth()
|
|
||||||
local leftCellPadding, rightCellPadding = self:GetCellPadding();
|
|
||||||
return (self.calculatedWidth - leftCellPadding) - rightCellPadding;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetFullWidth()
|
|
||||||
return self:GetCalculatedWidth() + self:GetPadding();
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:SetDisplayUnderPreviousHeader(displayUnderPreviousHeader)
|
|
||||||
self.displayUnderPreviousHeader = displayUnderPreviousHeader;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderColumnMixin:GetDisplayUnderPreviousHeader()
|
|
||||||
return self.displayUnderPreviousHeader;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Constructs a table of frames within an existing set of row frames. These row frames could originate from
|
|
||||||
-- a hybrid scroll frame or statically fixed set. To populate the table, assign a data provider (CAPI or lua function)
|
|
||||||
-- that can retrieve an object by index (number).
|
|
||||||
TableBuilderMixin = {};
|
|
||||||
function TableBuilderMixin:Init(rows)
|
|
||||||
self.columns = {};
|
|
||||||
self.leftMargin = 0;
|
|
||||||
self.rightMargin = 0;
|
|
||||||
self.columnHeaderOverlap = 0;
|
|
||||||
self.tableWidth = 0;
|
|
||||||
self.headerPoolCollection = CreateFramePoolCollection();
|
|
||||||
self:SetRows(rows);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetDataProvider()
|
|
||||||
return self.dataProvider;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:SetDataProvider(dataProvider)
|
|
||||||
self.dataProvider = dataProvider;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetDataProviderData(dataIndex)
|
|
||||||
local dataProvider = self:GetDataProvider();
|
|
||||||
return dataProvider and dataProvider(dataIndex) or nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Controls the margins of the left-most and right-most columns within the table.
|
|
||||||
function TableBuilderMixin:SetTableMargins(leftMargin, rightMargin)
|
|
||||||
rightMargin = rightMargin or leftMargin; -- Use leftMargin as the default for both.
|
|
||||||
self.leftMargin = leftMargin;
|
|
||||||
self.rightMargin = rightMargin;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Column headers overlap to make a consistent display.
|
|
||||||
function TableBuilderMixin:SetColumnHeaderOverlap(columnHeaderOverlap)
|
|
||||||
self.columnHeaderOverlap = columnHeaderOverlap;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Can be used to set the table width, particularly if no header frames are involved.
|
|
||||||
function TableBuilderMixin:SetTableWidth(tableWidth)
|
|
||||||
self.tableWidth = tableWidth;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetTableWidth()
|
|
||||||
return self.tableWidth;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetTableMargins()
|
|
||||||
return self.leftMargin, self.rightMargin;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetColumnHeaderOverlap()
|
|
||||||
return self.columnHeaderOverlap;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetColumns()
|
|
||||||
return self.columns;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetHeaderContainer()
|
|
||||||
return self.headerContainer;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:SetHeaderContainer(headerContainer)
|
|
||||||
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
|
||||||
self.headerContainer = headerContainer;
|
|
||||||
self:SetTableWidth(headerContainer:GetWidth());
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ReleaseRowPools()
|
|
||||||
local rows = self.rows;
|
|
||||||
if rows then
|
|
||||||
for k, row in pairs(rows) do
|
|
||||||
local poolCollection = row.poolCollection;
|
|
||||||
if poolCollection then
|
|
||||||
poolCollection:ReleaseAll();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:SetRows(rows)
|
|
||||||
-- Release any previous rows, though I can't imagine a case where the rows
|
|
||||||
-- are being exchanged.
|
|
||||||
self:ReleaseRowPools();
|
|
||||||
|
|
||||||
self.rows = rows;
|
|
||||||
for k, row in pairs(rows) do
|
|
||||||
if not row.poolCollection then
|
|
||||||
row.poolCollection = CreateFramePoolCollection();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetHeaderPoolCollection()
|
|
||||||
return self.headerPoolCollection;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetRows()
|
|
||||||
return self.rows;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ConstructHeader(templateType, template)
|
|
||||||
local headerContainer = self:GetHeaderContainer();
|
|
||||||
assert(headerContainer ~= nil, "A header container must be set with TableBuilderMixin:SetHeaderContainer before adding column headers.")
|
|
||||||
local headerPoolCollection = self:GetHeaderPoolCollection();
|
|
||||||
local pool = headerPoolCollection:GetOrCreatePool(templateType, headerContainer, template);
|
|
||||||
return pool:Acquire(template);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ConstructCells(templateType, template)
|
|
||||||
local cells = {};
|
|
||||||
for k, row in pairs(self:GetRows()) do
|
|
||||||
local pool = row.poolCollection:GetOrCreatePool(templateType, row, template);
|
|
||||||
local cell = pool:Acquire(template);
|
|
||||||
tinsert(cells, cell);
|
|
||||||
end
|
|
||||||
return cells;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:Arrange()
|
|
||||||
local columns = self:GetColumns();
|
|
||||||
if columns and #columns > 0 then
|
|
||||||
self:CalculateColumnSpacing();
|
|
||||||
self:ArrangeHeaders();
|
|
||||||
self:ArrangeCells();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:Reset()
|
|
||||||
self.columns = {};
|
|
||||||
self:GetHeaderPoolCollection():ReleaseAll();
|
|
||||||
self:ReleaseRowPools();
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:Populate(offset, count)
|
|
||||||
local dataProvider = self:GetDataProvider();
|
|
||||||
local columns = self:GetColumns();
|
|
||||||
for rowIndex = 1, count do
|
|
||||||
local dataIndex = rowIndex + offset;
|
|
||||||
local rowData = dataProvider(dataIndex);
|
|
||||||
if not rowData then
|
|
||||||
break;
|
|
||||||
end
|
|
||||||
|
|
||||||
local row = self:GetRowByIndex(rowIndex);
|
|
||||||
if row then
|
|
||||||
-- Data is assigned to the rows and elements so they can
|
|
||||||
-- access it later in tooltips.
|
|
||||||
row.rowData = rowData;
|
|
||||||
if row.Populate then
|
|
||||||
row:Populate(rowData, dataIndex);
|
|
||||||
end
|
|
||||||
|
|
||||||
for columnIndex, p in ipairs(columns) do
|
|
||||||
local cell = self:GetCellByIndex(rowIndex, columnIndex);
|
|
||||||
if cell.Populate then
|
|
||||||
cell.rowData = rowData;
|
|
||||||
cell:Populate(rowData, dataIndex);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetCellByIndex(rowIndex, index)
|
|
||||||
local row = self:GetRowByIndex(rowIndex);
|
|
||||||
return row.cells[index];
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:GetRowByIndex(rowIndex)
|
|
||||||
return self.rows[rowIndex];
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:AddColumn()
|
|
||||||
local column = CreateAndInitFromMixin(TableBuilderColumnMixin, self);
|
|
||||||
tinsert(self.columns, column);
|
|
||||||
return column;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:CalculateColumnSpacing()
|
|
||||||
-- The arrangement of frames is daisy-chained left to right. The margin on the left side
|
|
||||||
-- is created by adding the margin to it's anchor offset, and the margin on the right side
|
|
||||||
-- is created by subtracting space from the remaining fill space.
|
|
||||||
local columns = self:GetColumns();
|
|
||||||
local paddingTotal = 0;
|
|
||||||
local fillCoefficientTotal = 0;
|
|
||||||
local fixedWidthTotal = 0;
|
|
||||||
for columnIndex, column in ipairs(columns) do
|
|
||||||
if column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
|
|
||||||
fillCoefficientTotal = fillCoefficientTotal + column:GetFillCoefficient();
|
|
||||||
else
|
|
||||||
fixedWidthTotal = fixedWidthTotal + column:GetFixedWidth();
|
|
||||||
end
|
|
||||||
|
|
||||||
paddingTotal = paddingTotal + column:GetPadding();
|
|
||||||
end
|
|
||||||
|
|
||||||
local tableWidth = self:GetTableWidth();
|
|
||||||
local leftMargin, rightMargin = self:GetTableMargins();
|
|
||||||
local fillWidthTotal = tableWidth - paddingTotal - (leftMargin + rightMargin) - fixedWidthTotal;
|
|
||||||
for k, column in pairs(columns) do
|
|
||||||
if fillCoefficientTotal > 0 and column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
|
|
||||||
local fillRatio = column:GetFillCoefficient() / fillCoefficientTotal;
|
|
||||||
local width = fillRatio * fillWidthTotal;
|
|
||||||
column:SetCalculatedWidth(width);
|
|
||||||
else
|
|
||||||
local width = column:GetFixedWidth();
|
|
||||||
column:SetCalculatedWidth(width);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ArrangeHorizontally(frame, relativeTo, width, pointTop, pointRelativeTop, pointBottom, pointRelativeBottom, xOffset)
|
|
||||||
frame:SetPoint(pointTop, relativeTo, pointRelativeTop, xOffset, 0);
|
|
||||||
frame:SetPoint(pointBottom, relativeTo, pointRelativeBottom, xOffset, 0);
|
|
||||||
frame:SetWidth(width);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ArrangeHeaders()
|
|
||||||
local headerOverlap = self:GetColumnHeaderOverlap();
|
|
||||||
local leftMargin, rightMargin = self:GetTableMargins();
|
|
||||||
local columns = self:GetColumns();
|
|
||||||
|
|
||||||
-- Any trailing columns without headers should add to the width of the last column with a header.
|
|
||||||
local numColumns = #columns;
|
|
||||||
local lastActiveHeaderIndex = numColumns;
|
|
||||||
local trailingWidth = 0;
|
|
||||||
for i, column in ipairs(columns) do
|
|
||||||
if column:GetHeaderFrame() then
|
|
||||||
trailingWidth = 0;
|
|
||||||
lastActiveHeaderIndex = i;
|
|
||||||
else
|
|
||||||
trailingWidth = trailingWidth + column:GetFullWidth();
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local previousHeader = nil;
|
|
||||||
local accumulatedWidth = 0;
|
|
||||||
local columnIndex = 1;
|
|
||||||
|
|
||||||
while columnIndex <= numColumns do
|
|
||||||
local column = columns[columnIndex];
|
|
||||||
accumulatedWidth = accumulatedWidth + column:GetCalculatedWidth();
|
|
||||||
|
|
||||||
local isLastIndex = columnIndex == lastActiveHeaderIndex;
|
|
||||||
local header = column:GetHeaderFrame();
|
|
||||||
if header then
|
|
||||||
if isLastIndex then
|
|
||||||
accumulatedWidth = accumulatedWidth + trailingWidth;
|
|
||||||
else
|
|
||||||
for j = columnIndex + 1, #columns do
|
|
||||||
local nextColumn = columns[j];
|
|
||||||
if nextColumn:GetDisplayUnderPreviousHeader() then
|
|
||||||
columnIndex = columnIndex + 1;
|
|
||||||
accumulatedWidth = accumulatedWidth + nextColumn:GetFullWidth();
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if previousHeader == nil then
|
|
||||||
self:ArrangeHorizontally(header, header:GetParent(), accumulatedWidth, "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin);
|
|
||||||
else
|
|
||||||
self:ArrangeHorizontally(header, previousHeader, accumulatedWidth + headerOverlap, "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() - headerOverlap);
|
|
||||||
end
|
|
||||||
|
|
||||||
accumulatedWidth = 0;
|
|
||||||
previousHeader = header;
|
|
||||||
end
|
|
||||||
|
|
||||||
if isLastIndex then
|
|
||||||
break;
|
|
||||||
end
|
|
||||||
|
|
||||||
columnIndex = columnIndex + 1;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableBuilderMixin:ArrangeCells()
|
|
||||||
local columns = self:GetColumns();
|
|
||||||
local leftMargin, rightMargin = self:GetTableMargins();
|
|
||||||
for rowIndex, row in ipairs(self:GetRows()) do
|
|
||||||
local cells = {};
|
|
||||||
local height = row:GetHeight();
|
|
||||||
|
|
||||||
local column = columns[1];
|
|
||||||
local cell = column:GetCellByRowIndex(rowIndex);
|
|
||||||
tinsert(cells, cell);
|
|
||||||
cell:SetHeight(height);
|
|
||||||
local leftCellPadding, rightCellPadding = column:GetCellPadding();
|
|
||||||
|
|
||||||
self:ArrangeHorizontally(cell, row, column:GetCellWidth(), "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin + leftCellPadding);
|
|
||||||
|
|
||||||
local previousCell = cell;
|
|
||||||
local previousRightCellPadding = rightCellPadding;
|
|
||||||
for columnIndex = 2, #columns do
|
|
||||||
column = columns[columnIndex];
|
|
||||||
cell = column:GetCellByRowIndex(rowIndex);
|
|
||||||
tinsert(cells, cell);
|
|
||||||
cell:SetHeight(height);
|
|
||||||
leftCellPadding, rightCellPadding = column:GetCellPadding();
|
|
||||||
|
|
||||||
self:ArrangeHorizontally(cell, previousCell, column:GetCellWidth(), "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() + leftCellPadding + previousRightCellPadding);
|
|
||||||
previousCell = cell;
|
|
||||||
previousRightCellPadding = rightCellPadding;
|
|
||||||
end
|
|
||||||
|
|
||||||
row.cells = cells;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ... are additional mixins
|
|
||||||
function CreateTableBuilder(rows, ...)
|
|
||||||
local tableBuilder = CreateAndInitFromMixin(TableBuilderMixin, rows);
|
|
||||||
Mixin(tableBuilder, ...);
|
|
||||||
return tableBuilder;
|
|
||||||
end
|
|
||||||
@@ -1,697 +0,0 @@
|
|||||||
local tRemove = table.remove;
|
|
||||||
local tInsert = table.insert;
|
|
||||||
local tWipe = table.wipe;
|
|
||||||
|
|
||||||
TableUtil = {};
|
|
||||||
|
|
||||||
TableUtil.Constants =
|
|
||||||
{
|
|
||||||
AssociativePriorityTable = true,
|
|
||||||
ArraylikePriorityTable = false,
|
|
||||||
IsIndexTable = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ipairs_reverse(table)
|
|
||||||
local function Enumerator(table, index)
|
|
||||||
index = index - 1;
|
|
||||||
local value = table[index];
|
|
||||||
if value ~= nil then
|
|
||||||
return index, value;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return Enumerator, table, #table + 1;
|
|
||||||
end
|
|
||||||
|
|
||||||
function CreateTableEnumerator(tbl, minIndex, maxIndex)
|
|
||||||
minIndex = minIndex and (minIndex - 1) or 0;
|
|
||||||
maxIndex = maxIndex or math.huge;
|
|
||||||
|
|
||||||
local function Enumerator(tbl, index)
|
|
||||||
index = index + 1;
|
|
||||||
if index <= maxIndex then
|
|
||||||
local value = tbl[index];
|
|
||||||
if value ~= nil then
|
|
||||||
return index, value;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Enumerator, tbl, minIndex;
|
|
||||||
end
|
|
||||||
|
|
||||||
function CreateTableReverseEnumerator(tbl, minIndex, maxIndex)
|
|
||||||
minIndex = minIndex or 1;
|
|
||||||
maxIndex = (maxIndex or #tbl) + 1;
|
|
||||||
|
|
||||||
local function Enumerator(tbl, index)
|
|
||||||
index = index - 1;
|
|
||||||
if index >= minIndex then
|
|
||||||
local value = tbl[index];
|
|
||||||
if value ~= nil then
|
|
||||||
return index, value;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Enumerator, tbl, maxIndex;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tCount(t)
|
|
||||||
if type(t) ~= "table" then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
local size = 0
|
|
||||||
for _ in pairs(t) do
|
|
||||||
size = size + 1
|
|
||||||
end
|
|
||||||
return size
|
|
||||||
end
|
|
||||||
|
|
||||||
function tDeleteItem(tbl, item)
|
|
||||||
local size = #tbl;
|
|
||||||
local index = size;
|
|
||||||
while index > 0 do
|
|
||||||
if item == tbl[index] then
|
|
||||||
tRemove(tbl, index);
|
|
||||||
end
|
|
||||||
index = index - 1;
|
|
||||||
end
|
|
||||||
return size - #tbl;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tIndexOf(tbl, item)
|
|
||||||
for i, v in ipairs(tbl) do
|
|
||||||
if item == v then
|
|
||||||
return i;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function tContains(tbl, item)
|
|
||||||
local index = 1;
|
|
||||||
while tbl[index] do
|
|
||||||
if ( item == tbl[index] ) then
|
|
||||||
return 1;
|
|
||||||
end
|
|
||||||
index = index + 1;
|
|
||||||
end
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tContainsValue(tbl, item)
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
if item == v then
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.ContainsAllKeys(lhsTable, rhsTable)
|
|
||||||
for key, _ in pairs(lhsTable) do
|
|
||||||
if rhsTable[key] == nil then
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- Check for any keys that are in rhsTable and not lhsTable.
|
|
||||||
for key, _ in pairs(rhsTable) do
|
|
||||||
if lhsTable[key] == nil then
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.CompareValuesAsKeys(lhsTable, rhsTable, valueToKeyOp)
|
|
||||||
local lhsKeys = CopyTransformedValuesAsKeys(lhsTable, valueToKeyOp);
|
|
||||||
local rhsKeys = CopyTransformedValuesAsKeys(rhsTable, valueToKeyOp);
|
|
||||||
return TableUtil.ContainsAllKeys(lhsKeys, rhsKeys)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- This is a deep compare on the values of the table (based on depth) but not a deep comparison
|
|
||||||
-- of the keys, as this would be an expensive check and won't be necessary in most cases.
|
|
||||||
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
|
|
||||||
|
|
||||||
-- Check for any keys that are in rhsTable and not lhsTable.
|
|
||||||
for key, value in pairs(rhsTable) do
|
|
||||||
if lhsTable[key] == nil then
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tInvert(tbl)
|
|
||||||
local inverted = {};
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
inverted[v] = k;
|
|
||||||
end
|
|
||||||
return inverted;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.TrySet(tbl, key)
|
|
||||||
if not tbl[key] then
|
|
||||||
tbl[key] = true;
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.CopyUnique(tbl, isIndexTable)
|
|
||||||
local found = {};
|
|
||||||
local function FilterPredicate(value)
|
|
||||||
return TableUtil.TrySet(found, value);
|
|
||||||
end
|
|
||||||
|
|
||||||
return tFilter(tbl, FilterPredicate, isIndexTable);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.CopyUniqueByPredicate(tbl, isIndexTable, unaryPredicate)
|
|
||||||
local found = {};
|
|
||||||
local function FilterPredicate(value)
|
|
||||||
return TableUtil.TrySet(found, unaryPredicate(value));
|
|
||||||
end
|
|
||||||
|
|
||||||
return tFilter(tbl, FilterPredicate, isIndexTable);
|
|
||||||
end
|
|
||||||
|
|
||||||
function tFilter(tbl, pred, isIndexTable)
|
|
||||||
local out = {};
|
|
||||||
|
|
||||||
if (isIndexTable) then
|
|
||||||
local currentIndex = 1;
|
|
||||||
for i, v in ipairs(tbl) do
|
|
||||||
if (pred(v)) then
|
|
||||||
out[currentIndex] = v;
|
|
||||||
currentIndex = currentIndex + 1;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
if (pred(v)) then
|
|
||||||
out[k] = v;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return out;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tAppendAll(table, addedArray)
|
|
||||||
for i, element in ipairs(addedArray) do
|
|
||||||
tinsert(table, element);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function tInsertUnique(tbl, item)
|
|
||||||
if not tContains(tbl, item) then
|
|
||||||
table.insert(tbl, item);
|
|
||||||
return #tbl;
|
|
||||||
end
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function tUnorderedRemove(tbl, index)
|
|
||||||
if index ~= #tbl then
|
|
||||||
tbl[index] = tbl[#tbl];
|
|
||||||
end
|
|
||||||
|
|
||||||
tRemove(tbl);
|
|
||||||
end
|
|
||||||
|
|
||||||
function CopyTable(settings, shallow)
|
|
||||||
local copy = {};
|
|
||||||
for k, v in pairs(settings) do
|
|
||||||
if type(v) == "table" and not shallow then
|
|
||||||
copy[k] = CopyTable(v);
|
|
||||||
else
|
|
||||||
copy[k] = v;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return copy;
|
|
||||||
end
|
|
||||||
|
|
||||||
function MergeTable(destination, source)
|
|
||||||
for k, v in pairs(source) do
|
|
||||||
destination[k] = v;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Useful if there are external references to a table but we want to set
|
|
||||||
-- that table's key-value pairs to be exactly the same as another table's key-value pairs.
|
|
||||||
function SetTablePairsToTable(destination, source)
|
|
||||||
tWipe(destination);
|
|
||||||
MergeTable(destination, source);
|
|
||||||
end
|
|
||||||
|
|
||||||
function Accumulate(tbl)
|
|
||||||
local count = 0;
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
count = count + v;
|
|
||||||
end
|
|
||||||
return count;
|
|
||||||
end
|
|
||||||
|
|
||||||
function AccumulateOp(tbl, op)
|
|
||||||
local count = 0;
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
count = count + op(v);
|
|
||||||
end
|
|
||||||
return count;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.Execute(tbl, op)
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
op(v);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.ExecuteUntil(tbl, op)
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
local operationResult = op(v);
|
|
||||||
if operationResult then
|
|
||||||
return operationResult;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.Transform(tbl, op)
|
|
||||||
local result = {};
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
table.insert(result, op(v));
|
|
||||||
end
|
|
||||||
return result;
|
|
||||||
end
|
|
||||||
|
|
||||||
function ContainsIf(tbl, pred)
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
if (pred(v)) then
|
|
||||||
return true;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FindInTableIf(tbl, pred)
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
if (pred(v)) then
|
|
||||||
return k, v;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function FindValueInTableIf(tbl, pred)
|
|
||||||
local _, value = FindInTableIf(tbl, pred);
|
|
||||||
return value;
|
|
||||||
end
|
|
||||||
|
|
||||||
local function FindSortedIndexImplementation(tbl, searchComparison, startIndex, rangeStart, rangeEnd)
|
|
||||||
local comparisonResult = searchComparison(tbl[startIndex]);
|
|
||||||
if comparisonResult == 0 then
|
|
||||||
return startIndex;
|
|
||||||
end
|
|
||||||
|
|
||||||
if comparisonResult > 0 then
|
|
||||||
if startIndex >= rangeEnd then
|
|
||||||
return startIndex + 1;
|
|
||||||
end
|
|
||||||
|
|
||||||
rangeStart = startIndex + 1;
|
|
||||||
return FindSortedIndexImplementation(tbl, searchComparison, startIndex + math.ceil((rangeEnd - startIndex) / 2), rangeStart, rangeEnd);
|
|
||||||
end
|
|
||||||
|
|
||||||
-- comparisonResult < 0
|
|
||||||
if startIndex <= rangeStart then
|
|
||||||
return startIndex;
|
|
||||||
end
|
|
||||||
|
|
||||||
rangeEnd = startIndex - 1;
|
|
||||||
return FindSortedIndexImplementation(tbl, searchComparison, math.floor(startIndex / 2), rangeStart, rangeEnd);
|
|
||||||
end
|
|
||||||
|
|
||||||
function FindSortedIndex(tbl, searchComparison)
|
|
||||||
local numTable = #tbl;
|
|
||||||
local startingIndex = math.ceil(numTable / 2);
|
|
||||||
if startingIndex == 0 then
|
|
||||||
return 1;
|
|
||||||
end
|
|
||||||
|
|
||||||
return FindSortedIndexImplementation(tbl, searchComparison, startingIndex, 1, numTable);
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableIsEmpty(tbl)
|
|
||||||
return next(tbl) == nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableHasAnyEntries(tbl)
|
|
||||||
return next(tbl) ~= nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
function CopyValuesAsKeys(tbl)
|
|
||||||
local output = {};
|
|
||||||
for k, v in ipairs(tbl) do
|
|
||||||
output[v] = v;
|
|
||||||
end
|
|
||||||
return output;
|
|
||||||
end
|
|
||||||
|
|
||||||
function CopyTransformedValuesAsKeys(tbl, transformOp)
|
|
||||||
local output = {};
|
|
||||||
for _, v in ipairs(tbl) do
|
|
||||||
output[transformOp(v)] = v;
|
|
||||||
end
|
|
||||||
return output;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Addresses the problem where nil values within a varargs list are not preserved when constructing
|
|
||||||
-- a table, resulting a table with a smaller size than expected. Should be paired with a call to
|
|
||||||
-- SafeUnpack when unpacking the table.
|
|
||||||
function SafePack(...)
|
|
||||||
local tbl = { ... };
|
|
||||||
tbl.n = select("#", ...);
|
|
||||||
return tbl;
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Upacks a table that was constructed using SafePack.
|
|
||||||
function SafeUnpack(tbl, startIndex)
|
|
||||||
return unpack(tbl, startIndex or 1, tbl.n);
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Returns the length of a table, accounting for the possibility of a table constructed using SafePack.
|
|
||||||
function SafeLength(tbl)
|
|
||||||
if not tbl then
|
|
||||||
return 0;
|
|
||||||
end
|
|
||||||
|
|
||||||
local operatorCount = #tbl;
|
|
||||||
local safePackCount = tbl.n;
|
|
||||||
|
|
||||||
if safePackCount and operatorCount ~= safePackCount then
|
|
||||||
return safePackCount;
|
|
||||||
end
|
|
||||||
|
|
||||||
return operatorCount;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetOrCreateTableEntry(table, key, defaultValue)
|
|
||||||
local currentValue = table[key];
|
|
||||||
local isNewValue = (currentValue == nil);
|
|
||||||
if isNewValue then
|
|
||||||
if defaultValue ~= nil then
|
|
||||||
currentValue = defaultValue;
|
|
||||||
else
|
|
||||||
currentValue = {};
|
|
||||||
end
|
|
||||||
table[key] = currentValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
return currentValue, isNewValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetOrCreateTableEntryByCallback(table, key, callback)
|
|
||||||
local currentValue = table[key];
|
|
||||||
local isNewValue = (currentValue == nil);
|
|
||||||
if isNewValue then
|
|
||||||
currentValue = callback(key);
|
|
||||||
table[key] = currentValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
return currentValue, isNewValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetRandomArrayEntry(array)
|
|
||||||
return array[math.random(1, #array)];
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetRandomTableValue(tbl)
|
|
||||||
local value;
|
|
||||||
local n = 0;
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
n = n + 1;
|
|
||||||
local r = math.random();
|
|
||||||
if r <= (1 / n) then
|
|
||||||
value = v;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return value;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetKeysArray(tbl)
|
|
||||||
local keysArray = {};
|
|
||||||
for key in pairs(tbl) do
|
|
||||||
tInsert(keysArray, key);
|
|
||||||
end
|
|
||||||
|
|
||||||
return keysArray;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetValuesArray(tbl)
|
|
||||||
local valuesArray = {};
|
|
||||||
for key, value in pairs(tbl) do
|
|
||||||
tInsert(valuesArray, value);
|
|
||||||
end
|
|
||||||
|
|
||||||
return valuesArray;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetPairsArray(tbl)
|
|
||||||
local pairsArray = {};
|
|
||||||
for key, value in pairs(tbl) do
|
|
||||||
tInsert(pairsArray, { key = key, value = value, });
|
|
||||||
end
|
|
||||||
|
|
||||||
return pairsArray;
|
|
||||||
end
|
|
||||||
|
|
||||||
function SwapTableEntries(lhsTable, rhsTable, key)
|
|
||||||
local lhsValue = lhsTable[key];
|
|
||||||
lhsTable[key] = rhsTable[key];
|
|
||||||
rhsTable[key] = lhsValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
function GetKeysArraySortedByValue(tbl)
|
|
||||||
local keysArray = GetKeysArray(tbl);
|
|
||||||
|
|
||||||
table.sort(keysArray, function(a, b)
|
|
||||||
return tbl[a] < tbl[b];
|
|
||||||
end);
|
|
||||||
|
|
||||||
return keysArray;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.OperateOnKeys(tbl, operation)
|
|
||||||
for key, value in pairs(tbl) do
|
|
||||||
operation(key);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.GetTableValueListFromEnumeration(tableKey, ...)
|
|
||||||
local values = {};
|
|
||||||
for enumerationKey, tbl in ... do
|
|
||||||
table.insert(values, tbl[tableKey]);
|
|
||||||
end
|
|
||||||
|
|
||||||
return values;
|
|
||||||
end
|
|
||||||
|
|
||||||
function TableUtil.GetHighestNumericalValueInTable(table)
|
|
||||||
local highestValue = nil;
|
|
||||||
for key, value in pairs(table) do
|
|
||||||
if type(value) == "number" and (not highestValue or value > highestValue) then
|
|
||||||
highestValue = value;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return highestValue;
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
This utility creates and returns a table of elements that are sorted by value "priority".
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
comparator - comparator(A, B) returns whether A has higher priority than B
|
|
||||||
isAssociative - whether the table should be associative (true) or array-like (false). Only values have priorities for associative tables, not keys.
|
|
||||||
|
|
||||||
Return usage:
|
|
||||||
t[k]/t:Get(k) - returns the value stored with key/index k
|
|
||||||
t[k] = v (Associative only) - stores the value v at key k, sorting accordingly
|
|
||||||
t:Insert(v) (Array-like only) - inserts the value v, sorting accordingly
|
|
||||||
t:Remove(k) - removes the element at key/index k
|
|
||||||
t:Iterate(cb) - calls function cb on each key/index value pair in sorted priority order. !!WARNING!! This must be used instead of pairs(t)/ipairs(t)
|
|
||||||
t:GetTop() - returns the highest priority element
|
|
||||||
t:Pop() - returns and removes the highest priority element
|
|
||||||
t:GetBottom() - returns the lowest priority element
|
|
||||||
t:Size() - returns the number of stored elements. !!WARNING!! This must be used instead of #t
|
|
||||||
t:Clear() - removes all elements
|
|
||||||
--]]
|
|
||||||
function TableUtil.CreatePriorityTable(comparator, isAssociative)
|
|
||||||
local sortedArray = {};
|
|
||||||
|
|
||||||
local ShiftPositionMap;
|
|
||||||
local keyToPosMap;
|
|
||||||
if isAssociative then
|
|
||||||
keyToPosMap = {};
|
|
||||||
|
|
||||||
ShiftPositionMap = function(position, shiftUp)
|
|
||||||
local mod = shiftUp and 1 or -1;
|
|
||||||
for k, p in pairs(keyToPosMap) do
|
|
||||||
if p >= position then
|
|
||||||
keyToPosMap[k] = p + mod;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function SortedInsert(k, v)
|
|
||||||
if isAssociative then
|
|
||||||
local prevPos = keyToPosMap[k];
|
|
||||||
if prevPos then
|
|
||||||
tRemove(sortedArray, prevPos);
|
|
||||||
local shiftUp = false;
|
|
||||||
ShiftPositionMap(prevPos, shiftUp);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local top = (#sortedArray > 0) and (#sortedArray + 1) or 1;
|
|
||||||
local bottom = 1;
|
|
||||||
while top ~= bottom do
|
|
||||||
local mid = math.floor((top - bottom) / 2) + bottom;
|
|
||||||
if not comparator(v, sortedArray[mid]) then
|
|
||||||
bottom = mid + 1;
|
|
||||||
else
|
|
||||||
top = mid;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local idx = bottom;
|
|
||||||
tInsert(sortedArray, idx, v);
|
|
||||||
|
|
||||||
if isAssociative then
|
|
||||||
local shiftUp = true;
|
|
||||||
ShiftPositionMap(idx, shiftUp);
|
|
||||||
keyToPosMap[k] = idx;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local t = {};
|
|
||||||
|
|
||||||
function t:Get(k)
|
|
||||||
local key = isAssociative and keyToPosMap[k] or k;
|
|
||||||
return sortedArray[key];
|
|
||||||
end
|
|
||||||
|
|
||||||
if not isAssociative then
|
|
||||||
function t:Insert(v)
|
|
||||||
SortedInsert(nil, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:Remove(k)
|
|
||||||
local key = isAssociative and keyToPosMap[k] or k;
|
|
||||||
tRemove(sortedArray, key);
|
|
||||||
if isAssociative then
|
|
||||||
keyToPosMap[k] = nil;
|
|
||||||
local shiftUp = false;
|
|
||||||
ShiftPositionMap(key, shiftUp);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:GetTop()
|
|
||||||
return #sortedArray > 0 and sortedArray[1];
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:GetBottom()
|
|
||||||
return #sortedArray > 0 and sortedArray[#sortedArray];
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:Pop()
|
|
||||||
if t:Size() == 0 then
|
|
||||||
return nil;
|
|
||||||
end
|
|
||||||
|
|
||||||
local top = t:GetTop();
|
|
||||||
local removalKey = isAssociative and tInvert(keyToPosMap)[1] or 1;
|
|
||||||
t:Remove(removalKey);
|
|
||||||
return top;
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:Iterate(callback)
|
|
||||||
local posToKeyMap = isAssociative and tInvert(keyToPosMap) or nil;
|
|
||||||
for pos, v in ipairs(sortedArray) do
|
|
||||||
local key = isAssociative and posToKeyMap[pos] or pos;
|
|
||||||
local done = callback(key, v);
|
|
||||||
if done then
|
|
||||||
return;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:Size()
|
|
||||||
return #sortedArray;
|
|
||||||
end
|
|
||||||
|
|
||||||
function t:Clear()
|
|
||||||
sortedArray = {};
|
|
||||||
if isAssociative then
|
|
||||||
keyToPosMap = {};
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local mt =
|
|
||||||
{
|
|
||||||
__index = function(t, k)
|
|
||||||
return t:Get(k);
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
if isAssociative then
|
|
||||||
mt.__newindex = function(t, k, v)
|
|
||||||
if v ~= nil then
|
|
||||||
SortedInsert(k, v);
|
|
||||||
else
|
|
||||||
t:Remove(k);
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
mt.__newindex = function(t, k, v)
|
|
||||||
error("Attempted to assign a value to an array-like priority queue index. Use Insert()/Remove() instead.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
setmetatable(t, mt);
|
|
||||||
|
|
||||||
return t;
|
|
||||||
end
|
|
||||||
|
|
||||||
function DeepMergeTable(destination, source)
|
|
||||||
for k, v in pairs(source) do
|
|
||||||
if type(v) == "table" then
|
|
||||||
if type(destination[k]) == "table" then
|
|
||||||
DeepMergeTable(destination[k], v)
|
|
||||||
else
|
|
||||||
destination[k] = CopyTable(v)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
destination[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,850 +0,0 @@
|
|||||||
local ADVENTURE_TYPE = {
|
|
||||||
PRIMARY = 1,
|
|
||||||
SECONDARY = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
local ADVENTURE_CATEGORY = {
|
|
||||||
DEV = 0,
|
|
||||||
NOVICE_TIPS = 1,
|
|
||||||
WINTERGRASP = 2,
|
|
||||||
CATEGORY_BOSSES = 3,
|
|
||||||
SPEC_INFO = 4,
|
|
||||||
QUEST_DAILY_CRYSTALS = 5,
|
|
||||||
BATTLE_PASS_QUEST_DAILY = 6,
|
|
||||||
QUEST_RENEGADE = 7,
|
|
||||||
QUEST_SIRUS = 8,
|
|
||||||
SEASONAL_EVENT = 9,
|
|
||||||
QUEST_WEEKLY_PVE = 10,
|
|
||||||
BRAWL = 11,
|
|
||||||
BATTLE_PASS_QUEST_WEEKLY = 12,
|
|
||||||
RAID_NEWEST_NORMAL = 13,
|
|
||||||
RAID_NEWEST_HEROIC = 14,
|
|
||||||
WEEKEND_EVENT = 15,
|
|
||||||
QUEST_DAILY_ISLAND = 16,
|
|
||||||
ZONE_QUEST = 17,
|
|
||||||
TUTORIAL_COINS = 100,
|
|
||||||
MISC = 128,
|
|
||||||
}
|
|
||||||
|
|
||||||
local ADVENTURE_CATEGORY_PRIORITY = {
|
|
||||||
[ADVENTURE_CATEGORY.DEV] = 0,
|
|
||||||
[ADVENTURE_CATEGORY.NOVICE_TIPS] = 1,
|
|
||||||
[ADVENTURE_CATEGORY.WINTERGRASP] = 1,
|
|
||||||
[ADVENTURE_CATEGORY.CATEGORY_BOSSES] = 1,
|
|
||||||
[ADVENTURE_CATEGORY.SPEC_INFO] = 3,
|
|
||||||
[ADVENTURE_CATEGORY.QUEST_DAILY_CRYSTALS] = 1,
|
|
||||||
[ADVENTURE_CATEGORY.BATTLE_PASS_QUEST_DAILY] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.QUEST_RENEGADE] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.QUEST_SIRUS] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.SEASONAL_EVENT] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.QUEST_WEEKLY_PVE] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.BRAWL] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.BATTLE_PASS_QUEST_WEEKLY] = 5,
|
|
||||||
[ADVENTURE_CATEGORY.RAID_NEWEST_NORMAL] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.RAID_NEWEST_HEROIC] = 2,
|
|
||||||
[ADVENTURE_CATEGORY.WEEKEND_EVENT] = 4,
|
|
||||||
[ADVENTURE_CATEGORY.QUEST_DAILY_ISLAND] = 5,
|
|
||||||
[ADVENTURE_CATEGORY.ZONE_QUEST] = 5,
|
|
||||||
[ADVENTURE_CATEGORY.TUTORIAL_COINS] = 6,
|
|
||||||
[ADVENTURE_CATEGORY.MISC] = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
local CONDITION_CHECK = {
|
|
||||||
PLAYER_IN_GM_MODE = 0,
|
|
||||||
PLAYER_FACTION = 1,
|
|
||||||
PLAYER_LEVEL = 2,
|
|
||||||
PLAYER_HAS_BUFF = 3,
|
|
||||||
PLAYER_HAS_DEBUFF = 4,
|
|
||||||
PLAYER_HAS_ITEM = 5,
|
|
||||||
PLAYER_SPELL_KNOWN = 6,
|
|
||||||
PET_SPELL_KNOWN = 7,
|
|
||||||
REALM_STAGE = 8,
|
|
||||||
REALM_DUNGEON_ACTUAL = 9,
|
|
||||||
REALM_GAME_EVENT_ACTIVE = 10,
|
|
||||||
PLAYER_QUEST_COMPLETE = 11,
|
|
||||||
PLAYER_ACHIEVEMENT_COMPLETE = 12,
|
|
||||||
PLAYER_BATTLEPASS_DAILY_COUNT = 13,
|
|
||||||
PLAYER_BATTLEPASS_WEEKLY_COUNT = 14,
|
|
||||||
REALM_WINTERGRASP_REGISTRATION_ACTIVE = 15,
|
|
||||||
REALM_BRAWL_ACTIVE = 16,
|
|
||||||
PLAYER_LFG_DUNGEON_JOINABLE = 17,
|
|
||||||
PLAYER_LFG_MINIGAME_JOINABLE = 18,
|
|
||||||
PLAYER_CUSTOM_VALUE = 19,
|
|
||||||
PLAYER_HAS_QUEST_IN_QUESTLOG = 20,
|
|
||||||
PLAYER_TALENT_GROUPS_COUNT = 21,
|
|
||||||
REALM_ID = 22,
|
|
||||||
PLAYER_IN_CHALLENGE_MODE = 23,
|
|
||||||
}
|
|
||||||
|
|
||||||
local CONDITION_OPERATOR = {
|
|
||||||
COMP_TYPE_EQ = 0,
|
|
||||||
COMP_TYPE_HIGH = 1,
|
|
||||||
COMP_TYPE_LOW = 2,
|
|
||||||
COMP_TYPE_HIGH_EQ = 3,
|
|
||||||
COMP_TYPE_LOW_EQ = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
local ACTION_BUTTON_TYPE = {
|
|
||||||
NONE = 0,
|
|
||||||
QUEST_START = 1,
|
|
||||||
UI_PVP_ARENA_RATING = 2,
|
|
||||||
UI_PVP_HONOR_RANDOM = 3,
|
|
||||||
UI_PVP_HONOR_WINTERGRASP = 4,
|
|
||||||
UI_PVP_HONOR_BRAWL = 5,
|
|
||||||
UI_PVE_LFG_DUNGEON_RANDOM = 6,
|
|
||||||
UI_PVE_LFG_DUNGEON_ID = 7,
|
|
||||||
UI_PVE_LFG_MINIGAME_ID = 8,
|
|
||||||
UI_EJ_DUNGEON_ID = 9,
|
|
||||||
UI_BATTLEPASS_QUESTS = 10,
|
|
||||||
UI_HEAD_HUNTING = 11,
|
|
||||||
}
|
|
||||||
|
|
||||||
local error = error
|
|
||||||
local ipairs = ipairs
|
|
||||||
local pairs = pairs
|
|
||||||
local select = select
|
|
||||||
local tonumber = tonumber
|
|
||||||
local type = type
|
|
||||||
local mathmax, mathmin, mathrandom = math.max, math.min, math.random
|
|
||||||
local strconcat, strformat, strgsub = strconcat, string.format, string.gsub
|
|
||||||
local tinsert, tsort, twipe, tCompare = table.insert, table.sort, table.wipe, tCompare
|
|
||||||
|
|
||||||
local CanQueueForWintergrasp = CanQueueForWintergrasp
|
|
||||||
local GetAchievementInfo = GetAchievementInfo
|
|
||||||
local GetFramerate = GetFramerate
|
|
||||||
local GetItemCount = GetItemCount
|
|
||||||
local GetQuestLinkByID = GetQuestLinkByID
|
|
||||||
local GetSpellInfo = GetSpellInfo
|
|
||||||
local GetTitleForQuestID = GetTitleForQuestID
|
|
||||||
local IsLFGDungeonJoinable = IsLFGDungeonJoinable
|
|
||||||
local IsQuestCompleted = IsQuestCompleted
|
|
||||||
local IsQuestDataCached = IsQuestDataCached
|
|
||||||
local IsSpellKnown = IsSpellKnown
|
|
||||||
local QueryQuestStart = QueryQuestStart
|
|
||||||
local UnitFactionGroup = UnitFactionGroup
|
|
||||||
local UnitLevel = UnitLevel
|
|
||||||
local debugprofilestop = debugprofilestop
|
|
||||||
|
|
||||||
local FireCustomClientEvent = FireCustomClientEvent
|
|
||||||
local GetCurrentBrawlID = C_PvP.GetCurrentBrawlID
|
|
||||||
local IsGameEventActive = IsGameEventActive
|
|
||||||
local IsGMAccount = IsGMAccount
|
|
||||||
local IsInterfaceDevClient = IsInterfaceDevClient
|
|
||||||
--local RequestQuestCacheByID = RequestQuestCacheByID
|
|
||||||
|
|
||||||
local AJ_ACTION_TEXT_JOIN_BATTLE = AJ_ACTION_TEXT_JOIN_BATTLE
|
|
||||||
local AJ_ACTION_TEXT_JOIN_GROUP = AJ_ACTION_TEXT_JOIN_GROUP
|
|
||||||
local AJ_ACTION_TEXT_OPEN_EJ = AJ_ACTION_TEXT_OPEN_EJ
|
|
||||||
local AJ_ACTION_TEXT_SHOW_QUEST = AJ_ACTION_TEXT_SHOW_QUEST
|
|
||||||
local AJ_ACTION_TEXT_START_HUNT = AJ_ACTION_TEXT_START_HUNT
|
|
||||||
local AJ_ACTION_TEXT_START_QUEST = AJ_ACTION_TEXT_START_QUEST
|
|
||||||
local AJ_PRIMARY_REWARD_TEXT = AJ_PRIMARY_REWARD_TEXT
|
|
||||||
local UNKNOWN = UNKNOWN
|
|
||||||
|
|
||||||
local COLOR_PATTERN = strformat("^|cff%s|H", string.rep("[0-9A-Fa-f]", 6))
|
|
||||||
local QUESTION_MARK_ICON = strgsub(QUESTION_MARK_ICON, "%.BLP$", "")
|
|
||||||
|
|
||||||
local NUM_SECONDARY_SUGGESTIONS = 2
|
|
||||||
local MAX_INT = 2^32/2-1
|
|
||||||
|
|
||||||
local PRIVATE = {
|
|
||||||
DIRTY = true,
|
|
||||||
|
|
||||||
FRAMETIME_TARGET = 1 / 55,
|
|
||||||
FRAMETIME_AVAILABLE = 8,
|
|
||||||
FRAMETIME_RESERVE = 4,
|
|
||||||
|
|
||||||
REGISTRY = {},
|
|
||||||
SUGGESTIONS = {},
|
|
||||||
PRIMARY_OFFSET_INDEX = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
PRIVATE.eventHandler = CreateFrame("Frame")
|
|
||||||
PRIVATE.eventHandler:Hide()
|
|
||||||
PRIVATE.eventHandler:SetScript("OnUpdate", function(self, elapsed)
|
|
||||||
local frametimeStep = PRIVATE.FRAMETIME_TARGET - elapsed
|
|
||||||
|
|
||||||
if frametimeStep ~= 0 then
|
|
||||||
frametimeStep = frametimeStep * 1000
|
|
||||||
PRIVATE.FRAMETIME_AVAILABLE = mathmax(5, PRIVATE.FRAMETIME_AVAILABLE + frametimeStep)
|
|
||||||
end
|
|
||||||
|
|
||||||
if PRIVATE.BUILD_COROUTINE then
|
|
||||||
PRIVATE.BUILD_COROUTINE_TIMESTAMP = debugprofilestop()
|
|
||||||
local status, result, progress = coroutine.resume(PRIVATE.BUILD_COROUTINE)
|
|
||||||
|
|
||||||
if not status then
|
|
||||||
PRIVATE.BUILD_COROUTINE = nil
|
|
||||||
self:Hide()
|
|
||||||
error(result, 2)
|
|
||||||
elseif not result then
|
|
||||||
if PRIVATE.BUILD_COROUTINE_DEBUG then print("AJ_BUILD_COROUTINE", progress) end
|
|
||||||
else
|
|
||||||
PRIVATE.BUILD_COROUTINE = nil
|
|
||||||
self:Hide()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
self:Hide()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
PRIVATE.Initialize = function()
|
|
||||||
if PRIVATE.initialized then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.initialized = true
|
|
||||||
|
|
||||||
_G.ADVENTURE_TYPE = nil
|
|
||||||
_G.ADVENTURE_CATEGORY = nil
|
|
||||||
_G.ADVENTURE_CATEGORY_PRIORITY = nil
|
|
||||||
_G.CONDITION_CHECK = nil
|
|
||||||
_G.ACTION_BUTTON_TYPE = nil
|
|
||||||
|
|
||||||
local ADVENTURE_REGISTRY = _G.ADVENTURE_REGISTRY or {}
|
|
||||||
if not IsInterfaceDevClient() then
|
|
||||||
_G.ADVENTURE_REGISTRY = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, adventureType in pairs(ADVENTURE_TYPE) do
|
|
||||||
PRIVATE.REGISTRY[adventureType] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
for index, entry in ipairs(ADVENTURE_REGISTRY) do
|
|
||||||
if not entry.rngPriority then
|
|
||||||
entry.rngPriority = mathrandom(1, MAX_INT)
|
|
||||||
end
|
|
||||||
|
|
||||||
if entry.content then
|
|
||||||
entry.content = strgsub(entry.content, "\\n", "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
tinsert(PRIVATE.REGISTRY[entry.type], entry)
|
|
||||||
end
|
|
||||||
|
|
||||||
for adventureType, registry in pairs(PRIVATE.REGISTRY) do
|
|
||||||
tsort(registry, PRIVATE.SortEntries)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.SortEntries = function(a, b)
|
|
||||||
if ADVENTURE_CATEGORY_PRIORITY[a.category] ~= ADVENTURE_CATEGORY_PRIORITY[b.category] then
|
|
||||||
return ADVENTURE_CATEGORY_PRIORITY[a.category] < ADVENTURE_CATEGORY_PRIORITY[b.category]
|
|
||||||
elseif a.rngPriority ~= b.rngPriority then
|
|
||||||
return a.rngPriority < b.rngPriority
|
|
||||||
elseif a.id ~= b.id then
|
|
||||||
return a.id < b.id
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.IsMinigameAvailable = function(minigameID)
|
|
||||||
for index = 1, C_MiniGames.GetNumGames() do
|
|
||||||
if C_MiniGames.GetGameIDFromIndex(index) == minigameID then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.ApplyConditionOperator = function(operator, conditionValue, value)
|
|
||||||
if operator == CONDITION_OPERATOR.COMP_TYPE_EQ then
|
|
||||||
return value == conditionValue
|
|
||||||
elseif operator == CONDITION_OPERATOR.COMP_TYPE_HIGH then
|
|
||||||
return value > conditionValue
|
|
||||||
elseif operator == CONDITION_OPERATOR.COMP_TYPE_LOW then
|
|
||||||
return value < conditionValue
|
|
||||||
elseif operator == CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ then
|
|
||||||
return value >= conditionValue
|
|
||||||
elseif operator == CONDITION_OPERATOR.COMP_TYPE_LOW_EQ then
|
|
||||||
return value <= conditionValue
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.CheckCondition = function(entry, conditionType, conditionValue, operator, invert)
|
|
||||||
local isComplete = false
|
|
||||||
|
|
||||||
if conditionType == CONDITION_CHECK.PLAYER_QUEST_COMPLETE then
|
|
||||||
isComplete = IsQuestCompleted(conditionValue)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_QUEST_IN_QUESTLOG then
|
|
||||||
isComplete = GetQuestLogIndexByID(conditionValue) ~= nil
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_ACHIEVEMENT_COMPLETE then
|
|
||||||
isComplete = select(4, GetAchievementInfo(conditionValue))
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_BATTLEPASS_DAILY_COUNT then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_BattlePass.GetNumQuests(Enum.BattlePass.QuestType.Daily))
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_BATTLEPASS_WEEKLY_COUNT then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_BattlePass.GetNumQuests(Enum.BattlePass.QuestType.Weekly))
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_STAGE then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_Service.GetRealmStage())
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_LEVEL then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, UnitLevel("player"))
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_FACTION then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_EQ, conditionValue, SERVER_PLAYER_FACTION_GROUP[UnitFactionGroup("player")])
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_CUSTOM_VALUE then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH, 0, C_Service.GetCustomValue(conditionValue) or 0)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_TALENT_GROUPS_COUNT then
|
|
||||||
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH, conditionValue, C_Talent.GetNumTalentGroups() or 1)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_ITEM then
|
|
||||||
isComplete = GetItemCount(conditionValue) > 0
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_BUFF then
|
|
||||||
isComplete = C_Unit.FindAuraBySpell("player", conditionValue, "HELPFUL") ~= nil
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_DEBUFF then
|
|
||||||
isComplete = C_Unit.FindAuraBySpell("player", conditionValue, "HARMFUL") ~= nil
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_SPELL_KNOWN then
|
|
||||||
isComplete = IsSpellKnown(conditionValue, false)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PET_SPELL_KNOWN then
|
|
||||||
isComplete = IsSpellKnown(conditionValue, true)
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_ID then
|
|
||||||
isComplete = C_Service.GetRealmID() == conditionValue
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_GAME_EVENT_ACTIVE then
|
|
||||||
isComplete = IsGameEventActive(conditionValue)
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_BRAWL_ACTIVE then
|
|
||||||
isComplete = GetCurrentBrawlID() == conditionValue
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_WINTERGRASP_REGISTRATION_ACTIVE then
|
|
||||||
isComplete = CanQueueForWintergrasp() ~= nil
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_LFG_DUNGEON_JOINABLE then
|
|
||||||
isComplete = conditionValue ~= 0 and IsLFGDungeonJoinable(conditionValue)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_LFG_MINIGAME_JOINABLE then
|
|
||||||
isComplete = conditionValue ~= 0 and PRIVATE.IsMinigameAvailable(conditionValue)
|
|
||||||
elseif conditionType == CONDITION_CHECK.REALM_DUNGEON_ACTUAL then
|
|
||||||
isComplete = C_EncounterJournal.IsActualInstance(conditionValue, entry.ej_difficultyID)
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_IN_CHALLENGE_MODE then
|
|
||||||
isComplete = (C_Hardcore.GetActiveChallengeID() or 0) == conditionValue
|
|
||||||
elseif conditionType == CONDITION_CHECK.PLAYER_IN_GM_MODE then
|
|
||||||
isComplete = C_Service.IsInGMMode()
|
|
||||||
else -- unhandled condition
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
if (not invert and isComplete)
|
|
||||||
or (invert and not isComplete)
|
|
||||||
then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.CheckConditions = function(entry)
|
|
||||||
for index, condition in ipairs(entry.conditions) do
|
|
||||||
local isComplete = PRIVATE.CheckCondition(entry, condition.type, condition.value, condition.operator, condition.invert)
|
|
||||||
if not isComplete then
|
|
||||||
--[[
|
|
||||||
if condition.altID then
|
|
||||||
if type(condition.altID) == "table" then
|
|
||||||
for idIndex, conditionAltID in ipairs(condition.altID) do
|
|
||||||
isComplete = PRIVATE.CheckCondition(entry, condition.type, conditionAltID, condition.operator, condition.invert)
|
|
||||||
if isComplete then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
isComplete = PRIVATE.CheckCondition(entry, condition.type, condition.altID, condition.operator, condition.invert)
|
|
||||||
end
|
|
||||||
|
|
||||||
if not isComplete then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
--]]
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.UpdateSuggestionsCoroutine = function(isLevelUp)
|
|
||||||
local oldIDs = PRIVATE.GetUniqueSuggestionIDs()
|
|
||||||
|
|
||||||
local numEntries = 0
|
|
||||||
local processedEntries = 0
|
|
||||||
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
|
|
||||||
numEntries = numEntries + #registry
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- Build Suggestions
|
|
||||||
twipe(PRIVATE.SUGGESTIONS)
|
|
||||||
|
|
||||||
local numPrimary = 0
|
|
||||||
local numSecondary = 0
|
|
||||||
|
|
||||||
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
|
|
||||||
local isPrimary = adventureType == ADVENTURE_TYPE.PRIMARY
|
|
||||||
|
|
||||||
PRIVATE.SUGGESTIONS[adventureType] = {}
|
|
||||||
|
|
||||||
for index, entry in ipairs(registry) do
|
|
||||||
processedEntries = processedEntries + 1
|
|
||||||
|
|
||||||
if PRIVATE.CheckConditions(entry) then
|
|
||||||
if isPrimary then
|
|
||||||
numPrimary = numPrimary + 1
|
|
||||||
tinsert(PRIVATE.SUGGESTIONS[adventureType], entry)
|
|
||||||
else
|
|
||||||
if numSecondary >= NUM_SECONDARY_SUGGESTIONS or numPrimary == 0 then
|
|
||||||
numPrimary = numPrimary + 1
|
|
||||||
tinsert(PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY], entry)
|
|
||||||
else
|
|
||||||
numSecondary = numSecondary + 1
|
|
||||||
tinsert(PRIVATE.SUGGESTIONS[adventureType], entry)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if (debugprofilestop() - PRIVATE.BUILD_COROUTINE_TIMESTAMP) > PRIVATE.FRAMETIME_BUDGET then
|
|
||||||
coroutine.yield(false, processedEntries / numEntries)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local newIDs = PRIVATE.GetUniqueSuggestionIDs()
|
|
||||||
local newAdventureNotice = tCompare(oldIDs, newIDs, 2)
|
|
||||||
|
|
||||||
local offsetFound
|
|
||||||
|
|
||||||
do -- try to select previously displayed primary suggestion
|
|
||||||
if PRIVATE.PRIMARY_OFFSET_ID then
|
|
||||||
local suggestion, offsetIndex = PRIVATE.GetSuggestionEntryByID(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_ID)
|
|
||||||
if suggestion then
|
|
||||||
PRIVATE.PRIMARY_OFFSET_INDEX = offsetIndex
|
|
||||||
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
|
|
||||||
offsetFound = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not offsetFound then
|
|
||||||
local suggestion = PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, 1)
|
|
||||||
if suggestion then
|
|
||||||
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
|
|
||||||
else
|
|
||||||
PRIVATE.PRIMARY_OFFSET_ID = nil
|
|
||||||
end
|
|
||||||
PRIVATE.PRIMARY_OFFSET_INDEX = 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
FireCustomClientEvent("AJ_REFRESH_DISPLAY", newAdventureNotice)
|
|
||||||
|
|
||||||
return true, 1
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.UpdateSuggestions = function(isLevelUp)
|
|
||||||
if PRIVATE.BUILD_COROUTINE then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local framerate = GetFramerate()
|
|
||||||
PRIVATE.FRAMETIME_TARGET = framerate > 63 and (1 / 60) or (1 / 55)
|
|
||||||
PRIVATE.FRAMETIME_BUDGET = 1000 / framerate - PRIVATE.FRAMETIME_RESERVE
|
|
||||||
|
|
||||||
PRIVATE.BUILD_COROUTINE = coroutine.create(PRIVATE.UpdateSuggestionsCoroutine)
|
|
||||||
PRIVATE.BUILD_COROUTINE_TIMESTAMP = debugprofilestop()
|
|
||||||
|
|
||||||
local status, result, progress = coroutine.resume(PRIVATE.BUILD_COROUTINE, isLevelUp)
|
|
||||||
if not status then
|
|
||||||
PRIVATE.BUILD_COROUTINE = nil
|
|
||||||
PRIVATE.eventHandler:Hide()
|
|
||||||
error(result, 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
if coroutine.status(PRIVATE.BUILD_COROUTINE) == "dead" then
|
|
||||||
PRIVATE.BUILD_COROUTINE = nil
|
|
||||||
else
|
|
||||||
if PRIVATE.BUILD_COROUTINE_DEBUG then print("AJ_BUILD_COROUTINE", progress) end
|
|
||||||
PRIVATE.eventHandler:Show()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetNumPrimarySuggestions = function()
|
|
||||||
if PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY] then
|
|
||||||
return #PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY]
|
|
||||||
end
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetNumSecondarySuggestions = function()
|
|
||||||
if PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.SECONDARY] then
|
|
||||||
return #PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.SECONDARY]
|
|
||||||
end
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetButtonText = function(suggestion)
|
|
||||||
if suggestion.actionType == ACTION_BUTTON_TYPE.NONE then
|
|
||||||
-- skip
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.QUEST_START then
|
|
||||||
return AJ_ACTION_TEXT_START_QUEST
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL
|
|
||||||
then
|
|
||||||
return AJ_ACTION_TEXT_JOIN_BATTLE
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID
|
|
||||||
then
|
|
||||||
return AJ_ACTION_TEXT_JOIN_GROUP
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_EJ_DUNGEON_ID then
|
|
||||||
return AJ_ACTION_TEXT_OPEN_EJ
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_BATTLEPASS_QUESTS then
|
|
||||||
return AJ_ACTION_TEXT_SHOW_QUEST
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_HEAD_HUNTING then
|
|
||||||
return AJ_ACTION_TEXT_START_HUNT
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ACTION_TARGET_VIEW = {
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING] = "pvp-arena-rating",
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM] = "pvp-honor-random",
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP] = "pvp-honor-wintergrasp",
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL] = "pvp-honor-brawl",
|
|
||||||
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM] = "pve-lfg-dungeon",
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID] = "pve-lfg-dungeon",
|
|
||||||
[ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID] = "pve-lfg-minigame",
|
|
||||||
}
|
|
||||||
|
|
||||||
PRIVATE.ActivateEntry = function(suggestion)
|
|
||||||
if suggestion.actionType == ACTION_BUTTON_TYPE.NONE then
|
|
||||||
-- skip
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.QUEST_START then
|
|
||||||
local questID = suggestion.actionParam
|
|
||||||
if questID then
|
|
||||||
QueryQuestStart(questID, Enum.QueryQuestStartSource.Suggestion, suggestion.id)
|
|
||||||
-- FireCustomClientEvent("AJ_QUEST_LOG_OPEN", questID or 0, uiMapID or 0)
|
|
||||||
else
|
|
||||||
GMError(string.format("[Suggestion::ActivateEntry] no quest id to start [suggestionID=%d]", suggestion.id))
|
|
||||||
end
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP
|
|
||||||
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL
|
|
||||||
then
|
|
||||||
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
|
|
||||||
FireCustomClientEvent("AJ_ACTION_PVP_LFG", desiredViewType)
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM then
|
|
||||||
for i, lfgDungeonID in ipairs({258, 259, 261}) do
|
|
||||||
if IsLFGDungeonJoinable(lfgDungeonID) then
|
|
||||||
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
|
|
||||||
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, lfgDungeonID)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID then
|
|
||||||
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
|
|
||||||
local lfgDungeonID = suggestion.actionParam or 0
|
|
||||||
if IsLFGDungeonJoinable(lfgDungeonID) then
|
|
||||||
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, lfgDungeonID)
|
|
||||||
end
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID then
|
|
||||||
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
|
|
||||||
local minigameID = suggestion.actionParam or 0
|
|
||||||
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, minigameID)
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_EJ_DUNGEON_ID then
|
|
||||||
local dungeonID = suggestion.actionParam or 0
|
|
||||||
local difficultyID = suggestion.actionParam2 or -1
|
|
||||||
FireCustomClientEvent("AJ_ACTION_EJ_DUNGEON", dungeonID, difficultyID)
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_BATTLEPASS_QUESTS then
|
|
||||||
local showQuestTab = true
|
|
||||||
FireCustomClientEvent("AJ_ACTION_BATTLE_PASS", showQuestTab)
|
|
||||||
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_HEAD_HUNTING then
|
|
||||||
FireCustomClientEvent("AJ_ACTION_HEAD_HUNTING")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetSuggestionEntry = function(suggestionType, index)
|
|
||||||
if PRIVATE.SUGGESTIONS[suggestionType] then
|
|
||||||
return PRIVATE.SUGGESTIONS[suggestionType][index]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetSuggestionEntryByID = function(suggestionType, id)
|
|
||||||
if PRIVATE.SUGGESTIONS[suggestionType] then
|
|
||||||
for index, suggestion in ipairs(PRIVATE.SUGGESTIONS[suggestionType]) do
|
|
||||||
if suggestion.id == id then
|
|
||||||
return suggestion, index
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.OnItemLoaded = function()
|
|
||||||
FireCustomClientEvent("AJ_REWARD_DATA_RECEIVED")
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.OnItemLoadedContent = function()
|
|
||||||
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.OnQuestLoadedContent = function(questID)
|
|
||||||
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetQuestLink = function(questID)
|
|
||||||
questID = tonumber(questID)
|
|
||||||
|
|
||||||
if IsQuestDataCached(questID) then
|
|
||||||
local questLink = GetQuestLinkByID(questID)
|
|
||||||
if questLink then
|
|
||||||
questLink = strgsub(questLink, COLOR_PATTERN, "|cff8B0000|H")
|
|
||||||
return questLink
|
|
||||||
end
|
|
||||||
else
|
|
||||||
RequestQuestCacheByID(questID, PRIVATE.OnQuestLoadedContent)
|
|
||||||
end
|
|
||||||
|
|
||||||
local name = GetTitleForQuestID(questID)
|
|
||||||
if not name then
|
|
||||||
name = IsGMAccount() and strformat("%s (quest:%s)", UNKNOWN, questID) or UNKNOWN
|
|
||||||
end
|
|
||||||
return strformat("|cff8B0000|Hquest:%d:-1|h[%s]|h|r", questID, name)
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetItemLink = function(itemID)
|
|
||||||
itemID = tonumber(itemID)
|
|
||||||
local name, link = C_Item.GetItemInfo(itemID, nil, PRIVATE.OnItemLoadedContent, true)
|
|
||||||
if not link then
|
|
||||||
name = IsGMAccount() and strformat("%s (item:%s)", UNKNOWN, itemID) or UNKNOWN
|
|
||||||
return strformat("|cffffffff[%s]|r", name)
|
|
||||||
end
|
|
||||||
return link
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetSpellLink = function(spellID)
|
|
||||||
spellID = tonumber(spellID)
|
|
||||||
local name = GetSpellInfo(spellID)
|
|
||||||
if not name then
|
|
||||||
name = IsGMAccount() and strformat("%s (spell:%s)", UNKNOWN, spellID) or UNKNOWN
|
|
||||||
return strformat("|cffffffff[%s]|r", name)
|
|
||||||
end
|
|
||||||
return strformat("|cff000080|Hspell:%d|h[%s]|h|r", spellID, name)
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetSuggestionInfo = function(suggestionType, index)
|
|
||||||
local suggestion = PRIVATE.GetSuggestionEntry(suggestionType, index)
|
|
||||||
if suggestion then
|
|
||||||
local description = suggestion.content
|
|
||||||
|
|
||||||
if description then
|
|
||||||
description = strgsub(description, "{quest:(%d+)}", PRIVATE.GetQuestLink)
|
|
||||||
description = strgsub(description, "{item:(%d+)}", PRIVATE.GetItemLink)
|
|
||||||
description = strgsub(description, "{spell:(%d+)}", PRIVATE.GetSpellLink)
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
title = suggestion.title,
|
|
||||||
description = description,
|
|
||||||
buttonText = PRIVATE.GetButtonText(suggestion),
|
|
||||||
iconPath = suggestion.icon and strconcat([[Interface\Icons\]], suggestion.icon) or QUESTION_MARK_ICON,
|
|
||||||
ej_instanceID = suggestion.ej_instanceID,
|
|
||||||
difficultyID = suggestion.ej_difficultyID or 1,
|
|
||||||
expansionLevel = suggestion.ej_expansionLevel,
|
|
||||||
isRandomDungeon = suggestion.ej_isRandomDungeon,
|
|
||||||
hideDifficulty = suggestion.ej_showDifficulty ~= 1,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetSuggestionByIndex = function(suggestionIndex)
|
|
||||||
if suggestionIndex <= 1 then
|
|
||||||
return PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_INDEX)
|
|
||||||
else
|
|
||||||
return PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.SECONDARY, suggestionIndex - 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetReward = function(suggestion)
|
|
||||||
if not suggestion then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local numRewards = #suggestion.rewards
|
|
||||||
|
|
||||||
if numRewards > 0 then
|
|
||||||
if numRewards == 1 and suggestion.rewardDesc then
|
|
||||||
local itemName, itemLink, itemRarity, iLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(suggestion.rewards[1], nil, PRIVATE.OnItemLoaded, true)
|
|
||||||
local rewardDesc = type(suggestion.rewardDesc) == "string" and suggestion.rewardDesc or AJ_PRIMARY_REWARD_TEXT
|
|
||||||
|
|
||||||
return {
|
|
||||||
isRewardTable = false,
|
|
||||||
rewardDesc = rewardDesc,
|
|
||||||
itemLink = itemLink,
|
|
||||||
itemIcon = itemTexture,
|
|
||||||
itemQuantity = 1,
|
|
||||||
}
|
|
||||||
else
|
|
||||||
local rewards = {}
|
|
||||||
|
|
||||||
for index, itemID in ipairs(suggestion.rewards) do
|
|
||||||
local itemName, itemLink, itemRarity, iLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(suggestion.rewards[index], nil, PRIVATE.OnItemLoaded, true)
|
|
||||||
if itemLink then
|
|
||||||
tinsert(rewards, {
|
|
||||||
itemLink = itemLink,
|
|
||||||
itemIcon = itemTexture,
|
|
||||||
itemQuantity = 1,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #rewards > 0 then
|
|
||||||
rewards.isRewardList = true
|
|
||||||
return rewards
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif false then -- currency item
|
|
||||||
return {
|
|
||||||
isRewardTable = false,
|
|
||||||
rewardDesc = AJ_PRIMARY_REWARD_TEXT,
|
|
||||||
currencyIcon = nil,
|
|
||||||
currencyQuantity = nil,
|
|
||||||
currencyIcon = nil,
|
|
||||||
}
|
|
||||||
else
|
|
||||||
local minItemLevel = suggestion.reward_minItemLevel
|
|
||||||
local maxItemLevel = suggestion.reward_maxItemLevel
|
|
||||||
local itemLevel = suggestion.reward_itemLevel
|
|
||||||
|
|
||||||
if itemLevel or minItemLevel or maxItemLevel then
|
|
||||||
if itemLevel then
|
|
||||||
minItemLevel = nil
|
|
||||||
maxItemLevel = nil
|
|
||||||
elseif not minItemLevel or not maxItemLevel then
|
|
||||||
minItemLevel = nil
|
|
||||||
maxItemLevel = nil
|
|
||||||
itemLevel = minItemLevel or maxItemLevel
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
isRewardTable = true,
|
|
||||||
itemLevel = itemLevel,
|
|
||||||
minItemLevel = minItemLevel,
|
|
||||||
maxItemLevel = maxItemLevel,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PRIVATE.GetUniqueSuggestionIDs = function()
|
|
||||||
local suggestionIDs = {}
|
|
||||||
|
|
||||||
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
|
|
||||||
suggestionIDs[adventureType] = {}
|
|
||||||
|
|
||||||
for index, entry in ipairs(registry) do
|
|
||||||
suggestionIDs[adventureType][entry.id] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return suggestionIDs
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
if IsInterfaceDevClient() then
|
|
||||||
PRIVATE.BUILD_COROUTINE_DEBUG = true
|
|
||||||
PRIVATE_AJ = PRIVATE
|
|
||||||
end
|
|
||||||
PRIVATE.Initialize()
|
|
||||||
end
|
|
||||||
|
|
||||||
C_AdventureJournal = {}
|
|
||||||
|
|
||||||
function C_AdventureJournal.ReloadData()
|
|
||||||
if IsInterfaceDevClient() then
|
|
||||||
if _G.ADVENTURE_REGISTRY and PRIVATE.initialized then
|
|
||||||
PRIVATE.initialized = nil
|
|
||||||
twipe(PRIVATE.REGISTRY)
|
|
||||||
PRIVATE.Initialize()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.CanBeShown()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.UpdateSuggestions(isLevelUp)
|
|
||||||
PRIVATE.UpdateSuggestions(not not isLevelUp)
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.SetPrimaryOffset(offset)
|
|
||||||
if type(offset) ~= "number" then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.SetPrimaryOffset' (number expected, got %s)", offset ~= nil and type(offset) or "no value"), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
offset = offset + 1
|
|
||||||
|
|
||||||
if offset < 1 or offset > PRIVATE.GetNumPrimarySuggestions() then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.SetPrimaryOffset' (index %s out of range [1 - %s])", offset, PRIVATE.GetNumPrimarySuggestions()), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
if offset == PRIVATE.PRIMARY_OFFSET_INDEX then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local suggestion = PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, offset)
|
|
||||||
if suggestion then
|
|
||||||
PRIVATE.PRIMARY_OFFSET_INDEX = offset
|
|
||||||
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
|
|
||||||
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.GetPrimaryOffset()
|
|
||||||
return PRIVATE.PRIMARY_OFFSET_INDEX - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.GetNumAvailableSuggestions()
|
|
||||||
return PRIVATE.GetNumPrimarySuggestions()
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.GetSuggestions(suggestionTable)
|
|
||||||
if type(suggestionTable) ~= "table" then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.GetSuggestions' (table expected, got %s)", suggestionTable ~= nil and type(suggestionTable) or "no value"), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
twipe(suggestionTable)
|
|
||||||
|
|
||||||
if PRIVATE.GetNumPrimarySuggestions() > 0 then
|
|
||||||
local suggestion = PRIVATE.GetSuggestionInfo(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_INDEX)
|
|
||||||
if suggestion then
|
|
||||||
tinsert(suggestionTable, suggestion)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for index = 1, mathmin(NUM_SECONDARY_SUGGESTIONS, PRIVATE.GetNumSecondarySuggestions()) do
|
|
||||||
local suggestion = PRIVATE.GetSuggestionInfo(ADVENTURE_TYPE.SECONDARY, index)
|
|
||||||
if suggestion then
|
|
||||||
tinsert(suggestionTable, suggestion)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.GetReward(suggestionIndex)
|
|
||||||
if type(suggestionIndex) ~= "number" then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.GetReward' (number expected, got %s)", suggestionIndex ~= nil and type(suggestionIndex) or "no value"), 2)
|
|
||||||
elseif suggestionIndex < 1 or suggestionIndex > NUM_SECONDARY_SUGGESTIONS + 1 then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.GetReward' (index %s out of range [1 - %s])", suggestionIndex, NUM_SECONDARY_SUGGESTIONS + 1), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
return PRIVATE.GetReward(PRIVATE.GetSuggestionByIndex(suggestionIndex))
|
|
||||||
end
|
|
||||||
|
|
||||||
function C_AdventureJournal.ActivateEntry(suggestionIndex)
|
|
||||||
if type(suggestionIndex) ~= "number" then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.ActivateEntry' (number expected, got %s)", suggestionIndex ~= nil and type(suggestionIndex) or "no value"), 2)
|
|
||||||
elseif suggestionIndex < 1 or suggestionIndex > NUM_SECONDARY_SUGGESTIONS + 1 then
|
|
||||||
error(strformat("bad argument #1 to 'C_AdventureJournal.ActivateEntry' (index %s out of range [1 - %s])", suggestionIndex, NUM_SECONDARY_SUGGESTIONS + 1), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local suggestion = PRIVATE.GetSuggestionByIndex(suggestionIndex)
|
|
||||||
if suggestion then
|
|
||||||
PRIVATE.ActivateEntry(suggestion)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1694,6 +1694,11 @@ do -- Section
|
|||||||
if not abilityIcon and sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID] > 0 then
|
if not abilityIcon and sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID] > 0 then
|
||||||
abilityIcon = select(3, GetSpellInfo(sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID]))
|
abilityIcon = select(3, GetSpellInfo(sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID]))
|
||||||
end
|
end
|
||||||
|
if not abilityIcon and
|
||||||
|
(sectionInfo[ENCOUNTER_SECTION_FIELD.DESCRIPTION_SPELL_ID] > 0 or
|
||||||
|
sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID] > 0) then
|
||||||
|
abilityIcon = QUESTION_MARK_ICON
|
||||||
|
end
|
||||||
|
|
||||||
if sectionInfo[ENCOUNTER_SECTION_FIELD.DIFFCULTY_MASK] ~= -1 then
|
if sectionInfo[ENCOUNTER_SECTION_FIELD.DIFFCULTY_MASK] ~= -1 then
|
||||||
local difficultyMask = PRIVATE.GetDifficultyMask(PRIVATE.SELECTED_DIFFICULTY)
|
local difficultyMask = PRIVATE.GetDifficultyMask(PRIVATE.SELECTED_DIFFICULTY)
|
||||||
|
|||||||
@@ -313,6 +313,11 @@ function L.GetObjectiveInfo(index)
|
|||||||
questID = o.questID,
|
questID = o.questID,
|
||||||
questChainID = o.questChainID,
|
questChainID = o.questChainID,
|
||||||
instanceID = o.instanceID,
|
instanceID = o.instanceID,
|
||||||
|
ej_instanceID = o.ej_instanceID or o.ejInstanceID,
|
||||||
|
journalInstanceID = o.journalInstanceID,
|
||||||
|
mapID = o.mapID,
|
||||||
|
worldMapAreaID = o.worldMapAreaID,
|
||||||
|
dungeonID = o.dungeonID,
|
||||||
encounterID = o.encounterID,
|
encounterID = o.encounterID,
|
||||||
achievementID = o.achievementID,
|
achievementID = o.achievementID,
|
||||||
factionID = o.factionID,
|
factionID = o.factionID,
|
||||||
@@ -342,7 +347,19 @@ function L.GetRecommendationInfo(index)
|
|||||||
loc = r.loc,
|
loc = r.loc,
|
||||||
level = r.level,
|
level = r.level,
|
||||||
biome = r.biome,
|
biome = r.biome,
|
||||||
|
artwork = r.artwork,
|
||||||
|
cover = r.cover,
|
||||||
|
coverImage = r.coverImage,
|
||||||
|
bgImage = r.bgImage,
|
||||||
|
buttonImage = r.buttonImage,
|
||||||
|
icon = r.icon,
|
||||||
instanceID = r.instanceID,
|
instanceID = r.instanceID,
|
||||||
|
ej_instanceID = r.ej_instanceID or r.ejInstanceID,
|
||||||
|
journalInstanceID = r.journalInstanceID,
|
||||||
|
mapID = r.mapID,
|
||||||
|
worldMapAreaID = r.worldMapAreaID,
|
||||||
|
targetID = r.targetID,
|
||||||
|
dungeonID = r.dungeonID,
|
||||||
encounterID = r.encounterID,
|
encounterID = r.encounterID,
|
||||||
priority = r.priority or 0,
|
priority = r.priority or 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,7 @@
|
|||||||
## X-Hidden: 1
|
## X-Hidden: 1
|
||||||
|
|
||||||
MoonWellClient.lua
|
MoonWellClient.lua
|
||||||
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache1.lua
|
|
||||||
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache2.lua
|
|
||||||
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache3.lua
|
|
||||||
EncounterJournal\EncounterJournalCompat.lua
|
EncounterJournal\EncounterJournalCompat.lua
|
||||||
EncounterJournal\SharedXML\Pools.lua
|
|
||||||
EncounterJournal\SharedXML\TableUtil.lua
|
|
||||||
EncounterJournal\SharedXML\SoundKitConstants.lua
|
EncounterJournal\SharedXML\SoundKitConstants.lua
|
||||||
EncounterJournal\Generated_EncounterJournalData.lua
|
EncounterJournal\Generated_EncounterJournalData.lua
|
||||||
EncounterJournal\EncounterJournalData.lua
|
EncounterJournal\EncounterJournalData.lua
|
||||||
@@ -22,23 +17,15 @@ EncounterJournal\EncounterJournalTierFilter.lua
|
|||||||
EncounterJournal\EncounterJournalTierSnapshot.lua
|
EncounterJournal\EncounterJournalTierSnapshot.lua
|
||||||
EncounterJournal\GlobalStrings.lua
|
EncounterJournal\GlobalStrings.lua
|
||||||
EncounterJournal\Utils\C_EncounterJournal.lua
|
EncounterJournal\Utils\C_EncounterJournal.lua
|
||||||
EncounterJournal\Utils\C_AdventureJournal.lua
|
|
||||||
EncounterJournal\Utils\C_PlayerGuide.lua
|
EncounterJournal\Utils\C_PlayerGuide.lua
|
||||||
EncounterJournal\EncounterJournalInit.lua
|
EncounterJournal\EncounterJournalInit.lua
|
||||||
EncounterJournal\EncounterJournalFrameMethods.lua
|
EncounterJournal\EncounterJournalFrameMethods.lua
|
||||||
EncounterJournal\EncounterJournalFonts.xml
|
EncounterJournal\EncounterJournalFonts.xml
|
||||||
EncounterJournal\SharedXML\SharedTooltipTemplates.xml
|
|
||||||
EncounterJournal\SharedXML\TableBuilder.lua
|
|
||||||
EncounterJournal\SharedXML\EventHandler.lua
|
|
||||||
EncounterJournal\SharedXML\SharedUIPanelTemplates.xml
|
EncounterJournal\SharedXML\SharedUIPanelTemplates.xml
|
||||||
EncounterJournal\SharedXML\SharedUIPanelPKBTTemplates.xml
|
|
||||||
EncounterJournal\SharedXML\HybridScrollFrame.xml
|
EncounterJournal\SharedXML\HybridScrollFrame.xml
|
||||||
EncounterJournal\SharedXML\NavigationBar.xml
|
EncounterJournal\SharedXML\NavigationBar.xml
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
|
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
|
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
|
||||||
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
|
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_LootJournalItems.xml
|
|
||||||
EncounterJournal\Custom_EncounterJournal\Custom_ItemBrowser.xml
|
|
||||||
EncounterJournal\EncounterJournalScrollFix.lua
|
EncounterJournal\EncounterJournalScrollFix.lua
|
||||||
EncounterJournal\EncounterJournalDungeonMode.lua
|
EncounterJournal\EncounterJournalDungeonMode.lua
|
||||||
EncounterJournal\EncounterJournalOpen.lua
|
EncounterJournal\EncounterJournalOpen.lua
|
||||||
|
|||||||