2026-02-06 13:47:03 -08:00
|
|
|
#include "ui/quest_log_screen.hpp"
|
|
|
|
|
#include "core/application.hpp"
|
|
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include <imgui.h>
|
|
|
|
|
|
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
namespace {
|
2026-02-09 17:46:45 -08:00
|
|
|
// Helper function to replace gender placeholders, pronouns, and name
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
|
|
|
|
game::Gender gender = game::Gender::NONBINARY;
|
2026-02-09 17:46:45 -08:00
|
|
|
std::string playerName = "Adventurer";
|
2026-02-09 17:39:21 -08:00
|
|
|
const auto* character = gameHandler.getActiveCharacter();
|
|
|
|
|
if (character) {
|
|
|
|
|
gender = character->gender;
|
2026-02-09 17:46:45 -08:00
|
|
|
if (!character->name.empty()) {
|
|
|
|
|
playerName = character->name;
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
|
|
|
|
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
|
|
|
|
|
|
|
|
|
std::string result = text;
|
|
|
|
|
|
|
|
|
|
auto trim = [](std::string& s) {
|
|
|
|
|
s.erase(0, s.find_first_not_of(" \t\n\r"));
|
|
|
|
|
s.erase(s.find_last_not_of(" \t\n\r") + 1);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-09 17:46:45 -08:00
|
|
|
// Replace simple placeholders
|
2026-02-09 17:39:21 -08:00
|
|
|
size_t pos = 0;
|
|
|
|
|
while ((pos = result.find('$', pos)) != std::string::npos) {
|
|
|
|
|
if (pos + 1 >= result.length()) break;
|
|
|
|
|
|
|
|
|
|
char code = result[pos + 1];
|
|
|
|
|
std::string replacement;
|
|
|
|
|
|
|
|
|
|
switch (code) {
|
2026-02-17 16:16:51 -08:00
|
|
|
case 'n': case 'N': replacement = playerName; break;
|
2026-02-09 17:39:21 -08:00
|
|
|
case 'p': replacement = pronouns.subject; break;
|
|
|
|
|
case 'o': replacement = pronouns.object; break;
|
|
|
|
|
case 's': replacement = pronouns.possessive; break;
|
|
|
|
|
case 'S': replacement = pronouns.possessiveP; break;
|
|
|
|
|
case 'g': pos++; continue;
|
|
|
|
|
default: pos++; continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.replace(pos, 2, replacement);
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace $g placeholders
|
|
|
|
|
pos = 0;
|
|
|
|
|
while ((pos = result.find("$g", pos)) != std::string::npos) {
|
|
|
|
|
size_t endPos = result.find(';', pos);
|
|
|
|
|
if (endPos == std::string::npos) break;
|
|
|
|
|
|
|
|
|
|
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> parts;
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
size_t colonPos;
|
|
|
|
|
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
|
|
|
|
std::string part = placeholder.substr(start, colonPos - start);
|
|
|
|
|
trim(part);
|
|
|
|
|
parts.push_back(part);
|
|
|
|
|
start = colonPos + 1;
|
|
|
|
|
}
|
|
|
|
|
std::string lastPart = placeholder.substr(start);
|
|
|
|
|
trim(lastPart);
|
|
|
|
|
parts.push_back(lastPart);
|
|
|
|
|
|
|
|
|
|
std::string replacement;
|
|
|
|
|
if (parts.size() >= 3) {
|
|
|
|
|
switch (gender) {
|
|
|
|
|
case game::Gender::MALE: replacement = parts[0]; break;
|
|
|
|
|
case game::Gender::FEMALE: replacement = parts[1]; break;
|
|
|
|
|
case game::Gender::NONBINARY: replacement = parts[2]; break;
|
|
|
|
|
}
|
|
|
|
|
} else if (parts.size() >= 2) {
|
|
|
|
|
switch (gender) {
|
|
|
|
|
case game::Gender::MALE: replacement = parts[0]; break;
|
|
|
|
|
case game::Gender::FEMALE: replacement = parts[1]; break;
|
|
|
|
|
case game::Gender::NONBINARY:
|
|
|
|
|
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
pos = endPos + 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.replace(pos, endPos - pos + 1, replacement);
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
|
|
|
|
// L key toggle (edge-triggered)
|
2026-02-14 21:56:38 -08:00
|
|
|
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
|
|
|
|
bool lDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_L);
|
2026-02-06 13:47:03 -08:00
|
|
|
if (lDown && !lKeyWasDown) {
|
|
|
|
|
open = !open;
|
|
|
|
|
}
|
|
|
|
|
lKeyWasDown = lDown;
|
|
|
|
|
|
|
|
|
|
if (!open) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
float logW = 380.0f;
|
|
|
|
|
float logH = std::min(450.0f, screenH - 120.0f);
|
|
|
|
|
float logX = (screenW - logW) * 0.5f;
|
|
|
|
|
float logY = 80.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(logX, logY), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(logW, logH), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
bool stillOpen = true;
|
|
|
|
|
if (ImGui::Begin("Quest Log", &stillOpen)) {
|
|
|
|
|
const auto& quests = gameHandler.getQuestLog();
|
|
|
|
|
|
|
|
|
|
if (quests.empty()) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active quests.");
|
|
|
|
|
} else {
|
|
|
|
|
// Left panel: quest list
|
|
|
|
|
ImGui::BeginChild("QuestList", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4), true);
|
|
|
|
|
for (size_t i = 0; i < quests.size(); i++) {
|
|
|
|
|
const auto& q = quests[i];
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
|
|
|
|
ImVec4 color = q.complete
|
|
|
|
|
? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) // Green for complete
|
|
|
|
|
: ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // Gold for active
|
|
|
|
|
|
|
|
|
|
bool selected = (selectedIndex == static_cast<int>(i));
|
|
|
|
|
if (ImGui::Selectable("##quest", selected, 0, ImVec2(0, 20))) {
|
|
|
|
|
selectedIndex = static_cast<int>(i);
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextColored(color, "%s%s",
|
|
|
|
|
q.title.c_str(),
|
|
|
|
|
q.complete ? " (Complete)" : "");
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
// Details panel for selected quest
|
|
|
|
|
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(quests.size())) {
|
|
|
|
|
const auto& sel = quests[static_cast<size_t>(selectedIndex)];
|
|
|
|
|
|
|
|
|
|
if (!sel.objectives.empty()) {
|
|
|
|
|
ImGui::Separator();
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
|
|
|
|
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 04:06:14 -08:00
|
|
|
if (!sel.killCounts.empty() || !sel.itemCounts.empty()) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tracked Progress");
|
|
|
|
|
for (const auto& [entry, progress] : sel.killCounts) {
|
|
|
|
|
ImGui::BulletText("Kill %u: %u/%u", entry, progress.first, progress.second);
|
|
|
|
|
}
|
|
|
|
|
for (const auto& [itemId, count] : sel.itemCounts) {
|
|
|
|
|
std::string itemLabel = "Item " + std::to_string(itemId);
|
|
|
|
|
if (const auto* info = gameHandler.getItemInfo(itemId)) {
|
|
|
|
|
if (!info->name.empty()) itemLabel = info->name;
|
|
|
|
|
}
|
|
|
|
|
ImGui::BulletText("%s: %u", itemLabel.c_str(), count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Abandon button
|
|
|
|
|
if (!sel.complete) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::Button("Abandon Quest")) {
|
|
|
|
|
gameHandler.abandonQuest(sel.questId);
|
|
|
|
|
if (selectedIndex >= static_cast<int>(quests.size())) {
|
|
|
|
|
selectedIndex = static_cast<int>(quests.size()) - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!stillOpen) {
|
|
|
|
|
open = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}} // namespace wowee::ui
|