/*
* The ManaPlus Client
* Copyright (C) 2012-2020 The ManaPlus Developers
* Copyright (C) 2020-2023 The ManaVerse Developers
*
* This file is part of The ManaPlus 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 "gui/windows/questswindow.h"
#include "actormanager.h"
#include "configuration.h"
#include "effectmanager.h"
#include "being/localplayer.h"
#include "enums/gui/layouttype.h"
#include "gui/gui.h"
#include "gui/fonts/font.h"
#include "gui/models/questsmodel.h"
#include "gui/windows/setupwindow.h"
#include "gui/widgets/button.h"
#include "gui/widgets/browserbox.h"
#include "gui/widgets/containerplacer.h"
#include "gui/widgets/createwidget.h"
#include "gui/widgets/layout.h"
#include "gui/widgets/extendedlistbox.h"
#include "gui/widgets/itemlinkhandler.h"
#include "gui/widgets/scrollarea.h"
#include "utils/delete2.h"
#include "utils/foreach.h"
#include "utils/gettext.h"
#include "resources/questeffect.h"
#include "resources/questitem.h"
#include "resources/db/questdb.h"
#include "resources/map/map.h"
#include "debug.h"
QuestsWindow *questsWindow = nullptr;
QuestsWindow::QuestsWindow() :
// TRANSLATORS: quests window name
Window(_("Quests"), Modal_false, nullptr, "quests.xml"),
ActionListener(),
mQuestsModel(new QuestsModel),
mQuestsListBox(CREATEWIDGETR(ExtendedListBox,
this, mQuestsModel, "extendedlistbox.xml", 13)),
mQuestScrollArea(new ScrollArea(this, mQuestsListBox,
fromBool(getOptionBool("showlistbackground", false), Opaque),
"quests_list_background.xml")),
mItemLinkHandler(new ItemLinkHandler),
mText(new BrowserBox(this, Opaque_true,
"browserbox.xml")),
mTextScrollArea(new ScrollArea(this, mText,
fromBool(getOptionBool("showtextbackground", false), Opaque),
"quests_text_background.xml")),
// TRANSLATORS: quests window button
mCloseButton(new Button(this, _("Close"), "close", BUTTON_SKIN, this)),
mCompleteIcon(Theme::getImageFromThemeXml("complete_icon.xml", "")),
mIncompleteIcon(Theme::getImageFromThemeXml("incomplete_icon.xml", "")),
mMapEffects(),
mVars(nullptr),
mQuests(nullptr),
mAllEffects(nullptr),
mNpcEffects(),
mQuestLinks(),
mQuestReverseLinks(),
mNewQuestEffectId(paths.getIntValue("newQuestEffectId")),
mCompleteQuestEffectId(paths.getIntValue("completeQuestEffectId")),
mMap(nullptr)
{
setWindowName("Quests");
setResizable(true);
setCloseButton(true);
setStickyButtonLock(true);
setSaveVisible(true);
setDefaultSize(400, 350, ImagePosition::RIGHT, 0, 0);
setMinWidth(310);
setMinHeight(220);
if (setupWindow != nullptr)
setupWindow->registerWindowForReset(this);
mQuestsListBox->setActionEventId("select");
mQuestsListBox->addActionListener(this);
mQuestScrollArea->setHorizontalScrollPolicy(ScrollArea::SHOW_NEVER);
mText->setOpaque(Opaque_false);
mText->setLinkHandler(mItemLinkHandler);
mTextScrollArea->setHorizontalScrollPolicy(ScrollArea::SHOW_NEVER);
mQuestsListBox->setWidth(500);
if ((gui == nullptr) || gui->getNpcFont()->getHeight() < 20)
mQuestsListBox->setRowHeight(20);
else
mQuestsListBox->setRowHeight(gui->getNpcFont()->getHeight());
ContainerPlacer placer(nullptr, nullptr);
placer = getPlacer(0, 0);
placer(0, 0, mQuestScrollArea, 4, 3).setPadding(3);
placer(4, 0, mTextScrollArea, 4, 3).setPadding(3);
placer(7, 3, mCloseButton, 1, 1);
Layout &layout = getLayout();
layout.setRowHeight(0, LayoutType::SET);
loadWindowState();
enableVisibleSound(true);
QuestDb::load();
mVars = QuestDb::getVars();
mQuests = QuestDb::getQuests();
mAllEffects = QuestDb::getAllEffects();
}
QuestsWindow::~QuestsWindow()
{
delete2(mQuestsModel)
QuestDb::unload();
delete2(mItemLinkHandler)
mQuestLinks.clear();
mQuestReverseLinks.clear();
if (mCompleteIcon != nullptr)
{
mCompleteIcon->decRef();
mCompleteIcon = nullptr;
}
if (mIncompleteIcon != nullptr)
{
mIncompleteIcon->decRef();
mIncompleteIcon = nullptr;
}
}
void QuestsWindow::action(const ActionEvent &event)
{
const std::string &eventId = event.getId();
if (eventId == "select")
{
const int id = mQuestsListBox->getSelected();
if (id < 0)
return;
showQuest(mQuestLinks[id]);
}
else if (eventId == "close")
{
setVisible(Visible_false);
}
}
void QuestsWindow::updateQuest(const int var,
const int val1,
const int val2,
const int val3,
const int time1)
{
(*mVars)[var] = QuestVar(val1, val2, val3, time1);
}
void QuestsWindow::rebuild(const bool playSound)
{
mQuestsModel->clear();
mQuestLinks.clear();
mQuestReverseLinks.clear();
StringVect &names = mQuestsModel->getNames();
STD_VECTOR &images = mQuestsModel->getImages();
STD_VECTOR complete;
STD_VECTOR incomplete;
STD_VECTOR hidden;
int updatedQuest = -1;
int newCompleteStatus = -1;
FOR_EACHP (NpcQuestVarMapCIter, it, mVars)
{
const int var = (*it).first;
const QuestVar &val = (*it).second;
const STD_VECTOR &quests = (*mQuests)[var];
FOR_EACH (STD_VECTOR::const_iterator, it2, quests)
{
if (*it2 == nullptr)
continue;
QuestItem *const quest = *it2;
// complete quest
if (quest->complete.find(val.var1) != quest->complete.end())
{
complete.push_back(quest);
}
// incomplete quest
else if (quest->incomplete.find(val.var1) !=
quest->incomplete.end())
{
incomplete.push_back(quest);
}
// hidden quest
else
{
hidden.push_back(quest);
}
}
}
int k = 0;
for (STD_VECTOR::const_iterator it = complete.begin(),
it_end = complete.end(); it != it_end; ++ it, k ++)
{
QuestItem *const quest = *it;
if (quest->completeFlag == 0 || (quest->broken
&& quest->completeFlag == -1))
{
updatedQuest = k;
newCompleteStatus = 1;
}
quest->completeFlag = 1;
mQuestLinks.push_back(quest);
mQuestReverseLinks[quest->var] = k;
names.push_back(quest->name);
if (mCompleteIcon != nullptr)
{
mCompleteIcon->incRef();
images.push_back(mCompleteIcon);
}
else
{
images.push_back(nullptr);
}
}
for (STD_VECTOR::const_iterator it = incomplete.begin(),
it_end = incomplete.end(); it != it_end; ++ it, k ++)
{
QuestItem *const quest = *it;
if (quest->completeFlag == -1)
{
updatedQuest = k;
newCompleteStatus = 0;
}
quest->completeFlag = 0;
mQuestLinks.push_back(quest);
mQuestReverseLinks[quest->var] = k;
names.push_back(quest->name);
if (mIncompleteIcon != nullptr)
{
mIncompleteIcon->incRef();
images.push_back(mIncompleteIcon);
}
else
{
images.push_back(nullptr);
}
}
FOR_EACH (STD_VECTOR::const_iterator, it, hidden)
(*it)->completeFlag = -1;
if (updatedQuest == -1 || updatedQuest >= CAST_S32(
mQuestLinks.size()))
{
updatedQuest = CAST_S32(mQuestLinks.size() - 1);
}
if (updatedQuest >= 0)
{
mQuestsListBox->setSelected(updatedQuest);
showQuest(mQuestLinks[updatedQuest]);
if (playSound && (effectManager != nullptr))
{
switch (newCompleteStatus)
{
case 0:
effectManager->trigger(mNewQuestEffectId, localPlayer, 0);
break;
case 1:
effectManager->trigger(mCompleteQuestEffectId,
localPlayer,
0);
break;
default:
break;
}
}
}
updateEffects();
}
void QuestsWindow::showQuest(const QuestItem *const quest)
{
if (quest == nullptr)
return;
const STD_VECTOR &texts = quest->texts;
const QuestVar &var = (*mVars)[quest->var];
const std::string var1 = toString(var.var1);
const std::string var2 = toString(var.var2);
const std::string var3 = toString(var.var3);
const std::string timeStr = timeDiffToString(var.time1);
mText->clearRows();
FOR_EACH (STD_VECTOR::const_iterator, it, texts)
{
const QuestItemText &data = *it;
std::string text = data.text;
replaceAll(text, "{@@var1}", var1);
replaceAll(text, "{@@var2}", var2);
replaceAll(text, "{@@var3}", var3);
replaceAll(text, "{@@time}", timeStr);
switch (data.type)
{
case QuestType::TEXT:
default:
mText->addRow(text,
false);
break;
case QuestType::NAME:
mText->addRow(std::string("[").append(text).append("]"),
false);
break;
case QuestType::REWARD:
mText->addRow(std::string(
// TRANSLATORS: quest reward
_("Reward:")).append(
" ").append(
text),
false);
break;
case QuestType::GIVER:
mText->addRow(std::string(
// TRANSLATORS: quest giver name
_("Quest Giver:")).append(
" ").append(
text),
false);
break;
case QuestType::NPC:
mText->addRow(std::string(
// TRANSLATORS: quest npc name
_("Npc:")).append(
" ").append(
text),
false);
break;
case QuestType::COORDINATES:
mText->addRow(std::string(
strprintf("%s [@@=navigate %s %s|%s@@]",
// TRANSLATORS: quest coordinates
_("Coordinates:"),
data.data1.c_str(),
data.data2.c_str(),
text.c_str())),
false);
break;
}
}
mText->updateHeight();
}
void QuestsWindow::setMap(const Map *const map)
{
if (mMap != map)
{
mMap = map;
mMapEffects.clear();
if (mMap == nullptr)
return;
const std::string name = mMap->getProperty("shortName",
std::string());
FOR_EACHP (STD_VECTOR::const_iterator, it, mAllEffects)
{
const QuestEffect *const effect = *it;
if ((effect != nullptr) && name == effect->map)
mMapEffects.push_back(effect);
}
updateEffects();
}
}
void QuestsWindow::updateEffects()
{
NpcQuestEffectMap oldNpc = mNpcEffects;
mNpcEffects.clear();
FOR_EACH (STD_VECTOR::const_iterator,
it, mMapEffects)
{
const QuestEffect *const effect = *it;
if (effect != nullptr)
{
const NpcQuestVarMapCIter varIt = mVars->find(effect->var);
if (varIt != mVars->end())
{
const std::set &vals = effect->values;
if (vals.find((*mVars)[effect->var].var1) != vals.end())
mNpcEffects[effect->id] = effect;
}
}
}
if (actorManager == nullptr)
return;
std::set removeEffects;
std::map addEffects;
// for old effects
FOR_EACH (NpcQuestEffectMapCIter, it, oldNpc)
{
const BeingTypeId id = (*it).first;
const QuestEffect *const effect = (*it).second;
const NpcQuestEffectMapCIter itNew = mNpcEffects.find(id);
if (itNew == mNpcEffects.end())
{ // in new list no effect for this npc
removeEffects.insert(id);
}
else
{ // in new list exists effect for this npc
const QuestEffect *const newEffect = (*itNew).second;
if (effect != newEffect)
{ // new effects is not equal to old effect
addEffects[id] = newEffect->effectId;
removeEffects.insert(id);
}
}
}
// for new effects
FOR_EACH (NpcQuestEffectMapCIter, it, mNpcEffects)
{
const BeingTypeId id = (*it).first;
const QuestEffect *const effect = (*it).second;
const NpcQuestEffectMapCIter itNew = oldNpc.find(id);
// check if old effect was not present
if (itNew == oldNpc.end())
addEffects[id] = effect->effectId;
}
if (!removeEffects.empty() || !addEffects.empty())
actorManager->updateEffects(addEffects, removeEffects);
}
void QuestsWindow::addEffect(Being *const being)
{
if (being == nullptr)
return;
const BeingTypeId id = being->getSubType();
const std::map::const_iterator
it = mNpcEffects.find(id);
if (it != mNpcEffects.end())
{
const QuestEffect *const effect = (*it).second;
if (effect != nullptr)
being->addSpecialEffect(effect->effectId);
}
}
void QuestsWindow::selectQuest(const int varId)
{
std::map::const_iterator it = mQuestReverseLinks.find(varId);
if (it == mQuestReverseLinks.end())
return;
if (mVisible == Visible_false)
setVisible(Visible_true);
const int listPos = (*it).second;
if (listPos < 0)
return;
showQuest(mQuestLinks[listPos]);
mQuestsListBox->setSelected(listPos);
requestMoveToTop();
}