From 0cf3205e150227cde2a58d921ed6a29e4180966b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 02:19:58 +0200 Subject: [PATCH 01/28] Fix quest regressions --- lib/mapObjects/CQuest.cpp | 57 +++++++++++++++++++++++++++++++++------ lib/mapObjects/CQuest.h | 4 +-- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index b32d833f8..18ee673f5 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -178,6 +178,49 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const } } +void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const +{ + switch (missionType) + { + case CQuest::MISSION_ART: + for(auto & elem : m5arts) + { + if(h->hasArt(elem)) + { + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + } + else + { + const auto * assembly = h->getAssemblyByConstituent(elem); + assert(assembly); + auto parts = assembly->getPartsInfo(); + + // Remove the assembly + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + + // Disassemble this backpack artifact + for(const auto & ci : parts) + { + if(ci.art->getTypeId() != elem) + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); + } + } + } + break; + case CQuest::MISSION_ARMY: + cb->takeCreatures(h->id, m6creatures); + break; + case CQuest::MISSION_RESOURCES: + for (int i = 0; i < 7; ++i) + { + cb->giveResource(h->getOwner(), static_cast(i), -static_cast(m7resources[i])); + } + break; + default: + break; + } +} + void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const { MetaString text; @@ -590,9 +633,9 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomFirst) quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get()); if(!quest->isCustomNext) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); + quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); if(!quest->isCustomComplete) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); } else { @@ -758,11 +801,6 @@ int CGSeerHut::checkDirection() const } } -void CGSeerHut::completeQuest() const //reward -{ - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete -} - const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); @@ -785,7 +823,10 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) { CRewardableObject::blockingDialogAnswered(hero, answer); if(answer) - completeQuest(); + { + quest->completeQuest(cb, hero); + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + } } void CGSeerHut::afterAddToMap(CMap* map) diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 82a6abd7e..535691b98 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -83,7 +83,7 @@ public: virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry - virtual void completeQuest (const CGHeroInstance * h) const {}; + virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; virtual void addReplacements(MetaString &out, const std::string &base) const; void addArtifactID(const ArtifactID & id); @@ -156,8 +156,6 @@ public: const CGHeroInstance *getHeroToKill(bool allowNull = false) const; const CGCreature *getCreatureToKill(bool allowNull = false) const; void getRolloverText (MetaString &text, bool onHover) const; - void finishQuest (const CGHeroInstance * h, ui32 accept) const; //common for both objects - virtual void completeQuest() const; void afterAddToMap(CMap * map) override; From 937a8d63c72802ecf27ef06a38941e9eba463385 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 02:33:16 +0200 Subject: [PATCH 02/28] Check for artifacts copies in limiter --- lib/rewardable/Limiter.cpp | 19 ++++++++++++++++--- lib/rewardable/Limiter.h | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index bf3fb2911..9611f67aa 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -17,6 +17,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" #include "../CSkillHandler.h" +#include "../ArtifactUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -93,12 +94,24 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } - for(const auto & art : artifacts) { - if (!hero->hasArt(art)) + std::unordered_map artifactsRequirements; // artifact ID -> required count + for(const auto & art : artifacts) + ++artifactsRequirements[art]; + + size_t reqSlots = 0; + for(const auto & elem : artifactsRequirements) + { + // check required amount of artifacts + if(hero->getArtPosCount(elem.first, false, true, true) < elem.second) + return false; + if(!hero->hasArt(elem.first)) + reqSlots += hero->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; + } + if(!ArtifactUtils::isBackpackFreeSlots(hero, reqSlots)) return false; } - + for(const auto & sublimiter : noneOf) { if (sublimiter->heroAllowed(hero)) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 68639bb68..1f92cf4f0 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -52,7 +52,7 @@ struct DLL_LINKAGE Limiter std::map secondary; /// artifacts that hero needs to have (equipped or in backpack) to trigger this - /// Note: does not checks for multiple copies of the same arts + /// checks for artifacts copies if same artifact id is included multiple times std::vector artifacts; /// Spells that hero must have in the spellbook From 6a7b23c007dd44b2cf6a778b637d0ce7607342c3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 04:19:12 +0200 Subject: [PATCH 03/28] Additional properties for limiter --- docs/modders/Map_Objects/Rewardable.md | 36 +++++++++++++++++++--- lib/JsonRandom.cpp | 41 ++++++++++++++++++++++++++ lib/JsonRandom.h | 4 +++ lib/rewardable/Info.cpp | 4 +++ lib/rewardable/Limiter.cpp | 14 +++++++++ lib/rewardable/Limiter.h | 10 +++++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 1694b7e7a..3aa316327 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -72,7 +72,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid if any of these conditions are true "anyOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] @@ -80,12 +80,12 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid only if none of these conditions are true "noneOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } @@ -95,7 +95,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // object will be disappeared after taking reward is set to true "removeObject": false - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ], @@ -450,4 +450,32 @@ Keep in mind, that all randomization is performed on map load and on object rese "spell" : "townPortal", "schoolLevel": 3 } +``` + +### Player color +- Can be used as limiter +- Can NOT be used as reward +- Only players with specific color can pass the limiter +- If not specified or empty all colors may pass the limiter + +```jsonc +"colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] +``` + +### Hero types +- Can be used as limiter +- Can NOT be used as reward +- Only specific heroes can pass the limiter + +```jsonc +"heroes" : [ "orrin" ] +``` + +### Hero classes +- Can be used as limiter +- Can NOT be used as reward +- Only heroes belonging to specific classes can pass the limiter + +```jsonc +"heroClasses" : [ "battlemage" ] ``` \ No newline at end of file diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 270c47b80..95ce1f4f4 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -22,6 +22,7 @@ #include "CCreatureSet.h" #include "spells/CSpellHandler.h" #include "CSkillHandler.h" +#include "CHeroHandler.h" #include "IGameCallback.h" #include "mapObjects/IObjectInterface.h" #include "modding/IdentifierStorage.h" @@ -282,6 +283,46 @@ namespace JsonRandom return ret; } + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + std::set def; + + for(auto & color : GameConstants::PLAYER_COLOR_NAMES) + def.insert(color); + + for(auto & entry : value.Vector()) + { + auto key = loadKey(entry, rng, def); + auto pos = vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, key); + if(pos < 0) + logMod->warn("Unable to determine player color %s", key); + else + ret.emplace_back(pos); + } + return ret; + } + + std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroTypes()->getByIndex(VLC->identifiers()->getIdentifier("hero", entry.String()).value())->getId()); + } + return ret; + } + + std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroClasses()->getByIndex(VLC->identifiers()->getIdentifier("heroClass", entry.String()).value())->getId()); + } + return ret; + } + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) { CStackBasicDescriptor stack; diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 2ac9e1d65..ef066d14e 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -48,6 +48,10 @@ namespace JsonRandom DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value); + DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); //DLL_LINKAGE std::vector loadComponents(const JsonNode & value); } diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index c82fca352..b9a8f1b2b 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -122,6 +122,10 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); limiter.spells = JsonRandom::loadSpells(source["spells"], rng, spells); limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + + limiter.players = JsonRandom::loadColors(source["colors"], rng); + limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); + limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); limiter.allOf = configureSublimiters(object, rng, source["allOf"] ); limiter.anyOf = configureSublimiters(object, rng, source["anyOf"] ); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 9611f67aa..8167da026 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -16,6 +16,7 @@ #include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" +#include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../ArtifactUtils.h" @@ -112,6 +113,16 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } + if(!players.empty() && !vstd::contains(players, hero->getOwner())) + return false; + + if(!heroes.empty() && !vstd::contains(heroes, hero->type->getId())) + return false; + + if(!heroClasses.empty() && !vstd::contains(heroClasses, hero->type->heroClass->getId())) + return false; + + for(const auto & sublimiter : noneOf) { if (sublimiter->heroAllowed(hero)) @@ -143,6 +154,9 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); + handler.serializeIdArray("hero", heroes); + handler.serializeIdArray("heroClass", heroClasses); + handler.serializeIdArray("color", players); handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); handler.enterArray("creatures").serializeStruct(creatures); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 1f92cf4f0..300e572ae 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -60,6 +60,13 @@ struct DLL_LINKAGE Limiter /// creatures that hero needs to have std::vector creatures; + + /// only heroes/hero classes from list could pass limiter + std::vector heroes; + std::vector heroClasses; + + /// only player colors can pass limiter + std::vector players; /// sub-limiters, all must pass for this limiter to pass LimitersList allOf; @@ -91,6 +98,9 @@ struct DLL_LINKAGE Limiter h & allOf; h & anyOf; h & noneOf; + h & heroes; + h & heroClasses; + h & players; } void serializeJson(JsonSerializeFormat & handler); From b7568a160cd033125e6bf6d5af30e1f0180b64d1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 04:26:08 +0200 Subject: [PATCH 04/28] Fixes --- lib/constants/EntityIdentifiers.cpp | 21 +++++++++++++++++++++ lib/constants/EntityIdentifiers.h | 4 ++++ lib/rewardable/Limiter.cpp | 6 +++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 4fe197b5c..eaa790c9b 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include @@ -103,6 +105,25 @@ namespace GameConstants #endif } +si32 HeroClassID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string HeroClassID::encode(const si32 index) +{ + return VLC->heroClasses()->getByIndex(index)->getJsonKey(); +} + +std::string HeroClassID::entityType() +{ + return "heroClass"; +} + si32 HeroTypeID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7545d5a2a..7ca9a1333 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -223,6 +223,10 @@ class HeroClassID : public Identifier { public: using Identifier::Identifier; + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); }; class HeroTypeID : public Identifier diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 8167da026..9255fbbb1 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -154,9 +154,9 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); - handler.serializeIdArray("hero", heroes); - handler.serializeIdArray("heroClass", heroClasses); - handler.serializeIdArray("color", players); + handler.serializeIdArray("heroes", heroes); + handler.serializeIdArray("heroClasses", heroClasses); + handler.serializeIdArray("colors", players); handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); handler.enterArray("creatures").serializeStruct(creatures); From 1460541ee53c77ef2696dce5fc2383bd8ef1cf3b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 05:24:40 +0200 Subject: [PATCH 05/28] New limiter based quests --- AI/Nullkiller/Goals/CompleteQuest.cpp | 7 +- AI/VCAI/Goals/CompleteQuest.cpp | 19 +- lib/mapObjects/CQuest.cpp | 300 ++++++++++-------------- lib/mapObjects/CQuest.h | 18 +- lib/mapping/MapFormatH3M.cpp | 34 ++- lib/rewardable/Limiter.cpp | 48 ++++ lib/rewardable/Limiter.h | 7 +- lib/rewardable/Reward.h | 2 +- lib/rmg/modificators/TreasurePlacer.cpp | 6 +- 9 files changed, 205 insertions(+), 236 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index e4d3fda47..00518d51b 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -63,9 +63,6 @@ TGoalVec CompleteQuest::decompose() const return missionLevel(); case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - break; case CQuest::MISSION_KEYMASTER: @@ -137,7 +134,7 @@ TGoalVec CompleteQuest::missionArt() const CaptureObjectsBehavior findArts; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->artifacts) { solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); } @@ -223,7 +220,7 @@ TGoalVec CompleteQuest::missionResources() const TGoalVec CompleteQuest::missionDestroyObj() const { - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return CaptureObjectsBehavior(q.obj).decompose(); diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index f724e308b..fb851bf04 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -54,9 +54,6 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() return missionLevel(); case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - break; case CQuest::MISSION_KEYMASTER: @@ -137,7 +134,7 @@ TGoalVec CompleteQuest::missionArt() const if(!solutions.empty()) return solutions; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->artifacts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } @@ -165,7 +162,7 @@ TGoalVec CompleteQuest::missionArmy() const if(!solutions.empty()) return solutions; - for(auto creature : q.quest->m6creatures) + for(auto creature : q.quest->creatures) { solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); } @@ -179,7 +176,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const if(solutions.empty()) { - for(int i = 0; i < q.quest->m2stats.size(); ++i) + for(int i = 0; i < q.quest->primary.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); @@ -195,7 +192,7 @@ TGoalVec CompleteQuest::missionLevel() const if(solutions.empty()) { - logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); + logAi->debug("Don't know how to reach hero level %d", q.quest->heroLevel); } return solutions; @@ -227,10 +224,10 @@ TGoalVec CompleteQuest::missionResources() const } else { - for(int i = 0; i < q.quest->m7resources.size(); ++i) + for(int i = 0; i < q.quest->resources.size(); ++i) { - if(q.quest->m7resources[i]) - solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->m7resources[i]))); + if(q.quest->resources[i]) + solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->resources[i]))); } } } @@ -246,7 +243,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const { TGoalVec solutions; - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return ai->ah->howToVisitObj(q.obj); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 18ee673f5..e744226b0 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -40,7 +40,7 @@ CQuest::CQuest(): missionType(MISSION_NONE), progress(NOT_ACTIVE), lastDay(-1), - m13489val(0), + killTarget(-1), textOption(0), completedOption(0), stackDirection(0), @@ -99,7 +99,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) ui32 count = 0; ui32 slotsCount = 0; bool hasExtraCreatures = false; - for(cre = q->m6creatures.begin(); cre != q->m6creatures.end(); ++cre) + for(cre = q->creatures.begin(); cre != q->creatures.end(); ++cre) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { @@ -121,110 +121,53 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) bool CQuest::checkQuest(const CGHeroInstance * h) const { - switch (missionType) + if(!heroAllowed(h)) + return false; + + if(killTarget >= 0) { - case MISSION_NONE: - return true; - case MISSION_LEVEL: - return m13489val <= h->level; - case MISSION_PRIMARY_STAT: - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - { - if(h->getPrimSkillLevel(static_cast(i)) < static_cast(m2stats[i])) - return false; - } - return true; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if(!CGHeroInstance::cb->getObjByQuestIdentifier(m13489val)) - return true; - return false; - case MISSION_ART: - { - // if the object was deserialized - if(artifactsRequirements.empty()) - for(const auto & id : m5arts) - ++artifactsRequirements[id]; - - size_t reqSlots = 0; - for(const auto & elem : artifactsRequirements) - { - // check required amount of artifacts - if(h->getArtPosCount(elem.first, false, true, true) < elem.second) - return false; - if(!h->hasArt(elem.first)) - reqSlots += h->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; - } - if(ArtifactUtils::isBackpackFreeSlots(h, reqSlots)) - return true; - else - return false; - } - case MISSION_ARMY: - return checkMissionArmy(this, h); - case MISSION_RESOURCES: - for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) //including Mithril ? - { //Quest has no direct access to callback - if(CGHeroInstance::cb->getResource(h->tempOwner, i) < static_cast(m7resources[i])) - return false; - } - return true; - case MISSION_HERO: - return m13489val == h->type->getIndex(); - case MISSION_PLAYER: - return m13489val == h->getOwner().getNum(); - default: + if(!CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; } + + return true; } void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { - switch (missionType) + for(auto & elem : artifacts) { - case CQuest::MISSION_ART: - for(auto & elem : m5arts) - { - if(h->hasArt(elem)) - { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); - } - else - { - const auto * assembly = h->getAssemblyByConstituent(elem); - assert(assembly); - auto parts = assembly->getPartsInfo(); + if(h->hasArt(elem)) + { + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + } + else + { + const auto * assembly = h->getAssemblyByConstituent(elem); + assert(assembly); + auto parts = assembly->getPartsInfo(); - // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + // Remove the assembly + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); - // Disassemble this backpack artifact - for(const auto & ci : parts) - { - if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); - } - } - } - break; - case CQuest::MISSION_ARMY: - cb->takeCreatures(h->id, m6creatures); - break; - case CQuest::MISSION_RESOURCES: - for (int i = 0; i < 7; ++i) + // Disassemble this backpack artifact + for(const auto & ci : parts) { - cb->giveResource(h->getOwner(), static_cast(i), -static_cast(m7resources[i])); + if(ci.art->getTypeId() != elem) + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); } - break; - default: - break; + } } + + cb->takeCreatures(h->id, creatures); + cb->giveResources(h->getOwner(), resources); } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const { MetaString text; bool failRequirements = (h ? !checkQuest(h) : true); + loadComponents(components, h); if(firstVisit) { @@ -241,20 +184,18 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components switch (missionType) { case MISSION_LEVEL: - components.emplace_back(Component::EComponentType::EXPERIENCE, 0, m13489val, 0); if(!isCustom) - iwText.replaceNumber(m13489val); + iwText.replaceNumber(heroLevel); //TODO: heroLevel break; case MISSION_PRIMARY_STAT: { MetaString loot; for(int i = 0; i < 4; ++i) { - if(m2stats[i]) + if(primary[i]) { - components.emplace_back(Component::EComponentType::PRIM_SKILL, i, m2stats[i], 0); loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -268,10 +209,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components addReplacements(iwText, text.toString()); break; case MISSION_HERO: - //FIXME: portrait may not match hero, if custom portrait was set in map editor - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0); - if(!isCustom) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + if(!isCustom && !heroes.empty()) + iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_KILL_CREATURE: { @@ -285,9 +224,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { - components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); } @@ -298,9 +236,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { - components.emplace_back(elem); loot.appendRawString("%s"); loot.replaceCreatureName(elem); } @@ -313,11 +250,10 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components MetaString loot; for(int i = 0; i < 7; ++i) { - if(m7resources[i]) + if(resources[i]) { - components.emplace_back(Component::EComponentType::RESOURCE, i, m7resources[i], 0); loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -326,9 +262,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components } break; case MISSION_PLAYER: - components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0); - if(!isCustom) - iwText.replaceLocalString(EMetaText::COLOR, m13489val); + if(!isCustom && !players.empty()) + iwText.replaceLocalString(EMetaText::COLOR, players.front()); break; } } @@ -349,17 +284,17 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const switch(missionType) { case MISSION_LEVEL: - ms.replaceNumber(m13489val); + ms.replaceNumber(heroLevel); break; case MISSION_PRIMARY_STAT: { MetaString loot; for (int i = 0; i < 4; ++i) { - if (m2stats[i]) + if (primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -375,7 +310,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const case MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -386,7 +321,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const case MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -399,10 +334,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const MetaString loot; for (int i = 0; i < 7; ++i) { - if (m7resources[i]) + if (resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -410,10 +345,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const } break; case MISSION_HERO: - ms.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + ms.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_PLAYER: - ms.replaceRawString(VLC->generaltexth->colors[m13489val]); + ms.replaceRawString(VLC->generaltexth->colors[players.front()]); break; default: break; @@ -427,18 +362,18 @@ void CQuest::getCompletionText(MetaString &iwText) const { case CQuest::MISSION_LEVEL: if (!isCustomComplete) - iwText.replaceNumber(m13489val); + iwText.replaceNumber(heroLevel); break; case CQuest::MISSION_PRIMARY_STAT: { MetaString loot; - assert(m2stats.size() <= 4); - for (int i = 0; i < m2stats.size(); ++i) + assert(primary.size() <= 4); + for (int i = 0; i < primary.size(); ++i) { - if (m2stats[i]) + if (primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -449,7 +384,7 @@ void CQuest::getCompletionText(MetaString &iwText) const case CQuest::MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -461,7 +396,7 @@ void CQuest::getCompletionText(MetaString &iwText) const case CQuest::MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -475,10 +410,10 @@ void CQuest::getCompletionText(MetaString &iwText) const MetaString loot; for (int i = 0; i < 7; ++i) { - if (m7resources[i]) + if (resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -492,22 +427,16 @@ void CQuest::getCompletionText(MetaString &iwText) const addReplacements(iwText, completedText.toString()); break; case MISSION_HERO: - if (!isCustomComplete) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + if (!isCustomComplete && !heroes.empty()) + iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_PLAYER: - if (!isCustomComplete) - iwText.replaceRawString(VLC->generaltexth->colors[m13489val]); + if (!isCustomComplete && !players.empty()) + iwText.replaceRawString(VLC->generaltexth->colors[players.front()]); break; } } -void CQuest::addArtifactID(const ArtifactID & id) -{ - m5arts.push_back(id); - ++artifactsRequirements[id]; -} - void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { auto q = handler.enterStruct(fieldName); @@ -522,6 +451,8 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi isCustomNext = !nextVisitText.empty(); isCustomComplete = !completedText.empty(); } + + Rewardable::Limiter::serializeJson(handler); static const std::vector MISSION_TYPE_JSON = { @@ -530,57 +461,64 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi handler.serializeEnum("missionType", missionType, Emission::MISSION_NONE, MISSION_TYPE_JSON); handler.serializeInt("timeLimit", lastDay, -1); + handler.serializeInstance("killTarget", killTarget, -1); - switch (missionType) + if(!handler.saving) { - case MISSION_NONE: - break; - case MISSION_LEVEL: - handler.serializeInt("heroLevel", m13489val, -1); - break; - case MISSION_PRIMARY_STAT: + switch (missionType) { - auto primarySkills = handler.enterStruct("primarySkills"); - if(!handler.saving) - m2stats.resize(GameConstants::PRIMARY_SKILLS); - - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], m2stats[i], 0); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - handler.serializeInstance("killTarget", m13489val, static_cast(-1)); - break; - case MISSION_ART: - //todo: ban artifacts - handler.serializeIdArray("artifacts", m5arts); - break; - case MISSION_ARMY: - { - auto a = handler.enterArray("creatures"); - a.serializeStruct(m6creatures); - } - break; - case MISSION_RESOURCES: - { - auto r = handler.enterStruct("resources"); - - for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) + case MISSION_NONE: + break; + case MISSION_LEVEL: + handler.serializeInt("heroLevel", heroLevel, -1); + break; + case MISSION_PRIMARY_STAT: + { + auto primarySkills = handler.enterStruct("primarySkills"); + for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + } + break; + case MISSION_KILL_HERO: + case MISSION_KILL_CREATURE: + break; + case MISSION_ART: + handler.serializeIdArray("artifacts", artifacts); + break; + case MISSION_ARMY: { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], m7resources[idx], 0); + auto a = handler.enterArray("creatures"); + a.serializeStruct(creatures); } + break; + case MISSION_RESOURCES: + { + auto r = handler.enterStruct("resources"); + + for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) + { + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); + } + } + break; + case MISSION_HERO: + { + ui32 temp; + handler.serializeId("hero", temp, 0); + heroes.emplace_back(temp); + } + break; + case MISSION_PLAYER: + { + ui32 temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); + players.emplace_back(temp); + } + break; + default: + logGlobal->error("Invalid quest mission type"); + break; } - break; - case MISSION_HERO: - handler.serializeId("hero", m13489val, 0); - break; - case MISSION_PLAYER: - handler.serializeId("player", m13489val, PlayerColor::NEUTRAL); - break; - default: - logGlobal->error("Invalid quest mission type"); - break; } } @@ -803,7 +741,7 @@ int CGSeerHut::checkDirection() const const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; assert(o && (o->ID == Obj::HERO || o->ID == Obj::PRISON)); @@ -812,7 +750,7 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; assert(o && o->ID == Obj::MONSTER); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 535691b98..3485a51a1 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -17,10 +17,8 @@ VCMI_LIB_NAMESPACE_BEGIN class CGCreature; -class DLL_LINKAGE CQuest final +class DLL_LINKAGE CQuest: public Rewardable::Limiter { - mutable std::unordered_map artifactsRequirements; // artifact ID -> required count - public: enum Emission { MISSION_NONE = 0, @@ -54,12 +52,7 @@ public: Emission missionType; Eprogress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit - - ui32 m13489val; - std::vector m2stats; - std::vector m5arts; // artifact IDs. Add IDs through addArtifactID(), not directly to the field. - std::vector m6creatures; //pair[cre id, cre count], CreatureSet info irrelevant - TResources m7resources; + int killTarget; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info @@ -85,7 +78,6 @@ public: virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; virtual void addReplacements(MetaString &out, const std::string &base) const; - void addArtifactID(const ArtifactID & id); bool operator== (const CQuest & quest) const { @@ -98,11 +90,6 @@ public: h & missionType; h & progress; h & lastDay; - h & m13489val; - h & m2stats; - h & m5arts; - h & m6creatures; - h & m7resources; h & textOption; h & stackToKill; h & stackDirection; @@ -115,6 +102,7 @@ public: h & isCustomNext; h & isCustomComplete; h & completedOption; + h & static_cast(*this); } void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 70646288f..08b3e1545 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1871,7 +1871,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(artID != ArtifactID::NONE) { //not none quest - hut->quest->addArtifactID(artID); + hut->quest->artifacts.push_back(artID); hut->quest->missionType = CQuest::MISSION_ART; } else @@ -1986,10 +1986,9 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) return; case CQuest::MISSION_PRIMARY_STAT: { - guard->quest->m2stats.resize(4); for(int x = 0; x < 4; ++x) { - guard->quest->m2stats[x] = reader->readUInt8(); + guard->quest->primary[x] = reader->readUInt8(); } } break; @@ -1997,7 +1996,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case CQuest::MISSION_KILL_HERO: case CQuest::MISSION_KILL_CREATURE: { - guard->quest->m13489val = reader->readUInt32(); + guard->quest->killTarget = reader->readUInt32(); break; } case CQuest::MISSION_ART: @@ -2006,7 +2005,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) for(int yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); - guard->quest->addArtifactID(artid); + guard->quest->artifacts.push_back(artid); map->allowedArtifact[artid] = false; //these are unavailable for random generation } break; @@ -2014,29 +2013,29 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case CQuest::MISSION_ARMY: { int typeNumber = reader->readUInt8(); - guard->quest->m6creatures.resize(typeNumber); + guard->quest->creatures.resize(typeNumber); for(int hh = 0; hh < typeNumber; ++hh) { - guard->quest->m6creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->m6creatures[hh].count = reader->readUInt16(); + guard->quest->creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->creatures[hh].count = reader->readUInt16(); } break; } case CQuest::MISSION_RESOURCES: { for(int x = 0; x < 7; ++x) - guard->quest->m7resources[x] = reader->readUInt32(); + guard->quest->resources[x] = reader->readUInt32(); break; } case CQuest::MISSION_HERO: { - guard->quest->m13489val = reader->readHero().getNum(); + guard->quest->heroes.push_back(reader->readHero()); break; } case CQuest::MISSION_PLAYER: { - guard->quest->m13489val = reader->readPlayer().getNum(); + guard->quest->players.push_back(reader->readPlayer()); break; } case CQuest::MISSION_HOTA_MULTI: @@ -2045,22 +2044,19 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 0) { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_HERO_CLASS; + guard->quest->missionType = CQuest::MISSION_HOTA_HERO_CLASS; std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); - - logGlobal->warn("Map '%s': Quest at %s 'Belong to one of %d classes' is not implemented!", mapName, position.toString(), heroClasses.size()); + for(auto & hc : heroClasses) + guard->quest->heroClasses.push_back(hc); break; } if(missionSubID == 1) { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_REACH_DATE; - uint32_t daysPassed = reader->readUInt32(); - - logGlobal->warn("Map '%s': Quest at %s 'Wait till %d days passed' is not implemented!", mapName, position.toString(), daysPassed); + guard->quest->missionType = CQuest::MISSION_HOTA_REACH_DATE; + guard->quest->daysPassed = reader->readUInt32(); break; } - assert(0); break; } default: diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 9255fbbb1..e32eeb778 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -146,6 +146,54 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } +void Rewardable::Limiter::loadComponents(std::vector & comps, + const CGHeroInstance * h) const +{ + if (heroExperience) + { + comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); + } + if (heroLevel) + comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + + if (manaPoints || manaPercentage > 0) + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, 0, 0); + + for (size_t i=0; i(i), primary[i], 0); + } + + for(const auto & entry : secondary) + comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + + for(const auto & entry : artifacts) + comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + + for(const auto & entry : spells) + comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + + for(const auto & entry : creatures) + comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + + for(const auto & entry : players) + comps.emplace_back(Component::EComponentType::FLAG, entry, 0, 0); + + //FIXME: portrait may not match hero, if custom portrait was set in map editor + for(const auto & entry : heroes) + comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroTypes()->getById(entry)->getIconIndex(), 0, 0); + + for(const auto & entry : heroClasses) + comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroClasses()->getById(entry)->getIconIndex(), 0, 0); + + for (size_t i=0; i(i), resources[i], 0); + } +} + void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("dayOfWeek", dayOfWeek); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 300e572ae..5a29b0eaa 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CStackBasicDescriptor; +struct Component; namespace Rewardable { @@ -78,9 +79,13 @@ struct DLL_LINKAGE Limiter LimitersList noneOf; Limiter(); - ~Limiter(); + virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; + + /// Generates list of components that describes reward for a specific hero + virtual void loadComponents(std::vector & comps, + const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) { diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 3167a3546..59fd44bcc 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -86,7 +86,7 @@ struct DLL_LINKAGE Reward si32 calculateManaPoints(const CGHeroInstance * h) const; Reward(); - ~Reward(); + virtual ~Reward(); template void serialize(Handler &h, const int version) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index eff834612..f3d44ad8f 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -464,7 +464,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; @@ -515,7 +515,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; @@ -540,7 +540,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; From 3a17eeb3301ec760582a09cb9bfe71aade1dd8e4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 11:42:42 +0200 Subject: [PATCH 06/28] Fix typos in rewards widget --- mapeditor/inspector/rewardswidget.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index cfd900326..40ac8f392 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -347,7 +347,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) if(widget->value()) - vinfo.reward.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + vinfo.limiter.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } } @@ -445,10 +445,10 @@ void RewardsWidget::loadCurrentVisitInfo(int index) ui->lHeroExperience->setValue(vinfo.limiter.heroExperience); ui->lManaPoints->setValue(vinfo.limiter.manaPoints); ui->lManaPercentage->setValue(vinfo.limiter.manaPercentage); - ui->lAttack->setValue(vinfo.reward.primary[0]); - ui->lDefence->setValue(vinfo.reward.primary[1]); - ui->lPower->setValue(vinfo.reward.primary[2]); - ui->lKnowledge->setValue(vinfo.reward.primary[3]); + ui->lAttack->setValue(vinfo.limiter.primary[0]); + ui->lDefence->setValue(vinfo.limiter.primary[1]); + ui->lPower->setValue(vinfo.limiter.primary[2]); + ui->lKnowledge->setValue(vinfo.limiter.primary[3]); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) From 63bdfb8ff622f0e42d256301a098ff73c69e71df Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 15:54:14 +0200 Subject: [PATCH 07/28] New quests implemented --- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/CQuest.h | 1 + mapeditor/inspector/inspector.cpp | 5 +- mapeditor/inspector/questwidget.cpp | 490 ++++++++++++++------ mapeditor/inspector/questwidget.h | 30 +- mapeditor/inspector/questwidget.ui | 626 +++++++++++++++++++++++++- mapeditor/inspector/rewardswidget.cpp | 86 ++++ mapeditor/inspector/rewardswidget.ui | 116 ++++- 8 files changed, 1196 insertions(+), 162 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index e744226b0..98f235e6b 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -46,7 +46,8 @@ CQuest::CQuest(): stackDirection(0), isCustomFirst(false), isCustomNext(false), - isCustomComplete(false) + isCustomComplete(false), + repeatedQuest(false) { } @@ -444,6 +445,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi handler.serializeStruct("firstVisitText", firstVisitText); handler.serializeStruct("nextVisitText", nextVisitText); handler.serializeStruct("completedText", completedText); + handler.serializeBool("repeatedQuest", repeatedQuest, false); if(!handler.saving) { diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3485a51a1..6f3976c07 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -53,6 +53,7 @@ public: Eprogress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit int killTarget; + bool repeatedQuest; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index d6fa5183b..c19eda842 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -410,7 +410,7 @@ void Inspector::updateProperties(CGEvent * o) void Inspector::updateProperties(CGSeerHut * o) { - if(!o) return; + if(!o || !o->quest) return; { //Mission type auto * delegate = new InspectorDelegate; @@ -421,9 +421,10 @@ void Inspector::updateProperties(CGSeerHut * o) addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); + addProperty("Repeat quest", o->quest->repeatedQuest, false); { //Quest - auto * delegate = new QuestDelegate(*controller.map(), *o); + auto * delegate = new QuestDelegate(controller, *o->quest); addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); } } diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index a21392983..0cf1a1315 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -10,21 +10,122 @@ #include "StdInc.h" #include "questwidget.h" #include "ui_questwidget.h" +#include "../mapcontroller.h" #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGCreature.h" -QuestWidget::QuestWidget(const CMap & _map, CGSeerHut & _sh, QWidget *parent) : +QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *parent) : QDialog(parent), - map(_map), - seerhut(_sh), + controller(_controller), + quest(_sh), ui(new Ui::QuestWidget) { + setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); + + ui->lDayOfWeek->addItem(tr("None")); + for(int i = 1; i <= 7; ++i) + ui->lDayOfWeek->addItem(tr("Day %1").arg(i)); + + //fill resources + ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) + { + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + ui->lResources->setItem(i, 0, item); + ui->lResources->setCellWidget(i, 1, new QSpinBox); + } + + //fill artifacts + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!controller.map()->allowedArtifact[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + ui->lArtifacts->addItem(item); + } + + //fill spells + for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!controller.map()->allowedSpells[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + ui->lSpells->addItem(item); + } + + //fill skills + ui->lSkills->setRowCount(controller.map()->allowedAbilities.size()); + for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + { + auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + + auto * widget = new QComboBox; + for(auto & s : NSecondarySkill::levels) + widget->addItem(QString::fromStdString(s)); + + if(!controller.map()->allowedAbilities[i]) + { + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + widget->setEnabled(false); + } + + ui->lSkills->setItem(i, 0, item); + ui->lSkills->setCellWidget(i, 1, widget); + } + + //fill creatures + for(auto & creature : VLC->creh->objects) + { + ui->lCreatureId->addItem(QString::fromStdString(creature->getNameSingularTranslated())); + ui->lCreatureId->setItemData(ui->lCreatureId->count() - 1, creature->getIndex()); + } + + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } } QuestWidget::~QuestWidget() @@ -34,137 +135,259 @@ QuestWidget::~QuestWidget() void QuestWidget::obtainData() { - assert(seerhut.quest); - bool activeId = false; - bool activeAmount = false; - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - activeAmount = true; - ui->targetId->addItem("Reach level"); - ui->targetAmount->setText(QString::number(seerhut.quest->m13489val)); - break; - case CQuest::Emission::MISSION_PRIMARY_STAT: - activeId = true; - activeAmount = true; - for(auto s : NPrimarySkill::names) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m2stats.size(); ++i) - { - if(seerhut.quest->m2stats[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m2stats[i])); - break; //TODO: support multiple stats - } - } - break; - case CQuest::Emission::MISSION_KILL_HERO: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_KILL_CREATURE: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_ART: - activeId = true; - for(int i = 0; i < map.allowedArtifact.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->arth->objects.at(i)->getNameTranslated())); - if(!seerhut.quest->m5arts.empty()) - ui->targetId->setCurrentIndex(seerhut.quest->m5arts.front()); - //TODO: support multiple artifacts - break; - case CQuest::Emission::MISSION_ARMY: - activeId = true; - activeAmount = true; - break; - case CQuest::Emission::MISSION_RESOURCES: - activeId = true; - activeAmount = true; - for(auto s : GameConstants::RESOURCE_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m7resources.size(); ++i) - { - if(seerhut.quest->m7resources[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m7resources[i])); - break; //TODO: support multiple resources - } - } - break; - case CQuest::Emission::MISSION_HERO: - activeId = true; - for(int i = 0; i < map.allowedHeroes.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->heroh->objects.at(i)->getNameTranslated())); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_PLAYER: - activeId = true; - for(auto s : GameConstants::PLAYER_COLOR_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_KEYMASTER: - break; - default: - break; + ui->lDayOfWeek->setCurrentIndex(quest.dayOfWeek); + ui->lDaysPassed->setValue(quest.daysPassed); + ui->lHeroLevel->setValue(quest.heroLevel); + ui->lHeroExperience->setValue(quest.heroExperience); + ui->lManaPoints->setValue(quest.manaPoints); + ui->lManaPercentage->setValue(quest.manaPercentage); + ui->lAttack->setValue(quest.primary[0]); + ui->lDefence->setValue(quest.primary[1]); + ui->lPower->setValue(quest.primary[2]); + ui->lKnowledge->setValue(quest.primary[3]); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + widget->setValue(quest.resources[i]); } - ui->targetId->setEnabled(activeId); - ui->targetAmount->setEnabled(activeAmount); -} - -QString QuestWidget::commitChanges() -{ - assert(seerhut.quest); - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - seerhut.quest->m13489val = ui->targetAmount->text().toInt(); - return QString("Reach lvl ").append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_PRIMARY_STAT: - seerhut.quest->m2stats.resize(sizeof(NPrimarySkill::names), 0); - seerhut.quest->m2stats[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support multiple stats - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_KILL_HERO: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_KILL_CREATURE: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_ART: - seerhut.quest->m5arts.clear(); - seerhut.quest->m5arts.push_back(ArtifactID(ui->targetId->currentIndex())); - //TODO: support multiple artifacts - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_ARMY: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_RESOURCES: - seerhut.quest->m7resources[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support resources - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_HERO: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_PLAYER: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_KEYMASTER: - return QString("N/A"); - default: - return QString("N/A"); + for(auto i : quest.artifacts) + ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : quest.spells) + ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : quest.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); } + for(auto & i : quest.creatures) + { + int index = i.type->getIndex(); + ui->lCreatureId->setCurrentIndex(index); + ui->lCreatureAmount->setValue(i.count); + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); + } + for(auto & i : quest.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.heroClasses) + { + for(int e = 0; e < ui->lHeroClasses->count(); ++e) + { + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + + if(quest.killTarget >= 0 && quest.killTarget < controller.map()->objects.size()) + ui->lKillTarget->setText(QString::fromStdString(controller.map()->objects[quest.killTarget]->instanceName)); + else + quest.killTarget = -1; } -QuestDelegate::QuestDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), QStyledItemDelegate() +bool QuestWidget::commitChanges() +{ + quest.dayOfWeek = ui->lDayOfWeek->currentIndex(); + quest.daysPassed = ui->lDaysPassed->value(); + quest.heroLevel = ui->lHeroLevel->value(); + quest.heroExperience = ui->lHeroExperience->value(); + quest.manaPoints = ui->lManaPoints->value(); + quest.manaPercentage = ui->lManaPercentage->value(); + quest.primary[0] = ui->lAttack->value(); + quest.primary[1] = ui->lDefence->value(); + quest.primary[2] = ui->lPower->value(); + quest.primary[3] = ui->lKnowledge->value(); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + quest.resources[i] = widget->value(); + } + + quest.artifacts.clear(); + for(int i = 0; i < ui->lArtifacts->count(); ++i) + { + if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) + quest.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + quest.spells.clear(); + for(int i = 0; i < ui->lSpells->count(); ++i) + { + if(ui->lSpells->item(i)->checkState() == Qt::Checked) + quest.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + quest.secondary.clear(); + for(int i = 0; i < ui->lSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) + { + if(widget->currentIndex() > 0) + quest.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + quest.creatures.clear(); + for(int i = 0; i < ui->lCreatures->rowCount(); ++i) + { + int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) + if(widget->value()) + quest.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + quest.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + quest.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + quest.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + quest.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + quest.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + quest.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } + + //quest.killTarget is set directly in object picking + + return true; +} + +void QuestWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) +{ + QTableWidgetItem * item = nullptr; + QSpinBox * widget = nullptr; + for(int i = 0; i < listWidget->rowCount(); ++i) + { + if(auto * cname = listWidget->item(i, 0)) + { + if(cname->data(Qt::UserRole).toInt() == comboWidget->currentData().toInt()) + { + item = cname; + widget = qobject_cast(listWidget->cellWidget(i, 1)); + break; + } + } + } + + if(!item) + { + listWidget->setRowCount(listWidget->rowCount() + 1); + item = new QTableWidgetItem(comboWidget->currentText()); + listWidget->setItem(listWidget->rowCount() - 1, 0, item); + } + + item->setData(Qt::UserRole, comboWidget->currentData()); + + if(!widget) + { + widget = new QSpinBox; + widget->setRange(spinWidget->minimum(), spinWidget->maximum()); + listWidget->setCellWidget(listWidget->rowCount() - 1, 1, widget); + } + + widget->setValue(spinWidget->value()); +} + +void QuestWidget::on_lKillTargetSelect_clicked() +{ + auto pred = [](const CGObjectInstance * obj) -> bool + { + if(auto * o = dynamic_cast(obj)) + return o->ID != Obj::PRISON; + if(auto * o = dynamic_cast(obj)) + return true; + return false; + }; + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(pred); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + hide(); +} + +void QuestWidget::onTargetPicked(const CGObjectInstance * obj) +{ + show(); + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + if(!obj) //discarded + { + quest.killTarget = -1; + ui->lKillTarget->setText(""); + return; + } + + ui->lKillTarget->setText(QString::fromStdString(obj->instanceName)); + quest.killTarget = obj->id; +} + +void QuestWidget::on_lCreatureAdd_clicked() +{ + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); +} + + +void QuestWidget::on_lCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->lCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->lCreatures->removeRow(i); +} + +QuestDelegate::QuestDelegate(MapController & c, CQuest & t): controller(c), quest(t), QStyledItemDelegate() { } QWidget * QuestDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const { - return new QuestWidget(map, seerhut, parent); + return new QuestWidget(controller, quest, parent); } void QuestDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const @@ -183,11 +406,26 @@ void QuestDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, c { if(auto *ed = qobject_cast(editor)) { - auto quest = ed->commitChanges(); - model->setData(index, quest); + ed->commitChanges(); } else { QStyledItemDelegate::setModelData(editor, model, index); } } + +bool QuestDelegate::eventFilter(QObject * object, QEvent * event) +{ + if(auto * ed = qobject_cast(object)) + { + if(event->type() == QEvent::Hide || event->type() == QEvent::FocusOut) + return false; + if(event->type() == QEvent::Close) + { + emit commitData(ed); + emit closeEditor(ed); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} diff --git a/mapeditor/inspector/questwidget.h b/mapeditor/inspector/questwidget.h index cef1ad551..3237c0b65 100644 --- a/mapeditor/inspector/questwidget.h +++ b/mapeditor/inspector/questwidget.h @@ -16,20 +16,33 @@ namespace Ui { class QuestWidget; } +class MapController; + class QuestWidget : public QDialog { Q_OBJECT public: - explicit QuestWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr); + explicit QuestWidget(MapController &, CQuest &, QWidget *parent = nullptr); ~QuestWidget(); void obtainData(); - QString commitChanges(); + bool commitChanges(); + +private slots: + void onTargetPicked(const CGObjectInstance *); + + void on_lKillTargetSelect_clicked(); + + void on_lCreatureAdd_clicked(); + + void on_lCreatureRemove_clicked(); private: - CGSeerHut & seerhut; - const CMap & map; + void onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget); + + CQuest & quest; + MapController & controller; Ui::QuestWidget *ui; }; @@ -39,13 +52,16 @@ class QuestDelegate : public QStyledItemDelegate public: using QStyledItemDelegate::QStyledItemDelegate; - QuestDelegate(const CMap &, CGSeerHut &); + QuestDelegate(MapController &, CQuest &); QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; void setEditorData(QWidget * editor, const QModelIndex & index) const override; void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; +protected: + bool eventFilter(QObject * object, QEvent * event) override; + private: - CGSeerHut & seerhut; - const CMap & map; + CQuest & quest; + MapController & controller; }; diff --git a/mapeditor/inspector/questwidget.ui b/mapeditor/inspector/questwidget.ui index c5f2931b8..f08b3bf94 100644 --- a/mapeditor/inspector/questwidget.ui +++ b/mapeditor/inspector/questwidget.ui @@ -9,8 +9,8 @@ 0 0 - 429 - 89 + 531 + 495 @@ -19,34 +19,616 @@ true - + - - - - 0 - 0 - + + + + + Day of week + + + + + + + + 120 + 0 + + + + + + + + Days passed + + + + + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + 999 + + + + + + + % + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Kill hero/monster + + + + + + + true + + + + + + + ... + + + + + + + + + Primary skills + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + + + + Defence + + + + + + + + + + Spell power + + + + + + + + + + Knowledge + + + + + + + - - - - 0 - 0 - + + + Qt::LeftToRight - - - 60 - 16777215 - + + QTabWidget::North - - Qt::ImhDigitsOnly + + QTabWidget::Rounded + + 0 + + + Qt::ElideNone + + + true + + + false + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 40ac8f392..a509f4a24 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -13,6 +13,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" +#include "../lib/CHeroHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/constants/StringConstants.h" @@ -131,6 +132,36 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : } } + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } + //fill spell cast for(auto & s : NSecondarySkill::levels) ui->castLevel->addItem(QString::fromStdString(s)); @@ -349,6 +380,27 @@ void RewardsWidget::saveCurrentVisitInfo(int index) if(widget->value()) vinfo.limiter.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } + + vinfo.limiter.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + vinfo.limiter.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } } void RewardsWidget::loadCurrentVisitInfo(int index) @@ -472,6 +524,40 @@ void RewardsWidget::loadCurrentVisitInfo(int index) ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); } + + for(auto & i : vinfo.limiter.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : vinfo.limiter.heroClasses) + { + for(int e = 0; e < ui->lHeroClasses->count(); ++e) + { + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : vinfo.limiter.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } + } + } } void RewardsWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index c3681e258..2ae3c6676 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -487,6 +487,12 @@ 0 + + Qt::ElideNone + + + true + true @@ -843,7 +849,7 @@ false - 21 + 24 @@ -1185,6 +1191,12 @@ 0 + + Qt::ElideNone + + + true + true @@ -1229,7 +1241,7 @@ false - 21 + 24 @@ -1333,7 +1345,7 @@ false - 21 + 24 @@ -1429,7 +1441,7 @@ false - 21 + 24 @@ -1437,6 +1449,102 @@ + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + From bb238f9b72cf7e39451d0c239dd21a39aeb31b9b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 19:15:34 +0200 Subject: [PATCH 08/28] New quests work --- AI/Nullkiller/Goals/CompleteQuest.cpp | 33 +- .../Rules/AIMovementAfterDestinationRule.cpp | 2 +- AI/VCAI/Goals/CompleteQuest.cpp | 33 +- client/windows/CQuestLog.cpp | 16 +- lib/CGeneralTextHandler.cpp | 2 +- lib/mapObjects/CQuest.cpp | 567 ++++++------------ lib/mapObjects/CQuest.h | 40 +- lib/mapping/MapFormatH3M.cpp | 67 ++- lib/mapping/MapFormatH3M.h | 2 +- lib/rewardable/Limiter.cpp | 2 +- lib/rmg/modificators/TreasurePlacer.cpp | 12 +- mapeditor/inspector/inspector.cpp | 40 -- mapeditor/inspector/inspector.h | 1 - 13 files changed, 294 insertions(+), 523 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 00518d51b..96b9d317a 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -37,39 +37,32 @@ TGoalVec CompleteQuest::decompose() const } logAi->debug("Trying to realize quest: %s", questToString()); - - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: + + if(!q.quest->artifacts.empty()) return missionArt(); - case CQuest::MISSION_HERO: + if(!q.quest->heroes.empty()) return missionHero(); - case CQuest::MISSION_ARMY: + if(!q.quest->creatures.empty()) return missionArmy(); - case CQuest::MISSION_RESOURCES: + if(q.quest->resources.nonZero()) return missionResources(); - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + if(q.quest->killTarget >= 0) return missionDestroyObj(); - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); + for(auto & s : q.quest->primary) + if(s) + return missionIncreasePrimaryStat(); - case CQuest::MISSION_LEVEL: + if(q.quest->heroLevel > 0) return missionLevel(); - - case CQuest::MISSION_PLAYER: - break; - - case CQuest::MISSION_KEYMASTER: + + if(q.quest->questName == CQuest::missionName(10)) return missionKeymaster(); - } //end of switch - return TGoalVec(); } @@ -104,7 +97,7 @@ std::string CompleteQuest::questToString() const return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent"; } - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index dafcb4758..68638efc5 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,7 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE) + if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->questName == CQuest::missionName(0)) { return false; } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index fb851bf04..d915efd62 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -25,41 +25,34 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; - if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) + if(q.quest->progress != CQuest::COMPLETE) { logAi->debug("Trying to realize quest: %s", questToString()); - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: + if(!q.quest->artifacts.empty()) return missionArt(); - case CQuest::MISSION_HERO: + if(!q.quest->heroes.empty()) return missionHero(); - case CQuest::MISSION_ARMY: + if(!q.quest->creatures.empty()) return missionArmy(); - case CQuest::MISSION_RESOURCES: + if(q.quest->resources.nonZero()) return missionResources(); - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + if(q.quest->killTarget >= 0) return missionDestroyObj(); - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); + for(auto & s : q.quest->primary) + if(s) + return missionIncreasePrimaryStat(); - case CQuest::MISSION_LEVEL: + if(q.quest->heroLevel > 0) return missionLevel(); - - case CQuest::MISSION_PLAYER: - break; - case CQuest::MISSION_KEYMASTER: + if(q.quest->questName == CQuest::missionName(10)) return missionKeymaster(); - - } //end of switch } return TGoalVec(); @@ -67,7 +60,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() TSubgoal CompleteQuest::whatToDoToAchieve() { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) { throw cannotFulfillGoalException("Can not complete inactive quest"); } @@ -101,7 +94,7 @@ std::string CompleteQuest::completeMessage() const std::string CompleteQuest::questToString() const { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index de1f9878e..208a74744 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -149,7 +149,7 @@ void CQuestLog::recreateLabelList() for (int i = 0; i < quests.size(); ++i) { // Quests with MISSION_NONE type don't have text for them and can't be displayed - if (quests[i].quest->missionType == CQuest::MISSION_NONE) + if (quests[i].quest->questName == CQuest::missionName(0)) continue; if (quests[i].quest->progress == CQuest::COMPLETE) @@ -236,7 +236,7 @@ void CQuestLog::selectQuest(int which, int labelId) MetaString text; std::vector components; - currentQuest->quest->getVisitText (text, components, currentQuest->quest->isCustomFirst, true); + currentQuest->quest->getVisitText(text, components, true); if(description->slider) description->slider->scrollToMin(); // scroll text to start position description->setText(text.toString()); //TODO: use special log entry text @@ -247,9 +247,15 @@ void CQuestLog::selectQuest(int which, int labelId) int descriptionHeight = DESCRIPTION_HEIGHT_MAX; if(componentsSize) { - descriptionHeight -= 15; CComponent::ESize imageSize = CComponent::large; - switch (currentQuest->quest->missionType) + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 155; + } + else + descriptionHeight -= 130; + /*switch (currentQuest->quest->missionType) { case CQuest::MISSION_ARMY: { @@ -285,7 +291,7 @@ void CQuestLog::selectQuest(int which, int labelId) default: descriptionHeight -= 115; break; - } + }*/ OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 09d046ef7..657fb564e 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -547,7 +547,7 @@ CGeneralTextHandler::CGeneralTextHandler(): for (size_t i = 0; i < 9; ++i) //9 types of quests { - std::string questName = CQuest::missionName(static_cast(1+i)); + std::string questName = CQuest::missionName(1+i); for (size_t j = 0; j < 5; ++j) { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 98f235e6b..d704c8850 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -37,7 +37,6 @@ std::map > CGKeys::playerKeyMap; //TODO: Remove constructor CQuest::CQuest(): qid(-1), - missionType(MISSION_NONE), progress(NOT_ACTIVE), lastDay(-1), killTarget(-1), @@ -47,7 +46,8 @@ CQuest::CQuest(): isCustomFirst(false), isCustomNext(false), isCustomComplete(false), - repeatedQuest(false) + repeatedQuest(false), + questName(CQuest::missionName(0)) { } @@ -57,7 +57,7 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -const std::string & CQuest::missionName(CQuest::Emission mission) +const std::string & CQuest::missionName(int mission) { static const std::array names = { "empty", @@ -164,277 +164,137 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const cb->giveResources(h->getOwner(), resources); } -void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const +void CQuest::addTextReplacements(MetaString & text) const +{ + if(heroLevel > 0) + text.replaceNumber(heroLevel); + + { //primary skills + MetaString loot; + for(int i = 0; i < 4; ++i) + { + if(primary[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(primary[i]); + loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); + } + } + if(!loot.empty()) + text.replaceRawString(loot.buildList()); + } + + if(killTarget >= 0 && !heroName.empty()) + { + //components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + addKillTargetReplacements(text); + } + + if(killTarget >= 0 && stackToKill.type) + { + //components.emplace_back(stackToKill); + addKillTargetReplacements(text); + } + + if(!heroes.empty()) + text.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); + + if(!artifacts.empty()) + { + MetaString loot; + for(const auto & elem : artifacts) + { + loot.appendRawString("%s"); + loot.replaceLocalString(EMetaText::ART_NAMES, elem); + } + text.replaceRawString(loot.buildList()); + } + + if(!creatures.empty()) + { + MetaString loot; + for(const auto & elem : creatures) + { + loot.appendRawString("%s"); + loot.replaceCreatureName(elem); + } + text.replaceRawString(loot.buildList()); + } + + if(resources.nonZero()) + { + MetaString loot; + for(int i = 0; i < 7; ++i) + { + if(resources[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(resources[i]); + loot.replaceLocalString(EMetaText::RES_NAMES, i); + } + } + text.replaceRawString(loot.buildList()); + } + + if(!players.empty()) + text.replaceLocalString(EMetaText::COLOR, players.front()); +} + +void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { - MetaString text; bool failRequirements = (h ? !checkQuest(h) : true); loadComponents(components, h); if(firstVisit) - { - isCustom = isCustomFirst; - text = firstVisitText; - iwText.appendRawString(text.toString()); - } + iwText.appendRawString(firstVisitText.toString()); else if(failRequirements) - { - isCustom = isCustomNext; - text = nextVisitText; - iwText.appendRawString(text.toString()); - } - switch (missionType) - { - case MISSION_LEVEL: - if(!isCustom) - iwText.replaceNumber(heroLevel); //TODO: heroLevel - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for(int i = 0; i < 4; ++i) - { - if(primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); - if(!isCustom) - addReplacements(iwText, text.toString()); - break; - case MISSION_HERO: - if(!isCustom && !heroes.empty()) - iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_KILL_CREATURE: - { - components.emplace_back(stackToKill); - if(!isCustom) - { - addReplacements(iwText, text.toString()); - } - } - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for(int i = 0; i < 7; ++i) - { - if(resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_PLAYER: - if(!isCustom && !players.empty()) - iwText.replaceLocalString(EMetaText::COLOR, players.front()); - break; - } + iwText.appendRawString(nextVisitText.toString()); + + addTextReplacements(iwText); } void CQuest::getRolloverText(MetaString &ms, bool onHover) const { - // Quests with MISSION_NONE type don't have a text for them - assert(missionType != MISSION_NONE); - if(onHover) ms.appendRawString("\n\n"); - std::string questName = missionName(missionType); std::string questState = missionState(onHover ? 3 : 4); - ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption)); + ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); - switch(missionType) - { - case MISSION_LEVEL: - ms.replaceNumber(heroLevel); - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for (int i = 0; i < 4; ++i) - { - if (primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - ms.replaceRawString(heroName); - break; - case MISSION_KILL_CREATURE: - ms.replaceCreatureName(stackToKill); - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_HERO: - ms.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_PLAYER: - ms.replaceRawString(VLC->generaltexth->colors[players.front()]); - break; - default: - break; - } + addTextReplacements(ms); } void CQuest::getCompletionText(MetaString &iwText) const { iwText.appendRawString(completedText.toString()); - switch(missionType) + + addTextReplacements(iwText); +} + +void CQuest::defineQuestName() +{ + //standard quests + questName = CQuest::missionName(0); + if(heroLevel > 0) questName = CQuest::missionName(1); + for(auto & s : primary) if(s) questName = CQuest::missionName(2); + if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); + if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); + if(!artifacts.empty()) questName = CQuest::missionName(5); + if(!creatures.empty()) questName = CQuest::missionName(6); + if(resources.nonZero()) questName = CQuest::missionName(7); + if(!heroes.empty()) questName = CQuest::missionName(8); + if(!players.empty()) questName = CQuest::missionName(9); +} + +void CQuest::addKillTargetReplacements(MetaString &out) const +{ + if(!heroName.empty()) + out.replaceTextID(heroName); + if(stackToKill.type) { - case CQuest::MISSION_LEVEL: - if (!isCustomComplete) - iwText.replaceNumber(heroLevel); - break; - case CQuest::MISSION_PRIMARY_STAT: - { - MetaString loot; - assert(primary.size() <= 4); - for (int i = 0; i < primary.size(); ++i) - { - if (primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - break; - } - case CQuest::MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if (!isCustomComplete) - addReplacements(iwText, completedText.toString()); - break; - case MISSION_HERO: - if (!isCustomComplete && !heroes.empty()) - iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_PLAYER: - if (!isCustomComplete && !players.empty()) - iwText.replaceRawString(VLC->generaltexth->colors[players.front()]); - break; + out.replaceCreatureName(stackToKill); + out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); } } @@ -456,85 +316,87 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi Rewardable::Limiter::serializeJson(handler); - static const std::vector MISSION_TYPE_JSON = - { - "None", "Level", "PrimaryStat", "KillHero", "KillCreature", "Artifact", "Army", "Resources", "Hero", "Player" - }; - - handler.serializeEnum("missionType", missionType, Emission::MISSION_NONE, MISSION_TYPE_JSON); handler.serializeInt("timeLimit", lastDay, -1); handler.serializeInstance("killTarget", killTarget, -1); - if(!handler.saving) + if(!handler.saving) //compatibility with legacy vmaps { - switch (missionType) + std::string missionType = "None"; + handler.serializeString("missionType", missionType); + if(missionType == "None") + return; + + if(missionType == "Level") + handler.serializeInt("heroLevel", heroLevel, -1); + + if(missionType == "PrimaryStat") { - case MISSION_NONE: - break; - case MISSION_LEVEL: - handler.serializeInt("heroLevel", heroLevel, -1); - break; - case MISSION_PRIMARY_STAT: - { - auto primarySkills = handler.enterStruct("primarySkills"); - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - break; - case MISSION_ART: - handler.serializeIdArray("artifacts", artifacts); - break; - case MISSION_ARMY: - { - auto a = handler.enterArray("creatures"); - a.serializeStruct(creatures); - } - break; - case MISSION_RESOURCES: - { - auto r = handler.enterStruct("resources"); + auto primarySkills = handler.enterStruct("primarySkills"); + for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + } + + if(missionType == "Artifact") + handler.serializeIdArray("artifacts", artifacts); + + if(missionType == "Army") + { + auto a = handler.enterArray("creatures"); + a.serializeStruct(creatures); + } + + if(missionType == "Resources") + { + auto r = handler.enterStruct("resources"); - for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) - { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); - } - } - break; - case MISSION_HERO: + for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) { - ui32 temp; - handler.serializeId("hero", temp, 0); - heroes.emplace_back(temp); + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); } - break; - case MISSION_PLAYER: - { - ui32 temp; - handler.serializeId("player", temp, PlayerColor::NEUTRAL); - players.emplace_back(temp); - } - break; - default: - logGlobal->error("Invalid quest mission type"); - break; + } + + if(missionType == "Hero") + { + ui32 temp; + handler.serializeId("hero", temp, 0); + heroes.emplace_back(temp); + } + + if(missionType == "Player") + { + ui32 temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); + players.emplace_back(temp); } } } +bool IQuestObject::checkQuest(const CGHeroInstance* h) const +{ + return quest->checkQuest(h); +} + +void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const +{ + quest->getVisitText(text,components, FirstVisit, h); +} + +void IQuestObject::afterAddToMapCommon(CMap * map) const +{ + map->addNewQuestInstance(quest); +} + void CGSeerHut::setObjToKill() { - if(quest->missionType == CQuest::MISSION_KILL_CREATURE) + if(getCreatureToKill(true)) { quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? assert(quest->stackToKill.type); quest->stackToKill.count = 0; //no count in info window quest->stackDirection = checkDirection(); } - else if(quest->missionType == CQuest::MISSION_KILL_HERO) + else if(getHeroToKill(true)) { quest->heroName = getHeroToKill(false)->getNameTranslated(); quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); @@ -566,27 +428,28 @@ void CGSeerHut::initObj(CRandomGenerator & rand) CRewardableObject::initObj(rand); quest->progress = CQuest::NOT_ACTIVE; - if(quest->missionType) - { - std::string questName = quest->missionName(quest->missionType); - - if(!quest->isCustomFirst) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get()); - if(!quest->isCustomNext) - quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); - if(!quest->isCustomComplete) - quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); - } - else + + quest->defineQuestName(); + + if(quest->questName == quest->missionName(0)) { quest->progress = CQuest::COMPLETE; quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } + else + { + if(!quest->isCustomFirst) + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(0), quest->textOption).get()); + if(!quest->isCustomNext) + quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(1), quest->textOption).get()); + if(!quest->isCustomComplete) + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(2), quest->textOption).get()); + } } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const { - quest->getRolloverText (text, onHover);//TODO: simplify? + quest->getRolloverText(text, onHover);//TODO: simplify? if(!onHover) text.replaceRawString(seerName); } @@ -600,7 +463,7 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const boost::algorithm::replace_first(hoverName, "%s", seerName); } - if(quest->progress & quest->missionType) //rollover when the quest is active + if(quest->progress/* & quest->missionType*/) //rollover when the quest is active { MetaString ms; getRolloverText (ms, true); @@ -609,47 +472,12 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CQuest::addReplacements(MetaString &out, const std::string &base) const -{ - switch(missionType) - { - case MISSION_KILL_CREATURE: - if(stackToKill.type) - { - out.replaceCreatureName(stackToKill); - if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster - { - out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); - } - } - break; - case MISSION_KILL_HERO: - out.replaceTextID(heroName); - break; - } -} - -bool IQuestObject::checkQuest(const CGHeroInstance* h) const -{ - return quest->checkQuest(h); -} - -void IQuestObject::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const -{ - quest->getVisitText (text,components, isCustom, FirstVisit, h); -} - -void IQuestObject::afterAddToMapCommon(CMap * map) const -{ - map->addNewQuestInstance(quest); -} - void CGSeerHut::setPropertyDer (ui8 what, ui32 val) { switch(what) { case 10: - quest->progress = static_cast(val); + quest->progress = static_cast(val); break; } } @@ -671,11 +499,9 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const { bool firstVisit = !quest->progress; bool failRequirements = !checkQuest(h); - bool isCustom = false; if(firstVisit) { - isCustom = quest->isCustomFirst; cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::IN_PROGRESS); AddQuest aq; @@ -683,14 +509,10 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const aq.player = h->tempOwner; cb->sendAndApply(&aq); //TODO: merge with setObjProperty? } - else if(failRequirements) - { - isCustom = quest->isCustomNext; - } if(firstVisit || failRequirements) { - getVisitText (iw.text, iw.components, isCustom, firstVisit, h); + getVisitText (iw.text, iw.components, firstVisit, h); cb->showInfoDialog(&iw); } @@ -765,7 +587,10 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + if(quest && quest->repeatedQuest) + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::NOT_ACTIVE); + else + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete } } @@ -919,12 +744,12 @@ void CGBorderGuard::initObj(CRandomGenerator & rand) blockVisit = true; } -void CGBorderGuard::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const +void CGBorderGuard::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - text.appendLocalString(EMetaText::ADVOB_TXT,18); + text.appendLocalString(EMetaText::ADVOB_TXT, 18); } -void CGBorderGuard::getRolloverText (MetaString &text, bool onHover) const +void CGBorderGuard::getRolloverText(MetaString &text, bool onHover) const { if (!onHover) { diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 6f3976c07..b524c02fa 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -20,37 +20,21 @@ class CGCreature; class DLL_LINKAGE CQuest: public Rewardable::Limiter { public: - enum Emission { - MISSION_NONE = 0, - MISSION_LEVEL = 1, - MISSION_PRIMARY_STAT = 2, - MISSION_KILL_HERO = 3, - MISSION_KILL_CREATURE = 4, - MISSION_ART = 5, - MISSION_ARMY = 6, - MISSION_RESOURCES = 7, - MISSION_HERO = 8, - MISSION_PLAYER = 9, - MISSION_HOTA_MULTI = 10, - // end of H3 missions - MISSION_KEYMASTER = 100, - MISSION_HOTA_HERO_CLASS = 101, - MISSION_HOTA_REACH_DATE = 102 - }; - enum Eprogress { + enum EProgress { NOT_ACTIVE, IN_PROGRESS, COMPLETE }; - static const std::string & missionName(Emission mission); - static const std::string & missionState(int index); + static const std::string & missionName(int index); + static const std::string & missionState(int index); + + std::string questName; si32 qid; //unique quest id for serialization / identification - Emission missionType; - Eprogress progress; + EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit int killTarget; bool repeatedQuest; @@ -74,11 +58,13 @@ public: static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); virtual bool checkQuest (const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addReplacements(MetaString &out, const std::string &base) const; + virtual void addTextReplacements(MetaString &out) const; + virtual void addKillTargetReplacements(MetaString &out) const; + void defineQuestName(); bool operator== (const CQuest & quest) const { @@ -88,7 +74,6 @@ public: template void serialize(Handler &h, const int version) { h & qid; - h & missionType; h & progress; h & lastDay; h & textOption; @@ -103,6 +88,7 @@ public: h & isCustomNext; h & isCustomComplete; h & completedOption; + h & questName; h & static_cast(*this); } @@ -117,7 +103,7 @@ public: ///Information about quest should remain accessible even if IQuestObject removed from map ///All CQuest objects are freed in CMap destructor virtual ~IQuestObject() = default; - virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual bool checkQuest (const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) @@ -215,7 +201,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; + void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; void getRolloverText (MetaString &text, bool onHover) const; bool checkQuest (const CGHeroInstance * h) const override; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 08b3e1545..97a2d6820 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1858,11 +1858,30 @@ enum class ESeerHutRewardType : uint8_t CREATURE = 10, }; +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) { + EQuestMission missionType = EQuestMission::NONE; if(features.levelAB) { - readQuest(hut, position); + missionType = static_cast(readQuest(hut, position)); } else { @@ -1872,11 +1891,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { //not none quest hut->quest->artifacts.push_back(artID); - hut->quest->missionType = CQuest::MISSION_ART; - } - else - { - hut->quest->missionType = CQuest::MISSION_NONE; + missionType = EQuestMission::ARTIFACT; } hut->quest->lastDay = -1; //no timeout hut->quest->isCustomFirst = false; @@ -1884,7 +1899,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con hut->quest->isCustomComplete = false; } - if(hut->quest->missionType) + if(missionType != EQuestMission::NONE) { auto rewardType = static_cast(reader->readUInt8()); Rewardable::VisitInfo vinfo; @@ -1976,15 +1991,15 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } } -void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { - guard->quest->missionType = static_cast(reader->readUInt8()); + auto missionId = reader->readUInt8(); - switch(guard->quest->missionType) + switch(static_cast(missionId)) { - case CQuest::MISSION_NONE: - return; - case CQuest::MISSION_PRIMARY_STAT: + case EQuestMission::NONE: + return missionId; + case EQuestMission::PRIMARY_SKILL: { for(int x = 0; x < 4; ++x) { @@ -1992,14 +2007,17 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } } break; - case CQuest::MISSION_LEVEL: - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + case EQuestMission::LEVEL: + { + guard->quest->heroLevel = reader->readUInt32(); + } + case EQuestMission::KILL_HERO: + case EQuestMission::KILL_CREATURE: { guard->quest->killTarget = reader->readUInt32(); break; } - case CQuest::MISSION_ART: + case EQuestMission::ARTIFACT: { int artNumber = reader->readUInt8(); for(int yy = 0; yy < artNumber; ++yy) @@ -2010,7 +2028,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } break; } - case CQuest::MISSION_ARMY: + case EQuestMission::ARMY: { int typeNumber = reader->readUInt8(); guard->quest->creatures.resize(typeNumber); @@ -2021,30 +2039,30 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } break; } - case CQuest::MISSION_RESOURCES: + case EQuestMission::RESOURCES: { for(int x = 0; x < 7; ++x) guard->quest->resources[x] = reader->readUInt32(); break; } - case CQuest::MISSION_HERO: + case EQuestMission::HERO: { guard->quest->heroes.push_back(reader->readHero()); break; } - case CQuest::MISSION_PLAYER: + case EQuestMission::PLAYER: { guard->quest->players.push_back(reader->readPlayer()); break; } - case CQuest::MISSION_HOTA_MULTI: + case EQuestMission::HOTA_MULTI: { uint32_t missionSubID = reader->readUInt32(); if(missionSubID == 0) { - guard->quest->missionType = CQuest::MISSION_HOTA_HERO_CLASS; + missionId = int(EQuestMission::HOTA_HERO_CLASS); std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) @@ -2053,7 +2071,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } if(missionSubID == 1) { - guard->quest->missionType = CQuest::MISSION_HOTA_REACH_DATE; + missionId = int(EQuestMission::HOTA_REACH_DATE); guard->quest->daysPassed = reader->readUInt32(); break; } @@ -2072,6 +2090,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); guard->quest->isCustomComplete = !guard->quest->completedText.empty(); + return missionId; } CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 6a0180b54..3eb7a3226 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -204,7 +204,7 @@ private: * * @param guard the quest guard where that quest should be applied to */ - void readQuest(IQuestObject * guard, const int3 & position); + int readQuest(IQuestObject * guard, const int3 & position); void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index e32eeb778..6c82eeb2e 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -26,7 +26,7 @@ Rewardable::Limiter::Limiter() : dayOfWeek(0) , daysPassed(0) , heroExperience(0) - , heroLevel(0) + , heroLevel(-1) , manaPercentage(0) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index f3d44ad8f..542c8294e 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -460,13 +460,9 @@ void TreasurePlacer::addAllPossibleObjects() reward.reward.creatures.emplace_back(creature->getId(), creaturesAmount); reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - - obj->quest->missionType = CQuest::MISSION_ART; - + ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -513,11 +509,8 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -538,11 +531,8 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index c19eda842..903cfaf22 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -29,20 +29,6 @@ #include "PickObjectDelegate.h" #include "../mapcontroller.h" -static QList> MissionIdentifiers -{ - {QObject::tr("None"), QVariant::fromValue(int(CQuest::Emission::MISSION_NONE))}, - {QObject::tr("Reach level"), QVariant::fromValue(int(CQuest::Emission::MISSION_LEVEL))}, - {QObject::tr("Stats"), QVariant::fromValue(int(CQuest::Emission::MISSION_PRIMARY_STAT))}, - {QObject::tr("Kill hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_HERO))}, - {QObject::tr("Kill monster"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_CREATURE))}, - {QObject::tr("Artifact"), QVariant::fromValue(int(CQuest::Emission::MISSION_ART))}, - {QObject::tr("Army"), QVariant::fromValue(int(CQuest::Emission::MISSION_ARMY))}, - {QObject::tr("Resources"), QVariant::fromValue(int(CQuest::Emission::MISSION_RESOURCES))}, - {QObject::tr("Hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_HERO))}, - {QObject::tr("Player"), QVariant::fromValue(int(CQuest::Emission::MISSION_PLAYER))}, -}; - static QList> CharacterIdentifiers { {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, @@ -411,12 +397,6 @@ void Inspector::updateProperties(CGEvent * o) void Inspector::updateProperties(CGSeerHut * o) { if(!o || !o->quest) return; - - { //Mission type - auto * delegate = new InspectorDelegate; - delegate->options = MissionIdentifiers; - addProperty("Mission type", o->quest->missionType, delegate, false); - } addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); @@ -678,8 +658,6 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & { if(!o) return; - if(key == "Mission type") - o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") @@ -798,24 +776,6 @@ QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) return item; } -QTableWidgetItem * Inspector::addProperty(CQuest::Emission value) -{ - auto * item = new QTableWidgetItem; - item->setFlags(Qt::NoItemFlags); - item->setData(Qt::UserRole, QVariant::fromValue(int(value))); - - for(auto & i : MissionIdentifiers) - { - if(i.second.toInt() == value) - { - item->setText(i.first); - break; - } - } - - return item; -} - //======================================================================== Inspector::Inspector(MapController & c, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), controller(c) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 69633a3db..5e2e1f96a 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -96,7 +96,6 @@ protected: QTableWidgetItem * addProperty(bool value); QTableWidgetItem * addProperty(CGObjectInstance * value); QTableWidgetItem * addProperty(CGCreature::Character value); - QTableWidgetItem * addProperty(CQuest::Emission value); QTableWidgetItem * addProperty(PropertyEditorPlaceholder value); //===============END OF DECLARATION======================================= From d2d64dbddda7c2f92d088932cf1abde49d0ae17d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 01:20:55 +0200 Subject: [PATCH 09/28] Bugfixes --- lib/mapObjects/CQuest.cpp | 24 +++++++++-------- lib/mapObjects/CQuest.h | 6 ++--- lib/rewardable/Limiter.cpp | 39 ++++++++++++++++++++++++--- lib/rewardable/Limiter.h | 2 +- mapeditor/inspector/inspector.cpp | 32 +++++++++++++++------- mapeditor/inspector/questwidget.cpp | 4 ++- mapeditor/inspector/rewardswidget.cpp | 6 ++++- 7 files changed, 83 insertions(+), 30 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index d704c8850..1ab0e85cf 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -127,7 +127,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const if(killTarget >= 0) { - if(!CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) + if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; } @@ -164,7 +164,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const cb->giveResources(h->getOwner(), resources); } -void CQuest::addTextReplacements(MetaString & text) const +void CQuest::addTextReplacements(MetaString & text, std::vector & components) const { if(heroLevel > 0) text.replaceNumber(heroLevel); @@ -186,13 +186,13 @@ void CQuest::addTextReplacements(MetaString & text) const if(killTarget >= 0 && !heroName.empty()) { - //components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); addKillTargetReplacements(text); } if(killTarget >= 0 && stackToKill.type) { - //components.emplace_back(stackToKill); + components.emplace_back(stackToKill); addKillTargetReplacements(text); } @@ -250,7 +250,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components else if(failRequirements) iwText.appendRawString(nextVisitText.toString()); - addTextReplacements(iwText); + addTextReplacements(iwText, components); } void CQuest::getRolloverText(MetaString &ms, bool onHover) const @@ -262,14 +262,16 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); - addTextReplacements(ms); + std::vector components; + addTextReplacements(ms, components); } void CQuest::getCompletionText(MetaString &iwText) const { iwText.appendRawString(completedText.toString()); - addTextReplacements(iwText); + std::vector components; + addTextReplacements(iwText, components); } void CQuest::defineQuestName() @@ -379,7 +381,7 @@ bool IQuestObject::checkQuest(const CGHeroInstance* h) const void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - quest->getVisitText(text,components, FirstVisit, h); + quest->getVisitText(text, components, FirstVisit, h); } void IQuestObject::afterAddToMapCommon(CMap * map) const @@ -431,9 +433,11 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->defineQuestName(); + if(quest->empty() && quest->killTarget == -1) + quest->progress = CQuest::COMPLETE; + if(quest->questName == quest->missionName(0)) { - quest->progress = CQuest::COMPLETE; quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } else @@ -568,7 +572,6 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && (o->ID == Obj::HERO || o->ID == Obj::PRISON)); return dynamic_cast(o); } @@ -577,7 +580,6 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && o->ID == Obj::MONSTER); return dynamic_cast(o); } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index b524c02fa..6a6383fe3 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -57,12 +57,12 @@ public: CQuest(); //TODO: Remove constructor static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); - virtual bool checkQuest (const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not + virtual void getVisitText(MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addTextReplacements(MetaString &out) const; + virtual void addTextReplacements(MetaString &out, std::vector & components) const; virtual void addKillTargetReplacements(MetaString &out) const; void defineQuestName(); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 6c82eeb2e..8843d4e39 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -35,6 +35,37 @@ Rewardable::Limiter::Limiter() Rewardable::Limiter::~Limiter() = default; +bool Rewardable::Limiter::empty() const +{ + if(dayOfWeek != 0 + || daysPassed != 0 + || heroLevel > 0 + || heroExperience > 0 + || manaPoints > 0 + || manaPercentage > 0 + || !secondary.empty() + || !creatures.empty() + || !spells.empty() + || !artifacts.empty() + || !players.empty() + || !heroes.empty() + || !heroClasses.empty() + || resources.nonZero() + || std::find_if(primary.begin(), primary.end(), [](si32 i){return i != 0;}) != primary.end()) + return false; + + for(const auto & sub : {noneOf, allOf, anyOf}) + { + for(const auto & sublimiter : sub) + { + if(!sublimiter->empty()) + return false; + } + } + + return true; +} + bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) @@ -150,14 +181,16 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, const CGHeroInstance * h) const { if (heroExperience) - { comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - } + if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaPoints || manaPercentage > 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, 0, 0); + { + int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); + } for (size_t i=0; i>; /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures) -/// NOTE: in future should (partially) replace seer hut/quest guard quests checks struct DLL_LINKAGE Limiter { /// day of week, unused if 0, 1-7 will test for current day of week @@ -82,6 +81,7 @@ struct DLL_LINKAGE Limiter virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; + bool empty() const; /// Generates list of components that describes reward for a specific hero virtual void loadComponents(std::vector & comps, diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 903cfaf22..8b636a9dd 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -519,7 +519,8 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -541,7 +542,8 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -549,7 +551,8 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -565,7 +568,8 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -604,10 +608,12 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); if(key == "Biography") - o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); + o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -643,7 +649,8 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -659,11 +666,16 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(!o) return; if(key == "First visit text") - o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + if(key == "Repeat quest") + o->quest->repeatedQuest = value.toBool(); } diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index 0cf1a1315..bf611834e 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -42,7 +42,9 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); item->setData(Qt::UserRole, QVariant::fromValue(i)); ui->lResources->setItem(i, 0, item); - ui->lResources->setCellWidget(i, 1, new QSpinBox); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + ui->lResources->setCellWidget(i, 1, spinBox); } //fill artifacts diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index a509f4a24..92f773f01 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -56,7 +56,11 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); item->setData(Qt::UserRole, QVariant::fromValue(i)); w->setItem(i, 0, item); - w->setCellWidget(i, 1, new QSpinBox); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + if(w == ui->rResources) + spinBox->setMinimum(i == GameResID::GOLD ? -999999 : -999); + w->setCellWidget(i, 1, spinBox); } } From 5eeda3cd25c0bacd24e645160087fe6ece965e58 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 01:53:25 +0200 Subject: [PATCH 10/28] Quests mostly work --- .../Pathfinding/Rules/AIMovementAfterDestinationRule.cpp | 2 +- lib/mapObjects/CQuest.cpp | 3 ++- mapeditor/inspector/inspector.cpp | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 68638efc5..89c71820b 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,7 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->questName == CQuest::missionName(0)) + if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->empty() && questObj->quest->killTarget == -1) { return false; } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 1ab0e85cf..0469ca5be 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -431,6 +431,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->progress = CQuest::NOT_ACTIVE; + setObjToKill(); quest->defineQuestName(); if(quest->empty() && quest->killTarget == -1) @@ -447,7 +448,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomNext) quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(1), quest->textOption).get()); if(!quest->isCustomComplete) - quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(2), quest->textOption).get()); + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 8b636a9dd..4a4590974 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -402,6 +402,7 @@ void Inspector::updateProperties(CGSeerHut * o) addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); addProperty("Repeat quest", o->quest->repeatedQuest, false); + addProperty("Time limit", o->quest->lastDay, false); { //Quest auto * delegate = new QuestDelegate(controller, *o->quest); @@ -676,6 +677,8 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); if(key == "Repeat quest") o->quest->repeatedQuest = value.toBool(); + if(key == "Time limit") + o->quest->lastDay = value.toString().toInt(); } From 31d71ddd25f99db39e7e4eb1eea481c4176c9469 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 02:33:17 +0200 Subject: [PATCH 11/28] Minor fixes in text --- lib/mapObjects/CQuest.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 0469ca5be..5ddaef105 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -28,6 +28,7 @@ #include "../mapping/CMap.h" #include "../modding/ModScope.h" #include "../modding/ModUtility.h" +#include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -59,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -169,6 +170,9 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(heroLevel > 0) text.replaceNumber(heroLevel); + if(heroExperience > 0) + text.replaceNumber(heroExperience); + { //primary skills MetaString loot; for(int i = 0; i < 4; ++i) @@ -180,6 +184,17 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } + + for(auto & skill : secondary) + { + loot.appendTextID(VLC->skillh->getById(skill.first)->getNameTextID()); + } + + for(auto & spell : spells) + { + loot.appendTextID(VLC->spellh->getById(spell)->getNameTextID()); + } + if(!loot.empty()) text.replaceRawString(loot.buildList()); } @@ -237,7 +252,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com } if(!players.empty()) - text.replaceLocalString(EMetaText::COLOR, players.front()); + { + MetaString loot; + for(auto & p : players) + loot.appendLocalString(EMetaText::COLOR, p); + + text.replaceRawString(loot.buildList()); + } } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const @@ -280,6 +301,8 @@ void CQuest::defineQuestName() questName = CQuest::missionName(0); if(heroLevel > 0) questName = CQuest::missionName(1); for(auto & s : primary) if(s) questName = CQuest::missionName(2); + if(!spells.empty()) questName = CQuest::missionName(2); + if(!secondary.empty()) questName = CQuest::missionName(2); if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); if(!artifacts.empty()) questName = CQuest::missionName(5); From 62c4e8a98d344a988a34801d3f2804ea5e7072e9 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 02:39:10 +0200 Subject: [PATCH 12/28] Fix compiling --- lib/mapping/MapFormatH3M.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 97a2d6820..9fc78927d 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2005,11 +2005,12 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { guard->quest->primary[x] = reader->readUInt8(); } + break; } - break; case EQuestMission::LEVEL: { guard->quest->heroLevel = reader->readUInt32(); + break; } case EQuestMission::KILL_HERO: case EQuestMission::KILL_CREATURE: From 5b10b457cff0b2952d17743ce38c3917fc916320 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 00:47:19 +0200 Subject: [PATCH 13/28] Fix code review suggestions --- AI/Nullkiller/Goals/CompleteQuest.cpp | 16 +-- .../Rules/AIMovementAfterDestinationRule.cpp | 4 +- AI/VCAI/Goals/CompleteQuest.cpp | 28 ++--- docs/modders/Map_Objects/Rewardable.md | 1 - lib/CCreatureSet.cpp | 8 ++ lib/CCreatureSet.h | 2 + lib/mapObjects/CQuest.cpp | 103 +++++++++--------- lib/mapObjects/CQuest.h | 8 +- lib/mapping/MapFormatH3M.cpp | 26 ++--- lib/rewardable/Info.cpp | 2 +- lib/rewardable/Limiter.cpp | 57 ++++------ lib/rewardable/Limiter.h | 11 +- lib/rewardable/Reward.h | 4 +- lib/rmg/modificators/TreasurePlacer.cpp | 6 +- mapeditor/inspector/questwidget.cpp | 92 ++++++++-------- 15 files changed, 186 insertions(+), 182 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 96b9d317a..20ef080f2 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -38,26 +38,26 @@ TGoalVec CompleteQuest::decompose() const logAi->debug("Trying to realize quest: %s", questToString()); - if(!q.quest->artifacts.empty()) + if(!q.quest->mission.artifacts.empty()) return missionArt(); - if(!q.quest->heroes.empty()) + if(!q.quest->mission.heroes.empty()) return missionHero(); - if(!q.quest->creatures.empty()) + if(!q.quest->mission.creatures.empty()) return missionArmy(); - if(q.quest->resources.nonZero()) + if(q.quest->mission.resources.nonZero()) return missionResources(); - if(q.quest->killTarget >= 0) + if(q.quest->killTarget != ObjectInstanceID::NONE) return missionDestroyObj(); - for(auto & s : q.quest->primary) + for(auto & s : q.quest->mission.primary) if(s) return missionIncreasePrimaryStat(); - if(q.quest->heroLevel > 0) + if(q.quest->mission.heroLevel > 0) return missionLevel(); if(q.quest->questName == CQuest::missionName(10)) @@ -127,7 +127,7 @@ TGoalVec CompleteQuest::missionArt() const CaptureObjectsBehavior findArts; - for(auto art : q.quest->artifacts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 89c71820b..7f6664a22 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,9 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->empty() && questObj->quest->killTarget == -1) + if(destination.nodeObject->ID == Obj::QUEST_GUARD + && questObj->quest->mission == Rewardable::Limiter{} + && questObj->quest->killTarget == ObjectInstanceID::NONE) { return false; } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index d915efd62..66241ff8b 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -29,26 +29,26 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { logAi->debug("Trying to realize quest: %s", questToString()); - if(!q.quest->artifacts.empty()) + if(!q.quest->mission.artifacts.empty()) return missionArt(); - if(!q.quest->heroes.empty()) + if(!q.quest->mission.heroes.empty()) return missionHero(); - if(!q.quest->creatures.empty()) + if(!q.quest->mission.creatures.empty()) return missionArmy(); - if(q.quest->resources.nonZero()) + if(q.quest->mission.resources.nonZero()) return missionResources(); - if(q.quest->killTarget >= 0) + if(q.quest->killTarget != ObjectInstanceID::NONE) return missionDestroyObj(); - for(auto & s : q.quest->primary) + for(auto & s : q.quest->mission.primary) if(s) return missionIncreasePrimaryStat(); - if(q.quest->heroLevel > 0) + if(q.quest->mission.heroLevel > 0) return missionLevel(); if(q.quest->questName == CQuest::missionName(10)) @@ -127,7 +127,7 @@ TGoalVec CompleteQuest::missionArt() const if(!solutions.empty()) return solutions; - for(auto art : q.quest->artifacts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } @@ -155,7 +155,7 @@ TGoalVec CompleteQuest::missionArmy() const if(!solutions.empty()) return solutions; - for(auto creature : q.quest->creatures) + for(auto creature : q.quest->mission.creatures) { solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); } @@ -169,7 +169,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const if(solutions.empty()) { - for(int i = 0; i < q.quest->primary.size(); ++i) + for(int i = 0; i < q.quest->mission.primary.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); @@ -185,7 +185,7 @@ TGoalVec CompleteQuest::missionLevel() const if(solutions.empty()) { - logAi->debug("Don't know how to reach hero level %d", q.quest->heroLevel); + logAi->debug("Don't know how to reach hero level %d", q.quest->mission.heroLevel); } return solutions; @@ -217,10 +217,10 @@ TGoalVec CompleteQuest::missionResources() const } else { - for(int i = 0; i < q.quest->resources.size(); ++i) + for(int i = 0; i < q.quest->mission.resources.size(); ++i) { - if(q.quest->resources[i]) - solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->resources[i]))); + if(q.quest->mission.resources[i]) + solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->mission.resources[i]))); } } } diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 3aa316327..9be9b6fcd 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -456,7 +456,6 @@ Keep in mind, that all randomization is performed on map load and on object rese - Can be used as limiter - Can NOT be used as reward - Only players with specific color can pass the limiter -- If not specified or empty all colors may pass the limiter ```jsonc "colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 317cabaf8..9f8a3b20f 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1029,6 +1029,14 @@ void CStackBasicDescriptor::setType(const CCreature * c) type = c; } +bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r) +{ + return (!l.type && !r.type) + || (l.type && r.type + && l.type->getId() == r.type->getId() + && l.count == r.count); +} + void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("amount", count); diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 6dff6b7fa..abc2d09d1 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -42,6 +42,8 @@ public: TQuantity getCount() const; virtual void setType(const CCreature * c); + + friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5ddaef105..f8de8ec4b 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -40,7 +40,7 @@ CQuest::CQuest(): qid(-1), progress(NOT_ACTIVE), lastDay(-1), - killTarget(-1), + killTarget(ObjectInstanceID::NONE), textOption(0), completedOption(0), stackDirection(0), @@ -101,7 +101,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) ui32 count = 0; ui32 slotsCount = 0; bool hasExtraCreatures = false; - for(cre = q->creatures.begin(); cre != q->creatures.end(); ++cre) + for(cre = q->mission.creatures.begin(); cre != q->mission.creatures.end(); ++cre) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { @@ -123,10 +123,10 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) bool CQuest::checkQuest(const CGHeroInstance * h) const { - if(!heroAllowed(h)) + if(!mission.heroAllowed(h)) return false; - if(killTarget >= 0) + if(killTarget != ObjectInstanceID::NONE) { if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; @@ -137,7 +137,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { - for(auto & elem : artifacts) + for(auto & elem : mission.artifacts) { if(h->hasArt(elem)) { @@ -161,36 +161,36 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const } } - cb->takeCreatures(h->id, creatures); - cb->giveResources(h->getOwner(), resources); + cb->takeCreatures(h->id, mission.creatures); + cb->giveResources(h->getOwner(), mission.resources); } void CQuest::addTextReplacements(MetaString & text, std::vector & components) const { - if(heroLevel > 0) - text.replaceNumber(heroLevel); + if(mission.heroLevel > 0) + text.replaceNumber(mission.heroLevel); - if(heroExperience > 0) - text.replaceNumber(heroExperience); + if(mission.heroExperience > 0) + text.replaceNumber(mission.heroExperience); { //primary skills MetaString loot; for(int i = 0; i < 4; ++i) { - if(primary[i]) + if(mission.primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); + loot.replaceNumber(mission.primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } - for(auto & skill : secondary) + for(auto & skill : mission.secondary) { loot.appendTextID(VLC->skillh->getById(skill.first)->getNameTextID()); } - for(auto & spell : spells) + for(auto & spell : mission.spells) { loot.appendTextID(VLC->spellh->getById(spell)->getNameTextID()); } @@ -199,25 +199,25 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(killTarget >= 0 && !heroName.empty()) + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) { components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); addKillTargetReplacements(text); } - if(killTarget >= 0 && stackToKill.type) + if(killTarget != ObjectInstanceID::NONE && stackToKill.type) { components.emplace_back(stackToKill); addKillTargetReplacements(text); } - if(!heroes.empty()) - text.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); + if(!mission.heroes.empty()) + text.replaceRawString(VLC->heroh->getById(mission.heroes.front())->getNameTranslated()); - if(!artifacts.empty()) + if(!mission.artifacts.empty()) { MetaString loot; - for(const auto & elem : artifacts) + for(const auto & elem : mission.artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -225,10 +225,10 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(!creatures.empty()) + if(!mission.creatures.empty()) { MetaString loot; - for(const auto & elem : creatures) + for(const auto & elem : mission.creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -236,25 +236,25 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(resources.nonZero()) + if(mission.resources.nonZero()) { MetaString loot; for(int i = 0; i < 7; ++i) { - if(resources[i]) + if(mission.resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); + loot.replaceNumber(mission.resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } text.replaceRawString(loot.buildList()); } - if(!players.empty()) + if(!mission.players.empty()) { MetaString loot; - for(auto & p : players) + for(auto & p : mission.players) loot.appendLocalString(EMetaText::COLOR, p); text.replaceRawString(loot.buildList()); @@ -264,7 +264,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { bool failRequirements = (h ? !checkQuest(h) : true); - loadComponents(components, h); + mission.loadComponents(components, h); if(firstVisit) iwText.appendRawString(firstVisitText.toString()); @@ -299,17 +299,17 @@ void CQuest::defineQuestName() { //standard quests questName = CQuest::missionName(0); - if(heroLevel > 0) questName = CQuest::missionName(1); - for(auto & s : primary) if(s) questName = CQuest::missionName(2); - if(!spells.empty()) questName = CQuest::missionName(2); - if(!secondary.empty()) questName = CQuest::missionName(2); - if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); - if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); - if(!artifacts.empty()) questName = CQuest::missionName(5); - if(!creatures.empty()) questName = CQuest::missionName(6); - if(resources.nonZero()) questName = CQuest::missionName(7); - if(!heroes.empty()) questName = CQuest::missionName(8); - if(!players.empty()) questName = CQuest::missionName(9); + if(mission.heroLevel > 0) questName = CQuest::missionName(1); + for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2); + if(!mission.spells.empty()) questName = CQuest::missionName(2); + if(!mission.secondary.empty()) questName = CQuest::missionName(2); + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3); + if(killTarget != ObjectInstanceID::NONE && stackToKill.getType()) questName = CQuest::missionName(4); + if(!mission.artifacts.empty()) questName = CQuest::missionName(5); + if(!mission.creatures.empty()) questName = CQuest::missionName(6); + if(mission.resources.nonZero()) questName = CQuest::missionName(7); + if(!mission.heroes.empty()) questName = CQuest::missionName(8); + if(!mission.players.empty()) questName = CQuest::missionName(9); } void CQuest::addKillTargetReplacements(MetaString &out) const @@ -339,10 +339,9 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi isCustomComplete = !completedText.empty(); } - Rewardable::Limiter::serializeJson(handler); - handler.serializeInt("timeLimit", lastDay, -1); - handler.serializeInstance("killTarget", killTarget, -1); + handler.serializeStruct("limiter", mission); + handler.serializeInstance("killTarget", killTarget, ObjectInstanceID::NONE); if(!handler.saving) //compatibility with legacy vmaps { @@ -352,22 +351,22 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi return; if(missionType == "Level") - handler.serializeInt("heroLevel", heroLevel, -1); + handler.serializeInt("heroLevel", mission.heroLevel, -1); if(missionType == "PrimaryStat") { auto primarySkills = handler.enterStruct("primarySkills"); for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + handler.serializeInt(NPrimarySkill::names[i], mission.primary[i], 0); } if(missionType == "Artifact") - handler.serializeIdArray("artifacts", artifacts); + handler.serializeIdArray("artifacts", mission.artifacts); if(missionType == "Army") { auto a = handler.enterArray("creatures"); - a.serializeStruct(creatures); + a.serializeStruct(mission.creatures); } if(missionType == "Resources") @@ -376,7 +375,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mission.resources[idx], 0); } } @@ -384,14 +383,14 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi { ui32 temp; handler.serializeId("hero", temp, 0); - heroes.emplace_back(temp); + mission.heroes.emplace_back(temp); } if(missionType == "Player") { ui32 temp; handler.serializeId("player", temp, PlayerColor::NEUTRAL); - players.emplace_back(temp); + mission.players.emplace_back(temp); } } @@ -457,7 +456,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) setObjToKill(); quest->defineQuestName(); - if(quest->empty() && quest->killTarget == -1) + if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE) quest->progress = CQuest::COMPLETE; if(quest->questName == quest->missionName(0)) @@ -504,7 +503,7 @@ void CGSeerHut::setPropertyDer (ui8 what, ui32 val) { switch(what) { - case 10: + case CGSeerHut::OBJPROP_VISITED: quest->progress = static_cast(val); break; } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 6a6383fe3..2fd8520da 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGCreature; -class DLL_LINKAGE CQuest: public Rewardable::Limiter +class DLL_LINKAGE CQuest final { public: @@ -36,7 +36,8 @@ public: EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit - int killTarget; + ObjectInstanceID killTarget; + Rewardable::Limiter mission; bool repeatedQuest; // following fields are used only for kill creature/hero missions, the original @@ -89,7 +90,8 @@ public: h & isCustomComplete; h & completedOption; h & questName; - h & static_cast(*this); + h & mission; + h & killTarget; } void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 9fc78927d..112511213 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1890,7 +1890,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(artID != ArtifactID::NONE) { //not none quest - hut->quest->artifacts.push_back(artID); + hut->quest->mission.artifacts.push_back(artID); missionType = EQuestMission::ARTIFACT; } hut->quest->lastDay = -1; //no timeout @@ -2003,19 +2003,19 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { for(int x = 0; x < 4; ++x) { - guard->quest->primary[x] = reader->readUInt8(); + guard->quest->mission.primary[x] = reader->readUInt8(); } break; } case EQuestMission::LEVEL: { - guard->quest->heroLevel = reader->readUInt32(); + guard->quest->mission.heroLevel = reader->readUInt32(); break; } case EQuestMission::KILL_HERO: case EQuestMission::KILL_CREATURE: { - guard->quest->killTarget = reader->readUInt32(); + guard->quest->killTarget = ObjectInstanceID(reader->readUInt32()); break; } case EQuestMission::ARTIFACT: @@ -2024,7 +2024,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) for(int yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); - guard->quest->artifacts.push_back(artid); + guard->quest->mission.artifacts.push_back(artid); map->allowedArtifact[artid] = false; //these are unavailable for random generation } break; @@ -2032,29 +2032,29 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case EQuestMission::ARMY: { int typeNumber = reader->readUInt8(); - guard->quest->creatures.resize(typeNumber); + guard->quest->mission.creatures.resize(typeNumber); for(int hh = 0; hh < typeNumber; ++hh) { - guard->quest->creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->creatures[hh].count = reader->readUInt16(); + guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->mission.creatures[hh].count = reader->readUInt16(); } break; } case EQuestMission::RESOURCES: { for(int x = 0; x < 7; ++x) - guard->quest->resources[x] = reader->readUInt32(); + guard->quest->mission.resources[x] = reader->readUInt32(); break; } case EQuestMission::HERO: { - guard->quest->heroes.push_back(reader->readHero()); + guard->quest->mission.heroes.push_back(reader->readHero()); break; } case EQuestMission::PLAYER: { - guard->quest->players.push_back(reader->readPlayer()); + guard->quest->mission.players.push_back(reader->readPlayer()); break; } case EQuestMission::HOTA_MULTI: @@ -2067,13 +2067,13 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) - guard->quest->heroClasses.push_back(hc); + guard->quest->mission.heroClasses.push_back(hc); break; } if(missionSubID == 1) { missionId = int(EQuestMission::HOTA_REACH_DATE); - guard->quest->daysPassed = reader->readUInt32(); + guard->quest->mission.daysPassed = reader->readUInt32(); break; } break; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b9a8f1b2b..6453b9175 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -112,7 +112,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); limiter.resources = JsonRandom::loadResources(source["resources"], rng); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 8843d4e39..d49ccd550 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -27,7 +27,7 @@ Rewardable::Limiter::Limiter() , daysPassed(0) , heroExperience(0) , heroLevel(-1) - , manaPercentage(0) + , manaPercentage(-1) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) { @@ -35,35 +35,26 @@ Rewardable::Limiter::Limiter() Rewardable::Limiter::~Limiter() = default; -bool Rewardable::Limiter::empty() const +bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) { - if(dayOfWeek != 0 - || daysPassed != 0 - || heroLevel > 0 - || heroExperience > 0 - || manaPoints > 0 - || manaPercentage > 0 - || !secondary.empty() - || !creatures.empty() - || !spells.empty() - || !artifacts.empty() - || !players.empty() - || !heroes.empty() - || !heroClasses.empty() - || resources.nonZero() - || std::find_if(primary.begin(), primary.end(), [](si32 i){return i != 0;}) != primary.end()) - return false; - - for(const auto & sub : {noneOf, allOf, anyOf}) - { - for(const auto & sublimiter : sub) - { - if(!sublimiter->empty()) - return false; - } - } - - return true; + return l.dayOfWeek == r.dayOfWeek + && l.daysPassed == r.daysPassed + && l.heroLevel == r.heroLevel + && l.heroExperience == r.heroExperience + && l.manaPoints == r.manaPoints + && l.manaPercentage == r.manaPercentage + && l.secondary == r.secondary + && l.creatures == r.creatures + && l.spells == r.spells + && l.artifacts == r.artifacts + && l.players == r.players + && l.heroes == r.heroes + && l.heroClasses == r.heroClasses + && l.resources == r.resources + && l.primary == r.primary + && l.noneOf == r.noneOf + && l.allOf == r.allOf + && l.anyOf == r.anyOf; } bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const @@ -127,7 +118,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const } { - std::unordered_map artifactsRequirements; // artifact ID -> required count + std::unordered_map artifactsRequirements; // artifact ID -> required count for(const auto & art : artifacts) ++artifactsRequirements[art]; @@ -186,9 +177,9 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); - if (manaPoints || manaPercentage > 0) + if (manaPoints || manaPercentage >= 0) { - int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + int absoluteMana = (h->manaLimit() && manaPercentage >= 0) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); } @@ -232,7 +223,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("dayOfWeek", dayOfWeek); handler.serializeInt("daysPassed", daysPassed); resources.serializeJson(handler, "resources"); - handler.serializeInt("manaPercentage", manaPercentage); + handler.serializeInt("manaPercentage", manaPercentage, -1); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); handler.serializeIdArray("heroes", heroes); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index fd41dd031..a29843615 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -26,7 +26,7 @@ using LimitersList = std::vector>; /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures) -struct DLL_LINKAGE Limiter +struct DLL_LINKAGE Limiter final { /// day of week, unused if 0, 1-7 will test for current day of week si32 dayOfWeek; @@ -81,7 +81,6 @@ struct DLL_LINKAGE Limiter virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; - bool empty() const; /// Generates list of components that describes reward for a specific hero virtual void loadComponents(std::vector & comps, @@ -100,12 +99,12 @@ struct DLL_LINKAGE Limiter h & secondary; h & artifacts; h & creatures; - h & allOf; - h & anyOf; - h & noneOf; h & heroes; h & heroClasses; h & players; + h & allOf; + h & anyOf; + h & noneOf; } void serializeJson(JsonSerializeFormat & handler); @@ -113,4 +112,6 @@ struct DLL_LINKAGE Limiter } +bool DLL_LINKAGE operator== (const Rewardable::Limiter & l, const Rewardable::Limiter & r); + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 59fd44bcc..bed4d91b5 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -29,7 +29,7 @@ using RewardsList = std::vector>; /// Reward that can be granted to a hero /// NOTE: eventually should replace seer hut rewards and events/pandoras -struct DLL_LINKAGE Reward +struct DLL_LINKAGE Reward final { /// resources that will be given to player TResources resources; @@ -86,7 +86,7 @@ struct DLL_LINKAGE Reward si32 calculateManaPoints(const CGHeroInstance * h) const; Reward(); - virtual ~Reward(); + ~Reward(); template void serialize(Handler &h, const int version) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 542c8294e..20dc751b2 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -462,7 +462,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -510,7 +510,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -532,7 +532,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index bf611834e..e50d91c86 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -137,40 +137,40 @@ QuestWidget::~QuestWidget() void QuestWidget::obtainData() { - ui->lDayOfWeek->setCurrentIndex(quest.dayOfWeek); - ui->lDaysPassed->setValue(quest.daysPassed); - ui->lHeroLevel->setValue(quest.heroLevel); - ui->lHeroExperience->setValue(quest.heroExperience); - ui->lManaPoints->setValue(quest.manaPoints); - ui->lManaPercentage->setValue(quest.manaPercentage); - ui->lAttack->setValue(quest.primary[0]); - ui->lDefence->setValue(quest.primary[1]); - ui->lPower->setValue(quest.primary[2]); - ui->lKnowledge->setValue(quest.primary[3]); + ui->lDayOfWeek->setCurrentIndex(quest.mission.dayOfWeek); + ui->lDaysPassed->setValue(quest.mission.daysPassed); + ui->lHeroLevel->setValue(quest.mission.heroLevel); + ui->lHeroExperience->setValue(quest.mission.heroExperience); + ui->lManaPoints->setValue(quest.mission.manaPoints); + ui->lManaPercentage->setValue(quest.mission.manaPercentage); + ui->lAttack->setValue(quest.mission.primary[0]); + ui->lDefence->setValue(quest.mission.primary[1]); + ui->lPower->setValue(quest.mission.primary[2]); + ui->lKnowledge->setValue(quest.mission.primary[3]); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) - widget->setValue(quest.resources[i]); + widget->setValue(quest.mission.resources[i]); } - for(auto i : quest.artifacts) + for(auto i : quest.mission.artifacts) ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); - for(auto i : quest.spells) + for(auto i : quest.mission.spells) ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); - for(auto & i : quest.secondary) + for(auto & i : quest.mission.secondary) { int index = VLC->skills()->getById(i.first)->getIndex(); if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) widget->setCurrentIndex(i.second); } - for(auto & i : quest.creatures) + for(auto & i : quest.mission.creatures) { int index = i.type->getIndex(); ui->lCreatureId->setCurrentIndex(index); ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); } - for(auto & i : quest.heroes) + for(auto & i : quest.mission.heroes) { for(int e = 0; e < ui->lHeroes->count(); ++e) { @@ -181,7 +181,7 @@ void QuestWidget::obtainData() } } } - for(auto & i : quest.heroClasses) + for(auto & i : quest.mission.heroClasses) { for(int e = 0; e < ui->lHeroClasses->count(); ++e) { @@ -192,7 +192,7 @@ void QuestWidget::obtainData() } } } - for(auto & i : quest.players) + for(auto & i : quest.mission.players) { for(int e = 0; e < ui->lPlayers->count(); ++e) { @@ -204,81 +204,81 @@ void QuestWidget::obtainData() } } - if(quest.killTarget >= 0 && quest.killTarget < controller.map()->objects.size()) + if(quest.killTarget != ObjectInstanceID::NONE && quest.killTarget < controller.map()->objects.size()) ui->lKillTarget->setText(QString::fromStdString(controller.map()->objects[quest.killTarget]->instanceName)); else - quest.killTarget = -1; + quest.killTarget = ObjectInstanceID::NONE; } bool QuestWidget::commitChanges() { - quest.dayOfWeek = ui->lDayOfWeek->currentIndex(); - quest.daysPassed = ui->lDaysPassed->value(); - quest.heroLevel = ui->lHeroLevel->value(); - quest.heroExperience = ui->lHeroExperience->value(); - quest.manaPoints = ui->lManaPoints->value(); - quest.manaPercentage = ui->lManaPercentage->value(); - quest.primary[0] = ui->lAttack->value(); - quest.primary[1] = ui->lDefence->value(); - quest.primary[2] = ui->lPower->value(); - quest.primary[3] = ui->lKnowledge->value(); + quest.mission.dayOfWeek = ui->lDayOfWeek->currentIndex(); + quest.mission.daysPassed = ui->lDaysPassed->value(); + quest.mission.heroLevel = ui->lHeroLevel->value(); + quest.mission.heroExperience = ui->lHeroExperience->value(); + quest.mission.manaPoints = ui->lManaPoints->value(); + quest.mission.manaPercentage = ui->lManaPercentage->value(); + quest.mission.primary[0] = ui->lAttack->value(); + quest.mission.primary[1] = ui->lDefence->value(); + quest.mission.primary[2] = ui->lPower->value(); + quest.mission.primary[3] = ui->lKnowledge->value(); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) - quest.resources[i] = widget->value(); + quest.mission.resources[i] = widget->value(); } - quest.artifacts.clear(); + quest.mission.artifacts.clear(); for(int i = 0; i < ui->lArtifacts->count(); ++i) { if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) - quest.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + quest.mission.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); } - quest.spells.clear(); + quest.mission.spells.clear(); for(int i = 0; i < ui->lSpells->count(); ++i) { if(ui->lSpells->item(i)->checkState() == Qt::Checked) - quest.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + quest.mission.spells.push_back(VLC->spells()->getByIndex(i)->getId()); } - quest.secondary.clear(); + quest.mission.secondary.clear(); for(int i = 0; i < ui->lSkills->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) { if(widget->currentIndex() > 0) - quest.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + quest.mission.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); } } - quest.creatures.clear(); + quest.mission.creatures.clear(); for(int i = 0; i < ui->lCreatures->rowCount(); ++i) { int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) if(widget->value()) - quest.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + quest.mission.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } - quest.heroes.clear(); + quest.mission.heroes.clear(); for(int i = 0; i < ui->lHeroes->count(); ++i) { if(ui->lHeroes->item(i)->checkState() == Qt::Checked) - quest.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + quest.mission.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); } - quest.heroClasses.clear(); + quest.mission.heroClasses.clear(); for(int i = 0; i < ui->lHeroClasses->count(); ++i) { if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) - quest.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + quest.mission.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); } - quest.players.clear(); + quest.mission.players.clear(); for(int i = 0; i < ui->lPlayers->count(); ++i) { if(ui->lPlayers->item(i)->checkState() == Qt::Checked) - quest.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + quest.mission.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); } //quest.killTarget is set directly in object picking @@ -358,7 +358,7 @@ void QuestWidget::onTargetPicked(const CGObjectInstance * obj) if(!obj) //discarded { - quest.killTarget = -1; + quest.killTarget = ObjectInstanceID::NONE; ui->lKillTarget->setText(""); return; } From 8335fffaea5417af192d06584156f97648037bca Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 00:58:32 +0200 Subject: [PATCH 14/28] Fix rumors --- lib/CGameInfoCallback.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index bc3dfebe3..077f862c5 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -662,9 +662,9 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav case RumorState::TYPE_SPECIAL: text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first); if(rumor.first == RumorState::RUMOR_GRAIL) - text.replaceTextID(TextIdentifier("core", "genrltxt", "arraytxt", 158 + rumor.second).get()); + text.replaceTextID(TextIdentifier("core", "arraytxt", 158 + rumor.second).get()); else - text.replaceTextID(TextIdentifier("core", "genrltxt", "capitalColors", rumor.second).get()); + text.replaceTextID(TextIdentifier("core", "plcolors", rumor.second).get()); break; case RumorState::TYPE_MAP: @@ -672,7 +672,7 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav break; case RumorState::TYPE_RAND: - text.replaceTextID(TextIdentifier("core", "genrltxt", "randtvrn", rumor.first).get()); + text.replaceTextID(TextIdentifier("core", "randtvrn", rumor.first).get()); break; } From 6093f042dd2a92080d583eb024b8eee52ccad395 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:26:07 +0200 Subject: [PATCH 15/28] Text container fix --- lib/CGeneralTextHandler.cpp | 10 +++++----- lib/CGeneralTextHandler.h | 2 +- lib/mapping/CMapHeader.cpp | 3 +++ lib/mapping/CMapInfo.cpp | 3 +++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 657fb564e..2a5e280d7 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -264,21 +264,21 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) { - subContainers.insert(&container); + subContainers.push_back(&container); } void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { - subContainers.erase(&container); + std::remove(subContainers.begin(), subContainers.end(), &container); } const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const { if(stringsLocalizations.count(identifier.get()) == 0) { - for(const auto * container : subContainers) - if(container->identifierExists(identifier)) - return container->deserialize(identifier); + for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) + if((*containerIter)->identifierExists(identifier)) + return (*containerIter)->deserialize(identifier); logGlobal->error("Unable to find localization for string '%s'", identifier.get()); return identifier.get(); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 3807ddc7a..7bc7b3b94 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -146,7 +146,7 @@ protected: /// map identifier -> localization std::unordered_map stringsLocalizations; - std::set subContainers; + std::vector subContainers; /// add selected string to internal storage as high-priority strings void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index c86a923b8..2ca1ee8e0 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -144,6 +144,9 @@ ui8 CMapHeader::levels() const void CMapHeader::registerMapStrings() { + VLC->generaltexth->removeSubContainer(*this); + VLC->generaltexth->addSubContainer(*this); + //get supported languages. Assuming that translation containing most strings is the base language std::set mapLanguages, mapBaseLanguages; int maxStrings = 0; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index f037ef7d4..8fecd28e9 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -105,7 +105,10 @@ std::string CMapInfo::getNameTranslated() const if(campaign && !campaign->getNameTranslated().empty()) return campaign->getNameTranslated(); else if(mapHeader && !mapHeader->name.empty()) + { + mapHeader->registerMapStrings(); return mapHeader->name.toString(); + } else return VLC->generaltexth->allTexts[508]; } From 2b88e6941c744eb3e4b0bd46630f6a3978f9bd63 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:43:24 +0200 Subject: [PATCH 16/28] Fix mana limiter --- lib/rewardable/Info.cpp | 2 +- lib/rewardable/Limiter.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 6453b9175..b9a8f1b2b 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -112,7 +112,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); limiter.resources = JsonRandom::loadResources(source["resources"], rng); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index d49ccd550..48328d7a1 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -27,7 +27,7 @@ Rewardable::Limiter::Limiter() , daysPassed(0) , heroExperience(0) , heroLevel(-1) - , manaPercentage(-1) + , manaPercentage(0) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) { @@ -177,9 +177,9 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); - if (manaPoints || manaPercentage >= 0) + if (manaPoints || manaPercentage > 0) { - int absoluteMana = (h->manaLimit() && manaPercentage >= 0) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); } @@ -223,7 +223,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("dayOfWeek", dayOfWeek); handler.serializeInt("daysPassed", daysPassed); resources.serializeJson(handler, "resources"); - handler.serializeInt("manaPercentage", manaPercentage, -1); + handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); handler.serializeIdArray("heroes", heroes); From 7b7aaa36b2608c35756db6111957522fc7e109ea Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:44:58 +0200 Subject: [PATCH 17/28] fix compiling --- mapeditor/inspector/questwidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index e50d91c86..97e22f586 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -328,7 +328,7 @@ void QuestWidget::on_lKillTargetSelect_clicked() { if(auto * o = dynamic_cast(obj)) return o->ID != Obj::PRISON; - if(auto * o = dynamic_cast(obj)) + if(dynamic_cast(obj)) return true; return false; }; From 22c9eecd7d83702c7e5ef379d5b347b17165c672 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 08:54:54 +0200 Subject: [PATCH 18/28] Fix mistake --- lib/CGeneralTextHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 2a5e280d7..f68d6b3d4 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -269,7 +269,7 @@ void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { - std::remove(subContainers.begin(), subContainers.end(), &container); + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); } const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const From 7ccd4cdcb270b5e1aea253b4bd5ef99c236423cc Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 21:10:42 +0200 Subject: [PATCH 19/28] Refactor quests progress --- .../Pathfinding/Actions/QuestAction.cpp | 2 +- AI/VCAI/Goals/CompleteQuest.cpp | 2 +- client/windows/CQuestLog.cpp | 4 +- lib/mapObjects/CQuest.cpp | 39 +++++++++++-------- lib/mapObjects/CQuest.h | 15 +++---- lib/rewardable/Limiter.cpp | 5 +++ lib/rewardable/Limiter.h | 1 + 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index dae8c7bb2..1efebafca 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -25,7 +25,7 @@ namespace AIPathfinding return dynamic_cast(questInfo.obj)->checkQuest(node->actor->hero); } - return questInfo.quest->progress == CQuest::NOT_ACTIVE + return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner()) || questInfo.quest->checkQuest(node->actor->hero); } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 66241ff8b..42743c9ab 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -25,7 +25,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; - if(q.quest->progress != CQuest::COMPLETE) + if(!q.quest->isCompleted) { logAi->debug("Trying to realize quest: %s", questToString()); diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 208a74744..b4b3d80af 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -152,7 +152,7 @@ void CQuestLog::recreateLabelList() if (quests[i].quest->questName == CQuest::missionName(0)) continue; - if (quests[i].quest->progress == CQuest::COMPLETE) + if (quests[i].quest->isCompleted) { completeMissing = false; if (hideComplete) @@ -180,7 +180,7 @@ void CQuestLog::recreateLabelList() labels.push_back(label); // Select latest active quest - if (quests[i].quest->progress != CQuest::COMPLETE) + if(!quests[i].quest->isCompleted) selectQuest(i, currentLabel); currentLabel = static_cast(labels.size()); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index f8de8ec4b..ee45b3e01 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -38,7 +38,7 @@ std::map > CGKeys::playerKeyMap; //TODO: Remove constructor CQuest::CQuest(): qid(-1), - progress(NOT_ACTIVE), + isCompleted(false), lastDay(-1), killTarget(ObjectInstanceID::NONE), textOption(0), @@ -450,14 +450,12 @@ void CGSeerHut::initObj(CRandomGenerator & rand) init(rand); CRewardableObject::initObj(rand); - - quest->progress = CQuest::NOT_ACTIVE; setObjToKill(); quest->defineQuestName(); if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE) - quest->progress = CQuest::COMPLETE; + quest->isCompleted = true; if(quest->questName == quest->missionName(0)) { @@ -484,13 +482,15 @@ void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const std::string CGSeerHut::getHoverText(PlayerColor player) const { std::string hoverName = getObjectName(); - if(ID == Obj::SEER_HUT && quest->progress != CQuest::NOT_ACTIVE) + if(ID == Obj::SEER_HUT && quest->activeForPlayers.count(player)) { hoverName = VLC->generaltexth->allTexts[347]; boost::algorithm::replace_first(hoverName, "%s", seerName); } - if(quest->progress/* & quest->missionType*/) //rollover when the quest is active + if(quest->activeForPlayers.count(player) + && (quest->mission != Rewardable::Limiter{} + || quest->killTarget != ObjectInstanceID::NONE)) //rollover when the quest is active { MetaString ms; getRolloverText (ms, true); @@ -499,13 +499,21 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CGSeerHut::setPropertyDer (ui8 what, ui32 val) +void CGSeerHut::setPropertyDer(ui8 what, ui32 val) { switch(what) { - case CGSeerHut::OBJPROP_VISITED: - quest->progress = static_cast(val); + case CGSeerHut::SEERHUT_VISITED: + { + quest->activeForPlayers.emplace(val); break; + } + case CGSeerHut::SEERHUT_COMPLETE: + { + quest->isCompleted = val; + quest->activeForPlayers.clear(); + break; + } } } @@ -514,7 +522,7 @@ void CGSeerHut::newTurn(CRandomGenerator & rand) const CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { - cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); + cb->setObjProperty (id, CGSeerHut::SEERHUT_COMPLETE, true); } } @@ -522,14 +530,14 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const { InfoWindow iw; iw.player = h->getOwner(); - if(quest->progress < CQuest::COMPLETE) + if(!quest->isCompleted) { - bool firstVisit = !quest->progress; + bool firstVisit = !quest->activeForPlayers.count(h->getOwner()); bool failRequirements = !checkQuest(h); if(firstVisit) { - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::IN_PROGRESS); + cb->setObjProperty(id, CGSeerHut::SEERHUT_VISITED, h->getOwner()); AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); @@ -612,10 +620,7 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - if(quest && quest->repeatedQuest) - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::NOT_ACTIVE); - else - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete } } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 2fd8520da..f12a8ae8a 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -21,12 +21,6 @@ class DLL_LINKAGE CQuest final { public: - enum EProgress { - NOT_ACTIVE, - IN_PROGRESS, - COMPLETE - }; - static const std::string & missionName(int index); static const std::string & missionState(int index); @@ -34,11 +28,12 @@ public: si32 qid; //unique quest id for serialization / identification - EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit ObjectInstanceID killTarget; Rewardable::Limiter mission; bool repeatedQuest; + bool isCompleted; + std::set activeForPlayers; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info @@ -75,7 +70,8 @@ public: template void serialize(Handler &h, const int version) { h & qid; - h & progress; + h & isCompleted; + h & activeForPlayers; h & lastDay; h & textOption; h & stackToKill; @@ -143,7 +139,8 @@ public: h & seerName; } protected: - static constexpr int OBJPROP_VISITED = 10; + static constexpr int SEERHUT_VISITED = 10; + static constexpr int SEERHUT_COMPLETE = 11; void setPropertyDer(ui8 what, ui32 val) override; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 48328d7a1..b0f8c74ac 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -57,6 +57,11 @@ bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) && l.anyOf == r.anyOf; } +bool operator!=(const Rewardable::Limiter & l, const Rewardable::Limiter & r) +{ + return !(l == r); +} + bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index a29843615..35e1b8af9 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -113,5 +113,6 @@ struct DLL_LINKAGE Limiter final } bool DLL_LINKAGE operator== (const Rewardable::Limiter & l, const Rewardable::Limiter & r); +bool DLL_LINKAGE operator!= (const Rewardable::Limiter & l, const Rewardable::Limiter & r); VCMI_LIB_NAMESPACE_END From 14b030d2ebd9e9943191fbb8c8df0deb8df74fca Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 23:15:24 +0200 Subject: [PATCH 20/28] Support hota quest gates --- lib/mapObjects/CQuest.cpp | 15 ++++++++++++++- lib/mapObjects/CQuest.h | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index ee45b3e01..10818b2aa 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -710,10 +710,23 @@ void CGQuestGuard::init(CRandomGenerator & rand) configuration.info.push_back({}); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - configuration.info.back().reward.removeObject = true; + configuration.info.back().reward.removeObject = subID == 0 ? true : false; configuration.canRefuse = true; } +void CGQuestGuard::onHeroVisit(const CGHeroInstance * h) const +{ + if(!quest->isCompleted) + CGSeerHut::onHeroVisit(h); + else + cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, false); +} + +bool CGQuestGuard::passableFor(PlayerColor color) const +{ + return quest->isCompleted; +} + void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) { //quest only, do not call base class diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index f12a8ae8a..3f76d3550 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -151,6 +151,9 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: void init(CRandomGenerator & rand) override; + + void onHeroVisit(const CGHeroInstance * h) const override; + bool passableFor(PlayerColor color) const override; template void serialize(Handler &h, const int version) { From 3199b7261f5fa01bb378a71ed4007ef0a274b7df Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 23:28:10 +0200 Subject: [PATCH 21/28] Cosmetic fixes for map editor --- mapeditor/inspector/inspector.cpp | 15 +++++++++++++++ mapeditor/inspector/inspector.h | 1 + 2 files changed, 16 insertions(+) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 4a4590974..613aa2c94 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -410,6 +410,14 @@ void Inspector::updateProperties(CGSeerHut * o) } } +void Inspector::updateProperties(CGQuestGuard * o) +{ + if(!o || !o->quest) return; + + addProperty("Reward", PropertyEditorPlaceholder(), nullptr, true); + addProperty("Repeat quest", o->quest->repeatedQuest, true); +} + void Inspector::updateProperties() { if(!obj) @@ -452,6 +460,7 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGPandoraBox); UPDATE_OBJ_PROPERTIES(CGEvent); UPDATE_OBJ_PROPERTIES(CGSeerHut); + UPDATE_OBJ_PROPERTIES(CGQuestGuard); table->show(); } @@ -498,6 +507,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGPandoraBox); SET_PROPERTIES(CGEvent); SET_PROPERTIES(CGSeerHut); + SET_PROPERTIES(CGQuestGuard); } void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVariant & value) @@ -681,6 +691,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & o->quest->lastDay = value.toString().toInt(); } +void Inspector::setProperty(CGQuestGuard * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + //===============IMPLEMENT PROPERTY VALUE TYPE============================ QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 5e2e1f96a..e4a85715f 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -82,6 +82,7 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); DECLARE_OBJ_PROPERTY_METHODS(CGEvent); DECLARE_OBJ_PROPERTY_METHODS(CGSeerHut); + DECLARE_OBJ_PROPERTY_METHODS(CGQuestGuard); //===============DECLARE PROPERTY VALUE TYPE============================== QTableWidgetItem * addProperty(unsigned int value); From 74a90cde5d078d4ad8d70d24e2dfdb3b048ddde1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 12:52:01 +0200 Subject: [PATCH 22/28] Fix hero level limiter --- lib/mapObjects/CQuest.cpp | 4 ++-- lib/rewardable/Limiter.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 10818b2aa..51d0d1ead 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -60,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -351,7 +351,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi return; if(missionType == "Level") - handler.serializeInt("heroLevel", mission.heroLevel, -1); + handler.serializeInt("heroLevel", mission.heroLevel); if(missionType == "PrimaryStat") { diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b0f8c74ac..b23c50610 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -179,7 +179,7 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroExperience) comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - if (heroLevel) + if (heroLevel > 0) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaPoints || manaPercentage > 0) From fbf5492fd9c0f17beab94732e643da751995d72a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:09:10 +0200 Subject: [PATCH 23/28] Remove unnecessary virtuals --- lib/rewardable/Limiter.h | 4 ++-- lib/rewardable/Reward.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 35e1b8af9..6fa52925e 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -78,12 +78,12 @@ struct DLL_LINKAGE Limiter final LimitersList noneOf; Limiter(); - virtual ~Limiter(); + ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; /// Generates list of components that describes reward for a specific hero - virtual void loadComponents(std::vector & comps, + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index bed4d91b5..9493083f1 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -78,7 +78,7 @@ struct DLL_LINKAGE Reward final bool removeObject; /// Generates list of components that describes reward for a specific hero - virtual void loadComponents(std::vector & comps, + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; Component getDisplayedComponent(const CGHeroInstance * h) const; From fadf086e61db25f6c7be1c98f3eaa62635d43378 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:29:52 +0200 Subject: [PATCH 24/28] Fix review comments --- AI/Nullkiller/Goals/CompleteQuest.cpp | 3 --- AI/VCAI/Goals/CompleteQuest.cpp | 13 +++++++++---- client/windows/CQuestLog.cpp | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 20ef080f2..d0e981a78 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -59,9 +59,6 @@ TGoalVec CompleteQuest::decompose() const if(q.quest->mission.heroLevel > 0) return missionLevel(); - - if(q.quest->questName == CQuest::missionName(10)) - return missionKeymaster(); return TGoalVec(); } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 42743c9ab..cb78ee8db 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -21,6 +21,11 @@ bool CompleteQuest::operator==(const CompleteQuest & other) const return q.quest->qid == other.q.quest->qid; } +bool isKeyMaster(const QuestInfo & q) +{ + return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD); +} + TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; @@ -28,6 +33,9 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() if(!q.quest->isCompleted) { logAi->debug("Trying to realize quest: %s", questToString()); + + if(isKeyMaster(q)) + return missionKeymaster(); if(!q.quest->mission.artifacts.empty()) return missionArt(); @@ -50,9 +58,6 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() if(q.quest->mission.heroLevel > 0) return missionLevel(); - - if(q.quest->questName == CQuest::missionName(10)) - return missionKeymaster(); } return TGoalVec(); @@ -60,7 +65,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() TSubgoal CompleteQuest::whatToDoToAchieve() { - if(q.quest->questName == CQuest::missionName(0)) + if(q.quest->mission == Rewardable::Limiter{}) { throw cannotFulfillGoalException("Can not complete inactive quest"); } diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index b4b3d80af..c43870c9b 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -148,8 +148,8 @@ void CQuestLog::recreateLabelList() int currentLabel = 0; for (int i = 0; i < quests.size(); ++i) { - // Quests with MISSION_NONE type don't have text for them and can't be displayed - if (quests[i].quest->questName == CQuest::missionName(0)) + // Quests without mision don't have text for them and can't be displayed + if (quests[i].quest->mission == Rewardable::Limiter{}) continue; if (quests[i].quest->isCompleted) From 9108f7e3e52f7f5f86bac9530928d10377a13f5e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:40:28 +0200 Subject: [PATCH 25/28] Remove assert triggered while flying --- server/CGameHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 273ea0514..88eeda6d0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1095,8 +1095,6 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if (isInTheMap(guardPos)) guardian = getTile(guardPos)->visitableObjects.back(); - assert(guardian == nullptr || dynamic_cast(guardian) != nullptr); - const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT; const bool disembarking = h->boat && t.terType->isLand() From 2bf8cdc9f525099348439c700af0466955f01b30 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 12:52:45 +0200 Subject: [PATCH 26/28] Hota-related bugs were fixed --- lib/mapObjects/CQuest.cpp | 16 ++++++++++------ lib/mapping/MapFormatH3M.cpp | 1 + lib/rewardable/Limiter.cpp | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 51d0d1ead..29f18e9da 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -60,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -71,7 +71,9 @@ const std::string & CQuest::missionName(int mission) "bringResources", "bringHero", "bringPlayer", - "keymaster" + "keymaster", + "hota", + "other" }; if(static_cast(mission) < names.size()) @@ -299,6 +301,7 @@ void CQuest::defineQuestName() { //standard quests questName = CQuest::missionName(0); + if(mission != Rewardable::Limiter{}) questName = CQuest::missionName(12); if(mission.heroLevel > 0) questName = CQuest::missionName(1); for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2); if(!mission.spells.empty()) questName = CQuest::missionName(2); @@ -310,6 +313,7 @@ void CQuest::defineQuestName() if(mission.resources.nonZero()) questName = CQuest::missionName(7); if(!mission.heroes.empty()) questName = CQuest::missionName(8); if(!mission.players.empty()) questName = CQuest::missionName(9); + if(mission.daysPassed > 0 || !mission.heroClasses.empty()) questName = CQuest::missionName(11); } void CQuest::addKillTargetReplacements(MetaString &out) const @@ -425,10 +429,6 @@ void CGSeerHut::setObjToKill() quest->heroName = getHeroToKill(false)->getNameTranslated(); quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); } - - quest->getCompletionText(configuration.onSelect); - for(auto & i : configuration.info) - quest->getCompletionText(i.message); } void CGSeerHut::init(CRandomGenerator & rand) @@ -470,6 +470,10 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomComplete) quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } + + quest->getCompletionText(configuration.onSelect); + for(auto & i : configuration.info) + quest->getCompletionText(i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 112511213..f1b15605f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1830,6 +1830,7 @@ CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const Objec if(features.levelHOTA3) { uint32_t repeateableQuestsCount = reader->readUInt32(); + hut->quest->repeatedQuest = repeateableQuestsCount != 0; if(repeateableQuestsCount != 0) logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b23c50610..b5c5f9384 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -72,7 +72,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(daysPassed != 0) { - if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed) + if (IObjectInterface::cb->getDate(Date::DAY) <= daysPassed) return false; } From 5f52e6b35e5bdc9ee8a0c425f9ce7482ca5e8b62 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 23:35:37 +0200 Subject: [PATCH 27/28] Revert "Auxiliary commit to revert individual files from 2bf8cdc9f525099348439c700af0466955f01b30" This reverts commit d9952495391f7ac77dd32dcf33f487e146b3655b. --- lib/rewardable/Limiter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b5c5f9384..b23c50610 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -72,7 +72,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(daysPassed != 0) { - if (IObjectInterface::cb->getDate(Date::DAY) <= daysPassed) + if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed) return false; } From 4651893b48c3842090ac83d745fd650fbae406e4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 14 Oct 2023 00:15:15 +0200 Subject: [PATCH 28/28] Fix hota quests --- lib/mapObjects/CQuest.cpp | 8 +++++++- lib/mapping/MapFormatH3M.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 29f18e9da..85e154128 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -261,6 +261,9 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } + + if(lastDay >= 0) + text.replaceNumber(lastDay - IObjectInterface::cb->getDate(Date::DAY)); } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const @@ -273,6 +276,9 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components else if(failRequirements) iwText.appendRawString(nextVisitText.toString()); + if(lastDay >= 0) + iwText.appendTextID(TextIdentifier("core", "seerhut", "time", textOption).get()); + addTextReplacements(iwText, components); } @@ -283,7 +289,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const std::string questState = missionState(onHover ? 3 : 4); - ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); + ms.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, questState, textOption).get()); std::vector components; addTextReplacements(ms, components); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index f1b15605f..b5c2ded6e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2074,7 +2074,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 1) { missionId = int(EQuestMission::HOTA_REACH_DATE); - guard->quest->mission.daysPassed = reader->readUInt32(); + guard->quest->mission.daysPassed = reader->readUInt32() + 1; break; } break;