From 44bdd2cbf3340ec488dde77051f65d520deb0027 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 24 Jan 2023 23:31:07 +0200 Subject: [PATCH] Implemented selectable messages for visited & empty state --- config/objects/rewardableBonusing.json | 17 ++- config/objects/rewardableOncePerHero.json | 16 ++- config/objects/rewardableOncePerWeek.json | 8 +- config/objects/rewardableOnceVisitable.json | 14 ++- lib/NetPacks.h | 2 +- lib/mapObjects/CRewardableConstructor.cpp | 45 ++++++-- lib/mapObjects/CRewardableConstructor.h | 2 + lib/mapObjects/CRewardableObject.cpp | 115 +++++++++++--------- lib/mapObjects/CRewardableObject.h | 42 ++++--- 9 files changed, 173 insertions(+), 88 deletions(-) diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index 5305af113..c1cd1ae93 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -313,9 +313,24 @@ "rarity" : 40 }, - "onVisitedMessage" : 136, // TODO: alternative message with Cavalier -> Champions upgrade & text ID 139 "visitMode" : "bonus", "selectMode" : "selectFirst", + + "onVisited" : [ + { + "message" : 139, + "limiter" : { + "creatures" : [ { "type" : "cavalier", "amount" : 1 } ], + }, + "changeCreatures" : { + "cavalier" : "champion" + } + }, + { + "message" : 136 + }, + ], + "rewards" : [ { "limiter" : { diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index 26579c9dd..49c6a556e 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -215,7 +215,17 @@ "rarity" : 50 }, - "onEmptyMessage" : 150, //TODO: ID 150 should be used for Gold version, ID 152 - for Gems version + "onEmpty" : [ + { + "message" : 150, + "appearChance" : { "min" : 34, "max" : 67 } + }, + { + "message" : 152, + "appearChance" : { "min" : 67, "max" : 67 } + } + ], + "onVisitedMessage" : 147, "visitMode" : "hero", "selectMode" : "selectFirst", "canRefuse" : true, @@ -271,12 +281,12 @@ { "limiter" : { "resources" : { "gold" : 1000 } }, "resources" : { "gold" : -1000 }, - "primary" : { "spellpower" : 2 } + "primary" : { "spellpower" : 1 } }, { "limiter" : { "resources" : { "gold" : 1000 } }, "resources" : { "gold" : -1000 }, - "primary" : { "knowledge" : 2 } + "primary" : { "knowledge" : 1 } } ] } diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index 24ea682a7..5288a7ef3 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -93,7 +93,7 @@ "rarity" : 50 }, - "onEmptyMessage" : 93, + "onVisitedMessage" : 93, "resetParameters" : { "period" : 7, "visitors" : true, @@ -134,7 +134,7 @@ "rarity" : 80 }, - "onEmptyMessage" : 169, + "onVisitedMessage" : 169, "resetParameters" : { "period" : 7, "visitors" : true, @@ -175,7 +175,7 @@ "rarity" : 50 }, - "onEmptyMessage" : 165, + "onVisitedMessage" : 165, "resetParameters" : { "period" : 7, "visitors" : true @@ -187,7 +187,7 @@ "limiter" : { "daysPassed" : 8 }, "message" : 164, "resources" : { "gold" : 1000 } - } + }, { "message" : 164, "resources" : { "gold" : 500 } diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 1ed138f19..e84b76d89 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -18,7 +18,7 @@ "rarity" : 100 }, - "onEmptyMessage" : 65, + "onVisitedMessage" : 65, "visitMode" : "once", "selectMode" : "selectFirst", "rewards" : [ @@ -53,7 +53,7 @@ "rarity" : 100 }, - "onEmptyMessage" : 38, + "onVisitedMessage" : 38, "blockedVisitable" : true, "visitMode" : "once", "selectMode" : "selectFirst", @@ -89,7 +89,7 @@ "rarity" : 50 }, - "onEmptyMessage" : 156, + "onVisitedMessage" : 156, "visitMode" : "once", "selectMode" : "selectFirst", "rewards" : [ @@ -136,9 +136,15 @@ }, "onSelectMessage" : 161, - "onEmptyMessage" : 163, "visitMode" : "once", "selectMode" : "selectFirst", + + "onVisited" : [ + { + "message" : 163, + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + } + ], "rewards" : [ { "appearChance" : { "max" : 30 }, diff --git a/lib/NetPacks.h b/lib/NetPacks.h index f8b64d317..092e76061 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1178,7 +1178,7 @@ namespace ObjProperty BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, //object with reward - REWARD_RANDOMIZE, REWARD_SELECT + REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED }; } diff --git a/lib/mapObjects/CRewardableConstructor.cpp b/lib/mapObjects/CRewardableConstructor.cpp index 5b1c6f58a..3d693a1c7 100644 --- a/lib/mapObjects/CRewardableConstructor.cpp +++ b/lib/mapObjects/CRewardableConstructor.cpp @@ -138,13 +138,14 @@ void CRandomRewardObjectInfo::configureResetInfo(CRewardableObject * object, CRa resetParameters.rewards = source["rewards"].Bool(); } -void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRandomGenerator & rng) const +void CRandomRewardObjectInfo::configureRewards( + CRewardableObject * object, + CRandomGenerator & rng, const + JsonNode & source, + std::map & thrownDice, + CRewardVisitInfo::ERewardEventType event ) const { - object->info.clear(); - - std::map thrownDice; - - for (const JsonNode & reward : parameters["rewards"].Vector()) + for (const JsonNode & reward : source.Vector()) { if (!reward["appearChance"].isNull()) { @@ -172,21 +173,47 @@ void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRando configureLimiter(object, rng, info.limiter, reward["limiter"]); configureReward(object, rng, info.reward, reward); + info.visitType = event; info.message = loadMessage(reward["message"]); for (const auto & artifact : info.reward.artifacts ) info.message.addReplacement(MetaString::ART_NAMES, artifact.getNum()); - + for (const auto & artifact : info.reward.spells ) info.message.addReplacement(MetaString::SPELL_NAME, artifact.getNum()); object->info.push_back(info); } +} + +void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRandomGenerator & rng) const +{ + object->info.clear(); + + std::map thrownDice; + + configureRewards(object, rng, parameters["rewards"], thrownDice, CRewardVisitInfo::EVENT_FIRST_VISIT); + configureRewards(object, rng, parameters["onVisited"], thrownDice, CRewardVisitInfo::EVENT_ALREADY_VISITED); + configureRewards(object, rng, parameters["onEmpty"], thrownDice, CRewardVisitInfo::EVENT_NOT_AVAILABLE); object->blockVisit= parameters["blockedVisitable"].Bool(); object->onSelect = loadMessage(parameters["onSelectMessage"]); - object->onVisited = loadMessage(parameters["onVisitedMessage"]); - object->onEmpty = loadMessage(parameters["onEmptyMessage"]); + + if (!parameters["onVisitedMessage"].isNull()) + { + CRewardVisitInfo onVisited; + onVisited.visitType = CRewardVisitInfo::EVENT_ALREADY_VISITED; + onVisited.message = loadMessage(parameters["onVisitedMessage"]); + object->info.push_back(onVisited); + } + + if (!parameters["onEmptyMessage"].isNull()) + { + CRewardVisitInfo onEmpty; + onEmpty.visitType = CRewardVisitInfo::EVENT_NOT_AVAILABLE; + onEmpty.message = loadMessage(parameters["onEmptyMessage"]); + object->info.push_back(onEmpty); + } configureResetInfo(object, rng, object->resetParameters, parameters["resetParameters"]); diff --git a/lib/mapObjects/CRewardableConstructor.h b/lib/mapObjects/CRewardableConstructor.h index 883901faf..021da8d8f 100644 --- a/lib/mapObjects/CRewardableConstructor.h +++ b/lib/mapObjects/CRewardableConstructor.h @@ -20,6 +20,8 @@ class DLL_LINKAGE CRandomRewardObjectInfo : public IObjectInfo { JsonNode parameters; + void configureRewards(CRewardableObject * object, CRandomGenerator & rng, const JsonNode & source, std::map & thrownDice, CRewardVisitInfo::ERewardEventType mode) const; + void configureLimiter(CRewardableObject * object, CRandomGenerator & rng, CRewardLimiter & limiter, const JsonNode & source) const; TRewardLimitersList configureSublimiters(CRewardableObject * object, CRandomGenerator & rng, const JsonNode & source) const; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 07e40f540..8c154b976 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -102,7 +102,7 @@ bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const return false; } -std::vector CRewardableObject::getAvailableRewards(const CGHeroInstance * hero) const +std::vector CRewardableObject::getAvailableRewards(const CGHeroInstance * hero, CRewardVisitInfo::ERewardEventType event) const { std::vector ret; @@ -110,7 +110,7 @@ std::vector CRewardableObject::getAvailableRewards(const CGHeroInstance * { const CRewardVisitInfo & visit = info[i]; - if(visit.limiter.heroAllowed(hero)) + if(event == visit.visitType && visit.limiter.heroAllowed(hero)) { logGlobal->trace("Reward %d is allowed", i); ret.push_back(static_cast(i)); @@ -119,16 +119,11 @@ std::vector CRewardableObject::getAvailableRewards(const CGHeroInstance * return ret; } -CRewardVisitInfo CRewardableObject::getVisitInfo(int index, const CGHeroInstance *) const -{ - return info[index]; -} - void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { - auto grantRewardWithMessage = [&](int index) -> void + auto grantRewardWithMessage = [&](int index, bool markAsVisit) -> void { - auto vi = getVisitInfo(index, h); + auto vi = info[index]; logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); // show message only if it is not empty if (!vi.message.toString().empty()) @@ -140,7 +135,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const cb->showInfoDialog(&iw); } // grant reward afterwards. Note that it may remove object - grantReward(index, h); + grantReward(index, h, markAsVisit); }; auto selectRewardsMessage = [&](std::vector rewards, const MetaString & dialog) -> void { @@ -148,40 +143,38 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const sd.player = h->tempOwner; sd.text = dialog; for (auto index : rewards) - sd.components.push_back(getVisitInfo(index, h).reward.getDisplayedComponent(h)); + sd.components.push_back(info[index].reward.getDisplayedComponent(h)); cb->showBlockingDialog(&sd); }; - if(!wasVisited(h)) + if(!wasVisitedBefore(h)) { - auto rewards = getAvailableRewards(h); + auto rewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_FIRST_VISIT); bool objectRemovalPossible = false; for(auto index : rewards) { - if(getVisitInfo(index, h).reward.removeObject) + if(info[index].reward.removeObject) objectRemovalPossible = true; } logGlobal->debug("Visiting object with %d possible rewards", rewards.size()); switch (rewards.size()) { - case 0: // no available rewards, e.g. empty flotsam + case 0: // no available rewards, e.g. visiting School of War without gold { - InfoWindow iw; - iw.player = h->tempOwner; - if (!onEmpty.toString().empty()) - iw.text = onEmpty; + auto emptyRewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_NOT_AVAILABLE); + if (!emptyRewards.empty()) + grantRewardWithMessage(emptyRewards[0], false); else - iw.text = onVisited; - cb->showInfoDialog(&iw); + logMod->warn("No applicable message for visiting empty object!"); break; } case 1: // one reward. Just give it with message { if (canRefuse) - selectRewardsMessage(rewards, getVisitInfo(rewards[0], h).message); + selectRewardsMessage(rewards, info[rewards[0]].message); else - grantRewardWithMessage(rewards[0]); + grantRewardWithMessage(rewards[0], true); break; } default: // multiple rewards. Act according to select mode @@ -191,14 +184,14 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const selectRewardsMessage(rewards, onSelect); break; case SELECT_FIRST: // give first available - grantRewardWithMessage(rewards[0]); + grantRewardWithMessage(rewards[0], true); break; } break; } } - if(!objectRemovalPossible && getAvailableRewards(h).size() == 0) + if(!objectRemovalPossible && getAvailableRewards(h, CRewardVisitInfo::EVENT_FIRST_VISIT).size() == 0) { ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); cb->sendAndApply(&cov); @@ -207,19 +200,18 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const else { logGlobal->debug("Revisiting already visited object"); - InfoWindow iw; - iw.player = h->tempOwner; - if (!onVisited.toString().empty()) - iw.text = onVisited; + + auto visitedRewards = getAvailableRewards(h, CRewardVisitInfo::EVENT_ALREADY_VISITED); + if (!visitedRewards.empty()) + grantRewardWithMessage(visitedRewards[0], false); else - iw.text = onEmpty; - cb->showInfoDialog(&iw); + logMod->warn("No applicable message for visiting already visited object!"); } } void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const { - grantRewardAfterLevelup(getVisitInfo(selectedReward, hero), hero); + grantRewardAfterLevelup(info[selectedReward], hero); } void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const @@ -229,8 +221,8 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 if(answer > 0 && answer-1 < info.size()) { - auto list = getAvailableRewards(hero); - grantReward(list[answer - 1], hero); + auto list = getAvailableRewards(hero, CRewardVisitInfo::EVENT_FIRST_VISIT); + grantReward(list[answer - 1], hero, true); } else { @@ -238,18 +230,18 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 } } -void CRewardableObject::onRewardGiven(const CGHeroInstance * hero) const +void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero, bool markVisited) const { - // no implementation, virtual function for overrides -} + if (markVisited) + { + cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, true); + + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); + cb->sendAndApply(&cov); + } -void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const -{ - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); - cb->sendAndApply(&cov); cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID); - - grantRewardBeforeLevelup(getVisitInfo(rewardID, hero), hero); + grantRewardBeforeLevelup(info[rewardID], hero); } void CRewardableObject::grantRewardBeforeLevelup(const CRewardVisitInfo & info, const CGHeroInstance * hero) const @@ -366,23 +358,39 @@ void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, c cb->giveCreatures(this, hero, creatures, false); } - onRewardGiven(hero); - if(info.reward.removeObject) cb->removeObject(this); } +bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) const +{ + switch (visitMode) + { + case VISIT_UNLIMITED: + return false; + case VISIT_ONCE: + return onceVisitableObjectCleared; + case VISIT_PLAYER: + return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id)); + case VISIT_BONUS: + return contextHero->hasBonusFrom(Bonus::OBJECT, ID); + case VISIT_HERO: + return contextHero->visitedObjects.count(ObjectInstanceID(id)); + default: + return false; + } + +} + bool CRewardableObject::wasVisited(PlayerColor player) const { switch (visitMode) { case VISIT_UNLIMITED: case VISIT_BONUS: - return false; - case VISIT_ONCE: - return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); case VISIT_HERO: return false; + case VISIT_ONCE: case VISIT_PLAYER: return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); default: @@ -394,8 +402,6 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const { switch (visitMode) { - case VISIT_UNLIMITED: - return false; case VISIT_BONUS: return h->hasBonusFrom(Bonus::OBJECT, ID); case VISIT_HERO: @@ -429,7 +435,8 @@ void CRewardInfo::loadComponents(std::vector & comps, if (gainedLevels) comps.push_back(Component(Component::EXPERIENCE, 1, gainedLevels, 0)); - if (manaDiff) comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff, 0)); + if (manaDiff || manaPercentage >= 0) + comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff, 0)); for (size_t i=0; isetObjProperty(id, ObjProperty::REWARD_CLEARED, false); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id); cb->sendAndApply(&cov); } @@ -526,6 +538,7 @@ CRewardableObject::CRewardableObject(): selectMode(0), visitMode(0), selectedReward(0), + onceVisitableObjectCleared(false), canRefuse(false) {} diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 8c1d6726c..a39b844bf 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -207,12 +207,23 @@ public: class DLL_LINKAGE CRewardVisitInfo { public: + enum ERewardEventType + { + EVENT_INVALID, + EVENT_FIRST_VISIT, + EVENT_ALREADY_VISITED, + EVENT_NOT_AVAILABLE + }; + CRewardLimiter limiter; CRewardInfo reward; /// Message that will be displayed on granting of this reward, if not empty MetaString message; + /// Event to which this reward is assigned + ERewardEventType visitType; + CRewardVisitInfo() = default; template void serialize(Handler &h, const int version) @@ -220,6 +231,7 @@ public: h & limiter; h & reward; h & message; + h & visitType; } }; @@ -258,26 +270,24 @@ protected: }; /// filters list of visit info and returns rewards that can be granted to current hero - virtual std::vector getAvailableRewards(const CGHeroInstance * hero) const; + virtual std::vector getAvailableRewards(const CGHeroInstance * hero, CRewardVisitInfo::ERewardEventType event ) const; - virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; - - virtual CRewardVisitInfo getVisitInfo(int index, const CGHeroInstance *h) const; + virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero, bool markVisited) const; virtual void triggerReset() const; - /// Rewards that can be granted by an object - std::vector info; - - /// MetaString's that contain text for messages for specific situations + /// Message that will be shown if player needs to select one of multiple rewards MetaString onSelect; - MetaString onVisited; - MetaString onEmpty; + + /// Rewards that can be applied by an object + std::vector info; /// how reward will be selected, uses ESelectMode enum ui8 selectMode; + /// contols who can visit an object, uses EVisitMode enum ui8 visitMode; + /// reward selected by player ui16 selectedReward; @@ -287,6 +297,12 @@ protected: /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse; + /// return true if this object was "cleared" before and no longer has rewards applicable to selected hero + /// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before + bool wasVisitedBefore(const CGHeroInstance * contextHero) const; + + bool onceVisitableObjectCleared; + public: EVisitMode getVisitMode() const; ui16 getResetDuration() const; @@ -311,9 +327,6 @@ public: /// applies player selection of reward void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - /// function that will be called once reward is fully granted to hero - virtual void onRewardGiven(const CGHeroInstance * hero) const; - void initObj(CRandomGenerator & rand) override; CRewardableObject(); @@ -325,11 +338,10 @@ public: h & canRefuse; h & resetParameters; h & onSelect; - h & onVisited; - h & onEmpty; h & visitMode; h & selectMode; h & selectedReward; + h & onceVisitableObjectCleared; } // for configuration/object setup