базовый encounter journal
@@ -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
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1015 B |
|
After Width: | Height: | Size: 1015 B |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 307 B |
@@ -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 того же размера и сконвертировать).
|
||||
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -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>
|
||||
@@ -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 KeyRingButton = _G.KeyRingButton;
|
||||
|
||||
-- Button collections (dynamically set based on server)
|
||||
local MICRO_BUTTONS
|
||||
-- Button collections (rebuilt after addons create optional custom buttons).
|
||||
local function BuildMicroButtonList()
|
||||
if isAscensionServer then
|
||||
return {
|
||||
_G.CharacterMicroButton,
|
||||
_G.SpellbookMicroButton,
|
||||
_G.TalentMicroButton,
|
||||
_G.AchievementMicroButton,
|
||||
_G.QuestLogMicroButton,
|
||||
_G.SocialsMicroButton,
|
||||
_G.LFDMicroButton,
|
||||
_G.EncounterJournalMicroButton,
|
||||
_G.PathToAscensionMicroButton,
|
||||
_G.ChallengesMicroButton,
|
||||
_G.MainMenuMicroButton,
|
||||
_G.HelpMicroButton
|
||||
}
|
||||
end
|
||||
|
||||
if isAscensionServer then
|
||||
MICRO_BUTTONS = {
|
||||
_G.CharacterMicroButton,
|
||||
_G.SpellbookMicroButton,
|
||||
_G.TalentMicroButton,
|
||||
_G.AchievementMicroButton,
|
||||
_G.QuestLogMicroButton,
|
||||
_G.SocialsMicroButton,
|
||||
_G.LFDMicroButton,
|
||||
_G.PathToAscensionMicroButton,
|
||||
_G.ChallengesMicroButton,
|
||||
_G.MainMenuMicroButton,
|
||||
_G.HelpMicroButton
|
||||
}
|
||||
else
|
||||
MICRO_BUTTONS = {
|
||||
return {
|
||||
_G.CharacterMicroButton,
|
||||
_G.SpellbookMicroButton,
|
||||
_G.TalentMicroButton,
|
||||
@@ -95,6 +96,7 @@ else
|
||||
_G.QuestLogMicroButton,
|
||||
_G.SocialsMicroButton,
|
||||
_G.LFDMicroButton,
|
||||
_G.EncounterJournalMicroButton,
|
||||
_G.CollectionsMicroButton,
|
||||
_G.PVPMicroButton,
|
||||
_G.MainMenuMicroButton,
|
||||
@@ -102,6 +104,7 @@ else
|
||||
}
|
||||
end
|
||||
|
||||
local MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
local bagslots = {_G.CharacterBag0Slot, _G.CharacterBag1Slot, _G.CharacterBag2Slot, _G.CharacterBag3Slot};
|
||||
|
||||
@@ -783,6 +786,8 @@ local function ApplyMicromenuSystem()
|
||||
return
|
||||
end
|
||||
|
||||
MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
-- Store original states first
|
||||
StoreOriginalMicroButtonStates()
|
||||
|
||||
@@ -911,6 +916,8 @@ local function ApplyMicromenuSystem()
|
||||
or (_G.PVPParentFrame and _G.PVPParentFrame:IsVisible() and true or false)
|
||||
or (_G.BattlefieldFrame and _G.BattlefieldFrame:IsVisible() and true or false)
|
||||
or (_G.HonorFrame and _G.HonorFrame:IsVisible() and true or false)
|
||||
elseif buttonName == "EncounterJournal" then
|
||||
return (_G.EncounterJournal and _G.EncounterJournal:IsVisible() and true or false)
|
||||
end
|
||||
|
||||
return pressed
|
||||
@@ -919,6 +926,152 @@ local function ApplyMicromenuSystem()
|
||||
-- ============================================================================
|
||||
-- SECTION 5: SPECIALIZED BUTTON SETUP
|
||||
-- ============================================================================
|
||||
local function SetupEncounterJournalButton(button)
|
||||
local iconTexture = 'Interface\\EncounterJournal\\UI-EJ-PortraitIcon'
|
||||
local backgroundTexture = 'Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\uimicromenu2x'
|
||||
local buttonWidth, buttonHeight = button:GetSize()
|
||||
local dx, dy = -1, 1
|
||||
local offX, offY = button:GetPushedTextOffset()
|
||||
|
||||
if not button.DragonUIEncounterJournalIcon then
|
||||
button.DragonUIEncounterJournalIcon = button:CreateTexture(nil, 'ARTWORK')
|
||||
end
|
||||
|
||||
local icon = button.DragonUIEncounterJournalIcon
|
||||
icon:SetTexture(iconTexture)
|
||||
icon:SetTexCoord(0, 1, 0, 1)
|
||||
icon:ClearAllPoints()
|
||||
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
|
||||
icon:SetSize(buttonWidth - 8, buttonWidth - 8)
|
||||
icon:SetAlpha(1)
|
||||
icon:Show()
|
||||
|
||||
local highlightTexture = button:GetHighlightTexture()
|
||||
if highlightTexture then
|
||||
highlightTexture:SetTexture(iconTexture)
|
||||
highlightTexture:SetTexCoord(0, 1, 0, 1)
|
||||
highlightTexture:ClearAllPoints()
|
||||
highlightTexture:SetAllPoints(icon)
|
||||
highlightTexture:SetBlendMode('ADD')
|
||||
highlightTexture:SetAlpha(0.55)
|
||||
end
|
||||
|
||||
if not button.DragonUIBackground then
|
||||
local bg = button:CreateTexture(nil, 'BACKGROUND')
|
||||
bg:SetTexture(backgroundTexture)
|
||||
bg:SetSize(buttonWidth, buttonHeight + 1)
|
||||
bg:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
|
||||
bg:SetPoint('CENTER', dx, dy)
|
||||
button.DragonUIBackground = bg
|
||||
|
||||
local bgPushed = button:CreateTexture(nil, 'BACKGROUND')
|
||||
bgPushed:SetTexture(backgroundTexture)
|
||||
bgPushed:SetSize(buttonWidth, buttonHeight + 1)
|
||||
bgPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
|
||||
bgPushed:SetPoint('CENTER', dx + offX, dy + offY)
|
||||
bgPushed:Hide()
|
||||
button.DragonUIBackgroundPushed = bgPushed
|
||||
else
|
||||
button.DragonUIBackground:SetTexture(backgroundTexture)
|
||||
button.DragonUIBackground:SetTexCoord(0.0654297, 0.12793, 0.330078, 0.490234)
|
||||
button.DragonUIBackground:ClearAllPoints()
|
||||
button.DragonUIBackground:SetPoint('CENTER', dx, dy)
|
||||
button.DragonUIBackground:SetSize(buttonWidth, buttonHeight + 1)
|
||||
|
||||
if button.DragonUIBackgroundPushed then
|
||||
button.DragonUIBackgroundPushed:SetTexture(backgroundTexture)
|
||||
button.DragonUIBackgroundPushed:SetTexCoord(0.0654297, 0.12793, 0.494141, 0.654297)
|
||||
button.DragonUIBackgroundPushed:ClearAllPoints()
|
||||
button.DragonUIBackgroundPushed:SetPoint('CENTER', dx + offX, dy + offY)
|
||||
button.DragonUIBackgroundPushed:SetSize(buttonWidth, buttonHeight + 1)
|
||||
end
|
||||
end
|
||||
|
||||
button.dragonUIState = button.dragonUIState or {}
|
||||
button.dragonUIState.pushed = IsSpecialMicroButtonActive(button, "EncounterJournal")
|
||||
button.dragonUILastState = button.dragonUIState.pushed
|
||||
button.dragonUITimer = button.dragonUITimer or 0
|
||||
|
||||
button.HandleDragonUIState = function()
|
||||
local state = button.dragonUIState
|
||||
local hlTex = button:GetHighlightTexture()
|
||||
if state and state.pushed then
|
||||
if icon then
|
||||
icon:ClearAllPoints()
|
||||
icon:SetPoint('CENTER', button, 'CENTER', offX, offY)
|
||||
icon:SetAlpha(0.7)
|
||||
end
|
||||
if button.DragonUIBackground then
|
||||
button.DragonUIBackground:Hide()
|
||||
end
|
||||
if button.DragonUIBackgroundPushed then
|
||||
button.DragonUIBackgroundPushed:Show()
|
||||
end
|
||||
if hlTex then
|
||||
hlTex:ClearAllPoints()
|
||||
hlTex:SetPoint('TOPLEFT', icon, 'TOPLEFT', 0, 0)
|
||||
hlTex:SetPoint('BOTTOMRIGHT', icon, 'BOTTOMRIGHT', 0, 0)
|
||||
end
|
||||
else
|
||||
if icon then
|
||||
icon:ClearAllPoints()
|
||||
icon:SetPoint('CENTER', button, 'CENTER', 0, 0)
|
||||
icon:SetAlpha(1)
|
||||
end
|
||||
if button.DragonUIBackground then
|
||||
button.DragonUIBackground:Show()
|
||||
end
|
||||
if button.DragonUIBackgroundPushed then
|
||||
button.DragonUIBackgroundPushed:Hide()
|
||||
end
|
||||
if hlTex then
|
||||
hlTex:ClearAllPoints()
|
||||
hlTex:SetAllPoints(icon)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
button:SetScript('OnUpdate', function(self, elapsed)
|
||||
self.dragonUITimer = (self.dragonUITimer or 0) + elapsed
|
||||
if self.dragonUITimer >= 0.1 then
|
||||
self.dragonUITimer = 0
|
||||
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
|
||||
if currentState ~= self.dragonUILastState then
|
||||
self.dragonUILastState = currentState
|
||||
if self.dragonUIState then
|
||||
self.dragonUIState.pushed = currentState
|
||||
end
|
||||
if self.HandleDragonUIState then
|
||||
self.HandleDragonUIState()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if not button.DragonUIStateHooks then
|
||||
button:HookScript('OnMouseDown', function(self)
|
||||
if self.dragonUIState then
|
||||
self.dragonUIState.pushed = true
|
||||
end
|
||||
if self.HandleDragonUIState then
|
||||
self.HandleDragonUIState()
|
||||
end
|
||||
end)
|
||||
button:HookScript('OnMouseUp', function(self)
|
||||
local currentState = IsSpecialMicroButtonActive(self, "EncounterJournal")
|
||||
if self.dragonUIState then
|
||||
self.dragonUIState.pushed = currentState
|
||||
end
|
||||
if self.HandleDragonUIState then
|
||||
self.HandleDragonUIState()
|
||||
end
|
||||
end)
|
||||
button.DragonUIStateHooks = true
|
||||
end
|
||||
|
||||
button.HandleDragonUIState()
|
||||
end
|
||||
|
||||
local function SetupPVPButton(button)
|
||||
-- Mirror the Character button pattern:
|
||||
-- Instead of fighting WoW's internal NormalTexture alpha management,
|
||||
@@ -1582,6 +1735,8 @@ local function ApplyMicromenuSystem()
|
||||
end
|
||||
|
||||
local function setupMicroButtons(xOffset)
|
||||
MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
local buttonxOffset = 0
|
||||
|
||||
local useGrayscale = addon.db.profile.micromenu.grayscale_icons
|
||||
@@ -1705,9 +1860,11 @@ local function ApplyMicromenuSystem()
|
||||
|
||||
local isCharacterButton = (buttonName == "Character")
|
||||
local isPVPButton = (buttonName == "PVP")
|
||||
local isEncounterJournalButton = (buttonName == "EncounterJournal")
|
||||
|
||||
local upCoords = not isCharacterButton and not isPVPButton and GetColoredTextureCoords(name, "Up") or nil
|
||||
local shouldUseGrayscale = useGrayscale or (not isPVPButton and not upCoords and not isCharacterButton)
|
||||
local upCoords = not isCharacterButton and not isPVPButton and not isEncounterJournalButton and GetColoredTextureCoords(name, "Up") or nil
|
||||
local shouldUseGrayscale = (useGrayscale and not isEncounterJournalButton) or
|
||||
(not isPVPButton and not isEncounterJournalButton and not upCoords and not isCharacterButton)
|
||||
|
||||
if shouldUseGrayscale then
|
||||
-- Grayscale icons
|
||||
@@ -1741,6 +1898,8 @@ local function ApplyMicromenuSystem()
|
||||
end
|
||||
elseif isPVPButton then
|
||||
SetupPVPButton(button)
|
||||
elseif isEncounterJournalButton then
|
||||
SetupEncounterJournalButton(button)
|
||||
elseif isCharacterButton then
|
||||
SetupCharacterButton(button)
|
||||
else
|
||||
@@ -2245,6 +2404,8 @@ end
|
||||
return
|
||||
end
|
||||
|
||||
MICRO_BUTTONS = BuildMicroButtonList()
|
||||
|
||||
local useGrayscale = addon.db.profile.micromenu.grayscale_icons
|
||||
local configMode = useGrayscale and "grayscale" or "normal"
|
||||
local config = addon.db.profile.micromenu[configMode]
|
||||
|
||||
@@ -2107,7 +2107,7 @@
|
||||
</Layer>
|
||||
</Layers>
|
||||
<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>
|
||||
<Anchor point="BOTTOMLEFT" relativePoint="TOPLEFT" x="25" y="-45"/>
|
||||
</Anchors>
|
||||
@@ -2115,6 +2115,14 @@
|
||||
<OnClick function="EJ_ContentTab_OnClick"/>
|
||||
</Scripts>
|
||||
</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">
|
||||
<Anchors>
|
||||
<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)
|
||||
LOOTJOURNAL_SOURCE_TOOLTIP_HEAD = LOOTJOURNAL_SOURCE_TOOLTIP_HEAD or "Способ получения:"
|
||||
RETURN_TO_DEFAULT = RETURN_TO_DEFAULT or "Настройки по умолчанию"
|
||||
GUIDEBOOK = GUIDEBOOK or "Путеводитель"
|
||||
INSTANCES = INSTANCES or "Подземелья"
|
||||
RAIDS = RAIDS or "Рейды"
|
||||
LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты"
|
||||
|
||||
@@ -49,12 +49,14 @@ local function ConfigureTierTab(tab, text)
|
||||
end
|
||||
|
||||
local function PositionVisibleTierTabs(instanceSelect)
|
||||
local guidebookTab = instanceSelect and instanceSelect.guidebookTab
|
||||
local dungeonsTab = instanceSelect and instanceSelect.dungeonsTab
|
||||
local raidsTab = instanceSelect and instanceSelect.raidsTab
|
||||
if not dungeonsTab or not raidsTab then
|
||||
return
|
||||
end
|
||||
|
||||
ConfigureTierTab(guidebookTab, GUIDEBOOK or "Guidebook")
|
||||
ConfigureTierTab(dungeonsTab, INSTANCES or "Instances")
|
||||
ConfigureTierTab(raidsTab, RAIDS or "Raids")
|
||||
|
||||
@@ -96,10 +98,10 @@ local function SelectVisibleInstanceTab(journal)
|
||||
end
|
||||
|
||||
if journal:IsShown()
|
||||
and instanceSelect:IsShown()
|
||||
and (not journal.encounter or not journal.encounter:IsShown())
|
||||
and selectedTab
|
||||
and EJ_ContentTab_Select
|
||||
and instanceSelect:IsShown()
|
||||
and (not journal.encounter or not journal.encounter:IsShown())
|
||||
and selectedTab
|
||||
and EJ_ContentTab_Select
|
||||
then
|
||||
EJ_ContentTab_Select(selectedTab)
|
||||
end
|
||||
|
||||
@@ -15,10 +15,12 @@ local function MoonWell_ToggleEncounterJournal()
|
||||
MoonWell_EncounterJournalApplyDungeonMode()
|
||||
end
|
||||
|
||||
if EncounterJournal_OpenJournal then
|
||||
EncounterJournal_OpenJournal()
|
||||
else
|
||||
ShowUIPanel(EncounterJournal)
|
||||
ShowUIPanel(EncounterJournal)
|
||||
|
||||
if EJPlayerGuide_OpenFrame then
|
||||
EJPlayerGuide_OpenFrame()
|
||||
elseif EncounterJournal.instanceSelect and EncounterJournal.instanceSelect.guideTab and EJ_ContentTab_Select then
|
||||
EJ_ContentTab_Select(EncounterJournal.instanceSelect.guideTab:GetID())
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,3 +30,40 @@ SLASH_MOONWELLENCOUNTERJOURNAL3 = "/encounterjournal"
|
||||
SlashCmdList.MOONWELLENCOUNTERJOURNAL = MoonWell_ToggleEncounterJournal
|
||||
|
||||
MoonWell_ToggleEncounterJournal = MoonWell_ToggleEncounterJournal
|
||||
|
||||
local function MoonWell_EncounterJournalMicroButton_OnEnter(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:AddLine("Путеводитель по приключениям", 1, 1, 1)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
local function MoonWell_CreateEncounterJournalMicroButton()
|
||||
local button = EncounterJournalMicroButton
|
||||
if not button then
|
||||
local iconTexture = "Interface\\EncounterJournal\\UI-EJ-PortraitIcon"
|
||||
button = CreateFrame("Button", "EncounterJournalMicroButton", MainMenuBarArtFrame or UIParent)
|
||||
button:SetSize(32, 40)
|
||||
button:SetPoint("BOTTOMLEFT", LFDMicroButton or MainMenuMicroButton or HelpMicroButton or UIParent, "BOTTOMRIGHT", 0, 0)
|
||||
button:RegisterForClicks("LeftButtonUp")
|
||||
button:SetNormalTexture(iconTexture)
|
||||
button:SetPushedTexture(iconTexture)
|
||||
button:SetDisabledTexture(iconTexture)
|
||||
button:SetHighlightTexture(iconTexture)
|
||||
button:SetPushedTextOffset(1, -1)
|
||||
|
||||
local highlight = button:GetHighlightTexture()
|
||||
if highlight then
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetAlpha(0.55)
|
||||
end
|
||||
end
|
||||
|
||||
button:SetScript("OnClick", MoonWell_ToggleEncounterJournal)
|
||||
button:SetScript("OnEnter", MoonWell_EncounterJournalMicroButton_OnEnter)
|
||||
button:SetScript("OnLeave", GameTooltip_Hide)
|
||||
button:Show()
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
MoonWell_CreateEncounterJournalMicroButton()
|
||||
|
||||
@@ -36,3 +36,40 @@ ENCOUNTER_JOURNAL_SEARCH_RESULTS = "Результаты поиска для \"%
|
||||
ENCOUNTER_JOURNAL_SHOW_MAP = "Показать\nкарту"
|
||||
ENCOUNTER_JOURNAL_SHOW_SEARCH_RESULTS = "Показать все результаты: %d"
|
||||
LOOT_JOURNAL_ITEM_SETS = "Комплекты"
|
||||
ENCOUNTER_JOURNAL_NAVIGATION_HOME = "Главная"
|
||||
|
||||
-- Путеводитель — вкладка
|
||||
MW_PLAYER_GUIDE = "Путеводитель"
|
||||
MW_PLAYER_GUIDE_REFRESH = "Обновить"
|
||||
|
||||
-- Заголовки секций
|
||||
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
|
||||
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
|
||||
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
|
||||
|
||||
-- Шапка этапа
|
||||
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
|
||||
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
|
||||
|
||||
-- Пустое состояние
|
||||
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
|
||||
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. " ..
|
||||
"Это может занять несколько секунд после входа в игру."
|
||||
|
||||
-- Загрузка
|
||||
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
|
||||
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
|
||||
|
||||
-- Caps-плашки типов целей
|
||||
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
|
||||
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
|
||||
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
|
||||
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
|
||||
MW_PG_TYPE_RAID = "РЕЙД"
|
||||
MW_PG_TYPE_BOSS = "БОСС"
|
||||
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
|
||||
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
|
||||
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
|
||||
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
|
||||
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
|
||||
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
|
||||
|
||||
@@ -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 = "Аукиндон · 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" },
|
||||
},
|
||||
rewards = {},
|
||||
})
|
||||
end
|
||||
@@ -23,6 +23,7 @@ EncounterJournal\EncounterJournalTierSnapshot.lua
|
||||
EncounterJournal\GlobalStrings.lua
|
||||
EncounterJournal\Utils\C_EncounterJournal.lua
|
||||
EncounterJournal\Utils\C_AdventureJournal.lua
|
||||
EncounterJournal\Utils\C_PlayerGuide.lua
|
||||
EncounterJournal\EncounterJournalInit.lua
|
||||
EncounterJournal\EncounterJournalFrameMethods.lua
|
||||
EncounterJournal\EncounterJournalFonts.xml
|
||||
@@ -35,6 +36,7 @@ EncounterJournal\SharedXML\HybridScrollFrame.xml
|
||||
EncounterJournal\SharedXML\NavigationBar.xml
|
||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
|
||||
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
|
||||
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
|
||||
EncounterJournal\Custom_EncounterJournal\Custom_LootJournalItems.xml
|
||||
EncounterJournal\Custom_EncounterJournal\Custom_ItemBrowser.xml
|
||||
EncounterJournal\EncounterJournalScrollFix.lua
|
||||
|
||||