базовый encounter journal

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

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

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

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+263
View File
@@ -0,0 +1,263 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>PlayerGuide — Texture Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet" />
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
font-family: 'EB Garamond', Georgia, serif;
background:
radial-gradient(ellipse at 50% 0%, #1c2c48 0%, transparent 60%),
radial-gradient(ellipse at 100% 100%, #0d1422 0%, transparent 70%),
linear-gradient(180deg, #0a1018 0%, #050810 100%);
color: #d8c8a0;
padding: 60px 50px 80px;
}
h1 {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 36px; margin: 0 0 6px;
color: #ffd76a;
text-shadow: 0 0 14px rgba(197,151,46,0.5), 0 2px 0 #000;
letter-spacing: 0.02em;
}
.sub {
font-style: italic; color: #a89878;
margin: 0 0 36px; font-size: 16px;
}
.group {
margin: 0 0 44px;
}
.group-title {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
color: #ffd76a; text-shadow: 0 1px 0 #000;
display: flex; align-items: center; gap: 12px;
margin: 0 0 18px;
}
.group-title::before {
content: ''; width: 3px; height: 14px;
background: #ffd76a; box-shadow: 0 0 6px #ffd76a88;
}
.group-title::after {
content: ''; flex: 1; height: 1px;
background: linear-gradient(90deg, rgba(138,106,38,0.5), transparent);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
}
.item {
background: linear-gradient(180deg, rgba(28,44,72,0.4), rgba(10,16,24,0.4));
border: 1px solid rgba(255,215,106,0.18);
border-radius: 3px;
padding: 12px 12px 10px;
display: flex; flex-direction: column; align-items: center;
}
.preview {
width: 100%; aspect-ratio: 1 / 1;
display: flex; align-items: center; justify-content: center;
background:
linear-gradient(45deg, #0d0905 25%, transparent 25%),
linear-gradient(-45deg, #0d0905 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #0d0905 75%),
linear-gradient(-45deg, transparent 75%, #0d0905 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
background-color: #18120a;
border: 1px solid rgba(125,90,42,0.4);
margin-bottom: 8px;
overflow: hidden;
}
.preview.wide { aspect-ratio: 2 / 1; }
.preview.bar { aspect-ratio: 4 / 1; }
.preview img {
max-width: 100%; max-height: 100%;
image-rendering: pixelated;
}
.name {
font-family: 'Cinzel', serif; font-weight: 600;
font-size: 11px; color: #ffe9a0;
text-shadow: 0 1px 0 #000;
letter-spacing: 0.04em; text-align: center;
line-height: 1.2;
}
.size {
font-size: 10px; color: #a89878;
font-style: italic; margin-top: 3px;
font-variant-numeric: tabular-nums;
}
/* Showcase row */
.showcase {
margin: 24px 0 40px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.showcase-card {
background: linear-gradient(180deg, rgba(60,40,18,0.35), rgba(25,15,8,0.55));
border: 1px solid #5a4220;
box-shadow: inset 0 1px 0 rgba(255,200,100,0.18), 0 4px 12px rgba(0,0,0,0.5);
border-radius: 2px;
padding: 18px;
display: flex; gap: 14px; align-items: stretch;
}
.showcase-card h3 {
margin: 0 0 4px;
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 14px; color: #ffe9a0; text-shadow: 0 1px 0 #000;
}
.showcase-card p {
margin: 0; font-style: italic; font-size: 12px; color: #a89878;
}
.progress-demo {
margin-top: 10px;
height: 14px; width: 100%; position: relative;
background: url('Card-Background.png');
}
</style>
</head>
<body>
<h1>PlayerGuide — лист текстур</h1>
<p class="sub">33 PNG-файла, готовы к конвертации в BLP. Шахматный фон под превью = прозрачность.</p>
<!-- Demo: показываем как они стыкуются вместе -->
<div class="group">
<div class="group-title">Демонстрация (как это смотрится в карточке)</div>
<div class="showcase">
<div class="showcase-card">
<img src="TypeIcon-Dungeon.png" width="44" height="44" alt="" />
<div style="flex:1;">
<h3>Пройти Кузню Крови</h3>
<p>Цитадель Адского Пламени • рек. уровень 61—64</p>
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<div style="flex:1; height:8px; background: url('ProgressBar-BG.png'); position:relative;">
<div style="width:35%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
<span style="font-family:Cinzel,serif; font-size:11px; color:#caa760;">0 / 1</span>
</div>
</div>
</div>
<div class="showcase-card" style="background: url('Card-Background.png'); border-color: #5a4220;">
<img src="StageBadge-Active.png" width="48" height="48" alt="" />
<div style="flex:1;">
<h3 style="color:#ffd76a;">Подготовка к Нордсколу</h3>
<p>Этап V из VII · 62% завершено</p>
<div style="margin-top:10px; height:14px; position:relative; background: url('ProgressBar-BG.png');">
<div style="width:62%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Группы текстур -->
<div class="group">
<div class="group-title">Бейджи этапов</div>
<div class="grid" id="stage"></div>
</div>
<div class="group">
<div class="group-title">Прогресс-бар</div>
<div class="grid" id="progress"></div>
</div>
<div class="group">
<div class="group-title">Карточка</div>
<div class="grid" id="card"></div>
</div>
<div class="group">
<div class="group-title">Декор</div>
<div class="grid" id="decor"></div>
</div>
<div class="group">
<div class="group-title">Обложки рекомендаций (биомы)</div>
<div class="grid" id="biome"></div>
</div>
<div class="group">
<div class="group-title">Иконки типов целей</div>
<div class="grid" id="types"></div>
</div>
<div class="group">
<div class="group-title">Иконки статусов</div>
<div class="grid" id="status"></div>
</div>
<div class="group">
<div class="group-title">Кнопки</div>
<div class="grid" id="buttons"></div>
</div>
<script>
const groups = {
stage: [
['StageBadge-Active', '64×64'],
['StageBadge-Done', '64×64'],
['StageBadge-Locked', '64×64'],
],
progress: [
['ProgressBar-Fill', '128×16', 'bar'],
['ProgressBar-BG', '128×16', 'bar'],
],
card: [
['Card-Background', '256×256'],
['Card-Border', '256×32', 'bar'],
],
decor: [
['Section-Divider', '256×16', 'bar'],
['Corner-Ornament', '64×64'],
['Top-Wave-Glow', '512×64', 'bar'],
],
biome: [
['Biome-Fire', '256×128', 'wide'],
['Biome-Arcane', '256×128', 'wide'],
['Biome-Storm', '256×128', 'wide'],
['Biome-Shadow', '256×128', 'wide'],
['Biome-Nature', '256×128', 'wide'],
['Biome-Holy', '256×128', 'wide'],
],
types: [
['TypeIcon-Level', '64×64'],
['TypeIcon-Quest', '64×64'],
['TypeIcon-QuestChain', '64×64'],
['TypeIcon-Dungeon', '64×64'],
['TypeIcon-Raid', '64×64'],
['TypeIcon-Boss', '64×64'],
['TypeIcon-Achievement', '64×64'],
['TypeIcon-Reputation', '64×64'],
['TypeIcon-Profession', '64×64'],
['TypeIcon-Item', '64×64'],
['TypeIcon-Currency', '64×64'],
['TypeIcon-Custom', '64×64'],
],
status: [
['TypeIcon-Done', '64×64'],
['TypeIcon-Locked', '64×64'],
],
buttons: [
['Button-Up', '128×32', 'bar'],
['Button-Down', '128×32', 'bar'],
['Button-Highlight', '128×32', 'bar'],
],
};
for (const [groupId, items] of Object.entries(groups)) {
const el = document.getElementById(groupId);
for (const [name, size, shape] of items) {
const item = document.createElement('div');
item.className = 'item';
item.innerHTML = `
<div class="preview ${shape || ''}">
<img src="${name}.png" alt="${name}" />
</div>
<div class="name">${name}</div>
<div class="size">${size}</div>
`;
el.appendChild(item);
}
}
</script>
</body>
</html>