403 lines
15 KiB
Lua
403 lines
15 KiB
Lua
--[[----------------------------------------------------------------------------
|
||
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,
|
||
}
|