базовый encounter journal

This commit is contained in:
2026-06-13 11:46:39 +04:00
parent d8d94f10b0
commit b0da9ec10b
90 changed files with 4521 additions and 366 deletions
+276
View File
@@ -0,0 +1,276 @@
--[[----------------------------------------------------------------------------
C_PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
Клиентское хранилище данных Путеводителя + UI-facing API.
Соответствует контракту из player-guide-requirements.md → "Client API".
Подключается до PlayerGuide.xml/.lua в .toc.
------------------------------------------------------------------------------]]
C_PlayerGuide = C_PlayerGuide or {}
local L = C_PlayerGuide
-- ──────────────────────────────────────────────────────────────────────────
-- Состояние
local currentData = nil -- последний валидный payload от сервера
local lastError = nil -- строка с описанием последней проблемы валидации
-- Состояние UI: "data" | "empty" | "loading"
local uiState = "loading"
-- Подписчики на обновления (PlayerGuide.lua вешает сюда свой Refresh)
local listeners = {}
-- ──────────────────────────────────────────────────────────────────────────
-- Утилиты
local function notify()
for _, fn in ipairs(listeners) do
-- pcall чтобы один сломанный listener не уронил остальные
pcall(fn, uiState, currentData)
end
end
local function isTable(v) return type(v) == "table" end
-- Валидация payload'а. Возвращает true либо false + строка ошибки.
local function validate(data)
if not isTable(data) then return false, "payload is not a table" end
if data.stageID and type(data.stageID) ~= "number" then
return false, "stageID is not a number"
end
if data.progress and type(data.progress) ~= "number" then
return false, "progress is not a number"
end
if data.objectives and not isTable(data.objectives) then
return false, "objectives is not a table"
end
-- Уникальность id целей
if isTable(data.objectives) then
local seen = {}
for i, o in ipairs(data.objectives) do
if not isTable(o) then
return false, "objective #" .. i .. " is not a table"
end
if o.id and seen[o.id] then
return false, "duplicate objective id: " .. tostring(o.id)
end
if o.id then seen[o.id] = true end
end
end
return true
end
-- ──────────────────────────────────────────────────────────────────────────
-- Подписки (для PlayerGuide.lua)
function L.RegisterListener(fn)
table.insert(listeners, fn)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичный API (см. требования → Client API)
function L.SetData(data)
local ok, err = validate(data)
if not ok then
lastError = err
uiState = "empty"
notify()
return false, err
end
currentData = data
lastError = nil
uiState = "data"
notify()
return true
end
function L.Clear()
currentData = nil
lastError = nil
uiState = "empty"
notify()
end
function L.SetLoading()
if uiState ~= "data" then
uiState = "loading"
notify()
end
end
function L.GetState()
return uiState
end
function L.GetLastError()
return lastError
end
function L.HasData()
return uiState == "data" and currentData ~= nil
end
function L.GetStageInfo()
if not L.HasData() then return nil end
return currentData.stageID,
currentData.stageName,
currentData.stageDescription,
currentData.progress or 0
end
-- Доп. метод: список всех этапов (для цепочки сверху)
function L.GetStages()
if not L.HasData() then return nil end
return currentData.stages or {} -- сервер опционально шлёт массив этапов
end
function L.GetNumObjectives()
if not L.HasData() or not currentData.objectives then return 0 end
return #currentData.objectives
end
function L.GetObjectiveInfo(index)
if not L.HasData() or not currentData.objectives then return nil end
local o = currentData.objectives[index]
if not o then return nil end
return {
id = o.id,
type = o.type or "custom",
title = o.title or "",
description = o.description or o.subtitle or "",
subtitle = o.subtitle, -- наше расширение поля для UI
completed = o.completed and true or false,
progress = o.progress or 0,
required = o.required or 1,
targetID = o.targetID,
questID = o.questID,
instanceID = o.instanceID,
encounterID = o.encounterID,
achievementID = o.achievementID,
factionID = o.factionID,
itemID = o.itemID,
currencyID = o.currencyID,
order = o.order or index,
}
end
function L.GetNumRecommendations()
if not L.HasData() or not currentData.recommendations then return 0 end
return #currentData.recommendations
end
function L.GetRecommendationInfo(index)
if not L.HasData() or not currentData.recommendations then return nil end
local r = currentData.recommendations[index]
if not r then return nil end
return {
id = r.id,
type = r.type or "dungeon",
title = r.title or r.name or "",
name = r.name or r.title or "",
description = r.description or r.loc or "",
loc = r.loc, -- локация/подзаголовок
level = r.level,
biome = r.biome, -- ключ биом-обложки
instanceID = r.instanceID,
encounterID = r.encounterID,
priority = r.priority or 0,
}
end
function L.GetNumRewards()
if not L.HasData() or not currentData.rewards then return 0 end
return #currentData.rewards
end
function L.GetRewardInfo(index)
if not L.HasData() or not currentData.rewards then return nil end
local r = currentData.rewards[index]
if not r then return nil end
return r
end
-- ──────────────────────────────────────────────────────────────────────────
-- Сетевой слой (AIO)
-- MoonWellPlayerGuide.lua перехватывает SetData() из serverToClient и вызывает
-- L.SetData(payload). RequestRefresh шлёт обратно в обратную сторону.
function L.RequestRefresh()
L.SetLoading()
if AIO and AIO.Handle then
-- name берётся из serverToClient на сервере. Тут — лёгкая обёртка.
AIO.Handle("MoonWellPlayerGuide", "RequestRefresh")
else
-- Без AIO (например, в режиме разработки) — отдаём заглушку через
-- 0.5 секунды, чтобы UI не висел в Loading навечно.
if C_Timer and C_Timer.After then
C_Timer.After(0.5, function() L.Clear() end)
end
end
end
function L.RequestTrackObjective(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestTrackObjective", objectiveID)
end
end
function L.RequestOpenTarget(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestOpenTarget", objectiveID)
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Статика для разработки (видна, пока сервер не прислал реальный payload).
-- Можно убрать перед релизом.
function L._InjectDevPayload()
L.SetData({
version = 1,
stageID = 5,
stageName = "Подготовка к Нордсколу",
stageDescription =
"Завершите ключевые цепочки заданий в Запределье и докажите готовность к походу на север. "..
"По прибытии в Нордскол вам понадобится снаряжение героического уровня.",
progress = 62,
stages = {
{ id = 1, name = "Начало пути", short = "Начало пути", era = "1—58", status = "done" },
{ id = 2, name = "Огненные Недра", short = "Огненные Недра", era = "60", status = "done" },
{ id = 3, name = "Сквозь Тёмный портал", short = "Тёмный портал", era = "58—60", status = "done" },
{ id = 4, name = "Покорение Запределья", short = "Запределье", era = "60—68", status = "done" },
{ id = 5, name = "Подготовка к Нордсколу", short = "К Нордсколу", era = "68—70", status = "active" },
{ id = 6, name = "Заря Нордскола", short = "Нордскол", era = "70—78", status = "locked" },
{ id = 7, name = "Цитадель Ледяной Короны",short = "Ледяная Корона", era = "78—80", status = "locked" },
},
objectives = {
{ id = 1, type = "level", title = "Достигнуть 68 уровня",
subtitle = "Текущий уровень: 68. Можно отправляться в Нордскол.",
completed = true, progress = 68, required = 68, order = 1 },
{ id = 2, type = "quest_chain", title = "Цепочка «Поиск виверны» в Награнде",
subtitle = "Кисть Шамана • Награнд • награда: «Привязка к виверне»",
completed = false, progress = 5, required = 8, order = 2 },
{ id = 3, type = "dungeon", title = "Пройти Кузню Крови",
subtitle = "Цитадель Адского Пламени • рек. уровень 61—64",
completed = false, progress = 0, required = 1, order = 3, instanceID = 256 },
{ id = 4, type = "dungeon", title = "Пройти Аукиндон: Подземелья Аукенай",
subtitle = "Награнд • рек. уровень 64—66",
completed = false, progress = 0, required = 1, order = 4, instanceID = 257 },
{ id = 5, type = "reputation", title = "Получить «Дружелюбие» с Кенарийской экспедицией",
subtitle = "Текущее положение: Нейтрально • Долина Призрачной Луны",
completed = false, progress = 1200, required = 3000, order = 5, factionID = 942 },
},
recommendations = {
{ id = 1, type = "dungeon", name = "Кузня Крови", loc = "Цитадель Адского Пламени", level = "61—64", biome = "fire" },
{ id = 2, type = "dungeon", name = "Подземелья Аукенай", loc = "Аукиндон", level = "64—66", biome = "arcane" },
{ id = 3, type = "dungeon", name = "Сетеккские залы", loc = "Награнд", level = "67—69", biome = "storm" },
{ id = 4, type = "dungeon", name = "Темный Лабиринт", loc = "Аукиндон", level = "68—70", biome = "shadow" },
{ id = 5, type = "dungeon", name = "Ботаника", loc = "Долина Призрачной Луны", level = "69—70", biome = "nature" },
{ id = 6, type = "dungeon", name = "Расколотые Залы", loc = "Цитадель Адского Пламени", level = "70 H", biome = "fire" },
},
rewards = {}, -- блок наград временно скрыт (см. SHOW_REWARDS в PlayerGuide.lua)
})
end
+78
View File
@@ -0,0 +1,78 @@
--[[----------------------------------------------------------------------------
AIO/MoonWellPlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
AIO-handler (клиентская часть) для namespace "MoonWellPlayerGuide".
Сервер отправляет SetData(payload), клиент кладёт его в C_PlayerGuide.
Клиент шлёт RequestRefresh обратно при нажатии «Обновить».
Контракт описан в player-guide-requirements.md → "AIO Contract".
------------------------------------------------------------------------------]]
local AIO = AIO or require("AIO")
if not AIO then return end -- безопасный no-op без AIO
local handlers = AIO.AddHandlers("MoonWellPlayerGuide", {})
-- ──────────────────────────────────────────────────────────────────────────
-- Server → Client
-- Полная замена payload
function handlers.SetData(player, payload)
if C_PlayerGuide and C_PlayerGuide.SetData then
local ok, err = C_PlayerGuide.SetData(payload)
if not ok and DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage(
"|cffff8080[Путеводитель]|r Ошибка payload: "..tostring(err))
end
end
end
-- Обновление одной цели (для частичных update'ов)
function handlers.UpdateObjective(player, objective)
if not (C_PlayerGuide and C_PlayerGuide.HasData()) then return end
-- Простая стратегия: пересобрать payload локально с заменой цели по id.
-- C_PlayerGuide хранит данные internally — даём прямой доступ через _patch:
local n = C_PlayerGuide.GetNumObjectives()
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if o and o.id == objective.id then
-- Грязный, но рабочий способ для MVP — отдать обратно весь массив.
-- Для прод-уровня лучше выставить C_PlayerGuide.PatchObjective(o).
o.progress = objective.progress or o.progress
o.required = objective.required or o.required
o.completed = objective.completed
if MW_PlayerGuide and MW_PlayerGuide.Refresh then
MW_PlayerGuide.Refresh()
end
break
end
end
end
-- Замена только инфы об этапе (без массива objectives)
function handlers.SetStage(player, stagePayload)
if not stagePayload then return end
-- Объединяем с текущим payload'ом
local _, name, desc, progress = C_PlayerGuide.GetStageInfo()
C_PlayerGuide.SetData({
stageID = stagePayload.stageID,
stageName = stagePayload.stageName or name,
stageDescription = stagePayload.stageDescription or desc,
progress = stagePayload.progress or progress,
stages = stagePayload.stages,
objectives = stagePayload.objectives or {},
recommendations = stagePayload.recommendations or {},
rewards = stagePayload.rewards or {},
})
end
-- Сообщения от сервера (баблы / chat-фрейм)
function handlers.Notify(player, message)
if DEFAULT_CHAT_FRAME and message then
DEFAULT_CHAT_FRAME:AddMessage("|cffffd76a[Путеводитель]|r "..tostring(message))
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Client → Server: ничего регистрировать не нужно — это просто AIO.Handle
-- внутри C_PlayerGuide.RequestRefresh / RequestTrackObjective / RequestOpenTarget.
+402
View File
@@ -0,0 +1,402 @@
--[[----------------------------------------------------------------------------
PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
UI-логика вкладки «Путеводитель». Парный файл PlayerGuide.xml.
Зависимости:
* C_PlayerGuide (Utils/C_PlayerGuide.lua) — данные и API
* MW_PLAYER_GUIDE* (Localization/ruRU.lua) — строки
* EncounterJournal (Custom_EncounterJournal) — родительский фрейм
------------------------------------------------------------------------------]]
-- ──────────────────────────────────────────────────────────────────────────
-- Конфигурация фичи
local SHOW_REWARDS = false -- блок наград временно отключён
local SHOW_RECOMMENDATIONS = true
local DENSITY = "compact" -- compact | regular | cozy
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 } -- золото
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
-- Биом → цвет «обложки» рекомендации (rgb 0..1).
-- В будущем заменить на текстуры Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*.
local BIOME_COLOR = {
fire = { 0.83, 0.29, 0.10 },
arcane = { 0.48, 0.31, 0.83 },
storm = { 0.24, 0.56, 0.77 },
shadow = { 0.35, 0.16, 0.54 },
nature = { 0.31, 0.63, 0.29 },
holy = { 0.83, 0.69, 0.24 },
}
-- Тип цели → глиф (заменить на иконки из BLP, когда будет атлас).
local TYPE_GLYPH = {
level = "",
quest = "!",
quest_chain = "!",
dungeon = "",
raid = "",
boss = "",
achievement = "",
reputation = "",
profession = "",
item = "",
currency = "",
custom = "",
}
-- Тип цели → плейсхолдер-иконка из встроенных текстур 3.3.5a.
local TYPE_ICON = {
level = "Interface\\Icons\\Ability_Warrior_Challange",
quest = "Interface\\Icons\\INV_Misc_Note_01",
quest_chain = "Interface\\Icons\\INV_Misc_Note_05",
dungeon = "Interface\\Icons\\INV_Misc_Key_06",
raid = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
boss = "Interface\\Icons\\Ability_Warrior_DecisiveStrike",
achievement = "Interface\\Icons\\Achievement_GuildPerk_HonorableMention",
reputation = "Interface\\Icons\\INV_Scroll_03",
profession = "Interface\\Icons\\Trade_Engineering",
item = "Interface\\Icons\\INV_Misc_Gem_Diamond_02",
currency = "Interface\\Icons\\INV_Misc_Coin_17",
custom = "Interface\\Icons\\INV_Misc_QuestionMark",
}
-- ──────────────────────────────────────────────────────────────────────────
-- Хелперы
local function pct(cur, req)
if not req or req <= 0 then return 0 end
return math.floor((cur / req) * 100 + 0.5)
end
local function setRGB(fs, c) fs:SetTextColor(c.r or c[1], c.g or c[2], c.b or c[3]) end
local function showOrHide(frame, show)
if not frame then return end
if show then frame:Show() else frame:Hide() end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Цепочка этапов
local stageBadges = {} -- кэш созданных бейджей по индексу
local function buildStageChain(stages)
if not PlayerGuideFrame then return end
local container = PlayerGuideFrame.StageChain
-- Прячем лишние бейджи, если этапов стало меньше
for i = (#stages + 1), #stageBadges do
stageBadges[i]:Hide()
end
if #stages == 0 then return end
local width = container:GetWidth()
if width == 0 then width = 600 end
local slot = width / #stages
for i, s in ipairs(stages) do
local badge = stageBadges[i]
if not badge then
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
stageBadges[i] = badge
end
badge:ClearAllPoints()
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
badge:Show()
-- Текст
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
badge.Label:SetText(s.short or s.name or "")
-- Цветовая схема по статусу
local status = s.status or "locked"
if status == "active" then
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
badge.Label:SetTextColor(1.0, 0.84, 0.42)
badge.BadgeBG:SetVertexColor(1.0, 0.84, 0.42, 1)
elseif status == "done" then
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
badge.Label:SetTextColor(0.79, 0.65, 0.38)
badge.BadgeBG:SetVertexColor(0.55, 0.40, 0.18, 1)
-- Можно заменить римскую цифру на «✓» для завершённых:
badge.Roman:SetText("")
else
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
badge.Label:SetTextColor(0.40, 0.34, 0.22)
badge.BadgeBG:SetVertexColor(0.20, 0.15, 0.08, 1)
end
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки целей
local objectiveCards = {}
local CARD_HEIGHT = { compact = 44, regular = 56, cozy = 72 }
local CARD_GAP = { compact = 4, regular = 6, cozy = 8 }
local function renderObjectives()
local section = PlayerGuideFrame.ObjectivesSection
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumObjectives()
-- Скрыть лишние
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
local h = CARD_HEIGHT[DENSITY] or 44
local gap = CARD_GAP[DENSITY] or 4
local done = 0
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if not o then break end
local card = objectiveCards[i]
if not card then
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
objectiveCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
card:SetHeight(h)
card.objectiveData = o
card:Show()
-- Иконка типа
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
card.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) -- crop borders Blizz-стиль
-- Тип-плашка caps
card.TypeLabel:SetText(_G["MW_PG_TYPE_"..string.upper(o.type)] or string.upper(o.type))
-- Заголовок и подзаголовок
card.Title:SetText(o.title or "")
card.Subtitle:SetText(o.subtitle or o.description or "")
-- Прогресс
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
card.ProgressBar:SetValue(o.progress or 0)
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
-- Цвет по статусу
if o.completed then
done = done + 1
card.Title:SetTextColor(0.62, 0.84, 0.47)
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
card.Count:SetTextColor(0.62, 0.84, 0.47)
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
else
card.Title:SetTextColor(1.0, 0.91, 0.63)
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
card.Count:SetTextColor(0.79, 0.65, 0.38)
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
end
end
parent:SetHeight(math.max(1, n * (h + gap)))
section.Count:SetText(string.format("· %d / %d", done, n))
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки рекомендаций
local recCards = {}
local function renderRecommendations()
local section = PlayerGuideFrame.RecommendationsSection
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumRecommendations()
if n == 0 then section:Hide(); return end
section:Show()
for i = (n + 1), #recCards do recCards[i]:Hide() end
local cardW = 148
local gap = 8
for i = 1, n do
local r = C_PlayerGuide.GetRecommendationInfo(i)
if not r then break end
local card = recCards[i]
if not card then
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
recCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
card.recData = r
card:Show()
local biome = BIOME_COLOR[r.biome] or BIOME_COLOR.fire
card.Artwork:SetVertexColor(biome[1], biome[2], biome[3], 1)
card.Name:SetText(r.name or "")
card.Loc:SetText(r.loc or "")
card.Level:SetText(r.level or "")
end
parent:SetWidth(math.max(1, n * (cardW + gap)))
section.Count:SetText("· "..n)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Главный Refresh
local function showState(state)
local F = PlayerGuideFrame
local hasData = (state == "data")
showOrHide(F.StageChain, hasData)
showOrHide(F.Header, hasData)
showOrHide(F.ObjectivesSection, hasData)
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
showOrHide(F.EmptyState, state == "empty")
showOrHide(F.LoadingState, state == "loading")
end
local function refresh()
if not PlayerGuideFrame then return end
local state = C_PlayerGuide.GetState()
showState(state)
if state ~= "data" then return end
-- Заголовок этапа
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
local stages = C_PlayerGuide.GetStages() or {}
PlayerGuideFrame.Header.Pretitle:SetText(
string.format(MW_PLAYER_GUIDE_STAGE_OF or "Этап %d из %d", id or 0, #stages))
PlayerGuideFrame.Header.Title:SetText(name or "")
PlayerGuideFrame.Header.Description:SetText(desc or "")
PlayerGuideFrame.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
PlayerGuideFrame.Header.ProgressPercent:SetText((progress or 0) .. "%")
PlayerGuideFrame.Header.ProgressBar:SetMinMaxValues(0, 100)
PlayerGuideFrame.Header.ProgressBar:SetValue(progress or 0)
buildStageChain(stages)
renderObjectives()
renderRecommendations()
end
-- ──────────────────────────────────────────────────────────────────────────
-- XML script handlers
function MW_PlayerGuide_OnLoad(self)
-- Подписываемся на изменения C_PlayerGuide
C_PlayerGuide.RegisterListener(function() refresh() end)
end
function MW_PlayerGuide_OnShow(self)
-- Если данных ещё нет — попросим сервер
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
refresh()
end
function MW_PlayerGuide_RefreshButton_OnClick(self)
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
-- Клик по карточке цели — открывает связанный таргет (см. требования)
function MW_PlayerGuide_Objective_OnClick(self)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID)
end
elseif o.type == "boss" then
if o.encounterID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID, o.encounterID)
end
elseif o.type == "achievement" and o.achievementID then
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
if AchievementFrame_SelectAchievement then
AchievementFrame_SelectAchievement(o.achievementID)
end
elseif o.type == "item" and o.itemID then
-- Запрос подсказки предмета
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemByID(o.itemID)
GameTooltip:Show()
end
else
-- Любой нестандартный тип — пытаемся попросить сервер открыть таргет
C_PlayerGuide.RequestOpenTarget(o.id)
end
end
function MW_PlayerGuide_Objective_OnEnter(self)
local o = self.objectiveData
if not o then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
if o.subtitle and o.subtitle ~= "" then
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
end
if not o.completed then
GameTooltip:AddLine(string.format("Прогресс: %d / %d", o.progress, o.required),
0.79, 0.65, 0.38)
end
GameTooltip:Show()
end
function MW_PlayerGuide_Objective_OnLeave(self) GameTooltip:Hide() end
function MW_PlayerGuide_Rec_OnClick(self)
local r = self.recData
if r and r.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(r.instanceID)
end
end
function MW_PlayerGuide_Rec_OnEnter(self)
local r = self.recData
if not r then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(r.name or "", 1, 0.91, 0.63)
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
if r.level then GameTooltip:AddLine("Уровень: "..r.level, 0.79, 0.65, 0.38) end
GameTooltip:Show()
end
function MW_PlayerGuide_Rec_OnLeave(self) GameTooltip:Hide() end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичные хелперы для Custom_EncounterJournal.lua (routing вкладки)
function PlayerGuide_Show()
if PlayerGuideFrame then
PlayerGuideFrame:Show()
end
end
function PlayerGuide_Hide()
if PlayerGuideFrame then
PlayerGuideFrame:Hide()
end
end
-- Глобальный экспорт для отладки
MW_PlayerGuide = {
Refresh = refresh,
Show = PlayerGuide_Show,
Hide = PlayerGuide_Hide,
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
}
+498
View File
@@ -0,0 +1,498 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<!-- ============================================================
PlayerGuide.xml — вкладка «Путеводитель» в Encounter Journal
Канон WotLK: тёмно-синий фон, золотые акценты, Friz Quadrata.
Подключается ПОСЛЕ Custom_EncounterJournal.xml в .toc, чтобы
PlayerGuideFrame мог анкориться к EncounterJournal как parent.
============================================================ -->
<!-- ───────── Шрифты (Friz Quadrata из клиента 3.3.5a) ───────── -->
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
<Color r="0.85" g="0.78" b="0.63"/>
</Font>
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
<Color r="0.63" g="0.46" b="0.19"/>
</Font>
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
<Color r="0.66" g="0.59" b="0.47"/>
</Font>
<!-- ───────── Backdrop'ы ───────── -->
<!-- Карточка цели (полупрозрачная пергамент-плашка) -->
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
<Size><AbsDimension x="0" y="44"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border"
tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.22" g="0.15" b="0.07" a="0.65"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Иконка типа цели (32×32, золотая рамка применяется отдельной текстурой) -->
<Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
<Texture parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="42" y="42"/></Size>
<Anchors><Anchor point="CENTER" relativeKey="$parent.Icon"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="10" y="-1"/></Offset>
</Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="6" y="0"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-10" y="-6"/></Offset></Anchor>
</Anchors>
<Color r="0.79" g="0.65" b="0.38"/>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="0" y="6"/></Size>
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Icon" relativePoint="RIGHT">
<Offset><AbsDimension x="10" y="-12"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="-12"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
</Frames>
<Scripts>
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
</Scripts>
</Frame>
<!-- Карточка рекомендованного подземелья -->
<Button name="MW_PG_RecCardTemplate" virtual="true">
<Size><AbsDimension x="148" y="106"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.10" g="0.07" b="0.03" a="0.85"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Биом-плашка (плейсхолдер: цветной градиент) -->
<Texture parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="138" y="58"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Level" inherits="GameFontNormalSmall" justifyH="CENTER">
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.Artwork" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-4" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="130" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Artwork" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="130" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Name" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
</Scripts>
</Button>
<!-- Бейдж этапа в цепочке (круглая золотая монета с цифрой) -->
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
<Size><AbsDimension x="38" y="38"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BadgeBG" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- ───────── Корневой фрейм ───────── -->
<!-- Парент: EncounterJournal.encounter (см. PlayerGuide.lua → Init) -->
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-58"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<!-- «Волна» сверху, как в оригинальном EJ — добавляется через
родительский фон. Здесь — тонкая золотая внутренняя рамка. -->
<Layer level="BORDER">
<Texture parentKey="TopGlow" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.84" b="0.42" a="0.35"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- 1) Цепочка этапов сверху -->
<Frame parentKey="StageChain">
<Size><AbsDimension x="0" y="62"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-4"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-8" y="-4"/></Offset></Anchor>
</Anchors>
<!-- Бейджи создаются программно в Lua (PlayerGuide_BuildStageChain) -->
</Frame>
<!-- 2) Заголовок текущего этапа -->
<Frame parentKey="Header">
<Size><AbsDimension x="0" y="78"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.StageChain" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="12" y="-12"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.StageChain" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-12" y="-12"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Pretitle" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Title" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-6"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
<Size><AbsDimension x="240" y="28"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressLabel" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="240" y="10"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressPercent" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="100" y="22"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressBar" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 3) Секция «Задачи этапа» — заголовок + список карточек -->
<Frame parentKey="ObjectivesSection">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Header" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="-2" y="-10"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.Header" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="2" y="-10"/></Offset>
</Anchor>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="142"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="600" y="200"/></Size>
<!-- Карточки целей вставляются программно через ObjectiveCardTemplate -->
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 4) Секция «Подходящие подземелья» — горизонтальная карусель -->
<Frame parentKey="RecommendationsSection">
<Size><AbsDimension x="0" y="132"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="6" y="6"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="900" y="110"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 5) Пустое состояние -->
<Frame parentKey="EmptyState" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="200"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="28"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="420" y="60"/></Size>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="120" y="24"/></Size>
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Body" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-14"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 6) Состояние загрузки -->
<Frame parentKey="LoadingState" hidden="true">
<Anchors><Anchor point="CENTER"/></Anchors>
<Size><AbsDimension x="300" y="80"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
<Anchors><Anchor point="TOP"/></Anchors>
<Size><AbsDimension x="300" y="20"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="300" y="16"/></Size>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
</Scripts>
</Frame>
</Ui>
+64
View File
@@ -0,0 +1,64 @@
# 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`).
@@ -0,0 +1,120 @@
--[[----------------------------------------------------------------------------
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)
@@ -0,0 +1,57 @@
--[[----------------------------------------------------------------------------
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"/>
@@ -0,0 +1,43 @@
--[[----------------------------------------------------------------------------
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 = "ЗАДАЧА"
@@ -0,0 +1,28 @@
# patches/MoonWellClient.toc.patch
#
# Сниппет для добавления в существующий MoonWellClient.toc. ВАЖНО: порядок
# подключения файлов критичен — C_PlayerGuide.lua должен идти ДО PlayerGuide.lua,
# а PlayerGuide.xml — ПОСЛЕ Custom_EncounterJournal.xml (PlayerGuideFrame
# анкорится к EncounterJournal как parent).
#
# Вставьте строки ниже в соответствующие места вашего .toc.
# ─── Локализация ────────────────────────────────────────────────────────────
# В блок Localization, рядом с остальными ruRU.lua:
Localization\ruRU.lua
# ─── Хранилище и API ────────────────────────────────────────────────────────
# В блок EncounterJournal\Utils\, до подключения чего-либо из
# Custom_EncounterJournal\:
EncounterJournal\Utils\C_PlayerGuide.lua
# ─── Сам Encounter Journal (ОБНОВЛЁННЫЙ ПОРЯДОК) ────────────────────────────
# Эти строки заменяют ваш существующий блок Custom_EncounterJournal:
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.lua
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
# ─── AIO-handler ────────────────────────────────────────────────────────────
# Должен подключаться ПОСЛЕ C_PlayerGuide.lua и AIO/AIO_Client.lua:
AIO\MoonWellPlayerGuide.lua
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

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

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+263
View File
@@ -0,0 +1,263 @@
<!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>
+448
View File
@@ -0,0 +1,448 @@
# 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
```
@@ -69,25 +69,26 @@ local MainMenuBarBackpackButton = _G.MainMenuBarBackpackButton;
local HelpMicroButton = _G.HelpMicroButton; local HelpMicroButton = _G.HelpMicroButton;
local KeyRingButton = _G.KeyRingButton; local KeyRingButton = _G.KeyRingButton;
-- Button collections (dynamically set based on server) -- Button collections (rebuilt after addons create optional custom buttons).
local MICRO_BUTTONS local function BuildMicroButtonList()
if isAscensionServer then
return {
_G.CharacterMicroButton,
_G.SpellbookMicroButton,
_G.TalentMicroButton,
_G.AchievementMicroButton,
_G.QuestLogMicroButton,
_G.SocialsMicroButton,
_G.LFDMicroButton,
_G.EncounterJournalMicroButton,
_G.PathToAscensionMicroButton,
_G.ChallengesMicroButton,
_G.MainMenuMicroButton,
_G.HelpMicroButton
}
end
if isAscensionServer then return {
MICRO_BUTTONS = {
_G.CharacterMicroButton,
_G.SpellbookMicroButton,
_G.TalentMicroButton,
_G.AchievementMicroButton,
_G.QuestLogMicroButton,
_G.SocialsMicroButton,
_G.LFDMicroButton,
_G.PathToAscensionMicroButton,
_G.ChallengesMicroButton,
_G.MainMenuMicroButton,
_G.HelpMicroButton
}
else
MICRO_BUTTONS = {
_G.CharacterMicroButton, _G.CharacterMicroButton,
_G.SpellbookMicroButton, _G.SpellbookMicroButton,
_G.TalentMicroButton, _G.TalentMicroButton,
@@ -95,6 +96,7 @@ else
_G.QuestLogMicroButton, _G.QuestLogMicroButton,
_G.SocialsMicroButton, _G.SocialsMicroButton,
_G.LFDMicroButton, _G.LFDMicroButton,
_G.EncounterJournalMicroButton,
_G.CollectionsMicroButton, _G.CollectionsMicroButton,
_G.PVPMicroButton, _G.PVPMicroButton,
_G.MainMenuMicroButton, _G.MainMenuMicroButton,
@@ -102,6 +104,7 @@ else
} }
end end
local MICRO_BUTTONS = BuildMicroButtonList()
local bagslots = {_G.CharacterBag0Slot, _G.CharacterBag1Slot, _G.CharacterBag2Slot, _G.CharacterBag3Slot}; local bagslots = {_G.CharacterBag0Slot, _G.CharacterBag1Slot, _G.CharacterBag2Slot, _G.CharacterBag3Slot};
@@ -783,6 +786,8 @@ local function ApplyMicromenuSystem()
return return
end end
MICRO_BUTTONS = BuildMicroButtonList()
-- Store original states first -- Store original states first
StoreOriginalMicroButtonStates() StoreOriginalMicroButtonStates()
@@ -911,6 +916,8 @@ local function ApplyMicromenuSystem()
or (_G.PVPParentFrame and _G.PVPParentFrame:IsVisible() and true or false) or (_G.PVPParentFrame and _G.PVPParentFrame:IsVisible() and true or false)
or (_G.BattlefieldFrame and _G.BattlefieldFrame:IsVisible() and true or false) or (_G.BattlefieldFrame and _G.BattlefieldFrame:IsVisible() and true or false)
or (_G.HonorFrame and _G.HonorFrame:IsVisible() and true or false) or (_G.HonorFrame and _G.HonorFrame:IsVisible() and true or false)
elseif buttonName == "EncounterJournal" then
return (_G.EncounterJournal and _G.EncounterJournal:IsVisible() and true or false)
end end
return pressed return pressed
@@ -919,6 +926,152 @@ local function ApplyMicromenuSystem()
-- ============================================================================ -- ============================================================================
-- SECTION 5: SPECIALIZED BUTTON SETUP -- SECTION 5: SPECIALIZED BUTTON SETUP
-- ============================================================================ -- ============================================================================
local function SetupEncounterJournalButton(button)
local iconTexture = 'Interface\\EncounterJournal\\UI-EJ-PortraitIcon'
local backgroundTexture = 'Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\uimicromenu2x'
local buttonWidth, buttonHeight = button:GetSize()
local dx, dy = -1, 1
local offX, offY = button:GetPushedTextOffset()
if not button.DragonUIEncounterJournalIcon then
button.DragonUIEncounterJournalIcon = button:CreateTexture(nil, 'ARTWORK')
end
local icon = button.DragonUIEncounterJournalIcon
icon:SetTexture(iconTexture)
icon:SetTexCoord(0, 1, 0, 1)
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
icon:SetSize(buttonWidth - 8, buttonWidth - 8)
icon:SetAlpha(1)
icon:Show()
local highlightTexture = button:GetHighlightTexture()
if highlightTexture then
highlightTexture:SetTexture(iconTexture)
highlightTexture:SetTexCoord(0, 1, 0, 1)
highlightTexture:ClearAllPoints()
highlightTexture:SetAllPoints(icon)
highlightTexture:SetBlendMode('ADD')
highlightTexture:SetAlpha(0.55)
end
if not button.DragonUIBackground then
local bg = button:CreateTexture(nil, 'BACKGROUND')
bg:SetTexture(backgroundTexture)
bg:SetSize(buttonWidth, buttonHeight + 1)
bg:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
bg:SetPoint('CENTER', dx, dy)
button.DragonUIBackground = bg
local bgPushed = button:CreateTexture(nil, 'BACKGROUND')
bgPushed:SetTexture(backgroundTexture)
bgPushed:SetSize(buttonWidth, buttonHeight + 1)
bgPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
bgPushed:SetPoint('CENTER', dx + offX, dy + offY)
bgPushed:Hide()
button.DragonUIBackgroundPushed = bgPushed
else
button.DragonUIBackground:SetTexture(backgroundTexture)
button.DragonUIBackground:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
button.DragonUIBackground:ClearAllPoints()
button.DragonUIBackground:SetPoint('CENTER', dx, dy)
button.DragonUIBackground:SetSize(buttonWidth, buttonHeight + 1)
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:SetTexture(backgroundTexture)
button.DragonUIBackgroundPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
button.DragonUIBackgroundPushed:ClearAllPoints()
button.DragonUIBackgroundPushed:SetPoint('CENTER', dx + offX, dy + offY)
button.DragonUIBackgroundPushed:SetSize(buttonWidth, buttonHeight + 1)
end
end
button.dragonUIState = button.dragonUIState or {}
button.dragonUIState.pushed = IsSpecialMicroButtonActive(button, "EncounterJournal")
button.dragonUILastState = button.dragonUIState.pushed
button.dragonUITimer = button.dragonUITimer or 0
button.HandleDragonUIState = function()
local state = button.dragonUIState
local hlTex = button:GetHighlightTexture()
if state and state.pushed then
if icon then
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', offX, offY)
icon:SetAlpha(0.7)
end
if button.DragonUIBackground then
button.DragonUIBackground:Hide()
end
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:Show()
end
if hlTex then
hlTex:ClearAllPoints()
hlTex:SetPoint('TOPLEFT', icon, 'TOPLEFT', 0, 0)
hlTex:SetPoint('BOTTOMRIGHT', icon, 'BOTTOMRIGHT', 0, 0)
end
else
if icon then
icon:ClearAllPoints()
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
icon:SetAlpha(1)
end
if button.DragonUIBackground then
button.DragonUIBackground:Show()
end
if button.DragonUIBackgroundPushed then
button.DragonUIBackgroundPushed:Hide()
end
if hlTex then
hlTex:ClearAllPoints()
hlTex:SetAllPoints(icon)
end
end
end
button:SetScript('OnUpdate', function(self, elapsed)
self.dragonUITimer = (self.dragonUITimer or 0) + elapsed
if self.dragonUITimer >= 0.1 then
self.dragonUITimer = 0
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
if currentState ~= self.dragonUILastState then
self.dragonUILastState = currentState
if self.dragonUIState then
self.dragonUIState.pushed = currentState
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end
end
end)
if not button.DragonUIStateHooks then
button:HookScript('OnMouseDown', function(self)
if self.dragonUIState then
self.dragonUIState.pushed = true
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end)
button:HookScript('OnMouseUp', function(self)
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
if self.dragonUIState then
self.dragonUIState.pushed = currentState
end
if self.HandleDragonUIState then
self.HandleDragonUIState()
end
end)
button.DragonUIStateHooks = true
end
button.HandleDragonUIState()
end
local function SetupPVPButton(button) local function SetupPVPButton(button)
-- Mirror the Character button pattern: -- Mirror the Character button pattern:
-- Instead of fighting WoW's internal NormalTexture alpha management, -- Instead of fighting WoW's internal NormalTexture alpha management,
@@ -1582,6 +1735,8 @@ local function ApplyMicromenuSystem()
end end
local function setupMicroButtons(xOffset) local function setupMicroButtons(xOffset)
MICRO_BUTTONS = BuildMicroButtonList()
local buttonxOffset = 0 local buttonxOffset = 0
local useGrayscale = addon.db.profile.micromenu.grayscale_icons local useGrayscale = addon.db.profile.micromenu.grayscale_icons
@@ -1705,9 +1860,11 @@ local function ApplyMicromenuSystem()
local isCharacterButton = (buttonName == "Character") local isCharacterButton = (buttonName == "Character")
local isPVPButton = (buttonName == "PVP") local isPVPButton = (buttonName == "PVP")
local isEncounterJournalButton = (buttonName == "EncounterJournal")
local upCoords = not isCharacterButton and not isPVPButton and GetColoredTextureCoords(name, "Up") or nil local upCoords = not isCharacterButton and not isPVPButton and not isEncounterJournalButton and GetColoredTextureCoords(name, "Up") or nil
local shouldUseGrayscale = useGrayscale or (not isPVPButton and not upCoords and not isCharacterButton) local shouldUseGrayscale = (useGrayscale and not isEncounterJournalButton) or
(not isPVPButton and not isEncounterJournalButton and not upCoords and not isCharacterButton)
if shouldUseGrayscale then if shouldUseGrayscale then
-- Grayscale icons -- Grayscale icons
@@ -1741,6 +1898,8 @@ local function ApplyMicromenuSystem()
end end
elseif isPVPButton then elseif isPVPButton then
SetupPVPButton(button) SetupPVPButton(button)
elseif isEncounterJournalButton then
SetupEncounterJournalButton(button)
elseif isCharacterButton then elseif isCharacterButton then
SetupCharacterButton(button) SetupCharacterButton(button)
else else
@@ -2245,6 +2404,8 @@ end
return return
end end
MICRO_BUTTONS = BuildMicroButtonList()
local useGrayscale = addon.db.profile.micromenu.grayscale_icons local useGrayscale = addon.db.profile.micromenu.grayscale_icons
local configMode = useGrayscale and "grayscale" or "normal" local configMode = useGrayscale and "grayscale" or "normal"
local config = addon.db.profile.micromenu[configMode] local config = addon.db.profile.micromenu[configMode]
@@ -2107,7 +2107,7 @@
</Layer> </Layer>
</Layers> </Layers>
<Frames> <Frames>
<Button name="$parentSuggestTab" inherits="EncounterTierTabTemplate" text="AJ_SUGGESTED_CONTENT_TAB" parentKey="suggestTab" id="1"> <Button name="$parentGuideTab" inherits="EncounterTierTabTemplate" text="MW_PLAYER_GUIDE" parentKey="guideTab" id="5">
<Anchors> <Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="TOPLEFT" x="25" y="-45"/> <Anchor point="BOTTOMLEFT" relativePoint="TOPLEFT" x="25" y="-45"/>
</Anchors> </Anchors>
@@ -2115,6 +2115,14 @@
<OnClick function="EJ_ContentTab_OnClick"/> <OnClick function="EJ_ContentTab_OnClick"/>
</Scripts> </Scripts>
</Button> </Button>
<Button name="$parentSuggestTab" inherits="EncounterTierTabTemplate" text="AJ_SUGGESTED_CONTENT_TAB" parentKey="suggestTab" id="1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentGuideTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/>
</Anchors>
<Scripts>
<OnClick function="EJ_ContentTab_OnClick"/>
</Scripts>
</Button>
<Button name="$parentDungeonTab" inherits="EncounterTierTabTemplate" text="INSTANCES" parentKey="dungeonsTab" id="2"> <Button name="$parentDungeonTab" inherits="EncounterTierTabTemplate" text="INSTANCES" parentKey="dungeonsTab" id="2">
<Anchors> <Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentSuggestTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/> <Anchor point="BOTTOMLEFT" relativeTo="$parentSuggestTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/>
@@ -0,0 +1,484 @@
local SHOW_RECOMMENDATIONS = true
local DENSITY = "regular"
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 }
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
local BIOME_TEX = {
fire = T .. "Biome-Fire",
arcane = T .. "Biome-Arcane",
storm = T .. "Biome-Storm",
shadow = T .. "Biome-Shadow",
nature = T .. "Biome-Nature",
holy = T .. "Biome-Holy",
}
local BADGE_TEX = {
active = T .. "StageBadge-Active",
done = T .. "StageBadge-Done",
locked = T .. "StageBadge-Locked",
}
local TYPE_ICON = {
level = T .. "TypeIcon-Level",
quest = T .. "TypeIcon-Quest",
quest_chain = T .. "TypeIcon-QuestChain",
dungeon = T .. "TypeIcon-Dungeon",
raid = T .. "TypeIcon-Raid",
boss = T .. "TypeIcon-Boss",
achievement = T .. "TypeIcon-Achievement",
reputation = T .. "TypeIcon-Reputation",
profession = T .. "TypeIcon-Profession",
item = T .. "TypeIcon-Item",
currency = T .. "TypeIcon-Currency",
custom = T .. "TypeIcon-Custom",
done = T .. "TypeIcon-Done",
locked = T .. "TypeIcon-Locked",
}
local function showOrHide(frame, show)
if not frame then return end
if show then frame:Show() else frame:Hide() end
end
local function getScrollBar(scrollFrame)
if not scrollFrame then return nil end
return scrollFrame.ScrollBar or _G[scrollFrame:GetName() .. "ScrollBar"]
end
local function updateScrollBar(scrollFrame, contentHeight, contentWidth)
if not scrollFrame then return end
local viewHeight = scrollFrame:GetHeight() or 0
local viewWidth = scrollFrame:GetWidth() or 0
local scrollBar = getScrollBar(scrollFrame)
if scrollFrame.ScrollChild and viewWidth > 0 then
scrollFrame.ScrollChild:SetWidth(math.max(1, contentWidth or viewWidth))
end
if scrollBar then
if contentHeight and viewHeight > 0 and contentHeight > viewHeight + 2 then
scrollBar:Show()
else
scrollBar:Hide()
end
end
end
local function hideScrollBar(scrollFrame)
local scrollBar = getScrollBar(scrollFrame)
if scrollBar then scrollBar:Hide() end
end
local function getBodyChild()
local body = PlayerGuideFrame and PlayerGuideFrame.BodyScroll
return body and body.ScrollChild
end
-- ─────────────────────────────────── Цепочка этапов ──
local stageBadges = {}
local function buildStageChain(stages)
if not PlayerGuideFrame then return end
local container = PlayerGuideFrame.StageChain
for i = (#stages + 1), #stageBadges do stageBadges[i]:Hide() end
if #stages == 0 then return end
local width = container:GetWidth()
if width == 0 then width = 600 end
local slot = width / #stages
for i, s in ipairs(stages) do
local badge = stageBadges[i]
if not badge then
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
stageBadges[i] = badge
end
badge:ClearAllPoints()
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
badge:Show()
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
badge.Label:SetText(s.short or s.name or "")
local status = s.status or "locked"
badge.BadgeBG:SetTexture(BADGE_TEX[status] or BADGE_TEX.locked)
if status == "active" then
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
badge.Label:SetTextColor(1.0, 0.84, 0.42)
elseif status == "done" then
badge.Roman:SetText("v")
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
badge.Label:SetTextColor(0.79, 0.65, 0.38)
else
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
badge.Label:SetTextColor(0.40, 0.34, 0.22)
end
end
end
-- ─────────────────────────────────── Карточки целей ──
local objectiveCards = {}
local CARD_HEIGHT = { compact = 54, regular = 70, cozy = 82 }
local CARD_GAP = { compact = 6, regular = 8, cozy = 10 }
local function renderObjectives()
local section = PlayerGuideFrame.ObjectivesSection
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumObjectives()
local width = 0
if PlayerGuideFrame.BodyScroll then
width = PlayerGuideFrame.BodyScroll:GetWidth()
end
if width == 0 then width = section:GetWidth() end
if width == 0 then width = section.ScrollFrame:GetWidth() end
if width == 0 then width = 860 end
-- Body child right inset 8, section right inset 0, scrollframe left/right insets 6/8.
-- The card then keeps 8px equal visual padding on both sides of the objective row.
parent:SetWidth(math.max(1, width - 30))
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
local h = CARD_HEIGHT[DENSITY] or 44
local gap = CARD_GAP[DENSITY] or 4
local done = 0
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if not o then break end
local card = objectiveCards[i]
if not card then
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
objectiveCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
card:SetHeight(h)
card.objectiveData = o
card:Show()
if card.SetBackdropColor then
card:SetBackdropColor(0, 0, 0, 0.72)
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
end
card.TypeLabel:SetText(_G["MW_PG_TYPE_" .. string.upper(o.type or "custom")] or string.upper(o.type or ""))
card.Title:SetText(o.title or "")
card.Subtitle:SetText(o.subtitle or o.description or "")
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
card.ProgressBar:SetValue(o.progress or 0)
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
if o.completed then
done = done + 1
card.Icon:SetTexture(TYPE_ICON.done)
card.Icon:SetTexCoord(0, 1, 0, 1)
card.Title:SetTextColor(0.62, 0.84, 0.47)
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
card.Count:SetTextColor(0.62, 0.84, 0.47)
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
if card.SetBackdropBorderColor then
card:SetBackdropBorderColor(0.26, 0.52, 0.18, 0.9)
end
else
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
card.Icon:SetTexCoord(0, 1, 0, 1)
card.Title:SetTextColor(1.0, 0.91, 0.63)
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
card.Count:SetTextColor(0.79, 0.65, 0.38)
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
end
if o.required == 1 and (o.progress or 0) == 0 then
card.ProgressBar:Hide()
else
card.ProgressBar:Show()
end
end
parent:SetHeight(math.max(1, n * h + math.max(0, n - 1) * gap))
section:SetHeight(48 + parent:GetHeight())
hideScrollBar(section.ScrollFrame)
section.Count:SetText(string.format(" %d / %d", done, n))
end
-- ─────────────────────────────── Карточки рекомендаций ──
local recCards = {}
local function renderRecommendations()
local section = PlayerGuideFrame.RecommendationsSection
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumRecommendations()
if n == 0 then section:Hide(); return end
section:Show()
for i = (n + 1), #recCards do recCards[i]:Hide() end
local cardW = 176
local gap = 16
for i = 1, n do
local r = C_PlayerGuide.GetRecommendationInfo(i)
if not r then break end
local card = recCards[i]
if not card then
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
recCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
card.recData = r
card:Show()
if card.SetBackdropColor then
card:SetBackdropColor(0, 0, 0, 0.88)
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
end
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
card.Name:SetText(r.name or r.title or "")
card.Loc:SetText(r.loc or r.description or "")
card.Level:SetText(r.level or "")
end
parent:SetWidth(math.max(1, n * cardW + math.max(0, n - 1) * gap))
parent:SetHeight(116)
section:SetHeight(162)
hideScrollBar(section.ScrollFrame)
section.Count:SetText(" " .. n)
end
local function updateBodyContent()
local F = PlayerGuideFrame
local child = getBodyChild()
if not F or not child then return end
local width = F.BodyScroll:GetWidth()
if width == 0 then width = 860 end
child:SetWidth(math.max(1, width - 8))
local y = 0
F.ObjectivesSection:ClearAllPoints()
F.ObjectivesSection:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
F.ObjectivesSection:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, y)
y = y - F.ObjectivesSection:GetHeight() - 16
if F.RecommendationsSection:IsShown() then
F.RecommendationsSection:ClearAllPoints()
F.RecommendationsSection:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
F.RecommendationsSection:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, y)
y = y - F.RecommendationsSection:GetHeight()
end
local contentHeight = math.max(1, -y + 8)
child:SetHeight(contentHeight)
updateScrollBar(F.BodyScroll, contentHeight, child:GetWidth())
end
-- ─────────────────────────────────────── Layout ──
local function applyLayout(self, hasChain)
local chain = self.StageChain
local hdr = self.Header
local body = self.BodyScroll
chain:ClearAllPoints()
chain:SetPoint("TOPLEFT", self, "TOPLEFT", 28, -8)
chain:SetPoint("TOPRIGHT", self, "TOPRIGHT", -28, -8)
hdr:ClearAllPoints()
if hasChain then
hdr:SetPoint("TOPLEFT", chain, "BOTTOMLEFT", 0, -6)
hdr:SetPoint("TOPRIGHT", chain, "BOTTOMRIGHT", 0, -6)
else
hdr:SetPoint("TOPLEFT", self, "TOPLEFT", 30, -68)
hdr:SetPoint("TOPRIGHT", self, "TOPRIGHT", -30, -68)
end
body:ClearAllPoints()
body:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", -4, -8)
body:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -30, 16)
end
-- ─────────────────────────────────────── Show/hide ──
local function showState(state)
local F = PlayerGuideFrame
local hasData = (state == "data")
-- StageChain visibility controlled in refresh() based on #stages
showOrHide(F.Header, hasData)
showOrHide(F.BodyScroll, hasData)
showOrHide(F.ObjectivesSection, hasData)
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
showOrHide(F.EmptyState, state == "empty")
showOrHide(F.LoadingState, state == "loading")
if not hasData then showOrHide(F.StageChain, false) end
end
-- ─────────────────────────────────────── Render ──
local function refresh()
if not PlayerGuideFrame then return end
local state = C_PlayerGuide.GetState()
showState(state)
if state ~= "data" then return end
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
local stages = C_PlayerGuide.GetStages() or {}
local F = PlayerGuideFrame
if #stages > 0 then
F.Header.Pretitle:SetText(string.format(
MW_PLAYER_GUIDE_STAGE_OF or "ЭТАП %d ИЗ %d", id or 0, #stages))
else
F.Header.Pretitle:SetText(string.format("ЭТАП %d", id or 0))
end
F.Header.Title:SetText(name or "")
F.Header.Description:SetText(desc or "")
F.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
F.Header.ProgressPercent:SetText((progress or 0) .. "%")
F.Header.ProgressBar:SetMinMaxValues(0, 100)
F.Header.ProgressBar:SetValue(progress or 0)
local hasChain = #stages > 0
showOrHide(F.StageChain, hasChain)
applyLayout(F, hasChain)
buildStageChain(stages)
updateBodyContent()
renderObjectives()
renderRecommendations()
updateBodyContent()
end
-- ─────────────────────────────────── XML callbacks ──
function MW_PlayerGuide_OnLoad(self)
EncounterJournal.playerGuideFrame = self
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
if bodyChild then
self.ObjectivesSection:SetParent(bodyChild)
self.RecommendationsSection:SetParent(bodyChild)
end
applyLayout(self, false)
local objScrollBar = getScrollBar(self.ObjectivesSection.ScrollFrame)
if objScrollBar then objScrollBar:Hide() end
local recScrollBar = getScrollBar(self.RecommendationsSection.ScrollFrame)
if recScrollBar then recScrollBar:Hide() end
hideScrollBar(self.BodyScroll)
C_PlayerGuide.RegisterListener(function() refresh() end)
end
function MW_PlayerGuide_OnShow()
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
refresh()
end
function MW_PlayerGuide_RefreshButton_OnClick()
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
function MW_PlayerGuide_Objective_OnClick(self)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and o.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(o.instanceID)
EncounterJournal_DisplayInstance(o.instanceID)
end
elseif o.type == "boss" then
if o.encounterID and o.encounterID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EncounterJournal_DisplayEncounter(o.encounterID)
end
elseif o.type == "achievement" then
if o.achievementID then
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
if AchievementFrame_SelectAchievement then
AchievementFrame_SelectAchievement(o.achievementID)
end
end
elseif o.type == "item" then
if o.itemID and o.itemID > 0 then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemByID(o.itemID)
GameTooltip:Show()
end
elseif o.type == "quest" or o.type == "quest_chain" then
local qid = o.questID or o.questChainID or o.targetID
if qid and qid > 0 then
pcall(SetItemRef, "quest:" .. qid .. ":-1", "", "LeftButton")
end
else
C_PlayerGuide.RequestOpenTarget(o.id)
end
end
function MW_PlayerGuide_Objective_OnEnter(self)
local o = self.objectiveData
if not o then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
if o.subtitle and o.subtitle ~= "" then
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
end
if not o.completed then
GameTooltip:AddLine(
string.format("Прогресс: %d / %d", o.progress or 0, o.required or 1),
0.79, 0.65, 0.38)
end
GameTooltip:Show()
end
function MW_PlayerGuide_Objective_OnLeave() GameTooltip:Hide() end
function MW_PlayerGuide_Rec_OnClick(self)
local r = self.recData
if r and r.instanceID and r.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(r.instanceID)
EncounterJournal_DisplayInstance(r.instanceID)
end
end
function MW_PlayerGuide_Rec_OnEnter(self)
local r = self.recData
if not r then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(r.name or r.title or "", 1, 0.91, 0.63)
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
if r.level then GameTooltip:AddLine("Уровень: " .. r.level, 0.79, 0.65, 0.38) end
GameTooltip:Show()
end
function MW_PlayerGuide_Rec_OnLeave() GameTooltip:Hide() end
-- ─────────────────────────────── Экспорт для отладки ──
MW_PlayerGuide = {
Refresh = refresh,
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
}
@@ -0,0 +1,486 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\..\FrameXML\UI.xsd">
<Script file="PlayerGuide.lua"/>
<!-- ───────── Шрифты ───────── -->
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
<Color r="0.85" g="0.78" b="0.63"/>
</Font>
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
<Color r="0.63" g="0.46" b="0.19"/>
</Font>
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
<Color r="0.66" g="0.59" b="0.47"/>
</Font>
<!-- ───────── Карточка цели ───────── -->
<!-- Высота = 60px (DENSITY "regular"). Позиции: TypeLabel+Title строка 1, Subtitle строка 2, бар внизу. -->
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
<Size><AbsDimension x="0" y="70"/></Size>
<Backdrop bgFile="Interface\Buttons\WHITE8X8"
edgeFile="Interface\Buttons\WHITE8X8"
tile="true">
<BackgroundInsets><AbsInset left="0" right="0" top="0" bottom="0"/></BackgroundInsets>
<EdgeSize><AbsValue val="1"/></EdgeSize>
<TileSize><AbsValue val="64"/></TileSize>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<!-- Рамка иконки — фоновый слой, центрируется по LEFT -->
<Texture name="$parentIconBorder" parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="50" y="50"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor>
</Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentIcon" parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
<Size><AbsDimension x="34" y="34"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Строка 1: TypeLabel (префикс) + Title (основной текст) на одной строке -->
<FontString name="$parentTypeLabel" parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
<Size><AbsDimension x="90" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="72" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="$parentTitle" parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="185" y="-16"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-118" y="-16"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Строка 2: Subtitle (описание/прогресс) -->
<FontString name="$parentSubtitle" parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="72" y="-36"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-118" y="-36"/></Offset></Anchor>
</Anchors>
</FontString>
<!-- Count: мелко снизу справа -->
<FontString name="$parentCount" parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="110" y="10"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-18" y="10"/></Offset></Anchor>
</Anchors>
<Color r="0.63" g="0.46" b="0.19"/>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Прогресс-бар: тонкая полоска у самого низа карточки -->
<StatusBar name="$parentProgressBar" parentKey="ProgressBar">
<Size><AbsDimension x="0" y="4"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="72" y="8"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-118" y="8"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
</Frames>
<Scripts>
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
</Scripts>
</Frame>
<!-- ───────── Карточка рекомендации ───────── -->
<Button name="MW_PG_RecCardTemplate" virtual="true">
<!-- Height 96: fits in the 100px visible area of RecommendationsSection scrollframe -->
<Size><AbsDimension x="176" y="116"/></Size>
<Backdrop bgFile="Interface\Buttons\WHITE8X8"
edgeFile="Interface\Buttons\WHITE8X8"
tile="true">
<BackgroundInsets><AbsInset left="0" right="0" top="0" bottom="0"/></BackgroundInsets>
<EdgeSize><AbsValue val="1"/></EdgeSize>
<TileSize><AbsValue val="64"/></TileSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="168" y="72"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
<FontString name="$parentLevel" parentKey="Level" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-9"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Name: 4px below artwork (artwork bottom y=61, name at y=65) -->
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-80"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<!-- Loc: 3px below name (name bottom y=79, loc at y=82) -->
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-96"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
</Scripts>
</Button>
<!-- ───────── Бейдж этапа ───────── -->
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
<Size><AbsDimension x="38" y="38"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBadgeBG" parentKey="BadgeBG"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Locked">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentRoman" parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
<Anchors><Anchor point="CENTER"/></Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString name="$parentLabel" parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- ───────── Корневой фрейм ───────── -->
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="EncounterJournalInset" relativePoint="TOPLEFT">
<Offset><AbsDimension x="5" y="-4"/></Offset>
</Anchor>
<Anchor point="BOTTOMRIGHT" relativeTo="EncounterJournalInset" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-3" y="0"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BackgroundPage"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Background-Page"
setAllPoints="true">
<TexCoords left="0" right="1" top="0" bottom="1"/>
</Texture>
</Layer>
<Layer level="BORDER">
<Texture parentKey="TabDivider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="28" y="-52"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-28" y="-52"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- 1) Цепочка этапов — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideStageChain" parentKey="StageChain">
<Size><AbsDimension x="0" y="62"/></Size>
</Frame>
<!-- 2) Заголовок этапа — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideHeader" parentKey="Header">
<Size><AbsDimension x="0" y="82"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideHeaderPretitle" parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderTitle" parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-18"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderDesc" parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-48"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderProgLabel" parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideHeaderProgPct" parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
<Size><AbsDimension x="240" y="28"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-14"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar name="PlayerGuideHeaderProgBar" parentKey="ProgressBar">
<Size><AbsDimension x="240" y="10"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-52"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
<Button name="PlayerGuideHeaderRefresh" parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="100" y="22"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="-72"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
self:SetNormalTexture(T.."Button-Up")
self:SetPushedTexture(T.."Button-Down")
self:SetHighlightTexture(T.."Button-Highlight")
</OnLoad>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 3) Задачи этапа — позиция задаётся Lua в OnLoad -->
<ScrollFrame name="PlayerGuideBodyScroll" parentKey="BodyScroll" inherits="UIPanelScrollFrameTemplate">
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="860" y="1"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
<Frame name="PlayerGuideObjSection" parentKey="ObjectivesSection">
<Layers>
<Layer level="OVERLAY">
<Texture name="PlayerGuideObjOrnament" parentKey="Ornament"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Corner-Ornament">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString name="PlayerGuideObjHeading" parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideObjCount" parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="PlayerGuideObjHeading" relativePoint="RIGHT">
<Offset><AbsDimension x="6" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="PlayerGuideObjDivider" parentKey="Divider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-24"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-24"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame name="PlayerGuideObjScroll" parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-48"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-8" y="0"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="600" y="200"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 4) Подходящие подземелья — позиция задаётся Lua в OnLoad -->
<Frame name="PlayerGuideRecsSection" parentKey="RecommendationsSection">
<Size><AbsDimension x="0" y="150"/></Size>
<Layers>
<Layer level="OVERLAY">
<Texture name="PlayerGuideRecsOrnament" parentKey="Ornament"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Corner-Ornament">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="-1"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString name="PlayerGuideRecsHeading" parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString name="PlayerGuideRecsCount" parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeTo="PlayerGuideRecsHeading" relativePoint="RIGHT">
<Offset><AbsDimension x="6" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="PlayerGuideRecsDivider" parentKey="Divider"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Section-Divider">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-24"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-4" y="-24"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame name="PlayerGuideRecScroll" parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="-46"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-36" y="0"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="900" y="96"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 5) Пустое состояние -->
<Frame name="PlayerGuideEmptyState" parentKey="EmptyState" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="200"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideEmptyTitle" parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="28"/></Size>
</FontString>
<FontString name="PlayerGuideEmptyBody" parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideEmptyTitle" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="420" y="60"/></Size>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="PlayerGuideEmptyRefresh" parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="120" y="24"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideEmptyBody" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-14"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
local T = "Interface\\AddOns\\MoonWellClient\\EncounterJournal\\Textures\\PlayerGuide\\"
self:SetNormalTexture(T.."Button-Up")
self:SetPushedTexture(T.."Button-Down")
self:SetHighlightTexture(T.."Button-Highlight")
</OnLoad>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 6) Загрузка -->
<Frame name="PlayerGuideLoadingState" parentKey="LoadingState" hidden="true">
<Anchors><Anchor point="CENTER"/></Anchors>
<Size><AbsDimension x="300" y="80"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="PlayerGuideLoadingTitle" parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
<Anchors><Anchor point="TOP"/></Anchors>
<Size><AbsDimension x="300" y="20"/></Size>
</FontString>
<FontString name="PlayerGuideLoadingBody" parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
<Anchors>
<Anchor point="TOP" relativeTo="PlayerGuideLoadingTitle" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="300" y="16"/></Size>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
</Scripts>
</Frame>
</Ui>
@@ -355,6 +355,7 @@ EnsureColorMethods(GREEN_FONT_COLOR)
EnsureColorMethods(DISABLED_FONT_COLOR) EnsureColorMethods(DISABLED_FONT_COLOR)
LOOTJOURNAL_SOURCE_TOOLTIP_HEAD = LOOTJOURNAL_SOURCE_TOOLTIP_HEAD or "Способ получения:" LOOTJOURNAL_SOURCE_TOOLTIP_HEAD = LOOTJOURNAL_SOURCE_TOOLTIP_HEAD or "Способ получения:"
RETURN_TO_DEFAULT = RETURN_TO_DEFAULT or "Настройки по умолчанию" RETURN_TO_DEFAULT = RETURN_TO_DEFAULT or "Настройки по умолчанию"
GUIDEBOOK = GUIDEBOOK or "Путеводитель"
INSTANCES = INSTANCES or "Подземелья" INSTANCES = INSTANCES or "Подземелья"
RAIDS = RAIDS or "Рейды" RAIDS = RAIDS or "Рейды"
LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты" LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты"
@@ -49,12 +49,14 @@ local function ConfigureTierTab(tab, text)
end end
local function PositionVisibleTierTabs(instanceSelect) local function PositionVisibleTierTabs(instanceSelect)
local guidebookTab = instanceSelect and instanceSelect.guidebookTab
local dungeonsTab = instanceSelect and instanceSelect.dungeonsTab local dungeonsTab = instanceSelect and instanceSelect.dungeonsTab
local raidsTab = instanceSelect and instanceSelect.raidsTab local raidsTab = instanceSelect and instanceSelect.raidsTab
if not dungeonsTab or not raidsTab then if not dungeonsTab or not raidsTab then
return return
end end
ConfigureTierTab(guidebookTab, GUIDEBOOK or "Guidebook")
ConfigureTierTab(dungeonsTab, INSTANCES or "Instances") ConfigureTierTab(dungeonsTab, INSTANCES or "Instances")
ConfigureTierTab(raidsTab, RAIDS or "Raids") ConfigureTierTab(raidsTab, RAIDS or "Raids")
@@ -96,10 +98,10 @@ local function SelectVisibleInstanceTab(journal)
end end
if journal:IsShown() if journal:IsShown()
and instanceSelect:IsShown() and instanceSelect:IsShown()
and (not journal.encounter or not journal.encounter:IsShown()) and (not journal.encounter or not journal.encounter:IsShown())
and selectedTab and selectedTab
and EJ_ContentTab_Select and EJ_ContentTab_Select
then then
EJ_ContentTab_Select(selectedTab) EJ_ContentTab_Select(selectedTab)
end end
@@ -15,10 +15,12 @@ local function MoonWell_ToggleEncounterJournal()
MoonWell_EncounterJournalApplyDungeonMode() MoonWell_EncounterJournalApplyDungeonMode()
end end
if EncounterJournal_OpenJournal then ShowUIPanel(EncounterJournal)
EncounterJournal_OpenJournal()
else if EJPlayerGuide_OpenFrame then
ShowUIPanel(EncounterJournal) EJPlayerGuide_OpenFrame()
elseif EncounterJournal.instanceSelect and EncounterJournal.instanceSelect.guideTab and EJ_ContentTab_Select then
EJ_ContentTab_Select(EncounterJournal.instanceSelect.guideTab:GetID())
end end
end end
@@ -28,3 +30,40 @@ SLASH_MOONWELLENCOUNTERJOURNAL3 = "/encounterjournal"
SlashCmdList.MOONWELLENCOUNTERJOURNAL = MoonWell_ToggleEncounterJournal SlashCmdList.MOONWELLENCOUNTERJOURNAL = MoonWell_ToggleEncounterJournal
MoonWell_ToggleEncounterJournal = MoonWell_ToggleEncounterJournal MoonWell_ToggleEncounterJournal = MoonWell_ToggleEncounterJournal
local function MoonWell_EncounterJournalMicroButton_OnEnter(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine("Путеводитель по приключениям", 1, 1, 1)
GameTooltip:Show()
end
local function MoonWell_CreateEncounterJournalMicroButton()
local button = EncounterJournalMicroButton
if not button then
local iconTexture = "Interface\\EncounterJournal\\UI-EJ-PortraitIcon"
button = CreateFrame("Button", "EncounterJournalMicroButton", MainMenuBarArtFrame or UIParent)
button:SetSize(32, 40)
button:SetPoint("BOTTOMLEFT", LFDMicroButton or MainMenuMicroButton or HelpMicroButton or UIParent, "BOTTOMRIGHT", 0, 0)
button:RegisterForClicks("LeftButtonUp")
button:SetNormalTexture(iconTexture)
button:SetPushedTexture(iconTexture)
button:SetDisabledTexture(iconTexture)
button:SetHighlightTexture(iconTexture)
button:SetPushedTextOffset(1, -1)
local highlight = button:GetHighlightTexture()
if highlight then
highlight:SetBlendMode("ADD")
highlight:SetAlpha(0.55)
end
end
button:SetScript("OnClick", MoonWell_ToggleEncounterJournal)
button:SetScript("OnEnter", MoonWell_EncounterJournalMicroButton_OnEnter)
button:SetScript("OnLeave", GameTooltip_Hide)
button:Show()
return button
end
MoonWell_CreateEncounterJournalMicroButton()
@@ -36,3 +36,40 @@ ENCOUNTER_JOURNAL_SEARCH_RESULTS = "Результаты поиска для \"%
ENCOUNTER_JOURNAL_SHOW_MAP = "Показать\nкарту" ENCOUNTER_JOURNAL_SHOW_MAP = "Показать\nкарту"
ENCOUNTER_JOURNAL_SHOW_SEARCH_RESULTS = "Показать все результаты: %d" ENCOUNTER_JOURNAL_SHOW_SEARCH_RESULTS = "Показать все результаты: %d"
LOOT_JOURNAL_ITEM_SETS = "Комплекты" LOOT_JOURNAL_ITEM_SETS = "Комплекты"
ENCOUNTER_JOURNAL_NAVIGATION_HOME = "Главная"
-- Путеводитель — вкладка
MW_PLAYER_GUIDE = "Путеводитель"
MW_PLAYER_GUIDE_REFRESH = "Обновить"
-- Заголовки секций
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
-- Шапка этапа
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
-- Пустое состояние
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. " ..
"Это может занять несколько секунд после входа в игру."
-- Загрузка
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
-- Caps-плашки типов целей
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
MW_PG_TYPE_RAID = "РЕЙД"
MW_PG_TYPE_BOSS = "БОСС"
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
@@ -0,0 +1,419 @@
C_PlayerGuide = C_PlayerGuide or {}
local L = C_PlayerGuide
local PREFIX = "MWPG"
local SEP = "\t"
local currentData = nil
local lastError = nil
local uiState = "loading" -- "data" | "empty" | "loading"
local listeners = {}
local inbox = {} -- [cmd] = { total, got, chunks }
-- ===== minimal JSON decoder =====
local function jsonDecode(s)
if type(s) ~= "string" then return nil end
local pos = 1
local parseValue, parseString, parseNumber, parseObject, parseArray
local function skipWS()
local _, e = s:find("^%s*", pos)
if e then pos = e + 1 end
end
parseString = function()
pos = pos + 1
local result = {}
while pos <= #s do
local c = s:sub(pos, pos)
if c == '"' then
pos = pos + 1
return table.concat(result)
elseif c == '\\' then
pos = pos + 1
local esc = s:sub(pos, pos)
pos = pos + 1
if esc == '"' then result[#result+1] = '"'
elseif esc == '\\' then result[#result+1] = '\\'
elseif esc == '/' then result[#result+1] = '/'
elseif esc == 'n' then result[#result+1] = '\n'
elseif esc == 'r' then result[#result+1] = '\r'
elseif esc == 't' then result[#result+1] = '\t'
elseif esc == 'u' then
local hex = s:sub(pos, pos + 3)
pos = pos + 4
local code = tonumber(hex, 16) or 0
if code < 0x80 then
result[#result+1] = string.char(code)
elseif code < 0x800 then
result[#result+1] = string.char(
0xC0 + math.floor(code / 64),
0x80 + (code % 64))
else
result[#result+1] = string.char(
0xE0 + math.floor(code / 4096),
0x80 + math.floor((code % 4096) / 64),
0x80 + (code % 64))
end
else
result[#result+1] = esc
end
else
result[#result+1] = c
pos = pos + 1
end
end
return table.concat(result)
end
parseNumber = function()
local numStr = s:match("^-?%d+%.?%d*[eE]?[+-]?%d*", pos)
if not numStr then return 0 end
pos = pos + #numStr
return tonumber(numStr) or 0
end
parseArray = function()
pos = pos + 1
local arr = {}
skipWS()
if s:sub(pos, pos) == ']' then pos = pos + 1; return arr end
while pos <= #s do
arr[#arr+1] = parseValue()
skipWS()
local c = s:sub(pos, pos)
if c == ']' then pos = pos + 1; break
elseif c == ',' then pos = pos + 1 end
end
return arr
end
parseObject = function()
pos = pos + 1
local obj = {}
skipWS()
if s:sub(pos, pos) == '}' then pos = pos + 1; return obj end
while pos <= #s do
skipWS()
local key = parseString()
skipWS()
pos = pos + 1
local val = parseValue()
if key ~= nil then obj[key] = val end
skipWS()
local c = s:sub(pos, pos)
if c == '}' then pos = pos + 1; break
elseif c == ',' then pos = pos + 1 end
end
return obj
end
parseValue = function()
skipWS()
local c = s:sub(pos, pos)
if c == '"' then return parseString()
elseif c == '{' then return parseObject()
elseif c == '[' then return parseArray()
elseif c == 't' then pos = pos + 4; return true
elseif c == 'f' then pos = pos + 5; return false
elseif c == 'n' then pos = pos + 4; return nil
else return parseNumber()
end
end
local ok, result = pcall(parseValue)
if ok then return result end
geterrorhandler()("[PlayerGuide] JSON parse error: " .. tostring(result))
return nil
end
-- ===== internal =====
local function notify()
for _, fn in ipairs(listeners) do
pcall(fn, uiState, currentData)
end
end
local function isTable(v) return type(v) == "table" end
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
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
-- ===== send =====
local function sendCmd(cmd, arg)
local body = cmd
if arg ~= nil then body = body .. SEP .. tostring(arg) end
local player = UnitName("player")
if player then SendAddonMessage(PREFIX, body, "WHISPER", player) end
end
-- ===== receive dispatch =====
local function onAddonMessage(prefix, message, channel)
if prefix ~= PREFIX or channel ~= "WHISPER" then return end
local cmd, seq, total, body =
message:match("^([^\t]+)\t(%d+)\t(%d+)\t(.*)$")
if not cmd then return end
seq = tonumber(seq) or 0
total = tonumber(total) or 0
if seq == 0 or total == 0 then return end
local box = inbox[cmd]
if not box or box.total ~= total then
box = { total = total, got = 0, chunks = {} }
inbox[cmd] = box
end
if box.chunks[seq] == nil then
box.chunks[seq] = body
box.got = box.got + 1
end
if box.got < total then return end
local full = table.concat(box.chunks)
inbox[cmd] = nil
if cmd == "SetData" then
local data = jsonDecode(full)
L.SetData(data)
elseif cmd == "SetStage" then
local stage = jsonDecode(full)
if isTable(stage) and L.HasData() then
currentData.stageID = stage.stageID or currentData.stageID
currentData.stageName = stage.stageName or currentData.stageName
currentData.stageDescription = stage.stageDescription or currentData.stageDescription
currentData.progress = stage.progress or currentData.progress
if stage.stages then currentData.stages = stage.stages end
if stage.objectives then currentData.objectives = stage.objectives end
if stage.recommendations then currentData.recommendations = stage.recommendations end
notify()
end
elseif cmd == "UpdateObjective" then
local obj = jsonDecode(full)
if isTable(obj) and obj.id and L.HasData() then
local objectives = currentData.objectives or {}
for i, existing in ipairs(objectives) do
if existing.id == obj.id then
objectives[i] = obj
break
end
end
notify()
end
elseif cmd == "Notify" then
if DEFAULT_CHAT_FRAME and full ~= "" then
DEFAULT_CHAT_FRAME:AddMessage("|cffffd76a[Путеводитель]|r " .. full)
end
end
end
local _frame = CreateFrame("Frame")
_frame:RegisterEvent("CHAT_MSG_ADDON")
_frame:RegisterEvent("PLAYER_LOGIN")
_frame:SetScript("OnEvent", function(_, event, ...)
if event == "CHAT_MSG_ADDON" then
onAddonMessage(...)
elseif event == "PLAYER_LOGIN" then
L.RequestRefresh()
end
end)
-- ===== public API =====
function L.RegisterListener(fn)
if type(fn) == "function" then table.insert(listeners, fn) 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.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.GetStageInfo()
local d = currentData
if not d then return nil end
return d.stageID, d.stageName, d.stageDescription, d.progress or 0
end
function L.GetStages()
local d = currentData
if not d then return nil end
return d.stages or {}
end
function L.GetNumObjectives()
local d = currentData
if not d or not d.objectives then return 0 end
return #d.objectives
end
function L.GetObjectiveInfo(index)
local d = currentData
if not d or not d.objectives then return nil end
local o = d.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,
completed = o.completed and true or false,
progress = o.progress or 0,
required = o.required or 1,
targetID = o.targetID,
questID = o.questID,
questChainID = o.questChainID,
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()
local d = currentData
if not d or not d.recommendations then return 0 end
return #d.recommendations
end
function L.GetRecommendationInfo(index)
local d = currentData
if not d or not d.recommendations then return nil end
local r = d.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()
local d = currentData
if not d or not d.rewards then return 0 end
return #d.rewards
end
function L.GetRewardInfo(index)
local d = currentData
if not d or not d.rewards then return nil end
return d.rewards[index]
end
function L.RequestRefresh()
L.SetLoading()
sendCmd("RequestRefresh")
end
function L.RequestTrackObjective(objectiveID)
sendCmd("RequestTrackObjective", objectiveID)
end
function L.RequestOpenTarget(objectiveID)
sendCmd("RequestOpenTarget", objectiveID)
end
function L._InjectDevPayload()
L.SetData({
version = 1,
stageID = 5,
stageName = "Подготовка к Нордсколу",
stageDescription =
"Завершите ключевые цепочки заданий в Запределье и докажите готовность к походу на север.",
progress = 62,
stages = {
{ id = 1, name = "Начало пути", short = "Начало пути", status = "done" },
{ id = 2, name = "Огненные Недра", short = "Огн. Недра", status = "done" },
{ id = 3, name = "Тёмный портал", short = "Тёмный портал", status = "done" },
{ id = 4, name = "Покорение Запределья", short = "Запределье", status = "done" },
{ id = 5, name = "Подготовка к Нордсколу",short = "К Нордсколу", status = "active" },
{ id = 6, name = "Заря Нордскола", short = "Нордскол", status = "locked" },
{ id = 7, name = "Цитадель Ледяной Короны",short = "Ледяная Корона",status = "locked" },
},
objectives = {
{ id = 1, type = "level", title = "Достигнуть 68 уровня",
subtitle = "Можно отправляться в Нордскол.",
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 = "Аукиндон · 6466",
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 = "6164", biome = "fire" },
{ id = 2, type = "dungeon", name = "Подземелья Аукенай", loc = "Аукиндон", level = "6466", biome = "arcane" },
{ id = 3, type = "dungeon", name = "Сетеккские залы", loc = "Награнд", level = "6769", biome = "storm" },
{ id = 4, type = "dungeon", name = "Тёмный Лабиринт", loc = "Аукиндон", level = "6870", biome = "shadow" },
{ id = 5, type = "dungeon", name = "Ботаника", loc = "Долина Призрачной Луны", level = "6970", biome = "nature" },
},
rewards = {},
})
end
@@ -23,6 +23,7 @@ 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_AdventureJournal.lua
EncounterJournal\Utils\C_PlayerGuide.lua
EncounterJournal\EncounterJournalInit.lua EncounterJournal\EncounterJournalInit.lua
EncounterJournal\EncounterJournalFrameMethods.lua EncounterJournal\EncounterJournalFrameMethods.lua
EncounterJournal\EncounterJournalFonts.xml EncounterJournal\EncounterJournalFonts.xml
@@ -35,6 +36,7 @@ EncounterJournal\SharedXML\HybridScrollFrame.xml
EncounterJournal\SharedXML\NavigationBar.xml EncounterJournal\SharedXML\NavigationBar.xml
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua 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\Custom_LootJournalItems.xml EncounterJournal\Custom_EncounterJournal\Custom_LootJournalItems.xml
EncounterJournal\Custom_EncounterJournal\Custom_ItemBrowser.xml EncounterJournal\Custom_EncounterJournal\Custom_ItemBrowser.xml
EncounterJournal\EncounterJournalScrollFix.lua EncounterJournal\EncounterJournalScrollFix.lua