277 lines
12 KiB
Lua
277 lines
12 KiB
Lua
--[[----------------------------------------------------------------------------
|
||
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
|