Files
moonwell-client/client-export/C_PlayerGuide.lua

277 lines
12 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[----------------------------------------------------------------------------
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