/* * The Mana Client * Copyright (C) 2025 The Mana Developers * * This file is part of The Mana Client. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "questswindow.h" #include "configuration.h" #include "gui/setup.h" #include "gui/widgets/browserbox.h" #include "gui/widgets/checkbox.h" #include "gui/widgets/itemlinkhandler.h" #include "gui/widgets/layout.h" #include "gui/widgets/listbox.h" #include "gui/widgets/scrollarea.h" #include "net/net.h" #include "net/playerhandler.h" #include "resources/questdb.h" #include "utils/gettext.h" #include #include class QuestsModel final : public gcn::ListModel { public: int getNumberOfElements() override { return mQuests.size(); } std::string getElementAt(int i) override { return mQuests[i].name(); } const std::vector &getQuests() const { return mQuests; } void setQuests(const std::vector &quests) { mQuests = quests; } private: std::vector mQuests; }; class QuestsListBox final : public ListBox { public: QuestsListBox(QuestsModel *model) : ListBox(model) {} unsigned getRowHeight() const override; void draw(gcn::Graphics *graphics) override; }; unsigned QuestsListBox::getRowHeight() const { auto rowHeight = ListBox::getRowHeight(); if (auto icon = gui->getTheme()->getIcon("complete")) rowHeight = std::max(rowHeight, icon->getHeight() + 2); return rowHeight; } void QuestsListBox::draw(gcn::Graphics *gcnGraphics) { if (!mListModel) return; auto *graphics = static_cast(gcnGraphics); auto *model = static_cast(getListModel()); const int rowHeight = getRowHeight(); auto theme = gui->getTheme(); auto completeIcon = theme->getIcon("complete"); auto incompleteIcon = theme->getIcon("incomplete"); // Draw filled rectangle around the selected list element if (mSelected >= 0) { auto highlightColor = Theme::getThemeColor(Theme::HIGHLIGHT); highlightColor.a = gui->getTheme()->getGuiAlpha(); graphics->setColor(highlightColor); graphics->fillRectangle(gcn::Rectangle(0, rowHeight * mSelected, getWidth(), rowHeight)); } // Draw the list elements graphics->setFont(getFont()); graphics->setColor(Theme::getThemeColor(Theme::TEXT)); const int fontHeight = getFont()->getHeight(); int y = 0; for (auto &quest : model->getQuests()) { int x = 1; if (const Image *icon = quest.completed ? completeIcon : incompleteIcon) { graphics->drawImage(icon, x, y + (rowHeight - icon->getHeight()) / 2); x += icon->getWidth() + 4; } graphics->drawText(quest.name(), x, y + (rowHeight - fontHeight) / 2); y += rowHeight; } } QuestsWindow::QuestsWindow() : Window(_("Quests")) , mQuestsModel(std::make_unique()) , mQuestsListBox(new QuestsListBox(mQuestsModel.get())) , mHideCompletedCheckBox(new CheckBox(_("Hide completed"), config.hideCompletedQuests)) , mQuestDetails(new BrowserBox(BrowserBox::AUTO_WRAP)) , mLinkHandler(std::make_unique()) { setWindowName("Quests"); setupWindow->registerWindowForReset(this); setResizable(true); setCloseButton(true); setSaveVisible(true); setDefaultSize(387, 307, WindowAlignment::Center); setMinWidth(316); setMinHeight(179); mQuestsListBox->addSelectionListener(this); mHideCompletedCheckBox->setActionEventId("hideCompleted"); mHideCompletedCheckBox->addActionListener(this); auto questListScrollArea = new ScrollArea(mQuestsListBox); questListScrollArea->setHorizontalScrollPolicy(gcn::ScrollArea::SHOW_NEVER); mQuestDetails->setLinkHandler(mLinkHandler.get()); mQuestDetailsScrollArea = new ScrollArea(mQuestDetails); mQuestDetailsScrollArea->setHorizontalScrollPolicy(gcn::ScrollArea::SHOW_NEVER); auto place = getPlacer(0, 0); place(0, 0, questListScrollArea, 2, 2).setPadding(2); place(2, 0, mQuestDetailsScrollArea, 3, 2).setPadding(2); place = getPlacer(0, 1); place(0, 0, mHideCompletedCheckBox); getLayout().setRowHeight(1, 0); // Don't scale up the bottom row listen(Event::QuestsChannel); refreshQuestList(); loadWindowState(); } void QuestsWindow::action(const gcn::ActionEvent &event) { if (event.getId() == "hideCompleted") { config.hideCompletedQuests = mHideCompletedCheckBox->isSelected(); refreshQuestList(); } } void QuestsWindow::valueChanged(const gcn::SelectionEvent &event) { if (mSelectedQuestIndex != mQuestsListBox->getSelected()) updateQuestDetails(); } void QuestsWindow::event(Event::Channel channel, const Event &event) { if (channel == Event::QuestsChannel) { if (event.getType() == Event::QuestVarsChanged) refreshQuestList(); } } void QuestsWindow::refreshQuestList() { // Store the currently selected quest state and varId to preserve selection const QuestState *selectedQuestState = nullptr; int selectedVarId = -1; if (mSelectedQuestIndex >= 0 && mSelectedQuestIndex < mQuestsModel->getNumberOfElements()) { const auto &selectedQuest = mQuestsModel->getQuests().at(mSelectedQuestIndex); selectedQuestState = selectedQuest.state; selectedVarId = selectedQuest.varId; } auto &questVars = Net::getPlayerHandler()->getQuestVars(); auto newQuests = QuestDB::getQuestsEntries(questVars, config.hideCompletedQuests); // Put completed quests at the top std::stable_sort(newQuests.begin(), newQuests.end(), [](const QuestEntry &a, const QuestEntry &b) { return a.completed > b.completed; }); mQuestsModel->setQuests(newQuests); if (!selectedQuestState) return; // Try to find and reselect the same quest, preferring exact state match int newSelectedIndex = -1; for (int i = 0; i < static_cast(newQuests.size()); ++i) { if (newQuests[i].state == selectedQuestState) { newSelectedIndex = i; break; } else if (newSelectedIndex == -1 && newQuests[i].varId == selectedVarId) { newSelectedIndex = i; // Don't break here - continue looking for exact state match } } if (mSelectedQuestIndex != newSelectedIndex) mQuestsListBox->setSelected(newSelectedIndex); else updateQuestDetails(); } void QuestsWindow::updateQuestDetails() { mQuestDetails->clearRows(); mSelectedQuestIndex = mQuestsListBox->getSelected(); if (mSelectedQuestIndex < 0 || mSelectedQuestIndex >= mQuestsModel->getNumberOfElements()) return; const QuestEntry &quest = mQuestsModel->getQuests().at(mSelectedQuestIndex); for (const auto &row : quest.rows()) { switch (row.type) { case QuestRowType::Text: mQuestDetails->addRow(row.text); break; case QuestRowType::Name: mQuestDetails->addRow("[" + row.text + "]"); break; case QuestRowType::Reward: mQuestDetails->addRow(strprintf(_("Reward: %s"), row.text.c_str())); break; case QuestRowType::Giver: mQuestDetails->addRow(strprintf(_("Quest Giver: %s"), row.text.c_str())); break; case QuestRowType::Coordinates: mQuestDetails->addRow(strprintf(_("Coordinates: %s (%d, %d)"), row.text.c_str(), row.x, row.y)); break; case QuestRowType::NPC: mQuestDetails->addRow(strprintf(_("NPC: %s"), row.text.c_str())); break; } } }