diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 5b012ea00..412b6d6c7 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1873,8 +1873,6 @@ std::vector VCAI::getUnblockedHeroes() const bool VCAI::canAct (HeroPtr h) const { - bool digsTile = false; - auto mission = lockedHeroes.find(h); if (mission != lockedHeroes.end()) { diff --git a/client/GUIClasses.cpp b/client/GUIClasses.cpp index cf399467b..5cdf409ad 100644 --- a/client/GUIClasses.cpp +++ b/client/GUIClasses.cpp @@ -5963,7 +5963,7 @@ void MoraleLuckBox::set(const IBonusBearer *node) else { //it's a creature window - if (morale && node->hasBonusOfType (Bonus::UNDEAD) || + if ((morale && node->hasBonusOfType(Bonus::UNDEAD)) || node->hasBonusOfType(Bonus::BLOCK_MORALE) || node->hasBonusOfType(Bonus::NON_LIVING)) { text += CGI->generaltexth->arraytxt[113]; //unaffected by morale diff --git a/lib/BattleState.cpp b/lib/BattleState.cpp index 4b530864f..0f6876913 100644 --- a/lib/BattleState.cpp +++ b/lib/BattleState.cpp @@ -1,4 +1,14 @@ -#include "StdInc.h" +/* + * BattleState.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" #include "BattleState.h" #include @@ -11,18 +21,7 @@ #include "NetPacks.h" #include "JsonNode.h" #include "filesystem/Filesystem.h" - - -/* - * BattleState.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -extern std::minstd_rand ran; +#include "CRandomGenerator.h" const CStack * BattleInfo::getNextStack() const { @@ -1133,7 +1132,7 @@ std::pair CStack::countKilledByAttack(int damageReceived) const return std::make_pair(killedCount, newRemainingHP); } -void CStack::prepareAttacked(BattleStackAttacked &bsa, boost::optional customCount /*= boost::none*/) const +void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional customCount /*= boost::none*/) const { auto afterAttack = countKilledByAttack(bsa.damageAmount); @@ -1141,7 +1140,7 @@ void CStack::prepareAttacked(BattleStackAttacked &bsa, boost::optional cust bsa.newHP = afterAttack.second; - if (bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage) + if(bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage) { bsa.flags |= BattleStackAttacked::CLONE_KILLED; return; // no rebirth I believe @@ -1156,19 +1155,27 @@ void CStack::prepareAttacked(BattleStackAttacked &bsa, boost::optional cust bsa.killedAmount = countToUse; //we cannot kill more creatures than we have int resurrectFactor = valOfBonuses(Bonus::REBIRTH); - if (resurrectFactor > 0 && casts) //there must be casts left + if(resurrectFactor > 0 && casts) //there must be casts left { - int resurrectedCount = base->count * resurrectFactor / 100; - if (resurrectedCount) - resurrectedCount += ((base->count * resurrectFactor / 100.0 - resurrectedCount) > ran()%100 / 100.0) ? 1 : 0; //last stack has proportional chance to rebirth - else //only one unit - resurrectedCount += ((base->count * resurrectFactor / 100.0) > ran()%100 / 100.0) ? 1 : 0; - if (hasBonusOfType(Bonus::REBIRTH, 1)) - vstd::amax (resurrectedCount, 1); //resurrect at least one Sacred Phoenix - if (resurrectedCount) + int resurrectedStackCount = base->count * resurrectFactor / 100; + + // last stack has proportional chance to rebirth + auto diff = base->count * resurrectFactor / 100.0 - resurrectedStackCount; + if (diff > rand.nextDouble(0, 0.99)) + { + resurrectedStackCount += 1; + } + + if(hasBonusOfType(Bonus::REBIRTH, 1)) + { + // resurrect at least one Sacred Phoenix + vstd::amax(resurrectedStackCount, 1); + } + + if(resurrectedStackCount > 0) { bsa.flags |= BattleStackAttacked::REBIRTH; - bsa.newAmount = resurrectedCount; //risky? + bsa.newAmount = resurrectedStackCount; //risky? bsa.newHP = MaxHealth(); //resore full health } } diff --git a/lib/BattleState.h b/lib/BattleState.h index 93a2cda0f..8e18a8ea2 100644 --- a/lib/BattleState.h +++ b/lib/BattleState.h @@ -27,7 +27,7 @@ class CArmedInstance; class CGTownInstance; class CStackInstance; struct BattleStackAttacked; - +class CRandomGenerator; //only for use in BattleInfo @@ -222,7 +222,7 @@ public: std::vector getSurroundingHexes(BattleHex attackerPos = BattleHex::INVALID) const; // get six or 8 surrounding hexes depending on creature size std::pair countKilledByAttack(int damageReceived) const; //returns pair - void prepareAttacked(BattleStackAttacked &bsa, boost::optional customCount = boost::none) const; //requires bsa.damageAmout filled + void prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional customCount = boost::none) const; //requires bsa.damageAmout filled template void serialize(Handler &h, const int version) { diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b54a9cf14..f7c5393f8 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -1,3 +1,13 @@ +/* + * CArtHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + #include "StdInc.h" #include "CArtHandler.h" @@ -9,20 +19,10 @@ #include "CObjectHandler.h" #include "NetPacksBase.h" #include "GameConstants.h" +#include "CRandomGenerator.h" using namespace boost::assign; -/* - * CArtHandler.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ - -extern std::minstd_rand ran; // Note: list must match entries in ArtTraits.txt #define ART_POS_LIST \ ART_POS(SPELLBOOK) \ @@ -422,11 +422,7 @@ CreatureID CArtHandler::machineIDToCreature(ArtifactID id) return CreatureID::NONE; //this artifact is not a creature } -ArtifactID CArtHandler::getRandomArt(int flags) -{ - return getArtSync(ran(), flags, true); -} -ArtifactID CArtHandler::getArtSync (ui32 rand, int flags, bool erasePicked) +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) { auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) { @@ -466,9 +462,8 @@ ArtifactID CArtHandler::getArtSync (ui32 rand, int flags, bool erasePicked) std::vector > out; getAllowed(out); - ArtifactID artID = out[rand % out.size()]->id; - if(erasePicked) - erasePickedArt (artID); + ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; + erasePickedArt(artID); return artID; } diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 0058d2a5f..1105f9d4a 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -24,6 +24,7 @@ class CGHeroInstance; struct ArtifactLocation; class CArtifactSet; class CArtifactInstance; +class CRandomGenerator; #define ART_BEARER_LIST \ ART_BEARER(HERO)\ @@ -63,13 +64,13 @@ public: std::string nodeName() const override; void addNewBonus(Bonus *b) override; - virtual void levelUpArtifact (CArtifactInstance * art){}; - - ui32 price; - std::map > possibleSlots; //Bearer Type => ids of slots where artifact can be placed - std::unique_ptr > constituents; // Artifacts IDs a combined artifact consists of, or nullptr. - std::vector constituentOf; // Reverse map of constituents - combined arts that include this art - EartClass aClass; + virtual void levelUpArtifact (CArtifactInstance * art){}; + + ui32 price; + std::map > possibleSlots; //Bearer Type => ids of slots where artifact can be placed + std::unique_ptr > constituents; // Artifacts IDs a combined artifact consists of, or nullptr. + std::vector constituentOf; // Reverse map of constituents - combined arts that include this art + EartClass aClass; ArtifactID id; template void serialize(Handler &h, const int version) @@ -188,18 +189,6 @@ public: class DLL_LINKAGE CArtHandler : public IHandlerBase //handles artifacts { - CArtifact * loadFromJson(const JsonNode & node); - - void addSlot(CArtifact * art, const std::string & slotID); - void loadSlots(CArtifact * art, const JsonNode & node); - void loadClass(CArtifact * art, const JsonNode & node); - void loadType(CArtifact * art, const JsonNode & node); - void loadComponents(CArtifact * art, const JsonNode & node); - void loadGrowingArt(CGrowingArtifact * art, const JsonNode & node); - - void giveArtBonus(ArtifactID aid, Bonus::BonusType type, int val, int subtype = -1, Bonus::ValueType valType = Bonus::BASE_NUMBER, shared_ptr limiter = shared_ptr(), int additionalinfo = 0); - void giveArtBonus(ArtifactID aid, Bonus::BonusType type, int val, int subtype, shared_ptr propagator, int additionalinfo = 0); - void giveArtBonus(ArtifactID aid, Bonus *bonus); public: std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! @@ -213,9 +202,9 @@ public: void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of gibven class. No side effects boost::optional&> listFromClass(CArtifact::EartClass artifactClass); - void erasePickedArt(ArtifactID id); - ArtifactID getRandomArt (int flags); - ArtifactID getArtSync (ui32 rand, int flags, bool erasePicked = false); + + /// Gets a artifact ID randomly and removes the selected artifact from this handler. + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); bool legalArtifact(ArtifactID id); void getAllowedArts(std::vector > &out, std::vector *arts, int flag); void getAllowed(std::vector > &out, int flags); @@ -245,6 +234,22 @@ public: & growingArtifacts; //if(!h.saving) sortArts(); } + +private: + CArtifact * loadFromJson(const JsonNode & node); + + void addSlot(CArtifact * art, const std::string & slotID); + void loadSlots(CArtifact * art, const JsonNode & node); + void loadClass(CArtifact * art, const JsonNode & node); + void loadType(CArtifact * art, const JsonNode & node); + void loadComponents(CArtifact * art, const JsonNode & node); + void loadGrowingArt(CGrowingArtifact * art, const JsonNode & node); + + void giveArtBonus(ArtifactID aid, Bonus::BonusType type, int val, int subtype = -1, Bonus::ValueType valType = Bonus::BASE_NUMBER, shared_ptr limiter = shared_ptr(), int additionalinfo = 0); + void giveArtBonus(ArtifactID aid, Bonus::BonusType type, int val, int subtype, shared_ptr propagator, int additionalinfo = 0); + void giveArtBonus(ArtifactID aid, Bonus *bonus); + + void erasePickedArt(ArtifactID id); }; struct DLL_LINKAGE ArtSlotInfo @@ -263,13 +268,13 @@ struct DLL_LINKAGE ArtSlotInfo }; class DLL_LINKAGE CArtifactSet -{ -public: - std::vector artifactsInBackpack; //hero's artifacts from bag - std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 - - ArtSlotInfo &retreiveNewArtSlot(ArtifactPosition slot); - void setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art, bool locked); +{ +public: + std::vector artifactsInBackpack; //hero's artifacts from bag + std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 + + ArtSlotInfo &retreiveNewArtSlot(ArtifactPosition slot); + void setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art, bool locked); void eraseArtSlot(ArtifactPosition slot); const ArtSlotInfo *getSlot(ArtifactPosition pos) const; diff --git a/lib/CBattleCallback.cpp b/lib/CBattleCallback.cpp index 2b7e0771f..ff3635300 100644 --- a/lib/CBattleCallback.cpp +++ b/lib/CBattleCallback.cpp @@ -1052,7 +1052,7 @@ std::pair CBattleInfoCallback::battleEstimateDamage(const BattleAtta { BattleStackAttacked bsa; bsa.damageAmount = ret.*pairElems[i]; - bai.defender->prepareAttacked(bsa, bai.defenderCount); + bai.defender->prepareAttacked(bsa, gs->getRandomGenerator(), bai.defenderCount); auto retaliationAttack = bai.reverse(); retaliationAttack.attackerCount = bsa.newAmount; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index b5a1de42e..dfba235b8 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -1054,14 +1054,14 @@ CCreatureHandler::~CCreatureHandler() creature.dellNull(); } -CreatureID CCreatureHandler::pickRandomMonster(const std::function &randGen, int tier) const +CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const { int r = 0; if(tier == -1) //pick any allowed creature { do { - r = vstd::pickRandomElementOf(creatures, randGen)->idNumber; + r = (*RandomGeneratorUtil::nextItem(creatures, rand))->idNumber; } while (VLC->creh->creatures[r] && VLC->creh->creatures[r]->special); // find first "not special" creature } else @@ -1082,7 +1082,7 @@ CreatureID CCreatureHandler::pickRandomMonster(const std::function &randG return CreatureID::NONE; } - return vstd::pickRandomElementOf(allowed, randGen); + return *RandomGeneratorUtil::nextItem(allowed, rand); } assert (r >= 0); //should always be, but it crashed return CreatureID(r); diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index afa95486c..60cd56ee6 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -7,6 +7,7 @@ #include "GameConstants.h" #include "JsonNode.h" #include "IHandlerBase.h" +#include "CRandomGenerator.h" /* * CCreatureHandler.h, part of VCMI engine @@ -186,7 +187,7 @@ public: std::vector > > skillRequirements; // first - Bonus, second - which two skills are needed to use it void deserializationFix(); - CreatureID pickRandomMonster(const std::function &randGen = nullptr, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any + CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any void addBonusForTier(int tier, Bonus *b); //tier must be <1-7> void addBonusForAllCreatures(Bonus *b); diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index f1023a4b9..bf6d1d052 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -28,7 +28,6 @@ #include "CStopWatch.h" #include "mapping/CMapEditManager.h" -DLL_LINKAGE std::minstd_rand ran; class CGObjectInstance; #ifdef min @@ -363,7 +362,8 @@ static CGObjectInstance * createObject(Obj id, int subid, int3 pos, PlayerColor return nobj; } -CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native, PlayerColor player, const CTown *town, std::map > &available, const CHeroClass *bannedClass /*= nullptr*/) const +CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native, PlayerColor player, const CTown *town, + std::map > &available, CRandomGenerator & rand, const CHeroClass * bannedClass /*= nullptr*/) const { CGHeroInstance *ret = nullptr; @@ -388,11 +388,11 @@ CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native, PlayerColor pl if(!pool.size()) { logGlobal->errorStream() << "Cannot pick native hero for " << player << ". Picking any..."; - return pickHeroFor(false, player, town, available); + return pickHeroFor(false, player, town, available, rand); } else { - ret = pool[rand()%pool.size()]; + ret = *RandomGeneratorUtil::nextItem(pool, rand); } } else @@ -414,7 +414,7 @@ CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native, PlayerColor pl return nullptr; } - r = rand()%sum; + r = rand.nextInt(sum - 1); for (auto & elem : pool) { r -= elem->type->heroClass->selectionProbability[town->faction->index]; @@ -449,7 +449,7 @@ CGameState::CampaignHeroReplacement::CampaignHeroReplacement(CGHeroInstance * he } -int CGameState::pickNextHeroType(PlayerColor owner) const +int CGameState::pickNextHeroType(PlayerColor owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); if(ps.hero >= 0 && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero @@ -460,7 +460,7 @@ int CGameState::pickNextHeroType(PlayerColor owner) const return pickUnusedHeroTypeRandomly(owner); } -int CGameState::pickUnusedHeroTypeRandomly(PlayerColor owner) const +int CGameState::pickUnusedHeroTypeRandomly(PlayerColor owner) { //list of available heroes for this faction and others std::vector factionHeroes, otherHeroes; @@ -475,12 +475,16 @@ int CGameState::pickUnusedHeroTypeRandomly(PlayerColor owner) const } // select random hero native to "our" faction - if (!factionHeroes.empty()) - return factionHeroes.at(ran() % factionHeroes.size()).getNum(); + if(!factionHeroes.empty()) + { + return RandomGeneratorUtil::nextItem(factionHeroes, rand)->getNum(); + } logGlobal->warnStream() << "Cannot find free hero of appropriate faction for player " << owner << " - trying to get first available..."; if(!otherHeroes.empty()) - return otherHeroes.at(ran() % otherHeroes.size()).getNum(); + { + return RandomGeneratorUtil::nextItem(otherHeroes, rand)->getNum(); + } logGlobal->errorStream() << "No free allowed heroes!"; auto notAllowedHeroesButStillBetterThanCrash = getUnusedAllowedHeroes(true); @@ -497,29 +501,29 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) switch(obj->ID) { case Obj::RANDOM_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->getRandomArt (CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC)); + return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC)); case Obj::RANDOM_TREASURE_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->getRandomArt (CArtifact::ART_TREASURE)); + return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)); case Obj::RANDOM_MINOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->getRandomArt (CArtifact::ART_MINOR)); + return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)); case Obj::RANDOM_MAJOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->getRandomArt (CArtifact::ART_MAJOR)); + return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)); case Obj::RANDOM_RELIC_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->getRandomArt (CArtifact::ART_RELIC)); + return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(rand, CArtifact::ART_RELIC)); case Obj::RANDOM_HERO: return std::make_pair(Obj::HERO, pickNextHeroType(obj->tempOwner)); case Obj::RANDOM_MONSTER: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran))); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand)); case Obj::RANDOM_MONSTER_L1: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 1)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 1)); case Obj::RANDOM_MONSTER_L2: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 2)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 2)); case Obj::RANDOM_MONSTER_L3: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 3)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 3)); case Obj::RANDOM_MONSTER_L4: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 4)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 4)); case Obj::RANDOM_RESOURCE: - return std::make_pair(Obj::RESOURCE,ran()%7); //now it's OH3 style, use %8 for mithril + return std::make_pair(Obj::RESOURCE,rand.nextInt(6)); //now it's OH3 style, use %8 for mithril case Obj::RANDOM_TOWN: { PlayerColor align = PlayerColor((static_cast(obj))->alignment); @@ -539,18 +543,18 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) { do { - f = ran()%VLC->townh->factions.size(); + f = rand.nextInt(VLC->townh->factions.size() - 1); } while (VLC->townh->factions[f]->town == nullptr); // find playable faction } return std::make_pair(Obj::TOWN,f); } case Obj::RANDOM_MONSTER_L5: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 5)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 5)); case Obj::RANDOM_MONSTER_L6: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 6)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 6)); case Obj::RANDOM_MONSTER_L7: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(std::ref(ran), 7)); + return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(rand, 7)); case Obj::RANDOM_DWELLING: case Obj::RANDOM_DWELLING_LVL: case Obj::RANDOM_DWELLING_FACTION: @@ -561,7 +565,7 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) //if castle alignment available if (auto info = dynamic_cast(dwl->info)) { - faction = ran() % VLC->townh->factions.size(); + faction = rand.nextInt(VLC->townh->factions.size() - 1); if (info->asCastle) { for(auto & elem : map->objects) @@ -586,11 +590,11 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) } else { - while((!(info->castles[0]&(1<castles[0]&(1<7) && (info->castles[1]&(1<<(faction-8)))) break; - faction = ran()%GameConstants::F_NUMBER; + faction = rand.nextInt(GameConstants::F_NUMBER - 1); } } } @@ -601,9 +605,13 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) //if level set to range if (auto info = dynamic_cast(dwl->info)) - level = ((info->maxLevel-info->minLevel) ? (ran()%(info->maxLevel-info->minLevel)+info->minLevel) : (info->minLevel)); + { + level = rand.nextInt(info->minLevel, info->maxLevel); + } else // fixed level + { level = obj->subID; + } delete dwl->info; dwl->info = nullptr; @@ -627,9 +635,7 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) if (result.first == Obj::NO_OBJ) { logGlobal->errorStream() << "Error: failed to find creature for dwelling of "<< int(faction) << " of level " << int(level); - auto iter = VLC->objh->cregens.begin(); - std::advance(iter, ran() % VLC->objh->cregens.size() ); - result = std::make_pair(Obj::CREATURE_GENERATOR1, iter->first); + result = std::make_pair(Obj::CREATURE_GENERATOR1, RandomGeneratorUtil::nextItem(VLC->objh->cregens, rand)->first); } return result; @@ -763,7 +769,7 @@ BattleInfo * CGameState::setupBattle(int3 tile, const CArmedInstance *armies[2], void CGameState::init(StartInfo * si) { logGlobal->infoStream() << "\tUsing random seed: "<< si->seedToBeUsed; - ran.seed((boost::int32_t)si->seedToBeUsed); + rand.setSeed(si->seedToBeUsed); scenarioOps = CMemorySerializer::deepCopy(*si).release(); initialOpts = CMemorySerializer::deepCopy(*si).release(); si = nullptr; @@ -810,7 +816,7 @@ void CGameState::init(StartInfo * si) logGlobal->debugStream() << "\tChecking objectives"; map->checkForObjectives(); //needs to be run when all objects are properly placed - int seedAfterInit = ran(); + auto seedAfterInit = rand.nextInt(); logGlobal->infoStream() << "Seed after init is " << seedAfterInit << " (before was " << scenarioOps->seedToBeUsed << ")"; if(scenarioOps->seedPostInit > 0) { @@ -1034,10 +1040,14 @@ void CGameState::initGrailPosition() if(elem && elem->ID == Obj::HOLE) allowedPos -= elem->pos; - if(allowedPos.size()) - map->grailPos = allowedPos[ran() % allowedPos.size()]; + if(!allowedPos.empty()) + { + map->grailPos = *RandomGeneratorUtil::nextItem(allowedPos, rand); + } else + { logGlobal->warnStream() << "Warning: Grail cannot be placed, no appropriate tile found!"; + } } } @@ -1048,7 +1058,7 @@ void CGameState::initRandomFactionsForPlayers() { if(elem.second.castle==-1) { - int randomID = ran() % map->players[elem.first.getNum()].allowedFactions.size(); + auto randomID = rand.nextInt(map->players[elem.first.getNum()].allowedFactions.size() - 1); auto iter = map->players[elem.first.getNum()].allowedFactions.begin(); std::advance(iter, randomID); @@ -1177,7 +1187,7 @@ void CGameState::placeCampaignHeroes() auto unusedHeroTypeIds = getUnusedAllowedHeroes(); if(!unusedHeroTypeIds.empty()) { - heroTypeId = std::next(unusedHeroTypeIds.begin(), ran() % unusedHeroTypeIds.size())->getNum(); + heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, rand)).getNum(); } else { @@ -1680,23 +1690,23 @@ void CGameState::initStartingBonus() { //starting bonus if(scenarioOps->playerInfos[elem.first].bonus==PlayerSettings::RANDOM) - scenarioOps->playerInfos[elem.first].bonus = static_cast(ran()%3); + scenarioOps->playerInfos[elem.first].bonus = static_cast(rand.nextInt(2)); switch(scenarioOps->playerInfos[elem.first].bonus) { case PlayerSettings::GOLD: - elem.second.resources[Res::GOLD] += 500 + (ran()%6)*100; + elem.second.resources[Res::GOLD] += rand.nextInt(500, 1000); break; case PlayerSettings::RESOURCE: { int res = VLC->townh->factions[scenarioOps->playerInfos[elem.first].castle]->town->primaryRes; if(res == Res::WOOD_AND_ORE) { - elem.second.resources[Res::WOOD] += 5 + ran()%6; - elem.second.resources[Res::ORE] += 5 + ran()%6; + elem.second.resources[Res::WOOD] += rand.nextInt(5, 10); + elem.second.resources[Res::ORE] += rand.nextInt(5, 10); } else { - elem.second.resources[res] += 3 + ran()%4; + elem.second.resources[res] += rand.nextInt(3, 6); } break; } @@ -1708,7 +1718,7 @@ void CGameState::initStartingBonus() break; } CArtifact *toGive; - toGive = VLC->arth->artifacts[VLC->arth->getRandomArt (CArtifact::ART_TREASURE)]; + toGive = VLC->arth->artifacts[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]; CGHeroInstance *hero = elem.second.heroes[0]; giveHeroArtifact(hero, toGive->id); @@ -1756,9 +1766,13 @@ void CGameState::initTowns() { CGTownInstance * vti =(elem); if(!vti->town) + { vti->town = VLC->townh->factions[vti->subID]->town; - if (vti->name.length()==0) // if town hasn't name we draw it - vti->name = vti->town->names[ran()%vti->town->names.size()]; + } + if(vti->name.empty()) + { + vti->name = *RandomGeneratorUtil::nextItem(vti->town->names, rand); + } //init buildings if(vstd::contains(vti->builtBuildings, BuildingID::DEFAULT)) //give standard set of buildings @@ -1767,8 +1781,10 @@ void CGameState::initTowns() vti->builtBuildings.insert(BuildingID::VILLAGE_HALL); vti->builtBuildings.insert(BuildingID::TAVERN); vti->builtBuildings.insert(BuildingID::DWELL_FIRST); - if(ran()%2) + if(rand.nextInt(1) == 1) + { vti->builtBuildings.insert(BuildingID::DWELL_LVL_2); + } } //#1444 - remove entries that don't have buildings defined (like some unused extra town hall buildings) @@ -1817,7 +1833,7 @@ void CGameState::initTowns() ev.buildings.insert(BuildingID::HORDE_2); } } - //init spells + //init spells logGlobal->debugStream() << "\t\tTown init spells"; vti->spells.resize(GameConstants::SPELL_LEVELS); @@ -1826,7 +1842,7 @@ void CGameState::initTowns() CSpell *s = vti->obligatorySpells[z].toSpell(); vti->spells[s->level-1].push_back(s->id); vti->possibleSpells -= s->id; - } + } logGlobal->debugStream() << "\t\tTown init spells2"; while(vti->possibleSpells.size()) { @@ -1839,7 +1855,7 @@ void CGameState::initTowns() if (total == 0) // remaining spells have 0 probability break; - int r = ran()%total; + auto r = rand.nextInt(total - 1); for(ui32 ps=0; pspossibleSpells.size();ps++) { r -= vti->possibleSpells[ps].toSpell()->getProbability(vti->subID); @@ -1858,8 +1874,8 @@ void CGameState::initTowns() } vti->possibleSpells.clear(); if(vti->getOwner() != PlayerColor::NEUTRAL) - getPlayer(vti->getOwner())->towns.push_back(vti); - logGlobal->debugStream() << "\t\tTown init spells3"; + getPlayer(vti->getOwner())->towns.push_back(vti); + logGlobal->debugStream() << "\t\tTown init spells3"; } } @@ -1921,7 +1937,7 @@ void CGameState::initVisitingAndGarrisonedHeroes() } } -BFieldType CGameState::battleGetBattlefieldType(int3 tile) const +BFieldType CGameState::battleGetBattlefieldType(int3 tile) { if(tile==int3() && curB) tile = curB->tile; @@ -1971,13 +1987,13 @@ BFieldType CGameState::battleGetBattlefieldType(int3 tile) const switch(t.terType) { case ETerrainType::DIRT: - return BFieldType(rand()%3+3); + return BFieldType(rand.nextInt(3, 5)); case ETerrainType::SAND: return BFieldType::SAND_MESAS; //TODO: coast support case ETerrainType::GRASS: - return BFieldType(rand()%2+6); + return BFieldType(rand.nextInt(6, 7)); case ETerrainType::SNOW: - return BFieldType(rand()%2+10); + return BFieldType(rand.nextInt(10, 11)); case ETerrainType::SWAMP: return BFieldType::SWAMP_TREES; case ETerrainType::ROUGH: @@ -3630,3 +3646,8 @@ std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & vic os << victoryLossCheckResult.messageToSelf; return os; } + +CRandomGenerator & CGameState::getRandomGenerator() +{ + return rand; +} diff --git a/lib/CGameState.h b/lib/CGameState.h index 1a344fd59..721c9c741 100644 --- a/lib/CGameState.h +++ b/lib/CGameState.h @@ -16,7 +16,7 @@ #include "int3.h" #include "CObjectHandler.h" #include "IGameCallback.h" - +#include "CRandomGenerator.h" /* * CGameState.h, part of VCMI engine @@ -399,7 +399,8 @@ public: std::map > heroesPool; //[subID] - heroes available to buy; nullptr if not available std::map pavailable; // [subid] -> which players can recruit hero (binary flags) - CGHeroInstance * pickHeroFor(bool native, PlayerColor player, const CTown *town, std::map > &available, const CHeroClass *bannedClass = nullptr) const; + CGHeroInstance * pickHeroFor(bool native, PlayerColor player, const CTown *town, + std::map > &available, CRandomGenerator & rand, const CHeroClass *bannedClass = nullptr) const; template void serialize(Handler &h, const int version) { @@ -426,7 +427,7 @@ public: void giveHeroArtifact(CGHeroInstance *h, ArtifactID aid); void apply(CPack *pack); - BFieldType battleGetBattlefieldType(int3 tile) const; + BFieldType battleGetBattlefieldType(int3 tile); UpgradeInfo getUpgradeInfo(const CStackInstance &stack); PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2); bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile @@ -453,6 +454,9 @@ public: int getMovementCost(const CGHeroInstance *h, const int3 &src, const int3 &dest, int remainingMovePoints=-1, bool checkLast=true); int getDate(Date::EDateType mode=Date::DAY) const; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + // ----- getters, setters ----- + CRandomGenerator & getRandomGenerator(); + template void serialize(Handler &h, const int version) { h & scenarioOps & initialOpts & currentPlayer & day & map & players & teams & hpool & globalEffects; @@ -519,8 +523,11 @@ private: bool isUsedHero(HeroTypeID hid) const; //looks in heroes and prisons std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns - int pickUnusedHeroTypeRandomly(PlayerColor owner) const; // picks a unused hero type randomly - int pickNextHeroType(PlayerColor owner) const; // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly + int pickUnusedHeroTypeRandomly(PlayerColor owner); // picks a unused hero type randomly + int pickNextHeroType(PlayerColor owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly + + // ---- data ----- + CRandomGenerator rand; friend class CCallback; friend class CClient; diff --git a/lib/CObjectHandler.cpp b/lib/CObjectHandler.cpp index e9a86423a..40c5aa360 100644 --- a/lib/CObjectHandler.cpp +++ b/lib/CObjectHandler.cpp @@ -36,7 +36,6 @@ using namespace boost::assign; std::map > > CGTeleport::objs; std::vector > CGTeleport::gates; IGameCallback * IObjectInterface::cb = nullptr; -extern std::minstd_rand ran; std::map > CGKeys::playerKeyMap; std::map > CGMagi::eyelist; ui8 CGObelisk::obeliskCount; //how many obelisks are on map @@ -798,7 +797,7 @@ void CGHeroInstance::initArmy(IArmyDescriptor *dst /*= nullptr*/) dst = this; int howManyStacks = 0; //how many stacks will hero receives <1 - 3> - int pom = ran()%100; + int pom = cb->gameState()->getRandomGenerator().nextInt(99); int warMachinesGiven = 0; if(pom < 9) @@ -814,8 +813,7 @@ void CGHeroInstance::initArmy(IArmyDescriptor *dst /*= nullptr*/) { auto & stack = type->initialArmy[stackNo]; - int range = stack.maxAmount - stack.minAmount; - int count = ran()%(range+1) + stack.minAmount; + int count = cb->gameState()->getRandomGenerator().nextInt(stack.minAmount, stack.maxAmount); if(stack.creature >= CreatureID::CATAPULT && stack.creature <= CreatureID::ARROW_TOWERS) //war machine @@ -1465,7 +1463,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack) const { InfoWindow iw; - iw.soundID = soundBase::pickup01 + ran() % 7; + iw.soundID = soundBase::pickup01 + cb->gameState()->getRandomGenerator().nextInt(6); iw.player = tempOwner; iw.components.push_back(Component(raisedStack)); @@ -1553,7 +1551,7 @@ EAlignment::EAlignment CGHeroInstance::getAlignment() const void CGHeroInstance::initExp() { - exp=40+ (ran()) % 50; + exp = cb->gameState()->getRandomGenerator().nextInt(40, 89); level = 1; } @@ -1894,7 +1892,7 @@ void CGDwelling::newTurn() const if(ID == Obj::REFUGEE_CAMP) //if it's a refugee camp, we need to pick an available creature { - cb->setObjProperty(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster()); + cb->setObjProperty(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(cb->gameState()->getRandomGenerator())); } bool change = false; @@ -2774,7 +2772,7 @@ void CGVisitableOPH::initObj() { if(ID==Obj::TREE_OF_KNOWLEDGE) { - switch (ran() % 3) + switch (cb->gameState()->getRandomGenerator().nextInt(2)) { case 1: treePrice[Res::GOLD] = 2000; @@ -3292,13 +3290,13 @@ void CGCreature::initObj() character = -4; break; case 1: - character = 1 + ran()%7; + character = cb->gameState()->getRandomGenerator().nextInt(1, 7); break; case 2: - character = 1 + ran()%10; + character = cb->gameState()->getRandomGenerator().nextInt(1, 10); break; case 3: - character = 4 + ran()%7; + character = cb->gameState()->getRandomGenerator().nextInt(4, 10); break; case 4: character = 10; @@ -3308,14 +3306,11 @@ void CGCreature::initObj() stacks[SlotID(0)]->setType(CreatureID(subID)); TQuantity &amount = stacks[SlotID(0)]->count; CCreature &c = *VLC->creh->creatures[subID]; - if(!amount) + if(amount == 0) { - if(c.ammMax == c.ammMin) - amount = c.ammMax; - else - amount = c.ammMin + (ran() % (c.ammMax - c.ammMin)); + amount = cb->gameState()->getRandomGenerator().nextInt(c.ammMin, c.ammMax); - if(!amount) //armies with 0 creatures are illegal + if(amount == 0) //armies with 0 creatures are illegal { logGlobal->warnStream() << "Problem: stack " << nodeName() << " cannot have 0 creatures. Check properties of " << c.nodeName(); amount = 1; @@ -3666,7 +3661,7 @@ void CGMine::initObj() if(subID >= 7) //Abandoned Mine { //set guardians - int howManyTroglodytes = 100 + ran()%100; + int howManyTroglodytes = cb->gameState()->getRandomGenerator().nextInt(100, 199); auto troglodytes = new CStackInstance(CreatureID::TROGLODYTES, howManyTroglodytes); putStack(SlotID(0), troglodytes); @@ -3676,8 +3671,8 @@ void CGMine::initObj() if(tempOwner.getNum() & 1<(i)); - assert(possibleResources.size()); - producedResource = possibleResources[ran()%possibleResources.size()]; + assert(!possibleResources.empty()); + producedResource = *RandomGeneratorUtil::nextItem(possibleResources, cb->gameState()->getRandomGenerator()); tempOwner = PlayerColor::NEUTRAL; hoverName = VLC->generaltexth->mines[7].first + "\n" + VLC->generaltexth->allTexts[202] + " " + troglodytes->getQuantityTXT(false) + " " + troglodytes->type->namePl; } @@ -3761,13 +3756,13 @@ void CGResource::initObj() switch(subID) { case 6: - amount = 500 + (ran()%6)*100; + amount = cb->gameState()->getRandomGenerator().nextInt(500, 1000); break; case 0: case 2: - amount = 6 + (ran()%5); + amount = cb->gameState()->getRandomGenerator().nextInt(6, 10); break; default: - amount = 3 + (ran()%3); + amount = cb->gameState()->getRandomGenerator().nextInt(3, 5); break; } } @@ -4171,12 +4166,12 @@ void CGPickable::initObj() switch(ID) { case Obj::CAMPFIRE: - val2 = (ran()%3) + 4; //4 - 6 + val2 = cb->gameState()->getRandomGenerator().nextInt(4, 6); val1 = val2 * 100; - type = ran()%6; //given resource + type = cb->gameState()->getRandomGenerator().nextInt(5); // given resource break; case Obj::FLOTSAM: - switch(type = ran()%4) + switch(type = cb->gameState()->getRandomGenerator().nextInt(3)) { case 0: val1 = val2 = 0; @@ -4197,7 +4192,7 @@ void CGPickable::initObj() break; case Obj::SEA_CHEST: { - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp < 20) { val1 = 0; @@ -4211,31 +4206,31 @@ void CGPickable::initObj() else { val1 = 1000; - val2 = cb->getRandomArt (CArtifact::ART_TREASURE); + val2 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE); type = 1; } } break; case Obj::SHIPWRECK_SURVIVOR: { - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp < 55) - val1 = cb->getRandomArt (CArtifact::ART_TREASURE); + val1 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE); else if(hlp < 75) - val1 = cb->getRandomArt (CArtifact::ART_MINOR); + val1 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_MINOR); else if(hlp < 95) - val1 = cb->getRandomArt (CArtifact::ART_MAJOR); + val1 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_MAJOR); else - val1 = cb->getRandomArt (CArtifact::ART_RELIC); + val1 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_RELIC); } break; case Obj::TREASURE_CHEST: { - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp >= 95) { type = 1; - val1 = cb->getRandomArt (CArtifact::ART_TREASURE); + val1 = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE); return; } else if (hlp >= 65) @@ -4733,8 +4728,8 @@ void CGSeerHut::setObjToKill() void CGSeerHut::init() { - seerName = VLC->generaltexth->seerNames[ran()%VLC->generaltexth->seerNames.size()]; - quest->textOption = ran() % 3; + seerName = *RandomGeneratorUtil::nextItem(VLC->generaltexth->seerNames, cb->gameState()->getRandomGenerator()); + quest->textOption = cb->gameState()->getRandomGenerator().nextInt(2); } void CGSeerHut::initObj() @@ -5058,7 +5053,7 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGQuestGuard::init() { blockVisit = true; - quest->textOption = (ran() % 3) + 3; //3-5 + quest->textOption = cb->gameState()->getRandomGenerator().nextInt(3, 5); } void CGQuestGuard::completeQuest(const CGHeroInstance *h) const { @@ -5066,7 +5061,7 @@ void CGQuestGuard::completeQuest(const CGHeroInstance *h) const } void CGWitchHut::initObj() { - ability = allowedAbilities[ran()%allowedAbilities.size()]; + ability = *RandomGeneratorUtil::nextItem(allowedAbilities, cb->gameState()->getRandomGenerator()); } void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const @@ -5862,13 +5857,13 @@ void CGShrine::initObj() std::vector possibilities; cb->getAllowedSpells (possibilities, level); - if(!possibilities.size()) + if(possibilities.empty()) { logGlobal->errorStream() << "Error: cannot init shrine, no allowed spells!"; return; } - spell = possibilities[ran() % possibilities.size()]; + spell = *RandomGeneratorUtil::nextItem(possibilities, cb->gameState()->getRandomGenerator()); } } @@ -5889,8 +5884,10 @@ const std::string & CGShrine::getHoverText() const void CGSignBottle::initObj() { //if no text is set than we pick random from the predefined ones - if(!message.size()) - message = VLC->generaltexth->randsign[ran()%VLC->generaltexth->randsign.size()]; + if(message.empty()) + { + message = *RandomGeneratorUtil::nextItem(VLC->generaltexth->randsign, cb->gameState()->getRandomGenerator()); + } if(ID == Obj::OCEAN_BOTTLE) { @@ -5930,10 +5927,9 @@ void CGScholar::onHeroVisit( const CGHeroInstance * h ) const ))) //hero doesn't have a spellbook or already knows the spell or doesn't have Wisdom { type = PRIM_SKILL; - bid = ran() % GameConstants::PRIMARY_SKILLS; + bid = cb->gameState()->getRandomGenerator().nextInt(GameConstants::PRIMARY_SKILLS - 1); } - InfoWindow iw; iw.soundID = soundBase::gazebo; iw.player = h->getOwner(); @@ -5971,20 +5967,20 @@ void CGScholar::initObj() blockVisit = true; if(bonusType == RANDOM) { - bonusType = static_cast(ran()%3); + bonusType = static_cast(cb->gameState()->getRandomGenerator().nextInt(2)); switch(bonusType) { case PRIM_SKILL: - bonusID = ran() % GameConstants::PRIMARY_SKILLS; + bonusID = cb->gameState()->getRandomGenerator().nextInt(GameConstants::PRIMARY_SKILLS -1); break; case SECONDARY_SKILL: - bonusID = ran() % GameConstants::SKILL_QUANTITY; + bonusID = cb->gameState()->getRandomGenerator().nextInt(GameConstants::SKILL_QUANTITY -1); break; case SPELL: std::vector possibilities; for (int i = 1; i < 6; ++i) cb->getAllowedSpells (possibilities, i); - bonusID = possibilities[ran() % possibilities.size()]; + bonusID = *RandomGeneratorUtil::nextItem(possibilities, cb->gameState()->getRandomGenerator()); break; } } @@ -6124,11 +6120,11 @@ void CGOnceVisitable::initObj() case Obj::CORPSE: { blockVisit = true; - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp < 20) { artOrRes = 1; - bonusType = cb->getRandomArt (CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR); } else { @@ -6140,8 +6136,8 @@ void CGOnceVisitable::initObj() case Obj::LEAN_TO: { artOrRes = 2; - bonusType = ran()%6; //any basic resource without gold - bonusVal = ran()%4 + 1; + bonusType = cb->gameState()->getRandomGenerator().nextInt(5); //any basic resource without gold + bonusVal = cb->gameState()->getRandomGenerator().nextInt(1, 4); } break; @@ -6149,21 +6145,21 @@ void CGOnceVisitable::initObj() { artOrRes = 1; - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp < 30) - bonusType = cb->getRandomArt (CArtifact::ART_TREASURE); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE); else if(hlp < 80) - bonusType = cb->getRandomArt (CArtifact::ART_MINOR); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_MINOR); else if(hlp < 95) - bonusType = cb->getRandomArt (CArtifact::ART_MAJOR); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_MAJOR); else - bonusType = cb->getRandomArt (CArtifact::ART_RELIC); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_RELIC); } break; case Obj::WAGON: { - int hlp = ran()%100; + int hlp = cb->gameState()->getRandomGenerator().nextInt(99); if(hlp < 10) { @@ -6172,13 +6168,13 @@ void CGOnceVisitable::initObj() else if(hlp < 50) //minor or treasure art { artOrRes = 1; - bonusType = cb->getRandomArt (CArtifact::ART_TREASURE | CArtifact::ART_MINOR); + bonusType = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR); } else //2 - 5 of non-gold resource { artOrRes = 2; - bonusType = ran()%6; - bonusVal = ran()%4 + 2; + bonusType = cb->gameState()->getRandomGenerator().nextInt(5); + bonusVal = cb->gameState()->getRandomGenerator().nextInt(2, 5); } } break; @@ -6250,7 +6246,7 @@ void CBank::reset(ui16 var1) //prevents desync void CBank::initialize() const { - cb->setObjProperty (id, ObjProperty::BANK_RESET, ran()); //synchronous reset + cb->setObjProperty(id, ObjProperty::BANK_RESET, cb->gameState()->getRandomGenerator().nextInt()); //synchronous reset for (ui8 i = 0; i <= 3; i++) { @@ -6266,12 +6262,12 @@ void CBank::initialize() const default: assert(0); continue; } - int artID = cb->getArtSync(ran(), artClass, true); + int artID = VLC->arth->pickRandomArtifact(cb->gameState()->getRandomGenerator(), artClass); cb->setObjProperty(id, ObjProperty::BANK_ADD_ARTIFACT, artID); } } - cb->setObjProperty (id, ObjProperty::BANK_INIT_ARMY, ran()); //get army + cb->setObjProperty(id, ObjProperty::BANK_INIT_ARMY, cb->gameState()->getRandomGenerator().nextInt()); //get army } void CBank::setPropertyDer (ui8 what, ui32 val) /// random values are passed as arguments and processed identically on all clients @@ -6578,13 +6574,13 @@ void CGPyramid::initObj() if (available.size()) { bc = VLC->objh->banksInfo[21].front(); //TODO: remove hardcoded value? - spell = (available[ran()%available.size()]); + spell = *RandomGeneratorUtil::nextItem(available, cb->gameState()->getRandomGenerator()); } else { logGlobal->errorStream() <<"No spells available for Pyramid! Object set to empty."; } - setPropertyDer (ObjProperty::BANK_INIT_ARMY,ran()); //set guards at game start + setPropertyDer(ObjProperty::BANK_INIT_ARMY, cb->gameState()->getRandomGenerator().nextInt()); //set guards at game start } const std::string & CGPyramid::getHoverText() const { @@ -7556,21 +7552,27 @@ void CGBlackMarket::newTurn() const void CGUniversity::initObj() { - std::vector toChoose; - for (int i=0; iisAllowed(2,i)) + std::vector toChoose; + for(int i = 0; i < GameConstants::SKILL_QUANTITY; ++i) + { + if(cb->isAllowed(2, i)) + { toChoose.push_back(i); - if (toChoose.size() < 4) + } + } + if(toChoose.size() < 4) { logGlobal->warnStream()<<"Warning: less then 4 available skills was found by University initializer!"; return; } - for (int i=0; i<4; i++)//get 4 skills + // get 4 skills + for(int i = 0; i < 4; ++i) { - int skillPos = ran()%toChoose.size(); - skills.push_back(toChoose[skillPos]);//move it to selected - toChoose.erase(toChoose.begin()+skillPos);//remove from list + // move randomly one skill to selected and remove from list + auto it = RandomGeneratorUtil::nextItem(toChoose, cb->gameState()->getRandomGenerator()); + skills.push_back(*it); + toChoose.erase(it); } } diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 4396f603e..adf75a48c 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -20,44 +20,92 @@ typedef std::function TRand; /// The random generator randomly generates integers and real numbers("doubles") between /// a given range. This is a header only class and mainly a wrapper for /// convenient usage of the standard random API. -class CRandomGenerator +class CRandomGenerator : boost::noncopyable { public: /// Seeds the generator with the current time by default. CRandomGenerator() { - gen.seed((unsigned long)std::time(nullptr)); + rand.seed(static_cast(std::time(nullptr))); } - void seed(int value) + void setSeed(int seed) { - gen.seed(value); + rand.seed(seed); } /// Generate several integer numbers within the same range. - /// e.g.: auto a = gen.getRangeI(0,10); a(); a(); a(); - TRandI getRangeI(int lower, int upper) + /// e.g.: auto a = gen.getIntRange(0,10); a(); a(); a(); + /// requires: lower <= upper + TRandI getIntRange(int lower, int upper) { - return boost::bind(TIntDist(lower, upper), boost::ref(gen)); + return boost::bind(TIntDist(lower, upper), boost::ref(rand)); } - int getInteger(int lower, int upper) + /// Generates an integer between 0 and upper. + /// requires: 0 <= upper + int nextInt(int upper) { - return getRangeI(lower, upper)(); + return getIntRange(0, upper)(); + } + + /// requires: lower <= upper + int nextInt(int lower, int upper) + { + return getIntRange(lower, upper)(); } + /// Generates an integer between 0 and the maximum value it can hold. + int nextInt() + { + return TIntDist()(rand); + } + /// Generate several double/real numbers within the same range. - /// e.g.: auto a = gen.getRangeI(0,10); a(); a(); a(); - TRand getRange(double lower, double upper) + /// e.g.: auto a = gen.getDoubleRange(4.5,10.2); a(); a(); a(); + /// requires: lower <= upper + TRand getDoubleRange(double lower, double upper) { - return boost::bind(TRealDist(lower, upper), boost::ref(gen)); + return boost::bind(TRealDist(lower, upper), boost::ref(rand)); } - double getDouble(double lower, double upper) + /// Generates a double between 0 and upper. + /// requires: 0 <= upper + double nextDouble(double upper) { - return getRange(lower, upper)(); + return getDoubleRange(0, upper)(); + } + + /// requires: lower <= upper + double nextDouble(double lower, double upper) + { + return getDoubleRange(lower, upper)(); + } + + /// Generates a double between 0.0 and 1.0. + double nextDouble() + { + return TRealDist()(rand); } private: - TGenerator gen; + TGenerator rand; }; + +namespace RandomGeneratorUtil +{ + /// Gets an iterator to an element of a nonempty container randomly. Undefined behaviour if container is empty. + template + auto nextItem(const Container & container, CRandomGenerator & rand) -> decltype(std::begin(container)) + { + assert(!container.empty()); + return std::next(container.begin(), rand.nextInt(container.size() - 1)); + } + + template + auto nextItem(Container & container, CRandomGenerator & rand) -> decltype(std::begin(container)) + { + assert(!container.empty()); + return std::next(container.begin(), rand.nextInt(container.size() - 1)); + } +} diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 3692c97cc..97d25c516 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -1,3 +1,13 @@ +/* + * IGameCallback.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + #include "StdInc.h" #include "IGameCallback.h" @@ -20,24 +30,12 @@ #include "Connection.h" -/* - * IGameCallback.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ - //TODO make clean #define ERROR_SILENT_RET_VAL_IF(cond, txt, retVal) do {if(cond){return retVal;}} while(0) #define ERROR_VERBOSE_OR_NOT_RET_VAL_IF(cond, verbose, txt, retVal) do {if(cond){if(verbose)logGlobal->errorStream() << BOOST_CURRENT_FUNCTION << ": " << txt; return retVal;}} while(0) #define ERROR_RET_IF(cond, txt) do {if(cond){logGlobal->errorStream() << BOOST_CURRENT_FUNCTION << ": " << txt; return;}} while(0) #define ERROR_RET_VAL_IF(cond, txt, retVal) do {if(cond){logGlobal->errorStream() << BOOST_CURRENT_FUNCTION << ": " << txt; return retVal;}} while(0) -extern std::minstd_rand ran; - CGameState * CPrivilagedInfoCallback::gameState () { return gs; @@ -182,21 +180,11 @@ bool CGameInfoCallback::isAllowed( int type, int id ) void CPrivilagedInfoCallback::pickAllowedArtsSet(std::vector &out) { for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->artifacts[getRandomArt(CArtifact::ART_TREASURE)]); + out.push_back(VLC->arth->artifacts[VLC->arth->pickRandomArtifact(gameState()->getRandomGenerator(), CArtifact::ART_TREASURE)]); for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->artifacts[getRandomArt(CArtifact::ART_MINOR)]); + out.push_back(VLC->arth->artifacts[VLC->arth->pickRandomArtifact(gameState()->getRandomGenerator(), CArtifact::ART_MINOR)]); - out.push_back(VLC->arth->artifacts[getRandomArt(CArtifact::ART_MAJOR)]); -} - -ArtifactID CPrivilagedInfoCallback::getRandomArt (int flags) -{ - return VLC->arth->getRandomArt(flags); -} - -ArtifactID CPrivilagedInfoCallback::getArtSync (ui32 rand, int flags, bool erasePicked) -{ - return VLC->arth->getArtSync (rand, flags, erasePicked); + out.push_back(VLC->arth->artifacts[VLC->arth->pickRandomArtifact(gameState()->getRandomGenerator(), CArtifact::ART_MAJOR)]); } void CPrivilagedInfoCallback::getAllowedSpells(std::vector &out, ui16 level) diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 6964594fa..164e11d30 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -167,12 +167,10 @@ public: class DLL_LINKAGE CPrivilagedInfoCallback : public CGameInfoCallback { public: - CGameState * gameState (); + CGameState * gameState(); void getFreeTiles (std::vector &tiles) const; //used for random spawns void getTilesInRange(std::unordered_set &tiles, int3 pos, int radious, boost::optional player = boost::optional(), int mode=0) const; //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only unrevealed void getAllTiles (std::unordered_set &tiles, boost::optional player = boost::optional(), int level=-1, int surface=0) const; //returns all tiles on given level (-1 - both levels, otherwise number of level); surface: 0 - land and water, 1 - only land, 2 - only water - ArtifactID getRandomArt (int flags); - ArtifactID getArtSync (ui32 rand, int flags, bool erasePicked); //synchronous void pickAllowedArtsSet(std::vector &out); //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant void getAllowedSpells(std::vector &out, ui16 level); diff --git a/lib/mapping/CMapEditManager.cpp b/lib/mapping/CMapEditManager.cpp index fa248fbf7..ef24689cd 100644 --- a/lib/mapping/CMapEditManager.cpp +++ b/lib/mapping/CMapEditManager.cpp @@ -598,7 +598,7 @@ void CDrawTerrainOperation::updateTerrainViews() auto & tile = map->getTile(pos); if(!pattern.diffImages) { - tile.terView = gen->getInteger(mapping.first, mapping.second); + tile.terView = gen->nextInt(mapping.first, mapping.second); tile.extTileFlags = valRslt.flip; } else @@ -606,7 +606,7 @@ void CDrawTerrainOperation::updateTerrainViews() const int framesPerRot = (mapping.second - mapping.first + 1) / pattern.rotationTypesCount; int flip = (pattern.rotationTypesCount == 2 && valRslt.flip == 2) ? 1 : valRslt.flip; int firstFrame = mapping.first + flip * framesPerRot; - tile.terView = gen->getInteger(firstFrame, firstFrame + framesPerRot - 1); + tile.terView = gen->nextInt(firstFrame, firstFrame + framesPerRot - 1); tile.extTileFlags = 0; } } diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 2382918bf..089cba908 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -177,15 +177,15 @@ const std::map & CMapGenOptions::getAvailableTempla void CMapGenOptions::finalize() { - CRandomGenerator gen; - finalize(gen); + CRandomGenerator rand; + finalize(rand); } -void CMapGenOptions::finalize(CRandomGenerator & gen) +void CMapGenOptions::finalize(CRandomGenerator & rand) { if(!mapTemplate) { - mapTemplate = getPossibleTemplate(gen); + mapTemplate = getPossibleTemplate(rand); assert(mapTemplate); } @@ -194,22 +194,22 @@ void CMapGenOptions::finalize(CRandomGenerator & gen) auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers())); assert(!possiblePlayers.empty()); - playerCount = *std::next(possiblePlayers.begin(), gen.getInteger(0, possiblePlayers.size() - 1)); + playerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); updatePlayers(); } if(teamCount == RANDOM_SIZE) { - teamCount = gen.getInteger(0, playerCount - 1); + teamCount = rand.nextInt(playerCount - 1); } if(compOnlyPlayerCount == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getCpuPlayers().getNumbers(); - compOnlyPlayerCount = *std::next(possiblePlayers.begin(), gen.getInteger(0, possiblePlayers.size() - 1)); + compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); updateCompOnlyPlayers(); } if(compOnlyTeamCount == RANDOM_SIZE) { - compOnlyTeamCount = gen.getInteger(0, std::max(compOnlyPlayerCount - 1, 0)); + compOnlyTeamCount = rand.nextInt(std::max(compOnlyPlayerCount - 1, 0)); } // 1 team isn't allowed @@ -220,11 +220,11 @@ void CMapGenOptions::finalize(CRandomGenerator & gen) if(waterContent == EWaterContent::RANDOM) { - waterContent = static_cast(gen.getInteger(0, 2)); + waterContent = static_cast(rand.nextInt(2)); } if(monsterStrength == EMonsterStrength::RANDOM) { - monsterStrength = static_cast(gen.getInteger(0, 2)); + monsterStrength = static_cast(rand.nextInt(2)); } } @@ -313,7 +313,7 @@ bool CMapGenOptions::checkOptions() const } } -const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & gen) const +const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand) const { // Find potential templates const auto & tpls = getAvailableTemplates(); @@ -363,7 +363,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & gen) } else { - return *std::next(potentialTpls.begin(), gen.getInteger(0, potentialTpls.size() - 1)); + return *RandomGeneratorUtil::nextItem(potentialTpls, rand); } } diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index c564606f4..2c23eb181 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -143,7 +143,7 @@ public: /// a random number generator by keeping the options in a valid state. Check options should return true, otherwise /// this function fails. void finalize(); - void finalize(CRandomGenerator & gen); + void finalize(CRandomGenerator & rand); /// Returns false if there is no template available which fits to the currently selected options. bool checkOptions() const; @@ -156,7 +156,7 @@ private: PlayerColor getNextPlayerColor() const; void updateCompOnlyPlayers(); void updatePlayers(); - const CRmgTemplate * getPossibleTemplate(CRandomGenerator & gen) const; + const CRmgTemplate * getPossibleTemplate(CRandomGenerator & rand) const; si32 width, height; bool hasTwoLevels; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index d6d780cd0..810e53fe2 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -35,9 +35,9 @@ CMapGenerator::~CMapGenerator() std::unique_ptr CMapGenerator::generate(CMapGenOptions * mapGenOptions, int randomSeed /*= std::time(nullptr)*/) { this->randomSeed = randomSeed; - gen.seed(this->randomSeed); + rand.setSeed(this->randomSeed); this->mapGenOptions = mapGenOptions; - this->mapGenOptions->finalize(gen); + this->mapGenOptions->finalize(rand); map = make_unique(); editManager = map->getEditManager(); @@ -125,7 +125,8 @@ void CMapGenerator::addPlayerInfo() { player.canHumanPlay = true; } - auto itTeam = std::next(teamNumbers[j].begin(), gen.getInteger(0, teamNumbers[j].size() - 1)); + + auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); player.team = TeamID(*itTeam); teamNumbers[j].erase(itTeam); map->players[pSettings.getColor().getNum()] = player; @@ -138,9 +139,9 @@ void CMapGenerator::addPlayerInfo() void CMapGenerator::genTerrain() { map->initTerrain(); - editManager->clearTerrain(&gen); + editManager->clearTerrain(&rand); editManager->getTerrainSelection().selectRange(MapRect(int3(4, 4, 0), 24, 30)); - editManager->drawTerrain(ETerrainType::GRASS, &gen); + editManager->drawTerrain(ETerrainType::GRASS, &rand); } void CMapGenerator::genTowns() @@ -158,7 +159,11 @@ void CMapGenerator::genTowns() auto town = new CGTownInstance(); town->ID = Obj::TOWN; int townId = mapGenOptions->getPlayersSettings().find(PlayerColor(i))->second.getStartingTown(); - if(townId == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) townId = gen.getInteger(0, 8); // Default towns + if(townId == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) + { + // select default towns + townId = rand.nextInt(8); + } town->subID = townId; town->tempOwner = owner; town->appearance = VLC->dobjinfo->pickCandidates(town->ID, town->subID, map->getTile(townPos[side]).terType).front(); diff --git a/lib/rmg/CMapGenerator.h b/lib/rmg/CMapGenerator.h index eee921b32..b88bd08e8 100644 --- a/lib/rmg/CMapGenerator.h +++ b/lib/rmg/CMapGenerator.h @@ -36,7 +36,7 @@ private: CMapGenOptions * mapGenOptions; std::unique_ptr map; - CRandomGenerator gen; + CRandomGenerator rand; int randomSeed; CMapEditManager * editManager; }; diff --git a/rpm/vcmi.spec b/rpm/vcmi.spec index abcdb0435..a7263bce7 100644 --- a/rpm/vcmi.spec +++ b/rpm/vcmi.spec @@ -7,7 +7,7 @@ Group: Amusements/Games # The source for this package was pulled from upstream's vcs. Use the # following commands to generate the tarball: -# svn export -r HEAD https://vcmi.svn.sourceforge.net/svnroot/vcmi/tags/0.95 vcmi-0.9.5-1 +# svn export -r HEAD https://svn.code.sf.net/p/vcmi/code/tags/0.95 vcmi-0.9.5-1 # tar -cJf vcmi-0.9.5-1.tar.xz vcmi-0.9.5-1 Source: vcmi-0.9.5-1.tar.xz diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8d12b247e..bcc118bb5 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1,6409 +1,6413 @@ -#include "StdInc.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CFileInfo.h" -#include "../lib/int3.h" -#include "../lib/mapping/CCampaignHandler.h" -#include "../lib/StartInfo.h" -#include "../lib/CModHandler.h" -#include "../lib/CArtHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/CDefObjInfoHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CObjectHandler.h" -#include "../lib/CSpellHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CCreatureHandler.h" -#include "../lib/CGameState.h" -#include "../lib/BattleState.h" -#include "../lib/CondSh.h" -#include "../lib/NetPacks.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/mapping/CMap.h" -#include "../lib/VCMIDirs.h" -#include "../lib/ScopeGuard.h" -#include "../client/CSoundBase.h" -#include "CGameHandler.h" -#include "CVCMIServer.h" -#include "../lib/CCreatureSet.h" -#include "../lib/CThreadHelper.h" -#include "../lib/GameConstants.h" -#include "../lib/registerTypes/RegisterTypes.h" - -/* - * CGameHandler.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ - - -#ifndef _MSC_VER -#include -#endif -extern bool end2; -#ifdef min -#undef min -#endif -#ifdef max -#undef max -#endif - -#define COMPLAIN_RET_IF(cond, txt) do {if(cond){complain(txt); return;}} while(0) -#define COMPLAIN_RET_FALSE_IF(cond, txt) do {if(cond){complain(txt); return false;}} while(0) -#define COMPLAIN_RET(txt) {complain(txt); return false;} -#define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} -#define NEW_ROUND BattleNextRound bnr;\ - bnr.round = gs->curB->round + 1;\ - sendAndApply(&bnr); - -CondSh battleMadeAction; -CondSh battleResult(nullptr); -template class CApplyOnGH; - -class CBaseForGHApply -{ -public: - virtual bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const =0; - virtual ~CBaseForGHApply(){} - template static CBaseForGHApply *getApplier(const U * t=nullptr) - { - return new CApplyOnGH; - } -}; - -template class CApplyOnGH : public CBaseForGHApply -{ -public: - bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const - { - T *ptr = static_cast(pack); - ptr->c = c; - ptr->player = player; - return ptr->applyGh(gh); - } -}; - -template <> -class CApplyOnGH : public CBaseForGHApply -{ -public: - bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const - { - logGlobal->errorStream() << "Cannot apply on GH plain CPack!"; - assert(0); - return false; - } -}; - -static CApplier *applier = nullptr; - -CMP_stack cmpst ; - -static inline double distance(int3 a, int3 b) -{ - return std::sqrt( (double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y) ); -} -static void giveExp(BattleResult &r) -{ - r.exp[0] = 0; - r.exp[1] = 0; - for(auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) - { - r.exp[r.winner] += VLC->creh->creatures.at(i->first)->valOfBonuses(Bonus::STACK_HEALTH) * i->second; - } -} - -PlayerStatus PlayerStatuses::operator[](PlayerColor player) -{ - boost::unique_lock l(mx); - if(players.find(player) != players.end()) - { - return players.at(player); - } - else - { - throw std::runtime_error("No such player!"); - } -} -void PlayerStatuses::addPlayer(PlayerColor player) -{ - boost::unique_lock l(mx); - players[player]; -} - -bool PlayerStatuses::checkFlag(PlayerColor player, bool PlayerStatus::*flag) -{ - boost::unique_lock l(mx); - if(players.find(player) != players.end()) - { - return players[player].*flag; - } - else - { - throw std::runtime_error("No such player!"); - } -} - -void PlayerStatuses::setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val) -{ - boost::unique_lock l(mx); - if(players.find(player) != players.end()) - { - players[player].*flag = val; - } - else - { - throw std::runtime_error("No such player!"); - } - cv.notify_all(); -} - -template -void callWith(std::vector args, std::function fun, ui32 which) -{ - fun(args[which]); -} - -void CGameHandler::levelUpHero(const CGHeroInstance * hero, SecondarySkill skill) -{ - changeSecSkill(hero, skill, 1, 0); - expGiven(hero); -} - -void CGameHandler::levelUpHero(const CGHeroInstance * hero) -{ - // required exp for at least 1 lvl-up hasn't been reached - if(!hero->gainsLevel()) - { - return; - } - - //give prim skill - logGlobal->traceStream() << hero->name <<" got level "<level; - int r = rand()%100, pom=0, x=0; - - auto & skillChances = (hero->level>9) ? hero->type->heroClass->primarySkillLowLevel : hero->type->heroClass->primarySkillHighLevel; - - for(;xtraceStream() << "The hero gets the primary skill with the no. " << x << " with a probability of " << r << "%."; - SetPrimSkill sps; - sps.id = hero->id; - sps.which = static_cast(x); - sps.abs = false; - sps.val = 1; - sendAndApply(&sps); - - HeroLevelUp hlu; - hlu.hero = hero; - hlu.primskill = static_cast(x); - hlu.level = hero->level+1; - hlu.skills = hero->levelUpProposedSkills(); - - if(hlu.skills.size() == 0) - { - sendAndApply(&hlu); - levelUpHero(hero); - } - else if(hlu.skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - sendAndApply(&hlu); - auto rng = [&]() mutable -> ui32 - { - return hero->skillsInfo.distribution(); //must be determined - }; - levelUpHero(hero, vstd::pickRandomElementOf (hlu.skills, rng)); - } - else if(hlu.skills.size() > 1) - { - auto levelUpQuery = make_shared(hlu); - hlu.queryID = levelUpQuery->queryID; - queries.addQuery(levelUpQuery); - sendAndApply(&hlu); - //level up will be called on query reply - } -} - -void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) -{ - SetCommanderProperty scp; - - auto hero = dynamic_cast(c->armyObj); - if (hero) - scp.heroid = hero->id; - else - { - complain ("Commander is not led by hero!"); - return; - } - - scp.accumulatedBonus.subtype = 0; - scp.accumulatedBonus.additionalInfo = 0; - scp.accumulatedBonus.duration = Bonus::PERMANENT; - scp.accumulatedBonus.turnsRemain = 0; - scp.accumulatedBonus.source = Bonus::COMMANDER; - scp.accumulatedBonus.valType = Bonus::BASE_NUMBER; - if (skill <= ECommander::SPELL_POWER) - { - scp.which = SetCommanderProperty::BONUS; - - auto difference = [](std::vector< std::vector > skillLevels, std::vector secondarySkills, int skill)->int - { - int s = std::min (skill, (int)ECommander::SPELL_POWER); //spell power level controls also casts and resistance - return skillLevels.at(skill).at(secondarySkills.at(s)) - (secondarySkills.at(s) ? skillLevels.at(skill).at(secondarySkills.at(s)-1) : 0); - }; - - switch (skill) - { - case ECommander::ATTACK: - scp.accumulatedBonus.type = Bonus::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::ATTACK; - break; - case ECommander::DEFENSE: - scp.accumulatedBonus.type = Bonus::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::DEFENSE; - break; - case ECommander::HEALTH: - scp.accumulatedBonus.type = Bonus::STACK_HEALTH; - scp.accumulatedBonus.valType = Bonus::PERCENT_TO_BASE; - break; - case ECommander::DAMAGE: - scp.accumulatedBonus.type = Bonus::CREATURE_DAMAGE; - scp.accumulatedBonus.subtype = 0; - scp.accumulatedBonus.valType = Bonus::PERCENT_TO_BASE; - break; - case ECommander::SPEED: - scp.accumulatedBonus.type = Bonus::STACKS_SPEED; - break; - case ECommander::SPELL_POWER: - scp.accumulatedBonus.type = Bonus::MAGIC_RESISTANCE; - scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::RESISTANCE); - sendAndApply (&scp); //additional pack - scp.accumulatedBonus.type = Bonus::CREATURE_SPELL_POWER; - scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::SPELL_POWER) * 100; //like hero with spellpower = ability level - sendAndApply (&scp); //additional pack - scp.accumulatedBonus.type = Bonus::CASTS; - scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::CASTS); - sendAndApply (&scp); //additional pack - scp.accumulatedBonus.type = Bonus::CREATURE_ENCHANT_POWER; //send normally - break; - } - - scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, skill); - sendAndApply (&scp); - - scp.which = SetCommanderProperty::SECONDARY_SKILL; - scp.additionalInfo = skill; - scp.amount = c->secondarySkills.at(skill) + 1; - sendAndApply (&scp); - } - else if (skill >= 100) - { - scp.which = SetCommanderProperty::SPECIAL_SKILL; - scp.accumulatedBonus = *VLC->creh->skillRequirements.at(skill-100).first; - scp.additionalInfo = skill; //unnormalized - sendAndApply (&scp); - } - expGiven(hero); -} - -void CGameHandler::levelUpCommander(const CCommanderInstance * c) -{ - if (!c->gainsLevel()) - { - return; - } - CommanderLevelUp clu; - - auto hero = dynamic_cast(c->armyObj); - if (hero) - clu.hero = hero; - else - { - complain ("Commander is not led by hero!"); - return; - } - - //picking sec. skills for choice - - for (int i = 0; i <= ECommander::SPELL_POWER; ++i) - { - if (c->secondarySkills.at(i) < ECommander::MAX_SKILL_LEVEL) - clu.skills.push_back(i); - } - int i = 100; - for (auto specialSkill : VLC->creh->skillRequirements) - { - if (c->secondarySkills.at(specialSkill.second.first) == ECommander::MAX_SKILL_LEVEL - && c->secondarySkills.at(specialSkill.second.second) == ECommander::MAX_SKILL_LEVEL - && !vstd::contains (c->specialSKills, i)) - clu.skills.push_back (i); - ++i; - } - int skillAmount = clu.skills.size(); - - if(!skillAmount) - { - sendAndApply(&clu); - levelUpCommander(c); - } - else if(skillAmount == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - sendAndApply(&clu); - levelUpCommander(c, vstd::pickRandomElementOf (clu.skills, rand)); - } - else if(skillAmount > 1) //apply and ask for secondary skill - { - auto commanderLevelUp = make_shared(clu); - clu.queryID = commanderLevelUp->queryID; - queries.addQuery(commanderLevelUp); - sendAndApply(&clu); - } -} - -void CGameHandler::expGiven(const CGHeroInstance *hero) -{ - if(hero->gainsLevel()) - levelUpHero(hero); - else if(hero->commander && hero->commander->gainsLevel()) - levelUpCommander(hero->commander); - - //if(hero->commander && hero->level > hero->commander->level && hero->commander->gainsLevel()) -// levelUpCommander(hero->commander); -// else -// levelUpHero(hero); -} - -void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs) -{ - if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached - { - if (gs->map->levelLimit != 0) - { - TExpType expLimit = VLC->heroh->reqExp(gs->map->levelLimit); - TExpType resultingExp = abs ? val : hero->exp + val; - if (resultingExp > expLimit) - { - // set given experience to max possible, but don't decrease if hero already over top - abs = true; - val = std::max(expLimit, hero->exp); - - InfoWindow iw; - iw.player = hero->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 1); //can gain no more XP - iw.text.addReplacement(hero->name); - sendAndApply(&iw); - } - } - } - - SetPrimSkill sps; - sps.id = hero->id; - sps.which = which; - sps.abs = abs; - sps.val = val; - sendAndApply(&sps); - - //only for exp - hero may level up - if (which == PrimarySkill::EXPERIENCE) - { - if(hero->commander && hero->commander->alive) - { - //FIXME: trim experience according to map limit? - SetCommanderProperty scp; - scp.heroid = hero->id; - scp.which = SetCommanderProperty::EXPERIENCE; - scp.amount = val; - sendAndApply (&scp); - CBonusSystemNode::treeHasChanged(); - } - - expGiven(hero); - } -} - -void CGameHandler::changeSecSkill( const CGHeroInstance * hero, SecondarySkill which, int val, bool abs/*=false*/ ) -{ - SetSecSkill sss; - sss.id = hero->id; - sss.which = which; - sss.val = val; - sss.abs = abs; - sendAndApply(&sss); - - if(which == SecondarySkill::WISDOM) - { - if(hero && hero->visitedTown) - giveSpells(hero->visitedTown, hero); - } -} - -void CGameHandler::endBattle(int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) -{ - LOG_TRACE(logGlobal); - - //Fill BattleResult structure with exp info - giveExp(*battleResult.data); - - if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped - { - if (hero1) - battleResult.data->exp[1] += 500; - if (hero2) - battleResult.data->exp[0] += 500; - } - - if (hero1) - battleResult.data->exp[0] = hero1->calculateXp(battleResult.data->exp[0]);//scholar skill - if (hero2) - battleResult.data->exp[1] = hero2->calculateXp(battleResult.data->exp[1]); - - const CArmedInstance *bEndArmy1 = gs->curB->sides.at(0).armyObject; - const CArmedInstance *bEndArmy2 = gs->curB->sides.at(1).armyObject; - const BattleResult::EResult result = battleResult.get()->result; - - auto findBattleQuery = [this] () -> shared_ptr - { - for(auto &q : queries.allQueries()) - { - if(auto bq = std::dynamic_pointer_cast(q)) - if(bq->bi == gs->curB) - return bq; - } - return shared_ptr(); - }; - - auto battleQuery = findBattleQuery(); - if(!battleQuery) - { - logGlobal->errorStream() << "Cannot find battle query!"; - if(gs->initialOpts->mode == StartInfo::DUEL) - { - battleQuery = make_shared(gs->curB); - } - } - if(battleQuery != queries.topQuery(gs->curB->sides[0].color)) - complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " although in battle has no battle query at the top!"); - - battleQuery->result = *battleResult.data; - - //Check how many battle queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? boost::count(queries.allQueries(), battleQuery) : 0; - finishingBattle = make_unique(battleQuery, gs->initialOpts->mode == StartInfo::DUEL, queriedPlayers); - - - CasualtiesAfterBattle cab1(bEndArmy1, gs->curB), cab2(bEndArmy2, gs->curB); //calculate casualties before deleting battle - - if(finishingBattle->duel) - { - duelFinished(); - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - return; - } - - - ChangeSpells cs; //for Eagle Eye - - if(finishingBattle->winnerHero) - { - if(int eagleEyeLevel = finishingBattle->winnerHero->getSecSkillLevel(SecondarySkill::EAGLE_EYE)) - { - int maxLevel = eagleEyeLevel + 1; - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::EAGLE_EYE); - for(const CSpell *sp : gs->curB->sides.at(!battleResult.data->winner).usedSpellsHistory) - if(sp->level <= maxLevel && !vstd::contains(finishingBattle->winnerHero->spells, sp->id) && rand() % 100 < eagleEyeChance) - cs.spells.insert(sp->id); - } - } - - - std::vector arts; //display them in window - - if (result == BattleResult::NORMAL && finishingBattle->winnerHero) - { - if (finishingBattle->loserHero) - { - auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; //TODO: wrap it into a function, somehow (boost::variant -_-) - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation (finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig() && art->artType->id != 0) // don't move war machines or locked arts (spellbook) - { - arts.push_back (art->artType->id); - ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); - sendAndApply(&ma); - } - } - while (!finishingBattle->loserHero->artifactsInBackpack.empty()) - { - //we assume that no big artifacts can be found - MoveArtifact ma; - ma.src = ArtifactLocation (finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); - arts.push_back (art->artType->id); - ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); - sendAndApply(&ma); - } - if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? - { - artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation (finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - arts.push_back (art->artType->id); - ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); - sendAndApply(&ma); - } - } - } - } - for (auto armySlot : gs->curB->battleGetArmyObject(!battleResult.data->winner)->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation (armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - arts.push_back (art->artType->id); - ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); - sendAndApply(&ma); - } - } - } - } - - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - - if (arts.size()) //display loot - { - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - - iw.text.addTxt (MetaString::GENERAL_TXT, 30); //You have captured enemy artifact - - for (auto id : arts) //TODO; separate function to display loot for various ojects? - { - iw.components.push_back (Component (Component::ARTIFACT, id, 0, 0)); - if(iw.components.size() >= 14) - { - sendAndApply(&iw); - iw.components.clear(); - iw.text.addTxt (MetaString::GENERAL_TXT, 30); //repeat - } - } - if (iw.components.size()) - { - sendAndApply(&iw); - } - } - //Eagle Eye secondary skill handling - if(!cs.spells.empty()) - { - cs.learn = 1; - cs.hid = finishingBattle->winnerHero->id; - - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.addReplacement(finishingBattle->winnerHero->name); - - std::ostringstream names; - for(int i = 0; i < cs.spells.size(); i++) - { - names << "%s"; - if(i < cs.spells.size() - 2) - names << ", "; - else if(i < cs.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.addReplacement(names.str()); - - auto it = cs.spells.begin(); - for(int i = 0; i < cs.spells.size(); i++, it++) - { - iw.text.addReplacement(MetaString::SPELL_NAME, it->toEnum()); - if(i == cs.spells.size() - 2) //we just added pre-last name - iw.text.addReplacement(MetaString::GENERAL_TXT, 141); // " and " - iw.components.push_back(Component(Component::SPELL, *it, 0, 0)); - } - - sendAndApply(&iw); - sendAndApply(&cs); - } - - cab1.takeFromArmy(this); - cab2.takeFromArmy(this); //take casualties after battle is deleted - - //if one hero has lost we will erase him - if(battleResult.data->winner!=0 && hero1) - { - RemoveObject ro(hero1->id); - sendAndApply(&ro); - } - if(battleResult.data->winner!=1 && hero2) - { - RemoveObject ro(hero2->id); - sendAndApply(&ro); - } - - //give exp - if (battleResult.data->exp[0] && hero1 && battleResult.get()->winner == 0) - changePrimSkill(hero1, PrimarySkill::EXPERIENCE, battleResult.data->exp[0]); - else if (battleResult.data->exp[1] && hero2 && battleResult.get()->winner == 1) - changePrimSkill(hero2, PrimarySkill::EXPERIENCE, battleResult.data->exp[1]); - - queries.popIfTop(battleQuery); - - //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) -} - -void CGameHandler::battleAfterLevelUp( const BattleResult &result ) -{ - LOG_TRACE(logGlobal); - - - finishingBattle->remainingBattleQueriesCount--; - logGlobal->traceStream() << "Decremented queries count to " << finishingBattle->remainingBattleQueriesCount; - - if(finishingBattle->remainingBattleQueriesCount > 0) - //Battle results will be handled when all battle queries are closed - return; - - //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible - // but the battle consequences are applied after final player is unblocked. Hard to abuse... - // Still, it looks like a hole. - - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); - // Give raised units to winner and show dialog, if any were raised, - // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); - - if (necroSlot != SlotID()) - { - finishingBattle->winnerHero->showNecromancyDialog(raisedStack); - addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); - } - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - sendAndApply(&resultsApplied); - - setBattle(nullptr); - - if(visitObjectAfterVictory && result.winner==0) - { - logGlobal->traceStream() << "post-victory visit"; - visitObjectOnTile(*getTile(finishingBattle->winnerHero->getPosition()), finishingBattle->winnerHero); - } - visitObjectAfterVictory = false; - - //handle victory/loss of engaged players - std::set playerColors = boost::assign::list_of(finishingBattle->loser)(finishingBattle->victor); - checkVictoryLossConditions(playerColors); - - if(result.result == BattleResult::SURRENDER || result.result == BattleResult::ESCAPE) //loser has escaped or surrendered - { - SetAvailableHeroes sah; - sah.player = finishingBattle->loser; - sah.hid[0] = finishingBattle->loserHero->subID; - if(result.result == BattleResult::ESCAPE) //retreat - { - sah.army[0].clear(); - sah.army[0].setCreature(SlotID(0), finishingBattle->loserHero->type->initialArmy.at(0).creature, 1); - } - - if(const CGHeroInstance *another = getPlayer(finishingBattle->loser)->availableHeroes.at(1)) - sah.hid[1] = another->subID; - else - sah.hid[1] = -1; - - sendAndApply(&sah); - } -} - -void CGameHandler::prepareAttack(BattleAttack &bat, const CStack *att, const CStack *def, int distance, int targetHex) -{ - bat.bsa.clear(); - bat.stackAttacking = att->ID; - const int attackerLuck = att->LuckVal(); - - auto sideHeroBlocksLuck = [](const SideInBattle &side){ return NBonus::hasOfType(side.hero, Bonus::BLOCK_LUCK); }; - - if(!vstd::contains_if(gs->curB->sides, sideHeroBlocksLuck)) - { - if(attackerLuck > 0 && rand()%24 < attackerLuck) - { - bat.flags |= BattleAttack::LUCKY; - } - if (VLC->modh->settings.data["hardcodedFeatures"]["NEGATIVE_LUCK"].Bool()) // negative luck enabled - { - if (attackerLuck < 0 && rand()%24 < abs(attackerLuck)) - { - bat.flags |= BattleAttack::UNLUCKY; - } - } - } - - if (rand()%100 < att->valOfBonuses(Bonus::DOUBLE_DAMAGE_CHANCE)) - { - bat.flags |= BattleAttack::DEATH_BLOW; - } - - if(att->getCreature()->idNumber == CreatureID::BALLISTA) - { - static const int artilleryLvlToChance[] = {0, 50, 75, 100}; - const CGHeroInstance * owner = gs->curB->getHero(att->owner); - int chance = artilleryLvlToChance[owner->getSecSkillLevel(SecondarySkill::ARTILLERY)]; - if(chance > rand() % 100) - { - bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; - } - } - // only primary target - applyBattleEffects(bat, att, def, distance, false); - - if (!bat.shot()) //multiple-hex attack - only in meele - { - std::set attackedCreatures = gs->curB->getAttackedCreatures(att, targetHex); //creatures other than primary target - - for(const CStack * stack : attackedCreatures) - { - if (stack != def) //do not hit same stack twice - { - applyBattleEffects(bat, att, stack, distance, true); - } - } - } - - const Bonus * bonus = att->getBonusLocalFirst(Selector::type(Bonus::SPELL_LIKE_ATTACK)); - if (bonus && (bat.shot())) //TODO: make it work in meele? - { - bat.bsa.front().flags |= BattleStackAttacked::EFFECT; - bat.bsa.front().effect = VLC->spellh->objects.at(bonus->subtype)->mainEffectAnim; //hopefully it does not interfere with any other effect? - - std::set attackedCreatures = gs->curB->getAffectedCreatures(SpellID(bonus->subtype).toSpell(), bonus->val, att->owner, targetHex); - //TODO: get exact attacked hex for defender - - for(const CStack * stack : attackedCreatures) - { - if (stack != def) //do not hit same stack twice - { - applyBattleEffects(bat, att, stack, distance, true); - } - } - } -} -void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, const CStack *def, int distance, bool secondary) //helper function for prepareAttack -{ - BattleStackAttacked bsa; - if (secondary) - bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities - bsa.attackerID = att->ID; - bsa.stackAttacked = def->ID; - bsa.damageAmount = gs->curB->calculateDmg(att, def, gs->curB->battleGetOwner(att), gs->curB->battleGetOwner(def), bat.shot(), distance, bat.lucky(), bat.unlucky(), bat.deathBlow(), bat.ballistaDoubleDmg()); - def->prepareAttacked(bsa); //calculate casualties - - //life drain handling - if (att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving()) - { - StacksHealedOrResurrected shi; - shi.lifeDrain = (ui8)true; - shi.tentHealing = (ui8)false; - shi.drainedFrom = def->ID; - - StacksHealedOrResurrected::HealInfo hi; - hi.stackID = att->ID; - hi.healedHP = std::min (bsa.damageAmount * att->valOfBonuses (Bonus::LIFE_DRAIN) / 100, - att->MaxHealth() - att->firstHPleft + att->MaxHealth() * (att->baseAmount - att->count) ); - hi.lowLevelResurrection = false; - shi.healedStacks.push_back(hi); - - if (hi.healedHP > 0) - { - bsa.healedStacks.push_back(shi); - } - } - bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated - - //fire shield handling - if (!bat.shot() && def->hasBonusOfType(Bonus::FIRE_SHIELD) && !att->hasBonusOfType (Bonus::FIRE_IMMUNITY) && !bsa.killed() ) - { - BattleStackAttacked bsa2; - bsa2.stackAttacked = att->ID; //invert - bsa2.attackerID = def->ID; - bsa2.flags |= BattleStackAttacked::EFFECT; //FIXME: play anmation upon efreet and not attacker - bsa2.effect = 11; - - bsa2.damageAmount = (bsa.damageAmount * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense - att->prepareAttacked(bsa2); - bat.bsa.push_back(bsa2); - } -} -void CGameHandler::handleConnection(std::set players, CConnection &c) -{ - setThreadName("CGameHandler::handleConnection"); - srand(time(nullptr)); - - try - { - while(1)//server should never shut connection first //was: while(!end2) - { - CPack *pack = nullptr; - PlayerColor player = PlayerColor::NEUTRAL; - si32 requestID = -999; - int packType = 0; - - { - boost::unique_lock lock(*c.rmx); - c >> player >> requestID >> pack; //get the package - - if(!pack) - { - logGlobal ->errorStream() << boost::format("Received a null package marked as request %d from player %d") % requestID % player; - } - - packType = typeList.getTypeID(pack); //get the id of type - - logGlobal->traceStream() << boost::format("Received client message (request %d by player %d) of type with ID=%d (%s).\n") - % requestID % player.getNum() % packType % typeid(*pack).name(); - } - - //prepare struct informing that action was applied - auto sendPackageResponse = [&](bool succesfullyApplied) - { - PackageApplied applied; - applied.player = player; - applied.result = succesfullyApplied; - applied.packType = packType; - applied.requestID = requestID; - boost::unique_lock lock(*c.wmx); - c << &applied; - }; - - CBaseForGHApply *apply = applier->apps[packType]; //and appropriae applier object - if(isBlockedByQueries(pack, player)) - { - sendPackageResponse(false); - } - else if(apply) - { - const bool result = apply->applyOnGH(this,&c,pack, player); - if(!result) - complain("Got false in applying... that request must have been fishy!"); - logGlobal->traceStream() << "Message successfully applied (result=" << result << ")!"; - sendPackageResponse(true); - } - else - { - logGlobal->errorStream() << "Message cannot be applied, cannot find applier (unregistered type)!"; - sendPackageResponse(false); - } - - vstd::clear_pointer(pack); - } - } - catch(boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection - { - assert(!c.connected); //make sure that connection has been marked as broken - logGlobal->errorStream() << e.what(); - end2 = true; - } - HANDLE_EXCEPTION(end2 = true); - - logGlobal->errorStream() << "Ended handling connection"; -} - -int CGameHandler::moveStack(int stack, BattleHex dest) -{ - int ret = 0; - - const CStack *curStack = gs->curB->battleGetStackByID(stack), - *stackAtEnd = gs->curB->battleGetStackByPos(dest); - - assert(curStack); - assert(dest < GameConstants::BFIELD_SIZE); - - if (gs->curB->tacticDistance) - { - assert(gs->curB->isInTacticRange(dest)); - } - - if(curStack->position == dest) - return 0; - - //initing necessary tables - auto accessibility = getAccesibility(curStack); - - //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) - if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) - { - if(curStack->attackerOwned) - { - if(accessibility.accessible(dest+1, curStack)) - dest += BattleHex::RIGHT; - } - else - { - if(accessibility.accessible(dest-1, curStack)) - dest += BattleHex::LEFT; - } - } - - if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) - { - complain("Given destination is not accessible!"); - return 0; - } - - std::pair< std::vector, int > path = gs->curB->getPath(curStack->position, dest, curStack); - - ret = path.second; - - int creSpeed = gs->curB->tacticDistance ? GameConstants::BFIELD_SIZE : curStack->Speed(); - - if(curStack->hasBonusOfType(Bonus::FLYING)) - { - if(path.second <= creSpeed && path.first.size() > 0) - { - //inform clients about move - BattleStackMoved sm; - sm.stack = curStack->ID; - std::vector tiles; - tiles.push_back(path.first[0]); - sm.tilesToMove = tiles; - sm.distance = path.second; - sm.teleporting = false; - sendAndApply(&sm); - } - } - else //for non-flying creatures - { - // send one package with the creature path information - - shared_ptr obstacle; //obstacle that interrupted movement - std::vector tiles; - int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); - int v = path.first.size()-1; - -startWalking: - for(; v >= tilesToMove; --v) - { - BattleHex hex = path.first[v]; - tiles.push_back(hex); - - if((obstacle = battleGetObstacleOnPos(hex, false))) - { - //we walked onto something, so we finalize this portion of stack movement check into obstacle - break; - } - } - - if (tiles.size() > 0) - { - //commit movement - BattleStackMoved sm; - sm.stack = curStack->ID; - sm.distance = path.second; - sm.teleporting = false; - sm.tilesToMove = tiles; - sendAndApply(&sm); - } - - //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end - if(obstacle && curStack->position != dest) - { - handleDamageFromObstacle(*obstacle, curStack); - - //if stack didn't die in explosion, continue movement - if(!obstacle->stopsMovement() && curStack->alive()) - { - obstacle.reset(); - tiles.clear(); - v--; - goto startWalking; //TODO it's so evil - } - } - } - - //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - if(curStack->alive()) - { - if(auto theLastObstacle = battleGetObstacleOnPos(curStack->position, false)) - { - handleDamageFromObstacle(*theLastObstacle, curStack); - } - } - return ret; -} - -CGameHandler::CGameHandler(void) -{ - QID = 1; - //gs = nullptr; - IObjectInterface::cb = this; - applier = new CApplier; - registerTypesServerPacks(*applier); - visitObjectAfterVictory = false; - queries.gh = this; -} - -CGameHandler::~CGameHandler(void) -{ - delete applier; - applier = nullptr; - delete gs; -} - -void CGameHandler::init(StartInfo *si) -{ - //extern DLL_LINKAGE std::minstd_rand ran; - if(!si->seedToBeUsed) - si->seedToBeUsed = std::time(nullptr); - - gs = new CGameState(); - logGlobal->infoStream() << "Gamestate created!"; - gs->init(si); - logGlobal->infoStream() << "Gamestate initialized!"; - - for(auto & elem : gs->players) - states.addPlayer(elem.first); -} - -static bool evntCmp(const CMapEvent &a, const CMapEvent &b) -{ - return a.earlierThan(b); -} - -void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=false, bool clear = false) -{// bool forced = true - if creature should be replaced, if false - only if no creature was set - const PlayerState *p = gs->getPlayer(town->tempOwner); - if(!p) - { - logGlobal->warnStream() << "There is no player owner of town " << town->name << " at " << town->pos; - return; - } - - if (forced || town->creatures.at(GameConstants::CREATURES_PER_TOWN).second.empty())//we need to change creature - { - SetAvailableCreatures ssi; - ssi.tid = town->id; - ssi.creatures = town->creatures; - ssi.creatures[GameConstants::CREATURES_PER_TOWN].second.clear();//remove old one - - const std::vector > &dwellings = p->dwellings; - if (dwellings.empty())//no dwellings - just remove - { - sendAndApply(&ssi); - return; - } - - ui32 dwellpos = rand()%dwellings.size();//take random dwelling - ui32 creapos = rand()%dwellings.at(dwellpos)->creatures.size();//for multi-creature dwellings like Golem Factory - CreatureID creature = dwellings.at(dwellpos)->creatures.at(creapos).second[0]; - - if (clear) - ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = std::max((ui32)1, (VLC->creh->creatures.at(creature)->growth)/2); - else - ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = VLC->creh->creatures.at(creature)->growth; - ssi.creatures[GameConstants::CREATURES_PER_TOWN].second.push_back(creature); - sendAndApply(&ssi); - } -} - -void CGameHandler::newTurn() -{ - logGlobal->traceStream() << "Turn " << gs->day+1; - NewTurn n; - n.specialWeek = NewTurn::NO_ACTION; - n.creatureid = CreatureID::NONE; - n.day = gs->day + 1; - - bool firstTurn = !getDate(Date::DAY); - bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched - bool newMonth = getDate(Date::DAY_OF_MONTH) == 28; - - std::map hadGold;//starting gold - for buildings like dwarven treasury - srand(time(nullptr)); - - if (firstTurn) - { - for (auto obj : gs->map->objects) - { - if (obj && obj->ID == Obj::PRISON) //give imprisoned hero 0 exp to level him up. easiest to do at this point - { - changePrimSkill (getHero(obj->id), PrimarySkill::EXPERIENCE, 0); - } - } - } - - if (newWeek && !firstTurn) - { - n.specialWeek = NewTurn::NORMAL; - bool deityOfFireBuilt = false; - for(const CGTownInstance *t : gs->map->towns) - { - if(t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) - { - deityOfFireBuilt = true; - break; - } - } - - if(deityOfFireBuilt) - { - n.specialWeek = NewTurn::DEITYOFFIRE; - n.creatureid = CreatureID::IMP; - } - else - { - int monthType = rand()%100; - if(newMonth) //new month - { - if (monthType < 40) //double growth - { - n.specialWeek = NewTurn::DOUBLE_GROWTH; - if (VLC->modh->settings.ALL_CREATURES_GET_DOUBLE_MONTHS) - { - std::pair newMonster(54, VLC->creh->pickRandomMonster([]{ return rand(); })); - n.creatureid = newMonster.second; - } - else if(VLC->creh->doubledCreatures.size()) - { - const std::vector doubledCreatures (VLC->creh->doubledCreatures.begin(), VLC->creh->doubledCreatures.end()); - n.creatureid = vstd::pickRandomElementOf(doubledCreatures, []{ return rand(); }); - } - else - { - complain("Cannot find creature that can be spawned!"); - n.specialWeek = NewTurn::NORMAL; - } - } - else if (monthType < 50) - n.specialWeek = NewTurn::PLAGUE; - } - else //it's a week, but not full month - { - if (monthType < 25) - { - n.specialWeek = NewTurn::BONUS_GROWTH; //+5 - std::pair newMonster(54, VLC->creh->pickRandomMonster([]{ return rand(); })); - //TODO do not pick neutrals - n.creatureid = newMonster.second; - } - } - } - } - - std::map > pool = gs->hpool.heroesPool; - - for (auto & elem : gs->players) - { - if(elem.first == PlayerColor::NEUTRAL) - continue; - else if(elem.first >= PlayerColor::PLAYER_LIMIT) - assert(0); //illegal player number! - - std::pair playerGold(elem.first, elem.second.resources.at(Res::GOLD)); - hadGold.insert(playerGold); - - if(newWeek) //new heroes in tavern - { - SetAvailableHeroes sah; - sah.player = elem.first; - - //pick heroes and their armies - CHeroClass *banned = nullptr; - for (int j = 0; j < GameConstants::AVAILABLE_HEROES_PER_PLAYER; j++) - { - if(CGHeroInstance *h = gs->hpool.pickHeroFor(j == 0, elem.first, getNativeTown(elem.first), pool, banned)) //first hero - native if possible, second hero -> any other class - { - sah.hid[j] = h->subID; - h->initArmy(&sah.army[j]); - banned = h->type->heroClass; - } - else - sah.hid[j] = -1; - } - - sendAndApply(&sah); - } - - n.res[elem.first] = elem.second.resources; - - for(CGHeroInstance *h : (elem).second.heroes) - { - if(h->visitedTown) - giveSpells(h->visitedTown, h); - - NewTurn::Hero hth; - hth.id = h->id; - hth.move = h->maxMovePoints(gs->map->getTile(h->getPosition(false)).terType != ETerrainType::WATER); - - if(h->visitedTown && h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) //if hero starts turn in town with mage guild - hth.mana = std::max(h->mana, h->manaLimit()); //restore all mana - else - hth.mana = std::max((si32)(0), std::max(h->mana, std::min((si32)(h->mana + h->manaRegain()), h->manaLimit()))); - - n.heroes.insert(hth); - - if(!firstTurn) //not first day - { - n.res[elem.first][Res::GOLD] += h->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ESTATES)); //estates - - for (int k = 0; k < GameConstants::RESOURCE_QUANTITY; k++) - { - n.res[elem.first][k] += h->valOfBonuses(Bonus::GENERATE_RESOURCE, k); - } - } - } - } - for(CGTownInstance *t : gs->map->towns) - { - PlayerColor player = t->tempOwner; - handleTownEvents(t, n); - if(newWeek) //first day of week - { - if(t->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) - setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning - - if(!firstTurn) - if (t->hasBuilt(BuildingID::TREASURY, ETownType::RAMPART) && player < PlayerColor::PLAYER_LIMIT) - n.res[player][Res::GOLD] += hadGold.at(player)/10; //give 10% of starting gold - - if (!vstd::contains(n.cres, t->id)) - { - n.cres[t->id].tid = t->id; - n.cres[t->id].creatures = t->creatures; - } - auto & sac = n.cres.at(t->id); - - for (int k=0; k < GameConstants::CREATURES_PER_TOWN; k++) //creature growths - { - if (!t->creatures.at(k).second.empty()) // there are creatures at this level - { - ui32 &availableCount = sac.creatures.at(k).first; - const CCreature *cre = VLC->creh->creatures.at(t->creatures.at(k).second.back()); - - if (n.specialWeek == NewTurn::PLAGUE) - availableCount = t->creatures.at(k).first / 2; //halve their number, no growth - else - { - if(firstTurn) //first day of game: use only basic growths - availableCount = cre->growth; - else - availableCount += t->creatureGrowth(k); - - //Deity of fire week - upgrade both imps and upgrades - if (n.specialWeek == NewTurn::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, n.creatureid)) - availableCount += 15; - - if( cre->idNumber == n.creatureid ) //bonus week, effect applies only to identical creatures - { - if(n.specialWeek == NewTurn::DOUBLE_GROWTH) - availableCount *= 2; - else if(n.specialWeek == NewTurn::BONUS_GROWTH) - availableCount += 5; - } - } - } - } - } - if(!firstTurn && player < PlayerColor::PLAYER_LIMIT)//not the first day and town not neutral - { - if(t->hasBuilt(BuildingID::RESOURCE_SILO)) //there is resource silo - { - if(t->town->primaryRes == Res::WOOD_AND_ORE) //we'll give wood and ore - { - n.res[player][Res::WOOD] ++; - n.res[player][Res::ORE] ++; - } - else - { - n.res[player][t->town->primaryRes] ++; - } - } - - n.res[player][Res::GOLD] += t->dailyIncome(); - } - if(t->hasBuilt(BuildingID::GRAIL, ETownType::TOWER)) - { - // Skyship, probably easier to handle same as Veil of darkness - //do it every new day after veils apply - if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player - { - FoWChange fw; - fw.mode = 1; - fw.player = player; - // find all hidden tiles - const auto & fow = gs->getPlayerTeam(player)->fogOfWarMap; - for (size_t i=0; ihasBonusOfType (Bonus::DARKNESS)) - { - t->hideTiles(t->getOwner(), t->getBonusLocalFirst(Selector::type(Bonus::DARKNESS))->val); - } - //unhiding what shouldn't be hidden? //that's handled in netpacks client - } - - if(newMonth) - { - SetAvailableArtifacts saa; - saa.id = -1; - pickAllowedArtsSet(saa.arts); - sendAndApply(&saa); - } - sendAndApply(&n); - - if(newWeek) - { - //spawn wandering monsters - if (newMonth && (n.specialWeek == NewTurn::DOUBLE_GROWTH || n.specialWeek == NewTurn::DEITYOFFIRE)) - { - spawnWanderingMonsters(n.creatureid); - } - - //new week info popup - if(!firstTurn) - { - InfoWindow iw; - switch (n.specialWeek) - { - case NewTurn::DOUBLE_GROWTH: - iw.text.addTxt(MetaString::ARRAY_TXT, 131); - iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); - iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); - break; - case NewTurn::PLAGUE: - iw.text.addTxt(MetaString::ARRAY_TXT, 132); - break; - case NewTurn::BONUS_GROWTH: - iw.text.addTxt(MetaString::ARRAY_TXT, 134); - iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); - iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); - break; - case NewTurn::DEITYOFFIRE: - iw.text.addTxt(MetaString::ARRAY_TXT, 135); - iw.text.addReplacement(MetaString::CRE_SING_NAMES, 42); //%s imp - iw.text.addReplacement(MetaString::CRE_SING_NAMES, 42); //%s imp - iw.text.addReplacement2(15); //%+d 15 - iw.text.addReplacement(MetaString::CRE_SING_NAMES, 43); //%s familiar - iw.text.addReplacement2(15); //%+d 15 - break; - default: - if (newMonth) - { - iw.text.addTxt(MetaString::ARRAY_TXT, (130)); - iw.text.addReplacement(MetaString::ARRAY_TXT, 32 + rand()%10); - } - else - { - iw.text.addTxt(MetaString::ARRAY_TXT, (133)); - iw.text.addReplacement(MetaString::ARRAY_TXT, 43 + rand()%15); - } - } - for (auto & elem : gs->players) - { - iw.player = elem.first; - sendAndApply(&iw); - } - } - } - - logGlobal->traceStream() << "Info about turn " << n.day << "has been sent!"; - handleTimeEvents(); - //call objects - for(auto & elem : gs->map->objects) - { - if(elem) - elem->newTurn(); - } - - synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that -} -void CGameHandler::run(bool resume) -{ - LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); - - using namespace boost::posix_time; - for(CConnection *cc : conns) - { - if(!resume) - { - (*cc) << gs->initialOpts; // gs->scenarioOps - } - - std::set players; - (*cc) >> players; //how many players will be handled at that client - - std::stringstream sbuffer; - sbuffer << "Connection " << cc->connectionID << " will handle " << players.size() << " player: "; - for(PlayerColor color : players) - { - sbuffer << color << " "; - { - boost::unique_lock lock(gsm); - connections[color] = cc; - } - } - logGlobal->infoStream() << sbuffer.str(); - - cc->addStdVecItems(gs); - cc->enableStackSendingByID(); - cc->disableSmartPointerSerialization(); - } - - for(auto & elem : conns) - { - std::set pom; - for(auto j = connections.cbegin(); j!=connections.cend();j++) - if(j->second == elem) - pom.insert(j->first); - - boost::thread(boost::bind(&CGameHandler::handleConnection,this,pom,boost::ref(*elem))); - } - - if(gs->scenarioOps->mode == StartInfo::DUEL) - { - runBattle(); - end2 = true; - - - while(conns.size() && (*conns.begin())->isOpen()) - boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket - - return; - } - - auto playerTurnOrder = generatePlayerTurnOrder(); - - while(!end2) - { - if(!resume) newTurn(); - - std::list::iterator it; - if(resume) - { - it = std::find(playerTurnOrder.begin(), playerTurnOrder.end(), gs->currentPlayer); - } - else - { - it = playerTurnOrder.begin(); - } - - resume = false; - for(; it != playerTurnOrder.end(); it++) - { - auto playerColor = *it; - if(gs->players[playerColor].status == EPlayerStatus::INGAME) - { - states.setFlag(playerColor, &PlayerStatus::makingTurn, true); - - YourTurn yt; - yt.player = playerColor; - applyAndSend(&yt); - - checkVictoryLossConditionsForAll(); - - //wait till turn is done - boost::unique_lock lock(states.mx); - while(states.players.at(playerColor).makingTurn && !end2) - { - static time_duration p = milliseconds(200); - states.cv.timed_wait(lock,p); - } - } - } - } - while(conns.size() && (*conns.begin())->isOpen()) - boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket -} - -std::list CGameHandler::generatePlayerTurnOrder() const -{ - // Generate player turn order - std::list playerTurnOrder; - - for(const auto & player : gs->players) // add human players first - { - if(player.second.human) - playerTurnOrder.push_back(player.first); - } - for(const auto & player : gs->players) // then add non-human players - { - if(!player.second.human) - playerTurnOrder.push_back(player.first); - } - return std::move(playerTurnOrder); -} - -void CGameHandler::setupBattle( int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town ) -{ - battleResult.set(nullptr); - - //send info about battles - BattleStart bs; - bs.info = gs->setupBattle(tile, armies, heroes, creatureBank, town); - sendAndApply(&bs); -} - -void CGameHandler::checkForBattleEnd() -{ - if(auto result = battleIsFinished()) - { - setBattleResult(BattleResult::NORMAL, *result); - } -} - -void CGameHandler::giveSpells( const CGTownInstance *t, const CGHeroInstance *h ) -{ - if(!h->hasSpellbook()) - return; //hero hasn't spellbook - ChangeSpells cs; - cs.hid = h->id; - cs.learn = true; - for(int i=0; imageGuildLevel(),h->getSecSkillLevel(SecondarySkill::WISDOM)+2);i++) - { - if (t->hasBuilt(BuildingID::GRAIL, ETownType::CONFLUX)) //Aurora Borealis - { - std::vector spells; - getAllowedSpells(spells, i); - for (auto & spell : spells) - cs.spells.insert(spell); - } - else - { - for(int j=0; jspellsAtLevel(i+1,true) && jspells.at(i).size(); j++) - { - if(!vstd::contains(h->spells,t->spells.at(i).at(j))) - cs.spells.insert(t->spells.at(i).at(j)); - } - } - } - if(!cs.spells.empty()) - sendAndApply(&cs); -} - -void CGameHandler::setBlockVis(ObjectInstanceID objid, bool bv) -{ - SetObjectProperty sop(objid,2,bv); - sendAndApply(&sop); -} - -bool CGameHandler::removeObject( const CGObjectInstance * obj ) -{ - if(!obj || !getObj(obj->id)) - { - logGlobal->errorStream() << "Something wrong, that object already has been removed or hasn't existed!"; - return false; - } - - RemoveObject ro; - ro.id = obj->id; - sendAndApply(&ro); - - checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) - return true; -} - -void CGameHandler::setAmount(ObjectInstanceID objid, ui32 val) -{ - SetObjectProperty sop(objid,3,val); - sendAndApply(&sop); -} - -bool CGameHandler::moveHero( ObjectInstanceID hid, int3 dst, ui8 teleporting, PlayerColor asker /*= 255*/ ) -{ - const CGHeroInstance *h = getHero(hid); - - if(!h || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer)) //not turn of that hero or player can't simply teleport hero (at least not with this function) - ) - { - logGlobal->errorStream() << "Illegal call to move hero!"; - return false; - } - - logGlobal->traceStream() << "Player " << asker << " wants to move hero "<< hid.getNum() << " from "<< h->pos << " to " << dst; - const int3 hmpos = dst + int3(-1,0,0); - - if(!gs->map->isInTheMap(hmpos)) - { - logGlobal->errorStream() << "Destination tile is outside the map!"; - return false; - } - - const TerrainTile t = *gs->getTile(hmpos); - const int cost = gs->getMovementCost(h, h->getPosition(), hmpos, h->movement); - const int3 guardPos = gs->guardingCreaturePosition(hmpos); - - const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT; - const bool disembarking = h->boat && t.terType != ETerrainType::WATER && !t.blocked; - - //result structure for start - movement failed, no move points used - TryMoveHero tmh; - tmh.id = hid; - tmh.start = h->pos; - tmh.end = dst; - tmh.result = TryMoveHero::FAILED; - tmh.movePoints = h->movement; - - //check if destination tile is available - - //it's a rock or blocked and not visitable tile - //OR hero is on land and dest is water and (there is not present only one object - boat) - if(((t.terType == ETerrainType::ROCK || (t.blocked && !t.visitable && !h->hasBonusOfType(Bonus::FLYING_MOVEMENT) )) - && complain("Cannot move hero, destination tile is blocked!")) - || ((!h->boat && !h->canWalkOnSea() && t.terType == ETerrainType::WATER && (t.visitableObjects.size() < 1 || (t.visitableObjects.back()->ID != Obj::BOAT && t.visitableObjects.back()->ID != Obj::HERO))) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) - && complain("Cannot move hero, destination tile is on water!")) - || ((h->boat && t.terType != ETerrainType::WATER && t.blocked) - && complain("Cannot disembark hero, tile is blocked!")) - || ( (distance(h->pos, dst) >= 1.5 && !teleporting) - && complain("Tiles are not neighboring!")) - || ( (h->inTownGarrison) - && complain("Can not move garrisoned hero!")) - || ((h->movement < cost && dst != h->pos && !teleporting) - && complain("Hero doesn't have any movement points left!")) - /*|| (states.checkFlag(h->tempOwner, &PlayerStatus::engagedIntoBattle) - && complain("Cannot move hero during the battle"))*/) - { - //send info about movement failure - sendAndApply(&tmh); - return false; - } - - //several generic blocks of code - - // should be called if hero changes tile but before applying TryMoveHero package - auto leaveTile = [&]() - { - for(CGObjectInstance *obj : gs->map->getTile(int3(h->pos.x-1, h->pos.y, h->pos.z)).visitableObjects) - { - obj->onHeroLeave(h); - } - this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadious(), h->tempOwner, 1); - }; - - auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards, - EVisitDest visitDest, ELEaveTile leavingTile) -> bool - { - LOG_TRACE_PARAMS(logGlobal, "Hero %s starts movement from %s to %s", h->name % tmh.start % tmh.end); - - auto moveQuery = make_shared(tmh, h); - queries.addQuery(moveQuery); - - if(leavingTile == LEAVING_TILE) - leaveTile(); - - tmh.result = result; - sendAndApply(&tmh); - - if(lookForGuards == CHECK_FOR_GUARDS && this->isInTheMap(guardPos)) - { - tmh.attackedFrom = guardPos; - - const TerrainTile &guardTile = *gs->getTile(guardPos); - objectVisited(guardTile.visitableObjects.back(), h); - - moveQuery->visitDestAfterVictory = visitDest==VISIT_DEST; - } - else if(visitDest == VISIT_DEST) - { - visitObjectOnTile(t, h); - } - - queries.popIfTop(moveQuery); - logGlobal->traceStream() << "Hero " << h->name << " ends movement"; - return result != TryMoveHero::FAILED; - }; - - //interaction with blocking object (like resources) - auto blockingVisit = [&]() -> bool - { - for(CGObjectInstance *obj : t.visitableObjects) - { - if(obj != h && obj->blockVisit && !obj->passableFor(h->tempOwner)) - { - return doMove(TryMoveHero::BLOCKING_VISIT, this->IGNORE_GUARDS, VISIT_DEST, REMAINING_ON_TILE); - //this-> is needed for MVS2010 to recognize scope (?) - } - } - return false; - }; - - - if(embarking) - { - tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, false); - return doMove(TryMoveHero::EMBARK, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE); - //attack guards on embarking? In H3 creatures on water had no zone of control at all - } - - if(disembarking) - { - tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, true); - return doMove(TryMoveHero::DISEMBARK, CHECK_FOR_GUARDS, VISIT_DEST, LEAVING_TILE); - } - - if(teleporting) - { - if(blockingVisit()) // e.g. hero on the other side of teleporter - return true; - - doMove(TryMoveHero::TELEPORTATION, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE); - - // visit town for town portal \ castle gates - // do not use generic visitObjectOnTile to avoid double-teleporting - // if this moveHero call was triggered by teleporter - if (!t.visitableObjects.empty()) - { - if (CGTownInstance * town = dynamic_cast(t.visitableObjects.back())) - town->onHeroVisit(h); - } - - return true; - } - - //still here? it is standard movement! - { - tmh.movePoints = h->movement >= cost - ? h->movement - cost - : 0; - - if(blockingVisit()) - return true; - - doMove(TryMoveHero::SUCCESS, CHECK_FOR_GUARDS, VISIT_DEST, LEAVING_TILE); - return true; - } -} - -bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker/* = 255*/) -{ - const CGHeroInstance *h = getHero(hid); - const CGTownInstance *t = getTown(dstid); - - if ( !h || !t || h->getOwner() != gs->currentPlayer ) - logGlobal->errorStream()<<"Invalid call to teleportHero!"; - - const CGTownInstance *from = h->visitedTown; - if(((h->getOwner() != t->getOwner()) - && complain("Cannot teleport hero to another player")) - || ((!from || !from->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO)) - && complain("Hero must be in town with Castle gate for teleporting")) - || (!t->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO) - && complain("Cannot teleport hero to town without Castle gate in it"))) - return false; - int3 pos = t->visitablePos(); - pos += h->getVisitableOffset(); - moveHero(hid,pos,1); - return true; -} - -void CGameHandler::setOwner(const CGObjectInstance * obj, PlayerColor owner) -{ - PlayerColor oldOwner = getOwner(obj->id); - SetObjectProperty sop(obj->id, 1, owner.getNum()); - sendAndApply(&sop); - - std::set playerColors = boost::assign::list_of(owner)(oldOwner); - checkVictoryLossConditions(playerColors); - - if(owner < PlayerColor::PLAYER_LIMIT && dynamic_cast(obj)) //town captured - { - const CGTownInstance * town = dynamic_cast(obj); - if (town->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) - setPortalDwelling(town, true, false); - - if (!gs->getPlayer(owner)->towns.size())//player lost last town - { - InfoWindow iw; - iw.player = oldOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 6); //%s, you have lost your last town. If you do not conquer another town in the next week, you will be eliminated. - sendAndApply(&iw); - } - } - - const PlayerState * p = gs->getPlayer(owner); - - if((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4 ) && p && p->dwellings.size()==1)//first dwelling captured - { - for(const CGTownInstance *t : gs->getPlayer(owner)->towns) - { - if (t->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) - setPortalDwelling(t);//set initial creatures for all portals of summoning - } - } -} - -void CGameHandler::setHoverName(const CGObjectInstance * obj, MetaString* name) -{ - SetHoverName shn(obj->id, *name); - sendAndApply(&shn); -} - -void CGameHandler::showBlockingDialog( BlockingDialog *iw ) -{ - auto dialogQuery = make_shared(*iw); - queries.addQuery(dialogQuery); - iw->queryID = dialogQuery->queryID; - sendToAllClients(iw); -} - -void CGameHandler::giveResource(PlayerColor player, Res::ERes which, int val) //TODO: cap according to Bersy's suggestion -{ - if(!val) return; //don't waste time on empty call - SetResource sr; - sr.player = player; - sr.resid = which; - sr.val = gs->players.find(player)->second.resources.at(which) + val; - sendAndApply(&sr); -} - -void CGameHandler::giveResources(PlayerColor player, TResources resources) -{ - for(TResources::nziterator i(resources); i.valid(); i++) - giveResource(player, i->resType, i->resVal); -} - -void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) -{ - COMPLAIN_RET_IF(!creatures.stacksCount(), "Strange, giveCreatures called without args!"); - COMPLAIN_RET_IF(obj->stacksCount(), "Cannot give creatures from not-cleared object!"); - COMPLAIN_RET_IF(creatures.stacksCount() > GameConstants::ARMY_SIZE, "Too many stacks to give!"); - - //first we move creatures to give to make them army of object-source - for (auto & elem : creatures.Slots()) - { - addToSlot(StackLocation(obj, obj->getSlotFor(elem.second->type)), elem.second->type, elem.second->count); - } - - tryJoiningArmy(obj, h, remove, true); -} - -void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector &creatures) -{ - std::vector cres = creatures; - if (cres.size() <= 0) - return; - const CArmedInstance* obj = static_cast(getObj(objid)); - - for(CStackBasicDescriptor &sbd : cres) - { - TQuantity collected = 0; - while(collected < sbd.count) - { - bool foundSth = false; - for(auto i = obj->Slots().begin(); i != obj->Slots().end(); i++) - { - if(i->second->type == sbd.type) - { - TQuantity take = std::min(sbd.count - collected, i->second->count); //collect as much cres as we can - changeStackCount(StackLocation(obj, i->first), -take, false); - collected += take; - foundSth = true; - break; - } - } - - if(!foundSth) //we went through the whole loop and haven't found appropriate cres - { - complain("Unexpected failure during taking creatures!"); - return; - } - } - } -} - -void CGameHandler::showCompInfo(ShowInInfobox * comp) -{ - sendToAllClients(comp); -} -void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) -{ - HeroVisitCastle vc; - vc.hid = hero->id; - vc.tid = obj->id; - vc.flags |= 1; - sendAndApply(&vc); - vistiCastleObjects (obj, hero); - giveSpells (obj, hero); - - checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact? -} - -void CGameHandler::vistiCastleObjects (const CGTownInstance *t, const CGHeroInstance *h) -{ - std::vector::const_iterator i; - for (i = t->bonusingBuildings.begin(); i != t->bonusingBuildings.end(); i++) - (*i)->onHeroVisit (h); -} - -void CGameHandler::stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) -{ - HeroVisitCastle vc; - vc.hid = hero->id; - vc.tid = obj->id; - sendAndApply(&vc); -} - -void CGameHandler::removeArtifact(const ArtifactLocation &al) -{ - assert(al.getArt()); - EraseArtifact ea; - ea.al = al; - sendAndApply(&ea); -} -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) //use hero=nullptr for no hero -{ - engageIntoBattle(army1->tempOwner); - engageIntoBattle(army2->tempOwner); - - static const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - static const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; - - - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - - auto battleQuery = make_shared(gs->curB); - queries.addQuery(battleQuery); - - boost::thread(&CGameHandler::runBattle, this); -} - -void CGameHandler::startBattleI( const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) -{ - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); -} - -void CGameHandler::startBattleI( const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} - -void CGameHandler::changeSpells( const CGHeroInstance * hero, bool give, const std::set &spells ) -{ - ChangeSpells cs; - cs.hid = hero->id; - cs.spells = spells; - cs.learn = give; - sendAndApply(&cs); -} - -void CGameHandler::sendMessageTo( CConnection &c, const std::string &message ) -{ - SystemMessage sm; - sm.text = message; - boost::unique_lock lock(*c.wmx); - c << &sm; -} - -void CGameHandler::giveHeroBonus( GiveBonus * bonus ) -{ - sendAndApply(bonus); -} - -void CGameHandler::setMovePoints( SetMovePoints * smp ) -{ - sendAndApply(smp); -} - -void CGameHandler::setManaPoints( ObjectInstanceID hid, int val ) -{ - SetMana sm; - sm.hid = hid; - sm.val = val; - sendAndApply(&sm); -} - -void CGameHandler::giveHero( ObjectInstanceID id, PlayerColor player ) -{ - GiveHero gh; - gh.id = id; - gh.player = player; - sendAndApply(&gh); -} - -void CGameHandler::changeObjPos( ObjectInstanceID objid, int3 newPos, ui8 flags ) -{ - ChangeObjPos cop; - cop.objid = objid; - cop.nPos = newPos; - cop.flags = flags; - sendAndApply(&cop); -} - -void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID toHero) -{ - const CGHeroInstance * h1 = getHero(fromHero); - const CGHeroInstance * h2 = getHero(toHero); - - if ( h1->getSecSkillLevel(SecondarySkill::SCHOLAR) < h2->getSecSkillLevel(SecondarySkill::SCHOLAR) ) - { - std::swap (h1,h2);//1st hero need to have higher scholar level for correct message - std::swap(fromHero, toHero); - } - - int ScholarLevel = h1->getSecSkillLevel(SecondarySkill::SCHOLAR);//heroes can trade up to this level - if (!ScholarLevel || !h1->hasSpellbook() || !h2->hasSpellbook() ) - return;//no scholar skill or no spellbook - - int h1Lvl = std::min(ScholarLevel+1, h1->getSecSkillLevel(SecondarySkill::WISDOM)+2), - h2Lvl = std::min(ScholarLevel+1, h2->getSecSkillLevel(SecondarySkill::WISDOM)+2);//heroes can receive this levels - - ChangeSpells cs1; - cs1.learn = true; - cs1.hid = toHero;//giving spells to first hero - for(auto it : h1->spells) - if ( h2Lvl >= it.toSpell()->level && !vstd::contains(h2->spells, it))//hero can learn it and don't have it yet - cs1.spells.insert(it);//spell to learn - - ChangeSpells cs2; - cs2.learn = true; - cs2.hid = fromHero; - - for(auto it : h2->spells) - if ( h1Lvl >= it.toSpell()->level && !vstd::contains(h1->spells, it)) - cs2.spells.insert(it); - - if (!cs1.spells.empty() || !cs2.spells.empty())//create a message - { - InfoWindow iw; - iw.player = h1->tempOwner; - iw.components.push_back(Component(Component::SEC_SKILL, 18, ScholarLevel, 0)); - - iw.text.addTxt(MetaString::GENERAL_TXT, 139);//"%s, who has studied magic extensively, - iw.text.addReplacement(h1->name); - - if (!cs2.spells.empty())//if found new spell - apply - { - iw.text.addTxt(MetaString::GENERAL_TXT, 140);//learns - int size = cs2.spells.size(); - for(auto it : cs2.spells) - { - iw.components.push_back(Component(Component::SPELL, it, 1, 0)); - iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum()); - switch (size--) - { - case 2: iw.text.addTxt(MetaString::GENERAL_TXT, 141); - case 1: break; - default: iw.text << ", "; - } - } - iw.text.addTxt(MetaString::GENERAL_TXT, 142);//from %s - iw.text.addReplacement(h2->name); - sendAndApply(&cs2); - } - - if (!cs1.spells.empty() && !cs2.spells.empty() ) - { - iw.text.addTxt(MetaString::GENERAL_TXT, 141);//and - } - - if (!cs1.spells.empty()) - { - iw.text.addTxt(MetaString::GENERAL_TXT, 147);//teaches - int size = cs1.spells.size(); - for(auto it : cs1.spells) - { - iw.components.push_back(Component(Component::SPELL, it, 1, 0)); - iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum()); - switch (size--) - { - case 2: iw.text.addTxt(MetaString::GENERAL_TXT, 141); - case 1: break; - default: iw.text << ", "; - } } - iw.text.addTxt(MetaString::GENERAL_TXT, 148);//from %s - iw.text.addReplacement(h2->name); - sendAndApply(&cs1); - } - sendAndApply(&iw); - } -} - -void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) -{ - auto h1 = getHero(hero1), h2 = getHero(hero2); - - if( gameState()->getPlayerRelations(h1->getOwner(), h2->getOwner())) - { - auto exchange = make_shared(h1, h2); - ExchangeDialog hex; - hex.queryID = exchange->queryID; - hex.heroes[0] = getHero(hero1); - hex.heroes[1] = getHero(hero2); - sendAndApply(&hex); - useScholarSkill(hero1,hero2); - queries.addQuery(exchange); - } -} - -void CGameHandler::sendToAllClients( CPackForClient * info ) -{ - logGlobal->traceStream() << "Sending to all clients a package of type " << typeid(*info).name(); - for(auto & elem : conns) - { - boost::unique_lock lock(*(elem)->wmx); - *elem << info; - } -} - -void CGameHandler::sendAndApply(CPackForClient * info) -{ - sendToAllClients(info); - gs->apply(info); -} - -void CGameHandler::applyAndSend(CPackForClient * info) -{ - gs->apply(info); - sendToAllClients(info); -} - -void CGameHandler::sendAndApply(CGarrisonOperationPack * info) -{ - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForAll(); -} - -void CGameHandler::sendAndApply( SetResource * info ) -{ - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForPlayer(info->player); -} - -void CGameHandler::sendAndApply( SetResources * info ) -{ - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForPlayer(info->player); -} - -void CGameHandler::sendAndApply( NewStructures * info ) -{ - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForPlayer(getTown(info->tid)->tempOwner); -} - -void CGameHandler::save(const std::string & filename ) -{ - logGlobal->errorStream() << "Saving to " << filename; - CFileInfo info(filename); - //CResourceHandler::get("local")->createResource(info.getStem() + ".vlgm1"); - CResourceHandler::get("local")->createResource(info.getStem() + ".vsgm1"); - - { - logGlobal->infoStream() << "Ordering clients to serialize..."; - SaveGame sg(info.getStem() + ".vcgm1"); - sendToAllClients(&sg); - } - - try - { -// { -// logGlobal->infoStream() << "Serializing game info..."; -// CSaveFile save(CResourceHandler::get("local")->getResourceName(ResourceID(info.getStem(), EResType::LIB_SAVEGAME))); -// // char hlp[8] = "VCMISVG"; -// // save << hlp; -// saveCommonState(save); -// } - - { - CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourceID(info.getStem(), EResType::SERVER_SAVEGAME))); - saveCommonState(save); - logGlobal->infoStream() << "Saving server state"; - save << *this; - } - logGlobal->infoStream() << "Game has been successfully saved!"; - } - catch(std::exception &e) - { - logGlobal->errorStream() << "Failed to save game: " << e.what(); - } -} - -void CGameHandler::close() -{ - logGlobal->infoStream() << "We have been requested to close."; - - if(gs->initialOpts->mode == StartInfo::DUEL) - { - exit(0); - } - - //for(CConnection *cc : conns) - // if(cc && cc->socket && cc->socket->is_open()) - // cc->socket->close(); - //exit(0); -} - -bool CGameHandler::arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player ) -{ - const CArmedInstance *s1 = static_cast(gs->getObjInstance(id1)), - *s2 = static_cast(gs->getObjInstance(id2)); - const CCreatureSet &S1 = *s1, &S2 = *s2; - StackLocation sl1(s1, p1), sl2(s2, p2); - if(!sl1.slot.validSlot() || !sl2.slot.validSlot()) - { - complain("Invalid slot accessed!"); - return false; - } - - if(!isAllowedExchange(id1,id2)) - { - complain("Cannot exchange stacks between these two objects!\n"); - return false; - } - - if(what==1) //swap - { - if ( ((s1->tempOwner != player && s1->tempOwner != PlayerColor::UNFLAGGABLE) && s1->getStackCount(p1)) //why 254?? - || ((s2->tempOwner != player && s2->tempOwner != PlayerColor::UNFLAGGABLE) && s2->getStackCount(p2))) - { - complain("Can't take troops from another player!"); - return false; - } - - swapStacks(sl1, sl2); - } - else if(what==2)//merge - { - if (( s1->getCreature(p1) != s2->getCreature(p2) && complain("Cannot merge different creatures stacks!")) - || (((s1->tempOwner != player && s1->tempOwner != PlayerColor::UNFLAGGABLE) && s2->getStackCount(p2)) && complain("Can't take troops from another player!"))) - return false; - - moveStack(sl1, sl2); - } - else if(what==3) //split - { - const int countToMove = val - s2->getStackCount(p2); - const int countLeftOnSrc = s1->getStackCount(p1) - countToMove; - - if ( (s1->tempOwner != player && countLeftOnSrc < s1->getStackCount(p1) ) - || (s2->tempOwner != player && val < s2->getStackCount(p2) ) ) - { - complain("Can't move troops of another player!"); - return false; - } - - //general conditions checking - if((!vstd::contains(S1.stacks,p1) && complain("no creatures to split")) - || (val<1 && complain("no creatures to split")) ) - { - return false; - } - - - if(vstd::contains(S2.stacks,p2)) //dest. slot not free - it must be "rebalancing"... - { - int total = s1->getStackCount(p1) + s2->getStackCount(p2); - if( (total < val && complain("Cannot split that stack, not enough creatures!")) - || (s1->getCreature(p1) != s2->getCreature(p2) && complain("Cannot rebalance different creatures stacks!")) - ) - { - return false; - } - - moveStack(sl1, sl2, countToMove); - //S2.slots[p2]->count = val; - //S1.slots[p1]->count = total - val; - } - else //split one stack to the two - { - if(s1->getStackCount(p1) < val)//not enough creatures - { - complain("Cannot split that stack, not enough creatures!"); - return false; - } - - - moveStack(sl1, sl2, val); - } - - } - return true; -} - -PlayerColor CGameHandler::getPlayerAt( CConnection *c ) const -{ - std::set all; - for(auto i=connections.cbegin(); i!=connections.cend(); i++) - if(i->second == c) - all.insert(i->first); - - switch(all.size()) - { - case 0: - return PlayerColor::NEUTRAL; - case 1: - return *all.begin(); - default: - { - //if we have more than one player at this connection, try to pick active one - if(vstd::contains(all, gs->currentPlayer)) - return gs->currentPlayer; - else - return PlayerColor::CANNOT_DETERMINE; //cannot say which player is it - } - } -} - -bool CGameHandler::disbandCreature( ObjectInstanceID id, SlotID pos ) -{ - CArmedInstance *s1 = static_cast(gs->getObjInstance(id)); - if(!vstd::contains(s1->stacks,pos)) - { - complain("Illegal call to disbandCreature - no such stack in army!"); - return false; - } - - eraseStack(StackLocation(s1, pos)); - return true; -} - -bool CGameHandler::buildStructure( ObjectInstanceID tid, BuildingID requestedID, bool force /*=false*/ ) -{ - const CGTownInstance * t = getTown(tid); - if(!t) - COMPLAIN_RETF("No such town (ID=%s)!", tid); - if(!t->town->buildings.count(requestedID)) - COMPLAIN_RETF("Town of faction %s does not have info about building ID=%s!", t->town->faction->name % tid); - if (t->hasBuilt(requestedID)) - COMPLAIN_RETF("Building %s is already built in %s", t->town->buildings.at(requestedID)->Name() % t->name); - - const CBuilding * requestedBuilding = t->town->buildings.at(requestedID); - - //Vector with future list of built building and buildings in auto-mode that are not yet built. - std::vector remainingAutoBuildings; - std::set buildingsThatWillBe; - - //Check validity of request - if(!force) - { - switch (requestedBuilding->mode) - { - case CBuilding::BUILD_NORMAL : - if (gs->canBuildStructure(t, requestedID) != EBuildingState::ALLOWED) - COMPLAIN_RET("Cannot build that building!"); - break; - - case CBuilding::BUILD_AUTO : - case CBuilding::BUILD_SPECIAL: - COMPLAIN_RET("This building can not be constructed normally!"); - - case CBuilding::BUILD_GRAIL : - if(requestedBuilding->mode == CBuilding::BUILD_GRAIL) //needs grail - { - if(!t->visitingHero || !t->visitingHero->hasArt(ArtifactID::GRAIL)) - COMPLAIN_RET("Cannot build this without grail!") - else - removeArtifact(ArtifactLocation(t->visitingHero, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); - } - break; - } - } - - //Performs stuff that has to be done after new building is built - auto processBuiltStructure = [t, this](const BuildingID buildingID) - { - if(buildingID >= BuildingID::DWELL_FIRST) //dwelling - { - int level = (buildingID - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; - int upgradeNumber = (buildingID - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; - - if (upgradeNumber >= t->town->creatures.at(level).size()) - { - complain(boost::str(boost::format("Error ecountered when building dwelling (bid=%s):" - "no creature found (upgrade number %d, level %d!") - % buildingID % upgradeNumber % level)); - return; - } - - CCreature * crea = VLC->creh->creatures.at(t->town->creatures.at(level).at(upgradeNumber)); - - SetAvailableCreatures ssi; - ssi.tid = t->id; - ssi.creatures = t->creatures; - if (ssi.creatures[level].second.empty()) // first creature in a dwelling - ssi.creatures[level].first = crea->growth; - ssi.creatures[level].second.push_back(crea->idNumber); - sendAndApply(&ssi); - } - if ( t->subID == ETownType::DUNGEON && buildingID == BuildingID::PORTAL_OF_SUMMON ) - { - setPortalDwelling(t); - } - - if(buildingID <= BuildingID::MAGES_GUILD_5) //it's mage guild - { - if(t->visitingHero) - giveSpells(t,t->visitingHero); - if(t->garrisonHero) - giveSpells(t,t->garrisonHero); - } - }; - - //Checks if all requirements will be met with expected building list "buildingsThatWillBe" - auto areRequirementsFullfilled = [&](const BuildingID & buildID) - { - return buildingsThatWillBe.count(buildID); - }; - - //Init the vectors - for(auto & build : t->town->buildings) - { - if(t->hasBuilt(build.first)) - buildingsThatWillBe.insert(build.first); - else - { - if(build.second->mode == CBuilding::BUILD_AUTO) //not built auto building - remainingAutoBuildings.push_back(build.second); - } - } - - //Prepare structure (list of building ids will be filled later) - NewStructures ns; - ns.tid = tid; - ns.builded = force ? t->builded : (t->builded+1); - - std::queue buildingsToAdd; - buildingsToAdd.push(requestedBuilding); - - while(!buildingsToAdd.empty()) - { - auto b = buildingsToAdd.front(); - buildingsToAdd.pop(); - - ns.bid.insert(b->bid); - buildingsThatWillBe.insert(b->bid); - remainingAutoBuildings -= b; - - for(auto autoBuilding : remainingAutoBuildings) - { - if (autoBuilding->requirements.test(areRequirementsFullfilled)) - buildingsToAdd.push(autoBuilding); - } - } - - //Other post-built events - for(auto builtID : ns.bid) - processBuiltStructure(builtID); - - //Take cost - if (!force) - { - SetResources sr; - sr.player = t->tempOwner; - sr.res = gs->getPlayer(t->tempOwner)->resources - requestedBuilding->resources; - sendAndApply(&sr); - } - - //We know what has been built, appluy changes. Do this as final step to properly update town window - sendAndApply(&ns); - - // now when everything is built - reveal tiles for lookout tower - FoWChange fw; - fw.player = t->tempOwner; - fw.mode = 1; - t->getSightTiles(fw.tiles); - sendAndApply(&fw); - - if(t->visitingHero) - vistiCastleObjects (t, t->visitingHero); - if(t->garrisonHero) - vistiCastleObjects (t, t->garrisonHero); - - checkVictoryLossConditionsForPlayer(t->tempOwner); - return true; -} -bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) -{ -///incomplete, simply erases target building - const CGTownInstance * t = getTown(tid); - if (!vstd::contains(t->builtBuildings, bid)) - return false; - RazeStructures rs; - rs.tid = tid; - rs.bid.insert(bid); - rs.destroyed = t->destroyed + 1; - sendAndApply(&rs); -//TODO: Remove dwellers -// if (t->subID == 4 && bid == 17) //Veil of Darkness -// { -// RemoveBonus rb(RemoveBonus::TOWN); -// rb.whoID = t->id; -// rb.source = Bonus::TOWN_STRUCTURE; -// rb.id = 17; -// sendAndApply(&rb); -// } - return true; -} - -void CGameHandler::sendMessageToAll( const std::string &message ) -{ - SystemMessage sm; - sm.text = message; - sendToAllClients(&sm); -} - -bool CGameHandler::recruitCreatures( ObjectInstanceID objid, CreatureID crid, ui32 cram, si32 fromLvl ) -{ - const CGDwelling *dw = static_cast(gs->getObj(objid)); - const CArmedInstance *dst = nullptr; - const CCreature *c = VLC->creh->creatures.at(crid); - bool warMachine = c->hasBonusOfType(Bonus::SIEGE_WEAPON); - - //TODO: test for owning - - if(dw->ID == Obj::TOWN) - dst = (static_cast(dw))->getUpperArmy(); - else if(dw->ID == Obj::CREATURE_GENERATOR1 || dw->ID == Obj::CREATURE_GENERATOR4 - || dw->ID == Obj::REFUGEE_CAMP) //advmap dwelling - dst = getHero(gs->getPlayer(dw->tempOwner)->currentSelection); //TODO: check if current hero is really visiting dwelling - else if(dw->ID == Obj::WAR_MACHINE_FACTORY) - dst = dynamic_cast(getTile(dw->visitablePos())->visitableObjects.back()); - - assert(dw && dst); - - //verify - bool found = false; - int level = 0; - - for(; level < dw->creatures.size(); level++) //iterate through all levels - { - if ( (fromLvl != -1) && ( level !=fromLvl ) ) - continue; - const auto &cur = dw->creatures.at(level); //current level info - int i = 0; - for(; i < cur.second.size(); i++) //look for crid among available creatures list on current level - if(cur.second.at(i) == crid) - break; - - if(i < cur.second.size()) - { - found = true; - cram = std::min(cram, cur.first); //reduce recruited amount up to available amount - break; - } - } - SlotID slot = dst->getSlotFor(crid); - - if( (!found && complain("Cannot recruit: no such creatures!")) - || (cram > VLC->creh->creatures.at(crid)->maxAmount(gs->getPlayer(dst->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) - || (cram<=0 && complain("Cannot recruit: cram <= 0!")) - || (!slot.validSlot() && !warMachine && complain("Cannot recruit: no available slot!"))) - { - return false; - } - - //recruit - SetResources sr; - sr.player = dst->tempOwner; - sr.res = gs->getPlayer(dst->tempOwner)->resources - (c->cost * cram); - - SetAvailableCreatures sac; - sac.tid = objid; - sac.creatures = dw->creatures; - sac.creatures[level].first -= cram; - - sendAndApply(&sr); - sendAndApply(&sac); - - if(warMachine) - { - const CGHeroInstance *h = dynamic_cast(dst); - if(!h) - COMPLAIN_RET("Only hero can buy war machines"); - - switch(crid) - { - case 146: - giveHeroNewArtifact(h, VLC->arth->artifacts[4], ArtifactPosition::MACH1); - break; - case 147: - giveHeroNewArtifact(h, VLC->arth->artifacts[6], ArtifactPosition::MACH3); - break; - case 148: - giveHeroNewArtifact(h, VLC->arth->artifacts[5], ArtifactPosition::MACH2); - break; - default: - complain("This war machine cannot be recruited!"); - return false; - } - } - else - { - addToSlot(StackLocation(dst, slot), c, cram); - } - return true; -} - -bool CGameHandler::upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ) -{ - CArmedInstance *obj = static_cast(gs->getObjInstance(objid)); - assert(obj->hasStackAtSlot(pos)); - UpgradeInfo ui = gs->getUpgradeInfo(obj->getStack(pos)); - PlayerColor player = obj->tempOwner; - const PlayerState *p = getPlayer(player); - int crQuantity = obj->stacks.at(pos)->count; - int newIDpos= vstd::find_pos(ui.newID, upgID);//get position of new id in UpgradeInfo - - //check if upgrade is possible - if( (ui.oldID<0 || newIDpos == -1 ) && complain("That upgrade is not possible!")) - { - return false; - } - TResources totalCost = ui.cost.at(newIDpos) * crQuantity; - - //check if player has enough resources - if(!p->resources.canAfford(totalCost)) - COMPLAIN_RET("Cannot upgrade, not enough resources!"); - - //take resources - SetResources sr; - sr.player = player; - sr.res = p->resources - totalCost; - sendAndApply(&sr); - - //upgrade creature - changeStackType(StackLocation(obj, pos), VLC->creh->creatures.at(upgID)); - return true; -} - -bool CGameHandler::changeStackType(const StackLocation &sl, CCreature *c) -{ - if(!sl.army->hasStackAtSlot(sl.slot)) - COMPLAIN_RET("Cannot find a stack to change type"); - - SetStackType sst; - sst.sl = sl; - sst.type = c; - sendAndApply(&sst); - return true; -} - -void CGameHandler::moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging) -{ - assert(src->canBeMergedWith(*dst, allowMerging)); - while(src->stacksCount())//while there are unmoved creatures - { - auto i = src->Slots().begin(); //iterator to stack to move - StackLocation sl(src, i->first); //location of stack to move - - SlotID pos = dst->getSlotFor(i->second->type); - if(!pos.validSlot()) - { - //try to merge two other stacks to make place - std::pair toMerge; - if(dst->mergableStacks(toMerge, i->first) && allowMerging) - { - moveStack(StackLocation(dst, toMerge.first), StackLocation(dst, toMerge.second)); //merge toMerge.first into toMerge.second - assert(!dst->hasStackAtSlot(toMerge.first)); //we have now a new free slot - moveStack(sl, StackLocation(dst, toMerge.first)); //move stack to freed slot - } - else - { - complain("Unexpected failure during an attempt to move army from " + src->nodeName() + " to " + dst->nodeName() + "!"); - return; - } - } - else - { - moveStack(sl, StackLocation(dst, pos)); - } - } -} - -bool CGameHandler::garrisonSwap( ObjectInstanceID tid ) -{ - CGTownInstance *town = gs->getTown(tid); - if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army - { - - if(!town->visitingHero->canBeMergedWith(*town)) - { - complain("Cannot make garrison swap, not enough free slots!"); - return false; - } - - moveArmy(town, town->visitingHero, true); - - SetHeroesInTown intown; - intown.tid = tid; - intown.visiting = ObjectInstanceID(); - intown.garrison = town->visitingHero->id; - sendAndApply(&intown); - return true; - } - else if (town->garrisonHero && !town->visitingHero) //move hero out of the garrison - { - //check if moving hero out of town will break 8 wandering heroes limit - if(getHeroCount(town->garrisonHero->tempOwner,false) >= 8) - { - complain("Cannot move hero out of the garrison, there are already 8 wandering heroes!"); - return false; - } - - SetHeroesInTown intown; - intown.tid = tid; - intown.garrison = ObjectInstanceID(); - intown.visiting = town->garrisonHero->id; - sendAndApply(&intown); - return true; - } - else if(!!town->garrisonHero && town->visitingHero) //swap visiting and garrison hero - { - SetHeroesInTown intown; - intown.tid = tid; - intown.garrison = town->visitingHero->id; - intown.visiting = town->garrisonHero->id; - sendAndApply(&intown); - return true; - } - else - { - complain("Cannot swap garrison hero!"); - return false; - } -} - -// With the amount of changes done to the function, it's more like transferArtifacts. -// Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. -bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) -{ - ArtifactLocation src = al1, dst = al2; - const PlayerColor srcPlayer = src.owningPlayer(), dstPlayer = dst.owningPlayer(); - const CArmedInstance *srcObj = src.relatedObj(), *dstObj = dst.relatedObj(); - - // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcObj->id, dstObj->id)) - COMPLAIN_RET("That heroes cannot make any exchange!"); - - const CArtifactInstance *srcArtifact = src.getArt(); - const CArtifactInstance *destArtifact = dst.getArt(); - - if (srcArtifact == nullptr) - COMPLAIN_RET("No artifact to move!"); - if (destArtifact && srcPlayer != dstPlayer) - COMPLAIN_RET("Can't touch artifact on hero of another player!"); - - // Check if src/dest slots are appropriate for the artifacts exchanged. - // Moving to the backpack is always allowed. - if ((!srcArtifact || dst.slot < GameConstants::BACKPACK_START) - && srcArtifact && !srcArtifact->canBePutAt(dst, true)) - COMPLAIN_RET("Cannot move artifact!"); - - if ((srcArtifact && srcArtifact->artType->id == ArtifactID::ART_LOCK) || (destArtifact && destArtifact->artType->id == ArtifactID::ART_LOCK)) - COMPLAIN_RET("Cannot move artifact locks."); - - if (dst.slot >= GameConstants::BACKPACK_START && srcArtifact->artType->isBig()) - COMPLAIN_RET("Cannot put big artifacts in backpack!"); - if (src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) - COMPLAIN_RET("Cannot move catapult!"); - - if(dst.slot >= GameConstants::BACKPACK_START) - vstd::amin(dst.slot, ArtifactPosition(GameConstants::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size())); - - if (src.slot == dst.slot && src.artHolder == dst.artHolder) - COMPLAIN_RET("Won't move artifact: Dest same as source!"); - - if(dst.slot < GameConstants::BACKPACK_START && destArtifact) //moving art to another slot - { - //old artifact must be removed first - moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition( - dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START))); - } - - MoveArtifact ma; - ma.src = src; - ma.dst = dst; - sendAndApply(&ma); - return true; -} - -/** - * Assembles or disassembles a combination artifact. - * @param heroID ID of hero holding the artifact(s). - * @param artifactSlot The worn slot ID of the combination- or constituent artifact. - * @param assemble True for assembly operation, false for disassembly. - * @param assembleTo If assemble is true, this represents the artifact ID of the combination - * artifact to assemble to. Otherwise it's not used. - */ -bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) -{ - - CGHeroInstance *hero = gs->getHero(heroID); - const CArtifactInstance *destArtifact = hero->getArt(artifactSlot); - - if(!destArtifact) - COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); - - if(assemble) - { - CArtifact *combinedArt = VLC->arth->artifacts.at(assembleTo); - if(!combinedArt->constituents) - COMPLAIN_RET("assembleArtifacts: Artifact being attempted to assemble is not a combined artifacts!"); - if(!vstd::contains(destArtifact->assemblyPossibilities(hero), combinedArt)) - COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); - - AssembledArtifact aa; - aa.al = ArtifactLocation(hero, artifactSlot); - aa.builtArt = combinedArt; - sendAndApply(&aa); - } - else - { - if(!destArtifact->artType->constituents) - COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble is not a combined artifact!"); - - DisassembledArtifact da; - da.al = ArtifactLocation(hero, artifactSlot); - sendAndApply(&da); - } - - return false; -} - -bool CGameHandler::buyArtifact( ObjectInstanceID hid, ArtifactID aid ) -{ - CGHeroInstance *hero = gs->getHero(hid); - CGTownInstance *town = hero->visitedTown; - if(aid==ArtifactID::SPELLBOOK) - { - if((!town->hasBuilt(BuildingID::MAGES_GUILD_1) && complain("Cannot buy a spellbook, no mage guild in the town!")) - || (getResource(hero->getOwner(), Res::GOLD) < GameConstants::SPELLBOOK_GOLD_COST && complain("Cannot buy a spellbook, not enough gold!") ) - || (hero->getArt(ArtifactPosition::SPELLBOOK) && complain("Cannot buy a spellbook, hero already has a one!")) - ) - return false; - - giveResource(hero->getOwner(),Res::GOLD,-GameConstants::SPELLBOOK_GOLD_COST); - giveHeroNewArtifact(hero, VLC->arth->artifacts[0], ArtifactPosition::SPELLBOOK); - assert(hero->getArt(ArtifactPosition::SPELLBOOK)); - giveSpells(town,hero); - return true; - } - else if(aid < 7 && aid > 3) //war machine - { - int price = VLC->arth->artifacts.at(aid)->price; - - if(( hero->getArt(ArtifactPosition(9+aid)) && complain("Hero already has this machine!")) - || (gs->getPlayer(hero->getOwner())->resources.at(Res::GOLD) < price && complain("Not enough gold!"))) - { - return false; - } - if ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid ) - || ((town->hasBuilt(BuildingID::BALLISTA_YARD, ETownType::STRONGHOLD)) && aid == ArtifactID::BALLISTA)) - { - giveResource(hero->getOwner(),Res::GOLD,-price); - giveHeroNewArtifact(hero, VLC->arth->artifacts.at(aid), ArtifactPosition(9+aid)); - return true; - } - else - COMPLAIN_RET("This machine is unavailable here!"); - } - return false; -} - -bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, Res::ERes rid, ArtifactID aid) -{ - if(!vstd::contains(m->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), aid)) - COMPLAIN_RET("That artifact is unavailable!"); - - int b1, b2; - m->getOffer(rid, aid, b1, b2, EMarketMode::RESOURCE_ARTIFACT); - - if(getResource(h->tempOwner, rid) < b1) - COMPLAIN_RET("You can't afford to buy this artifact!"); - - SetResource sr; - sr.player = h->tempOwner; - sr.resid = rid; - sr.val = getResource(h->tempOwner, rid) - b1; - sendAndApply(&sr); - - - SetAvailableArtifacts saa; - if(m->o->ID == Obj::TOWN) - { - saa.id = -1; - saa.arts = CGTownInstance::merchantArtifacts; - } - else if(const CGBlackMarket *bm = dynamic_cast(m->o)) //black market - { - saa.id = bm->id.getNum(); - saa.arts = bm->artifacts; - } - else - COMPLAIN_RET("Wrong marktet..."); - - bool found = false; - for(const CArtifact *&art : saa.arts) - { - if(art && art->id == aid) - { - art = nullptr; - found = true; - break; - } - } - - if(!found) - COMPLAIN_RET("Cannot find selected artifact on the list"); - - sendAndApply(&saa); - - giveHeroNewArtifact(h, VLC->arth->artifacts.at(aid), ArtifactPosition::FIRST_AVAILABLE); - return true; -} - -bool CGameHandler::sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, Res::ERes rid ) -{ - const CArtifactInstance *art = h->getArtByInstanceId(aid); - if(!art) - COMPLAIN_RET("There is no artifact to sell!"); - if(art->artType->id < 7) - COMPLAIN_RET("Cannot sell a war machine or spellbook!"); - - int resVal = 0, dump = 1; - m->getOffer(art->artType->id, rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); - - removeArtifact(ArtifactLocation(h, h->getArtPos(art))); - giveResource(h->tempOwner, rid, resVal); - return true; -} - -//void CGameHandler::lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts) -//{ -// //const CGHeroInstance * h1 = dynamic_cast source; -// //auto s = boost::apply_visitor(GetArtifactSetPtr(), source); -// { -// } -//} - -bool CGameHandler::buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill) -{ - if (!h) - COMPLAIN_RET("You need hero to buy a skill!"); - - if (h->getSecSkillLevel(SecondarySkill(skill))) - COMPLAIN_RET("Hero already know this skill"); - - if (!h->canLearnSkill()) - COMPLAIN_RET("Hero can't learn any more skills"); - - if (h->type->heroClass->secSkillProbability.at(skill)==0)//can't learn this skill (like necromancy for most of non-necros) - COMPLAIN_RET("The hero can't learn this skill!"); - - if(!vstd::contains(m->availableItemsIds(EMarketMode::RESOURCE_SKILL), skill)) - COMPLAIN_RET("That skill is unavailable!"); - - if(getResource(h->tempOwner, Res::GOLD) < 2000)//TODO: remove hardcoded resource\summ? - COMPLAIN_RET("You can't afford to buy this skill"); - - SetResource sr; - sr.player = h->tempOwner; - sr.resid = Res::GOLD; - sr.val = getResource(h->tempOwner, Res::GOLD) - 2000; - sendAndApply(&sr); - - changeSecSkill(h, skill, 1, true); - return true; -} - -bool CGameHandler::tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2) -{ - int r1 = gs->getPlayer(player)->resources.at(id1), - r2 = gs->getPlayer(player)->resources.at(id2); - - vstd::amin(val, r1); //can't trade more resources than have - - int b1, b2; //base quantities for trade - market->getOffer(id1, id2, b1, b2, EMarketMode::RESOURCE_RESOURCE); - int units = val / b1; //how many base quantities we trade - - if(val%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error - { - //TODO: complain? - assert(0); - } - - SetResource sr; - sr.player = player; - sr.resid = static_cast(id1); - sr.val = r1 - b1 * units; - sendAndApply(&sr); - - sr.resid = static_cast(id2); - sr.val = r2 + b2 * units; - sendAndApply(&sr); - - return true; -} - -bool CGameHandler::sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, Res::ERes resourceID) -{ - if(!vstd::contains(hero->Slots(), slot)) - COMPLAIN_RET("Hero doesn't have any creature in that slot!"); - - const CStackInstance &s = hero->getStack(slot); - - if( s.count < count //can't sell more creatures than have - || (hero->Slots().size() == 1 && hero->needsLastStack() && s.count == count)) //can't sell last stack - { - COMPLAIN_RET("Not enough creatures in army!"); - } - - int b1, b2; //base quantities for trade - market->getOffer(s.type->idNumber, resourceID, b1, b2, EMarketMode::CREATURE_RESOURCE); - int units = count / b1; //how many base quantities we trade - - if(count%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error - { - //TODO: complain? - assert(0); - } - - changeStackCount(StackLocation(hero, slot), -count); - - SetResource sr; - sr.player = hero->tempOwner; - sr.resid = resourceID; - sr.val = getResource(hero->tempOwner, resourceID) + b2 * units; - sendAndApply(&sr); - - return true; -} - -bool CGameHandler::transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot) -{ - const CArmedInstance *army = nullptr; - if (hero) - army = hero; - else - army = dynamic_cast(market->o); - - if (!army) - COMPLAIN_RET("Incorrect call to transform in undead!"); - if(!army->hasStackAtSlot(slot)) - COMPLAIN_RET("Army doesn't have any creature in that slot!"); - - - const CStackInstance &s = army->getStack(slot); - int resCreature;//resulting creature - bone dragons or skeletons - - if (s.hasBonusOfType(Bonus::DRAGON_NATURE)) - resCreature = 68; - else - resCreature = 56; - - changeStackType(StackLocation(army, slot), VLC->creh->creatures.at(resCreature)); - return true; -} - -bool CGameHandler::sendResources(ui32 val, PlayerColor player, Res::ERes r1, PlayerColor r2) -{ - const PlayerState *p2 = gs->getPlayer(r2, false); - if(!p2 || p2->status != EPlayerStatus::INGAME) - { - complain("Dest player must be in game!"); - return false; - } - - si32 curRes1 = gs->getPlayer(player)->resources.at(r1), - curRes2 = gs->getPlayer(r2)->resources.at(r1); - val = std::min(si32(val),curRes1); - - SetResource sr; - sr.player = player; - sr.resid = r1; - sr.val = curRes1 - val; - sendAndApply(&sr); - - sr.player = r2; - sr.val = curRes2 + val; - sendAndApply(&sr); - - return true; -} - -bool CGameHandler::setFormation( ObjectInstanceID hid, ui8 formation ) -{ - gs->getHero(hid)-> formation = formation; - return true; -} - -bool CGameHandler::hireHero(const CGObjectInstance *obj, ui8 hid, PlayerColor player) -{ - const PlayerState *p = gs->getPlayer(player); - const CGTownInstance *t = gs->getTown(obj->id); - static const int GOLD_NEEDED = 2500; - - //common preconditions - if( (p->resources.at(Res::GOLD)= GameConstants::MAX_HEROES_PER_PLAYER && complain("Cannot hire hero, only 8 wandering heroes are allowed!"))) - return false; - - if(t) //tavern in town - { - if( (!t->hasBuilt(BuildingID::TAVERN) && complain("No tavern!")) - || (t->visitingHero && complain("There is visiting hero - no place!"))) - return false; - } - else if(obj->ID == Obj::TAVERN) - { - if(getTile(obj->visitablePos())->visitableObjects.back() != obj && complain("Tavern entry must be unoccupied!")) - return false; - } - - - const CGHeroInstance *nh = p->availableHeroes.at(hid); - if (!nh) - { - complain ("Hero is not available for hiring!"); - return false; - } - - HeroRecruited hr; - hr.tid = obj->id; - hr.hid = nh->subID; - hr.player = player; - hr.tile = obj->visitablePos() + nh->getVisitableOffset(); - sendAndApply(&hr); - - - std::map > pool = gs->unusedHeroesFromPool(); - - const CGHeroInstance *theOtherHero = p->availableHeroes.at(!hid); - const CGHeroInstance *newHero = nullptr; - if (theOtherHero) //on XXL maps all heroes can be imprisoned :( - newHero = gs->hpool.pickHeroFor(false, player, getNativeTown(player), pool, theOtherHero->type->heroClass); - - SetAvailableHeroes sah; - sah.player = player; - - if(newHero) - { - sah.hid[hid] = newHero->subID; - sah.army[hid].clear(); - sah.army[hid].setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); - } - else - sah.hid[hid] = -1; - - sah.hid[!hid] = theOtherHero ? theOtherHero->subID : -1; - sendAndApply(&sah); - - SetResource sr; - sr.player = player; - sr.resid = Res::GOLD; - sr.val = p->resources.at(Res::GOLD) - GOLD_NEEDED; - sendAndApply(&sr); - - if(t) - { - vistiCastleObjects (t, nh); - giveSpells (t,nh); - } - return true; -} - -bool CGameHandler::queryReply(QueryID qid, ui32 answer, PlayerColor player) -{ - boost::unique_lock lock(gsm); - - logGlobal->traceStream() << boost::format("Player %s attempts answering query %d with answer %d") % player % qid % answer; - - auto topQuery = queries.topQuery(player); - COMPLAIN_RET_FALSE_IF(!topQuery, "This player doesn't have any queries!"); - COMPLAIN_RET_FALSE_IF(topQuery->queryID != qid, "This player top query has different ID!"); - COMPLAIN_RET_FALSE_IF(!topQuery->endsByPlayerAnswer(), "This query cannot be ended by player's answer!"); - - if(auto dialogQuery = std::dynamic_pointer_cast(topQuery)) - dialogQuery->answer = answer; - - queries.popQuery(topQuery); - return true; -} - -static EndAction end_action; - -bool CGameHandler::makeBattleAction( BattleAction &ba ) -{ - bool ok = true; - - const CStack *stack = battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack - const CStack *destinationStack = ba.actionType == Battle::WALK_AND_ATTACK ? gs->curB->battleGetStackByPos(ba.additionalInfo) - : ba.actionType == Battle::SHOOT ? gs->curB->battleGetStackByPos(ba.destinationTile) - : nullptr; - const bool isAboutActiveStack = stack && (stack == battleActiveStack()); - - logGlobal->traceStream() << boost::format( - "Making action: type=%d; side=%d; stack=%s; dst=%s; additionalInfo=%d; stackAtDst=%s") - % ba.actionType % (int)ba.side % (stack ? stack->getName() : std::string("none")) - % ba.destinationTile % ba.additionalInfo % (destinationStack ? destinationStack->getName() : std::string("none")); - - switch(ba.actionType) - { - case Battle::WALK: //walk - case Battle::DEFEND: //defend - case Battle::WAIT: //wait - case Battle::WALK_AND_ATTACK: //walk or attack - case Battle::SHOOT: //shoot - case Battle::CATAPULT: //catapult - case Battle::STACK_HEAL: //healing with First Aid Tent - case Battle::DAEMON_SUMMONING: - case Battle::MONSTER_SPELL: - - if(!stack) - { - complain("No such stack!"); - return false; - } - if(!stack->alive()) - { - complain("This stack is dead: " + stack->nodeName()); - return false; - } - - if(battleTacticDist()) - { - if(stack && !stack->attackerOwned != battleGetTacticsSide()) - { - complain("This is not a stack of side that has tactics!"); - return false; - } - } - else if(!isAboutActiveStack) - { - complain("Action has to be about active stack!"); - return false; - } - } - - - switch(ba.actionType) - { - case Battle::END_TACTIC_PHASE: //wait - case Battle::BAD_MORALE: - case Battle::NO_ACTION: - { - StartAction start_action(ba); - sendAndApply(&start_action); - sendAndApply(&end_action); - break; - } - case Battle::WALK: - { - StartAction start_action(ba); - sendAndApply(&start_action); //start movement - int walkedTiles = moveStack(ba.stackNumber,ba.destinationTile); //move - if(!walkedTiles) - complain("Stack failed movement!"); - - sendAndApply(&end_action); - break; - } - case Battle::DEFEND: - { - //defensive stance //TODO: remove this bonus when stack becomes active - SetStackEffect sse; - sse.effect.push_back( Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL) ); - sse.effect.push_back( Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, gs->curB->stacks.at(ba.stackNumber)->valOfBonuses(Bonus::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE)); - sse.stacks.push_back(ba.stackNumber); - sendAndApply(&sse); - - //don't break - we share code with next case - } - case Battle::WAIT: - { - StartAction start_action(ba); - sendAndApply(&start_action); - sendAndApply(&end_action); - break; - } - case Battle::RETREAT: //retreat/flee - { - if(!gs->curB->battleCanFlee(gs->curB->sides.at(ba.side).color)) - complain("Cannot retreat!"); - else - setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case Battle::SURRENDER: - { - PlayerColor player = gs->curB->sides.at(ba.side).color; - int cost = gs->curB->battleGetSurrenderCost(player); - if(cost < 0) - complain("Cannot surrender!"); - else if(getResource(player, Res::GOLD) < cost) - complain("Not enough gold to surrender!"); - else - { - giveResource(player, Res::GOLD, -cost); - setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case Battle::WALK_AND_ATTACK: //walk or attack - { - StartAction start_action(ba); - sendAndApply(&start_action); //start movement and attack - - if(!stack || !destinationStack) - { - sendAndApply(&end_action); - break; - } - - BattleHex startingPos = stack->position; - int distance = moveStack(ba.stackNumber, ba.destinationTile); - - logGlobal->traceStream() << stack->nodeName() << " will attack " << destinationStack->nodeName(); - - if(stack->position != ba.destinationTile //we wasn't able to reach destination tile - && !(stack->doubleWide() - && ( stack->position == ba.destinationTile + (stack->attackerOwned ? +1 : -1 ) ) - ) //nor occupy specified hex - ) - { - std::string problem = "We cannot move this stack to its destination " + stack->getCreature()->namePl; - logGlobal->warnStream() << problem; - complain(problem); - ok = false; - sendAndApply(&end_action); - break; - } - - if(destinationStack && stack->ID == destinationStack->ID) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - complain(boost::str(boost::format("walk and attack error: no stack at additionalInfo tile (%d)!\n") % ba.additionalInfo)); - ok = false; - sendAndApply(&end_action); - break; - } - - if( !CStack::isMeleeAttackPossible(stack, destinationStack) ) - { - complain("Attack cannot be performed!"); - sendAndApply(&end_action); - ok = false; - break; - } - - //attack - - int totalAttacks = 1 + stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK), - (Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue(); //all unspicified attacks + melee attacks - - for (int i = 0; i < totalAttacks; ++i) - { - if (stack && - stack->alive() && //move can cause death, eg. by walking into the moat - destinationStack->alive()) - { - BattleAttack bat; - prepareAttack(bat, stack, destinationStack, (i ? 0 : distance), ba.additionalInfo); //no distance travelled on second attack - //prepareAttack(bat, stack, stackAtEnd, 0, ba.additionalInfo); - handleAttackBeforeCasting(bat); //only before first attack - sendAndApply(&bat); - handleAfterAttackCasting(bat); - } - - //counterattack - if (destinationStack - && !stack->hasBonusOfType(Bonus::BLOCKS_RETALIATION) - && destinationStack->ableToRetaliate() - && stack->alive()) //attacker may have died (fire shield) - { - BattleAttack bat; - prepareAttack(bat, destinationStack, stack, 0, stack->position); - bat.flags |= BattleAttack::COUNTER; - sendAndApply(&bat); - handleAfterAttackCasting(bat); - } - } - - //return - if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE) && startingPos != stack->position && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->ID == ba.stackNumber (rev 1431) - } - - sendAndApply(&end_action); - break; - } - case Battle::SHOOT: - { - if( !gs->curB->battleCanShoot(stack, ba.destinationTile) ) - { - complain("Cannot shoot!"); - break; - } - - StartAction start_action(ba); - sendAndApply(&start_action); //start shooting - - { - BattleAttack bat; - bat.flags |= BattleAttack::SHOT; - prepareAttack(bat, stack, destinationStack, 0, ba.destinationTile); - handleAttackBeforeCasting(bat); - sendAndApply(&bat); - handleAfterAttackCasting(bat); - } - - //second shot for ballista, only if hero has advanced artillery - - const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); - - if( destinationStack->alive() - && (stack->getCreature()->idNumber == CreatureID::BALLISTA) - && (attackingHero->getSecSkillLevel(SecondarySkill::ARTILLERY) >= SecSkillLevel::ADVANCED) - ) - { - BattleAttack bat2; - bat2.flags |= BattleAttack::SHOT; - prepareAttack(bat2, stack, destinationStack, 0, ba.destinationTile); - sendAndApply(&bat2); - } - //allow more than one additional attack - - int additionalAttacks = stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK), - (Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT))))->totalValue(); - for (int i = 0; i < additionalAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots - ) - { - BattleAttack bat; - bat.flags |= BattleAttack::SHOT; - prepareAttack(bat, stack, destinationStack, 0, ba.destinationTile); - sendAndApply(&bat); - handleAfterAttackCasting(bat); - } - } - - sendAndApply(&end_action); - break; - } - case Battle::CATAPULT: - { - auto getCatapultHitChance = [&](EWallPart::EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int - { - switch(part) - { - case EWallPart::GATE: - return sbi.gate; - case EWallPart::KEEP: - return sbi.keep; - case EWallPart::BOTTOM_TOWER: - case EWallPart::UPPER_TOWER: - return sbi.tower; - case EWallPart::BOTTOM_WALL: - case EWallPart::BELOW_GATE: - case EWallPart::OVER_GATE: - case EWallPart::UPPER_WALL: - return sbi.wall; - default: - return 0; - } - }; - - StartAction start_action(ba); - sendAndApply(&start_action); - auto onExit = vstd::makeScopeGuard([&]{ sendAndApply(&end_action); }); //if we started than we have to finish - - const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); - CHeroHandler::SBallisticsLevelInfo sbi = VLC->heroh->ballistics.at(attackingHero->getSecSkillLevel(SecondarySkill::BALLISTICS)); - - auto wallPart = gs->curB->battleHexToWallPart(ba.destinationTile); - if(!gs->curB->isWallPartPotentiallyAttackable(wallPart)) - { - complain("catapult tried to attack non-catapultable hex!"); - break; - } - - //in successive iterations damage is dealt but not yet subtracted from wall's HPs - auto ¤tHP = gs->curB->si.wallState; - - if (currentHP.at(wallPart) == EWallState::DESTROYED || currentHP.at(wallPart) == EWallState::NONE) - { - complain("catapult tried to attack already destroyed wall part!"); - break; - } - - for(int g=0; g allowedTargets; - for (size_t i=0; i< currentHP.size(); i++) - { - if (currentHP.at(i) != EWallState::DESTROYED && - currentHP.at(i) != EWallState::NONE) - allowedTargets.push_back(EWallPart::EWallPart(i)); - } - if (allowedTargets.empty()) - break; - attackedPart = allowedTargets.at(rand()%allowedTargets.size()); - } - } - while (!hitSuccessfull); - - if (!hitSuccessfull) // break triggered - no target to shoot at - break; - - CatapultAttack ca; //package for clients - CatapultAttack::AttackInfo attack; - attack.attackedPart = attackedPart; - attack.destinationTile = ba.destinationTile; - attack.damageDealt = 0; - - int dmgChance[] = { sbi.noDmg, sbi.oneDmg, sbi.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful - - int dmgRand = rand()%100; - //accumulating dmgChance - dmgChance[1] += dmgChance[0]; - dmgChance[2] += dmgChance[1]; - //calculating dealt damage - for(int damage = 0; damage < ARRAY_COUNT(dmgChance); ++damage) - { - if(dmgRand <= dmgChance[damage]) - { - attack.damageDealt = damage; - break; - } - } - // attacked tile may have changed - update destination - attack.destinationTile = gs->curB->wallPartToBattleHex(EWallPart::EWallPart(attack.attackedPart)); - - logGlobal->traceStream() << "Catapult attacks " << (int)attack.attackedPart - << " dealing " << (int)attack.damageDealt << " damage"; - - //removing creatures in turrets / keep if one is destroyed - if(attack.damageDealt > 0 && (attackedPart == EWallPart::KEEP || - attackedPart == EWallPart::BOTTOM_TOWER || attackedPart == EWallPart::UPPER_TOWER)) - { - int posRemove = -1; - switch(attackedPart) - { - case EWallPart::KEEP: - posRemove = -2; - break; - case EWallPart::BOTTOM_TOWER: - posRemove = -3; - break; - case EWallPart::UPPER_TOWER: - posRemove = -4; - break; - } - - BattleStacksRemoved bsr; - for(auto & elem : gs->curB->stacks) - { - if(elem->position == posRemove) - { - bsr.stackIDs.insert( elem->ID ); - break; - } - } - - sendAndApply(&bsr); - } - ca.attacker = ba.stackNumber; - ca.attackedParts.push_back(attack); - - sendAndApply(&ca); - } - //finish by scope guard - break; - } - case Battle::STACK_HEAL: //healing with First Aid Tent - { - StartAction start_action(ba); - sendAndApply(&start_action); - const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); - const CStack *healer = gs->curB->battleGetStackByID(ba.stackNumber), - *destStack = gs->curB->battleGetStackByPos(ba.destinationTile); - - if(healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER)) - { - complain("There is either no healer, no destination, or healer cannot heal :P"); - } - int maxHealable = destStack->MaxHealth() - destStack->firstHPleft; - int maxiumHeal = healer->count * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID)); - - int healed = std::min(maxHealable, maxiumHeal); - - if(healed == 0) - { - //nothing to heal.. should we complain? - } - else - { - StacksHealedOrResurrected shr; - shr.lifeDrain = false; - shr.tentHealing = true; - shr.drainedFrom = ba.stackNumber; - - StacksHealedOrResurrected::HealInfo hi; - hi.healedHP = healed; - hi.lowLevelResurrection = 0; - hi.stackID = destStack->ID; - - shr.healedStacks.push_back(hi); - sendAndApply(&shr); - } - - - sendAndApply(&end_action); - break; - } - case Battle::DAEMON_SUMMONING: - //TODO: From Strategija: - //Summon Demon is a level 2 spell. - { - StartAction start_action(ba); - sendAndApply(&start_action); - - const CStack *summoner = gs->curB->battleGetStackByID(ba.stackNumber), - *destStack = gs->curB->battleGetStackByPos(ba.destinationTile, false); - - BattleStackAdded bsa; - bsa.attacker = summoner->attackerOwned; - - bsa.creID = CreatureID(summoner->getBonusLocalFirst(Selector::type(Bonus::DAEMON_SUMMONING))->subtype); //in case summoner can summon more than one type of monsters... scream! - ui64 risedHp = summoner->count * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum()); - ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount; - - ui64 canRiseHp = std::min(targetHealth, risedHp); - ui32 canRiseAmount = canRiseHp / VLC->creh->creatures.at(bsa.creID)->MaxHealth(); - - bsa.amount = std::min(canRiseAmount, destStack->baseAmount); - - bsa.pos = gs->curB->getAvaliableHex(bsa.creID, bsa.attacker, destStack->position); - bsa.summoned = false; - - if (bsa.amount) //there's rare possibility single creature cannot rise desired type - { - BattleStacksRemoved bsr; //remove body - bsr.stackIDs.insert(destStack->ID); - sendAndApply(&bsr); - sendAndApply(&bsa); - - BattleSetStackProperty ssp; - ssp.stackID = ba.stackNumber; - ssp.which = BattleSetStackProperty::CASTS; //reduce number of casts - ssp.val = -1; - ssp.absolute = false; - sendAndApply(&ssp); - } - - sendAndApply(&end_action); - break; - } - case Battle::MONSTER_SPELL: - { - StartAction start_action(ba); - sendAndApply(&start_action); - - const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.additionalInfo); - BattleHex destination(ba.destinationTile); - - const Bonus *randSpellcaster = stack->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER)); - const Bonus * spellcaster = stack->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if(randSpellcaster && battleGetRandomStackSpell(stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = battleGetRandomStackSpell(stack, CBattleInfoCallback::RANDOM_GENIE); - - if(spellID < 0) - complain("That stack can't cast spells!"); - else - { - int spellLvl = 0; - if (spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if (randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - vstd::amin (spellLvl, 3); - - int casterSide = gs->curB->whatSide(stack->owner); - const CGHeroInstance * secHero = gs->curB->getHero(gs->curB->theOtherPlayer(stack->owner)); - - handleSpellCasting(spellID, spellLvl, destination, casterSide, stack->owner, nullptr, secHero, 0, ECastingMode::CREATURE_ACTIVE_CASTING, stack); - } - sendAndApply(&end_action); - break; - } - } - if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished - battleMadeAction.setn(true); - return ok; -} - -void CGameHandler::playerMessage( PlayerColor player, const std::string &message ) -{ - bool cheated=true; - PlayerMessage temp_message(player, message); - - sendAndApply(&temp_message); - if(message == "vcmiistari") //give all spells and 999 mana - { - SetMana sm; - ChangeSpells cs; - - CGHeroInstance *h = gs->getHero(gs->getPlayer(player)->currentSelection); - if(!h && complain("Cannot realize cheat, no hero selected!")) return; - - sm.hid = cs.hid = h->id; - - //give all spells - cs.learn = 1; - for(auto spell : VLC->spellh->objects) - { - if(!spell->creatureAbility) - cs.spells.insert(spell->id); - } - - //give mana - sm.val = 999; - - if(!h->hasSpellbook()) //hero doesn't have spellbook - giveHeroNewArtifact(h, VLC->arth->artifacts.at(0), ArtifactPosition::SPELLBOOK); //give spellbook - - sendAndApply(&cs); - sendAndApply(&sm); - } - else if (message == "vcmiarmenelos") //build all buildings in selected town - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - CGTownInstance *town; - - if (hero) - town = hero->visitedTown; - else - town = gs->getTown(gs->getPlayer(player)->currentSelection); - - if (town) - { - for (auto & build : town->town->buildings) - { - if (!town->hasBuilt(build.first) - && !build.second->Name().empty() - && build.first != BuildingID::SHIP) - { - buildStructure(town->id, build.first, true); - } - } - } - } - else if(message == "vcmiainur") //gives 5 archangels into each slot - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - const CCreature *archangel = VLC->creh->creatures.at(13); - if(!hero) return; - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - if(!hero->hasStackAtSlot(SlotID(i))) - insertNewStack(StackLocation(hero, SlotID(i)), archangel, 5); - } - else if(message == "vcmiangband") //gives 10 black knight into each slot - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - const CCreature *blackKnight = VLC->creh->creatures.at(66); - if(!hero) return; - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - if(!hero->hasStackAtSlot(SlotID(i))) - insertNewStack(StackLocation(hero, SlotID(i)), blackKnight, 10); - } - else if(message == "vcminoldor") //all war machines - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - if(!hero) return; - - if(!hero->getArt(ArtifactPosition::MACH1)) - giveHeroNewArtifact(hero, VLC->arth->artifacts.at(4), ArtifactPosition::MACH1); - if(!hero->getArt(ArtifactPosition::MACH2)) - giveHeroNewArtifact(hero, VLC->arth->artifacts.at(5), ArtifactPosition::MACH2); - if(!hero->getArt(ArtifactPosition::MACH3)) - giveHeroNewArtifact(hero, VLC->arth->artifacts.at(6), ArtifactPosition::MACH3); - } - else if(message == "vcminahar") //1000000 movement points - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - if(!hero) return; - SetMovePoints smp; - smp.hid = hero->id; - smp.val = 1000000; - sendAndApply(&smp); - } - else if(message == "vcmiformenos") //give resources - { - SetResources sr; - sr.player = player; - sr.res = gs->getPlayer(player)->resources; - for(int i=0;imap->width * gs->map->height * (gs->map->twoLevel ? 2 : 1)]; - int lastUnc = 0; - for(int i=0;imap->width;i++) - for(int j=0;jmap->height;j++) - for(int k = 0; k < (gs->map->twoLevel ? 2 : 1); k++) - if(!gs->getPlayerTeam(fc.player)->fogOfWarMap.at(i).at(j).at(k)) - hlp_tab[lastUnc++] = int3(i,j,k); - fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); - delete [] hlp_tab; - sendAndApply(&fc); - } - else if(message == "vcmiglorfindel") //selected hero gains a new level - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level+1) - VLC->heroh->reqExp(hero->level)); - } - else if(message == "vcmisilmaril") //player wins - { - gs->getPlayer(player)->enteredWinningCheatCode = 1; - } - else if(message == "vcmimelkor") //player looses - { - gs->getPlayer(player)->enteredLosingCheatCode = 1; - } - else if (message == "vcmiforgeofnoldorking") //hero gets all artifacts except war machines, spell scrolls and spell book - { - CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); - if(!hero) return; - for (int g = 7; g < VLC->arth->artifacts.size(); ++g) //including artifacts from mods - giveHeroNewArtifact(hero, VLC->arth->artifacts.at(g), ArtifactPosition::PRE_FIRST); - } - else - cheated = false; - if(cheated) - { - SystemMessage temp_message(VLC->generaltexth->allTexts.at(260)); - sendAndApply(&temp_message); - checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature - } -} - -void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex destination, ui8 casterSide, PlayerColor casterColor, const CGHeroInstance * caster, const CGHeroInstance * secHero, - int usedSpellPower, ECastingMode::ECastingMode mode, const CStack * stack, si32 selectedStack) -{ - const CSpell *spell = SpellID(spellID).toSpell(); - - - //Helper local function that creates obstacle on given position. Obstacle type is inferred from spell type. - //It creates, sends and applies needed package. - auto placeObstacle = [&](BattleHex pos) - { - static int obstacleIdToGive = gs->curB->obstacles.size() - ? (gs->curB->obstacles.back()->uniqueID+1) - : 0; - - auto obstacle = make_shared(); - switch(spellID.toEnum()) // :/ - { - case SpellID::QUICKSAND: - obstacle->obstacleType = CObstacleInstance::QUICKSAND; - obstacle->turnsRemaining = -1; - obstacle->visibleForAnotherSide = false; - break; - case SpellID::LAND_MINE: - obstacle->obstacleType = CObstacleInstance::LAND_MINE; - obstacle->turnsRemaining = -1; - obstacle->visibleForAnotherSide = false; - break; - case SpellID::FIRE_WALL: - obstacle->obstacleType = CObstacleInstance::FIRE_WALL; - obstacle->turnsRemaining = 2; - obstacle->visibleForAnotherSide = true; - break; - case SpellID::FORCE_FIELD: - obstacle->obstacleType = CObstacleInstance::FORCE_FIELD; - obstacle->turnsRemaining = 2; - obstacle->visibleForAnotherSide = true; - break; - default: - //this function cannot be used with spells that do not create obstacles - assert(0); - } - - obstacle->pos = pos; - obstacle->casterSide = casterSide; - obstacle->ID = spellID; - obstacle->spellLevel = spellLvl; - obstacle->casterSpellPower = usedSpellPower; - obstacle->uniqueID = obstacleIdToGive++; - - BattleObstaclePlaced bop; - bop.obstacle = obstacle; - sendAndApply(&bop); - }; - - BattleSpellCast sc; - sc.side = casterSide; - sc.id = spellID; - sc.skill = spellLvl; - sc.tile = destination; - sc.dmgToDisplay = 0; - sc.castedByHero = (bool)caster; - sc.attackerType = (stack ? stack->type->idNumber : CreatureID(CreatureID::NONE)); - sc.manaGained = 0; - sc.spellCost = 0; - - if (caster) //calculate spell cost - { - sc.spellCost = gs->curB->battleGetSpellCost(SpellID(spellID).toSpell(), caster); - - if (secHero && mode == ECastingMode::HERO_CASTING) //handle mana channel - { - int manaChannel = 0; - for(CStack * stack : gs->curB->stacks) //TODO: shouldn't bonus system handle it somehow? - { - if (stack->owner == secHero->tempOwner) - { - vstd::amax(manaChannel, stack->valOfBonuses(Bonus::MANA_CHANNELING)); - } - } - sc.manaGained = (manaChannel * sc.spellCost) / 100; - } - } - - //calculating affected creatures for all spells - //must be vector, as in Chain Lightning order matters - std::vector attackedCres; //what is that and what is sc.afectedCres? - if (mode != ECastingMode::ENCHANTER_CASTING) - { - auto creatures = gs->curB->getAffectedCreatures(spell, spellLvl, casterColor, destination); - std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres)); - } - else //enchanter - hit all possible stacks - { - for (const CStack * stack : gs->curB->stacks) - { - /*if it's non negative spell and our unit or non positive spell and hostile unit */ - if((!spell->isNegative() && stack->owner == casterColor) - || (!spell->isPositive() && stack->owner != casterColor)) - { - if(stack->isValidTarget()) //TODO: allow dead targets somewhere in the future - { - attackedCres.push_back(stack); - } - } - } - } - - for (auto cre : attackedCres) - { - sc.affectedCres.insert (cre->ID); - } - - //checking if creatures resist - sc.resisted = gs->curB->calculateResistedStacks(spell, caster, secHero, attackedCres, casterColor, mode, usedSpellPower, spellLvl); - - //calculating dmg to display - if (spellID == SpellID::DEATH_STARE || spellID == SpellID::ACID_BREATH_DAMAGE) - { - sc.dmgToDisplay = usedSpellPower; - if (spellID == SpellID::DEATH_STARE) - vstd::amin(sc.dmgToDisplay, (*attackedCres.begin())->count); //stack is already reduced after attack - } - StacksInjured si; - - //applying effects - - if (spell->isOffensiveSpell()) - { - int spellDamage = 0; - if (stack && mode != ECastingMode::MAGIC_MIRROR) - { - int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum()); - if (unitSpellPower) - sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities - else //Faerie Dragon - { - usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100; - sc.dmgToDisplay = 0; - } - } - int chainLightningModifier = 0; - for(auto & attackedCre : attackedCres) - { - if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell - continue; - - BattleStackAttacked bsa; - if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->getLevelInfo(spellLvl).range == "X" || mode == ECastingMode::ENCHANTER_CASTING)) - //display effect only upon primary target of area spell - //FIXME: if no stack is attacked, there is no animation and interface freezes - { - bsa.flags |= BattleStackAttacked::EFFECT; - bsa.effect = spell->mainEffectAnim; - } - if (spellDamage) - bsa.damageAmount = spellDamage >> chainLightningModifier; - else - bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier; - - sc.dmgToDisplay += bsa.damageAmount; - - bsa.stackAttacked = (attackedCre)->ID; - if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast - bsa.attackerID = stack->ID; - else - bsa.attackerID = -1; - (attackedCre)->prepareAttacked(bsa); - si.stacks.push_back(bsa); - - if (spellID == SpellID::CHAIN_LIGHTNING) - ++chainLightningModifier; - } - } - - if (spell->hasEffects()) - { - int stackSpellPower = 0; - if (stack && mode != ECastingMode::MAGIC_MIRROR) - { - stackSpellPower = stack->valOfBonuses(Bonus::CREATURE_ENCHANT_POWER); - } - SetStackEffect sse; - Bonus pseudoBonus; - pseudoBonus.sid = spellID; - pseudoBonus.val = spellLvl; - pseudoBonus.turnsRemain = gs->curB->calculateSpellDuration(spell, caster, stackSpellPower ? stackSpellPower : usedSpellPower); - CStack::stackEffectToFeature(sse.effect, pseudoBonus); - if (spellID == SpellID::SHIELD || spellID == SpellID::AIR_SHIELD) - { - sse.effect.back().val = (100 - sse.effect.back().val); //fix to original config: shield should display damage reduction - } - if (spellID == SpellID::BIND && stack)//bind - { - sse.effect.back().additionalInfo = stack->ID; //we need to know who casted Bind - } - const Bonus * bonus = nullptr; - if (caster) - bonus = caster->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, spellID)); - //TODO does hero specialty should affects his stack casting spells? - - si32 power = 0; - for(const CStack *affected : attackedCres) - { - if(vstd::contains(sc.resisted, affected->ID)) //this creature resisted the spell - continue; - sse.stacks.push_back(affected->ID); - - //Apply hero specials - peculiar enchants - const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines) - if (bonus) - { - switch(bonus->additionalInfo) - { - case 0: //normal - { - switch(tier) - { - case 1: case 2: - power = 3; - break; - case 3: case 4: - power = 2; - break; - case 5: case 6: - power = 1; - break; - } - Bonus specialBonus(sse.effect.back()); - specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely - sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); //additional premy to given effect - } - break; - case 1: //only Coronius as yet - { - power = std::max(5 - tier, 0); - Bonus specialBonus = CStack::featureGenerator(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK, power, pseudoBonus.turnsRemain); - specialBonus.sid = spellID; - sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); //additional attack to Slayer effect - } - break; - } - } - if (caster && caster->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, spellID)) //TODO: better handling of bonus percentages - { - int damagePercent = caster->level * caster->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, spellID.toEnum()) / tier; - Bonus specialBonus = CStack::featureGenerator(Bonus::CREATURE_DAMAGE, 0, damagePercent, pseudoBonus.turnsRemain); - specialBonus.valType = Bonus::PERCENT_TO_ALL; - specialBonus.sid = spellID; - sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); - } - } - - if(!sse.stacks.empty()) - sendAndApply(&sse); - - } - - if (spell->isRisingSpell() || spell->id == SpellID::CURE) - { - int hpGained = 0; - if (stack) - { - int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum()); - if (unitSpellPower) - hpGained = stack->count * unitSpellPower; //Archangel - else //Faerie Dragon-like effect - unused fo far - usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100; - } - StacksHealedOrResurrected shr; - shr.lifeDrain = (ui8)false; - shr.tentHealing = (ui8)false; - for(auto & attackedCre : attackedCres) - { - if(vstd::contains(sc.resisted, (attackedCre)->ID) //this creature resisted the spell - || (spellID == SpellID::ANIMATE_DEAD && !(attackedCre)->hasBonusOfType(Bonus::UNDEAD)) //we try to cast animate dead on living stack, TODO: showuld be not affected earlier - ) - continue; - StacksHealedOrResurrected::HealInfo hi; - hi.stackID = (attackedCre)->ID; - if (stack) //casted by creature - { - if (hpGained) - { - hi.healedHP = gs->curB->calculateHealedHP(hpGained, spell, attackedCre); //archangel - } - else - hi.healedHP = gs->curB->calculateHealedHP(spell, usedSpellPower, spellLvl, attackedCre); //any typical spell (commander's cure or animate dead) - } - else - hi.healedHP = gs->curB->calculateHealedHP(caster, spell, attackedCre, gs->curB->battleGetStackByID(selectedStack)); //Casted by hero - hi.lowLevelResurrection = spellLvl <= 1; - shr.healedStacks.push_back(hi); - } - if(!shr.healedStacks.empty()) - sendAndApply(&shr); - if (spellID == SpellID::SACRIFICE) //remove victim - { - if (selectedStack == gs->curB->activeStack) - //set another active stack than the one removed, or bad things will happen - //TODO: make that part of BattleStacksRemoved? what about client update? - { - //makeStackDoNothing(gs->curB->getStack (selectedStack)); - - BattleSetActiveStack sas; - - //std::vector hlp; - //battleGetStackQueue(hlp, 1, selectedStack); //next after this one - - //if(hlp.size()) - //{ - // sas.stack = hlp[0]->ID; - //} - //else - // complain ("No new stack to activate!"); - sas.stack = gs->curB->getNextStack()->ID; //why the hell next stack has same ID as current? - sendAndApply(&sas); - - } - BattleStacksRemoved bsr; - bsr.stackIDs.insert (selectedStack); //somehow it works for teleport? - sendAndApply(&bsr); - } - } - - switch (spellID) - { - case SpellID::QUICKSAND: - case SpellID::LAND_MINE: - { - std::vector availableTiles; - for(int i = 0; i < GameConstants::BFIELD_SIZE; i += 1) - { - BattleHex hex = i; - if(hex.getX() > 2 && hex.getX() < 14 && !battleGetStackByPos(hex, false) && !battleGetObstacleOnPos(hex, false)) - availableTiles.push_back(hex); - } - boost::range::random_shuffle(availableTiles); - - const int patchesForSkill[] = {4, 4, 6, 8}; - const int patchesToPut = std::min(patchesForSkill[spellLvl], availableTiles.size()); - - //land mines or quicksand patches are handled as spell created obstacles - for (int i = 0; i < patchesToPut; i++) - placeObstacle(availableTiles.at(i)); - } - - break; - case SpellID::FORCE_FIELD: - placeObstacle(destination); - break; - case SpellID::FIRE_WALL: - { - //fire wall is build from multiple obstacles - one fire piece for each affected hex - auto affectedHexes = spell->rangeInHexes(destination, spellLvl, casterSide); - for(BattleHex hex : affectedHexes) - placeObstacle(hex); - } - break; - case SpellID::TELEPORT: - { - BattleStackMoved bsm; - bsm.distance = -1; - bsm.stack = selectedStack; - std::vector tiles; - tiles.push_back(destination); - bsm.tilesToMove = tiles; - bsm.teleporting = true; - sendAndApply(&bsm); - break; - } - case SpellID::SUMMON_FIRE_ELEMENTAL: - case SpellID::SUMMON_EARTH_ELEMENTAL: - case SpellID::SUMMON_WATER_ELEMENTAL: - case SpellID::SUMMON_AIR_ELEMENTAL: - { //elemental summoning - CreatureID creID; - switch(spellID) - { - case SpellID::SUMMON_FIRE_ELEMENTAL: - creID = CreatureID::FIRE_ELEMENTAL; - break; - case SpellID::SUMMON_EARTH_ELEMENTAL: - creID = CreatureID::EARTH_ELEMENTAL; - break; - case SpellID::SUMMON_WATER_ELEMENTAL: - creID = CreatureID::WATER_ELEMENTAL; - break; - case SpellID::SUMMON_AIR_ELEMENTAL: - creID = CreatureID::AIR_ELEMENTAL; - break; - } - - BattleStackAdded bsa; - bsa.creID = creID; - bsa.attacker = !(bool)casterSide; - bsa.summoned = true; - bsa.pos = gs->curB->getAvaliableHex(creID, !(bool)casterSide); //TODO: unify it - - //TODO stack casting -> probably power will be zero; set the proper number of creatures manually - int percentBonus = caster ? caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, spellID.toEnum()) : 0; - - bsa.amount = usedSpellPower - * SpellID(spellID).toSpell()->getPower(spellLvl) - * (100 + percentBonus) / 100.0; //new feature - percentage bonus - if(bsa.amount) - sendAndApply(&bsa); - else - complain("Summoning elementals didn't summon any!"); - } - break; - case SpellID::CLONE: - { - const CStack * clonedStack = nullptr; - if (attackedCres.size()) - clonedStack = *attackedCres.begin(); - if (!clonedStack) - { - complain ("No target stack to clone!"); - break; - } - - BattleStackAdded bsa; - bsa.creID = clonedStack->type->idNumber; - bsa.attacker = !(bool)casterSide; - bsa.summoned = true; - bsa.pos = gs->curB->getAvaliableHex(bsa.creID, !(bool)casterSide); //TODO: unify it - bsa.amount = clonedStack->count; - sendAndApply (&bsa); - - BattleSetStackProperty ssp; - ssp.stackID = gs->curB->stacks.back()->ID; //how to get recent stack? - ssp.which = BattleSetStackProperty::CLONED; //using enum values - ssp.val = 0; - ssp.absolute = 1; - sendAndApply(&ssp); - } - break; - case SpellID::REMOVE_OBSTACLE: - { - if(auto obstacleToRemove = battleGetObstacleOnPos(destination, false)) - { - ObstaclesRemoved obr; - obr.obstacles.insert(obstacleToRemove->uniqueID); - sendAndApply(&obr); - } - else - complain("There's no obstacle to remove!"); - } - break; - case SpellID::DEATH_STARE: //handled in a bit different way - { - for(auto & attackedCre : attackedCres) - { - if((attackedCre)->hasBonusOfType(Bonus::UNDEAD) || (attackedCre)->hasBonusOfType(Bonus::NON_LIVING)) //this creature is immune - { - sc.dmgToDisplay = 0; //TODO: handle Death Stare for multiple targets (?) - continue; - } - - BattleStackAttacked bsa; - bsa.flags |= BattleStackAttacked::EFFECT; - bsa.effect = spell->mainEffectAnim; //from config\spell-Info.txt - bsa.damageAmount = usedSpellPower * (attackedCre)->valOfBonuses(Bonus::STACK_HEALTH); - bsa.stackAttacked = (attackedCre)->ID; - bsa.attackerID = -1; - (attackedCre)->prepareAttacked(bsa); - si.stacks.push_back(bsa); - } - } - break; - case SpellID::ACID_BREATH_DAMAGE: //new effect, separate from acid breath defense reduction - { - for(auto & attackedCre : attackedCres) //no immunities - { - BattleStackAttacked bsa; - bsa.flags |= BattleStackAttacked::EFFECT; - bsa.effect = spell->mainEffectAnim; - bsa.damageAmount = usedSpellPower; //damage times the number of attackers - bsa.stackAttacked = (attackedCre)->ID; - bsa.attackerID = -1; - (attackedCre)->prepareAttacked(bsa); - si.stacks.push_back(bsa); - } - } - break; - } - - sendAndApply(&sc); - if(!si.stacks.empty()) //after spellcast info shows - sendAndApply(&si); - - if (mode == ECastingMode::CREATURE_ACTIVE_CASTING || mode == ECastingMode::ENCHANTER_CASTING) //reduce number of casts remaining - { - BattleSetStackProperty ssp; - ssp.stackID = stack->ID; - ssp.which = BattleSetStackProperty::CASTS; - ssp.val = -1; - ssp.absolute = false; - sendAndApply(&ssp); - } - - //Magic Mirror effect - if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence - { - for(auto & attackedCre : attackedCres) - { - int mirrorChance = (attackedCre)->valOfBonuses(Bonus::MAGIC_MIRROR); - if(mirrorChance > rand()%100) - { - std::vector mirrorTargets; - std::vector & battleStacks = gs->curB->stacks; - for (auto & battleStack : battleStacks) - { - if(battleStack->owner == gs->curB->sides.at(casterSide).color) //get enemy stacks which can be affected by this spell - { - if (!gs->curB->battleIsImmune(nullptr, spell, ECastingMode::MAGIC_MIRROR, battleStack->position)) - mirrorTargets.push_back(battleStack); - } - } - if (!mirrorTargets.empty()) - { - int targetHex = mirrorTargets.at(rand() % mirrorTargets.size())->position; - handleSpellCasting(spellID, 0, targetHex, 1 - casterSide, (attackedCre)->owner, nullptr, (caster ? caster : nullptr), usedSpellPower, ECastingMode::MAGIC_MIRROR, (attackedCre)); - } - } - } - } -} - -bool CGameHandler::makeCustomAction( BattleAction &ba ) -{ - switch(ba.actionType) - { - case Battle::HERO_SPELL: - { - COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); - - - const CGHeroInstance *h = gs->curB->battleGetFightingHero(ba.side); - const CGHeroInstance *secondHero = gs->curB->battleGetFightingHero(!ba.side); - if(!h) - { - logGlobal->warnStream() << "Wrong caster!"; - return false; - } - if(ba.additionalInfo >= VLC->spellh->objects.size()) - { - logGlobal->warnStream() << "Wrong spell id (" << ba.additionalInfo << ")!"; - return false; - } - - const CSpell *s = SpellID(ba.additionalInfo).toSpell(); - if (s->mainEffectAnim > -1 - || s->id == SpellID::CLONE - || s->id >= SpellID::SUMMON_FIRE_ELEMENTAL - || s->id <= SpellID::SUMMON_AIR_ELEMENTAL - || s->id <= SpellID::SUMMON_EARTH_ELEMENTAL - || s->id <= SpellID::SUMMON_WATER_ELEMENTAL) - //TODO: special effects, like Clone - { - ui8 skill = h->getSpellSchoolLevel(s); //skill level - - ESpellCastProblem::ESpellCastProblem escp = gs->curB->battleCanCastThisSpell(h->tempOwner, s, ECastingMode::HERO_CASTING); - if(escp != ESpellCastProblem::OK) - { - logGlobal->warnStream() << "Spell cannot be cast!"; - logGlobal->warnStream() << "Problem : " << escp; - return false; - } - - StartAction start_action(ba); - sendAndApply(&start_action); //start spell casting - - handleSpellCasting (SpellID(ba.additionalInfo), skill, ba.destinationTile, ba.side, h->tempOwner, - h, secondHero, h->getPrimSkillLevel(PrimarySkill::SPELL_POWER), - ECastingMode::HERO_CASTING, nullptr, ba.selectedStack); - - sendAndApply(&end_action); - if( !gs->curB->battleGetStackByID(gs->curB->activeStack, true)) - { - battleMadeAction.setn(true); - } - checkForBattleEnd(); - if(battleResult.get()) - { - battleMadeAction.setn(true); - //battle will be ended by startBattle function - //endBattle(gs->curB->tile, gs->curB->heroes[0], gs->curB->heroes[1]); - } - - return true; - } - else - { - logGlobal->warnStream() << "Spell " << s->name << " is not yet supported!"; - return false; - } - } - } - return false; -} - -void CGameHandler::stackTurnTrigger(const CStack * st) -{ - BattleTriggerEffect bte; - bte.stackID = st->ID; - bte.effect = -1; - bte.val = 0; - bte.additionalInfo = 0; - if (st->alive()) - { - //unbind - if (st->getEffect (SpellID::BIND)) - { - bool unbind = true; - BonusList bl = *(st->getBonuses(Selector::type(Bonus::BIND_EFFECT))); - std::set stacks = gs->curB-> batteAdjacentCreatures(st); - - for(Bonus * b : bl) - { - const CStack * stack = gs->curB->battleGetStackByID(b->additionalInfo); //binding stack must be alive and adjacent - if (stack) - { - if (vstd::contains(stacks, stack)) //binding stack is still present - { - unbind = false; - } - } - } - if (unbind) - { - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::UNBIND; - ssp.stackID = st->ID; - sendAndApply(&ssp); - } - } - //regeneration - if(st->hasBonusOfType(Bonus::HP_REGENERATION)) - { - bte.effect = Bonus::HP_REGENERATION; - bte.val = std::min((int)(st->MaxHealth() - st->firstHPleft), st->valOfBonuses(Bonus::HP_REGENERATION)); - } - if(st->hasBonusOfType(Bonus::FULL_HP_REGENERATION)) - { - bte.effect = Bonus::HP_REGENERATION; - bte.val = st->MaxHealth() - st->firstHPleft; - } - if (bte.val) //anything to heal - sendAndApply(&bte); - - if(st->hasBonusOfType(Bonus::POISON)) - { - const Bonus * b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON).And(Selector::type(Bonus::STACK_HEALTH))); - if (b) //TODO: what if not?... - { - bte.val = std::max (b->val - 10, -(st->valOfBonuses(Bonus::POISON))); - if (bte.val < b->val) //(negative) poison effect increases - update it - { - bte.effect = Bonus::POISON; - sendAndApply(&bte); - } - } - } - if (st->hasBonusOfType(Bonus::MANA_DRAIN) && !vstd::contains(st->state, EBattleStackState::DRAINED_MANA)) - { - const CGHeroInstance * enemy = gs->curB->getHero(gs->curB->theOtherPlayer(st->owner)); - //const CGHeroInstance * owner = gs->curB->getHero(st->owner); - if (enemy) - { - ui32 manaDrained = st->valOfBonuses(Bonus::MANA_DRAIN); - vstd::amin(manaDrained, gs->curB->battleGetFightingHero(0)->mana); - if (manaDrained) - { - bte.effect = Bonus::MANA_DRAIN; - bte.val = manaDrained; - bte.additionalInfo = enemy->id.getNum(); //for sanity - sendAndApply(&bte); - } - } - } - if (st->isLiving() && !st->hasBonusOfType(Bonus::FEARLESS)) - { - bool fearsomeCreature = false; - for(CStack * stack : gs->curB->stacks) - { - if (stack->owner != st->owner && stack->alive() && stack->hasBonusOfType(Bonus::FEAR)) - { - fearsomeCreature = true; - break; - } - } - if (fearsomeCreature) - { - if (rand() % 100 < 10) //fixed 10% - { - bte.effect = Bonus::FEAR; - sendAndApply(&bte); - } - } - } - BonusList bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTER))); - int side = gs->curB->whatSide(st->owner); - if (bl.size() && st->casts && !gs->curB->sides.at(side).enchanterCounter) - { - int index = rand() % bl.size(); - SpellID spellID = SpellID(bl[index]->subtype); - if (gs->curB->battleCanCastThisSpell(st->owner, SpellID(spellID).toSpell(), ECastingMode::ENCHANTER_CASTING) == ESpellCastProblem::OK) //TODO: select another available? - { - int spellLeveL = bl[index]->val; //spell level - const CGHeroInstance * enemyHero = gs->curB->getHero(gs->curB->theOtherPlayer(st->owner)); - handleSpellCasting(spellID, spellLeveL, -1, side, st->owner, nullptr, enemyHero, 0, ECastingMode::ENCHANTER_CASTING, st); - - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; - ssp.absolute = false; - ssp.val = bl[index]->additionalInfo; //increase cooldown counter - ssp.stackID = st->ID; - sendAndApply(&ssp); - } - } - bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTED))); - for (auto b : bl) - { - SetStackEffect sse; - int val = bl.valOfBonuses (Selector::typeSubtype(b->type, b->subtype)); - if (val > 3) - { - for (auto s : gs->curB->battleGetAllStacks()) - { - if (st->owner == s->owner && s->isValidTarget()) //all allied - sse.stacks.push_back (s->ID); - } - } - else - sse.stacks.push_back (st->ID); - - Bonus pseudoBonus; - pseudoBonus.sid = b->subtype; - pseudoBonus.val = ((val > 3) ? (val - 3) : val); - pseudoBonus.turnsRemain = 50; - st->stackEffectToFeature (sse.effect, pseudoBonus); - if (sse.effect.size()) - sendAndApply (&sse); - } - } -} - -void CGameHandler::handleDamageFromObstacle(const CObstacleInstance &obstacle, const CStack * curStack) -{ - //we want to determine following vars depending on obstacle type - int damage = -1; - int effect = -1; - bool oneTimeObstacle = false; - - //helper info - const SpellCreatedObstacle *spellObstacle = dynamic_cast(&obstacle); //not nice but we may need spell params - const ui8 side = !curStack->attackerOwned; //if enemy is defending (false = 0), side of enemy hero is 1 (true) - const CGHeroInstance *hero = gs->curB->battleGetFightingHero(side); - - if(obstacle.obstacleType == CObstacleInstance::MOAT) - { - damage = battleGetMoatDmg(); - } - else if(obstacle.obstacleType == CObstacleInstance::LAND_MINE) - { - //You don't get hit by a Mine you can see. - if(gs->curB->battleIsObstacleVisibleForSide(obstacle, (BattlePerspective::BattlePerspective)side)) - return; - - oneTimeObstacle = true; - effect = 82; //makes - damage = gs->curB->calculateSpellDmg(SpellID(SpellID::LAND_MINE).toSpell(), hero, curStack, - spellObstacle->spellLevel, spellObstacle->casterSpellPower); - //TODO even if obstacle wasn't created by hero (Tower "moat") it should deal dmg as if casted by hero, - //if it is bigger than default dmg. Or is it just irrelevant H3 implementation quirk - } - else if(obstacle.obstacleType == CObstacleInstance::FIRE_WALL) - { - damage = gs->curB->calculateSpellDmg(SpellID(SpellID::FIRE_WALL).toSpell(), hero, curStack, - spellObstacle->spellLevel, spellObstacle->casterSpellPower); - } - else - { - //no other obstacle does damage to stack - return; - } - - BattleStackAttacked bsa; - if(effect >= 0) - { - bsa.flags |= BattleStackAttacked::EFFECT; - bsa.effect = effect; //makes POOF - } - bsa.damageAmount = damage; - bsa.stackAttacked = curStack->ID; - bsa.attackerID = -1; - curStack->prepareAttacked(bsa); - - StacksInjured si; - si.stacks.push_back(bsa); - sendAndApply(&si); - - if(oneTimeObstacle) - removeObstacle(obstacle); -} - -void CGameHandler::handleTimeEvents() -{ - gs->map->events.sort(evntCmp); - while(gs->map->events.size() && gs->map->events.front().firstOccurence+1 == gs->day) - { - CMapEvent ev = gs->map->events.front(); - for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) - { - PlayerState *pinfo = gs->getPlayer(PlayerColor(player)); - - if( pinfo //player exists - && (ev.players & 1<human) - || (ev.humanAffected && pinfo->human) - ) - ) - { - //give resources - SetResources sr; - sr.player = PlayerColor(player); - sr.res = pinfo->resources + ev.resources; - - //prepare dialog - InfoWindow iw; - iw.player = PlayerColor(player); - iw.text << ev.message; - - for (int i=0; imap->events.pop_front(); - - ev.firstOccurence += ev.nextOccurence; - auto it = gs->map->events.begin(); - while ( it !=gs->map->events.end() && it->earlierThanOrEqual(ev)) - it++; - gs->map->events.insert(it, ev); - } - else - { - gs->map->events.pop_front(); - } - } - - //TODO send only if changed - UpdateMapEvents ume; - ume.events = gs->map->events; - sendAndApply(&ume); -} - -void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) -{ - town->events.sort(evntCmp); - while(town->events.size() && town->events.front().firstOccurence == gs->day) - { - PlayerColor player = town->tempOwner; - CCastleEvent ev = town->events.front(); - PlayerState *pinfo = gs->getPlayer(player); - - if( pinfo //player exists - && (ev.players & 1<human) - || (ev.humanAffected && pinfo->human) ) ) - { - - - // dialog - InfoWindow iw; - iw.player = player; - iw.text << ev.message; - - if(ev.resources.nonZero()) - { - TResources was = n.res[player]; - n.res[player] += ev.resources; - n.res[player].amax(0); - - for (int i=0; iresources.at(i) != n.res.at(player).at(i)) //if resource had changed, we add it to the dialog - iw.components.push_back(Component(Component::RESOURCE,i,n.res.at(player).at(i)-was.at(i),0)); - - } - - for(auto & i : ev.buildings) - { - if ( town->hasBuilt(i)) - { - buildStructure(town->id, i, true); - iw.components.push_back(Component(Component::BUILDING, town->subID, i, 0)); - } - } - - if (!ev.creatures.empty() && !vstd::contains(n.cres, town->id)) - { - n.cres[town->id].tid = town->id; - n.cres[town->id].creatures = town->creatures; - } - auto & sac = n.cres[town->id]; - - for(si32 i=0;icreatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling - { - sac.creatures[i].first += ev.creatures.at(i); - iw.components.push_back(Component(Component::CREATURE, - town->creatures.at(i).second.back(), ev.creatures.at(i), 0)); - } - } - sendAndApply(&iw); //show dialog - } - - if(ev.nextOccurence) - { - town->events.pop_front(); - - ev.firstOccurence += ev.nextOccurence; - auto it = town->events.begin(); - while ( it != town->events.end() && it->earlierThanOrEqual(ev)) - it++; - town->events.insert(it, ev); - } - else - { - town->events.pop_front(); - } - } - - //TODO send only if changed - UpdateCastleEvents uce; - uce.town = town->id; - uce.events = town->events; - sendAndApply(&uce); -} - -bool CGameHandler::complain( const std::string &problem ) -{ - sendMessageToAll("Server encountered a problem: " + problem); - logGlobal->errorStream() << problem; - return true; -} - -void CGameHandler::showGarrisonDialog( ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) -{ - //PlayerColor player = getOwner(hid); - auto upperArmy = dynamic_cast(getObj(upobj)); - auto lowerArmy = dynamic_cast(getObj(hid)); - - assert(lowerArmy); - assert(upperArmy); - - auto garrisonQuery = make_shared(upperArmy, lowerArmy); - queries.addQuery(garrisonQuery); - - GarrisonDialog gd; - gd.hid = hid; - gd.objid = upobj; - gd.removableUnits = removableUnits; - gd.queryID = garrisonQuery->queryID; - sendAndApply(&gd); -} - -void CGameHandler::showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) -{ - OpenWindow ow; - ow.window = OpenWindow::THIEVES_GUILD; - ow.id1 = player.getNum(); - ow.id2 = requestingObjId.getNum(); - sendAndApply(&ow); -} - -bool CGameHandler::isAllowedExchange( ObjectInstanceID id1, ObjectInstanceID id2 ) -{ - if(id1 == id2) - return true; - - const CGObjectInstance *o1 = getObj(id1), *o2 = getObj(id2); - if(!o1 || !o2) - return true; //arranging stacks within an object should be always allowed - - if (o1 && o2) - { - if(o1->ID == Obj::TOWN) - { - const CGTownInstance *t = static_cast(o1); - if(t->visitingHero == o2 || t->garrisonHero == o2) - return true; - } - if(o2->ID == Obj::TOWN) - { - const CGTownInstance *t = static_cast(o2); - if(t->visitingHero == o1 || t->garrisonHero == o1) - return true; - } - - if (o1->ID == Obj::HERO && o2->ID == Obj::HERO) - { - const CGHeroInstance *h1 = static_cast(o1); - const CGHeroInstance *h2 = static_cast(o2); - - // two heroes in same town (garrisoned and visiting) - if (h1->visitedTown != nullptr && h2->visitedTown != nullptr && h1->visitedTown == h2->visitedTown) - return true; - } - - //Ongoing garrison exchange - if(auto dialog = std::dynamic_pointer_cast(queries.topQuery(o1->tempOwner))) - { - if(dialog->exchangingArmies.at(0) == o1 && dialog->exchangingArmies.at(1) == o2) - return true; - - if(dialog->exchangingArmies.at(1) == o1 && dialog->exchangingArmies.at(0) == o2) - return true; - } - } - - return false; -} - -void CGameHandler::objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ) -{ - logGlobal->traceStream() << h->nodeName() << " visits " << obj->getHoverText(); - auto visitQuery = make_shared(obj, h, obj->visitablePos()); - queries.addQuery(visitQuery); //TODO real visit pos - - HeroVisit hv; - hv.obj = obj; - hv.hero = h; - hv.player = h->tempOwner; - hv.starting = true; - sendAndApply(&hv); - - obj->onHeroVisit(h); - - queries.popIfTop(visitQuery); //visit ends here if no queries were created -} - -void CGameHandler::objectVisitEnded(const CObjectVisitQuery &query) -{ - logGlobal->traceStream() << query.visitingHero->nodeName() << " visit ends.\n"; - - HeroVisit hv; - hv.player = query.players.front(); - hv.obj = nullptr; //not necessary, moreover may have been deleted in the meantime - hv.hero = query.visitingHero; - assert(hv.hero); - hv.starting = false; - sendAndApply(&hv); -} - -bool CGameHandler::buildBoat( ObjectInstanceID objid ) -{ - const IShipyard *obj = IShipyard::castFrom(getObj(objid)); - - if(obj->shipyardStatus() != IBoatGenerator::GOOD) - { - complain("Cannot build boat in this shipyard!"); - return false; - } - else if(obj->o->ID == Obj::TOWN - && !static_cast(obj)->hasBuilt(BuildingID::SHIPYARD)) - { - complain("Cannot build boat in the town - no shipyard!"); - return false; - } - - const PlayerColor playerID = obj->o->tempOwner; - TResources boatCost; - obj->getBoatCost(boatCost); - TResources aviable = gs->getPlayer(playerID)->resources; - - if (!aviable.canAfford(boatCost)) - { - complain("Not enough resources to build a boat!"); - return false; - } - - int3 tile = obj->bestLocation(); - if(!gs->map->isInTheMap(tile)) - { - complain("Cannot find appropriate tile for a boat!"); - return false; - } - - //take boat cost - SetResources sr; - sr.player = playerID; - sr.res = (aviable - boatCost); - sendAndApply(&sr); - - //create boat - NewObject no; - no.ID = Obj::BOAT; - no.subID = obj->getBoatType(); - no.pos = tile + int3(1,0,0); - sendAndApply(&no); - - return true; -} - -void CGameHandler::engageIntoBattle( PlayerColor player ) -{ - //notify interfaces - PlayerBlocked pb; - pb.player = player; - pb.reason = PlayerBlocked::UPCOMING_BATTLE; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - sendAndApply(&pb); -} - -void CGameHandler::checkVictoryLossConditions(const std::set & playerColors) -{ - for(auto playerColor : playerColors) - { - if(gs->getPlayer(playerColor)) - checkVictoryLossConditionsForPlayer(playerColor); - } -} - -void CGameHandler::checkVictoryLossConditionsForAll() -{ - std::set playerColors; - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) - { - playerColors.insert(PlayerColor(i)); - } - checkVictoryLossConditions(playerColors); -} - -void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) -{ - const PlayerState *p = gs->getPlayer(player); - if(p->status != EPlayerStatus::INGAME) return; - - auto victoryLossCheckResult = gs->checkForVictoryAndLoss(player); - - if(victoryLossCheckResult.victory() || victoryLossCheckResult.loss()) - { - InfoWindow iw; - getVictoryLossMessage(player, victoryLossCheckResult, iw); - sendAndApply(&iw); - - PlayerEndsGame peg; - peg.player = player; - peg.victoryLossCheckResult = victoryLossCheckResult; - sendAndApply(&peg); - - if(victoryLossCheckResult.victory()) - { - //one player won -> all enemies lost - for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++) - { - if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME) - { - peg.player = i->first; - peg.victoryLossCheckResult = gameState()->getPlayerRelations(player, i->first) == PlayerRelations::ALLIES ? - victoryLossCheckResult : victoryLossCheckResult.invert(); // ally of winner - - InfoWindow iw; - getVictoryLossMessage(player, peg.victoryLossCheckResult, iw); - iw.player = i->first; - - sendAndApply(&iw); - sendAndApply(&peg); - } - } - - if(p->human) - { - end2 = true; - - if(gs->scenarioOps->campState) - { - std::vector crossoverHeroes; - for(CGHeroInstance * hero : gs->map->heroesOnMap) - { - if(hero->tempOwner == player) - { - // keep all heroes from the winning player - crossoverHeroes.push_back(hero); - } - else if(vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(hero->subID))) - { - // keep hero whether lost or won (like Xeron in AB campaign) - crossoverHeroes.push_back(hero); - } - } - // keep lost heroes which are in heroes pool - for(auto & heroPair : gs->hpool.heroesPool) - { - if(vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(heroPair.first))) - { - crossoverHeroes.push_back(heroPair.second.get()); - } - } - - gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); - - //Request clients to change connection mode - PrepareForAdvancingCampaign pfac; - sendAndApply(&pfac); - //Change connection mode - if(getPlayer(player)->human && getStartInfo()->campState) - { - for(auto connection : conns) - connection->prepareForSendingHeroes(); - } - - UpdateCampaignState ucs; - ucs.camp = gs->scenarioOps->campState; - sendAndApply(&ucs); - } - } - } - else - { - //player lost -> all his objects become unflagged (neutral) - auto hlp = p->heroes; - for (auto i = hlp.cbegin(); i != hlp.cend(); i++) //eliminate heroes - removeObject(*i); - - for (auto i = gs->map->objects.cbegin(); i != gs->map->objects.cend(); i++) //unflag objs - { - if(*i && (*i)->tempOwner == player) - setOwner(*i,PlayerColor::NEUTRAL); - } - - //eliminating one player may cause victory of another: - std::set playerColors; - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) - { - if(player.getNum() != i) playerColors.insert(PlayerColor(i)); - } - - //notify all players - for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++) - { - if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME) - { - InfoWindow iw; - getVictoryLossMessage(player, victoryLossCheckResult.invert(), iw); - iw.player = i->first; - sendAndApply(&iw); - } - } - - - checkVictoryLossConditions(playerColors); - } - - auto playerInfo = gs->getPlayer(gs->currentPlayer, false); - // If we are called before the actual game start, there might be no current player - if(playerInfo && playerInfo->status != EPlayerStatus::INGAME) - { - // If player making turn has lost his turn must be over as well - states.setFlag(gs->currentPlayer, &PlayerStatus::makingTurn, false); - } - } -} - -void CGameHandler::getVictoryLossMessage(PlayerColor player, EVictoryLossCheckResult victoryLossCheckResult, InfoWindow & out) const -{ - out.player = player; - out.text.clear(); - out.text << victoryLossCheckResult.messageToSelf; - // hackish, insert one player-specific string, if applicable - if (victoryLossCheckResult.messageToSelf.find("%s") != std::string::npos) - out.text.addReplacement(MetaString::COLOR, player.getNum()); - - out.components.push_back(Component(Component::FLAG, player.getNum(), 0, 0)); -} - -bool CGameHandler::dig( const CGHeroInstance *h ) -{ - for (auto i = gs->map->objects.cbegin(); i != gs->map->objects.cend(); i++) //unflag objs - { - if(*i && (*i)->ID == Obj::HOLE && (*i)->pos == h->getPosition()) - { - complain("Cannot dig - there is already a hole under the hero!"); - return false; - } - } - - if(h->diggingStatus() != CGHeroInstance::CAN_DIG) //checks for terrain and movement - COMPLAIN_RETF("Hero cannot dig (error code %d)!", h->diggingStatus()); - - //create a hole - NewObject no; - no.ID = Obj::HOLE; - no.pos = h->getPosition(); - no.subID = 0; - sendAndApply(&no); - - //take MPs - SetMovePoints smp; - smp.hid = h->id; - smp.val = 0; - sendAndApply(&smp); - - InfoWindow iw; - iw.player = h->tempOwner; - if(gs->map->grailPos == h->getPosition()) - { - iw.text.addTxt(MetaString::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " - iw.text.addTxt(MetaString::ART_NAMES, 2); - iw.soundID = soundBase::ULTIMATEARTIFACT; - giveHeroNewArtifact(h, VLC->arth->artifacts.at(2), ArtifactPosition::PRE_FIRST); //give grail - sendAndApply(&iw); - - iw.soundID = soundBase::invalid; - iw.text.clear(); - iw.text.addTxt(MetaString::ART_DESCR, 2); - sendAndApply(&iw); - } - else - { - iw.text.addTxt(MetaString::GENERAL_TXT, 59); //"Nothing here. \n Where could it be?" - iw.soundID = soundBase::Dig; - sendAndApply(&iw); - } - - return true; -} - -void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType attackMode, const CStack * attacker) -{ - if(attacker->hasBonusOfType(attackMode)) - { - std::set spellsToCast; - TBonusListPtr spells = attacker->getBonuses(Selector::type(attackMode)); - for(const Bonus *sf : *spells) - { - spellsToCast.insert (SpellID(sf->subtype)); - } - for(SpellID spellID : spellsToCast) - { - const CStack * oneOfAttacked = nullptr; - for (auto & elem : bat.bsa) - { - if (elem.newAmount > 0 && !elem.isSecondary()) //apply effects only to first target stack if it's alive - { - oneOfAttacked = gs->curB->battleGetStackByID(elem.stackAttacked); - break; - } - } - bool castMe = false; - if(oneOfAttacked == nullptr) //all attacked creatures have been killed - return; - int spellLevel = 0; - TBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); - for(const Bonus *sf : *spellsByType) - { - vstd::amax(spellLevel, sf->additionalInfo % 1000); //pick highest level - int meleeRanged = sf->additionalInfo / 1000; - if (meleeRanged == 0 || (meleeRanged == 1 && bat.shot()) || (meleeRanged == 2 && !bat.shot())) - castMe = true; - } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); - vstd::amin (chance, 100); - int destination = oneOfAttacked->position; - - const CSpell * spell = SpellID(spellID).toSpell(); - if(gs->curB->battleCanCastThisSpellHere(attacker->owner, spell, ECastingMode::AFTER_ATTACK_CASTING, oneOfAttacked->position) != ESpellCastProblem::OK) - continue; - - //check if spell should be casted (probability handling) - if(rand()%100 >= chance) - continue; - - //casting //TODO: check if spell can be blocked or target is immune - if (castMe) //stacks use 0 spell power. If needed, default = 3 or custom value is used - handleSpellCasting(spellID, spellLevel, destination, !attacker->attackerOwned, attacker->owner, nullptr, nullptr, 0, ECastingMode::AFTER_ATTACK_CASTING, attacker); - } - } -} - -void CGameHandler::handleAttackBeforeCasting (const BattleAttack & bat) -{ - const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking); - attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no death stare / acid breath needed? -} - -void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat ) -{ - const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking); - if (!attacker) //could be already dead - return; - attackCasting(bat, Bonus::SPELL_AFTER_ATTACK, attacker); - - if(bat.bsa.at(0).newAmount <= 0) - { - //don't try death stare or acid breath on dead stack (crash!) - return; - } - - if (attacker->hasBonusOfType(Bonus::DEATH_STARE) && bat.bsa.size()) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(Bonus::DEATH_STARE, 0) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distr(attacker->count, chanceToKill); - std::mt19937 rng(rand()); - - int staredCreatures = distr(rng); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = (attacker->count + cap - 1) / cap; //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->level(); - if (staredCreatures) - { - if (bat.bsa.at(0).newAmount > 0) //TODO: death stare was not originally available for multiple-hex attacks, but... - handleSpellCasting(SpellID::DEATH_STARE, 0, gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->position, - !attacker->attackerOwned, attacker->owner, nullptr, nullptr, staredCreatures, ECastingMode::AFTER_ATTACK_CASTING, attacker); - } - } - - int acidDamage = 0; - TBonusListPtr acidBreath = attacker->getBonuses(Selector::type(Bonus::ACID_BREATH)); - for(const Bonus *b : *acidBreath) - { - if (b->additionalInfo > rand()%100) - acidDamage += b->val; - } - if (acidDamage) - { - handleSpellCasting(SpellID::ACID_BREATH_DAMAGE, 0, gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->position, - !attacker->attackerOwned, attacker->owner, nullptr, nullptr, - acidDamage * attacker->count, ECastingMode::AFTER_ATTACK_CASTING, attacker); - } -} - -bool CGameHandler::castSpell(const CGHeroInstance *h, SpellID spellID, const int3 &pos) -{ - const CSpell *s = spellID.toSpell(); - int cost = h->getSpellCost(s); - int schoolLevel = h->getSpellSchoolLevel(s); - - if(!h->canCastThisSpell(s)) - COMPLAIN_RET("Hero cannot cast this spell!"); - if(h->mana < cost) - COMPLAIN_RET("Hero doesn't have enough spell points to cast this spell!"); - if(s->combatSpell) - COMPLAIN_RET("This function can be used only for adventure map spells!"); - - AdvmapSpellCast asc; - asc.caster = h; - asc.spellID = spellID; - sendAndApply(&asc); - - switch(spellID) - { - case SpellID::SUMMON_BOAT: - { - //check if spell works at all - if(rand() % 100 >= s->getPower(schoolLevel)) //power is % chance of success - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. - iw.text.addReplacement(h->name); - sendAndApply(&iw); - break; - } - - //try to find unoccupied boat to summon - const CGBoat *nearest = nullptr; - double dist = 0; - int3 summonPos = h->bestLocation(); - if(summonPos.x < 0) - COMPLAIN_RET("There is no water tile available!"); - - for(const CGObjectInstance *obj : gs->map->objects) - { - if(obj && obj->ID == Obj::BOAT) - { - const CGBoat *b = static_cast(obj); - if(b->hero) continue; //we're looking for unoccupied boat - - double nDist = distance(b->pos, h->getPosition()); - if(!nearest || nDist < dist) //it's first boat or closer than previous - { - nearest = b; - dist = nDist; - } - } - } - - if(nearest) //we found boat to summon - { - ChangeObjPos cop; - cop.objid = nearest->id; - cop.nPos = summonPos + int3(1,0,0);; - cop.flags = 1; - sendAndApply(&cop); - } - else if(schoolLevel < 2) //none or basic level -> cannot create boat :( - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon. - sendAndApply(&iw); - } - else //create boat - { - NewObject no; - no.ID = Obj::BOAT; - no.subID = h->getBoatType(); - no.pos = summonPos + int3(1,0,0);; - sendAndApply(&no); - } - break; - } - - case SpellID::SCUTTLE_BOAT: - { - //check if spell works at all - if(rand() % 100 >= s->getPower(schoolLevel)) //power is % chance of success - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed - iw.text.addReplacement(h->name); - sendAndApply(&iw); - break; - } - if(!gs->map->isInTheMap(pos)) - COMPLAIN_RET("Invalid dst tile for scuttle!"); - - //TODO: test range, visibility - const TerrainTile *t = &gs->map->getTile(pos); - if(!t->visitableObjects.size() || t->visitableObjects.back()->ID != Obj::BOAT) - COMPLAIN_RET("There is no boat to scuttle!"); - - RemoveObject ro; - ro.id = t->visitableObjects.back()->id; - sendAndApply(&ro); - break; - } - case SpellID::DIMENSION_DOOR: - { - const TerrainTile *dest = getTile(pos); - const TerrainTile *curr = getTile(h->getSightCenter()); - - if(!dest) - COMPLAIN_RET("Destination tile doesn't exist!"); - if(!h->movement) - COMPLAIN_RET("Hero needs movement points to cast Dimension Door!"); - if(h->getBonusesCount(Bonus::SPELL_EFFECT, SpellID::DIMENSION_DOOR) >= s->getPower(schoolLevel)) //limit casts per turn - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today. - iw.text.addReplacement(h->name); - sendAndApply(&iw); - break; - } - - GiveBonus gb; - gb.id = h->id.getNum(); - gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::NONE, Bonus::SPELL_EFFECT, 0, SpellID::DIMENSION_DOOR); - sendAndApply(&gb); - - if(!dest->isClear(curr)) //wrong dest tile - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed! - sendAndApply(&iw); - break; - } - - if (moveHero(h->id, pos + h->getVisitableOffset(), true)) - { - SetMovePoints smp; - smp.hid = h->id; - smp.val = std::max(0, h->movement - 300); - sendAndApply(&smp); - } - } - break; - case SpellID::FLY: - { - int subtype = schoolLevel >= 2 ? 1 : 2; //adv or expert - - GiveBonus gb; - gb.id = h->id.getNum(); - gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::FLYING_MOVEMENT, Bonus::SPELL_EFFECT, 0, SpellID::FLY, subtype); - sendAndApply(&gb); - } - break; - case SpellID::WATER_WALK: - { - int subtype = schoolLevel >= 2 ? 1 : 2; //adv or expert - - GiveBonus gb; - gb.id = h->id.getNum(); - gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::WATER_WALKING, Bonus::SPELL_EFFECT, 0, SpellID::WATER_WALK, subtype); - sendAndApply(&gb); - } - break; - - case SpellID::TOWN_PORTAL: - { - if (!gs->map->isInTheMap(pos)) - COMPLAIN_RET("Destination tile not present!") - TerrainTile tile = gs->map->getTile(pos); - if (tile.visitableObjects.empty() || tile.visitableObjects.back()->ID != Obj::TOWN ) - COMPLAIN_RET("Town not found for Town Portal!"); - - CGTownInstance * town = static_cast(tile.visitableObjects.back()); - if (town->tempOwner != h->tempOwner) - COMPLAIN_RET("Can't teleport to another player!"); - if (town->visitingHero) - COMPLAIN_RET("Can't teleport to occupied town!"); - - if (h->getSpellSchoolLevel(s) < 2) - { - double dist = town->pos.dist2d(h->pos); - ObjectInstanceID nearest = town->id; //nearest town's ID - for(const CGTownInstance * currTown : gs->getPlayer(h->tempOwner)->towns) - { - double curDist = currTown->pos.dist2d(h->pos); - if (nearest == ObjectInstanceID() || curDist < dist) - { - nearest = town->id; - dist = curDist; - } - } - if (town->id != nearest) - COMPLAIN_RET("This hero can only teleport to nearest town!") - } - moveHero(h->id, town->visitablePos() + h->getVisitableOffset() ,1); - } - break; - - case SpellID::VISIONS: - case SpellID::VIEW_EARTH: - case SpellID::DISGUISE: - case SpellID::VIEW_AIR: - default: - COMPLAIN_RET("This spell is not implemented yet!"); - } - - SetMana sm; - sm.hid = h->id; - sm.val = h->mana - cost; - sendAndApply(&sm); - - return true; -} - -void CGameHandler::visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h) -{ - if (!t.visitableObjects.empty()) - { - //to prevent self-visiting heroes on space press - if(t.visitableObjects.back() != h) - objectVisited(t.visitableObjects.back(), h); - else if(t.visitableObjects.size() > 1) - objectVisited(*(t.visitableObjects.end()-2),h); - } -} - -bool CGameHandler::sacrificeCreatures(const IMarket *market, const CGHeroInstance *hero, SlotID slot, ui32 count) -{ - int oldCount = hero->getStackCount(slot); - - if(oldCount < count) - COMPLAIN_RET("Not enough creatures to sacrifice!") - else if(oldCount == count && hero->Slots().size() == 1 && hero->needsLastStack()) - COMPLAIN_RET("Cannot sacrifice last creature!"); - - int crid = hero->getStack(slot).type->idNumber; - - changeStackCount(StackLocation(hero, slot), -count); - - int dump, exp; - market->getOffer(crid, 0, dump, exp, EMarketMode::CREATURE_EXP); - exp *= count; - changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(exp)); - - return true; -} - -bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, ArtifactPosition slot) -{ - ArtifactLocation al(hero, slot); - const CArtifactInstance *a = al.getArt(); - - if(!a) - COMPLAIN_RET("Cannot find artifact to sacrifice!"); - - - int dmp, expToGive; - m->getOffer(hero->getArtTypeId(slot), 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); - - removeArtifact(al); - changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive); - return true; -} - -void CGameHandler::makeStackDoNothing(const CStack * next) -{ - BattleAction doNothing; - doNothing.actionType = Battle::NO_ACTION; - doNothing.additionalInfo = 0; - doNothing.destinationTile = -1; - doNothing.side = !next->attackerOwned; - doNothing.stackNumber = next->ID; - - makeAutomaticAction(next, doNothing); -} - -bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) -{ - if(sl.army->hasStackAtSlot(sl.slot)) - COMPLAIN_RET("Slot is already taken!"); - - if(!sl.slot.validSlot()) - COMPLAIN_RET("Cannot insert stack to that slot!"); - - InsertNewStack ins; - ins.sl = sl; - ins.stack = CStackBasicDescriptor(c, count); - sendAndApply(&ins); - return true; -} - -bool CGameHandler::eraseStack(const StackLocation &sl, bool forceRemoval/* = false*/) -{ - if(!sl.army->hasStackAtSlot(sl.slot)) - COMPLAIN_RET("Cannot find a stack to erase"); - - if(sl.army->Slots().size() == 1 //from the last stack - && sl.army->needsLastStack() //that must be left - && !forceRemoval) //ignore above conditions if we are forcing removal - { - COMPLAIN_RET("Cannot erase the last stack!"); - } - - EraseStack es; - es.sl = sl; - sendAndApply(&es); - return true; -} - -bool CGameHandler::changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue /*= false*/) -{ - TQuantity currentCount = sl.army->getStackCount(sl.slot); - if((absoluteValue && count < 0) - || (!absoluteValue && -count > currentCount)) - { - COMPLAIN_RET("Cannot take more stacks than present!"); - } - - if((currentCount == -count && !absoluteValue) - || (!count && absoluteValue)) - { - eraseStack(sl); - } - else - { - ChangeStackCount csc; - csc.sl = sl; - csc.count = count; - csc.absoluteValue = absoluteValue; - sendAndApply(&csc); - } - return true; -} - -bool CGameHandler::addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) -{ - const CCreature *slotC = sl.army->getCreature(sl.slot); - if(!slotC) //slot is empty - insertNewStack(sl, c, count); - else if(c == slotC) - changeStackCount(sl, count); - else - { - COMPLAIN_RET("Cannot add " + c->namePl + " to slot " + boost::lexical_cast(sl.slot) + "!"); - } - return true; -} - -void CGameHandler::tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) -{ - if(removeObjWhenFinished) - removeAfterVisit(src); - - if(!src->canBeMergedWith(*dst, allowMerging)) - { - if (allowMerging) //do that, add all matching creatures. - { - bool cont = true; - while (cont) - { - for(auto i = src->stacks.begin(); i != src->stacks.end(); i++)//while there are unmoved creatures - { - SlotID pos = dst->getSlotFor(i->second->type); - if(pos.validSlot()) - { - moveStack(StackLocation(src, i->first), StackLocation(dst, pos)); - cont = true; - break; //or iterator crashes - } - cont = false; - } - } - } - showGarrisonDialog(src->id, dst->id, true); //show garrison window and optionally remove ourselves from map when player ends - } - else //merge - { - moveArmy(src, dst, allowMerging); - } -} - -bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) -{ - if(!src.army->hasStackAtSlot(src.slot)) - COMPLAIN_RET("No stack to move!"); - - if(dst.army->hasStackAtSlot(dst.slot) && dst.army->getCreature(dst.slot) != src.army->getCreature(src.slot)) - COMPLAIN_RET("Cannot move: stack of different type at destination pos!"); - - if(!dst.slot.validSlot()) - COMPLAIN_RET("Cannot move stack to that slot!"); - - if(count == -1) - { - count = src.army->getStackCount(src.slot); - } - - if(src.army != dst.army //moving away - && count == src.army->getStackCount(src.slot) //all creatures - && src.army->Slots().size() == 1 //from the last stack - && src.army->needsLastStack()) //that must be left - { - COMPLAIN_RET("Cannot move away the last creature!"); - } - - RebalanceStacks rs; - rs.src = src; - rs.dst = dst; - rs.count = count; - sendAndApply(&rs); - return true; -} - -bool CGameHandler::swapStacks(const StackLocation &sl1, const StackLocation &sl2) -{ - - if(!sl1.army->hasStackAtSlot(sl1.slot)) - return moveStack(sl2, sl1); - else if(!sl2.army->hasStackAtSlot(sl2.slot)) - return moveStack(sl1, sl2); - else - { - SwapStacks ss; - ss.sl1 = sl1; - ss.sl2 = sl2; - sendAndApply(&ss); - return true; - } -} - -void CGameHandler::runBattle() -{ - setBattle(gs->curB); - assert(gs->curB); - //TODO: pre-tactic stuff, call scripts etc. - - //tactic round - { - while(gs->curB->tacticDistance && !battleResult.get()) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - - //spells opening battle - for(int i = 0; i < 2; ++i) - { - auto h = gs->curB->battleGetFightingHero(i); - if(h && h->hasBonusOfType(Bonus::OPENING_BATTLE_SPELL)) - { - TBonusListPtr bl = h->getBonuses(Selector::type(Bonus::OPENING_BATTLE_SPELL)); - for (Bonus *b : *bl) - { - handleSpellCasting(SpellID(b->subtype), 3, -1, 0, h->tempOwner, nullptr, gs->curB->battleGetFightingHero(1-i), b->val, ECastingMode::HERO_CASTING, nullptr); - } - } - } - - //main loop - while(!battleResult.get()) //till the end of the battle ;] - { - NEW_ROUND; - auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it - for(auto &obstPtr : obstacles) - { - if(const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) - if(sco->turnsRemaining == 0) - removeObstacle(*obstPtr); - } - - const BattleInfo & curB = *gs->curB; - - //remove clones after all mechanics and animations are handled! - std::set stacksToRemove; - for (auto stack : curB.stacks) - { - if (stack->idDeadClone()) - stacksToRemove.insert(stack); - } - for (auto stack : stacksToRemove) - { - BattleStacksRemoved bsr; - bsr.stackIDs.insert(stack->ID); - sendAndApply(&bsr); - } - //stack loop - - const CStack *next; - while(!battleResult.get() && (next = curB.getNextStack()) && next->willMove()) - { - - //check for bad morale => freeze - int nextStackMorale = next->MoraleVal(); - if( nextStackMorale < 0 && - !(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE) - || NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses) - ) - { - if( rand()%24 < -2 * nextStackMorale) - { - //unit loses its turn - empty freeze action - BattleAction ba; - ba.actionType = Battle::BAD_MORALE; - ba.additionalInfo = 1; - ba.side = !next->attackerOwned; - ba.stackNumber = next->ID; - - makeAutomaticAction(next, ba); - continue; - } - } - - if(next->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE)) //while in berserk - { //fixme: stack should not attack itself - std::pair attackInfo = curB.getNearestStack(next, boost::logic::indeterminate); - if(attackInfo.first != nullptr) - { - BattleAction attack; - attack.actionType = Battle::WALK_AND_ATTACK; - attack.side = !next->attackerOwned; - attack.stackNumber = next->ID; - attack.additionalInfo = attackInfo.first->position; - attack.destinationTile = attackInfo.second; - - makeAutomaticAction(next, attack); - } - else - { - makeStackDoNothing(next); - } - continue; - } - - const CGHeroInstance * curOwner = gs->curB->battleGetOwner(next); - - if( (next->position < 0 || next->getCreature()->idNumber == CreatureID::BALLISTA) //arrow turret or ballista - && (!curOwner || curOwner->getSecSkillLevel(SecondarySkill::ARTILLERY) == 0)) //hero has no artillery - { - BattleAction attack; - attack.actionType = Battle::SHOOT; - attack.side = !next->attackerOwned; - attack.stackNumber = next->ID; - - for(auto & elem : gs->curB->stacks) - { - if(elem->owner != next->owner && elem->isValidTarget()) - { - attack.destinationTile = elem->position; - break; - } - } - - makeAutomaticAction(next, attack); - continue; - } - - if(next->getCreature()->idNumber == CreatureID::CATAPULT && (!curOwner || curOwner->getSecSkillLevel(SecondarySkill::BALLISTICS) == 0)) //catapult, hero has no ballistics - { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); - - if(!attackableBattleHexes.empty()) - { - BattleAction attack; - attack.destinationTile = attackableBattleHexes[rand() % attackableBattleHexes.size()]; - attack.actionType = Battle::CATAPULT; - attack.additionalInfo = 0; - attack.side = !next->attackerOwned; - attack.stackNumber = next->ID; - - makeAutomaticAction(next, attack); - } - else - { - makeStackDoNothing(next); - } - continue; - } - - if(next->getCreature()->idNumber == CreatureID::FIRST_AID_TENT) - { - std::vector< const CStack * > possibleStacks; - - //is there any clean algorithm for that? (boost.range seems to lack copy_if) -> remove_copy_if? - for(const CStack *s : battleGetAllStacks()) - if(s->owner == next->owner && s->canBeHealed()) - possibleStacks.push_back(s); - - if(!possibleStacks.size()) - { - makeStackDoNothing(next); - continue; - } - - if(!curOwner || curOwner->getSecSkillLevel(SecondarySkill::FIRST_AID) == 0) //no hero or hero has no first aid - { - range::random_shuffle(possibleStacks); - const CStack * toBeHealed = possibleStacks.front(); - - BattleAction heal; - heal.actionType = Battle::STACK_HEAL; - heal.additionalInfo = 0; - heal.destinationTile = toBeHealed->position; - heal.side = !next->attackerOwned; - heal.stackNumber = next->ID; - - makeAutomaticAction(next, heal); - continue; - } - } - - int numberOfAsks = 1; - bool breakOuter = false; - do - {//ask interface and wait for answer - if(!battleResult.get()) - { - stackTurnTrigger(next); //various effects - - if (vstd::contains(next->state, EBattleStackState::FEAR)) - { - makeStackDoNothing(next); //end immediately if stack was affected by fear - } - else - { - logGlobal->traceStream() << "Activating " << next->nodeName(); - BattleSetActiveStack sas; - sas.stack = next->ID; - sendAndApply(&sas); - boost::unique_lock lock(battleMadeAction.mx); - battleMadeAction.data = false; - while (next->alive() && //next is invalid after sacrificing current stack :? - (!battleMadeAction.data && !battleResult.get())) //active stack hasn't made its action and battle is still going - battleMadeAction.cond.wait(lock); - } - } - - if(battleResult.get()) //don't touch it, battle could be finished while waiting got action - { - breakOuter = true; - break; - } - - //we're after action, all results applied - checkForBattleEnd(); //check if this action ended the battle - - //check for good morale - nextStackMorale = next->MoraleVal(); - if(!vstd::contains(next->state,EBattleStackState::HAD_MORALE) //only one extra move per turn possible - && !vstd::contains(next->state,EBattleStackState::DEFENDING) - && !next->waited() - && !vstd::contains(next->state, EBattleStackState::FEAR) - && next->alive() - && nextStackMorale > 0 - && !(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE) - || NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses - ) - { - if(rand()%24 < nextStackMorale) //this stack hasn't got morale this turn - - { - BattleTriggerEffect bte; - bte.stackID = next->ID; - bte.effect = Bonus::MORALE; - bte.val = 1; - bte.additionalInfo = 0; - sendAndApply(&bte); //play animation - - ++numberOfAsks; //move this stack once more - } - } - - --numberOfAsks; - } while (numberOfAsks > 0); - - if (breakOuter) - { - break; - } - - } - } - - endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1)); -} - -bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) -{ - BattleSetActiveStack bsa; - bsa.stack = stack->ID; - bsa.askPlayerInterface = false; - sendAndApply(&bsa); - - bool ret = makeBattleAction(ba); - checkForBattleEnd(); - return ret; -} - -void CGameHandler::giveHeroArtifact(const CGHeroInstance *h, const CArtifactInstance *a, ArtifactPosition pos) -{ - assert(a->artType); - ArtifactLocation al; - al.artHolder = const_cast(h); - - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; - if(pos < 0) - { - if(pos == ArtifactPosition::FIRST_AVAILABLE) - slot = a->firstAvailableSlot(h); - else - slot = a->firstBackpackSlot(h); - } - else - { - slot = pos; - } - - al.slot = slot; - - if(slot < 0 || !a->canBePutAt(al)) - { - complain("Cannot put artifact in that slot!"); - return; - } - - putArtifact(al, a); -} -void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) -{ - PutArtifact pa; - pa.art = a; - pa.al = al; - sendAndApply(&pa); -} - -void CGameHandler::giveHeroNewArtifact(const CGHeroInstance *h, const CArtifact *artType, ArtifactPosition pos) -{ - CArtifactInstance *a = nullptr; - if(!artType->constituents) - { - a = new CArtifactInstance(); - } - else - { - a = new CCombinedArtifactInstance(); - } - a->artType = artType; //*NOT* via settype -> all bonus-related stuff must be done by NewArtifact apply - - NewArtifact na; - na.art = a; - sendAndApply(&na); // -> updates a!!!, will create a on other machines - - giveHeroArtifact(h, a, pos); -} - -void CGameHandler::setBattleResult(BattleResult::EResult resultType, int victoriusSide) -{ - if(battleResult.get()) - { - complain("There is already set result?"); - return; - } - auto br = new BattleResult; - br->result = resultType; - br->winner = victoriusSide; //surrendering side loses - gs->curB->calculateCasualties(br->casualties); - battleResult.set(br); -} - -void CGameHandler::commitPackage( CPackForClient *pack ) -{ - sendAndApply(pack); -} - -void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) -{ - std::vector::iterator tile; - std::vector tiles; - getFreeTiles(tiles); - ui32 amount = tiles.size() / 200; //Chance is 0.5% for each tile - std::random_shuffle(tiles.begin(), tiles.end()); - logGlobal->traceStream() << "Spawning wandering monsters. Found " << tiles.size() << " free tiles. Creature type: " << creatureID; - const CCreature *cre = VLC->creh->creatures.at(creatureID); - for (int i = 0; i < amount; ++i) - { - tile = tiles.begin(); - logGlobal->traceStream() << "\tSpawning monster at " << *tile; - putNewMonster(creatureID, cre->getRandomAmount(std::rand), *tile); - tiles.erase(tile); //not use it again - } -} - -void CGameHandler::removeObstacle(const CObstacleInstance &obstacle) -{ - ObstaclesRemoved obsRem; - obsRem.obstacles.insert(obstacle.uniqueID); - sendAndApply(&obsRem); -} - -void CGameHandler::synchronizeArtifactHandlerLists() -{ - UpdateArtHandlerLists uahl; - uahl.treasures = VLC->arth->treasures; - uahl.minors = VLC->arth->minors; - uahl.majors = VLC->arth->majors; - uahl.relics = VLC->arth->relics; - sendAndApply(&uahl); -} - -bool CGameHandler::isValidObject(const CGObjectInstance *obj) const -{ - return vstd::contains(gs->map->objects, obj); -} - -bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) -{ - if(dynamic_cast(pack)) - return false; - - auto query = queries.topQuery(player); - if(query && query->blocksPack(pack)) - { - complain(boost::str(boost::format("Player %s has to answer queries before attempting any further actions. Top query is %s!") % player % query->toString())); - return true; - } - - return false; -} - -void CGameHandler::removeAfterVisit(const CGObjectInstance *object) -{ - //If the object is being visited, there must be a matching query - for(const auto &query : queries.allQueries()) - { - if(auto someVistQuery = std::dynamic_pointer_cast(query)) - { - if(someVistQuery->visitedObject == object) - { - someVistQuery->removeObjectAfterVisit = true; - return; - } - } - }; - - //If we haven't returned so far, there is no query and no visit, call was wrong - assert("This function needs to be called during the object visit!"); -} - -bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) -{ - if(auto topQuery = queries.topQuery(hero->getOwner())) - if(auto visit = std::dynamic_pointer_cast(topQuery)) - return !(visit->visitedObject == obj && visit->visitingHero == hero); - - return true; -} - -void CGameHandler::duelFinished() -{ - auto si = getStartInfo(); - auto getName = [&](int i){ return si->getIthPlayersSettings(gs->curB->sides.at(i).color).name; }; - - int casualtiesPoints = 0; - logGlobal->debugStream() << boost::format("Winner side %d\nWinner casualties:") - % (int)battleResult.data->winner; - - for(auto & elem : battleResult.data->casualties[battleResult.data->winner]) - { - const CCreature *c = VLC->creh->creatures[elem.first]; - logGlobal->debugStream() << boost::format("\t* %d of %s") % elem.second % c->namePl; - casualtiesPoints += c->AIValue * elem.second; - } - logGlobal->debugStream() << boost::format("Total casualties points: %d") % casualtiesPoints; - - - time_t timeNow; - time(&timeNow); - - std::ofstream out(cmdLineOptions["resultsFile"].as(), std::ios::app); - if(out) - { - out << boost::format("%s\t%s\t%s\t%d\t%d\t%d\t%s\n") % si->mapname % getName(0) % getName(1) - % battleResult.data->winner % battleResult.data->result % casualtiesPoints - % asctime(localtime(&timeNow)); - } - else - { - logGlobal->errorStream() << "Cannot open to write " << cmdLineOptions["resultsFile"].as(); - } - - CSaveFile resultFile("result.vdrst"); - resultFile << *battleResult.data; - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - sendAndApply(&resultsApplied); - return; -} - -CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance *army, BattleInfo *bat) -{ - heroWithDeadCommander = ObjectInstanceID(); - - PlayerColor color = army->tempOwner; - if(color == PlayerColor::UNFLAGGABLE) - color = PlayerColor::NEUTRAL; - - for(CStack *st : bat->stacks) - { - if(vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account summoned stacks - continue; - if (st->owner != color) //remove only our stacks - continue; - - //FIXME: this info is also used in BattleInfo::calculateCasualties, refactor - st->count = std::max (0, st->count - st->resurrected); - - if (!st->count && !st->base) //we can imagine stacks of war mahcines that are not spawned by artifacts? - { - auto warMachine = VLC->arth->creatureToMachineID(st->type->idNumber); - if (warMachine != ArtifactID::NONE) - { - auto hero = dynamic_cast (army); - if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); - } - } - - if(!army->slotEmpty(st->slot) && st->count < army->getStackCount(st->slot)) - { - StackLocation sl(army, st->slot); - if(st->alive()) - newStackCounts.push_back(std::pair(sl, st->count)); - else - newStackCounts.push_back(std::pair(sl, 0)); - } - if (st->base && !st->count) - { - auto c = dynamic_cast (st->base); - if (c) //switch commander status to dead - { - auto h = dynamic_cast (army); - if (h && h->commander == c) - heroWithDeadCommander = army->id; //TODO: unify commander handling - } - } - } -} - -void CasualtiesAfterBattle::takeFromArmy(CGameHandler *gh) -{ - for(TStackAndItsNewCount &ncount : newStackCounts) - { - if(ncount.second > 0) - gh->changeStackCount(ncount.first, ncount.second, true); - else - gh->eraseStack(ncount.first, true); - } - for (auto al : removedWarMachines) - { - gh->removeArtifact(al); - } - if (heroWithDeadCommander != ObjectInstanceID()) - { - SetCommanderProperty scp; - scp.heroid = heroWithDeadCommander; - scp.which = SetCommanderProperty::ALIVE; - scp.amount = 0; - gh->sendAndApply (&scp); - } -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper(shared_ptr Query, bool Duel, int RemainingBattleQueriesCount) -{ - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; - duel = Duel; - remainingBattleQueriesCount = RemainingBattleQueriesCount; -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; -} +#include "StdInc.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CFileInfo.h" +#include "../lib/int3.h" +#include "../lib/mapping/CCampaignHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/CModHandler.h" +#include "../lib/CArtHandler.h" +#include "../lib/CBuildingHandler.h" +#include "../lib/CDefObjInfoHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CObjectHandler.h" +#include "../lib/CSpellHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CTownHandler.h" +#include "../lib/CCreatureHandler.h" +#include "../lib/CGameState.h" +#include "../lib/BattleState.h" +#include "../lib/CondSh.h" +#include "../lib/NetPacks.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/mapping/CMap.h" +#include "../lib/VCMIDirs.h" +#include "../lib/ScopeGuard.h" +#include "../client/CSoundBase.h" +#include "CGameHandler.h" +#include "CVCMIServer.h" +#include "../lib/CCreatureSet.h" +#include "../lib/CThreadHelper.h" +#include "../lib/GameConstants.h" +#include "../lib/registerTypes/RegisterTypes.h" + +/* + * CGameHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + + +#ifndef _MSC_VER +#include +#endif +extern bool end2; +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif + +#define COMPLAIN_RET_IF(cond, txt) do {if(cond){complain(txt); return;}} while(0) +#define COMPLAIN_RET_FALSE_IF(cond, txt) do {if(cond){complain(txt); return false;}} while(0) +#define COMPLAIN_RET(txt) {complain(txt); return false;} +#define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} +#define NEW_ROUND BattleNextRound bnr;\ + bnr.round = gs->curB->round + 1;\ + sendAndApply(&bnr); + +CondSh battleMadeAction; +CondSh battleResult(nullptr); +template class CApplyOnGH; + +class CBaseForGHApply +{ +public: + virtual bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const =0; + virtual ~CBaseForGHApply(){} + template static CBaseForGHApply *getApplier(const U * t=nullptr) + { + return new CApplyOnGH; + } +}; + +template class CApplyOnGH : public CBaseForGHApply +{ +public: + bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const + { + T *ptr = static_cast(pack); + ptr->c = c; + ptr->player = player; + return ptr->applyGh(gh); + } +}; + +template <> +class CApplyOnGH : public CBaseForGHApply +{ +public: + bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const + { + logGlobal->errorStream() << "Cannot apply on GH plain CPack!"; + assert(0); + return false; + } +}; + +static CApplier *applier = nullptr; + +CMP_stack cmpst ; + +static inline double distance(int3 a, int3 b) +{ + return std::sqrt( (double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y) ); +} +static void giveExp(BattleResult &r) +{ + r.exp[0] = 0; + r.exp[1] = 0; + for(auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) + { + r.exp[r.winner] += VLC->creh->creatures.at(i->first)->valOfBonuses(Bonus::STACK_HEALTH) * i->second; + } +} + +PlayerStatus PlayerStatuses::operator[](PlayerColor player) +{ + boost::unique_lock l(mx); + if(players.find(player) != players.end()) + { + return players.at(player); + } + else + { + throw std::runtime_error("No such player!"); + } +} +void PlayerStatuses::addPlayer(PlayerColor player) +{ + boost::unique_lock l(mx); + players[player]; +} + +bool PlayerStatuses::checkFlag(PlayerColor player, bool PlayerStatus::*flag) +{ + boost::unique_lock l(mx); + if(players.find(player) != players.end()) + { + return players[player].*flag; + } + else + { + throw std::runtime_error("No such player!"); + } +} + +void PlayerStatuses::setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val) +{ + boost::unique_lock l(mx); + if(players.find(player) != players.end()) + { + players[player].*flag = val; + } + else + { + throw std::runtime_error("No such player!"); + } + cv.notify_all(); +} + +template +void callWith(std::vector args, std::function fun, ui32 which) +{ + fun(args[which]); +} + +void CGameHandler::levelUpHero(const CGHeroInstance * hero, SecondarySkill skill) +{ + changeSecSkill(hero, skill, 1, 0); + expGiven(hero); +} + +void CGameHandler::levelUpHero(const CGHeroInstance * hero) +{ + // required exp for at least 1 lvl-up hasn't been reached + if(!hero->gainsLevel()) + { + return; + } + + //give prim skill + logGlobal->traceStream() << hero->name <<" got level "<level; + int r = rand()%100, pom=0, x=0; + + auto & skillChances = (hero->level>9) ? hero->type->heroClass->primarySkillLowLevel : hero->type->heroClass->primarySkillHighLevel; + + for(;xtraceStream() << "The hero gets the primary skill with the no. " << x << " with a probability of " << r << "%."; + SetPrimSkill sps; + sps.id = hero->id; + sps.which = static_cast(x); + sps.abs = false; + sps.val = 1; + sendAndApply(&sps); + + HeroLevelUp hlu; + hlu.hero = hero; + hlu.primskill = static_cast(x); + hlu.level = hero->level+1; + hlu.skills = hero->levelUpProposedSkills(); + + if(hlu.skills.size() == 0) + { + sendAndApply(&hlu); + levelUpHero(hero); + } + else if(hlu.skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically + { + sendAndApply(&hlu); + auto rng = [&]() mutable -> ui32 + { + return hero->skillsInfo.distribution(); //must be determined + }; + levelUpHero(hero, vstd::pickRandomElementOf (hlu.skills, rng)); + } + else if(hlu.skills.size() > 1) + { + auto levelUpQuery = make_shared(hlu); + hlu.queryID = levelUpQuery->queryID; + queries.addQuery(levelUpQuery); + sendAndApply(&hlu); + //level up will be called on query reply + } +} + +void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) +{ + SetCommanderProperty scp; + + auto hero = dynamic_cast(c->armyObj); + if (hero) + scp.heroid = hero->id; + else + { + complain ("Commander is not led by hero!"); + return; + } + + scp.accumulatedBonus.subtype = 0; + scp.accumulatedBonus.additionalInfo = 0; + scp.accumulatedBonus.duration = Bonus::PERMANENT; + scp.accumulatedBonus.turnsRemain = 0; + scp.accumulatedBonus.source = Bonus::COMMANDER; + scp.accumulatedBonus.valType = Bonus::BASE_NUMBER; + if (skill <= ECommander::SPELL_POWER) + { + scp.which = SetCommanderProperty::BONUS; + + auto difference = [](std::vector< std::vector > skillLevels, std::vector secondarySkills, int skill)->int + { + int s = std::min (skill, (int)ECommander::SPELL_POWER); //spell power level controls also casts and resistance + return skillLevels.at(skill).at(secondarySkills.at(s)) - (secondarySkills.at(s) ? skillLevels.at(skill).at(secondarySkills.at(s)-1) : 0); + }; + + switch (skill) + { + case ECommander::ATTACK: + scp.accumulatedBonus.type = Bonus::PRIMARY_SKILL; + scp.accumulatedBonus.subtype = PrimarySkill::ATTACK; + break; + case ECommander::DEFENSE: + scp.accumulatedBonus.type = Bonus::PRIMARY_SKILL; + scp.accumulatedBonus.subtype = PrimarySkill::DEFENSE; + break; + case ECommander::HEALTH: + scp.accumulatedBonus.type = Bonus::STACK_HEALTH; + scp.accumulatedBonus.valType = Bonus::PERCENT_TO_BASE; + break; + case ECommander::DAMAGE: + scp.accumulatedBonus.type = Bonus::CREATURE_DAMAGE; + scp.accumulatedBonus.subtype = 0; + scp.accumulatedBonus.valType = Bonus::PERCENT_TO_BASE; + break; + case ECommander::SPEED: + scp.accumulatedBonus.type = Bonus::STACKS_SPEED; + break; + case ECommander::SPELL_POWER: + scp.accumulatedBonus.type = Bonus::MAGIC_RESISTANCE; + scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::RESISTANCE); + sendAndApply (&scp); //additional pack + scp.accumulatedBonus.type = Bonus::CREATURE_SPELL_POWER; + scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::SPELL_POWER) * 100; //like hero with spellpower = ability level + sendAndApply (&scp); //additional pack + scp.accumulatedBonus.type = Bonus::CASTS; + scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, ECommander::CASTS); + sendAndApply (&scp); //additional pack + scp.accumulatedBonus.type = Bonus::CREATURE_ENCHANT_POWER; //send normally + break; + } + + scp.accumulatedBonus.val = difference (VLC->creh->skillLevels, c->secondarySkills, skill); + sendAndApply (&scp); + + scp.which = SetCommanderProperty::SECONDARY_SKILL; + scp.additionalInfo = skill; + scp.amount = c->secondarySkills.at(skill) + 1; + sendAndApply (&scp); + } + else if (skill >= 100) + { + scp.which = SetCommanderProperty::SPECIAL_SKILL; + scp.accumulatedBonus = *VLC->creh->skillRequirements.at(skill-100).first; + scp.additionalInfo = skill; //unnormalized + sendAndApply (&scp); + } + expGiven(hero); +} + +void CGameHandler::levelUpCommander(const CCommanderInstance * c) +{ + if (!c->gainsLevel()) + { + return; + } + CommanderLevelUp clu; + + auto hero = dynamic_cast(c->armyObj); + if (hero) + clu.hero = hero; + else + { + complain ("Commander is not led by hero!"); + return; + } + + //picking sec. skills for choice + + for (int i = 0; i <= ECommander::SPELL_POWER; ++i) + { + if (c->secondarySkills.at(i) < ECommander::MAX_SKILL_LEVEL) + clu.skills.push_back(i); + } + int i = 100; + for (auto specialSkill : VLC->creh->skillRequirements) + { + if (c->secondarySkills.at(specialSkill.second.first) == ECommander::MAX_SKILL_LEVEL + && c->secondarySkills.at(specialSkill.second.second) == ECommander::MAX_SKILL_LEVEL + && !vstd::contains (c->specialSKills, i)) + clu.skills.push_back (i); + ++i; + } + int skillAmount = clu.skills.size(); + + if(!skillAmount) + { + sendAndApply(&clu); + levelUpCommander(c); + } + else if(skillAmount == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically + { + sendAndApply(&clu); + levelUpCommander(c, vstd::pickRandomElementOf (clu.skills, rand)); + } + else if(skillAmount > 1) //apply and ask for secondary skill + { + auto commanderLevelUp = make_shared(clu); + clu.queryID = commanderLevelUp->queryID; + queries.addQuery(commanderLevelUp); + sendAndApply(&clu); + } +} + +void CGameHandler::expGiven(const CGHeroInstance *hero) +{ + if(hero->gainsLevel()) + levelUpHero(hero); + else if(hero->commander && hero->commander->gainsLevel()) + levelUpCommander(hero->commander); + + //if(hero->commander && hero->level > hero->commander->level && hero->commander->gainsLevel()) +// levelUpCommander(hero->commander); +// else +// levelUpHero(hero); +} + +void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs) +{ + if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached + { + if (gs->map->levelLimit != 0) + { + TExpType expLimit = VLC->heroh->reqExp(gs->map->levelLimit); + TExpType resultingExp = abs ? val : hero->exp + val; + if (resultingExp > expLimit) + { + // set given experience to max possible, but don't decrease if hero already over top + abs = true; + val = std::max(expLimit, hero->exp); + + InfoWindow iw; + iw.player = hero->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 1); //can gain no more XP + iw.text.addReplacement(hero->name); + sendAndApply(&iw); + } + } + } + + SetPrimSkill sps; + sps.id = hero->id; + sps.which = which; + sps.abs = abs; + sps.val = val; + sendAndApply(&sps); + + //only for exp - hero may level up + if (which == PrimarySkill::EXPERIENCE) + { + if(hero->commander && hero->commander->alive) + { + //FIXME: trim experience according to map limit? + SetCommanderProperty scp; + scp.heroid = hero->id; + scp.which = SetCommanderProperty::EXPERIENCE; + scp.amount = val; + sendAndApply (&scp); + CBonusSystemNode::treeHasChanged(); + } + + expGiven(hero); + } +} + +void CGameHandler::changeSecSkill( const CGHeroInstance * hero, SecondarySkill which, int val, bool abs/*=false*/ ) +{ + SetSecSkill sss; + sss.id = hero->id; + sss.which = which; + sss.val = val; + sss.abs = abs; + sendAndApply(&sss); + + if(which == SecondarySkill::WISDOM) + { + if(hero && hero->visitedTown) + giveSpells(hero->visitedTown, hero); + } +} + +void CGameHandler::endBattle(int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) +{ + LOG_TRACE(logGlobal); + + //Fill BattleResult structure with exp info + giveExp(*battleResult.data); + + if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped + { + if (hero1) + battleResult.data->exp[1] += 500; + if (hero2) + battleResult.data->exp[0] += 500; + } + + if (hero1) + battleResult.data->exp[0] = hero1->calculateXp(battleResult.data->exp[0]);//scholar skill + if (hero2) + battleResult.data->exp[1] = hero2->calculateXp(battleResult.data->exp[1]); + + const CArmedInstance *bEndArmy1 = gs->curB->sides.at(0).armyObject; + const CArmedInstance *bEndArmy2 = gs->curB->sides.at(1).armyObject; + const BattleResult::EResult result = battleResult.get()->result; + + auto findBattleQuery = [this] () -> shared_ptr + { + for(auto &q : queries.allQueries()) + { + if(auto bq = std::dynamic_pointer_cast(q)) + if(bq->bi == gs->curB) + return bq; + } + return shared_ptr(); + }; + + auto battleQuery = findBattleQuery(); + if(!battleQuery) + { + logGlobal->errorStream() << "Cannot find battle query!"; + if(gs->initialOpts->mode == StartInfo::DUEL) + { + battleQuery = make_shared(gs->curB); + } + } + if(battleQuery != queries.topQuery(gs->curB->sides[0].color)) + complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " although in battle has no battle query at the top!"); + + battleQuery->result = *battleResult.data; + + //Check how many battle queries were created (number of players blocked by battle) + const int queriedPlayers = battleQuery ? boost::count(queries.allQueries(), battleQuery) : 0; + finishingBattle = make_unique(battleQuery, gs->initialOpts->mode == StartInfo::DUEL, queriedPlayers); + + + CasualtiesAfterBattle cab1(bEndArmy1, gs->curB), cab2(bEndArmy2, gs->curB); //calculate casualties before deleting battle + + if(finishingBattle->duel) + { + duelFinished(); + sendAndApply(battleResult.data); //after this point casualties objects are destroyed + return; + } + + + ChangeSpells cs; //for Eagle Eye + + if(finishingBattle->winnerHero) + { + if(int eagleEyeLevel = finishingBattle->winnerHero->getSecSkillLevel(SecondarySkill::EAGLE_EYE)) + { + int maxLevel = eagleEyeLevel + 1; + double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::EAGLE_EYE); + for(const CSpell *sp : gs->curB->sides.at(!battleResult.data->winner).usedSpellsHistory) + if(sp->level <= maxLevel && !vstd::contains(finishingBattle->winnerHero->spells, sp->id) && rand() % 100 < eagleEyeChance) + cs.spells.insert(sp->id); + } + } + + + std::vector arts; //display them in window + + if (result == BattleResult::NORMAL && finishingBattle->winnerHero) + { + if (finishingBattle->loserHero) + { + auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; //TODO: wrap it into a function, somehow (boost::variant -_-) + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation (finishingBattle->loserHero, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig() && art->artType->id != 0) // don't move war machines or locked arts (spellbook) + { + arts.push_back (art->artType->id); + ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); + sendAndApply(&ma); + } + } + while (!finishingBattle->loserHero->artifactsInBackpack.empty()) + { + //we assume that no big artifacts can be found + MoveArtifact ma; + ma.src = ArtifactLocation (finishingBattle->loserHero, + ArtifactPosition(GameConstants::BACKPACK_START)); //backpack automatically shifts arts to beginning + const CArtifactInstance * art = ma.src.getArt(); + arts.push_back (art->artType->id); + ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); + sendAndApply(&ma); + } + if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? + { + artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation (finishingBattle->loserHero->commander.get(), artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + arts.push_back (art->artType->id); + ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); + sendAndApply(&ma); + } + } + } + } + for (auto armySlot : gs->curB->battleGetArmyObject(!battleResult.data->winner)->stacks) + { + auto artifactsWorn = armySlot.second->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation (armySlot.second, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + arts.push_back (art->artType->id); + ma.dst = ArtifactLocation (finishingBattle->winnerHero, art->firstAvailableSlot(finishingBattle->winnerHero)); + sendAndApply(&ma); + } + } + } + } + + sendAndApply(battleResult.data); //after this point casualties objects are destroyed + + if (arts.size()) //display loot + { + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + + iw.text.addTxt (MetaString::GENERAL_TXT, 30); //You have captured enemy artifact + + for (auto id : arts) //TODO; separate function to display loot for various ojects? + { + iw.components.push_back (Component (Component::ARTIFACT, id, 0, 0)); + if(iw.components.size() >= 14) + { + sendAndApply(&iw); + iw.components.clear(); + iw.text.addTxt (MetaString::GENERAL_TXT, 30); //repeat + } + } + if (iw.components.size()) + { + sendAndApply(&iw); + } + } + //Eagle Eye secondary skill handling + if(!cs.spells.empty()) + { + cs.learn = 1; + cs.hid = finishingBattle->winnerHero->id; + + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s + iw.text.addReplacement(finishingBattle->winnerHero->name); + + std::ostringstream names; + for(int i = 0; i < cs.spells.size(); i++) + { + names << "%s"; + if(i < cs.spells.size() - 2) + names << ", "; + else if(i < cs.spells.size() - 1) + names << "%s"; + } + names << "."; + + iw.text.addReplacement(names.str()); + + auto it = cs.spells.begin(); + for(int i = 0; i < cs.spells.size(); i++, it++) + { + iw.text.addReplacement(MetaString::SPELL_NAME, it->toEnum()); + if(i == cs.spells.size() - 2) //we just added pre-last name + iw.text.addReplacement(MetaString::GENERAL_TXT, 141); // " and " + iw.components.push_back(Component(Component::SPELL, *it, 0, 0)); + } + + sendAndApply(&iw); + sendAndApply(&cs); + } + + cab1.takeFromArmy(this); + cab2.takeFromArmy(this); //take casualties after battle is deleted + + //if one hero has lost we will erase him + if(battleResult.data->winner!=0 && hero1) + { + RemoveObject ro(hero1->id); + sendAndApply(&ro); + } + if(battleResult.data->winner!=1 && hero2) + { + RemoveObject ro(hero2->id); + sendAndApply(&ro); + } + + //give exp + if (battleResult.data->exp[0] && hero1 && battleResult.get()->winner == 0) + changePrimSkill(hero1, PrimarySkill::EXPERIENCE, battleResult.data->exp[0]); + else if (battleResult.data->exp[1] && hero2 && battleResult.get()->winner == 1) + changePrimSkill(hero2, PrimarySkill::EXPERIENCE, battleResult.data->exp[1]); + + queries.popIfTop(battleQuery); + + //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) +} + +void CGameHandler::battleAfterLevelUp( const BattleResult &result ) +{ + LOG_TRACE(logGlobal); + + + finishingBattle->remainingBattleQueriesCount--; + logGlobal->traceStream() << "Decremented queries count to " << finishingBattle->remainingBattleQueriesCount; + + if(finishingBattle->remainingBattleQueriesCount > 0) + //Battle results will be handled when all battle queries are closed + return; + + //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible + // but the battle consequences are applied after final player is unblocked. Hard to abuse... + // Still, it looks like a hole. + + // Necromancy if applicable. + const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); + // Give raised units to winner and show dialog, if any were raised, + // units will be given after casualties are taken + const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); + + if (necroSlot != SlotID()) + { + finishingBattle->winnerHero->showNecromancyDialog(raisedStack); + addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + } + + BattleResultsApplied resultsApplied; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + sendAndApply(&resultsApplied); + + setBattle(nullptr); + + if(visitObjectAfterVictory && result.winner==0) + { + logGlobal->traceStream() << "post-victory visit"; + visitObjectOnTile(*getTile(finishingBattle->winnerHero->getPosition()), finishingBattle->winnerHero); + } + visitObjectAfterVictory = false; + + //handle victory/loss of engaged players + std::set playerColors = boost::assign::list_of(finishingBattle->loser)(finishingBattle->victor); + checkVictoryLossConditions(playerColors); + + if(result.result == BattleResult::SURRENDER || result.result == BattleResult::ESCAPE) //loser has escaped or surrendered + { + SetAvailableHeroes sah; + sah.player = finishingBattle->loser; + sah.hid[0] = finishingBattle->loserHero->subID; + if(result.result == BattleResult::ESCAPE) //retreat + { + sah.army[0].clear(); + sah.army[0].setCreature(SlotID(0), finishingBattle->loserHero->type->initialArmy.at(0).creature, 1); + } + + if(const CGHeroInstance *another = getPlayer(finishingBattle->loser)->availableHeroes.at(1)) + sah.hid[1] = another->subID; + else + sah.hid[1] = -1; + + sendAndApply(&sah); + } +} + +void CGameHandler::prepareAttack(BattleAttack &bat, const CStack *att, const CStack *def, int distance, int targetHex) +{ + bat.bsa.clear(); + bat.stackAttacking = att->ID; + const int attackerLuck = att->LuckVal(); + + auto sideHeroBlocksLuck = [](const SideInBattle &side){ return NBonus::hasOfType(side.hero, Bonus::BLOCK_LUCK); }; + + if(!vstd::contains_if(gs->curB->sides, sideHeroBlocksLuck)) + { + if(attackerLuck > 0 && rand()%24 < attackerLuck) + { + bat.flags |= BattleAttack::LUCKY; + } + if (VLC->modh->settings.data["hardcodedFeatures"]["NEGATIVE_LUCK"].Bool()) // negative luck enabled + { + if (attackerLuck < 0 && rand()%24 < abs(attackerLuck)) + { + bat.flags |= BattleAttack::UNLUCKY; + } + } + } + + if (rand()%100 < att->valOfBonuses(Bonus::DOUBLE_DAMAGE_CHANCE)) + { + bat.flags |= BattleAttack::DEATH_BLOW; + } + + if(att->getCreature()->idNumber == CreatureID::BALLISTA) + { + static const int artilleryLvlToChance[] = {0, 50, 75, 100}; + const CGHeroInstance * owner = gs->curB->getHero(att->owner); + int chance = artilleryLvlToChance[owner->getSecSkillLevel(SecondarySkill::ARTILLERY)]; + if(chance > rand() % 100) + { + bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; + } + } + // only primary target + applyBattleEffects(bat, att, def, distance, false); + + if (!bat.shot()) //multiple-hex attack - only in meele + { + std::set attackedCreatures = gs->curB->getAttackedCreatures(att, targetHex); //creatures other than primary target + + for(const CStack * stack : attackedCreatures) + { + if (stack != def) //do not hit same stack twice + { + applyBattleEffects(bat, att, stack, distance, true); + } + } + } + + const Bonus * bonus = att->getBonusLocalFirst(Selector::type(Bonus::SPELL_LIKE_ATTACK)); + if (bonus && (bat.shot())) //TODO: make it work in meele? + { + bat.bsa.front().flags |= BattleStackAttacked::EFFECT; + bat.bsa.front().effect = VLC->spellh->objects.at(bonus->subtype)->mainEffectAnim; //hopefully it does not interfere with any other effect? + + std::set attackedCreatures = gs->curB->getAffectedCreatures(SpellID(bonus->subtype).toSpell(), bonus->val, att->owner, targetHex); + //TODO: get exact attacked hex for defender + + for(const CStack * stack : attackedCreatures) + { + if (stack != def) //do not hit same stack twice + { + applyBattleEffects(bat, att, stack, distance, true); + } + } + } +} +void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, const CStack *def, int distance, bool secondary) //helper function for prepareAttack +{ + BattleStackAttacked bsa; + if (secondary) + bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities + bsa.attackerID = att->ID; + bsa.stackAttacked = def->ID; + bsa.damageAmount = gs->curB->calculateDmg(att, def, gs->curB->battleGetOwner(att), gs->curB->battleGetOwner(def), bat.shot(), distance, bat.lucky(), bat.unlucky(), bat.deathBlow(), bat.ballistaDoubleDmg()); + def->prepareAttacked(bsa, gameState()->getRandomGenerator()); //calculate casualties + + //life drain handling + if (att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving()) + { + StacksHealedOrResurrected shi; + shi.lifeDrain = (ui8)true; + shi.tentHealing = (ui8)false; + shi.drainedFrom = def->ID; + + StacksHealedOrResurrected::HealInfo hi; + hi.stackID = att->ID; + hi.healedHP = std::min (bsa.damageAmount * att->valOfBonuses (Bonus::LIFE_DRAIN) / 100, + att->MaxHealth() - att->firstHPleft + att->MaxHealth() * (att->baseAmount - att->count) ); + hi.lowLevelResurrection = false; + shi.healedStacks.push_back(hi); + + if (hi.healedHP > 0) + { + bsa.healedStacks.push_back(shi); + } + } + bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated + + //fire shield handling + if (!bat.shot() && def->hasBonusOfType(Bonus::FIRE_SHIELD) && !att->hasBonusOfType (Bonus::FIRE_IMMUNITY) && !bsa.killed() ) + { + BattleStackAttacked bsa2; + bsa2.stackAttacked = att->ID; //invert + bsa2.attackerID = def->ID; + bsa2.flags |= BattleStackAttacked::EFFECT; //FIXME: play anmation upon efreet and not attacker + bsa2.effect = 11; + + bsa2.damageAmount = (bsa.damageAmount * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense + att->prepareAttacked(bsa2, gameState()->getRandomGenerator()); + bat.bsa.push_back(bsa2); + } +} +void CGameHandler::handleConnection(std::set players, CConnection &c) +{ + setThreadName("CGameHandler::handleConnection"); + srand(time(nullptr)); + + try + { + while(1)//server should never shut connection first //was: while(!end2) + { + CPack *pack = nullptr; + PlayerColor player = PlayerColor::NEUTRAL; + si32 requestID = -999; + int packType = 0; + + { + boost::unique_lock lock(*c.rmx); + c >> player >> requestID >> pack; //get the package + + if(!pack) + { + logGlobal ->errorStream() << boost::format("Received a null package marked as request %d from player %d") % requestID % player; + } + + packType = typeList.getTypeID(pack); //get the id of type + + logGlobal->traceStream() << boost::format("Received client message (request %d by player %d) of type with ID=%d (%s).\n") + % requestID % player.getNum() % packType % typeid(*pack).name(); + } + + //prepare struct informing that action was applied + auto sendPackageResponse = [&](bool succesfullyApplied) + { + PackageApplied applied; + applied.player = player; + applied.result = succesfullyApplied; + applied.packType = packType; + applied.requestID = requestID; + boost::unique_lock lock(*c.wmx); + c << &applied; + }; + + CBaseForGHApply *apply = applier->apps[packType]; //and appropriae applier object + if(isBlockedByQueries(pack, player)) + { + sendPackageResponse(false); + } + else if(apply) + { + const bool result = apply->applyOnGH(this,&c,pack, player); + if(!result) + complain("Got false in applying... that request must have been fishy!"); + logGlobal->traceStream() << "Message successfully applied (result=" << result << ")!"; + sendPackageResponse(true); + } + else + { + logGlobal->errorStream() << "Message cannot be applied, cannot find applier (unregistered type)!"; + sendPackageResponse(false); + } + + vstd::clear_pointer(pack); + } + } + catch(boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection + { + assert(!c.connected); //make sure that connection has been marked as broken + logGlobal->errorStream() << e.what(); + end2 = true; + } + HANDLE_EXCEPTION(end2 = true); + + logGlobal->errorStream() << "Ended handling connection"; +} + +int CGameHandler::moveStack(int stack, BattleHex dest) +{ + int ret = 0; + + const CStack *curStack = gs->curB->battleGetStackByID(stack), + *stackAtEnd = gs->curB->battleGetStackByPos(dest); + + assert(curStack); + assert(dest < GameConstants::BFIELD_SIZE); + + if (gs->curB->tacticDistance) + { + assert(gs->curB->isInTacticRange(dest)); + } + + if(curStack->position == dest) + return 0; + + //initing necessary tables + auto accessibility = getAccesibility(curStack); + + //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) + if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) + { + if(curStack->attackerOwned) + { + if(accessibility.accessible(dest+1, curStack)) + dest += BattleHex::RIGHT; + } + else + { + if(accessibility.accessible(dest-1, curStack)) + dest += BattleHex::LEFT; + } + } + + if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) + { + complain("Given destination is not accessible!"); + return 0; + } + + std::pair< std::vector, int > path = gs->curB->getPath(curStack->position, dest, curStack); + + ret = path.second; + + int creSpeed = gs->curB->tacticDistance ? GameConstants::BFIELD_SIZE : curStack->Speed(); + + if(curStack->hasBonusOfType(Bonus::FLYING)) + { + if(path.second <= creSpeed && path.first.size() > 0) + { + //inform clients about move + BattleStackMoved sm; + sm.stack = curStack->ID; + std::vector tiles; + tiles.push_back(path.first[0]); + sm.tilesToMove = tiles; + sm.distance = path.second; + sm.teleporting = false; + sendAndApply(&sm); + } + } + else //for non-flying creatures + { + // send one package with the creature path information + + shared_ptr obstacle; //obstacle that interrupted movement + std::vector tiles; + int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); + int v = path.first.size()-1; + +startWalking: + for(; v >= tilesToMove; --v) + { + BattleHex hex = path.first[v]; + tiles.push_back(hex); + + if((obstacle = battleGetObstacleOnPos(hex, false))) + { + //we walked onto something, so we finalize this portion of stack movement check into obstacle + break; + } + } + + if (tiles.size() > 0) + { + //commit movement + BattleStackMoved sm; + sm.stack = curStack->ID; + sm.distance = path.second; + sm.teleporting = false; + sm.tilesToMove = tiles; + sendAndApply(&sm); + } + + //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end + if(obstacle && curStack->position != dest) + { + handleDamageFromObstacle(*obstacle, curStack); + + //if stack didn't die in explosion, continue movement + if(!obstacle->stopsMovement() && curStack->alive()) + { + obstacle.reset(); + tiles.clear(); + v--; + goto startWalking; //TODO it's so evil + } + } + } + + //handling obstacle on the final field (separate, because it affects both flying and walking stacks) + if(curStack->alive()) + { + if(auto theLastObstacle = battleGetObstacleOnPos(curStack->position, false)) + { + handleDamageFromObstacle(*theLastObstacle, curStack); + } + } + return ret; +} + +CGameHandler::CGameHandler(void) +{ + QID = 1; + //gs = nullptr; + IObjectInterface::cb = this; + applier = new CApplier; + registerTypesServerPacks(*applier); + visitObjectAfterVictory = false; + queries.gh = this; +} + +CGameHandler::~CGameHandler(void) +{ + delete applier; + applier = nullptr; + delete gs; +} + +void CGameHandler::init(StartInfo *si) +{ + if(si->seedToBeUsed == 0) + { + si->seedToBeUsed = std::time(nullptr); + } + + gs = new CGameState(); + logGlobal->infoStream() << "Gamestate created!"; + gs->init(si); + logGlobal->infoStream() << "Gamestate initialized!"; + + for(auto & elem : gs->players) + { + states.addPlayer(elem.first); + } +} + +static bool evntCmp(const CMapEvent &a, const CMapEvent &b) +{ + return a.earlierThan(b); +} + +void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=false, bool clear = false) +{// bool forced = true - if creature should be replaced, if false - only if no creature was set + const PlayerState *p = gs->getPlayer(town->tempOwner); + if(!p) + { + logGlobal->warnStream() << "There is no player owner of town " << town->name << " at " << town->pos; + return; + } + + if (forced || town->creatures.at(GameConstants::CREATURES_PER_TOWN).second.empty())//we need to change creature + { + SetAvailableCreatures ssi; + ssi.tid = town->id; + ssi.creatures = town->creatures; + ssi.creatures[GameConstants::CREATURES_PER_TOWN].second.clear();//remove old one + + const std::vector > &dwellings = p->dwellings; + if (dwellings.empty())//no dwellings - just remove + { + sendAndApply(&ssi); + return; + } + + ui32 dwellpos = rand()%dwellings.size();//take random dwelling + ui32 creapos = rand()%dwellings.at(dwellpos)->creatures.size();//for multi-creature dwellings like Golem Factory + CreatureID creature = dwellings.at(dwellpos)->creatures.at(creapos).second[0]; + + if (clear) + ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = std::max((ui32)1, (VLC->creh->creatures.at(creature)->growth)/2); + else + ssi.creatures[GameConstants::CREATURES_PER_TOWN].first = VLC->creh->creatures.at(creature)->growth; + ssi.creatures[GameConstants::CREATURES_PER_TOWN].second.push_back(creature); + sendAndApply(&ssi); + } +} + +void CGameHandler::newTurn() +{ + logGlobal->traceStream() << "Turn " << gs->day+1; + NewTurn n; + n.specialWeek = NewTurn::NO_ACTION; + n.creatureid = CreatureID::NONE; + n.day = gs->day + 1; + + bool firstTurn = !getDate(Date::DAY); + bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched + bool newMonth = getDate(Date::DAY_OF_MONTH) == 28; + + std::map hadGold;//starting gold - for buildings like dwarven treasury + srand(time(nullptr)); + + if (firstTurn) + { + for (auto obj : gs->map->objects) + { + if (obj && obj->ID == Obj::PRISON) //give imprisoned hero 0 exp to level him up. easiest to do at this point + { + changePrimSkill (getHero(obj->id), PrimarySkill::EXPERIENCE, 0); + } + } + } + + if (newWeek && !firstTurn) + { + n.specialWeek = NewTurn::NORMAL; + bool deityOfFireBuilt = false; + for(const CGTownInstance *t : gs->map->towns) + { + if(t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO)) + { + deityOfFireBuilt = true; + break; + } + } + + if(deityOfFireBuilt) + { + n.specialWeek = NewTurn::DEITYOFFIRE; + n.creatureid = CreatureID::IMP; + } + else + { + int monthType = rand()%100; + if(newMonth) //new month + { + if (monthType < 40) //double growth + { + n.specialWeek = NewTurn::DOUBLE_GROWTH; + if (VLC->modh->settings.ALL_CREATURES_GET_DOUBLE_MONTHS) + { + std::pair newMonster(54, VLC->creh->pickRandomMonster(gs->getRandomGenerator())); + n.creatureid = newMonster.second; + } + else if(VLC->creh->doubledCreatures.size()) + { + const std::vector doubledCreatures (VLC->creh->doubledCreatures.begin(), VLC->creh->doubledCreatures.end()); + n.creatureid = vstd::pickRandomElementOf(doubledCreatures, []{ return rand(); }); + } + else + { + complain("Cannot find creature that can be spawned!"); + n.specialWeek = NewTurn::NORMAL; + } + } + else if (monthType < 50) + n.specialWeek = NewTurn::PLAGUE; + } + else //it's a week, but not full month + { + if (monthType < 25) + { + n.specialWeek = NewTurn::BONUS_GROWTH; //+5 + std::pair newMonster(54, VLC->creh->pickRandomMonster(gs->getRandomGenerator())); + //TODO do not pick neutrals + n.creatureid = newMonster.second; + } + } + } + } + + std::map > pool = gs->hpool.heroesPool; + + for (auto & elem : gs->players) + { + if(elem.first == PlayerColor::NEUTRAL) + continue; + else if(elem.first >= PlayerColor::PLAYER_LIMIT) + assert(0); //illegal player number! + + std::pair playerGold(elem.first, elem.second.resources.at(Res::GOLD)); + hadGold.insert(playerGold); + + if(newWeek) //new heroes in tavern + { + SetAvailableHeroes sah; + sah.player = elem.first; + + //pick heroes and their armies + CHeroClass *banned = nullptr; + for (int j = 0; j < GameConstants::AVAILABLE_HEROES_PER_PLAYER; j++) + { + //first hero - native if possible, second hero -> any other class + if(CGHeroInstance *h = gs->hpool.pickHeroFor(j == 0, elem.first, getNativeTown(elem.first), pool, gs->getRandomGenerator(), banned)) + { + sah.hid[j] = h->subID; + h->initArmy(&sah.army[j]); + banned = h->type->heroClass; + } + else + sah.hid[j] = -1; + } + + sendAndApply(&sah); + } + + n.res[elem.first] = elem.second.resources; + + for(CGHeroInstance *h : (elem).second.heroes) + { + if(h->visitedTown) + giveSpells(h->visitedTown, h); + + NewTurn::Hero hth; + hth.id = h->id; + hth.move = h->maxMovePoints(gs->map->getTile(h->getPosition(false)).terType != ETerrainType::WATER); + + if(h->visitedTown && h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) //if hero starts turn in town with mage guild + hth.mana = std::max(h->mana, h->manaLimit()); //restore all mana + else + hth.mana = std::max((si32)(0), std::max(h->mana, std::min((si32)(h->mana + h->manaRegain()), h->manaLimit()))); + + n.heroes.insert(hth); + + if(!firstTurn) //not first day + { + n.res[elem.first][Res::GOLD] += h->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ESTATES)); //estates + + for (int k = 0; k < GameConstants::RESOURCE_QUANTITY; k++) + { + n.res[elem.first][k] += h->valOfBonuses(Bonus::GENERATE_RESOURCE, k); + } + } + } + } + for(CGTownInstance *t : gs->map->towns) + { + PlayerColor player = t->tempOwner; + handleTownEvents(t, n); + if(newWeek) //first day of week + { + if(t->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) + setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning + + if(!firstTurn) + if (t->hasBuilt(BuildingID::TREASURY, ETownType::RAMPART) && player < PlayerColor::PLAYER_LIMIT) + n.res[player][Res::GOLD] += hadGold.at(player)/10; //give 10% of starting gold + + if (!vstd::contains(n.cres, t->id)) + { + n.cres[t->id].tid = t->id; + n.cres[t->id].creatures = t->creatures; + } + auto & sac = n.cres.at(t->id); + + for (int k=0; k < GameConstants::CREATURES_PER_TOWN; k++) //creature growths + { + if (!t->creatures.at(k).second.empty()) // there are creatures at this level + { + ui32 &availableCount = sac.creatures.at(k).first; + const CCreature *cre = VLC->creh->creatures.at(t->creatures.at(k).second.back()); + + if (n.specialWeek == NewTurn::PLAGUE) + availableCount = t->creatures.at(k).first / 2; //halve their number, no growth + else + { + if(firstTurn) //first day of game: use only basic growths + availableCount = cre->growth; + else + availableCount += t->creatureGrowth(k); + + //Deity of fire week - upgrade both imps and upgrades + if (n.specialWeek == NewTurn::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, n.creatureid)) + availableCount += 15; + + if( cre->idNumber == n.creatureid ) //bonus week, effect applies only to identical creatures + { + if(n.specialWeek == NewTurn::DOUBLE_GROWTH) + availableCount *= 2; + else if(n.specialWeek == NewTurn::BONUS_GROWTH) + availableCount += 5; + } + } + } + } + } + if(!firstTurn && player < PlayerColor::PLAYER_LIMIT)//not the first day and town not neutral + { + if(t->hasBuilt(BuildingID::RESOURCE_SILO)) //there is resource silo + { + if(t->town->primaryRes == Res::WOOD_AND_ORE) //we'll give wood and ore + { + n.res[player][Res::WOOD] ++; + n.res[player][Res::ORE] ++; + } + else + { + n.res[player][t->town->primaryRes] ++; + } + } + + n.res[player][Res::GOLD] += t->dailyIncome(); + } + if(t->hasBuilt(BuildingID::GRAIL, ETownType::TOWER)) + { + // Skyship, probably easier to handle same as Veil of darkness + //do it every new day after veils apply + if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player + { + FoWChange fw; + fw.mode = 1; + fw.player = player; + // find all hidden tiles + const auto & fow = gs->getPlayerTeam(player)->fogOfWarMap; + for (size_t i=0; ihasBonusOfType (Bonus::DARKNESS)) + { + t->hideTiles(t->getOwner(), t->getBonusLocalFirst(Selector::type(Bonus::DARKNESS))->val); + } + //unhiding what shouldn't be hidden? //that's handled in netpacks client + } + + if(newMonth) + { + SetAvailableArtifacts saa; + saa.id = -1; + pickAllowedArtsSet(saa.arts); + sendAndApply(&saa); + } + sendAndApply(&n); + + if(newWeek) + { + //spawn wandering monsters + if (newMonth && (n.specialWeek == NewTurn::DOUBLE_GROWTH || n.specialWeek == NewTurn::DEITYOFFIRE)) + { + spawnWanderingMonsters(n.creatureid); + } + + //new week info popup + if(!firstTurn) + { + InfoWindow iw; + switch (n.specialWeek) + { + case NewTurn::DOUBLE_GROWTH: + iw.text.addTxt(MetaString::ARRAY_TXT, 131); + iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); + iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); + break; + case NewTurn::PLAGUE: + iw.text.addTxt(MetaString::ARRAY_TXT, 132); + break; + case NewTurn::BONUS_GROWTH: + iw.text.addTxt(MetaString::ARRAY_TXT, 134); + iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); + iw.text.addReplacement(MetaString::CRE_SING_NAMES, n.creatureid); + break; + case NewTurn::DEITYOFFIRE: + iw.text.addTxt(MetaString::ARRAY_TXT, 135); + iw.text.addReplacement(MetaString::CRE_SING_NAMES, 42); //%s imp + iw.text.addReplacement(MetaString::CRE_SING_NAMES, 42); //%s imp + iw.text.addReplacement2(15); //%+d 15 + iw.text.addReplacement(MetaString::CRE_SING_NAMES, 43); //%s familiar + iw.text.addReplacement2(15); //%+d 15 + break; + default: + if (newMonth) + { + iw.text.addTxt(MetaString::ARRAY_TXT, (130)); + iw.text.addReplacement(MetaString::ARRAY_TXT, 32 + rand()%10); + } + else + { + iw.text.addTxt(MetaString::ARRAY_TXT, (133)); + iw.text.addReplacement(MetaString::ARRAY_TXT, 43 + rand()%15); + } + } + for (auto & elem : gs->players) + { + iw.player = elem.first; + sendAndApply(&iw); + } + } + } + + logGlobal->traceStream() << "Info about turn " << n.day << "has been sent!"; + handleTimeEvents(); + //call objects + for(auto & elem : gs->map->objects) + { + if(elem) + elem->newTurn(); + } + + synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that +} +void CGameHandler::run(bool resume) +{ + LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); + + using namespace boost::posix_time; + for(CConnection *cc : conns) + { + if(!resume) + { + (*cc) << gs->initialOpts; // gs->scenarioOps + } + + std::set players; + (*cc) >> players; //how many players will be handled at that client + + std::stringstream sbuffer; + sbuffer << "Connection " << cc->connectionID << " will handle " << players.size() << " player: "; + for(PlayerColor color : players) + { + sbuffer << color << " "; + { + boost::unique_lock lock(gsm); + connections[color] = cc; + } + } + logGlobal->infoStream() << sbuffer.str(); + + cc->addStdVecItems(gs); + cc->enableStackSendingByID(); + cc->disableSmartPointerSerialization(); + } + + for(auto & elem : conns) + { + std::set pom; + for(auto j = connections.cbegin(); j!=connections.cend();j++) + if(j->second == elem) + pom.insert(j->first); + + boost::thread(boost::bind(&CGameHandler::handleConnection,this,pom,boost::ref(*elem))); + } + + if(gs->scenarioOps->mode == StartInfo::DUEL) + { + runBattle(); + end2 = true; + + + while(conns.size() && (*conns.begin())->isOpen()) + boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket + + return; + } + + auto playerTurnOrder = generatePlayerTurnOrder(); + + while(!end2) + { + if(!resume) newTurn(); + + std::list::iterator it; + if(resume) + { + it = std::find(playerTurnOrder.begin(), playerTurnOrder.end(), gs->currentPlayer); + } + else + { + it = playerTurnOrder.begin(); + } + + resume = false; + for(; it != playerTurnOrder.end(); it++) + { + auto playerColor = *it; + if(gs->players[playerColor].status == EPlayerStatus::INGAME) + { + states.setFlag(playerColor, &PlayerStatus::makingTurn, true); + + YourTurn yt; + yt.player = playerColor; + applyAndSend(&yt); + + checkVictoryLossConditionsForAll(); + + //wait till turn is done + boost::unique_lock lock(states.mx); + while(states.players.at(playerColor).makingTurn && !end2) + { + static time_duration p = milliseconds(200); + states.cv.timed_wait(lock,p); + } + } + } + } + while(conns.size() && (*conns.begin())->isOpen()) + boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket +} + +std::list CGameHandler::generatePlayerTurnOrder() const +{ + // Generate player turn order + std::list playerTurnOrder; + + for(const auto & player : gs->players) // add human players first + { + if(player.second.human) + playerTurnOrder.push_back(player.first); + } + for(const auto & player : gs->players) // then add non-human players + { + if(!player.second.human) + playerTurnOrder.push_back(player.first); + } + return std::move(playerTurnOrder); +} + +void CGameHandler::setupBattle( int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town ) +{ + battleResult.set(nullptr); + + //send info about battles + BattleStart bs; + bs.info = gs->setupBattle(tile, armies, heroes, creatureBank, town); + sendAndApply(&bs); +} + +void CGameHandler::checkForBattleEnd() +{ + if(auto result = battleIsFinished()) + { + setBattleResult(BattleResult::NORMAL, *result); + } +} + +void CGameHandler::giveSpells( const CGTownInstance *t, const CGHeroInstance *h ) +{ + if(!h->hasSpellbook()) + return; //hero hasn't spellbook + ChangeSpells cs; + cs.hid = h->id; + cs.learn = true; + for(int i=0; imageGuildLevel(),h->getSecSkillLevel(SecondarySkill::WISDOM)+2);i++) + { + if (t->hasBuilt(BuildingID::GRAIL, ETownType::CONFLUX)) //Aurora Borealis + { + std::vector spells; + getAllowedSpells(spells, i); + for (auto & spell : spells) + cs.spells.insert(spell); + } + else + { + for(int j=0; jspellsAtLevel(i+1,true) && jspells.at(i).size(); j++) + { + if(!vstd::contains(h->spells,t->spells.at(i).at(j))) + cs.spells.insert(t->spells.at(i).at(j)); + } + } + } + if(!cs.spells.empty()) + sendAndApply(&cs); +} + +void CGameHandler::setBlockVis(ObjectInstanceID objid, bool bv) +{ + SetObjectProperty sop(objid,2,bv); + sendAndApply(&sop); +} + +bool CGameHandler::removeObject( const CGObjectInstance * obj ) +{ + if(!obj || !getObj(obj->id)) + { + logGlobal->errorStream() << "Something wrong, that object already has been removed or hasn't existed!"; + return false; + } + + RemoveObject ro; + ro.id = obj->id; + sendAndApply(&ro); + + checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) + return true; +} + +void CGameHandler::setAmount(ObjectInstanceID objid, ui32 val) +{ + SetObjectProperty sop(objid,3,val); + sendAndApply(&sop); +} + +bool CGameHandler::moveHero( ObjectInstanceID hid, int3 dst, ui8 teleporting, PlayerColor asker /*= 255*/ ) +{ + const CGHeroInstance *h = getHero(hid); + + if(!h || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer)) //not turn of that hero or player can't simply teleport hero (at least not with this function) + ) + { + logGlobal->errorStream() << "Illegal call to move hero!"; + return false; + } + + logGlobal->traceStream() << "Player " << asker << " wants to move hero "<< hid.getNum() << " from "<< h->pos << " to " << dst; + const int3 hmpos = dst + int3(-1,0,0); + + if(!gs->map->isInTheMap(hmpos)) + { + logGlobal->errorStream() << "Destination tile is outside the map!"; + return false; + } + + const TerrainTile t = *gs->getTile(hmpos); + const int cost = gs->getMovementCost(h, h->getPosition(), hmpos, h->movement); + const int3 guardPos = gs->guardingCreaturePosition(hmpos); + + const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT; + const bool disembarking = h->boat && t.terType != ETerrainType::WATER && !t.blocked; + + //result structure for start - movement failed, no move points used + TryMoveHero tmh; + tmh.id = hid; + tmh.start = h->pos; + tmh.end = dst; + tmh.result = TryMoveHero::FAILED; + tmh.movePoints = h->movement; + + //check if destination tile is available + + //it's a rock or blocked and not visitable tile + //OR hero is on land and dest is water and (there is not present only one object - boat) + if(((t.terType == ETerrainType::ROCK || (t.blocked && !t.visitable && !h->hasBonusOfType(Bonus::FLYING_MOVEMENT) )) + && complain("Cannot move hero, destination tile is blocked!")) + || ((!h->boat && !h->canWalkOnSea() && t.terType == ETerrainType::WATER && (t.visitableObjects.size() < 1 || (t.visitableObjects.back()->ID != Obj::BOAT && t.visitableObjects.back()->ID != Obj::HERO))) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) + && complain("Cannot move hero, destination tile is on water!")) + || ((h->boat && t.terType != ETerrainType::WATER && t.blocked) + && complain("Cannot disembark hero, tile is blocked!")) + || ( (distance(h->pos, dst) >= 1.5 && !teleporting) + && complain("Tiles are not neighboring!")) + || ( (h->inTownGarrison) + && complain("Can not move garrisoned hero!")) + || ((h->movement < cost && dst != h->pos && !teleporting) + && complain("Hero doesn't have any movement points left!")) + /*|| (states.checkFlag(h->tempOwner, &PlayerStatus::engagedIntoBattle) + && complain("Cannot move hero during the battle"))*/) + { + //send info about movement failure + sendAndApply(&tmh); + return false; + } + + //several generic blocks of code + + // should be called if hero changes tile but before applying TryMoveHero package + auto leaveTile = [&]() + { + for(CGObjectInstance *obj : gs->map->getTile(int3(h->pos.x-1, h->pos.y, h->pos.z)).visitableObjects) + { + obj->onHeroLeave(h); + } + this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadious(), h->tempOwner, 1); + }; + + auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards, + EVisitDest visitDest, ELEaveTile leavingTile) -> bool + { + LOG_TRACE_PARAMS(logGlobal, "Hero %s starts movement from %s to %s", h->name % tmh.start % tmh.end); + + auto moveQuery = make_shared(tmh, h); + queries.addQuery(moveQuery); + + if(leavingTile == LEAVING_TILE) + leaveTile(); + + tmh.result = result; + sendAndApply(&tmh); + + if(lookForGuards == CHECK_FOR_GUARDS && this->isInTheMap(guardPos)) + { + tmh.attackedFrom = guardPos; + + const TerrainTile &guardTile = *gs->getTile(guardPos); + objectVisited(guardTile.visitableObjects.back(), h); + + moveQuery->visitDestAfterVictory = visitDest==VISIT_DEST; + } + else if(visitDest == VISIT_DEST) + { + visitObjectOnTile(t, h); + } + + queries.popIfTop(moveQuery); + logGlobal->traceStream() << "Hero " << h->name << " ends movement"; + return result != TryMoveHero::FAILED; + }; + + //interaction with blocking object (like resources) + auto blockingVisit = [&]() -> bool + { + for(CGObjectInstance *obj : t.visitableObjects) + { + if(obj != h && obj->blockVisit && !obj->passableFor(h->tempOwner)) + { + return doMove(TryMoveHero::BLOCKING_VISIT, this->IGNORE_GUARDS, VISIT_DEST, REMAINING_ON_TILE); + //this-> is needed for MVS2010 to recognize scope (?) + } + } + return false; + }; + + + if(embarking) + { + tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, false); + return doMove(TryMoveHero::EMBARK, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE); + //attack guards on embarking? In H3 creatures on water had no zone of control at all + } + + if(disembarking) + { + tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, true); + return doMove(TryMoveHero::DISEMBARK, CHECK_FOR_GUARDS, VISIT_DEST, LEAVING_TILE); + } + + if(teleporting) + { + if(blockingVisit()) // e.g. hero on the other side of teleporter + return true; + + doMove(TryMoveHero::TELEPORTATION, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE); + + // visit town for town portal \ castle gates + // do not use generic visitObjectOnTile to avoid double-teleporting + // if this moveHero call was triggered by teleporter + if (!t.visitableObjects.empty()) + { + if (CGTownInstance * town = dynamic_cast(t.visitableObjects.back())) + town->onHeroVisit(h); + } + + return true; + } + + //still here? it is standard movement! + { + tmh.movePoints = h->movement >= cost + ? h->movement - cost + : 0; + + if(blockingVisit()) + return true; + + doMove(TryMoveHero::SUCCESS, CHECK_FOR_GUARDS, VISIT_DEST, LEAVING_TILE); + return true; + } +} + +bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker/* = 255*/) +{ + const CGHeroInstance *h = getHero(hid); + const CGTownInstance *t = getTown(dstid); + + if ( !h || !t || h->getOwner() != gs->currentPlayer ) + logGlobal->errorStream()<<"Invalid call to teleportHero!"; + + const CGTownInstance *from = h->visitedTown; + if(((h->getOwner() != t->getOwner()) + && complain("Cannot teleport hero to another player")) + || ((!from || !from->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO)) + && complain("Hero must be in town with Castle gate for teleporting")) + || (!t->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO) + && complain("Cannot teleport hero to town without Castle gate in it"))) + return false; + int3 pos = t->visitablePos(); + pos += h->getVisitableOffset(); + moveHero(hid,pos,1); + return true; +} + +void CGameHandler::setOwner(const CGObjectInstance * obj, PlayerColor owner) +{ + PlayerColor oldOwner = getOwner(obj->id); + SetObjectProperty sop(obj->id, 1, owner.getNum()); + sendAndApply(&sop); + + std::set playerColors = boost::assign::list_of(owner)(oldOwner); + checkVictoryLossConditions(playerColors); + + if(owner < PlayerColor::PLAYER_LIMIT && dynamic_cast(obj)) //town captured + { + const CGTownInstance * town = dynamic_cast(obj); + if (town->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) + setPortalDwelling(town, true, false); + + if (!gs->getPlayer(owner)->towns.size())//player lost last town + { + InfoWindow iw; + iw.player = oldOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 6); //%s, you have lost your last town. If you do not conquer another town in the next week, you will be eliminated. + sendAndApply(&iw); + } + } + + const PlayerState * p = gs->getPlayer(owner); + + if((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4 ) && p && p->dwellings.size()==1)//first dwelling captured + { + for(const CGTownInstance *t : gs->getPlayer(owner)->towns) + { + if (t->hasBuilt(BuildingID::PORTAL_OF_SUMMON, ETownType::DUNGEON)) + setPortalDwelling(t);//set initial creatures for all portals of summoning + } + } +} + +void CGameHandler::setHoverName(const CGObjectInstance * obj, MetaString* name) +{ + SetHoverName shn(obj->id, *name); + sendAndApply(&shn); +} + +void CGameHandler::showBlockingDialog( BlockingDialog *iw ) +{ + auto dialogQuery = make_shared(*iw); + queries.addQuery(dialogQuery); + iw->queryID = dialogQuery->queryID; + sendToAllClients(iw); +} + +void CGameHandler::giveResource(PlayerColor player, Res::ERes which, int val) //TODO: cap according to Bersy's suggestion +{ + if(!val) return; //don't waste time on empty call + SetResource sr; + sr.player = player; + sr.resid = which; + sr.val = gs->players.find(player)->second.resources.at(which) + val; + sendAndApply(&sr); +} + +void CGameHandler::giveResources(PlayerColor player, TResources resources) +{ + for(TResources::nziterator i(resources); i.valid(); i++) + giveResource(player, i->resType, i->resVal); +} + +void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) +{ + COMPLAIN_RET_IF(!creatures.stacksCount(), "Strange, giveCreatures called without args!"); + COMPLAIN_RET_IF(obj->stacksCount(), "Cannot give creatures from not-cleared object!"); + COMPLAIN_RET_IF(creatures.stacksCount() > GameConstants::ARMY_SIZE, "Too many stacks to give!"); + + //first we move creatures to give to make them army of object-source + for (auto & elem : creatures.Slots()) + { + addToSlot(StackLocation(obj, obj->getSlotFor(elem.second->type)), elem.second->type, elem.second->count); + } + + tryJoiningArmy(obj, h, remove, true); +} + +void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector &creatures) +{ + std::vector cres = creatures; + if (cres.size() <= 0) + return; + const CArmedInstance* obj = static_cast(getObj(objid)); + + for(CStackBasicDescriptor &sbd : cres) + { + TQuantity collected = 0; + while(collected < sbd.count) + { + bool foundSth = false; + for(auto i = obj->Slots().begin(); i != obj->Slots().end(); i++) + { + if(i->second->type == sbd.type) + { + TQuantity take = std::min(sbd.count - collected, i->second->count); //collect as much cres as we can + changeStackCount(StackLocation(obj, i->first), -take, false); + collected += take; + foundSth = true; + break; + } + } + + if(!foundSth) //we went through the whole loop and haven't found appropriate cres + { + complain("Unexpected failure during taking creatures!"); + return; + } + } + } +} + +void CGameHandler::showCompInfo(ShowInInfobox * comp) +{ + sendToAllClients(comp); +} +void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) +{ + HeroVisitCastle vc; + vc.hid = hero->id; + vc.tid = obj->id; + vc.flags |= 1; + sendAndApply(&vc); + vistiCastleObjects (obj, hero); + giveSpells (obj, hero); + + checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact? +} + +void CGameHandler::vistiCastleObjects (const CGTownInstance *t, const CGHeroInstance *h) +{ + std::vector::const_iterator i; + for (i = t->bonusingBuildings.begin(); i != t->bonusingBuildings.end(); i++) + (*i)->onHeroVisit (h); +} + +void CGameHandler::stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) +{ + HeroVisitCastle vc; + vc.hid = hero->id; + vc.tid = obj->id; + sendAndApply(&vc); +} + +void CGameHandler::removeArtifact(const ArtifactLocation &al) +{ + assert(al.getArt()); + EraseArtifact ea; + ea.al = al; + sendAndApply(&ea); +} +void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, + const CGTownInstance *town) //use hero=nullptr for no hero +{ + engageIntoBattle(army1->tempOwner); + engageIntoBattle(army2->tempOwner); + + static const CArmedInstance *armies[2]; + armies[0] = army1; + armies[1] = army2; + static const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + + + setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + + auto battleQuery = make_shared(gs->curB); + queries.addQuery(battleQuery); + + boost::thread(&CGameHandler::runBattle, this); +} + +void CGameHandler::startBattleI( const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +{ + startBattlePrimary(army1, army2, tile, + army1->ID == Obj::HERO ? static_cast(army1) : nullptr, + army2->ID == Obj::HERO ? static_cast(army2) : nullptr, + creatureBank); +} + +void CGameHandler::startBattleI( const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) +{ + startBattleI(army1, army2, army2->visitablePos(), creatureBank); +} + +void CGameHandler::changeSpells( const CGHeroInstance * hero, bool give, const std::set &spells ) +{ + ChangeSpells cs; + cs.hid = hero->id; + cs.spells = spells; + cs.learn = give; + sendAndApply(&cs); +} + +void CGameHandler::sendMessageTo( CConnection &c, const std::string &message ) +{ + SystemMessage sm; + sm.text = message; + boost::unique_lock lock(*c.wmx); + c << &sm; +} + +void CGameHandler::giveHeroBonus( GiveBonus * bonus ) +{ + sendAndApply(bonus); +} + +void CGameHandler::setMovePoints( SetMovePoints * smp ) +{ + sendAndApply(smp); +} + +void CGameHandler::setManaPoints( ObjectInstanceID hid, int val ) +{ + SetMana sm; + sm.hid = hid; + sm.val = val; + sendAndApply(&sm); +} + +void CGameHandler::giveHero( ObjectInstanceID id, PlayerColor player ) +{ + GiveHero gh; + gh.id = id; + gh.player = player; + sendAndApply(&gh); +} + +void CGameHandler::changeObjPos( ObjectInstanceID objid, int3 newPos, ui8 flags ) +{ + ChangeObjPos cop; + cop.objid = objid; + cop.nPos = newPos; + cop.flags = flags; + sendAndApply(&cop); +} + +void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID toHero) +{ + const CGHeroInstance * h1 = getHero(fromHero); + const CGHeroInstance * h2 = getHero(toHero); + + if ( h1->getSecSkillLevel(SecondarySkill::SCHOLAR) < h2->getSecSkillLevel(SecondarySkill::SCHOLAR) ) + { + std::swap (h1,h2);//1st hero need to have higher scholar level for correct message + std::swap(fromHero, toHero); + } + + int ScholarLevel = h1->getSecSkillLevel(SecondarySkill::SCHOLAR);//heroes can trade up to this level + if (!ScholarLevel || !h1->hasSpellbook() || !h2->hasSpellbook() ) + return;//no scholar skill or no spellbook + + int h1Lvl = std::min(ScholarLevel+1, h1->getSecSkillLevel(SecondarySkill::WISDOM)+2), + h2Lvl = std::min(ScholarLevel+1, h2->getSecSkillLevel(SecondarySkill::WISDOM)+2);//heroes can receive this levels + + ChangeSpells cs1; + cs1.learn = true; + cs1.hid = toHero;//giving spells to first hero + for(auto it : h1->spells) + if ( h2Lvl >= it.toSpell()->level && !vstd::contains(h2->spells, it))//hero can learn it and don't have it yet + cs1.spells.insert(it);//spell to learn + + ChangeSpells cs2; + cs2.learn = true; + cs2.hid = fromHero; + + for(auto it : h2->spells) + if ( h1Lvl >= it.toSpell()->level && !vstd::contains(h1->spells, it)) + cs2.spells.insert(it); + + if (!cs1.spells.empty() || !cs2.spells.empty())//create a message + { + InfoWindow iw; + iw.player = h1->tempOwner; + iw.components.push_back(Component(Component::SEC_SKILL, 18, ScholarLevel, 0)); + + iw.text.addTxt(MetaString::GENERAL_TXT, 139);//"%s, who has studied magic extensively, + iw.text.addReplacement(h1->name); + + if (!cs2.spells.empty())//if found new spell - apply + { + iw.text.addTxt(MetaString::GENERAL_TXT, 140);//learns + int size = cs2.spells.size(); + for(auto it : cs2.spells) + { + iw.components.push_back(Component(Component::SPELL, it, 1, 0)); + iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum()); + switch (size--) + { + case 2: iw.text.addTxt(MetaString::GENERAL_TXT, 141); + case 1: break; + default: iw.text << ", "; + } + } + iw.text.addTxt(MetaString::GENERAL_TXT, 142);//from %s + iw.text.addReplacement(h2->name); + sendAndApply(&cs2); + } + + if (!cs1.spells.empty() && !cs2.spells.empty() ) + { + iw.text.addTxt(MetaString::GENERAL_TXT, 141);//and + } + + if (!cs1.spells.empty()) + { + iw.text.addTxt(MetaString::GENERAL_TXT, 147);//teaches + int size = cs1.spells.size(); + for(auto it : cs1.spells) + { + iw.components.push_back(Component(Component::SPELL, it, 1, 0)); + iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum()); + switch (size--) + { + case 2: iw.text.addTxt(MetaString::GENERAL_TXT, 141); + case 1: break; + default: iw.text << ", "; + } } + iw.text.addTxt(MetaString::GENERAL_TXT, 148);//from %s + iw.text.addReplacement(h2->name); + sendAndApply(&cs1); + } + sendAndApply(&iw); + } +} + +void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) +{ + auto h1 = getHero(hero1), h2 = getHero(hero2); + + if( gameState()->getPlayerRelations(h1->getOwner(), h2->getOwner())) + { + auto exchange = make_shared(h1, h2); + ExchangeDialog hex; + hex.queryID = exchange->queryID; + hex.heroes[0] = getHero(hero1); + hex.heroes[1] = getHero(hero2); + sendAndApply(&hex); + useScholarSkill(hero1,hero2); + queries.addQuery(exchange); + } +} + +void CGameHandler::sendToAllClients( CPackForClient * info ) +{ + logGlobal->traceStream() << "Sending to all clients a package of type " << typeid(*info).name(); + for(auto & elem : conns) + { + boost::unique_lock lock(*(elem)->wmx); + *elem << info; + } +} + +void CGameHandler::sendAndApply(CPackForClient * info) +{ + sendToAllClients(info); + gs->apply(info); +} + +void CGameHandler::applyAndSend(CPackForClient * info) +{ + gs->apply(info); + sendToAllClients(info); +} + +void CGameHandler::sendAndApply(CGarrisonOperationPack * info) +{ + sendAndApply(static_cast(info)); + checkVictoryLossConditionsForAll(); +} + +void CGameHandler::sendAndApply( SetResource * info ) +{ + sendAndApply(static_cast(info)); + checkVictoryLossConditionsForPlayer(info->player); +} + +void CGameHandler::sendAndApply( SetResources * info ) +{ + sendAndApply(static_cast(info)); + checkVictoryLossConditionsForPlayer(info->player); +} + +void CGameHandler::sendAndApply( NewStructures * info ) +{ + sendAndApply(static_cast(info)); + checkVictoryLossConditionsForPlayer(getTown(info->tid)->tempOwner); +} + +void CGameHandler::save(const std::string & filename ) +{ + logGlobal->errorStream() << "Saving to " << filename; + CFileInfo info(filename); + //CResourceHandler::get("local")->createResource(info.getStem() + ".vlgm1"); + CResourceHandler::get("local")->createResource(info.getStem() + ".vsgm1"); + + { + logGlobal->infoStream() << "Ordering clients to serialize..."; + SaveGame sg(info.getStem() + ".vcgm1"); + sendToAllClients(&sg); + } + + try + { +// { +// logGlobal->infoStream() << "Serializing game info..."; +// CSaveFile save(CResourceHandler::get("local")->getResourceName(ResourceID(info.getStem(), EResType::LIB_SAVEGAME))); +// // char hlp[8] = "VCMISVG"; +// // save << hlp; +// saveCommonState(save); +// } + + { + CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourceID(info.getStem(), EResType::SERVER_SAVEGAME))); + saveCommonState(save); + logGlobal->infoStream() << "Saving server state"; + save << *this; + } + logGlobal->infoStream() << "Game has been successfully saved!"; + } + catch(std::exception &e) + { + logGlobal->errorStream() << "Failed to save game: " << e.what(); + } +} + +void CGameHandler::close() +{ + logGlobal->infoStream() << "We have been requested to close."; + + if(gs->initialOpts->mode == StartInfo::DUEL) + { + exit(0); + } + + //for(CConnection *cc : conns) + // if(cc && cc->socket && cc->socket->is_open()) + // cc->socket->close(); + //exit(0); +} + +bool CGameHandler::arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player ) +{ + const CArmedInstance *s1 = static_cast(gs->getObjInstance(id1)), + *s2 = static_cast(gs->getObjInstance(id2)); + const CCreatureSet &S1 = *s1, &S2 = *s2; + StackLocation sl1(s1, p1), sl2(s2, p2); + if(!sl1.slot.validSlot() || !sl2.slot.validSlot()) + { + complain("Invalid slot accessed!"); + return false; + } + + if(!isAllowedExchange(id1,id2)) + { + complain("Cannot exchange stacks between these two objects!\n"); + return false; + } + + if(what==1) //swap + { + if ( ((s1->tempOwner != player && s1->tempOwner != PlayerColor::UNFLAGGABLE) && s1->getStackCount(p1)) //why 254?? + || ((s2->tempOwner != player && s2->tempOwner != PlayerColor::UNFLAGGABLE) && s2->getStackCount(p2))) + { + complain("Can't take troops from another player!"); + return false; + } + + swapStacks(sl1, sl2); + } + else if(what==2)//merge + { + if (( s1->getCreature(p1) != s2->getCreature(p2) && complain("Cannot merge different creatures stacks!")) + || (((s1->tempOwner != player && s1->tempOwner != PlayerColor::UNFLAGGABLE) && s2->getStackCount(p2)) && complain("Can't take troops from another player!"))) + return false; + + moveStack(sl1, sl2); + } + else if(what==3) //split + { + const int countToMove = val - s2->getStackCount(p2); + const int countLeftOnSrc = s1->getStackCount(p1) - countToMove; + + if ( (s1->tempOwner != player && countLeftOnSrc < s1->getStackCount(p1) ) + || (s2->tempOwner != player && val < s2->getStackCount(p2) ) ) + { + complain("Can't move troops of another player!"); + return false; + } + + //general conditions checking + if((!vstd::contains(S1.stacks,p1) && complain("no creatures to split")) + || (val<1 && complain("no creatures to split")) ) + { + return false; + } + + + if(vstd::contains(S2.stacks,p2)) //dest. slot not free - it must be "rebalancing"... + { + int total = s1->getStackCount(p1) + s2->getStackCount(p2); + if( (total < val && complain("Cannot split that stack, not enough creatures!")) + || (s1->getCreature(p1) != s2->getCreature(p2) && complain("Cannot rebalance different creatures stacks!")) + ) + { + return false; + } + + moveStack(sl1, sl2, countToMove); + //S2.slots[p2]->count = val; + //S1.slots[p1]->count = total - val; + } + else //split one stack to the two + { + if(s1->getStackCount(p1) < val)//not enough creatures + { + complain("Cannot split that stack, not enough creatures!"); + return false; + } + + + moveStack(sl1, sl2, val); + } + + } + return true; +} + +PlayerColor CGameHandler::getPlayerAt( CConnection *c ) const +{ + std::set all; + for(auto i=connections.cbegin(); i!=connections.cend(); i++) + if(i->second == c) + all.insert(i->first); + + switch(all.size()) + { + case 0: + return PlayerColor::NEUTRAL; + case 1: + return *all.begin(); + default: + { + //if we have more than one player at this connection, try to pick active one + if(vstd::contains(all, gs->currentPlayer)) + return gs->currentPlayer; + else + return PlayerColor::CANNOT_DETERMINE; //cannot say which player is it + } + } +} + +bool CGameHandler::disbandCreature( ObjectInstanceID id, SlotID pos ) +{ + CArmedInstance *s1 = static_cast(gs->getObjInstance(id)); + if(!vstd::contains(s1->stacks,pos)) + { + complain("Illegal call to disbandCreature - no such stack in army!"); + return false; + } + + eraseStack(StackLocation(s1, pos)); + return true; +} + +bool CGameHandler::buildStructure( ObjectInstanceID tid, BuildingID requestedID, bool force /*=false*/ ) +{ + const CGTownInstance * t = getTown(tid); + if(!t) + COMPLAIN_RETF("No such town (ID=%s)!", tid); + if(!t->town->buildings.count(requestedID)) + COMPLAIN_RETF("Town of faction %s does not have info about building ID=%s!", t->town->faction->name % tid); + if (t->hasBuilt(requestedID)) + COMPLAIN_RETF("Building %s is already built in %s", t->town->buildings.at(requestedID)->Name() % t->name); + + const CBuilding * requestedBuilding = t->town->buildings.at(requestedID); + + //Vector with future list of built building and buildings in auto-mode that are not yet built. + std::vector remainingAutoBuildings; + std::set buildingsThatWillBe; + + //Check validity of request + if(!force) + { + switch (requestedBuilding->mode) + { + case CBuilding::BUILD_NORMAL : + if (gs->canBuildStructure(t, requestedID) != EBuildingState::ALLOWED) + COMPLAIN_RET("Cannot build that building!"); + break; + + case CBuilding::BUILD_AUTO : + case CBuilding::BUILD_SPECIAL: + COMPLAIN_RET("This building can not be constructed normally!"); + + case CBuilding::BUILD_GRAIL : + if(requestedBuilding->mode == CBuilding::BUILD_GRAIL) //needs grail + { + if(!t->visitingHero || !t->visitingHero->hasArt(ArtifactID::GRAIL)) + COMPLAIN_RET("Cannot build this without grail!") + else + removeArtifact(ArtifactLocation(t->visitingHero, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); + } + break; + } + } + + //Performs stuff that has to be done after new building is built + auto processBuiltStructure = [t, this](const BuildingID buildingID) + { + if(buildingID >= BuildingID::DWELL_FIRST) //dwelling + { + int level = (buildingID - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; + int upgradeNumber = (buildingID - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + + if (upgradeNumber >= t->town->creatures.at(level).size()) + { + complain(boost::str(boost::format("Error ecountered when building dwelling (bid=%s):" + "no creature found (upgrade number %d, level %d!") + % buildingID % upgradeNumber % level)); + return; + } + + CCreature * crea = VLC->creh->creatures.at(t->town->creatures.at(level).at(upgradeNumber)); + + SetAvailableCreatures ssi; + ssi.tid = t->id; + ssi.creatures = t->creatures; + if (ssi.creatures[level].second.empty()) // first creature in a dwelling + ssi.creatures[level].first = crea->growth; + ssi.creatures[level].second.push_back(crea->idNumber); + sendAndApply(&ssi); + } + if ( t->subID == ETownType::DUNGEON && buildingID == BuildingID::PORTAL_OF_SUMMON ) + { + setPortalDwelling(t); + } + + if(buildingID <= BuildingID::MAGES_GUILD_5) //it's mage guild + { + if(t->visitingHero) + giveSpells(t,t->visitingHero); + if(t->garrisonHero) + giveSpells(t,t->garrisonHero); + } + }; + + //Checks if all requirements will be met with expected building list "buildingsThatWillBe" + auto areRequirementsFullfilled = [&](const BuildingID & buildID) + { + return buildingsThatWillBe.count(buildID); + }; + + //Init the vectors + for(auto & build : t->town->buildings) + { + if(t->hasBuilt(build.first)) + buildingsThatWillBe.insert(build.first); + else + { + if(build.second->mode == CBuilding::BUILD_AUTO) //not built auto building + remainingAutoBuildings.push_back(build.second); + } + } + + //Prepare structure (list of building ids will be filled later) + NewStructures ns; + ns.tid = tid; + ns.builded = force ? t->builded : (t->builded+1); + + std::queue buildingsToAdd; + buildingsToAdd.push(requestedBuilding); + + while(!buildingsToAdd.empty()) + { + auto b = buildingsToAdd.front(); + buildingsToAdd.pop(); + + ns.bid.insert(b->bid); + buildingsThatWillBe.insert(b->bid); + remainingAutoBuildings -= b; + + for(auto autoBuilding : remainingAutoBuildings) + { + if (autoBuilding->requirements.test(areRequirementsFullfilled)) + buildingsToAdd.push(autoBuilding); + } + } + + //Other post-built events + for(auto builtID : ns.bid) + processBuiltStructure(builtID); + + //Take cost + if (!force) + { + SetResources sr; + sr.player = t->tempOwner; + sr.res = gs->getPlayer(t->tempOwner)->resources - requestedBuilding->resources; + sendAndApply(&sr); + } + + //We know what has been built, appluy changes. Do this as final step to properly update town window + sendAndApply(&ns); + + // now when everything is built - reveal tiles for lookout tower + FoWChange fw; + fw.player = t->tempOwner; + fw.mode = 1; + t->getSightTiles(fw.tiles); + sendAndApply(&fw); + + if(t->visitingHero) + vistiCastleObjects (t, t->visitingHero); + if(t->garrisonHero) + vistiCastleObjects (t, t->garrisonHero); + + checkVictoryLossConditionsForPlayer(t->tempOwner); + return true; +} +bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) +{ +///incomplete, simply erases target building + const CGTownInstance * t = getTown(tid); + if (!vstd::contains(t->builtBuildings, bid)) + return false; + RazeStructures rs; + rs.tid = tid; + rs.bid.insert(bid); + rs.destroyed = t->destroyed + 1; + sendAndApply(&rs); +//TODO: Remove dwellers +// if (t->subID == 4 && bid == 17) //Veil of Darkness +// { +// RemoveBonus rb(RemoveBonus::TOWN); +// rb.whoID = t->id; +// rb.source = Bonus::TOWN_STRUCTURE; +// rb.id = 17; +// sendAndApply(&rb); +// } + return true; +} + +void CGameHandler::sendMessageToAll( const std::string &message ) +{ + SystemMessage sm; + sm.text = message; + sendToAllClients(&sm); +} + +bool CGameHandler::recruitCreatures( ObjectInstanceID objid, CreatureID crid, ui32 cram, si32 fromLvl ) +{ + const CGDwelling *dw = static_cast(gs->getObj(objid)); + const CArmedInstance *dst = nullptr; + const CCreature *c = VLC->creh->creatures.at(crid); + bool warMachine = c->hasBonusOfType(Bonus::SIEGE_WEAPON); + + //TODO: test for owning + + if(dw->ID == Obj::TOWN) + dst = (static_cast(dw))->getUpperArmy(); + else if(dw->ID == Obj::CREATURE_GENERATOR1 || dw->ID == Obj::CREATURE_GENERATOR4 + || dw->ID == Obj::REFUGEE_CAMP) //advmap dwelling + dst = getHero(gs->getPlayer(dw->tempOwner)->currentSelection); //TODO: check if current hero is really visiting dwelling + else if(dw->ID == Obj::WAR_MACHINE_FACTORY) + dst = dynamic_cast(getTile(dw->visitablePos())->visitableObjects.back()); + + assert(dw && dst); + + //verify + bool found = false; + int level = 0; + + for(; level < dw->creatures.size(); level++) //iterate through all levels + { + if ( (fromLvl != -1) && ( level !=fromLvl ) ) + continue; + const auto &cur = dw->creatures.at(level); //current level info + int i = 0; + for(; i < cur.second.size(); i++) //look for crid among available creatures list on current level + if(cur.second.at(i) == crid) + break; + + if(i < cur.second.size()) + { + found = true; + cram = std::min(cram, cur.first); //reduce recruited amount up to available amount + break; + } + } + SlotID slot = dst->getSlotFor(crid); + + if( (!found && complain("Cannot recruit: no such creatures!")) + || (cram > VLC->creh->creatures.at(crid)->maxAmount(gs->getPlayer(dst->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) + || (cram<=0 && complain("Cannot recruit: cram <= 0!")) + || (!slot.validSlot() && !warMachine && complain("Cannot recruit: no available slot!"))) + { + return false; + } + + //recruit + SetResources sr; + sr.player = dst->tempOwner; + sr.res = gs->getPlayer(dst->tempOwner)->resources - (c->cost * cram); + + SetAvailableCreatures sac; + sac.tid = objid; + sac.creatures = dw->creatures; + sac.creatures[level].first -= cram; + + sendAndApply(&sr); + sendAndApply(&sac); + + if(warMachine) + { + const CGHeroInstance *h = dynamic_cast(dst); + if(!h) + COMPLAIN_RET("Only hero can buy war machines"); + + switch(crid) + { + case 146: + giveHeroNewArtifact(h, VLC->arth->artifacts[4], ArtifactPosition::MACH1); + break; + case 147: + giveHeroNewArtifact(h, VLC->arth->artifacts[6], ArtifactPosition::MACH3); + break; + case 148: + giveHeroNewArtifact(h, VLC->arth->artifacts[5], ArtifactPosition::MACH2); + break; + default: + complain("This war machine cannot be recruited!"); + return false; + } + } + else + { + addToSlot(StackLocation(dst, slot), c, cram); + } + return true; +} + +bool CGameHandler::upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ) +{ + CArmedInstance *obj = static_cast(gs->getObjInstance(objid)); + assert(obj->hasStackAtSlot(pos)); + UpgradeInfo ui = gs->getUpgradeInfo(obj->getStack(pos)); + PlayerColor player = obj->tempOwner; + const PlayerState *p = getPlayer(player); + int crQuantity = obj->stacks.at(pos)->count; + int newIDpos= vstd::find_pos(ui.newID, upgID);//get position of new id in UpgradeInfo + + //check if upgrade is possible + if( (ui.oldID<0 || newIDpos == -1 ) && complain("That upgrade is not possible!")) + { + return false; + } + TResources totalCost = ui.cost.at(newIDpos) * crQuantity; + + //check if player has enough resources + if(!p->resources.canAfford(totalCost)) + COMPLAIN_RET("Cannot upgrade, not enough resources!"); + + //take resources + SetResources sr; + sr.player = player; + sr.res = p->resources - totalCost; + sendAndApply(&sr); + + //upgrade creature + changeStackType(StackLocation(obj, pos), VLC->creh->creatures.at(upgID)); + return true; +} + +bool CGameHandler::changeStackType(const StackLocation &sl, CCreature *c) +{ + if(!sl.army->hasStackAtSlot(sl.slot)) + COMPLAIN_RET("Cannot find a stack to change type"); + + SetStackType sst; + sst.sl = sl; + sst.type = c; + sendAndApply(&sst); + return true; +} + +void CGameHandler::moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging) +{ + assert(src->canBeMergedWith(*dst, allowMerging)); + while(src->stacksCount())//while there are unmoved creatures + { + auto i = src->Slots().begin(); //iterator to stack to move + StackLocation sl(src, i->first); //location of stack to move + + SlotID pos = dst->getSlotFor(i->second->type); + if(!pos.validSlot()) + { + //try to merge two other stacks to make place + std::pair toMerge; + if(dst->mergableStacks(toMerge, i->first) && allowMerging) + { + moveStack(StackLocation(dst, toMerge.first), StackLocation(dst, toMerge.second)); //merge toMerge.first into toMerge.second + assert(!dst->hasStackAtSlot(toMerge.first)); //we have now a new free slot + moveStack(sl, StackLocation(dst, toMerge.first)); //move stack to freed slot + } + else + { + complain("Unexpected failure during an attempt to move army from " + src->nodeName() + " to " + dst->nodeName() + "!"); + return; + } + } + else + { + moveStack(sl, StackLocation(dst, pos)); + } + } +} + +bool CGameHandler::garrisonSwap( ObjectInstanceID tid ) +{ + CGTownInstance *town = gs->getTown(tid); + if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army + { + + if(!town->visitingHero->canBeMergedWith(*town)) + { + complain("Cannot make garrison swap, not enough free slots!"); + return false; + } + + moveArmy(town, town->visitingHero, true); + + SetHeroesInTown intown; + intown.tid = tid; + intown.visiting = ObjectInstanceID(); + intown.garrison = town->visitingHero->id; + sendAndApply(&intown); + return true; + } + else if (town->garrisonHero && !town->visitingHero) //move hero out of the garrison + { + //check if moving hero out of town will break 8 wandering heroes limit + if(getHeroCount(town->garrisonHero->tempOwner,false) >= 8) + { + complain("Cannot move hero out of the garrison, there are already 8 wandering heroes!"); + return false; + } + + SetHeroesInTown intown; + intown.tid = tid; + intown.garrison = ObjectInstanceID(); + intown.visiting = town->garrisonHero->id; + sendAndApply(&intown); + return true; + } + else if(!!town->garrisonHero && town->visitingHero) //swap visiting and garrison hero + { + SetHeroesInTown intown; + intown.tid = tid; + intown.garrison = town->visitingHero->id; + intown.visiting = town->garrisonHero->id; + sendAndApply(&intown); + return true; + } + else + { + complain("Cannot swap garrison hero!"); + return false; + } +} + +// With the amount of changes done to the function, it's more like transferArtifacts. +// Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. +bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) +{ + ArtifactLocation src = al1, dst = al2; + const PlayerColor srcPlayer = src.owningPlayer(), dstPlayer = dst.owningPlayer(); + const CArmedInstance *srcObj = src.relatedObj(), *dstObj = dst.relatedObj(); + + // Make sure exchange is even possible between the two heroes. + if(!isAllowedExchange(srcObj->id, dstObj->id)) + COMPLAIN_RET("That heroes cannot make any exchange!"); + + const CArtifactInstance *srcArtifact = src.getArt(); + const CArtifactInstance *destArtifact = dst.getArt(); + + if (srcArtifact == nullptr) + COMPLAIN_RET("No artifact to move!"); + if (destArtifact && srcPlayer != dstPlayer) + COMPLAIN_RET("Can't touch artifact on hero of another player!"); + + // Check if src/dest slots are appropriate for the artifacts exchanged. + // Moving to the backpack is always allowed. + if ((!srcArtifact || dst.slot < GameConstants::BACKPACK_START) + && srcArtifact && !srcArtifact->canBePutAt(dst, true)) + COMPLAIN_RET("Cannot move artifact!"); + + if ((srcArtifact && srcArtifact->artType->id == ArtifactID::ART_LOCK) || (destArtifact && destArtifact->artType->id == ArtifactID::ART_LOCK)) + COMPLAIN_RET("Cannot move artifact locks."); + + if (dst.slot >= GameConstants::BACKPACK_START && srcArtifact->artType->isBig()) + COMPLAIN_RET("Cannot put big artifacts in backpack!"); + if (src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) + COMPLAIN_RET("Cannot move catapult!"); + + if(dst.slot >= GameConstants::BACKPACK_START) + vstd::amin(dst.slot, ArtifactPosition(GameConstants::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size())); + + if (src.slot == dst.slot && src.artHolder == dst.artHolder) + COMPLAIN_RET("Won't move artifact: Dest same as source!"); + + if(dst.slot < GameConstants::BACKPACK_START && destArtifact) //moving art to another slot + { + //old artifact must be removed first + moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition( + dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START))); + } + + MoveArtifact ma; + ma.src = src; + ma.dst = dst; + sendAndApply(&ma); + return true; +} + +/** + * Assembles or disassembles a combination artifact. + * @param heroID ID of hero holding the artifact(s). + * @param artifactSlot The worn slot ID of the combination- or constituent artifact. + * @param assemble True for assembly operation, false for disassembly. + * @param assembleTo If assemble is true, this represents the artifact ID of the combination + * artifact to assemble to. Otherwise it's not used. + */ +bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +{ + + CGHeroInstance *hero = gs->getHero(heroID); + const CArtifactInstance *destArtifact = hero->getArt(artifactSlot); + + if(!destArtifact) + COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); + + if(assemble) + { + CArtifact *combinedArt = VLC->arth->artifacts.at(assembleTo); + if(!combinedArt->constituents) + COMPLAIN_RET("assembleArtifacts: Artifact being attempted to assemble is not a combined artifacts!"); + if(!vstd::contains(destArtifact->assemblyPossibilities(hero), combinedArt)) + COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); + + AssembledArtifact aa; + aa.al = ArtifactLocation(hero, artifactSlot); + aa.builtArt = combinedArt; + sendAndApply(&aa); + } + else + { + if(!destArtifact->artType->constituents) + COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble is not a combined artifact!"); + + DisassembledArtifact da; + da.al = ArtifactLocation(hero, artifactSlot); + sendAndApply(&da); + } + + return false; +} + +bool CGameHandler::buyArtifact( ObjectInstanceID hid, ArtifactID aid ) +{ + CGHeroInstance *hero = gs->getHero(hid); + CGTownInstance *town = hero->visitedTown; + if(aid==ArtifactID::SPELLBOOK) + { + if((!town->hasBuilt(BuildingID::MAGES_GUILD_1) && complain("Cannot buy a spellbook, no mage guild in the town!")) + || (getResource(hero->getOwner(), Res::GOLD) < GameConstants::SPELLBOOK_GOLD_COST && complain("Cannot buy a spellbook, not enough gold!") ) + || (hero->getArt(ArtifactPosition::SPELLBOOK) && complain("Cannot buy a spellbook, hero already has a one!")) + ) + return false; + + giveResource(hero->getOwner(),Res::GOLD,-GameConstants::SPELLBOOK_GOLD_COST); + giveHeroNewArtifact(hero, VLC->arth->artifacts[0], ArtifactPosition::SPELLBOOK); + assert(hero->getArt(ArtifactPosition::SPELLBOOK)); + giveSpells(town,hero); + return true; + } + else if(aid < 7 && aid > 3) //war machine + { + int price = VLC->arth->artifacts.at(aid)->price; + + if(( hero->getArt(ArtifactPosition(9+aid)) && complain("Hero already has this machine!")) + || (gs->getPlayer(hero->getOwner())->resources.at(Res::GOLD) < price && complain("Not enough gold!"))) + { + return false; + } + if ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid ) + || ((town->hasBuilt(BuildingID::BALLISTA_YARD, ETownType::STRONGHOLD)) && aid == ArtifactID::BALLISTA)) + { + giveResource(hero->getOwner(),Res::GOLD,-price); + giveHeroNewArtifact(hero, VLC->arth->artifacts.at(aid), ArtifactPosition(9+aid)); + return true; + } + else + COMPLAIN_RET("This machine is unavailable here!"); + } + return false; +} + +bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, Res::ERes rid, ArtifactID aid) +{ + if(!vstd::contains(m->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), aid)) + COMPLAIN_RET("That artifact is unavailable!"); + + int b1, b2; + m->getOffer(rid, aid, b1, b2, EMarketMode::RESOURCE_ARTIFACT); + + if(getResource(h->tempOwner, rid) < b1) + COMPLAIN_RET("You can't afford to buy this artifact!"); + + SetResource sr; + sr.player = h->tempOwner; + sr.resid = rid; + sr.val = getResource(h->tempOwner, rid) - b1; + sendAndApply(&sr); + + + SetAvailableArtifacts saa; + if(m->o->ID == Obj::TOWN) + { + saa.id = -1; + saa.arts = CGTownInstance::merchantArtifacts; + } + else if(const CGBlackMarket *bm = dynamic_cast(m->o)) //black market + { + saa.id = bm->id.getNum(); + saa.arts = bm->artifacts; + } + else + COMPLAIN_RET("Wrong marktet..."); + + bool found = false; + for(const CArtifact *&art : saa.arts) + { + if(art && art->id == aid) + { + art = nullptr; + found = true; + break; + } + } + + if(!found) + COMPLAIN_RET("Cannot find selected artifact on the list"); + + sendAndApply(&saa); + + giveHeroNewArtifact(h, VLC->arth->artifacts.at(aid), ArtifactPosition::FIRST_AVAILABLE); + return true; +} + +bool CGameHandler::sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, Res::ERes rid ) +{ + const CArtifactInstance *art = h->getArtByInstanceId(aid); + if(!art) + COMPLAIN_RET("There is no artifact to sell!"); + if(art->artType->id < 7) + COMPLAIN_RET("Cannot sell a war machine or spellbook!"); + + int resVal = 0, dump = 1; + m->getOffer(art->artType->id, rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); + + removeArtifact(ArtifactLocation(h, h->getArtPos(art))); + giveResource(h->tempOwner, rid, resVal); + return true; +} + +//void CGameHandler::lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts) +//{ +// //const CGHeroInstance * h1 = dynamic_cast source; +// //auto s = boost::apply_visitor(GetArtifactSetPtr(), source); +// { +// } +//} + +bool CGameHandler::buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill) +{ + if (!h) + COMPLAIN_RET("You need hero to buy a skill!"); + + if (h->getSecSkillLevel(SecondarySkill(skill))) + COMPLAIN_RET("Hero already know this skill"); + + if (!h->canLearnSkill()) + COMPLAIN_RET("Hero can't learn any more skills"); + + if (h->type->heroClass->secSkillProbability.at(skill)==0)//can't learn this skill (like necromancy for most of non-necros) + COMPLAIN_RET("The hero can't learn this skill!"); + + if(!vstd::contains(m->availableItemsIds(EMarketMode::RESOURCE_SKILL), skill)) + COMPLAIN_RET("That skill is unavailable!"); + + if(getResource(h->tempOwner, Res::GOLD) < 2000)//TODO: remove hardcoded resource\summ? + COMPLAIN_RET("You can't afford to buy this skill"); + + SetResource sr; + sr.player = h->tempOwner; + sr.resid = Res::GOLD; + sr.val = getResource(h->tempOwner, Res::GOLD) - 2000; + sendAndApply(&sr); + + changeSecSkill(h, skill, 1, true); + return true; +} + +bool CGameHandler::tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2) +{ + int r1 = gs->getPlayer(player)->resources.at(id1), + r2 = gs->getPlayer(player)->resources.at(id2); + + vstd::amin(val, r1); //can't trade more resources than have + + int b1, b2; //base quantities for trade + market->getOffer(id1, id2, b1, b2, EMarketMode::RESOURCE_RESOURCE); + int units = val / b1; //how many base quantities we trade + + if(val%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error + { + //TODO: complain? + assert(0); + } + + SetResource sr; + sr.player = player; + sr.resid = static_cast(id1); + sr.val = r1 - b1 * units; + sendAndApply(&sr); + + sr.resid = static_cast(id2); + sr.val = r2 + b2 * units; + sendAndApply(&sr); + + return true; +} + +bool CGameHandler::sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, Res::ERes resourceID) +{ + if(!vstd::contains(hero->Slots(), slot)) + COMPLAIN_RET("Hero doesn't have any creature in that slot!"); + + const CStackInstance &s = hero->getStack(slot); + + if( s.count < count //can't sell more creatures than have + || (hero->Slots().size() == 1 && hero->needsLastStack() && s.count == count)) //can't sell last stack + { + COMPLAIN_RET("Not enough creatures in army!"); + } + + int b1, b2; //base quantities for trade + market->getOffer(s.type->idNumber, resourceID, b1, b2, EMarketMode::CREATURE_RESOURCE); + int units = count / b1; //how many base quantities we trade + + if(count%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error + { + //TODO: complain? + assert(0); + } + + changeStackCount(StackLocation(hero, slot), -count); + + SetResource sr; + sr.player = hero->tempOwner; + sr.resid = resourceID; + sr.val = getResource(hero->tempOwner, resourceID) + b2 * units; + sendAndApply(&sr); + + return true; +} + +bool CGameHandler::transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot) +{ + const CArmedInstance *army = nullptr; + if (hero) + army = hero; + else + army = dynamic_cast(market->o); + + if (!army) + COMPLAIN_RET("Incorrect call to transform in undead!"); + if(!army->hasStackAtSlot(slot)) + COMPLAIN_RET("Army doesn't have any creature in that slot!"); + + + const CStackInstance &s = army->getStack(slot); + int resCreature;//resulting creature - bone dragons or skeletons + + if (s.hasBonusOfType(Bonus::DRAGON_NATURE)) + resCreature = 68; + else + resCreature = 56; + + changeStackType(StackLocation(army, slot), VLC->creh->creatures.at(resCreature)); + return true; +} + +bool CGameHandler::sendResources(ui32 val, PlayerColor player, Res::ERes r1, PlayerColor r2) +{ + const PlayerState *p2 = gs->getPlayer(r2, false); + if(!p2 || p2->status != EPlayerStatus::INGAME) + { + complain("Dest player must be in game!"); + return false; + } + + si32 curRes1 = gs->getPlayer(player)->resources.at(r1), + curRes2 = gs->getPlayer(r2)->resources.at(r1); + val = std::min(si32(val),curRes1); + + SetResource sr; + sr.player = player; + sr.resid = r1; + sr.val = curRes1 - val; + sendAndApply(&sr); + + sr.player = r2; + sr.val = curRes2 + val; + sendAndApply(&sr); + + return true; +} + +bool CGameHandler::setFormation( ObjectInstanceID hid, ui8 formation ) +{ + gs->getHero(hid)-> formation = formation; + return true; +} + +bool CGameHandler::hireHero(const CGObjectInstance *obj, ui8 hid, PlayerColor player) +{ + const PlayerState *p = gs->getPlayer(player); + const CGTownInstance *t = gs->getTown(obj->id); + static const int GOLD_NEEDED = 2500; + + //common preconditions + if( (p->resources.at(Res::GOLD)= GameConstants::MAX_HEROES_PER_PLAYER && complain("Cannot hire hero, only 8 wandering heroes are allowed!"))) + return false; + + if(t) //tavern in town + { + if( (!t->hasBuilt(BuildingID::TAVERN) && complain("No tavern!")) + || (t->visitingHero && complain("There is visiting hero - no place!"))) + return false; + } + else if(obj->ID == Obj::TAVERN) + { + if(getTile(obj->visitablePos())->visitableObjects.back() != obj && complain("Tavern entry must be unoccupied!")) + return false; + } + + + const CGHeroInstance *nh = p->availableHeroes.at(hid); + if (!nh) + { + complain ("Hero is not available for hiring!"); + return false; + } + + HeroRecruited hr; + hr.tid = obj->id; + hr.hid = nh->subID; + hr.player = player; + hr.tile = obj->visitablePos() + nh->getVisitableOffset(); + sendAndApply(&hr); + + + std::map > pool = gs->unusedHeroesFromPool(); + + const CGHeroInstance *theOtherHero = p->availableHeroes.at(!hid); + const CGHeroInstance *newHero = nullptr; + if (theOtherHero) //on XXL maps all heroes can be imprisoned :( + newHero = gs->hpool.pickHeroFor(false, player, getNativeTown(player), pool, gs->getRandomGenerator(), theOtherHero->type->heroClass); + + SetAvailableHeroes sah; + sah.player = player; + + if(newHero) + { + sah.hid[hid] = newHero->subID; + sah.army[hid].clear(); + sah.army[hid].setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); + } + else + sah.hid[hid] = -1; + + sah.hid[!hid] = theOtherHero ? theOtherHero->subID : -1; + sendAndApply(&sah); + + SetResource sr; + sr.player = player; + sr.resid = Res::GOLD; + sr.val = p->resources.at(Res::GOLD) - GOLD_NEEDED; + sendAndApply(&sr); + + if(t) + { + vistiCastleObjects (t, nh); + giveSpells (t,nh); + } + return true; +} + +bool CGameHandler::queryReply(QueryID qid, ui32 answer, PlayerColor player) +{ + boost::unique_lock lock(gsm); + + logGlobal->traceStream() << boost::format("Player %s attempts answering query %d with answer %d") % player % qid % answer; + + auto topQuery = queries.topQuery(player); + COMPLAIN_RET_FALSE_IF(!topQuery, "This player doesn't have any queries!"); + COMPLAIN_RET_FALSE_IF(topQuery->queryID != qid, "This player top query has different ID!"); + COMPLAIN_RET_FALSE_IF(!topQuery->endsByPlayerAnswer(), "This query cannot be ended by player's answer!"); + + if(auto dialogQuery = std::dynamic_pointer_cast(topQuery)) + dialogQuery->answer = answer; + + queries.popQuery(topQuery); + return true; +} + +static EndAction end_action; + +bool CGameHandler::makeBattleAction( BattleAction &ba ) +{ + bool ok = true; + + const CStack *stack = battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack + const CStack *destinationStack = ba.actionType == Battle::WALK_AND_ATTACK ? gs->curB->battleGetStackByPos(ba.additionalInfo) + : ba.actionType == Battle::SHOOT ? gs->curB->battleGetStackByPos(ba.destinationTile) + : nullptr; + const bool isAboutActiveStack = stack && (stack == battleActiveStack()); + + logGlobal->traceStream() << boost::format( + "Making action: type=%d; side=%d; stack=%s; dst=%s; additionalInfo=%d; stackAtDst=%s") + % ba.actionType % (int)ba.side % (stack ? stack->getName() : std::string("none")) + % ba.destinationTile % ba.additionalInfo % (destinationStack ? destinationStack->getName() : std::string("none")); + + switch(ba.actionType) + { + case Battle::WALK: //walk + case Battle::DEFEND: //defend + case Battle::WAIT: //wait + case Battle::WALK_AND_ATTACK: //walk or attack + case Battle::SHOOT: //shoot + case Battle::CATAPULT: //catapult + case Battle::STACK_HEAL: //healing with First Aid Tent + case Battle::DAEMON_SUMMONING: + case Battle::MONSTER_SPELL: + + if(!stack) + { + complain("No such stack!"); + return false; + } + if(!stack->alive()) + { + complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if(battleTacticDist()) + { + if(stack && !stack->attackerOwned != battleGetTacticsSide()) + { + complain("This is not a stack of side that has tactics!"); + return false; + } + } + else if(!isAboutActiveStack) + { + complain("Action has to be about active stack!"); + return false; + } + } + + + switch(ba.actionType) + { + case Battle::END_TACTIC_PHASE: //wait + case Battle::BAD_MORALE: + case Battle::NO_ACTION: + { + StartAction start_action(ba); + sendAndApply(&start_action); + sendAndApply(&end_action); + break; + } + case Battle::WALK: + { + StartAction start_action(ba); + sendAndApply(&start_action); //start movement + int walkedTiles = moveStack(ba.stackNumber,ba.destinationTile); //move + if(!walkedTiles) + complain("Stack failed movement!"); + + sendAndApply(&end_action); + break; + } + case Battle::DEFEND: + { + //defensive stance //TODO: remove this bonus when stack becomes active + SetStackEffect sse; + sse.effect.push_back( Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL) ); + sse.effect.push_back( Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, gs->curB->stacks.at(ba.stackNumber)->valOfBonuses(Bonus::DEFENSIVE_STANCE), + -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE)); + sse.stacks.push_back(ba.stackNumber); + sendAndApply(&sse); + + //don't break - we share code with next case + } + case Battle::WAIT: + { + StartAction start_action(ba); + sendAndApply(&start_action); + sendAndApply(&end_action); + break; + } + case Battle::RETREAT: //retreat/flee + { + if(!gs->curB->battleCanFlee(gs->curB->sides.at(ba.side).color)) + complain("Cannot retreat!"); + else + setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses + break; + } + case Battle::SURRENDER: + { + PlayerColor player = gs->curB->sides.at(ba.side).color; + int cost = gs->curB->battleGetSurrenderCost(player); + if(cost < 0) + complain("Cannot surrender!"); + else if(getResource(player, Res::GOLD) < cost) + complain("Not enough gold to surrender!"); + else + { + giveResource(player, Res::GOLD, -cost); + setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses + } + break; + } + case Battle::WALK_AND_ATTACK: //walk or attack + { + StartAction start_action(ba); + sendAndApply(&start_action); //start movement and attack + + if(!stack || !destinationStack) + { + sendAndApply(&end_action); + break; + } + + BattleHex startingPos = stack->position; + int distance = moveStack(ba.stackNumber, ba.destinationTile); + + logGlobal->traceStream() << stack->nodeName() << " will attack " << destinationStack->nodeName(); + + if(stack->position != ba.destinationTile //we wasn't able to reach destination tile + && !(stack->doubleWide() + && ( stack->position == ba.destinationTile + (stack->attackerOwned ? +1 : -1 ) ) + ) //nor occupy specified hex + ) + { + std::string problem = "We cannot move this stack to its destination " + stack->getCreature()->namePl; + logGlobal->warnStream() << problem; + complain(problem); + ok = false; + sendAndApply(&end_action); + break; + } + + if(destinationStack && stack->ID == destinationStack->ID) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + complain(boost::str(boost::format("walk and attack error: no stack at additionalInfo tile (%d)!\n") % ba.additionalInfo)); + ok = false; + sendAndApply(&end_action); + break; + } + + if( !CStack::isMeleeAttackPossible(stack, destinationStack) ) + { + complain("Attack cannot be performed!"); + sendAndApply(&end_action); + ok = false; + break; + } + + //attack + + int totalAttacks = 1 + stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK), + (Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue(); //all unspicified attacks + melee attacks + + for (int i = 0; i < totalAttacks; ++i) + { + if (stack && + stack->alive() && //move can cause death, eg. by walking into the moat + destinationStack->alive()) + { + BattleAttack bat; + prepareAttack(bat, stack, destinationStack, (i ? 0 : distance), ba.additionalInfo); //no distance travelled on second attack + //prepareAttack(bat, stack, stackAtEnd, 0, ba.additionalInfo); + handleAttackBeforeCasting(bat); //only before first attack + sendAndApply(&bat); + handleAfterAttackCasting(bat); + } + + //counterattack + if (destinationStack + && !stack->hasBonusOfType(Bonus::BLOCKS_RETALIATION) + && destinationStack->ableToRetaliate() + && stack->alive()) //attacker may have died (fire shield) + { + BattleAttack bat; + prepareAttack(bat, destinationStack, stack, 0, stack->position); + bat.flags |= BattleAttack::COUNTER; + sendAndApply(&bat); + handleAfterAttackCasting(bat); + } + } + + //return + if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE) && startingPos != stack->position && stack->alive()) + { + moveStack(ba.stackNumber, startingPos); + //NOTE: curStack->ID == ba.stackNumber (rev 1431) + } + + sendAndApply(&end_action); + break; + } + case Battle::SHOOT: + { + if( !gs->curB->battleCanShoot(stack, ba.destinationTile) ) + { + complain("Cannot shoot!"); + break; + } + + StartAction start_action(ba); + sendAndApply(&start_action); //start shooting + + { + BattleAttack bat; + bat.flags |= BattleAttack::SHOT; + prepareAttack(bat, stack, destinationStack, 0, ba.destinationTile); + handleAttackBeforeCasting(bat); + sendAndApply(&bat); + handleAfterAttackCasting(bat); + } + + //second shot for ballista, only if hero has advanced artillery + + const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); + + if( destinationStack->alive() + && (stack->getCreature()->idNumber == CreatureID::BALLISTA) + && (attackingHero->getSecSkillLevel(SecondarySkill::ARTILLERY) >= SecSkillLevel::ADVANCED) + ) + { + BattleAttack bat2; + bat2.flags |= BattleAttack::SHOT; + prepareAttack(bat2, stack, destinationStack, 0, ba.destinationTile); + sendAndApply(&bat2); + } + //allow more than one additional attack + + int additionalAttacks = stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK), + (Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT))))->totalValue(); + for (int i = 0; i < additionalAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots + ) + { + BattleAttack bat; + bat.flags |= BattleAttack::SHOT; + prepareAttack(bat, stack, destinationStack, 0, ba.destinationTile); + sendAndApply(&bat); + handleAfterAttackCasting(bat); + } + } + + sendAndApply(&end_action); + break; + } + case Battle::CATAPULT: + { + auto getCatapultHitChance = [&](EWallPart::EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int + { + switch(part) + { + case EWallPart::GATE: + return sbi.gate; + case EWallPart::KEEP: + return sbi.keep; + case EWallPart::BOTTOM_TOWER: + case EWallPart::UPPER_TOWER: + return sbi.tower; + case EWallPart::BOTTOM_WALL: + case EWallPart::BELOW_GATE: + case EWallPart::OVER_GATE: + case EWallPart::UPPER_WALL: + return sbi.wall; + default: + return 0; + } + }; + + StartAction start_action(ba); + sendAndApply(&start_action); + auto onExit = vstd::makeScopeGuard([&]{ sendAndApply(&end_action); }); //if we started than we have to finish + + const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); + CHeroHandler::SBallisticsLevelInfo sbi = VLC->heroh->ballistics.at(attackingHero->getSecSkillLevel(SecondarySkill::BALLISTICS)); + + auto wallPart = gs->curB->battleHexToWallPart(ba.destinationTile); + if(!gs->curB->isWallPartPotentiallyAttackable(wallPart)) + { + complain("catapult tried to attack non-catapultable hex!"); + break; + } + + //in successive iterations damage is dealt but not yet subtracted from wall's HPs + auto ¤tHP = gs->curB->si.wallState; + + if (currentHP.at(wallPart) == EWallState::DESTROYED || currentHP.at(wallPart) == EWallState::NONE) + { + complain("catapult tried to attack already destroyed wall part!"); + break; + } + + for(int g=0; g allowedTargets; + for (size_t i=0; i< currentHP.size(); i++) + { + if (currentHP.at(i) != EWallState::DESTROYED && + currentHP.at(i) != EWallState::NONE) + allowedTargets.push_back(EWallPart::EWallPart(i)); + } + if (allowedTargets.empty()) + break; + attackedPart = allowedTargets.at(rand()%allowedTargets.size()); + } + } + while (!hitSuccessfull); + + if (!hitSuccessfull) // break triggered - no target to shoot at + break; + + CatapultAttack ca; //package for clients + CatapultAttack::AttackInfo attack; + attack.attackedPart = attackedPart; + attack.destinationTile = ba.destinationTile; + attack.damageDealt = 0; + + int dmgChance[] = { sbi.noDmg, sbi.oneDmg, sbi.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful + + int dmgRand = rand()%100; + //accumulating dmgChance + dmgChance[1] += dmgChance[0]; + dmgChance[2] += dmgChance[1]; + //calculating dealt damage + for(int damage = 0; damage < ARRAY_COUNT(dmgChance); ++damage) + { + if(dmgRand <= dmgChance[damage]) + { + attack.damageDealt = damage; + break; + } + } + // attacked tile may have changed - update destination + attack.destinationTile = gs->curB->wallPartToBattleHex(EWallPart::EWallPart(attack.attackedPart)); + + logGlobal->traceStream() << "Catapult attacks " << (int)attack.attackedPart + << " dealing " << (int)attack.damageDealt << " damage"; + + //removing creatures in turrets / keep if one is destroyed + if(attack.damageDealt > 0 && (attackedPart == EWallPart::KEEP || + attackedPart == EWallPart::BOTTOM_TOWER || attackedPart == EWallPart::UPPER_TOWER)) + { + int posRemove = -1; + switch(attackedPart) + { + case EWallPart::KEEP: + posRemove = -2; + break; + case EWallPart::BOTTOM_TOWER: + posRemove = -3; + break; + case EWallPart::UPPER_TOWER: + posRemove = -4; + break; + } + + BattleStacksRemoved bsr; + for(auto & elem : gs->curB->stacks) + { + if(elem->position == posRemove) + { + bsr.stackIDs.insert( elem->ID ); + break; + } + } + + sendAndApply(&bsr); + } + ca.attacker = ba.stackNumber; + ca.attackedParts.push_back(attack); + + sendAndApply(&ca); + } + //finish by scope guard + break; + } + case Battle::STACK_HEAL: //healing with First Aid Tent + { + StartAction start_action(ba); + sendAndApply(&start_action); + const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); + const CStack *healer = gs->curB->battleGetStackByID(ba.stackNumber), + *destStack = gs->curB->battleGetStackByPos(ba.destinationTile); + + if(healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER)) + { + complain("There is either no healer, no destination, or healer cannot heal :P"); + } + int maxHealable = destStack->MaxHealth() - destStack->firstHPleft; + int maxiumHeal = healer->count * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID)); + + int healed = std::min(maxHealable, maxiumHeal); + + if(healed == 0) + { + //nothing to heal.. should we complain? + } + else + { + StacksHealedOrResurrected shr; + shr.lifeDrain = false; + shr.tentHealing = true; + shr.drainedFrom = ba.stackNumber; + + StacksHealedOrResurrected::HealInfo hi; + hi.healedHP = healed; + hi.lowLevelResurrection = 0; + hi.stackID = destStack->ID; + + shr.healedStacks.push_back(hi); + sendAndApply(&shr); + } + + + sendAndApply(&end_action); + break; + } + case Battle::DAEMON_SUMMONING: + //TODO: From Strategija: + //Summon Demon is a level 2 spell. + { + StartAction start_action(ba); + sendAndApply(&start_action); + + const CStack *summoner = gs->curB->battleGetStackByID(ba.stackNumber), + *destStack = gs->curB->battleGetStackByPos(ba.destinationTile, false); + + BattleStackAdded bsa; + bsa.attacker = summoner->attackerOwned; + + bsa.creID = CreatureID(summoner->getBonusLocalFirst(Selector::type(Bonus::DAEMON_SUMMONING))->subtype); //in case summoner can summon more than one type of monsters... scream! + ui64 risedHp = summoner->count * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum()); + ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount; + + ui64 canRiseHp = std::min(targetHealth, risedHp); + ui32 canRiseAmount = canRiseHp / VLC->creh->creatures.at(bsa.creID)->MaxHealth(); + + bsa.amount = std::min(canRiseAmount, destStack->baseAmount); + + bsa.pos = gs->curB->getAvaliableHex(bsa.creID, bsa.attacker, destStack->position); + bsa.summoned = false; + + if (bsa.amount) //there's rare possibility single creature cannot rise desired type + { + BattleStacksRemoved bsr; //remove body + bsr.stackIDs.insert(destStack->ID); + sendAndApply(&bsr); + sendAndApply(&bsa); + + BattleSetStackProperty ssp; + ssp.stackID = ba.stackNumber; + ssp.which = BattleSetStackProperty::CASTS; //reduce number of casts + ssp.val = -1; + ssp.absolute = false; + sendAndApply(&ssp); + } + + sendAndApply(&end_action); + break; + } + case Battle::MONSTER_SPELL: + { + StartAction start_action(ba); + sendAndApply(&start_action); + + const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); + SpellID spellID = SpellID(ba.additionalInfo); + BattleHex destination(ba.destinationTile); + + const Bonus *randSpellcaster = stack->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER)); + const Bonus * spellcaster = stack->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPELLCASTER, spellID)); + + //TODO special bonus for genies ability + if(randSpellcaster && battleGetRandomStackSpell(stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + spellID = battleGetRandomStackSpell(stack, CBattleInfoCallback::RANDOM_GENIE); + + if(spellID < 0) + complain("That stack can't cast spells!"); + else + { + int spellLvl = 0; + if (spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if (randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + vstd::amin (spellLvl, 3); + + int casterSide = gs->curB->whatSide(stack->owner); + const CGHeroInstance * secHero = gs->curB->getHero(gs->curB->theOtherPlayer(stack->owner)); + + handleSpellCasting(spellID, spellLvl, destination, casterSide, stack->owner, nullptr, secHero, 0, ECastingMode::CREATURE_ACTIVE_CASTING, stack); + } + sendAndApply(&end_action); + break; + } + } + if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished + battleMadeAction.setn(true); + return ok; +} + +void CGameHandler::playerMessage( PlayerColor player, const std::string &message ) +{ + bool cheated=true; + PlayerMessage temp_message(player, message); + + sendAndApply(&temp_message); + if(message == "vcmiistari") //give all spells and 999 mana + { + SetMana sm; + ChangeSpells cs; + + CGHeroInstance *h = gs->getHero(gs->getPlayer(player)->currentSelection); + if(!h && complain("Cannot realize cheat, no hero selected!")) return; + + sm.hid = cs.hid = h->id; + + //give all spells + cs.learn = 1; + for(auto spell : VLC->spellh->objects) + { + if(!spell->creatureAbility) + cs.spells.insert(spell->id); + } + + //give mana + sm.val = 999; + + if(!h->hasSpellbook()) //hero doesn't have spellbook + giveHeroNewArtifact(h, VLC->arth->artifacts.at(0), ArtifactPosition::SPELLBOOK); //give spellbook + + sendAndApply(&cs); + sendAndApply(&sm); + } + else if (message == "vcmiarmenelos") //build all buildings in selected town + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + CGTownInstance *town; + + if (hero) + town = hero->visitedTown; + else + town = gs->getTown(gs->getPlayer(player)->currentSelection); + + if (town) + { + for (auto & build : town->town->buildings) + { + if (!town->hasBuilt(build.first) + && !build.second->Name().empty() + && build.first != BuildingID::SHIP) + { + buildStructure(town->id, build.first, true); + } + } + } + } + else if(message == "vcmiainur") //gives 5 archangels into each slot + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + const CCreature *archangel = VLC->creh->creatures.at(13); + if(!hero) return; + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + if(!hero->hasStackAtSlot(SlotID(i))) + insertNewStack(StackLocation(hero, SlotID(i)), archangel, 5); + } + else if(message == "vcmiangband") //gives 10 black knight into each slot + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + const CCreature *blackKnight = VLC->creh->creatures.at(66); + if(!hero) return; + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + if(!hero->hasStackAtSlot(SlotID(i))) + insertNewStack(StackLocation(hero, SlotID(i)), blackKnight, 10); + } + else if(message == "vcminoldor") //all war machines + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + if(!hero) return; + + if(!hero->getArt(ArtifactPosition::MACH1)) + giveHeroNewArtifact(hero, VLC->arth->artifacts.at(4), ArtifactPosition::MACH1); + if(!hero->getArt(ArtifactPosition::MACH2)) + giveHeroNewArtifact(hero, VLC->arth->artifacts.at(5), ArtifactPosition::MACH2); + if(!hero->getArt(ArtifactPosition::MACH3)) + giveHeroNewArtifact(hero, VLC->arth->artifacts.at(6), ArtifactPosition::MACH3); + } + else if(message == "vcminahar") //1000000 movement points + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + if(!hero) return; + SetMovePoints smp; + smp.hid = hero->id; + smp.val = 1000000; + sendAndApply(&smp); + } + else if(message == "vcmiformenos") //give resources + { + SetResources sr; + sr.player = player; + sr.res = gs->getPlayer(player)->resources; + for(int i=0;imap->width * gs->map->height * (gs->map->twoLevel ? 2 : 1)]; + int lastUnc = 0; + for(int i=0;imap->width;i++) + for(int j=0;jmap->height;j++) + for(int k = 0; k < (gs->map->twoLevel ? 2 : 1); k++) + if(!gs->getPlayerTeam(fc.player)->fogOfWarMap.at(i).at(j).at(k)) + hlp_tab[lastUnc++] = int3(i,j,k); + fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); + delete [] hlp_tab; + sendAndApply(&fc); + } + else if(message == "vcmiglorfindel") //selected hero gains a new level + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level+1) - VLC->heroh->reqExp(hero->level)); + } + else if(message == "vcmisilmaril") //player wins + { + gs->getPlayer(player)->enteredWinningCheatCode = 1; + } + else if(message == "vcmimelkor") //player looses + { + gs->getPlayer(player)->enteredLosingCheatCode = 1; + } + else if (message == "vcmiforgeofnoldorking") //hero gets all artifacts except war machines, spell scrolls and spell book + { + CGHeroInstance *hero = gs->getHero(gs->getPlayer(player)->currentSelection); + if(!hero) return; + for (int g = 7; g < VLC->arth->artifacts.size(); ++g) //including artifacts from mods + giveHeroNewArtifact(hero, VLC->arth->artifacts.at(g), ArtifactPosition::PRE_FIRST); + } + else + cheated = false; + if(cheated) + { + SystemMessage temp_message(VLC->generaltexth->allTexts.at(260)); + sendAndApply(&temp_message); + checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature + } +} + +void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex destination, ui8 casterSide, PlayerColor casterColor, const CGHeroInstance * caster, const CGHeroInstance * secHero, + int usedSpellPower, ECastingMode::ECastingMode mode, const CStack * stack, si32 selectedStack) +{ + const CSpell *spell = SpellID(spellID).toSpell(); + + + //Helper local function that creates obstacle on given position. Obstacle type is inferred from spell type. + //It creates, sends and applies needed package. + auto placeObstacle = [&](BattleHex pos) + { + static int obstacleIdToGive = gs->curB->obstacles.size() + ? (gs->curB->obstacles.back()->uniqueID+1) + : 0; + + auto obstacle = make_shared(); + switch(spellID.toEnum()) // :/ + { + case SpellID::QUICKSAND: + obstacle->obstacleType = CObstacleInstance::QUICKSAND; + obstacle->turnsRemaining = -1; + obstacle->visibleForAnotherSide = false; + break; + case SpellID::LAND_MINE: + obstacle->obstacleType = CObstacleInstance::LAND_MINE; + obstacle->turnsRemaining = -1; + obstacle->visibleForAnotherSide = false; + break; + case SpellID::FIRE_WALL: + obstacle->obstacleType = CObstacleInstance::FIRE_WALL; + obstacle->turnsRemaining = 2; + obstacle->visibleForAnotherSide = true; + break; + case SpellID::FORCE_FIELD: + obstacle->obstacleType = CObstacleInstance::FORCE_FIELD; + obstacle->turnsRemaining = 2; + obstacle->visibleForAnotherSide = true; + break; + default: + //this function cannot be used with spells that do not create obstacles + assert(0); + } + + obstacle->pos = pos; + obstacle->casterSide = casterSide; + obstacle->ID = spellID; + obstacle->spellLevel = spellLvl; + obstacle->casterSpellPower = usedSpellPower; + obstacle->uniqueID = obstacleIdToGive++; + + BattleObstaclePlaced bop; + bop.obstacle = obstacle; + sendAndApply(&bop); + }; + + BattleSpellCast sc; + sc.side = casterSide; + sc.id = spellID; + sc.skill = spellLvl; + sc.tile = destination; + sc.dmgToDisplay = 0; + sc.castedByHero = (bool)caster; + sc.attackerType = (stack ? stack->type->idNumber : CreatureID(CreatureID::NONE)); + sc.manaGained = 0; + sc.spellCost = 0; + + if (caster) //calculate spell cost + { + sc.spellCost = gs->curB->battleGetSpellCost(SpellID(spellID).toSpell(), caster); + + if (secHero && mode == ECastingMode::HERO_CASTING) //handle mana channel + { + int manaChannel = 0; + for(CStack * stack : gs->curB->stacks) //TODO: shouldn't bonus system handle it somehow? + { + if (stack->owner == secHero->tempOwner) + { + vstd::amax(manaChannel, stack->valOfBonuses(Bonus::MANA_CHANNELING)); + } + } + sc.manaGained = (manaChannel * sc.spellCost) / 100; + } + } + + //calculating affected creatures for all spells + //must be vector, as in Chain Lightning order matters + std::vector attackedCres; //what is that and what is sc.afectedCres? + if (mode != ECastingMode::ENCHANTER_CASTING) + { + auto creatures = gs->curB->getAffectedCreatures(spell, spellLvl, casterColor, destination); + std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres)); + } + else //enchanter - hit all possible stacks + { + for (const CStack * stack : gs->curB->stacks) + { + /*if it's non negative spell and our unit or non positive spell and hostile unit */ + if((!spell->isNegative() && stack->owner == casterColor) + || (!spell->isPositive() && stack->owner != casterColor)) + { + if(stack->isValidTarget()) //TODO: allow dead targets somewhere in the future + { + attackedCres.push_back(stack); + } + } + } + } + + for (auto cre : attackedCres) + { + sc.affectedCres.insert (cre->ID); + } + + //checking if creatures resist + sc.resisted = gs->curB->calculateResistedStacks(spell, caster, secHero, attackedCres, casterColor, mode, usedSpellPower, spellLvl); + + //calculating dmg to display + if (spellID == SpellID::DEATH_STARE || spellID == SpellID::ACID_BREATH_DAMAGE) + { + sc.dmgToDisplay = usedSpellPower; + if (spellID == SpellID::DEATH_STARE) + vstd::amin(sc.dmgToDisplay, (*attackedCres.begin())->count); //stack is already reduced after attack + } + StacksInjured si; + + //applying effects + + if (spell->isOffensiveSpell()) + { + int spellDamage = 0; + if (stack && mode != ECastingMode::MAGIC_MIRROR) + { + int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum()); + if (unitSpellPower) + sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities + else //Faerie Dragon + { + usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100; + sc.dmgToDisplay = 0; + } + } + int chainLightningModifier = 0; + for(auto & attackedCre : attackedCres) + { + if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell + continue; + + BattleStackAttacked bsa; + if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->getLevelInfo(spellLvl).range == "X" || mode == ECastingMode::ENCHANTER_CASTING)) + //display effect only upon primary target of area spell + //FIXME: if no stack is attacked, there is no animation and interface freezes + { + bsa.flags |= BattleStackAttacked::EFFECT; + bsa.effect = spell->mainEffectAnim; + } + if (spellDamage) + bsa.damageAmount = spellDamage >> chainLightningModifier; + else + bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier; + + sc.dmgToDisplay += bsa.damageAmount; + + bsa.stackAttacked = (attackedCre)->ID; + if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast + bsa.attackerID = stack->ID; + else + bsa.attackerID = -1; + (attackedCre)->prepareAttacked(bsa, gs->getRandomGenerator()); + si.stacks.push_back(bsa); + + if (spellID == SpellID::CHAIN_LIGHTNING) + ++chainLightningModifier; + } + } + + if (spell->hasEffects()) + { + int stackSpellPower = 0; + if (stack && mode != ECastingMode::MAGIC_MIRROR) + { + stackSpellPower = stack->valOfBonuses(Bonus::CREATURE_ENCHANT_POWER); + } + SetStackEffect sse; + Bonus pseudoBonus; + pseudoBonus.sid = spellID; + pseudoBonus.val = spellLvl; + pseudoBonus.turnsRemain = gs->curB->calculateSpellDuration(spell, caster, stackSpellPower ? stackSpellPower : usedSpellPower); + CStack::stackEffectToFeature(sse.effect, pseudoBonus); + if (spellID == SpellID::SHIELD || spellID == SpellID::AIR_SHIELD) + { + sse.effect.back().val = (100 - sse.effect.back().val); //fix to original config: shield should display damage reduction + } + if (spellID == SpellID::BIND && stack)//bind + { + sse.effect.back().additionalInfo = stack->ID; //we need to know who casted Bind + } + const Bonus * bonus = nullptr; + if (caster) + bonus = caster->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, spellID)); + //TODO does hero specialty should affects his stack casting spells? + + si32 power = 0; + for(const CStack *affected : attackedCres) + { + if(vstd::contains(sc.resisted, affected->ID)) //this creature resisted the spell + continue; + sse.stacks.push_back(affected->ID); + + //Apply hero specials - peculiar enchants + const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines) + if (bonus) + { + switch(bonus->additionalInfo) + { + case 0: //normal + { + switch(tier) + { + case 1: case 2: + power = 3; + break; + case 3: case 4: + power = 2; + break; + case 5: case 6: + power = 1; + break; + } + Bonus specialBonus(sse.effect.back()); + specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely + sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); //additional premy to given effect + } + break; + case 1: //only Coronius as yet + { + power = std::max(5 - tier, 0); + Bonus specialBonus = CStack::featureGenerator(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK, power, pseudoBonus.turnsRemain); + specialBonus.sid = spellID; + sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); //additional attack to Slayer effect + } + break; + } + } + if (caster && caster->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, spellID)) //TODO: better handling of bonus percentages + { + int damagePercent = caster->level * caster->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, spellID.toEnum()) / tier; + Bonus specialBonus = CStack::featureGenerator(Bonus::CREATURE_DAMAGE, 0, damagePercent, pseudoBonus.turnsRemain); + specialBonus.valType = Bonus::PERCENT_TO_ALL; + specialBonus.sid = spellID; + sse.uniqueBonuses.push_back (std::pair (affected->ID, specialBonus)); + } + } + + if(!sse.stacks.empty()) + sendAndApply(&sse); + + } + + if (spell->isRisingSpell() || spell->id == SpellID::CURE) + { + int hpGained = 0; + if (stack) + { + int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum()); + if (unitSpellPower) + hpGained = stack->count * unitSpellPower; //Archangel + else //Faerie Dragon-like effect - unused fo far + usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100; + } + StacksHealedOrResurrected shr; + shr.lifeDrain = (ui8)false; + shr.tentHealing = (ui8)false; + for(auto & attackedCre : attackedCres) + { + if(vstd::contains(sc.resisted, (attackedCre)->ID) //this creature resisted the spell + || (spellID == SpellID::ANIMATE_DEAD && !(attackedCre)->hasBonusOfType(Bonus::UNDEAD)) //we try to cast animate dead on living stack, TODO: showuld be not affected earlier + ) + continue; + StacksHealedOrResurrected::HealInfo hi; + hi.stackID = (attackedCre)->ID; + if (stack) //casted by creature + { + if (hpGained) + { + hi.healedHP = gs->curB->calculateHealedHP(hpGained, spell, attackedCre); //archangel + } + else + hi.healedHP = gs->curB->calculateHealedHP(spell, usedSpellPower, spellLvl, attackedCre); //any typical spell (commander's cure or animate dead) + } + else + hi.healedHP = gs->curB->calculateHealedHP(caster, spell, attackedCre, gs->curB->battleGetStackByID(selectedStack)); //Casted by hero + hi.lowLevelResurrection = spellLvl <= 1; + shr.healedStacks.push_back(hi); + } + if(!shr.healedStacks.empty()) + sendAndApply(&shr); + if (spellID == SpellID::SACRIFICE) //remove victim + { + if (selectedStack == gs->curB->activeStack) + //set another active stack than the one removed, or bad things will happen + //TODO: make that part of BattleStacksRemoved? what about client update? + { + //makeStackDoNothing(gs->curB->getStack (selectedStack)); + + BattleSetActiveStack sas; + + //std::vector hlp; + //battleGetStackQueue(hlp, 1, selectedStack); //next after this one + + //if(hlp.size()) + //{ + // sas.stack = hlp[0]->ID; + //} + //else + // complain ("No new stack to activate!"); + sas.stack = gs->curB->getNextStack()->ID; //why the hell next stack has same ID as current? + sendAndApply(&sas); + + } + BattleStacksRemoved bsr; + bsr.stackIDs.insert (selectedStack); //somehow it works for teleport? + sendAndApply(&bsr); + } + } + + switch (spellID) + { + case SpellID::QUICKSAND: + case SpellID::LAND_MINE: + { + std::vector availableTiles; + for(int i = 0; i < GameConstants::BFIELD_SIZE; i += 1) + { + BattleHex hex = i; + if(hex.getX() > 2 && hex.getX() < 14 && !battleGetStackByPos(hex, false) && !battleGetObstacleOnPos(hex, false)) + availableTiles.push_back(hex); + } + boost::range::random_shuffle(availableTiles); + + const int patchesForSkill[] = {4, 4, 6, 8}; + const int patchesToPut = std::min(patchesForSkill[spellLvl], availableTiles.size()); + + //land mines or quicksand patches are handled as spell created obstacles + for (int i = 0; i < patchesToPut; i++) + placeObstacle(availableTiles.at(i)); + } + + break; + case SpellID::FORCE_FIELD: + placeObstacle(destination); + break; + case SpellID::FIRE_WALL: + { + //fire wall is build from multiple obstacles - one fire piece for each affected hex + auto affectedHexes = spell->rangeInHexes(destination, spellLvl, casterSide); + for(BattleHex hex : affectedHexes) + placeObstacle(hex); + } + break; + case SpellID::TELEPORT: + { + BattleStackMoved bsm; + bsm.distance = -1; + bsm.stack = selectedStack; + std::vector tiles; + tiles.push_back(destination); + bsm.tilesToMove = tiles; + bsm.teleporting = true; + sendAndApply(&bsm); + break; + } + case SpellID::SUMMON_FIRE_ELEMENTAL: + case SpellID::SUMMON_EARTH_ELEMENTAL: + case SpellID::SUMMON_WATER_ELEMENTAL: + case SpellID::SUMMON_AIR_ELEMENTAL: + { //elemental summoning + CreatureID creID; + switch(spellID) + { + case SpellID::SUMMON_FIRE_ELEMENTAL: + creID = CreatureID::FIRE_ELEMENTAL; + break; + case SpellID::SUMMON_EARTH_ELEMENTAL: + creID = CreatureID::EARTH_ELEMENTAL; + break; + case SpellID::SUMMON_WATER_ELEMENTAL: + creID = CreatureID::WATER_ELEMENTAL; + break; + case SpellID::SUMMON_AIR_ELEMENTAL: + creID = CreatureID::AIR_ELEMENTAL; + break; + } + + BattleStackAdded bsa; + bsa.creID = creID; + bsa.attacker = !(bool)casterSide; + bsa.summoned = true; + bsa.pos = gs->curB->getAvaliableHex(creID, !(bool)casterSide); //TODO: unify it + + //TODO stack casting -> probably power will be zero; set the proper number of creatures manually + int percentBonus = caster ? caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, spellID.toEnum()) : 0; + + bsa.amount = usedSpellPower + * SpellID(spellID).toSpell()->getPower(spellLvl) + * (100 + percentBonus) / 100.0; //new feature - percentage bonus + if(bsa.amount) + sendAndApply(&bsa); + else + complain("Summoning elementals didn't summon any!"); + } + break; + case SpellID::CLONE: + { + const CStack * clonedStack = nullptr; + if (attackedCres.size()) + clonedStack = *attackedCres.begin(); + if (!clonedStack) + { + complain ("No target stack to clone!"); + break; + } + + BattleStackAdded bsa; + bsa.creID = clonedStack->type->idNumber; + bsa.attacker = !(bool)casterSide; + bsa.summoned = true; + bsa.pos = gs->curB->getAvaliableHex(bsa.creID, !(bool)casterSide); //TODO: unify it + bsa.amount = clonedStack->count; + sendAndApply (&bsa); + + BattleSetStackProperty ssp; + ssp.stackID = gs->curB->stacks.back()->ID; //how to get recent stack? + ssp.which = BattleSetStackProperty::CLONED; //using enum values + ssp.val = 0; + ssp.absolute = 1; + sendAndApply(&ssp); + } + break; + case SpellID::REMOVE_OBSTACLE: + { + if(auto obstacleToRemove = battleGetObstacleOnPos(destination, false)) + { + ObstaclesRemoved obr; + obr.obstacles.insert(obstacleToRemove->uniqueID); + sendAndApply(&obr); + } + else + complain("There's no obstacle to remove!"); + } + break; + case SpellID::DEATH_STARE: //handled in a bit different way + { + for(auto & attackedCre : attackedCres) + { + if((attackedCre)->hasBonusOfType(Bonus::UNDEAD) || (attackedCre)->hasBonusOfType(Bonus::NON_LIVING)) //this creature is immune + { + sc.dmgToDisplay = 0; //TODO: handle Death Stare for multiple targets (?) + continue; + } + + BattleStackAttacked bsa; + bsa.flags |= BattleStackAttacked::EFFECT; + bsa.effect = spell->mainEffectAnim; //from config\spell-Info.txt + bsa.damageAmount = usedSpellPower * (attackedCre)->valOfBonuses(Bonus::STACK_HEALTH); + bsa.stackAttacked = (attackedCre)->ID; + bsa.attackerID = -1; + (attackedCre)->prepareAttacked(bsa, gameState()->getRandomGenerator()); + si.stacks.push_back(bsa); + } + } + break; + case SpellID::ACID_BREATH_DAMAGE: //new effect, separate from acid breath defense reduction + { + for(auto & attackedCre : attackedCres) //no immunities + { + BattleStackAttacked bsa; + bsa.flags |= BattleStackAttacked::EFFECT; + bsa.effect = spell->mainEffectAnim; + bsa.damageAmount = usedSpellPower; //damage times the number of attackers + bsa.stackAttacked = (attackedCre)->ID; + bsa.attackerID = -1; + (attackedCre)->prepareAttacked(bsa, gameState()->getRandomGenerator()); + si.stacks.push_back(bsa); + } + } + break; + } + + sendAndApply(&sc); + if(!si.stacks.empty()) //after spellcast info shows + sendAndApply(&si); + + if (mode == ECastingMode::CREATURE_ACTIVE_CASTING || mode == ECastingMode::ENCHANTER_CASTING) //reduce number of casts remaining + { + BattleSetStackProperty ssp; + ssp.stackID = stack->ID; + ssp.which = BattleSetStackProperty::CASTS; + ssp.val = -1; + ssp.absolute = false; + sendAndApply(&ssp); + } + + //Magic Mirror effect + if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence + { + for(auto & attackedCre : attackedCres) + { + int mirrorChance = (attackedCre)->valOfBonuses(Bonus::MAGIC_MIRROR); + if(mirrorChance > rand()%100) + { + std::vector mirrorTargets; + std::vector & battleStacks = gs->curB->stacks; + for (auto & battleStack : battleStacks) + { + if(battleStack->owner == gs->curB->sides.at(casterSide).color) //get enemy stacks which can be affected by this spell + { + if (!gs->curB->battleIsImmune(nullptr, spell, ECastingMode::MAGIC_MIRROR, battleStack->position)) + mirrorTargets.push_back(battleStack); + } + } + if (!mirrorTargets.empty()) + { + int targetHex = mirrorTargets.at(rand() % mirrorTargets.size())->position; + handleSpellCasting(spellID, 0, targetHex, 1 - casterSide, (attackedCre)->owner, nullptr, (caster ? caster : nullptr), usedSpellPower, ECastingMode::MAGIC_MIRROR, (attackedCre)); + } + } + } + } +} + +bool CGameHandler::makeCustomAction( BattleAction &ba ) +{ + switch(ba.actionType) + { + case Battle::HERO_SPELL: + { + COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); + + + const CGHeroInstance *h = gs->curB->battleGetFightingHero(ba.side); + const CGHeroInstance *secondHero = gs->curB->battleGetFightingHero(!ba.side); + if(!h) + { + logGlobal->warnStream() << "Wrong caster!"; + return false; + } + if(ba.additionalInfo >= VLC->spellh->objects.size()) + { + logGlobal->warnStream() << "Wrong spell id (" << ba.additionalInfo << ")!"; + return false; + } + + const CSpell *s = SpellID(ba.additionalInfo).toSpell(); + if (s->mainEffectAnim > -1 + || s->id == SpellID::CLONE + || s->id >= SpellID::SUMMON_FIRE_ELEMENTAL + || s->id <= SpellID::SUMMON_AIR_ELEMENTAL + || s->id <= SpellID::SUMMON_EARTH_ELEMENTAL + || s->id <= SpellID::SUMMON_WATER_ELEMENTAL) + //TODO: special effects, like Clone + { + ui8 skill = h->getSpellSchoolLevel(s); //skill level + + ESpellCastProblem::ESpellCastProblem escp = gs->curB->battleCanCastThisSpell(h->tempOwner, s, ECastingMode::HERO_CASTING); + if(escp != ESpellCastProblem::OK) + { + logGlobal->warnStream() << "Spell cannot be cast!"; + logGlobal->warnStream() << "Problem : " << escp; + return false; + } + + StartAction start_action(ba); + sendAndApply(&start_action); //start spell casting + + handleSpellCasting (SpellID(ba.additionalInfo), skill, ba.destinationTile, ba.side, h->tempOwner, + h, secondHero, h->getPrimSkillLevel(PrimarySkill::SPELL_POWER), + ECastingMode::HERO_CASTING, nullptr, ba.selectedStack); + + sendAndApply(&end_action); + if( !gs->curB->battleGetStackByID(gs->curB->activeStack, true)) + { + battleMadeAction.setn(true); + } + checkForBattleEnd(); + if(battleResult.get()) + { + battleMadeAction.setn(true); + //battle will be ended by startBattle function + //endBattle(gs->curB->tile, gs->curB->heroes[0], gs->curB->heroes[1]); + } + + return true; + } + else + { + logGlobal->warnStream() << "Spell " << s->name << " is not yet supported!"; + return false; + } + } + } + return false; +} + +void CGameHandler::stackTurnTrigger(const CStack * st) +{ + BattleTriggerEffect bte; + bte.stackID = st->ID; + bte.effect = -1; + bte.val = 0; + bte.additionalInfo = 0; + if (st->alive()) + { + //unbind + if (st->getEffect (SpellID::BIND)) + { + bool unbind = true; + BonusList bl = *(st->getBonuses(Selector::type(Bonus::BIND_EFFECT))); + std::set stacks = gs->curB-> batteAdjacentCreatures(st); + + for(Bonus * b : bl) + { + const CStack * stack = gs->curB->battleGetStackByID(b->additionalInfo); //binding stack must be alive and adjacent + if (stack) + { + if (vstd::contains(stacks, stack)) //binding stack is still present + { + unbind = false; + } + } + } + if (unbind) + { + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::UNBIND; + ssp.stackID = st->ID; + sendAndApply(&ssp); + } + } + //regeneration + if(st->hasBonusOfType(Bonus::HP_REGENERATION)) + { + bte.effect = Bonus::HP_REGENERATION; + bte.val = std::min((int)(st->MaxHealth() - st->firstHPleft), st->valOfBonuses(Bonus::HP_REGENERATION)); + } + if(st->hasBonusOfType(Bonus::FULL_HP_REGENERATION)) + { + bte.effect = Bonus::HP_REGENERATION; + bte.val = st->MaxHealth() - st->firstHPleft; + } + if (bte.val) //anything to heal + sendAndApply(&bte); + + if(st->hasBonusOfType(Bonus::POISON)) + { + const Bonus * b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON).And(Selector::type(Bonus::STACK_HEALTH))); + if (b) //TODO: what if not?... + { + bte.val = std::max (b->val - 10, -(st->valOfBonuses(Bonus::POISON))); + if (bte.val < b->val) //(negative) poison effect increases - update it + { + bte.effect = Bonus::POISON; + sendAndApply(&bte); + } + } + } + if (st->hasBonusOfType(Bonus::MANA_DRAIN) && !vstd::contains(st->state, EBattleStackState::DRAINED_MANA)) + { + const CGHeroInstance * enemy = gs->curB->getHero(gs->curB->theOtherPlayer(st->owner)); + //const CGHeroInstance * owner = gs->curB->getHero(st->owner); + if (enemy) + { + ui32 manaDrained = st->valOfBonuses(Bonus::MANA_DRAIN); + vstd::amin(manaDrained, gs->curB->battleGetFightingHero(0)->mana); + if (manaDrained) + { + bte.effect = Bonus::MANA_DRAIN; + bte.val = manaDrained; + bte.additionalInfo = enemy->id.getNum(); //for sanity + sendAndApply(&bte); + } + } + } + if (st->isLiving() && !st->hasBonusOfType(Bonus::FEARLESS)) + { + bool fearsomeCreature = false; + for(CStack * stack : gs->curB->stacks) + { + if (stack->owner != st->owner && stack->alive() && stack->hasBonusOfType(Bonus::FEAR)) + { + fearsomeCreature = true; + break; + } + } + if (fearsomeCreature) + { + if (rand() % 100 < 10) //fixed 10% + { + bte.effect = Bonus::FEAR; + sendAndApply(&bte); + } + } + } + BonusList bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTER))); + int side = gs->curB->whatSide(st->owner); + if (bl.size() && st->casts && !gs->curB->sides.at(side).enchanterCounter) + { + int index = rand() % bl.size(); + SpellID spellID = SpellID(bl[index]->subtype); + if (gs->curB->battleCanCastThisSpell(st->owner, SpellID(spellID).toSpell(), ECastingMode::ENCHANTER_CASTING) == ESpellCastProblem::OK) //TODO: select another available? + { + int spellLeveL = bl[index]->val; //spell level + const CGHeroInstance * enemyHero = gs->curB->getHero(gs->curB->theOtherPlayer(st->owner)); + handleSpellCasting(spellID, spellLeveL, -1, side, st->owner, nullptr, enemyHero, 0, ECastingMode::ENCHANTER_CASTING, st); + + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; + ssp.absolute = false; + ssp.val = bl[index]->additionalInfo; //increase cooldown counter + ssp.stackID = st->ID; + sendAndApply(&ssp); + } + } + bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTED))); + for (auto b : bl) + { + SetStackEffect sse; + int val = bl.valOfBonuses (Selector::typeSubtype(b->type, b->subtype)); + if (val > 3) + { + for (auto s : gs->curB->battleGetAllStacks()) + { + if (st->owner == s->owner && s->isValidTarget()) //all allied + sse.stacks.push_back (s->ID); + } + } + else + sse.stacks.push_back (st->ID); + + Bonus pseudoBonus; + pseudoBonus.sid = b->subtype; + pseudoBonus.val = ((val > 3) ? (val - 3) : val); + pseudoBonus.turnsRemain = 50; + st->stackEffectToFeature (sse.effect, pseudoBonus); + if (sse.effect.size()) + sendAndApply (&sse); + } + } +} + +void CGameHandler::handleDamageFromObstacle(const CObstacleInstance &obstacle, const CStack * curStack) +{ + //we want to determine following vars depending on obstacle type + int damage = -1; + int effect = -1; + bool oneTimeObstacle = false; + + //helper info + const SpellCreatedObstacle *spellObstacle = dynamic_cast(&obstacle); //not nice but we may need spell params + const ui8 side = !curStack->attackerOwned; //if enemy is defending (false = 0), side of enemy hero is 1 (true) + const CGHeroInstance *hero = gs->curB->battleGetFightingHero(side); + + if(obstacle.obstacleType == CObstacleInstance::MOAT) + { + damage = battleGetMoatDmg(); + } + else if(obstacle.obstacleType == CObstacleInstance::LAND_MINE) + { + //You don't get hit by a Mine you can see. + if(gs->curB->battleIsObstacleVisibleForSide(obstacle, (BattlePerspective::BattlePerspective)side)) + return; + + oneTimeObstacle = true; + effect = 82; //makes + damage = gs->curB->calculateSpellDmg(SpellID(SpellID::LAND_MINE).toSpell(), hero, curStack, + spellObstacle->spellLevel, spellObstacle->casterSpellPower); + //TODO even if obstacle wasn't created by hero (Tower "moat") it should deal dmg as if casted by hero, + //if it is bigger than default dmg. Or is it just irrelevant H3 implementation quirk + } + else if(obstacle.obstacleType == CObstacleInstance::FIRE_WALL) + { + damage = gs->curB->calculateSpellDmg(SpellID(SpellID::FIRE_WALL).toSpell(), hero, curStack, + spellObstacle->spellLevel, spellObstacle->casterSpellPower); + } + else + { + //no other obstacle does damage to stack + return; + } + + BattleStackAttacked bsa; + if(effect >= 0) + { + bsa.flags |= BattleStackAttacked::EFFECT; + bsa.effect = effect; //makes POOF + } + bsa.damageAmount = damage; + bsa.stackAttacked = curStack->ID; + bsa.attackerID = -1; + curStack->prepareAttacked(bsa, gameState()->getRandomGenerator()); + + StacksInjured si; + si.stacks.push_back(bsa); + sendAndApply(&si); + + if(oneTimeObstacle) + removeObstacle(obstacle); +} + +void CGameHandler::handleTimeEvents() +{ + gs->map->events.sort(evntCmp); + while(gs->map->events.size() && gs->map->events.front().firstOccurence+1 == gs->day) + { + CMapEvent ev = gs->map->events.front(); + for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) + { + PlayerState *pinfo = gs->getPlayer(PlayerColor(player)); + + if( pinfo //player exists + && (ev.players & 1<human) + || (ev.humanAffected && pinfo->human) + ) + ) + { + //give resources + SetResources sr; + sr.player = PlayerColor(player); + sr.res = pinfo->resources + ev.resources; + + //prepare dialog + InfoWindow iw; + iw.player = PlayerColor(player); + iw.text << ev.message; + + for (int i=0; imap->events.pop_front(); + + ev.firstOccurence += ev.nextOccurence; + auto it = gs->map->events.begin(); + while ( it !=gs->map->events.end() && it->earlierThanOrEqual(ev)) + it++; + gs->map->events.insert(it, ev); + } + else + { + gs->map->events.pop_front(); + } + } + + //TODO send only if changed + UpdateMapEvents ume; + ume.events = gs->map->events; + sendAndApply(&ume); +} + +void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) +{ + town->events.sort(evntCmp); + while(town->events.size() && town->events.front().firstOccurence == gs->day) + { + PlayerColor player = town->tempOwner; + CCastleEvent ev = town->events.front(); + PlayerState *pinfo = gs->getPlayer(player); + + if( pinfo //player exists + && (ev.players & 1<human) + || (ev.humanAffected && pinfo->human) ) ) + { + + + // dialog + InfoWindow iw; + iw.player = player; + iw.text << ev.message; + + if(ev.resources.nonZero()) + { + TResources was = n.res[player]; + n.res[player] += ev.resources; + n.res[player].amax(0); + + for (int i=0; iresources.at(i) != n.res.at(player).at(i)) //if resource had changed, we add it to the dialog + iw.components.push_back(Component(Component::RESOURCE,i,n.res.at(player).at(i)-was.at(i),0)); + + } + + for(auto & i : ev.buildings) + { + if ( town->hasBuilt(i)) + { + buildStructure(town->id, i, true); + iw.components.push_back(Component(Component::BUILDING, town->subID, i, 0)); + } + } + + if (!ev.creatures.empty() && !vstd::contains(n.cres, town->id)) + { + n.cres[town->id].tid = town->id; + n.cres[town->id].creatures = town->creatures; + } + auto & sac = n.cres[town->id]; + + for(si32 i=0;icreatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling + { + sac.creatures[i].first += ev.creatures.at(i); + iw.components.push_back(Component(Component::CREATURE, + town->creatures.at(i).second.back(), ev.creatures.at(i), 0)); + } + } + sendAndApply(&iw); //show dialog + } + + if(ev.nextOccurence) + { + town->events.pop_front(); + + ev.firstOccurence += ev.nextOccurence; + auto it = town->events.begin(); + while ( it != town->events.end() && it->earlierThanOrEqual(ev)) + it++; + town->events.insert(it, ev); + } + else + { + town->events.pop_front(); + } + } + + //TODO send only if changed + UpdateCastleEvents uce; + uce.town = town->id; + uce.events = town->events; + sendAndApply(&uce); +} + +bool CGameHandler::complain( const std::string &problem ) +{ + sendMessageToAll("Server encountered a problem: " + problem); + logGlobal->errorStream() << problem; + return true; +} + +void CGameHandler::showGarrisonDialog( ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) +{ + //PlayerColor player = getOwner(hid); + auto upperArmy = dynamic_cast(getObj(upobj)); + auto lowerArmy = dynamic_cast(getObj(hid)); + + assert(lowerArmy); + assert(upperArmy); + + auto garrisonQuery = make_shared(upperArmy, lowerArmy); + queries.addQuery(garrisonQuery); + + GarrisonDialog gd; + gd.hid = hid; + gd.objid = upobj; + gd.removableUnits = removableUnits; + gd.queryID = garrisonQuery->queryID; + sendAndApply(&gd); +} + +void CGameHandler::showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) +{ + OpenWindow ow; + ow.window = OpenWindow::THIEVES_GUILD; + ow.id1 = player.getNum(); + ow.id2 = requestingObjId.getNum(); + sendAndApply(&ow); +} + +bool CGameHandler::isAllowedExchange( ObjectInstanceID id1, ObjectInstanceID id2 ) +{ + if(id1 == id2) + return true; + + const CGObjectInstance *o1 = getObj(id1), *o2 = getObj(id2); + if(!o1 || !o2) + return true; //arranging stacks within an object should be always allowed + + if (o1 && o2) + { + if(o1->ID == Obj::TOWN) + { + const CGTownInstance *t = static_cast(o1); + if(t->visitingHero == o2 || t->garrisonHero == o2) + return true; + } + if(o2->ID == Obj::TOWN) + { + const CGTownInstance *t = static_cast(o2); + if(t->visitingHero == o1 || t->garrisonHero == o1) + return true; + } + + if (o1->ID == Obj::HERO && o2->ID == Obj::HERO) + { + const CGHeroInstance *h1 = static_cast(o1); + const CGHeroInstance *h2 = static_cast(o2); + + // two heroes in same town (garrisoned and visiting) + if (h1->visitedTown != nullptr && h2->visitedTown != nullptr && h1->visitedTown == h2->visitedTown) + return true; + } + + //Ongoing garrison exchange + if(auto dialog = std::dynamic_pointer_cast(queries.topQuery(o1->tempOwner))) + { + if(dialog->exchangingArmies.at(0) == o1 && dialog->exchangingArmies.at(1) == o2) + return true; + + if(dialog->exchangingArmies.at(1) == o1 && dialog->exchangingArmies.at(0) == o2) + return true; + } + } + + return false; +} + +void CGameHandler::objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ) +{ + logGlobal->traceStream() << h->nodeName() << " visits " << obj->getHoverText(); + auto visitQuery = make_shared(obj, h, obj->visitablePos()); + queries.addQuery(visitQuery); //TODO real visit pos + + HeroVisit hv; + hv.obj = obj; + hv.hero = h; + hv.player = h->tempOwner; + hv.starting = true; + sendAndApply(&hv); + + obj->onHeroVisit(h); + + queries.popIfTop(visitQuery); //visit ends here if no queries were created +} + +void CGameHandler::objectVisitEnded(const CObjectVisitQuery &query) +{ + logGlobal->traceStream() << query.visitingHero->nodeName() << " visit ends.\n"; + + HeroVisit hv; + hv.player = query.players.front(); + hv.obj = nullptr; //not necessary, moreover may have been deleted in the meantime + hv.hero = query.visitingHero; + assert(hv.hero); + hv.starting = false; + sendAndApply(&hv); +} + +bool CGameHandler::buildBoat( ObjectInstanceID objid ) +{ + const IShipyard *obj = IShipyard::castFrom(getObj(objid)); + + if(obj->shipyardStatus() != IBoatGenerator::GOOD) + { + complain("Cannot build boat in this shipyard!"); + return false; + } + else if(obj->o->ID == Obj::TOWN + && !static_cast(obj)->hasBuilt(BuildingID::SHIPYARD)) + { + complain("Cannot build boat in the town - no shipyard!"); + return false; + } + + const PlayerColor playerID = obj->o->tempOwner; + TResources boatCost; + obj->getBoatCost(boatCost); + TResources aviable = gs->getPlayer(playerID)->resources; + + if (!aviable.canAfford(boatCost)) + { + complain("Not enough resources to build a boat!"); + return false; + } + + int3 tile = obj->bestLocation(); + if(!gs->map->isInTheMap(tile)) + { + complain("Cannot find appropriate tile for a boat!"); + return false; + } + + //take boat cost + SetResources sr; + sr.player = playerID; + sr.res = (aviable - boatCost); + sendAndApply(&sr); + + //create boat + NewObject no; + no.ID = Obj::BOAT; + no.subID = obj->getBoatType(); + no.pos = tile + int3(1,0,0); + sendAndApply(&no); + + return true; +} + +void CGameHandler::engageIntoBattle( PlayerColor player ) +{ + //notify interfaces + PlayerBlocked pb; + pb.player = player; + pb.reason = PlayerBlocked::UPCOMING_BATTLE; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + sendAndApply(&pb); +} + +void CGameHandler::checkVictoryLossConditions(const std::set & playerColors) +{ + for(auto playerColor : playerColors) + { + if(gs->getPlayer(playerColor)) + checkVictoryLossConditionsForPlayer(playerColor); + } +} + +void CGameHandler::checkVictoryLossConditionsForAll() +{ + std::set playerColors; + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + playerColors.insert(PlayerColor(i)); + } + checkVictoryLossConditions(playerColors); +} + +void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) +{ + const PlayerState *p = gs->getPlayer(player); + if(p->status != EPlayerStatus::INGAME) return; + + auto victoryLossCheckResult = gs->checkForVictoryAndLoss(player); + + if(victoryLossCheckResult.victory() || victoryLossCheckResult.loss()) + { + InfoWindow iw; + getVictoryLossMessage(player, victoryLossCheckResult, iw); + sendAndApply(&iw); + + PlayerEndsGame peg; + peg.player = player; + peg.victoryLossCheckResult = victoryLossCheckResult; + sendAndApply(&peg); + + if(victoryLossCheckResult.victory()) + { + //one player won -> all enemies lost + for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++) + { + if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME) + { + peg.player = i->first; + peg.victoryLossCheckResult = gameState()->getPlayerRelations(player, i->first) == PlayerRelations::ALLIES ? + victoryLossCheckResult : victoryLossCheckResult.invert(); // ally of winner + + InfoWindow iw; + getVictoryLossMessage(player, peg.victoryLossCheckResult, iw); + iw.player = i->first; + + sendAndApply(&iw); + sendAndApply(&peg); + } + } + + if(p->human) + { + end2 = true; + + if(gs->scenarioOps->campState) + { + std::vector crossoverHeroes; + for(CGHeroInstance * hero : gs->map->heroesOnMap) + { + if(hero->tempOwner == player) + { + // keep all heroes from the winning player + crossoverHeroes.push_back(hero); + } + else if(vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(hero->subID))) + { + // keep hero whether lost or won (like Xeron in AB campaign) + crossoverHeroes.push_back(hero); + } + } + // keep lost heroes which are in heroes pool + for(auto & heroPair : gs->hpool.heroesPool) + { + if(vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(heroPair.first))) + { + crossoverHeroes.push_back(heroPair.second.get()); + } + } + + gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); + + //Request clients to change connection mode + PrepareForAdvancingCampaign pfac; + sendAndApply(&pfac); + //Change connection mode + if(getPlayer(player)->human && getStartInfo()->campState) + { + for(auto connection : conns) + connection->prepareForSendingHeroes(); + } + + UpdateCampaignState ucs; + ucs.camp = gs->scenarioOps->campState; + sendAndApply(&ucs); + } + } + } + else + { + //player lost -> all his objects become unflagged (neutral) + auto hlp = p->heroes; + for (auto i = hlp.cbegin(); i != hlp.cend(); i++) //eliminate heroes + removeObject(*i); + + for (auto i = gs->map->objects.cbegin(); i != gs->map->objects.cend(); i++) //unflag objs + { + if(*i && (*i)->tempOwner == player) + setOwner(*i,PlayerColor::NEUTRAL); + } + + //eliminating one player may cause victory of another: + std::set playerColors; + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + if(player.getNum() != i) playerColors.insert(PlayerColor(i)); + } + + //notify all players + for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++) + { + if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME) + { + InfoWindow iw; + getVictoryLossMessage(player, victoryLossCheckResult.invert(), iw); + iw.player = i->first; + sendAndApply(&iw); + } + } + + + checkVictoryLossConditions(playerColors); + } + + auto playerInfo = gs->getPlayer(gs->currentPlayer, false); + // If we are called before the actual game start, there might be no current player + if(playerInfo && playerInfo->status != EPlayerStatus::INGAME) + { + // If player making turn has lost his turn must be over as well + states.setFlag(gs->currentPlayer, &PlayerStatus::makingTurn, false); + } + } +} + +void CGameHandler::getVictoryLossMessage(PlayerColor player, EVictoryLossCheckResult victoryLossCheckResult, InfoWindow & out) const +{ + out.player = player; + out.text.clear(); + out.text << victoryLossCheckResult.messageToSelf; + // hackish, insert one player-specific string, if applicable + if (victoryLossCheckResult.messageToSelf.find("%s") != std::string::npos) + out.text.addReplacement(MetaString::COLOR, player.getNum()); + + out.components.push_back(Component(Component::FLAG, player.getNum(), 0, 0)); +} + +bool CGameHandler::dig( const CGHeroInstance *h ) +{ + for (auto i = gs->map->objects.cbegin(); i != gs->map->objects.cend(); i++) //unflag objs + { + if(*i && (*i)->ID == Obj::HOLE && (*i)->pos == h->getPosition()) + { + complain("Cannot dig - there is already a hole under the hero!"); + return false; + } + } + + if(h->diggingStatus() != CGHeroInstance::CAN_DIG) //checks for terrain and movement + COMPLAIN_RETF("Hero cannot dig (error code %d)!", h->diggingStatus()); + + //create a hole + NewObject no; + no.ID = Obj::HOLE; + no.pos = h->getPosition(); + no.subID = 0; + sendAndApply(&no); + + //take MPs + SetMovePoints smp; + smp.hid = h->id; + smp.val = 0; + sendAndApply(&smp); + + InfoWindow iw; + iw.player = h->tempOwner; + if(gs->map->grailPos == h->getPosition()) + { + iw.text.addTxt(MetaString::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " + iw.text.addTxt(MetaString::ART_NAMES, 2); + iw.soundID = soundBase::ULTIMATEARTIFACT; + giveHeroNewArtifact(h, VLC->arth->artifacts.at(2), ArtifactPosition::PRE_FIRST); //give grail + sendAndApply(&iw); + + iw.soundID = soundBase::invalid; + iw.text.clear(); + iw.text.addTxt(MetaString::ART_DESCR, 2); + sendAndApply(&iw); + } + else + { + iw.text.addTxt(MetaString::GENERAL_TXT, 59); //"Nothing here. \n Where could it be?" + iw.soundID = soundBase::Dig; + sendAndApply(&iw); + } + + return true; +} + +void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType attackMode, const CStack * attacker) +{ + if(attacker->hasBonusOfType(attackMode)) + { + std::set spellsToCast; + TBonusListPtr spells = attacker->getBonuses(Selector::type(attackMode)); + for(const Bonus *sf : *spells) + { + spellsToCast.insert (SpellID(sf->subtype)); + } + for(SpellID spellID : spellsToCast) + { + const CStack * oneOfAttacked = nullptr; + for (auto & elem : bat.bsa) + { + if (elem.newAmount > 0 && !elem.isSecondary()) //apply effects only to first target stack if it's alive + { + oneOfAttacked = gs->curB->battleGetStackByID(elem.stackAttacked); + break; + } + } + bool castMe = false; + if(oneOfAttacked == nullptr) //all attacked creatures have been killed + return; + int spellLevel = 0; + TBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); + for(const Bonus *sf : *spellsByType) + { + vstd::amax(spellLevel, sf->additionalInfo % 1000); //pick highest level + int meleeRanged = sf->additionalInfo / 1000; + if (meleeRanged == 0 || (meleeRanged == 1 && bat.shot()) || (meleeRanged == 2 && !bat.shot())) + castMe = true; + } + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); + vstd::amin (chance, 100); + int destination = oneOfAttacked->position; + + const CSpell * spell = SpellID(spellID).toSpell(); + if(gs->curB->battleCanCastThisSpellHere(attacker->owner, spell, ECastingMode::AFTER_ATTACK_CASTING, oneOfAttacked->position) != ESpellCastProblem::OK) + continue; + + //check if spell should be casted (probability handling) + if(rand()%100 >= chance) + continue; + + //casting //TODO: check if spell can be blocked or target is immune + if (castMe) //stacks use 0 spell power. If needed, default = 3 or custom value is used + handleSpellCasting(spellID, spellLevel, destination, !attacker->attackerOwned, attacker->owner, nullptr, nullptr, 0, ECastingMode::AFTER_ATTACK_CASTING, attacker); + } + } +} + +void CGameHandler::handleAttackBeforeCasting (const BattleAttack & bat) +{ + const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking); + attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no death stare / acid breath needed? +} + +void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat ) +{ + const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking); + if (!attacker) //could be already dead + return; + attackCasting(bat, Bonus::SPELL_AFTER_ATTACK, attacker); + + if(bat.bsa.at(0).newAmount <= 0) + { + //don't try death stare or acid breath on dead stack (crash!) + return; + } + + if (attacker->hasBonusOfType(Bonus::DEATH_STARE) && bat.bsa.size()) + { + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + double chanceToKill = attacker->valOfBonuses(Bonus::DEATH_STARE, 0) / 100.0f; + vstd::amin(chanceToKill, 1); //cap at 100% + + std::binomial_distribution<> distr(attacker->count, chanceToKill); + std::mt19937 rng(rand()); + + int staredCreatures = distr(rng); + + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = (attacker->count + cap - 1) / cap; //not much more than chance * count + vstd::amin(staredCreatures, maxToKill); + + staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->level(); + if (staredCreatures) + { + if (bat.bsa.at(0).newAmount > 0) //TODO: death stare was not originally available for multiple-hex attacks, but... + handleSpellCasting(SpellID::DEATH_STARE, 0, gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->position, + !attacker->attackerOwned, attacker->owner, nullptr, nullptr, staredCreatures, ECastingMode::AFTER_ATTACK_CASTING, attacker); + } + } + + int acidDamage = 0; + TBonusListPtr acidBreath = attacker->getBonuses(Selector::type(Bonus::ACID_BREATH)); + for(const Bonus *b : *acidBreath) + { + if (b->additionalInfo > rand()%100) + acidDamage += b->val; + } + if (acidDamage) + { + handleSpellCasting(SpellID::ACID_BREATH_DAMAGE, 0, gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->position, + !attacker->attackerOwned, attacker->owner, nullptr, nullptr, + acidDamage * attacker->count, ECastingMode::AFTER_ATTACK_CASTING, attacker); + } +} + +bool CGameHandler::castSpell(const CGHeroInstance *h, SpellID spellID, const int3 &pos) +{ + const CSpell *s = spellID.toSpell(); + int cost = h->getSpellCost(s); + int schoolLevel = h->getSpellSchoolLevel(s); + + if(!h->canCastThisSpell(s)) + COMPLAIN_RET("Hero cannot cast this spell!"); + if(h->mana < cost) + COMPLAIN_RET("Hero doesn't have enough spell points to cast this spell!"); + if(s->combatSpell) + COMPLAIN_RET("This function can be used only for adventure map spells!"); + + AdvmapSpellCast asc; + asc.caster = h; + asc.spellID = spellID; + sendAndApply(&asc); + + switch(spellID) + { + case SpellID::SUMMON_BOAT: + { + //check if spell works at all + if(rand() % 100 >= s->getPower(schoolLevel)) //power is % chance of success + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. + iw.text.addReplacement(h->name); + sendAndApply(&iw); + break; + } + + //try to find unoccupied boat to summon + const CGBoat *nearest = nullptr; + double dist = 0; + int3 summonPos = h->bestLocation(); + if(summonPos.x < 0) + COMPLAIN_RET("There is no water tile available!"); + + for(const CGObjectInstance *obj : gs->map->objects) + { + if(obj && obj->ID == Obj::BOAT) + { + const CGBoat *b = static_cast(obj); + if(b->hero) continue; //we're looking for unoccupied boat + + double nDist = distance(b->pos, h->getPosition()); + if(!nearest || nDist < dist) //it's first boat or closer than previous + { + nearest = b; + dist = nDist; + } + } + } + + if(nearest) //we found boat to summon + { + ChangeObjPos cop; + cop.objid = nearest->id; + cop.nPos = summonPos + int3(1,0,0);; + cop.flags = 1; + sendAndApply(&cop); + } + else if(schoolLevel < 2) //none or basic level -> cannot create boat :( + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon. + sendAndApply(&iw); + } + else //create boat + { + NewObject no; + no.ID = Obj::BOAT; + no.subID = h->getBoatType(); + no.pos = summonPos + int3(1,0,0);; + sendAndApply(&no); + } + break; + } + + case SpellID::SCUTTLE_BOAT: + { + //check if spell works at all + if(rand() % 100 >= s->getPower(schoolLevel)) //power is % chance of success + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed + iw.text.addReplacement(h->name); + sendAndApply(&iw); + break; + } + if(!gs->map->isInTheMap(pos)) + COMPLAIN_RET("Invalid dst tile for scuttle!"); + + //TODO: test range, visibility + const TerrainTile *t = &gs->map->getTile(pos); + if(!t->visitableObjects.size() || t->visitableObjects.back()->ID != Obj::BOAT) + COMPLAIN_RET("There is no boat to scuttle!"); + + RemoveObject ro; + ro.id = t->visitableObjects.back()->id; + sendAndApply(&ro); + break; + } + case SpellID::DIMENSION_DOOR: + { + const TerrainTile *dest = getTile(pos); + const TerrainTile *curr = getTile(h->getSightCenter()); + + if(!dest) + COMPLAIN_RET("Destination tile doesn't exist!"); + if(!h->movement) + COMPLAIN_RET("Hero needs movement points to cast Dimension Door!"); + if(h->getBonusesCount(Bonus::SPELL_EFFECT, SpellID::DIMENSION_DOOR) >= s->getPower(schoolLevel)) //limit casts per turn + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today. + iw.text.addReplacement(h->name); + sendAndApply(&iw); + break; + } + + GiveBonus gb; + gb.id = h->id.getNum(); + gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::NONE, Bonus::SPELL_EFFECT, 0, SpellID::DIMENSION_DOOR); + sendAndApply(&gb); + + if(!dest->isClear(curr)) //wrong dest tile + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed! + sendAndApply(&iw); + break; + } + + if (moveHero(h->id, pos + h->getVisitableOffset(), true)) + { + SetMovePoints smp; + smp.hid = h->id; + smp.val = std::max(0, h->movement - 300); + sendAndApply(&smp); + } + } + break; + case SpellID::FLY: + { + int subtype = schoolLevel >= 2 ? 1 : 2; //adv or expert + + GiveBonus gb; + gb.id = h->id.getNum(); + gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::FLYING_MOVEMENT, Bonus::SPELL_EFFECT, 0, SpellID::FLY, subtype); + sendAndApply(&gb); + } + break; + case SpellID::WATER_WALK: + { + int subtype = schoolLevel >= 2 ? 1 : 2; //adv or expert + + GiveBonus gb; + gb.id = h->id.getNum(); + gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::WATER_WALKING, Bonus::SPELL_EFFECT, 0, SpellID::WATER_WALK, subtype); + sendAndApply(&gb); + } + break; + + case SpellID::TOWN_PORTAL: + { + if (!gs->map->isInTheMap(pos)) + COMPLAIN_RET("Destination tile not present!") + TerrainTile tile = gs->map->getTile(pos); + if (tile.visitableObjects.empty() || tile.visitableObjects.back()->ID != Obj::TOWN ) + COMPLAIN_RET("Town not found for Town Portal!"); + + CGTownInstance * town = static_cast(tile.visitableObjects.back()); + if (town->tempOwner != h->tempOwner) + COMPLAIN_RET("Can't teleport to another player!"); + if (town->visitingHero) + COMPLAIN_RET("Can't teleport to occupied town!"); + + if (h->getSpellSchoolLevel(s) < 2) + { + double dist = town->pos.dist2d(h->pos); + ObjectInstanceID nearest = town->id; //nearest town's ID + for(const CGTownInstance * currTown : gs->getPlayer(h->tempOwner)->towns) + { + double curDist = currTown->pos.dist2d(h->pos); + if (nearest == ObjectInstanceID() || curDist < dist) + { + nearest = town->id; + dist = curDist; + } + } + if (town->id != nearest) + COMPLAIN_RET("This hero can only teleport to nearest town!") + } + moveHero(h->id, town->visitablePos() + h->getVisitableOffset() ,1); + } + break; + + case SpellID::VISIONS: + case SpellID::VIEW_EARTH: + case SpellID::DISGUISE: + case SpellID::VIEW_AIR: + default: + COMPLAIN_RET("This spell is not implemented yet!"); + } + + SetMana sm; + sm.hid = h->id; + sm.val = h->mana - cost; + sendAndApply(&sm); + + return true; +} + +void CGameHandler::visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h) +{ + if (!t.visitableObjects.empty()) + { + //to prevent self-visiting heroes on space press + if(t.visitableObjects.back() != h) + objectVisited(t.visitableObjects.back(), h); + else if(t.visitableObjects.size() > 1) + objectVisited(*(t.visitableObjects.end()-2),h); + } +} + +bool CGameHandler::sacrificeCreatures(const IMarket *market, const CGHeroInstance *hero, SlotID slot, ui32 count) +{ + int oldCount = hero->getStackCount(slot); + + if(oldCount < count) + COMPLAIN_RET("Not enough creatures to sacrifice!") + else if(oldCount == count && hero->Slots().size() == 1 && hero->needsLastStack()) + COMPLAIN_RET("Cannot sacrifice last creature!"); + + int crid = hero->getStack(slot).type->idNumber; + + changeStackCount(StackLocation(hero, slot), -count); + + int dump, exp; + market->getOffer(crid, 0, dump, exp, EMarketMode::CREATURE_EXP); + exp *= count; + changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(exp)); + + return true; +} + +bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, ArtifactPosition slot) +{ + ArtifactLocation al(hero, slot); + const CArtifactInstance *a = al.getArt(); + + if(!a) + COMPLAIN_RET("Cannot find artifact to sacrifice!"); + + + int dmp, expToGive; + m->getOffer(hero->getArtTypeId(slot), 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); + + removeArtifact(al); + changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive); + return true; +} + +void CGameHandler::makeStackDoNothing(const CStack * next) +{ + BattleAction doNothing; + doNothing.actionType = Battle::NO_ACTION; + doNothing.additionalInfo = 0; + doNothing.destinationTile = -1; + doNothing.side = !next->attackerOwned; + doNothing.stackNumber = next->ID; + + makeAutomaticAction(next, doNothing); +} + +bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) +{ + if(sl.army->hasStackAtSlot(sl.slot)) + COMPLAIN_RET("Slot is already taken!"); + + if(!sl.slot.validSlot()) + COMPLAIN_RET("Cannot insert stack to that slot!"); + + InsertNewStack ins; + ins.sl = sl; + ins.stack = CStackBasicDescriptor(c, count); + sendAndApply(&ins); + return true; +} + +bool CGameHandler::eraseStack(const StackLocation &sl, bool forceRemoval/* = false*/) +{ + if(!sl.army->hasStackAtSlot(sl.slot)) + COMPLAIN_RET("Cannot find a stack to erase"); + + if(sl.army->Slots().size() == 1 //from the last stack + && sl.army->needsLastStack() //that must be left + && !forceRemoval) //ignore above conditions if we are forcing removal + { + COMPLAIN_RET("Cannot erase the last stack!"); + } + + EraseStack es; + es.sl = sl; + sendAndApply(&es); + return true; +} + +bool CGameHandler::changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue /*= false*/) +{ + TQuantity currentCount = sl.army->getStackCount(sl.slot); + if((absoluteValue && count < 0) + || (!absoluteValue && -count > currentCount)) + { + COMPLAIN_RET("Cannot take more stacks than present!"); + } + + if((currentCount == -count && !absoluteValue) + || (!count && absoluteValue)) + { + eraseStack(sl); + } + else + { + ChangeStackCount csc; + csc.sl = sl; + csc.count = count; + csc.absoluteValue = absoluteValue; + sendAndApply(&csc); + } + return true; +} + +bool CGameHandler::addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) +{ + const CCreature *slotC = sl.army->getCreature(sl.slot); + if(!slotC) //slot is empty + insertNewStack(sl, c, count); + else if(c == slotC) + changeStackCount(sl, count); + else + { + COMPLAIN_RET("Cannot add " + c->namePl + " to slot " + boost::lexical_cast(sl.slot) + "!"); + } + return true; +} + +void CGameHandler::tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) +{ + if(removeObjWhenFinished) + removeAfterVisit(src); + + if(!src->canBeMergedWith(*dst, allowMerging)) + { + if (allowMerging) //do that, add all matching creatures. + { + bool cont = true; + while (cont) + { + for(auto i = src->stacks.begin(); i != src->stacks.end(); i++)//while there are unmoved creatures + { + SlotID pos = dst->getSlotFor(i->second->type); + if(pos.validSlot()) + { + moveStack(StackLocation(src, i->first), StackLocation(dst, pos)); + cont = true; + break; //or iterator crashes + } + cont = false; + } + } + } + showGarrisonDialog(src->id, dst->id, true); //show garrison window and optionally remove ourselves from map when player ends + } + else //merge + { + moveArmy(src, dst, allowMerging); + } +} + +bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) +{ + if(!src.army->hasStackAtSlot(src.slot)) + COMPLAIN_RET("No stack to move!"); + + if(dst.army->hasStackAtSlot(dst.slot) && dst.army->getCreature(dst.slot) != src.army->getCreature(src.slot)) + COMPLAIN_RET("Cannot move: stack of different type at destination pos!"); + + if(!dst.slot.validSlot()) + COMPLAIN_RET("Cannot move stack to that slot!"); + + if(count == -1) + { + count = src.army->getStackCount(src.slot); + } + + if(src.army != dst.army //moving away + && count == src.army->getStackCount(src.slot) //all creatures + && src.army->Slots().size() == 1 //from the last stack + && src.army->needsLastStack()) //that must be left + { + COMPLAIN_RET("Cannot move away the last creature!"); + } + + RebalanceStacks rs; + rs.src = src; + rs.dst = dst; + rs.count = count; + sendAndApply(&rs); + return true; +} + +bool CGameHandler::swapStacks(const StackLocation &sl1, const StackLocation &sl2) +{ + + if(!sl1.army->hasStackAtSlot(sl1.slot)) + return moveStack(sl2, sl1); + else if(!sl2.army->hasStackAtSlot(sl2.slot)) + return moveStack(sl1, sl2); + else + { + SwapStacks ss; + ss.sl1 = sl1; + ss.sl2 = sl2; + sendAndApply(&ss); + return true; + } +} + +void CGameHandler::runBattle() +{ + setBattle(gs->curB); + assert(gs->curB); + //TODO: pre-tactic stuff, call scripts etc. + + //tactic round + { + while(gs->curB->tacticDistance && !battleResult.get()) + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + + //spells opening battle + for(int i = 0; i < 2; ++i) + { + auto h = gs->curB->battleGetFightingHero(i); + if(h && h->hasBonusOfType(Bonus::OPENING_BATTLE_SPELL)) + { + TBonusListPtr bl = h->getBonuses(Selector::type(Bonus::OPENING_BATTLE_SPELL)); + for (Bonus *b : *bl) + { + handleSpellCasting(SpellID(b->subtype), 3, -1, 0, h->tempOwner, nullptr, gs->curB->battleGetFightingHero(1-i), b->val, ECastingMode::HERO_CASTING, nullptr); + } + } + } + + //main loop + while(!battleResult.get()) //till the end of the battle ;] + { + NEW_ROUND; + auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it + for(auto &obstPtr : obstacles) + { + if(const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) + if(sco->turnsRemaining == 0) + removeObstacle(*obstPtr); + } + + const BattleInfo & curB = *gs->curB; + + //remove clones after all mechanics and animations are handled! + std::set stacksToRemove; + for (auto stack : curB.stacks) + { + if (stack->idDeadClone()) + stacksToRemove.insert(stack); + } + for (auto stack : stacksToRemove) + { + BattleStacksRemoved bsr; + bsr.stackIDs.insert(stack->ID); + sendAndApply(&bsr); + } + //stack loop + + const CStack *next; + while(!battleResult.get() && (next = curB.getNextStack()) && next->willMove()) + { + + //check for bad morale => freeze + int nextStackMorale = next->MoraleVal(); + if( nextStackMorale < 0 && + !(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE) + || NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses) + ) + { + if( rand()%24 < -2 * nextStackMorale) + { + //unit loses its turn - empty freeze action + BattleAction ba; + ba.actionType = Battle::BAD_MORALE; + ba.additionalInfo = 1; + ba.side = !next->attackerOwned; + ba.stackNumber = next->ID; + + makeAutomaticAction(next, ba); + continue; + } + } + + if(next->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE)) //while in berserk + { //fixme: stack should not attack itself + std::pair attackInfo = curB.getNearestStack(next, boost::logic::indeterminate); + if(attackInfo.first != nullptr) + { + BattleAction attack; + attack.actionType = Battle::WALK_AND_ATTACK; + attack.side = !next->attackerOwned; + attack.stackNumber = next->ID; + attack.additionalInfo = attackInfo.first->position; + attack.destinationTile = attackInfo.second; + + makeAutomaticAction(next, attack); + } + else + { + makeStackDoNothing(next); + } + continue; + } + + const CGHeroInstance * curOwner = gs->curB->battleGetOwner(next); + + if( (next->position < 0 || next->getCreature()->idNumber == CreatureID::BALLISTA) //arrow turret or ballista + && (!curOwner || curOwner->getSecSkillLevel(SecondarySkill::ARTILLERY) == 0)) //hero has no artillery + { + BattleAction attack; + attack.actionType = Battle::SHOOT; + attack.side = !next->attackerOwned; + attack.stackNumber = next->ID; + + for(auto & elem : gs->curB->stacks) + { + if(elem->owner != next->owner && elem->isValidTarget()) + { + attack.destinationTile = elem->position; + break; + } + } + + makeAutomaticAction(next, attack); + continue; + } + + if(next->getCreature()->idNumber == CreatureID::CATAPULT && (!curOwner || curOwner->getSecSkillLevel(SecondarySkill::BALLISTICS) == 0)) //catapult, hero has no ballistics + { + const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); + + if(!attackableBattleHexes.empty()) + { + BattleAction attack; + attack.destinationTile = attackableBattleHexes[rand() % attackableBattleHexes.size()]; + attack.actionType = Battle::CATAPULT; + attack.additionalInfo = 0; + attack.side = !next->attackerOwned; + attack.stackNumber = next->ID; + + makeAutomaticAction(next, attack); + } + else + { + makeStackDoNothing(next); + } + continue; + } + + if(next->getCreature()->idNumber == CreatureID::FIRST_AID_TENT) + { + std::vector< const CStack * > possibleStacks; + + //is there any clean algorithm for that? (boost.range seems to lack copy_if) -> remove_copy_if? + for(const CStack *s : battleGetAllStacks()) + if(s->owner == next->owner && s->canBeHealed()) + possibleStacks.push_back(s); + + if(!possibleStacks.size()) + { + makeStackDoNothing(next); + continue; + } + + if(!curOwner || curOwner->getSecSkillLevel(SecondarySkill::FIRST_AID) == 0) //no hero or hero has no first aid + { + range::random_shuffle(possibleStacks); + const CStack * toBeHealed = possibleStacks.front(); + + BattleAction heal; + heal.actionType = Battle::STACK_HEAL; + heal.additionalInfo = 0; + heal.destinationTile = toBeHealed->position; + heal.side = !next->attackerOwned; + heal.stackNumber = next->ID; + + makeAutomaticAction(next, heal); + continue; + } + } + + int numberOfAsks = 1; + bool breakOuter = false; + do + {//ask interface and wait for answer + if(!battleResult.get()) + { + stackTurnTrigger(next); //various effects + + if (vstd::contains(next->state, EBattleStackState::FEAR)) + { + makeStackDoNothing(next); //end immediately if stack was affected by fear + } + else + { + logGlobal->traceStream() << "Activating " << next->nodeName(); + BattleSetActiveStack sas; + sas.stack = next->ID; + sendAndApply(&sas); + boost::unique_lock lock(battleMadeAction.mx); + battleMadeAction.data = false; + while (next->alive() && //next is invalid after sacrificing current stack :? + (!battleMadeAction.data && !battleResult.get())) //active stack hasn't made its action and battle is still going + battleMadeAction.cond.wait(lock); + } + } + + if(battleResult.get()) //don't touch it, battle could be finished while waiting got action + { + breakOuter = true; + break; + } + + //we're after action, all results applied + checkForBattleEnd(); //check if this action ended the battle + + //check for good morale + nextStackMorale = next->MoraleVal(); + if(!vstd::contains(next->state,EBattleStackState::HAD_MORALE) //only one extra move per turn possible + && !vstd::contains(next->state,EBattleStackState::DEFENDING) + && !next->waited() + && !vstd::contains(next->state, EBattleStackState::FEAR) + && next->alive() + && nextStackMorale > 0 + && !(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE) + || NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses + ) + { + if(rand()%24 < nextStackMorale) //this stack hasn't got morale this turn + + { + BattleTriggerEffect bte; + bte.stackID = next->ID; + bte.effect = Bonus::MORALE; + bte.val = 1; + bte.additionalInfo = 0; + sendAndApply(&bte); //play animation + + ++numberOfAsks; //move this stack once more + } + } + + --numberOfAsks; + } while (numberOfAsks > 0); + + if (breakOuter) + { + break; + } + + } + } + + endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1)); +} + +bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) +{ + BattleSetActiveStack bsa; + bsa.stack = stack->ID; + bsa.askPlayerInterface = false; + sendAndApply(&bsa); + + bool ret = makeBattleAction(ba); + checkForBattleEnd(); + return ret; +} + +void CGameHandler::giveHeroArtifact(const CGHeroInstance *h, const CArtifactInstance *a, ArtifactPosition pos) +{ + assert(a->artType); + ArtifactLocation al; + al.artHolder = const_cast(h); + + ArtifactPosition slot = ArtifactPosition::PRE_FIRST; + if(pos < 0) + { + if(pos == ArtifactPosition::FIRST_AVAILABLE) + slot = a->firstAvailableSlot(h); + else + slot = a->firstBackpackSlot(h); + } + else + { + slot = pos; + } + + al.slot = slot; + + if(slot < 0 || !a->canBePutAt(al)) + { + complain("Cannot put artifact in that slot!"); + return; + } + + putArtifact(al, a); +} +void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) +{ + PutArtifact pa; + pa.art = a; + pa.al = al; + sendAndApply(&pa); +} + +void CGameHandler::giveHeroNewArtifact(const CGHeroInstance *h, const CArtifact *artType, ArtifactPosition pos) +{ + CArtifactInstance *a = nullptr; + if(!artType->constituents) + { + a = new CArtifactInstance(); + } + else + { + a = new CCombinedArtifactInstance(); + } + a->artType = artType; //*NOT* via settype -> all bonus-related stuff must be done by NewArtifact apply + + NewArtifact na; + na.art = a; + sendAndApply(&na); // -> updates a!!!, will create a on other machines + + giveHeroArtifact(h, a, pos); +} + +void CGameHandler::setBattleResult(BattleResult::EResult resultType, int victoriusSide) +{ + if(battleResult.get()) + { + complain("There is already set result?"); + return; + } + auto br = new BattleResult; + br->result = resultType; + br->winner = victoriusSide; //surrendering side loses + gs->curB->calculateCasualties(br->casualties); + battleResult.set(br); +} + +void CGameHandler::commitPackage( CPackForClient *pack ) +{ + sendAndApply(pack); +} + +void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) +{ + std::vector::iterator tile; + std::vector tiles; + getFreeTiles(tiles); + ui32 amount = tiles.size() / 200; //Chance is 0.5% for each tile + std::random_shuffle(tiles.begin(), tiles.end()); + logGlobal->traceStream() << "Spawning wandering monsters. Found " << tiles.size() << " free tiles. Creature type: " << creatureID; + const CCreature *cre = VLC->creh->creatures.at(creatureID); + for (int i = 0; i < amount; ++i) + { + tile = tiles.begin(); + logGlobal->traceStream() << "\tSpawning monster at " << *tile; + putNewMonster(creatureID, cre->getRandomAmount(std::rand), *tile); + tiles.erase(tile); //not use it again + } +} + +void CGameHandler::removeObstacle(const CObstacleInstance &obstacle) +{ + ObstaclesRemoved obsRem; + obsRem.obstacles.insert(obstacle.uniqueID); + sendAndApply(&obsRem); +} + +void CGameHandler::synchronizeArtifactHandlerLists() +{ + UpdateArtHandlerLists uahl; + uahl.treasures = VLC->arth->treasures; + uahl.minors = VLC->arth->minors; + uahl.majors = VLC->arth->majors; + uahl.relics = VLC->arth->relics; + sendAndApply(&uahl); +} + +bool CGameHandler::isValidObject(const CGObjectInstance *obj) const +{ + return vstd::contains(gs->map->objects, obj); +} + +bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) +{ + if(dynamic_cast(pack)) + return false; + + auto query = queries.topQuery(player); + if(query && query->blocksPack(pack)) + { + complain(boost::str(boost::format("Player %s has to answer queries before attempting any further actions. Top query is %s!") % player % query->toString())); + return true; + } + + return false; +} + +void CGameHandler::removeAfterVisit(const CGObjectInstance *object) +{ + //If the object is being visited, there must be a matching query + for(const auto &query : queries.allQueries()) + { + if(auto someVistQuery = std::dynamic_pointer_cast(query)) + { + if(someVistQuery->visitedObject == object) + { + someVistQuery->removeObjectAfterVisit = true; + return; + } + } + }; + + //If we haven't returned so far, there is no query and no visit, call was wrong + assert("This function needs to be called during the object visit!"); +} + +bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) +{ + if(auto topQuery = queries.topQuery(hero->getOwner())) + if(auto visit = std::dynamic_pointer_cast(topQuery)) + return !(visit->visitedObject == obj && visit->visitingHero == hero); + + return true; +} + +void CGameHandler::duelFinished() +{ + auto si = getStartInfo(); + auto getName = [&](int i){ return si->getIthPlayersSettings(gs->curB->sides.at(i).color).name; }; + + int casualtiesPoints = 0; + logGlobal->debugStream() << boost::format("Winner side %d\nWinner casualties:") + % (int)battleResult.data->winner; + + for(auto & elem : battleResult.data->casualties[battleResult.data->winner]) + { + const CCreature *c = VLC->creh->creatures[elem.first]; + logGlobal->debugStream() << boost::format("\t* %d of %s") % elem.second % c->namePl; + casualtiesPoints += c->AIValue * elem.second; + } + logGlobal->debugStream() << boost::format("Total casualties points: %d") % casualtiesPoints; + + + time_t timeNow; + time(&timeNow); + + std::ofstream out(cmdLineOptions["resultsFile"].as(), std::ios::app); + if(out) + { + out << boost::format("%s\t%s\t%s\t%d\t%d\t%d\t%s\n") % si->mapname % getName(0) % getName(1) + % battleResult.data->winner % battleResult.data->result % casualtiesPoints + % asctime(localtime(&timeNow)); + } + else + { + logGlobal->errorStream() << "Cannot open to write " << cmdLineOptions["resultsFile"].as(); + } + + CSaveFile resultFile("result.vdrst"); + resultFile << *battleResult.data; + + BattleResultsApplied resultsApplied; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + sendAndApply(&resultsApplied); + return; +} + +CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance *army, BattleInfo *bat) +{ + heroWithDeadCommander = ObjectInstanceID(); + + PlayerColor color = army->tempOwner; + if(color == PlayerColor::UNFLAGGABLE) + color = PlayerColor::NEUTRAL; + + for(CStack *st : bat->stacks) + { + if(vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account summoned stacks + continue; + if (st->owner != color) //remove only our stacks + continue; + + //FIXME: this info is also used in BattleInfo::calculateCasualties, refactor + st->count = std::max (0, st->count - st->resurrected); + + if (!st->count && !st->base) //we can imagine stacks of war mahcines that are not spawned by artifacts? + { + auto warMachine = VLC->arth->creatureToMachineID(st->type->idNumber); + if (warMachine != ArtifactID::NONE) + { + auto hero = dynamic_cast (army); + if (hero) + removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + } + } + + if(!army->slotEmpty(st->slot) && st->count < army->getStackCount(st->slot)) + { + StackLocation sl(army, st->slot); + if(st->alive()) + newStackCounts.push_back(std::pair(sl, st->count)); + else + newStackCounts.push_back(std::pair(sl, 0)); + } + if (st->base && !st->count) + { + auto c = dynamic_cast (st->base); + if (c) //switch commander status to dead + { + auto h = dynamic_cast (army); + if (h && h->commander == c) + heroWithDeadCommander = army->id; //TODO: unify commander handling + } + } + } +} + +void CasualtiesAfterBattle::takeFromArmy(CGameHandler *gh) +{ + for(TStackAndItsNewCount &ncount : newStackCounts) + { + if(ncount.second > 0) + gh->changeStackCount(ncount.first, ncount.second, true); + else + gh->eraseStack(ncount.first, true); + } + for (auto al : removedWarMachines) + { + gh->removeArtifact(al); + } + if (heroWithDeadCommander != ObjectInstanceID()) + { + SetCommanderProperty scp; + scp.heroid = heroWithDeadCommander; + scp.which = SetCommanderProperty::ALIVE; + scp.amount = 0; + gh->sendAndApply (&scp); + } +} + +CGameHandler::FinishingBattleHelper::FinishingBattleHelper(shared_ptr Query, bool Duel, int RemainingBattleQueriesCount) +{ + assert(Query->result); + assert(Query->bi); + auto &result = *Query->result; + auto &info = *Query->bi; + + winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; + loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; + victor = info.sides[result.winner].color; + loser = info.sides[!result.winner].color; + duel = Duel; + remainingBattleQueriesCount = RemainingBattleQueriesCount; +} + +CGameHandler::FinishingBattleHelper::FinishingBattleHelper() +{ + winnerHero = loserHero = nullptr; +}