базовый encounter journal
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user