1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00

Rewardable objects may now define guards. Converted Crypt to rewardable.

This commit is contained in:
Ivan Savenko 2024-08-30 14:21:44 +00:00
parent a9c4683da6
commit 785036836c
12 changed files with 209 additions and 105 deletions

View File

@ -302,6 +302,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
return iat.army.getStrength();
}
case Obj::MONSTER:
case Obj::CRYPT:
{
//TODO!!!!!!!!
const CGCreature * cre = dynamic_cast<const CGCreature *>(obj);
@ -319,7 +320,6 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
}
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck

View File

@ -833,7 +833,7 @@
},
"crypt" : {
"index" :84,
"handler": "bank",
"handler": "configurable",
"base" : {
"sounds" : {
"ambient" : ["LOOPDEAD"],
@ -843,16 +843,27 @@
"types" : {
"crypt" : {
"index" : 0,
"resetDuration" : 0,
"name" : "Crypt",
"aiValue" : 1500,
"rmg" : {
"value" : 1000,
"rarity" : 100
},
"levels": [
"visitMode" : "once",
"selectMode" : "selectFirst",
"onGuardedMessage" : 119, // Do you want to search the graves?
"onVisited" : [
{
"chance": 30,
"message" : 120, // Such a despicable act reduces your army's morale.
"bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ]
}
],
"rewards": [
{
"appearChance" : { "min" : 0, "max" : 30 },
"guards": [
{ "amount": 10, "type": "skeleton" },
{ "amount": 10, "type": "walkingDead" },
@ -860,17 +871,14 @@
{ "amount": 10, "type": "skeleton" },
{ "amount": 10, "type": "skeleton" }
],
"combat_value": 75,
"reward" : {
"value": 1500,
"resources":
{
"gold" : 1500
}
"message" : 121, // you search the graves and find something
"resources":
{
"gold" : 1500
}
},
{
"chance": 30,
"appearChance" : { "min" : 30, "max" : 60 },
"guards": [
{ "amount": 13, "type": "skeleton" },
{ "amount": 10, "type": "walkingDead" },
@ -878,50 +886,42 @@
{ "amount": 10, "type": "walkingDead" },
{ "amount": 12, "type": "skeleton" }
],
"combat_value": 94,
"reward" : {
"value": 2000,
"resources":
{
"gold" : 2000
}
"message" : 121, // you search the graves and find something
"resources":
{
"gold" : 2000
}
},
{
"chance": 30,
"appearChance" : { "min" : 60, "max" : 90 },
"guards": [
{ "amount": 20, "type": "skeleton" },
{ "amount": 20, "type": "walkingDead" },
{ "amount": 10, "type": "wight" },
{ "amount": 5, "type": "vampire" }
],
"combat_value": 169,
"reward" : {
"value": 3500,
"resources":
{
"gold" : 2500
},
"artifacts": [ { "class" : "TREASURE" } ]
"message" : 121, // you search the graves and find something
"resources":
{
"gold" : 2500
},
"artifacts": [ { "class" : "TREASURE" } ]
}
},
{
"chance": 10,
"appearChance" : { "min" : 90, "max" : 100 },
"guards": [
{ "amount": 20, "type": "skeleton" },
{ "amount": 20, "type": "walkingDead" },
{ "amount": 10, "type": "wight" },
{ "amount": 10, "type": "vampire" }
],
"combat_value": 225,
"reward" : {
"value": 6000,
"resources":
{
"gold" : 5000
},
"artifacts": [ { "class" : "TREASURE" } ]
}
"message" : 121, // you search the graves and find something
"resources":
{
"gold" : 5000
},
"artifacts": [ { "class" : "TREASURE" } ]
}
]
}

View File

@ -62,17 +62,20 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal
void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RNG & rng) const
{
if(auto * rewardableObject = dynamic_cast<CRewardableObject*>(object))
{
rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID);
auto * rewardableObject = dynamic_cast<CRewardableObject*>(object);
if (rewardableObject->configuration.info.empty())
{
if (objectInfo.getParameters()["rewards"].isNull())
logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey());
else
logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey());
}
if (!rewardableObject)
throw std::runtime_error("Object " + std::to_string(object->getObjGroupIndex()) + ", " + std::to_string(object->getObjTypeIndex()) + " is not a rewardable object!" );
rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID);
rewardableObject->initializeGuards();
if (rewardableObject->configuration.info.empty())
{
if (objectInfo.getParameters()["rewards"].isNull())
logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey());
else
logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey());
}
}

View File

@ -15,7 +15,6 @@
#include <vcmi/spells/Service.h>
#include "../texts/CGeneralTextHandler.h"
#include "../CSoundBase.h"
#include "../IGameSettings.h"
#include "../CPlayerState.h"
#include "../mapObjectConstructors/CObjectClassesHandler.h"
@ -156,23 +155,18 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
case Obj::DRAGON_UTOPIA:
banktext = 47;
break;
case Obj::CRYPT:
banktext = 119;
break;
case Obj::SHIPWRECK:
banktext = 122;
break;
case Obj::PYRAMID:
banktext = 105;
break;
case Obj::CREATURE_BANK:
default:
banktext = 32;
break;
}
BlockingDialog bd(true, false);
bd.player = h->getOwner();
bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played
bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext);
bd.components = getPopupComponents(h->getOwner());
if (banktext == 32)
@ -196,17 +190,12 @@ void CBank::doVisit(const CGHeroInstance * hero) const
case Obj::DERELICT_SHIP:
textID = 43;
break;
case Obj::CRYPT:
textID = 121;
break;
case Obj::SHIPWRECK:
textID = 124;
break;
case Obj::PYRAMID:
textID = 106;
break;
case Obj::CREATURE_BANK:
case Obj::DRAGON_UTOPIA:
default:
textID = 34;
break;
@ -218,7 +207,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const
{
case Obj::SHIPWRECK:
case Obj::DERELICT_SHIP:
case Obj::CRYPT:
{
GiveBonus gbonus;
gbonus.id = hero->id;
@ -237,14 +225,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const
textID = 42;
gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.101");
break;
case Obj::CRYPT:
textID = 120;
gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.98");
break;
}
cb->giveHeroBonus(&gbonus);
iw.components.emplace_back(ComponentType::MORALE, -1);
iw.soundID = soundBase::invalid;
break;
}
case Obj::PYRAMID:
@ -258,8 +241,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const
iw.components.emplace_back(ComponentType::LUCK, -2);
break;
}
case Obj::CREATURE_BANK:
case Obj::DRAGON_UTOPIA:
default:
iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
iw.text.replaceRawString(getObjectName());

View File

@ -10,16 +10,19 @@
#include "StdInc.h"
#include "CRewardableObject.h"
#include "../gameState/CGameState.h"
#include "../texts/CGeneralTextHandler.h"
#include "../CPlayerState.h"
#include "../GameSettings.h"
#include "../IGameCallback.h"
#include "../gameState/CGameState.h"
#include "../mapObjectConstructors/AObjectTypeHandler.h"
#include "../mapObjectConstructors/CObjectClassesHandler.h"
#include "../mapObjectConstructors/CRewardableConstructor.h"
#include "../mapObjects/CGHeroInstance.h"
#include "../networkPacks/PacksForClient.h"
#include "../networkPacks/PacksForClientBattle.h"
#include "../serializer/JsonSerializeFormat.h"
#include "../texts/CGeneralTextHandler.h"
#include <vstd/RNG.h>
@ -91,7 +94,42 @@ std::vector<Component> CRewardableObject::loadComponents(const CGHeroInstance *
return result;
}
bool CRewardableObject::guardedPotentially() const
{
for (auto const & visitInfo : configuration.info)
if (!visitInfo.reward.guards.empty())
return true;
return false;
}
bool CRewardableObject::guardedPresently() const
{
return stacksCount() > 0;
}
void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
{
if (guardedPresently())
{
auto guardedIndexes = getAvailableRewards(h, Rewardable::EEventType::EVENT_GUARDED);
auto guardedReward = configuration.info.at(guardedIndexes.at(0));
// ask player to confirm attack
BlockingDialog bd(true, false);
bd.player = h->getOwner();
bd.text = guardedReward.message;
bd.components = getPopupComponents(h->getOwner());
cb->showBlockingDialog(&bd);
}
else
{
doHeroVisit(h);
}
}
void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const
{
if(!wasVisitedBefore(h))
{
@ -181,39 +219,54 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
}
void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
{
if(answer == 0)
if (result.winner == BattleSide::ATTACKER)
{
switch (configuration.visitMode)
{
case Rewardable::VISIT_UNLIMITED:
case Rewardable::VISIT_BONUS:
case Rewardable::VISIT_HERO:
case Rewardable::VISIT_LIMITER:
{
// workaround for object with refusable reward not getting marked as visited
// TODO: better solution that would also work for player-visitable objects
if (!wasScouted(hero->getOwner()))
{
ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id);
cb->sendAndApply(&cov);
}
}
}
return; // player refused
doHeroVisit(hero);
}
}
if(answer > 0 && answer-1 < configuration.info.size())
void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const
{
if(guardedPresently())
{
auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
markAsVisited(hero);
grantReward(list[answer - 1], hero);
if (answer)
cb->startBattleI(hero, this, true);
}
else
{
throw std::runtime_error("Unhandled choice");
if(answer == 0)
{
switch(configuration.visitMode)
{
case Rewardable::VISIT_UNLIMITED:
case Rewardable::VISIT_BONUS:
case Rewardable::VISIT_HERO:
case Rewardable::VISIT_LIMITER:
{
// workaround for object with refusable reward not getting marked as visited
// TODO: better solution that would also work for player-visitable objects
if(!wasScouted(hero->getOwner()))
{
ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id);
cb->sendAndApply(&cov);
}
}
}
return; // player refused
}
if(answer > 0 && answer - 1 < configuration.info.size())
{
auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
markAsVisited(hero);
grantReward(list[answer - 1], hero);
}
else
{
throw std::runtime_error("Unhandled choice");
}
}
}
@ -368,19 +421,41 @@ std::vector<Component> CRewardableObject::getPopupComponentsImpl(PlayerColor pla
if (!configuration.showScoutedPreview)
return {};
auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
if (rewardIndices.empty() && !configuration.info.empty())
if (guardedPresently())
{
// Object has valid config, but current hero has no rewards that he can receive.
// Usually this happens if hero has already visited this object -> show reward using context without any hero
// since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level
return loadComponents(nullptr, {0});
if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION))
return {};
std::map<CreatureID, int> guardsAmounts;
std::vector<Component> result;
for (auto const & slot : Slots())
if (slot.second)
guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount();
for (auto const & guard : guardsAmounts)
{
Component comp(ComponentType::CREATURE, guard.first, guard.second);
result.push_back(comp);
}
return result;
}
else
{
auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
if (rewardIndices.empty() && !configuration.info.empty())
{
// Object has valid config, but current hero has no rewards that he can receive.
// Usually this happens if hero has already visited this object -> show reward using context without any hero
// since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level
return loadComponents(nullptr, {0});
}
if (rewardIndices.empty())
return {};
if (rewardIndices.empty())
return {};
return loadComponents(hero, rewardIndices);
return loadComponents(hero, rewardIndices);
}
}
std::vector<Component> CRewardableObject::getPopupComponents(PlayerColor player) const
@ -440,4 +515,21 @@ void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler)
handler.serializeStruct("rewardable", static_cast<Rewardable::Interface&>(*this));
}
void CRewardableObject::initializeGuards()
{
clearSlots();
for (auto const & visitInfo : configuration.info)
{
for (auto const & guard : visitInfo.reward.guards)
{
auto slotID = getFreeSlot();
if (!slotID.validSlot())
return;
putStack(slotID, new CStackInstance(guard.getId(), guard.getCount()));
}
}
}
VCMI_LIB_NAMESPACE_END

View File

@ -45,7 +45,14 @@ protected:
std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
std::vector<Component> getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const;
void doHeroVisit(const CGHeroInstance *h) const;
/// Returns true if this object might have guards present, whether they were cleared or not
bool guardedPotentially() const;
/// Returns true if this object is currently guarded
bool guardedPresently() const;
public:
/// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player)
bool wasVisited(PlayerColor player) const override;
bool wasVisited(const CGHeroInstance * h) const override;
@ -56,6 +63,8 @@ public:
/// gives reward to player or ask for choice in case of multiple rewards
void onHeroVisit(const CGHeroInstance *h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
///possibly resets object state
void newTurn(vstd::RNG & rand) const override;
@ -66,6 +75,8 @@ public:
void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
void initObj(vstd::RNG & rand) override;
void initializeGuards();
void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;

View File

View File

View File

@ -2434,6 +2434,7 @@ void SetRewardableConfiguration::applyGs(CGameState *gs)
auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr);
assert(rewardablePtr);
rewardablePtr->configuration = configuration;
rewardablePtr->initializeGuards();
}
else
{

View File

@ -44,7 +44,8 @@ enum class EEventType
EVENT_INVALID = 0,
EVENT_FIRST_VISIT,
EVENT_ALREADY_VISITED,
EVENT_NOT_AVAILABLE
EVENT_NOT_AVAILABLE,
EVENT_GUARDED
};
constexpr std::array<std::string_view, 4> SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"};

View File

@ -174,6 +174,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, vstd:
reward.removeObject = source["removeObject"].Bool();
reward.bonuses = randomizer.loadBonuses(source["bonuses"]);
reward.guards = randomizer.loadCreatures(source["guards"], rng, variables);
reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables);
reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables);
@ -378,6 +380,16 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd:
object.info.push_back(onEmpty);
}
if (!parameters["onGuardedMessage"].isNull())
{
Rewardable::VisitInfo onGuarded;
onGuarded.visitType = Rewardable::EEventType::EVENT_GUARDED;
onGuarded.message = loadMessage(parameters["onGuardedMessage"], TextIdentifier(objectTextID, "onGuarded"));
replaceTextPlaceholders(onGuarded.message, object.variables);
object.info.push_back(onGuarded);
}
configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]);
object.canRefuse = parameters["canRefuse"].Bool();

View File

@ -82,6 +82,9 @@ struct DLL_LINKAGE Reward final
/// fixed value, in form of percentage from max
si32 movePercentage;
/// Guards that must be defeated in order to access this reward, empty if not guarded
std::vector<CStackBasicDescriptor> guards;
/// list of bonuses, e.g. morale/luck
std::vector<Bonus> bonuses;