Fix post-hearthstone asset gaps and add quest tracker interactivity

Hearthstone post-teleport fix:
- Expand same-map hearthstone precache from 5x5 to 9x9 tiles so workers
  have more tiles parsed before the player arrives at the bind point
- After same-map teleport arrival, enqueue the full load-radius tile grid
  (17x17 = 289 tiles) at the new position so background workers immediately
  start loading all WMOs/M2s visible from the new location

Quest tracker improvements:
- Clicking a quest in the tracker now opens the Quest Log (L)
- Remove NoInputs flag so the tracker window receives mouse events
- Show only tracked quests in tracker; fall back to all quests if none tracked
- Add Track/Untrack button in Quest Log details panel
- Abandoning a quest automatically untracks it
- Track state stored in GameHandler::trackedQuestIds_ (per-session)
This commit is contained in:
Kelsi 2026-03-10 05:18:45 -07:00
parent 682f47f66b
commit fb80b125bd
4 changed files with 72 additions and 15 deletions

View file

@ -966,6 +966,12 @@ public:
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
void abandonQuest(uint32_t questId);
bool requestQuestQuery(uint32_t questId, bool force = false);
bool isQuestTracked(uint32_t questId) const { return trackedQuestIds_.count(questId) > 0; }
void setQuestTracked(uint32_t questId, bool tracked) {
if (tracked) trackedQuestIds_.insert(questId);
else trackedQuestIds_.erase(questId);
}
const std::unordered_set<uint32_t>& getTrackedQuestIds() const { return trackedQuestIds_; }
bool isQuestQueryPending(uint32_t questId) const {
return pendingQuestQueryIds_.count(questId) > 0;
}
@ -1981,6 +1987,7 @@ private:
// Quest log
std::vector<QuestLogEntry> questLog_;
std::unordered_set<uint32_t> pendingQuestQueryIds_;
std::unordered_set<uint32_t> trackedQuestIds_;
bool pendingLoginQuestResync_ = false;
float pendingLoginQuestResyncTimeout_ = 0.0f;

View file

@ -1714,6 +1714,21 @@ void Application::setupUICallbacks() {
// (e.g. Hearthstone pre-loaded them) so they're GPU-uploaded before
// the first frame at the new position.
renderer->getTerrainManager()->processAllReadyTiles();
// Queue all remaining tiles within the load radius (8 tiles = 17x17)
// at the new position. precacheTiles skips already-loaded/pending tiles,
// so this only enqueues tiles that aren't yet in the pipeline.
// This ensures background workers immediately start loading everything
// visible from the new position (hearthstone may land far from old location).
{
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
std::vector<std::pair<int,int>> nearbyTiles;
nearbyTiles.reserve(289);
for (int dy = -8; dy <= 8; dy++)
for (int dx = -8; dx <= 8; dx++)
nearbyTiles.push_back({tileX + dx, tileY + dy});
renderer->getTerrainManager()->precacheTiles(nearbyTiles);
}
return;
}
@ -1976,13 +1991,15 @@ void Application::setupUICallbacks() {
if (mapId == loadedMapId_) {
// Same map: pre-enqueue tiles around the bind point so workers start
// loading them now. Uses render-space coords (canonicalToRender).
// Use radius 4 (9x9=81 tiles) — hearthstone cast is ~10s, enough time
// for workers to parse most of these before the player arrives.
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
std::vector<std::pair<int,int>> tiles;
tiles.reserve(25);
for (int dy = -2; dy <= 2; dy++)
for (int dx = -2; dx <= 2; dx++)
tiles.reserve(81);
for (int dy = -4; dy <= 4; dy++)
for (int dx = -4; dx <= 4; dx++)
tiles.push_back({tileX + dx, tileY + dy});
terrainMgr->precacheTiles(tiles);

View file

@ -4549,13 +4549,34 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
constexpr float RIGHT_MARGIN = 10.0f;
constexpr int MAX_QUESTS = 5;
// Build display list: tracked quests only, or all quests if none tracked
const auto& trackedIds = gameHandler.getTrackedQuestIds();
std::vector<const game::GameHandler::QuestLogEntry*> toShow;
toShow.reserve(MAX_QUESTS);
if (!trackedIds.empty()) {
for (const auto& q : questLog) {
if (q.questId == 0) continue;
if (trackedIds.count(q.questId)) toShow.push_back(&q);
if (static_cast<int>(toShow.size()) >= MAX_QUESTS) break;
}
}
// Fallback: show all quests if nothing is tracked
if (toShow.empty()) {
for (const auto& q : questLog) {
if (q.questId == 0) continue;
toShow.push_back(&q);
if (static_cast<int>(toShow.size()) >= MAX_QUESTS) break;
}
}
if (toShow.empty()) return;
float x = screenW - TRACKER_W - RIGHT_MARGIN;
float y = 200.0f; // below minimap area
ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBringToFrontOnFocus;
@ -4564,15 +4585,23 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f));
if (ImGui::Begin("##QuestTracker", nullptr, flags)) {
int shown = 0;
for (const auto& q : questLog) {
if (q.questId == 0) continue;
if (shown >= MAX_QUESTS) break;
for (int i = 0; i < static_cast<int>(toShow.size()); ++i) {
const auto& q = *toShow[i];
// Quest title in yellow (gold) if complete, white if in progress
// Clickable quest title — opens quest log
ImGui::PushID(q.questId);
ImVec4 titleCol = q.complete ? ImVec4(1.0f, 0.84f, 0.0f, 1.0f)
: ImVec4(1.0f, 1.0f, 0.85f, 1.0f);
ImGui::TextColored(titleCol, "%s", q.title.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, titleCol);
if (ImGui::Selectable(q.title.c_str(), false,
ImGuiSelectableFlags_None, ImVec2(TRACKER_W - 12.0f, 0))) {
questLogScreen.setOpen(true);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to open Quest Log");
}
ImGui::PopStyleColor();
ImGui::PopID();
// Objectives line (condensed)
if (q.complete) {
@ -4599,7 +4628,6 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
}
}
if (q.killCounts.empty() && q.itemCounts.empty() && !q.objectives.empty()) {
// Show the raw objectives text, truncated if needed
const std::string& obj = q.objectives;
if (obj.size() > 40) {
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
@ -4611,10 +4639,9 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
}
}
if (shown < MAX_QUESTS - 1 && shown < static_cast<int>(questLog.size()) - 1) {
if (i < static_cast<int>(toShow.size()) - 1) {
ImGui::Spacing();
}
++shown;
}
}
ImGui::End();

View file

@ -373,11 +373,17 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
}
}
// Abandon button
// Track / Abandon buttons
ImGui::Separator();
bool isTracked = gameHandler.isQuestTracked(sel.questId);
if (ImGui::Button(isTracked ? "Untrack" : "Track", ImVec2(100.0f, 0.0f))) {
gameHandler.setQuestTracked(sel.questId, !isTracked);
}
if (!sel.complete) {
ImGui::Separator();
ImGui::SameLine();
if (ImGui::Button("Abandon Quest", ImVec2(150.0f, 0.0f))) {
gameHandler.abandonQuest(sel.questId);
gameHandler.setQuestTracked(sel.questId, false);
selectedIndex = -1;
}
}