diff options
author | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-06-25 16:37:29 +0200 |
---|---|---|
committer | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-07-02 09:48:58 +0200 |
commit | 72d937f176abaaca814fb8407cf1b911e52aaffa (patch) | |
tree | 26240a014826e7ee06deb71dcd86b7f5def93fb4 | |
parent | e2042f2b6684e21b39214ce422212720b4374e1b (diff) | |
download | mana-72d937f176abaaca814fb8407cf1b911e52aaffa.tar.gz mana-72d937f176abaaca814fb8407cf1b911e52aaffa.tar.bz2 mana-72d937f176abaaca814fb8407cf1b911e52aaffa.tar.xz mana-72d937f176abaaca814fb8407cf1b911e52aaffa.zip |
Added Quests window
This window gives an overview over completed and currently active
quests. A persistent checkbox toggles whether completed quests are
shown. Item links are supported in quest texts.
New window icon by meway. Completed quest icon for Mana theme copied
from ManaPlus.
The Quests window has no shortcut for now.
-rw-r--r-- | data/graphics/gui/button-icon-quests.png | bin | 0 -> 450 bytes | |||
-rw-r--r-- | data/graphics/gui/jewelry/theme.xml | 3 | ||||
-rw-r--r-- | data/graphics/gui/quest-icons.png | bin | 0 -> 257 bytes | |||
-rw-r--r-- | data/graphics/gui/theme.xml | 3 | ||||
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/configuration.cpp | 1 | ||||
-rw-r--r-- | src/configuration.h | 1 | ||||
-rw-r--r-- | src/event.h | 6 | ||||
-rw-r--r-- | src/game.cpp | 17 | ||||
-rw-r--r-- | src/gui/questswindow.cpp | 282 | ||||
-rw-r--r-- | src/gui/questswindow.h | 70 | ||||
-rw-r--r-- | src/gui/widgets/browserbox.cpp | 2 | ||||
-rw-r--r-- | src/gui/widgets/itemlinkhandler.cpp | 8 | ||||
-rw-r--r-- | src/gui/widgets/itemlinkhandler.h | 6 | ||||
-rw-r--r-- | src/gui/windowmenu.cpp | 49 | ||||
-rw-r--r-- | src/keyboardconfig.cpp | 1 | ||||
-rw-r--r-- | src/keyboardconfig.h | 1 | ||||
-rw-r--r-- | src/net/playerhandler.h | 7 | ||||
-rw-r--r-- | src/net/tmwa/playerhandler.cpp | 2 | ||||
-rw-r--r-- | src/net/tmwa/playerhandler.h | 3 | ||||
-rw-r--r-- | src/resources/questdb.cpp | 53 | ||||
-rw-r--r-- | src/resources/questdb.h | 17 |
22 files changed, 501 insertions, 33 deletions
diff --git a/data/graphics/gui/button-icon-quests.png b/data/graphics/gui/button-icon-quests.png Binary files differnew file mode 100644 index 00000000..4c2fbd3b --- /dev/null +++ b/data/graphics/gui/button-icon-quests.png diff --git a/data/graphics/gui/jewelry/theme.xml b/data/graphics/gui/jewelry/theme.xml index a6c2e1a0..5253c24f 100644 --- a/data/graphics/gui/jewelry/theme.xml +++ b/data/graphics/gui/jewelry/theme.xml @@ -539,4 +539,7 @@ <icon name="offline" src="window.png" x="65" y="164" width="13" height="13" /> <icon name="online" src="window.png" x="49" y="164" width="13" height="13" /> + + <icon name="complete" src="window.png" x="1" y="223" width="20" height="18" /> + <icon name="incomplete" src="window.png" x="23" y="223" width="20" height="18" /> </theme> diff --git a/data/graphics/gui/quest-icons.png b/data/graphics/gui/quest-icons.png Binary files differnew file mode 100644 index 00000000..a35566a0 --- /dev/null +++ b/data/graphics/gui/quest-icons.png diff --git a/data/graphics/gui/theme.xml b/data/graphics/gui/theme.xml index 3ed93c6e..c1a76875 100644 --- a/data/graphics/gui/theme.xml +++ b/data/graphics/gui/theme.xml @@ -334,4 +334,7 @@ <icon name="offline" src="circle-gray.png" /> <icon name="online" src="circle-green.png" /> + + <icon name="complete" src="quest-icons.png" x="0" y="0" width="16" height="16" /> + <icon name="incomplete" src="quest-icons.png" x="16" y="0" width="16" height="16" /> </theme> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6e3f78bc..f7f3d205 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -230,6 +230,8 @@ set(SRCS gui/palette.h gui/popupmenu.cpp gui/popupmenu.h + gui/questswindow.cpp + gui/questswindow.h gui/quitdialog.cpp gui/quitdialog.h gui/recorder.cpp diff --git a/src/configuration.cpp b/src/configuration.cpp index b27fbe31..10398d6f 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -351,6 +351,7 @@ void serdeOptions(T option) option("showgender", &Config::showGender); option("showMonstersTakedDamage", &Config::showMonstersTakedDamage); option("showWarps", &Config::showWarps); + option("hideCompletedQuests", &Config::hideCompletedQuests); option("particleMaxCount", &Config::particleMaxCount); option("particleFastPhysics", &Config::particleFastPhysics); option("particleEmitterSkip", &Config::particleEmitterSkip); diff --git a/src/configuration.h b/src/configuration.h index 3ed3c77e..9e00cb74 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -181,6 +181,7 @@ struct Config bool showGender = false; bool showMonstersTakedDamage = false; bool showWarps = true; + bool hideCompletedQuests = false; int particleMaxCount = 3000; int particleFastPhysics = 0; int particleEmitterSkip = 1; diff --git a/src/event.h b/src/event.h index d3fdede9..3cbd9123 100644 --- a/src/event.h +++ b/src/event.h @@ -53,7 +53,8 @@ public: ItemChannel, NoticesChannel, NpcChannel, - StorageChannel + StorageChannel, + QuestsChannel }; enum Type @@ -99,7 +100,8 @@ public: UpdateStat, UpdateStatusEffect, Whisper, - WhisperError + WhisperError, + QuestVarsChanged, }; /** diff --git a/src/game.cpp b/src/game.cpp index 660a19db..34104aa7 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -21,15 +21,15 @@ #include "game.h" -#include "actorspritemanager.h" #include "actorsprite.h" +#include "actorspritemanager.h" #include "channelmanager.h" #include "client.h" #include "commandhandler.h" #include "configuration.h" #include "effectmanager.h" -#include "event.h" #include "emoteshortcut.h" +#include "event.h" #include "graphics.h" #include "itemshortcut.h" #include "joystick.h" @@ -41,23 +41,24 @@ #include "playerrelations.h" #include "sound.h" +#include "gui/abilitieswindow.h" #include "gui/chatwindow.h" #include "gui/debugwindow.h" #include "gui/equipmentwindow.h" #include "gui/gui.h" #include "gui/helpwindow.h" #include "gui/inventorywindow.h" -#include "gui/shortcutwindow.h" #include "gui/minimap.h" #include "gui/ministatuswindow.h" #include "gui/npcdialog.h" #include "gui/okdialog.h" #include "gui/outfitwindow.h" +#include "gui/questswindow.h" #include "gui/quitdialog.h" #include "gui/setup.h" -#include "gui/socialwindow.h" -#include "gui/abilitieswindow.h" +#include "gui/shortcutwindow.h" #include "gui/skilldialog.h" +#include "gui/socialwindow.h" #include "gui/statuswindow.h" #include "gui/textdialog.h" #include "gui/tradewindow.h" @@ -94,6 +95,7 @@ StatusWindow *statusWindow; MiniStatusWindow *miniStatusWindow; InventoryWindow *inventoryWindow; SkillDialog *skillDialog; +QuestsWindow *questsWindow; Minimap *minimap; EquipmentWindow *equipmentWindow; TradeWindow *tradeWindow; @@ -150,6 +152,7 @@ static void createGuiWindows() statusWindow = new StatusWindow; inventoryWindow = new InventoryWindow(PlayerInfo::getInventory()); skillDialog = new SkillDialog; + questsWindow = new QuestsWindow; helpWindow = new HelpWindow; debugWindow = new DebugWindow; itemShortcutWindow = new ShortcutWindow("ItemShortcut", @@ -602,6 +605,7 @@ bool Game::keyDownEvent(SDL_KeyboardEvent &event) statusWindow->setVisible(false); inventoryWindow->setVisible(false); skillDialog->setVisible(false); + questsWindow->setVisible(false); setupWindow->setVisible(false); equipmentWindow->setVisible(false); helpWindow->setVisible(false); @@ -621,6 +625,9 @@ bool Game::keyDownEvent(SDL_KeyboardEvent &event) case KeyboardConfig::KEY_WINDOW_SKILL: requestedWindow = skillDialog; break; + case KeyboardConfig::KEY_WINDOW_QUESTS: + requestedWindow = questsWindow; + break; case KeyboardConfig::KEY_WINDOW_MINIMAP: minimap->toggle(); return true; diff --git a/src/gui/questswindow.cpp b/src/gui/questswindow.cpp new file mode 100644 index 00000000..99d6bd6d --- /dev/null +++ b/src/gui/questswindow.cpp @@ -0,0 +1,282 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <guichan/font.hpp> + +#include <algorithm> + +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<QuestEntry> &getQuests() const + { return mQuests; } + + void setQuests(const std::vector<QuestEntry> &quests) + { mQuests = quests; } + +private: + std::vector<QuestEntry> 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<unsigned>(rowHeight, icon->getHeight() + 2); + + return rowHeight; +} + +void QuestsListBox::draw(gcn::Graphics *gcnGraphics) +{ + if (!mListModel) + return; + + auto *graphics = static_cast<Graphics *>(gcnGraphics); + auto *model = static_cast<QuestsModel *>(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<QuestsModel>()) + , mQuestsListBox(new QuestsListBox(mQuestsModel.get())) + , mHideCompletedCheckBox(new CheckBox(_("Hide completed"), config.hideCompletedQuests)) + , mQuestDetails(new BrowserBox(BrowserBox::AUTO_WRAP)) + , mLinkHandler(std::make_unique<ItemLinkHandler>()) +{ + setWindowName("Quests"); + setupWindow->registerWindowForReset(this); + setResizable(true); + setCloseButton(true); + setSaveVisible(true); + + setDefaultSize(387, 307, ImageRect::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<int>(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; + } + } +} diff --git a/src/gui/questswindow.h b/src/gui/questswindow.h new file mode 100644 index 00000000..22f2a32c --- /dev/null +++ b/src/gui/questswindow.h @@ -0,0 +1,70 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "eventlistener.h" + +#include "gui/widgets/window.h" + +#include <guichan/actionlistener.hpp> +#include <guichan/keylistener.hpp> +#include <guichan/selectionlistener.hpp> + +class BrowserBox; +class CheckBox; +class LinkHandler; +class QuestsListBox; +class QuestsModel; +class ScrollArea; + +/** + * Quests window. + * + * \ingroup Interface + */ +class QuestsWindow final : public Window, + public gcn::ActionListener, + public gcn::SelectionListener, + public EventListener +{ +public: + QuestsWindow(); + + void action(const gcn::ActionEvent &event) override; + + void valueChanged(const gcn::SelectionEvent &event) override; + + void event(Event::Channel channel, const Event &event) override; + +private: + void refreshQuestList(); + void updateQuestDetails(); + + int mSelectedQuestIndex = -1; + std::unique_ptr<QuestsModel> mQuestsModel; + QuestsListBox *mQuestsListBox; + CheckBox *mHideCompletedCheckBox; + BrowserBox *mQuestDetails; + ScrollArea *mQuestDetailsScrollArea; + std::unique_ptr<LinkHandler> mLinkHandler; +}; + +extern QuestsWindow *questsWindow; diff --git a/src/gui/widgets/browserbox.cpp b/src/gui/widgets/browserbox.cpp index 4fe85ae5..1deea30b 100644 --- a/src/gui/widgets/browserbox.cpp +++ b/src/gui/widgets/browserbox.cpp @@ -228,7 +228,7 @@ void BrowserBox::addRow(std::string_view row) void BrowserBox::clearRows() { mTextRows.clear(); - setSize(0, 0); + setSize(mMode == AUTO_SIZE ? 0 : getWidth(), 0); mHoveredLink.reset(); maybeRelayoutText(); } diff --git a/src/gui/widgets/itemlinkhandler.cpp b/src/gui/widgets/itemlinkhandler.cpp index f596fc82..81a332ad 100644 --- a/src/gui/widgets/itemlinkhandler.cpp +++ b/src/gui/widgets/itemlinkhandler.cpp @@ -42,6 +42,7 @@ ItemLinkHandler::ItemLinkHandler(Window *parent) : mParent(parent) { mItemPopup = std::make_unique<ItemPopup>(); + mItemPopup->addDeathListener(this); } ItemLinkHandler::~ItemLinkHandler() = default; @@ -97,3 +98,10 @@ void ItemLinkHandler::action(const gcn::ActionEvent &actionEvent) #endif } } + +void ItemLinkHandler::death(const gcn::Event &event) +{ + // If somebody else killed the PopupUp, make sure we don't also try to delete it + if (event.getSource() == mItemPopup.get()) + mItemPopup.release(); +} diff --git a/src/gui/widgets/itemlinkhandler.h b/src/gui/widgets/itemlinkhandler.h index 58202d33..637482bd 100644 --- a/src/gui/widgets/itemlinkhandler.h +++ b/src/gui/widgets/itemlinkhandler.h @@ -24,13 +24,14 @@ #include "gui/widgets/linkhandler.h" #include <guichan/actionlistener.hpp> +#include <guichan/deathlistener.hpp> #include <memory> class ItemPopup; class Window; -class ItemLinkHandler : public LinkHandler, gcn::ActionListener +class ItemLinkHandler : public LinkHandler, gcn::ActionListener, public gcn::DeathListener { public: ItemLinkHandler(Window *parent = nullptr); @@ -42,6 +43,9 @@ class ItemLinkHandler : public LinkHandler, gcn::ActionListener // ActionListener interface void action(const gcn::ActionEvent &actionEvent) override; + // DeathListener interface + void death(const gcn::Event &event) override; + private: std::unique_ptr<ItemPopup> mItemPopup; diff --git a/src/gui/windowmenu.cpp b/src/gui/windowmenu.cpp index 0b2d126f..2c1b6211 100644 --- a/src/gui/windowmenu.cpp +++ b/src/gui/windowmenu.cpp @@ -23,9 +23,10 @@ #include "graphics.h" +#include "gui/abilitieswindow.h" #include "gui/emotepopup.h" +#include "gui/questswindow.h" #include "gui/skilldialog.h" -#include "gui/abilitieswindow.h" #include "gui/widgets/button.h" #include "gui/widgets/window.h" @@ -34,6 +35,8 @@ #include "net/net.h" #include "net/playerhandler.h" +#include "resources/questdb.h" + #include "utils/gettext.h" #include <string> @@ -64,6 +67,9 @@ WindowMenu::WindowMenu() if (abilitiesWindow->hasAbilities()) addButton(N_("Abilities"), x, h, "button-icon-abilities.png"); + if (QuestDB::hasQuests()) + addButton(N_("Quests"), x, h, "button-icon-quests.png"); + addButton(N_("Social"), x, h, "button-icon-social.png", KeyboardConfig::KEY_WINDOW_SOCIAL); addButton(N_("Shortcuts"), x, h, "button-icon-shortcut.png", @@ -122,6 +128,10 @@ void WindowMenu::action(const gcn::ActionEvent &event) { window = skillDialog; } + else if (event.getId() == "Quests") + { + window = questsWindow; + } else if (event.getId() == "Abilities") { window = abilitiesWindow; @@ -166,12 +176,18 @@ static std::string createShortcutCaption(const std::string &text, KeyboardConfig::KeyAction key) { std::string caption = gettext(text.c_str()); + if (key != KeyboardConfig::KEY_NO_VALUE) { - caption += " ("; - caption += SDL_GetKeyName(keyboard.getKeyValue(key)); - caption += ")"; + auto keyValue = keyboard.getKeyValue(key); + if (keyValue > 0) + { + caption += " ("; + caption += SDL_GetKeyName(keyValue); + caption += ")"; + } } + return caption; } @@ -179,7 +195,7 @@ void WindowMenu::addButton(const std::string &text, int &x, int &h, const std::string &iconPath, KeyboardConfig::KeyAction key) { - auto *btn = new Button("", text, this); + auto *btn = new Button(std::string(), text, this); if (!iconPath.empty() && btn->setButtonIcon(iconPath)) { btn->setButtonPopupText(createShortcutCaption(text, key)); @@ -187,7 +203,7 @@ void WindowMenu::addButton(const std::string &text, int &x, int &h, else { btn->setCaption(gettext(text.c_str())); - btn->setButtonPopupText(createShortcutCaption("", key)); + btn->setButtonPopupText(createShortcutCaption(std::string(), key)); } btn->setPosition(x, 0); @@ -204,40 +220,45 @@ void WindowMenu::updatePopUpCaptions() if (!button) continue; - std::string eventId = button->getActionEventId(); + const std::string &eventId = button->getActionEventId(); if (eventId == "Status") { - button->setButtonPopupText(createShortcutCaption("Status", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_STATUS)); } else if (eventId == "Equipment") { - button->setButtonPopupText(createShortcutCaption("Equipment", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_EQUIPMENT)); } else if (eventId == "Inventory") { - button->setButtonPopupText(createShortcutCaption("Inventory", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_INVENTORY)); } else if (eventId == "Skills") { - button->setButtonPopupText(createShortcutCaption("Skills", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SKILL)); } + else if (eventId == "Quests") + { + button->setButtonPopupText( + createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_QUESTS)); + } else if (eventId == "Social") { - button->setButtonPopupText(createShortcutCaption("Social", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SOCIAL)); } else if (eventId == "Shortcuts") { - button->setButtonPopupText(createShortcutCaption("Shortcuts", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SHORTCUT)); } else if (eventId == "Setup") { - button->setButtonPopupText(createShortcutCaption("Setup", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SETUP)); } } diff --git a/src/keyboardconfig.cpp b/src/keyboardconfig.cpp index ec5b5e37..92d37ace 100644 --- a/src/keyboardconfig.cpp +++ b/src/keyboardconfig.cpp @@ -71,6 +71,7 @@ static KeyData const keyData[KeyboardConfig::KEY_TOTAL] = { { "WindowInventory", SDLK_F3, _("Inventory Window") }, { "WindowEquipment", SDLK_F4, _("Equipment Window") }, { "WindowSkill", SDLK_F5, _("Skill Window") }, + { "WindowQuests", KeyboardConfig::KEY_NO_VALUE, _("Quest Window") }, { "WindowMinimap", SDLK_F6, _("Minimap Window") }, { "WindowChat", SDLK_F7, _("Chat Window") }, { "WindowShortcut", SDLK_F8, _("Item Shortcut Window") }, diff --git a/src/keyboardconfig.h b/src/keyboardconfig.h index 6fc79ced..a9deee64 100644 --- a/src/keyboardconfig.h +++ b/src/keyboardconfig.h @@ -191,6 +191,7 @@ class KeyboardConfig KEY_WINDOW_INVENTORY, KEY_WINDOW_EQUIPMENT, KEY_WINDOW_SKILL, + KEY_WINDOW_QUESTS, KEY_WINDOW_MINIMAP, KEY_WINDOW_CHAT, KEY_WINDOW_SHORTCUT, diff --git a/src/net/playerhandler.h b/src/net/playerhandler.h index b9cf1abf..e5b86b2e 100644 --- a/src/net/playerhandler.h +++ b/src/net/playerhandler.h @@ -24,6 +24,8 @@ #include "being.h" #include "flooritem.h" +#include "resources/questdb.h" + namespace Net { class PlayerHandler @@ -80,6 +82,11 @@ class PlayerHandler * Return false when tiles-center positions only are to be used. */ virtual bool usePixelPrecision() = 0; + + const QuestVars &getQuestVars() const { return mQuestVars; } + + protected: + QuestVars mQuestVars; }; } // namespace Net diff --git a/src/net/tmwa/playerhandler.cpp b/src/net/tmwa/playerhandler.cpp index 14477e7b..e590d6e0 100644 --- a/src/net/tmwa/playerhandler.cpp +++ b/src/net/tmwa/playerhandler.cpp @@ -527,6 +527,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) int value = msg.readInt32(); mQuestVars.set(variable, value); updateQuestStatusEffects(); + Event::trigger(Event::QuestsChannel, Event::QuestVarsChanged); break; } @@ -542,6 +543,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) mQuestVars.set(variable, value); } updateQuestStatusEffects(); + Event::trigger(Event::QuestsChannel, Event::QuestVarsChanged); break; } } diff --git a/src/net/tmwa/playerhandler.h b/src/net/tmwa/playerhandler.h index 6d2da1af..49990d85 100644 --- a/src/net/tmwa/playerhandler.h +++ b/src/net/tmwa/playerhandler.h @@ -26,8 +26,6 @@ #include "net/tmwa/messagehandler.h" -#include "resources/questdb.h" - namespace TmwAthena { class PlayerHandler final : public MessageHandler, public Net::PlayerHandler, @@ -75,7 +73,6 @@ class PlayerHandler final : public MessageHandler, public Net::PlayerHandler, private: void updateQuestStatusEffects(); - QuestVars mQuestVars; QuestEffectMap mActiveQuestEffects; }; diff --git a/src/resources/questdb.cpp b/src/resources/questdb.cpp index 13bc2112..93ee3225 100644 --- a/src/resources/questdb.cpp +++ b/src/resources/questdb.cpp @@ -30,6 +30,13 @@ namespace QuestDB { // The quests are stored in a map using their variable ID as the key static std::unordered_map<int, Quest> quests; +// Helper function to check if a container contains a value +template<typename Container, typename Value> +static bool contains(const Container &container, const Value &value) +{ + return std::find(container.begin(), container.end(), value) != container.end(); +} + void readQuestVarNode(XML::Node node, const std::string &filename) { int varId = 0; @@ -93,6 +100,12 @@ void readQuestVarNode(XML::Node node, const std::string &filename) QuestRow &row = state.rows.emplace_back(rowType); row.text = questChild.textContent(); + + if (rowType == QuestRowType::Coordinates) + { + questChild.attribute("x", row.x); + questChild.attribute("y", row.y); + } } } } @@ -103,11 +116,9 @@ void unload() quests.clear(); } -const Quest &get(int var) +bool hasQuests() { - static Quest emptyQuest; - auto it = quests.find(var); - return it == quests.end() ? emptyQuest : it->second; + return !quests.empty(); } // In quests, the map name may include the file extension. This is discouraged @@ -123,7 +134,7 @@ QuestEffectMap getActiveEffects(const QuestVars &questVars, { QuestEffectMap activeEffects; - for (auto &[var, quest] : quests) + for (auto &[var, quest] : std::as_const(quests)) { auto value = questVars.get(var); @@ -131,7 +142,7 @@ QuestEffectMap getActiveEffects(const QuestVars &questVars, { if (baseName(effect.map) != mapName) continue; - if (std::find(effect.values.begin(), effect.values.end(), value) == effect.values.end()) + if (!contains(effect.values, value)) continue; activeEffects.set(effect.npcId, effect.statusEffectId); @@ -141,4 +152,34 @@ QuestEffectMap getActiveEffects(const QuestVars &questVars, return activeEffects; } +std::vector<QuestEntry> getQuestsEntries(const QuestVars &questVars, + bool skipCompleted) +{ + std::vector<QuestEntry> activeQuests; + + for (auto &[varId, quest] : std::as_const(quests)) + { + auto value = questVars.get(varId); + + for (auto &state : quest.states) + { + bool matchesIncomplete = contains(state.incomplete, value); + bool matchesComplete = contains(state.complete, value); + + if (skipCompleted && matchesComplete) + continue; + + if (matchesIncomplete || matchesComplete) + { + QuestEntry &entry = activeQuests.emplace_back(); + entry.varId = varId; + entry.completed = matchesComplete; + entry.state = &state; + } + } + } + + return activeQuests; +} + } // namespace QuestDB diff --git a/src/resources/questdb.h b/src/resources/questdb.h index 23817b81..e57655f8 100644 --- a/src/resources/questdb.h +++ b/src/resources/questdb.h @@ -85,6 +85,8 @@ struct QuestRow QuestRowType type; std::string text; + int x = 0; + int y = 0; }; struct QuestState @@ -102,13 +104,26 @@ struct Quest std::vector<QuestState> states; }; +struct QuestEntry +{ + int varId; + bool completed; + const QuestState *state; + + const std::string &name() const { return state->name; } + const std::vector<QuestRow> &rows() const { return state->rows; } +}; + namespace QuestDB { void readQuestVarNode(XML::Node node, const std::string &filename); void unload(); - const Quest &get(int var); + bool hasQuests(); QuestEffectMap getActiveEffects(const QuestVars &questVars, const std::string &mapName); + + std::vector<QuestEntry> getQuestsEntries(const QuestVars &questVars, + bool skipCompleted = false); }; |