Browse Source

Merge pull request #3505 from IvanSavenko/bugfixing

[1.4.3] Bugfixing
Ivan Savenko 1 year ago
parent
commit
bf62e47481

+ 12 - 1
CCallback.cpp

@@ -405,7 +405,10 @@ std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const
 
 std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
 {
-	return activeBattles.at(battleID);
+	if (activeBattles.count(battleID))
+		return activeBattles.at(battleID);
+
+	throw std::runtime_error("Failed to find battle " + std::to_string(battleID.getNum()) + " of player " + player->toString() + ". Number of ongoing battles: " + std::to_string(activeBattles.size()));
 }
 
 std::optional<PlayerColor> CBattleCallback::getPlayerID() const
@@ -415,10 +418,18 @@ std::optional<PlayerColor> CBattleCallback::getPlayerID() const
 
 void CBattleCallback::onBattleStarted(const IBattleInfo * info)
 {
+	if (activeBattles.count(info->getBattleID()) > 0)
+		throw std::runtime_error("Player " + player->toString() + " is already engaged in battle " + std::to_string(info->getBattleID().getNum()));
+
+	logGlobal->debug("Battle %d started for player %s", info->getBattleID(), player->toString());
 	activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
 }
 
 void CBattleCallback::onBattleEnded(const BattleID & battleID)
 {
+	if (activeBattles.count(battleID) == 0)
+		throw std::runtime_error("Player " + player->toString() + " is not engaged in battle " + std::to_string(battleID.getNum()));
+
+	logGlobal->debug("Battle %d ended for player %s", battleID, player->toString());
 	activeBattles.erase(battleID);
 }

+ 5 - 7
client/Client.cpp

@@ -560,18 +560,16 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
 
 void CClient::battleStarted(const BattleInfo * info)
 {
+	std::shared_ptr<CPlayerInterface> att, def;
+	auto & leftSide = info->sides[0];
+	auto & rightSide = info->sides[1];
+
 	for(auto & battleCb : battleCallbacks)
 	{
-		if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
-			|| !battleCb.first.isValidPlayer())
-		{
+		if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color)
 			battleCb.second->onBattleStarted(info);
-		}
 	}
 
-	std::shared_ptr<CPlayerInterface> att, def;
-	auto & leftSide = info->sides[0], & rightSide = info->sides[1];
-
 	//If quick combat is not, do not prepare interfaces for battleint
 	auto callBattleStart = [&](PlayerColor color, ui8 side)
 	{

+ 1 - 0
client/Client.h

@@ -202,6 +202,7 @@ public:
 	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
 	void giveHeroBonus(GiveBonus * bonus) override {};
 	void setMovePoints(SetMovePoints * smp) override {};
+	void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {};
 	void setManaPoints(ObjectInstanceID hid, int val) override {};
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};

+ 10 - 1
client/adventureMap/AdventureMapInterface.cpp

@@ -691,7 +691,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 			if(pathNode->layer == EPathfindingLayer::LAND)
 				CCS->curh->set(cursorMove[turns]);
 			else
-				CCS->curh->set(cursorSailVisit[turns]);
+				CCS->curh->set(cursorSail[turns]);
 			break;
 
 		case EPathNodeAction::VISIT:
@@ -706,6 +706,15 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 			}
 			else if(pathNode->layer == EPathfindingLayer::LAND)
 				CCS->curh->set(cursorVisit[turns]);
+			else if (pathNode->layer == EPathfindingLayer::SAIL &&
+					 objAtTile &&
+					 objAtTile->isCoastVisitable() &&
+					 pathNode->theNodeBefore &&
+					 pathNode->theNodeBefore->layer == EPathfindingLayer::LAND )
+			{
+				// exception - when visiting shipwreck located on coast from land - show 'horse' cursor, not 'ship' cursor
+				CCS->curh->set(cursorVisit[turns]);
+			}
 			else
 				CCS->curh->set(cursorSailVisit[turns]);
 			break;

+ 0 - 6
config/creatures/dungeon.json

@@ -411,12 +411,6 @@
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 5
 			},
-			"hateGiants" :
-			{
-				"type" : "HATE",
-				"subtype" : "creature.giant",
-				"val" : 50
-			},
 			"hateTitans" :
 			{
 				"type" : "HATE",

+ 4 - 0
docs/modders/Entities_Format/Spell_Format.md

@@ -61,6 +61,10 @@
 			"positive": true,
 		},
 		
+		// If true, then creature capable of casting this spell can cast this spell on itself
+		// If false, then creature can only cast this spell on other units
+		"canCastOnSelf" : false,
+		
 		// If true, spell won't be available on a map without water
 		"onlyOnWaterMap" : true,
 

+ 1 - 1
docs/players/Game_Mechanics.md

@@ -185,7 +185,7 @@ Following options can be used to configure simultaneous turns:
 
 While simultaneous turns are active, VCMI tracks contacts for each pair of player separately.
 
-Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses same rules as standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as wandering monsters or treasures that block path between heroes. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
+Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses rules similar to standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as pickable treasures that block path between heroes. Any existing wandering monsters that block path between heroes are ignored for range calculation. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc.
 
 Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other.
 

+ 1 - 0
include/vcmi/spells/Spell.h

@@ -44,6 +44,7 @@ public:
 	virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind)
 
 	virtual bool hasSchool(SpellSchool school) const = 0;
+	virtual bool canCastOnSelf() const = 0;
 	virtual void forEachSchool(const SchoolCallback & cb) const = 0;
 	virtual int32_t getCost(const int32_t skillLevel) const = 0;
 

+ 4 - 1
lib/CCreatureHandler.cpp

@@ -397,7 +397,10 @@ void CCreature::serializeJson(JsonSerializeFormat & handler)
 	if(!handler.saving)
 	{
 		if(ammMin > ammMax)
+		{
 			logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier);
+			std::swap(ammMin, ammMax);
+		}
 	}
 }
 
@@ -622,7 +625,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json
 	}
 	else
 	{
-		logGlobal->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
+		logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage);
 		cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin);
 		cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax);
 	}

+ 5 - 1
lib/CHeroHandler.cpp

@@ -473,7 +473,11 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const
 		hero->initialArmy[i].minAmount = static_cast<ui32>(source["min"].Float());
 		hero->initialArmy[i].maxAmount = static_cast<ui32>(source["max"].Float());
 
-		assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount);
+		if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount)
+		{
+			logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
+			std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount);
+		}
 
 		VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature)
 		{

+ 8 - 1
lib/CRandomGenerator.cpp

@@ -37,21 +37,25 @@ void CRandomGenerator::resetSeed()
 
 TRandI CRandomGenerator::getIntRange(int lower, int upper)
 {
+	assert(lower <= upper);
 	return std::bind(TIntDist(lower, upper), std::ref(rand));
 }
 
 vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper)
 {
+	assert(lower <= upper);
 	return std::bind(TInt64Dist(lower, upper), std::ref(rand));
 }
 
 int CRandomGenerator::nextInt(int upper)
 {
+	assert(0 <= upper);
 	return getIntRange(0, upper)();
 }
 
 int CRandomGenerator::nextInt(int lower, int upper)
 {
+	assert(lower <= upper);
 	return getIntRange(lower, upper)();
 }
 
@@ -62,16 +66,19 @@ int CRandomGenerator::nextInt()
 
 vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper)
 {
-    return std::bind(TRealDist(lower, upper), std::ref(rand));
+	assert(lower <= upper);
+	return std::bind(TRealDist(lower, upper), std::ref(rand));
 }
 
 double CRandomGenerator::nextDouble(double upper)
 {
+	assert(0 <= upper);
 	return getDoubleRange(0, upper)();
 }
 
 double CRandomGenerator::nextDouble(double lower, double upper)
 {
+	assert(lower <= upper);
 	return getDoubleRange(lower, upper)();
 }
 

+ 1 - 0
lib/IGameCallback.h

@@ -122,6 +122,7 @@ public:
 	virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0;
 	virtual void giveHeroBonus(GiveBonus * bonus)=0;
 	virtual void setMovePoints(SetMovePoints * smp)=0;
+	virtual void setMovePoints(ObjectInstanceID hid, int val, bool absolute)=0;
 	virtual void setManaPoints(ObjectInstanceID hid, int val)=0;
 	virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0;
 	virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0;

+ 9 - 5
lib/gameState/CGameStateCampaign.cpp

@@ -210,17 +210,21 @@ void CGameStateCampaign::placeCampaignHeroes()
 	// with the same hero type id
 	std::vector<CGHeroInstance *> removedHeroes;
 
-	std::set<HeroTypeID> heroesToRemove = campaignState->getReservedHeroes();
+	std::set<HeroTypeID> reservedHeroes = campaignState->getReservedHeroes();
+	std::set<HeroTypeID> heroesToRemove;
+
+	for (auto const & heroID : reservedHeroes )
+	{
+		// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
+		if (!campaignState->getHeroByType(heroID).isNull())
+			heroesToRemove.insert(heroID);
+	}
 
 	for(auto & campaignHeroReplacement : campaignHeroReplacements)
 		heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType());
 
 	for(auto & heroID : heroesToRemove)
 	{
-		// Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear
-		if (campaignState->getHeroByType(heroID).isNull())
-			continue;
-
 		auto * hero = gameState->getUsedHero(heroID);
 		if(hero)
 		{

+ 7 - 1
lib/gameState/TavernHeroesPool.cpp

@@ -40,7 +40,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const
 	return TavernSlotRole::NONE;
 }
 
-void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role)
+void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints)
 {
 	vstd::erase_if(currentTavern, [&](const TavernSlot & entry){
 		return entry.player == player && entry.slot == slot;
@@ -54,6 +54,12 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot,
 	if (h && army)
 		h->setToArmy(army);
 
+	if (h && replenishPoints)
+	{
+		h->setMovementPoints(h->movementPointsLimit(true));
+		h->mana = h->manaLimit();
+	}
+
 	TavernSlot newSlot;
 	newSlot.hero = h;
 	newSlot.player = player;

+ 1 - 1
lib/gameState/TavernHeroesPool.h

@@ -74,7 +74,7 @@ public:
 	void setAvailability(HeroTypeID hero, std::set<PlayerColor> mask);
 
 	/// Makes hero available in tavern of specified player
-	void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role);
+	void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 4 - 4
lib/mapObjects/CGHeroInstance.cpp

@@ -436,14 +436,14 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst)
 
 		int count = rand.nextInt(stack.minAmount, stack.maxAmount);
 
-		const CCreature * creature = stack.creature.toCreature();
-
-		if(creature == nullptr)
+		if(stack.creature == CreatureID::NONE)
 		{
-			logGlobal->error("Hero %s has invalid creature with id %d in initial army", getNameTranslated(), stack.creature.toEnum());
+			logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated());
 			continue;
 		}
 
+		const CCreature * creature = stack.creature.toCreature();
+
 		if(creature->warMachine != ArtifactID::NONE) //war machine
 		{
 			warMachinesGiven++;

+ 1 - 5
lib/mapObjects/CGTownBuilding.cpp

@@ -151,11 +151,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 				gb.id = heroID;
 				cb->giveHeroBonus(&gb);
 
-				SetMovePoints mp;
-				mp.val = 600;
-				mp.absolute = false;
-				mp.hid = heroID;
-				cb->setMovePoints(&mp);
+				cb->setMovePoints(heroID, 600, false);
 
 				iw.text.appendRawString(VLC->generaltexth->allTexts[580]);
 				cb->showInfoDialog(&iw);

+ 1 - 1
lib/networkPacks/NetPacksLib.cpp

@@ -959,7 +959,7 @@ void FoWChange::applyGs(CGameState *gs)
 
 void SetAvailableHero::applyGs(CGameState *gs)
 {
-	gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID);
+	gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID, replenishPoints);
 }
 
 void GiveBonus::applyGs(CGameState *gs)

+ 2 - 0
lib/networkPacks/PacksForClient.h

@@ -352,6 +352,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
 	PlayerColor player;
 	HeroTypeID hid; //HeroTypeID::NONE if no hero
 	CSimpleArmy army;
+	bool replenishPoints;
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -362,6 +363,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient
 		h & player;
 		h & hid;
 		h & army;
+		h & replenishPoints;
 	}
 };
 

+ 18 - 1
lib/spells/BattleSpellMechanics.cpp

@@ -213,7 +213,24 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem)
 
 	Target spellTarget = transformSpellTarget(target);
 
-    return effects->applicable(problem, this, target, spellTarget);
+	const battle::Unit * mainTarget = nullptr;
+
+	if (!getSpell()->canCastOnSelf())
+	{
+		if(spellTarget.front().unitValue)
+		{
+			mainTarget = target.front().unitValue;
+		}
+		else if(spellTarget.front().hexValue.isValid())
+		{
+			mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true);
+		}
+
+		if (mainTarget && mainTarget == caster)
+			return false; // can't cast on self
+	}
+
+	return effects->applicable(problem, this, target, spellTarget);
 }
 
 std::vector<const CStack *> BattleSpellMechanics::getAffectedStacks(const Target & target) const

+ 7 - 0
lib/spells/CSpellHandler.cpp

@@ -76,6 +76,7 @@ CSpell::CSpell():
 	power(0),
 	combat(false),
 	creatureAbility(false),
+	castOnSelf(false),
 	positiveness(ESpellPositiveness::NEUTRAL),
 	defaultProbability(0),
 	rising(false),
@@ -285,6 +286,11 @@ bool CSpell::hasBattleEffects() const
 	return levels[0].battleEffects.getType() == JsonNode::JsonType::DATA_STRUCT && !levels[0].battleEffects.Struct().empty();
 }
 
+bool CSpell::canCastOnSelf() const
+{
+	return castOnSelf;
+}
+
 const std::string & CSpell::getIconImmune() const
 {
 	return iconImmune;
@@ -702,6 +708,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode &
 		spell->school[info.id] = schoolNames[info.jsonName].Bool();
 	}
 
+	spell->castOnSelf = json["canCastOnSelf"].Bool();
 	spell->level = static_cast<si32>(json["level"].Integer());
 	spell->power = static_cast<si32>(json["power"].Integer());
 

+ 2 - 0
lib/spells/CSpellHandler.h

@@ -203,6 +203,7 @@ public:
 	int64_t calculateDamage(const spells::Caster * caster) const override;
 
 	bool hasSchool(SpellSchool school) const override;
+	bool canCastOnSelf() const override;
 
 	/**
 	 * Calls cb for each school this spell belongs to
@@ -329,6 +330,7 @@ private:
 	si32 power; //spell's power
 	bool combat; //is this spell combat (true) or adventure (false)
 	bool creatureAbility; //if true, only creatures can use this spell
+	bool castOnSelf; // if set, creature caster can cast this spell on itself
 	si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative
 
 	std::unique_ptr<spells::ISpellMechanicsFactory> mechanics;//(!) do not serialize

+ 0 - 1
lib/spells/effects/Timed.cpp

@@ -169,7 +169,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 			case 1: 
 				//Coronius style specialty bonus.
 				//Please note that actual Coronius isnt here, because Slayer is a spell that doesnt affect monster stats and is used only in calculateDmgRange
-				power = std::max(5 - tier, 0);
 				break;
 			}
 			if(m->isNegativeSpell())

+ 12 - 0
server/CGameHandler.cpp

@@ -1467,6 +1467,9 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
 	sendAndApply(&vc);
 	visitCastleObjects(obj, hero);
 	giveSpells (obj, hero);
+
+	if (obj->visitingHero && obj->garrisonHero)
+		useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id);
 	checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact?
 }
 
@@ -1510,6 +1513,15 @@ void CGameHandler::setMovePoints(SetMovePoints * smp)
 	sendAndApply(smp);
 }
 
+void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute)
+{
+	SetMovePoints smp;
+	smp.hid = hid;
+	smp.val = val;
+	smp.absolute = absolute;
+	sendAndApply(&smp);
+}
+
 void CGameHandler::setManaPoints(ObjectInstanceID hid, int val)
 {
 	SetMana sm;

+ 1 - 0
server/CGameHandler.h

@@ -142,6 +142,7 @@ public:
 	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override;
 	void giveHeroBonus(GiveBonus * bonus) override;
 	void setMovePoints(SetMovePoints * smp) override;
+	void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override;
 	void setManaPoints(ObjectInstanceID hid, int val) override;
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override;
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override;

+ 5 - 0
server/processors/HeroPoolProcessor.cpp

@@ -74,6 +74,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer
 	sah.slotID = selectSlotForRole(color, sah.roleID);
 	sah.player = color;
 	sah.hid = hero->getHeroType();
+	sah.replenishPoints = false;
 	gameHandler->sendAndApply(&sah);
 }
 
@@ -87,6 +88,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns
 	sah.hid = hero->getHeroType();
 	sah.army.clearSlots();
 	sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1);
+	sah.replenishPoints = false;
 
 	gameHandler->sendAndApply(&sah);
 }
@@ -98,6 +100,7 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS
 	sah.roleID = TavernSlotRole::NONE;
 	sah.slotID = slot;
 	sah.hid = HeroTypeID::NONE;
+	sah.replenishPoints = false;
 	gameHandler->sendAndApply(&sah);
 }
 
@@ -106,6 +109,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
 	SetAvailableHero sah;
 	sah.player = color;
 	sah.slotID = slot;
+	sah.replenishPoints = true;
 
 	CGHeroInstance *newHero = pickHeroFor(needNativeHero, color);
 
@@ -129,6 +133,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe
 	{
 		sah.hid = HeroTypeID::NONE;
 	}
+
 	gameHandler->sendAndApply(&sah);
 }
 

+ 1 - 0
test/mock/mock_IGameCallback.h

@@ -82,6 +82,7 @@ public:
 	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
 	void giveHeroBonus(GiveBonus * bonus) override {}
 	void setMovePoints(SetMovePoints * smp) override {}
+	void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {};
 	void setManaPoints(ObjectInstanceID hid, int val) override {}
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}

+ 1 - 0
test/mock/mock_spells_Spell.h

@@ -45,6 +45,7 @@ public:
 	MOCK_CONST_METHOD0(isOffensive, bool());
 	MOCK_CONST_METHOD0(isSpecial, bool());
 	MOCK_CONST_METHOD0(isMagical, bool());
+	MOCK_CONST_METHOD0(canCastOnSelf, bool());
 	MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool));
 	MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &));
 	MOCK_CONST_METHOD0(getCastSound, const std::string &());