浏览代码

Merge pull request #1928 from Nordsoft91/cast-reward

Spell cast reward
Nordsoft91 2 年之前
父节点
当前提交
ebd17c9e4a

+ 1 - 0
client/Client.h

@@ -231,6 +231,7 @@ public:
 	void changeObjPos(ObjectInstanceID objid, int3 newPos) override {};
 	void changeObjPos(ObjectInstanceID objid, int3 newPos) override {};
 	void sendAndApply(CPackForClient * pack) override {};
 	void sendAndApply(CPackForClient * pack) override {};
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
+	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {}
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {}
 	void changeFogOfWar(std::unordered_set<int3, ShashInt3> & tiles, PlayerColor player, bool hide) override {}
 	void changeFogOfWar(std::unordered_set<int3, ShashInt3> & tiles, PlayerColor player, bool hide) override {}

+ 3 - 0
cmake_modules/VCMI_lib.cmake

@@ -134,6 +134,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/spells/BattleSpellMechanics.cpp
 		${MAIN_LIB_DIR}/spells/BattleSpellMechanics.cpp
 		${MAIN_LIB_DIR}/spells/BonusCaster.cpp
 		${MAIN_LIB_DIR}/spells/BonusCaster.cpp
 		${MAIN_LIB_DIR}/spells/CSpellHandler.cpp
 		${MAIN_LIB_DIR}/spells/CSpellHandler.cpp
+		${MAIN_LIB_DIR}/spells/ExternalCaster.cpp
 		${MAIN_LIB_DIR}/spells/ISpellMechanics.cpp
 		${MAIN_LIB_DIR}/spells/ISpellMechanics.cpp
 		${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.cpp
 		${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.cpp
 		${MAIN_LIB_DIR}/spells/Problem.cpp
 		${MAIN_LIB_DIR}/spells/Problem.cpp
@@ -397,7 +398,9 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/spells/BattleSpellMechanics.h
 		${MAIN_LIB_DIR}/spells/BattleSpellMechanics.h
 		${MAIN_LIB_DIR}/spells/BonusCaster.h
 		${MAIN_LIB_DIR}/spells/BonusCaster.h
 		${MAIN_LIB_DIR}/spells/CSpellHandler.h
 		${MAIN_LIB_DIR}/spells/CSpellHandler.h
+		${MAIN_LIB_DIR}/spells/ExternalCaster.h
 		${MAIN_LIB_DIR}/spells/ISpellMechanics.h
 		${MAIN_LIB_DIR}/spells/ISpellMechanics.h
+		${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.h
 		${MAIN_LIB_DIR}/spells/Problem.h
 		${MAIN_LIB_DIR}/spells/Problem.h
 		${MAIN_LIB_DIR}/spells/ProxyCaster.h
 		${MAIN_LIB_DIR}/spells/ProxyCaster.h
 		${MAIN_LIB_DIR}/spells/TargetCondition.h
 		${MAIN_LIB_DIR}/spells/TargetCondition.h

+ 4 - 0
include/vcmi/spells/Caster.h

@@ -15,6 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class PlayerColor;
 class PlayerColor;
 struct MetaString;
 struct MetaString;
 class ServerCallback;
 class ServerCallback;
+class CGHeroInstance;
 
 
 namespace battle
 namespace battle
 {
 {
@@ -65,6 +66,9 @@ public:
 	virtual void getCastDescription(const Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const = 0;
 	virtual void getCastDescription(const Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const = 0;
 
 
 	virtual void spendMana(ServerCallback * server, const int32_t spellCost) const = 0;
 	virtual void spendMana(ServerCallback * server, const int32_t spellCost) const = 0;
+	
+	///used to identify actual hero caster
+	virtual const CGHeroInstance * getHeroCaster() const = 0;
 };
 };
 
 
 }
 }

+ 7 - 0
lib/IGameCallback.h

@@ -28,6 +28,11 @@ class CStackBasicDescriptor;
 class CGCreature;
 class CGCreature;
 struct ShashInt3;
 struct ShashInt3;
 
 
+namespace spells
+{
+	class Caster;
+}
+
 #if SCRIPTING_ENABLED
 #if SCRIPTING_ENABLED
 namespace scripting
 namespace scripting
 {
 {
@@ -132,6 +137,8 @@ public:
 	virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map
 	virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map
 	virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0;
 	virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0;
 	virtual void changeFogOfWar(std::unordered_set<int3, ShashInt3> &tiles, PlayerColor player, bool hide) = 0;
 	virtual void changeFogOfWar(std::unordered_set<int3, ShashInt3> &tiles, PlayerColor player, bool hide) = 0;
+	
+	virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0;
 };
 };
 
 
 class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback
 class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback

+ 5 - 0
lib/battle/CUnitState.cpp

@@ -418,6 +418,11 @@ int32_t CUnitState::getCasterUnitId() const
 	return static_cast<int32_t>(unitId());
 	return static_cast<int32_t>(unitId());
 }
 }
 
 
+const CGHeroInstance * CUnitState::getHeroCaster() const
+{
+	return nullptr;
+}
+
 int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const
 int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const
 {
 {
 	int32_t skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->getIndex()));
 	int32_t skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->getIndex()));

+ 1 - 0
lib/battle/CUnitState.h

@@ -192,6 +192,7 @@ public:
 	int64_t getEffectValue(const spells::Spell * spell) const override;
 	int64_t getEffectValue(const spells::Spell * spell) const override;
 
 
 	PlayerColor getCasterOwner() const override;
 	PlayerColor getCasterOwner() const override;
+	const CGHeroInstance * getHeroCaster() const override;
 	void getCasterName(MetaString & text) const override;
 	void getCasterName(MetaString & text) const override;
 	void getCastDescription(const spells::Spell * spell, const std::vector<const Unit *> & attacked, MetaString & text) const override;
 	void getCastDescription(const spells::Spell * spell, const std::vector<const Unit *> & attacked, MetaString & text) const override;
 
 

+ 6 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -570,7 +570,7 @@ TExpType CGHeroInstance::calculateXp(TExpType exp) const
 
 
 int32_t CGHeroInstance::getCasterUnitId() const
 int32_t CGHeroInstance::getCasterUnitId() const
 {
 {
-	return -1; //TODO: special value for attacker/defender hero
+	return id.getNum();
 }
 }
 
 
 int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const
 int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const
@@ -669,6 +669,11 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std::
 		attacked.at(0)->addNameReplacement(text, true);
 		attacked.at(0)->addNameReplacement(text, true);
 }
 }
 
 
+const CGHeroInstance * CGHeroInstance::getHeroCaster() const
+{
+	return this;
+}
+
 void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) const
 void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) const
 {
 {
 	if(spellCost != 0)
 	if(spellCost != 0)

+ 1 - 0
lib/mapObjects/CGHeroInstance.h

@@ -275,6 +275,7 @@ public:
 	int64_t getEffectValue(const spells::Spell * spell) const override;
 	int64_t getEffectValue(const spells::Spell * spell) const override;
 
 
 	PlayerColor getCasterOwner() const override;
 	PlayerColor getCasterOwner() const override;
+	const CGHeroInstance * getHeroCaster() const override;
 
 
 	void getCasterName(MetaString & text) const override;
 	void getCasterName(MetaString & text) const override;
 	void getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;

+ 5 - 0
lib/mapObjects/CRewardableConstructor.cpp

@@ -132,6 +132,11 @@ void CRandomRewardObjectInfo::configureReward(CRewardableObject * object, CRando
 	reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng);
 	reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng);
 	reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells);
 	reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells);
 	reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
 	reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng);
+	if(!source["spellCast"].isNull() && source["spellCast"].isStruct())
+	{
+		reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng);
+		reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer();
+	}
 
 
 	for ( auto node : source["changeCreatures"].Struct() )
 	for ( auto node : source["changeCreatures"].Struct() )
 	{
 	{

+ 15 - 3
lib/mapObjects/CRewardableObject.cpp

@@ -17,6 +17,8 @@
 #include "../IGameCallback.h"
 #include "../IGameCallback.h"
 #include "../CGameState.h"
 #include "../CGameState.h"
 #include "../CPlayerState.h"
 #include "../CPlayerState.h"
+#include "../spells/CSpellHandler.h"
+#include "../spells/ISpellMechanics.h"
 
 
 #include "CObjectClassesHandler.h"
 #include "CObjectClassesHandler.h"
 
 
@@ -142,7 +144,8 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 			iw.text = vi.message;
 			iw.text = vi.message;
 			vi.reward.loadComponents(iw.components, h);
 			vi.reward.loadComponents(iw.components, h);
 			iw.type = infoWindowType;
 			iw.type = infoWindowType;
-			cb->showInfoDialog(&iw);
+			if(!iw.components.empty() || !iw.text.toString().empty())
+				cb->showInfoDialog(&iw);
 		}
 		}
 		// grant reward afterwards. Note that it may remove object
 		// grant reward afterwards. Note that it may remove object
 		grantReward(index, h, markAsVisit);
 		grantReward(index, h, markAsVisit);
@@ -359,8 +362,17 @@ void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, c
 
 
 		cb->giveCreatures(this, hero, creatures, false);
 		cb->giveCreatures(this, hero, creatures, false);
 	}
 	}
-
-	if(info.reward.removeObject)
+	
+	if(info.reward.spellCast.first != SpellID::NONE)
+	{
+		caster.setActualCaster(hero);
+		caster.setSpellSchoolLevel(info.reward.spellCast.second);
+		cb->castSpell(&caster, info.reward.spellCast.first, int3{-1, -1, -1});
+		
+		if(info.reward.removeObject)
+			logMod->warn("Removal of object with spell casts is not supported!");
+	}
+	else if(info.reward.removeObject) //FIXME: object can't track spell cancel or finish, so removeObject leads to crash
 		cb->removeObject(this);
 		cb->removeObject(this);
 }
 }
 
 

+ 11 - 1
lib/mapObjects/CRewardableObject.h

@@ -14,6 +14,7 @@
 
 
 #include "../NetPacksBase.h"
 #include "../NetPacksBase.h"
 #include "../ResourceSet.h"
 #include "../ResourceSet.h"
+#include "../spells/ExternalCaster.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
@@ -169,6 +170,9 @@ public:
 	std::vector<ArtifactID> artifacts;
 	std::vector<ArtifactID> artifacts;
 	std::vector<SpellID> spells;
 	std::vector<SpellID> spells;
 	std::vector<CStackBasicDescriptor> creatures;
 	std::vector<CStackBasicDescriptor> creatures;
+	
+	/// actions that hero may execute and object caster. Pair of spellID and school level
+	std::pair<SpellID, int> spellCast;
 
 
 	/// list of components that will be added to reward description. First entry in list will override displayed component
 	/// list of components that will be added to reward description. First entry in list will override displayed component
 	std::vector<Component> extraComponents;
 	std::vector<Component> extraComponents;
@@ -191,7 +195,8 @@ public:
 		movePoints(0),
 		movePoints(0),
 		movePercentage(-1),
 		movePercentage(-1),
 		primary(4, 0),
 		primary(4, 0),
-		removeObject(false)
+		removeObject(false),
+		spellCast(SpellID::NONE, SecSkillLevel::NONE)
 	{}
 	{}
 
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	template <typename Handler> void serialize(Handler &h, const int version)
@@ -213,6 +218,8 @@ public:
 		h & spells;
 		h & spells;
 		h & creatures;
 		h & creatures;
 		h & creaturesChange;
 		h & creaturesChange;
+		if(version >= 821)
+			h & spellCast;
 	}
 	}
 };
 };
 
 
@@ -317,6 +324,9 @@ protected:
 	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
 	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
 
 
 	bool onceVisitableObjectCleared;
 	bool onceVisitableObjectCleared;
+	
+	/// caster to cast adveture spells
+	mutable spells::ExternalCaster caster;
 
 
 public:
 public:
 	EVisitMode getVisitMode() const;
 	EVisitMode getVisitMode() const;

+ 3 - 3
lib/mapObjects/JsonRandom.h

@@ -32,7 +32,7 @@ namespace JsonRandom
 	};
 	};
 
 
 	DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0);
 	DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0);
-	DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set<std::string> & valuesSet);
+	DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set<std::string> & valuesSet = {});
 	DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<si32> loadPrimary(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<si32> loadPrimary(const JsonNode & value, CRandomGenerator & rng);
@@ -41,8 +41,8 @@ namespace JsonRandom
 	DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<ArtifactID> loadArtifacts(const JsonNode & value, CRandomGenerator & rng);
 
 
-	DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector<SpellID> spells);
-	DLL_LINKAGE std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector<SpellID> & spells);
+	DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector<SpellID> spells = {});
+	DLL_LINKAGE std::vector<SpellID> loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector<SpellID> & spells = {});
 
 
 	DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng);
 	DLL_LINKAGE std::vector<CStackBasicDescriptor> loadCreatures(const JsonNode & value, CRandomGenerator & rng);

+ 92 - 66
lib/spells/AdventureSpellMechanics.cpp

@@ -37,27 +37,28 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad
 		return false;
 		return false;
 	}
 	}
 
 
-	const CGHeroInstance * caster = parameters.caster;
-
-	if(caster->inTownGarrison)
+	if(const CGHeroInstance * heroCaster = dynamic_cast<const CGHeroInstance *>(parameters.caster))
 	{
 	{
-		env->complain("Attempt to cast an adventure spell in town garrison");
-		return false;
-	}
+		if(heroCaster->inTownGarrison)
+		{
+			env->complain("Attempt to cast an adventure spell in town garrison");
+			return false;
+		}
 
 
-	const auto level = caster->getSpellSchoolLevel(owner);
-	const auto cost = owner->getCost(level);
+		const auto level = heroCaster->getSpellSchoolLevel(owner);
+		const auto cost = owner->getCost(level);
 
 
-	if(!caster->canCastThisSpell(owner))
-	{
-		env->complain("Hero cannot cast this spell!");
-		return false;
-	}
+		if(!heroCaster->canCastThisSpell(owner))
+		{
+			env->complain("Hero cannot cast this spell!");
+			return false;
+		}
 
 
-	if(caster->mana < cost)
-	{
-		env->complain("Hero doesn't have enough spell points to cast this spell!");
-		return false;
+		if(heroCaster->mana < cost)
+		{
+			env->complain("Hero doesn't have enough spell points to cast this spell!");
+			return false;
+		}
 	}
 	}
 
 
 	ESpellCastResult result = beginCast(env, parameters);
 	ESpellCastResult result = beginCast(env, parameters);
@@ -82,7 +83,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron
 		for(const Bonus & b : bonuses)
 		for(const Bonus & b : bonuses)
 		{
 		{
 			GiveBonus gb;
 			GiveBonus gb;
-			gb.id = parameters.caster->id.getNum();
+			gb.id = parameters.caster->getCasterUnitId();
 			gb.bonus = b;
 			gb.bonus = b;
 			env->apply(&gb);
 			env->apply(&gb);
 		}
 		}
@@ -105,7 +106,7 @@ ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env,
 void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 {
 	AdvmapSpellCast asc;
 	AdvmapSpellCast asc;
-	asc.casterID = parameters.caster->id;
+	asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId());
 	asc.spellID = owner->id;
 	asc.spellID = owner->id;
 	env->apply(&asc);
 	env->apply(&asc);
 
 
@@ -121,13 +122,7 @@ void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const Adventur
 	switch(result)
 	switch(result)
 	{
 	{
 	case ESpellCastResult::OK:
 	case ESpellCastResult::OK:
-		{
-			SetMana sm;
-			sm.hid = parameters.caster->id;
-			sm.absolute = false;
-			sm.val = -cost;
-			env->apply(&sm);
-		}
+		parameters.caster->spendMana(env, cost);
 		break;
 		break;
 	default:
 	default:
 		break;
 		break;
@@ -142,21 +137,28 @@ SummonBoatMechanics::SummonBoatMechanics(const CSpell * s):
 
 
 ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 {
-	if(parameters.caster->boat)
+	if(!parameters.caster->getHeroCaster())
+	{
+		env->complain("Not a hero caster!");
+		return ESpellCastResult::ERROR;
+	}
+	
+	if(parameters.caster->getHeroCaster()->boat)
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 333);//%s is already in boat
 		iw.text.addTxt(MetaString::GENERAL_TXT, 333);//%s is already in boat
-		iw.text.addReplacement(parameters.caster->getNameTranslated());
+		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 		return ESpellCastResult::CANCEL;
 	}
 	}
 
 
-	int3 summonPos = parameters.caster->bestLocation();
+	int3 summonPos = parameters.caster->getHeroCaster()->bestLocation();
+	
 	if(summonPos.x < 0)
 	if(summonPos.x < 0)
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 334);//There is no place to put the boat.
 		iw.text.addTxt(MetaString::GENERAL_TXT, 334);//There is no place to put the boat.
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 		return ESpellCastResult::CANCEL;
@@ -168,9 +170,9 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success
 	if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed.
 		iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed.
-		iw.text.addReplacement(parameters.caster->getNameTranslated());
+		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::OK;
 		return ESpellCastResult::OK;
 	}
 	}
@@ -186,7 +188,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 			if(b->hero)
 			if(b->hero)
 				continue; //we're looking for unoccupied boat
 				continue; //we're looking for unoccupied boat
 
 
-			double nDist = b->pos.dist2d(parameters.caster->visitablePos());
+			double nDist = b->pos.dist2d(parameters.caster->getHeroCaster()->visitablePos());
 			if(!nearest || nDist < dist) //it's first boat or closer than previous
 			if(!nearest || nDist < dist) //it's first boat or closer than previous
 			{
 			{
 				nearest = b;
 				nearest = b;
@@ -205,7 +207,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	else if(schoolLevel < 2) //none or basic level -> cannot create boat :(
 	else if(schoolLevel < 2) //none or basic level -> cannot create boat :(
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon.
 		iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon.
 		env->apply(&iw);
 		env->apply(&iw);
 	}
 	}
@@ -213,7 +215,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 	{
 		NewObject no;
 		NewObject no;
 		no.ID = Obj::BOAT;
 		no.ID = Obj::BOAT;
-		no.subID = parameters.caster->getBoatType();
+		no.subID = parameters.caster->getHeroCaster()->getBoatType();
 		no.pos = summonPos + int3(1,0,0);
 		no.pos = summonPos + int3(1,0,0);
 		env->apply(&no);
 		env->apply(&no);
 	}
 	}
@@ -233,9 +235,9 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen
 	if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success
 	if(env->getRNG()->getInt64Range(0, 99)() >= owner->getLevelPower(schoolLevel)) //power is % chance of success
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed
 		iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed
-		iw.text.addReplacement(parameters.caster->getNameTranslated());
+		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::OK;
 		return ESpellCastResult::OK;
 	}
 	}
@@ -273,9 +275,15 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 		env->complain("Destination is out of map!");
 		env->complain("Destination is out of map!");
 		return ESpellCastResult::ERROR;
 		return ESpellCastResult::ERROR;
 	}
 	}
+	
+	if(!parameters.caster->getHeroCaster())
+	{
+		env->complain("Not a hero caster!");
+		return ESpellCastResult::ERROR;
+	}
 
 
 	const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
 	const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
-	const TerrainTile * curr = env->getCb()->getTile(parameters.caster->getSightCenter());
+	const TerrainTile * curr = env->getCb()->getTile(parameters.caster->getHeroCaster()->getSightCenter());
 
 
 	if(nullptr == dest)
 	if(nullptr == dest)
 	{
 	{
@@ -289,7 +297,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 		return ESpellCastResult::ERROR;
 		return ESpellCastResult::ERROR;
 	}
 	}
 
 
-	if(parameters.caster->movement <= 0) //unlike town portal non-zero MP is enough
+	if(parameters.caster->getHeroCaster()->movement <= 0) //unlike town portal non-zero MP is enough
 	{
 	{
 		env->complain("Hero needs movement points to cast Dimension Door!");
 		env->complain("Hero needs movement points to cast Dimension Door!");
 		return ESpellCastResult::ERROR;
 		return ESpellCastResult::ERROR;
@@ -301,34 +309,34 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 	std::stringstream cachingStr;
 	std::stringstream cachingStr;
 	cachingStr << "source_" << Bonus::SPELL_EFFECT << "id_" << owner->id.num;
 	cachingStr << "source_" << Bonus::SPELL_EFFECT << "id_" << owner->id.num;
 
 
-	if(parameters.caster->getBonuses(Selector::source(Bonus::SPELL_EFFECT, owner->id), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn
+	if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(Bonus::SPELL_EFFECT, owner->id), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today.
 		iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today.
-		iw.text.addReplacement(parameters.caster->getNameTranslated());
+		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 		return ESpellCastResult::CANCEL;
 	}
 	}
 
 
 	GiveBonus gb;
 	GiveBonus gb;
-	gb.id = parameters.caster->id.getNum();
+	gb.id = parameters.caster->getCasterUnitId();
 	gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::NONE, Bonus::SPELL_EFFECT, 0, owner->id);
 	gb.bonus = Bonus(Bonus::ONE_DAY, Bonus::NONE, Bonus::SPELL_EFFECT, 0, owner->id);
 	env->apply(&gb);
 	env->apply(&gb);
 
 
 	if(!dest->isClear(curr)) //wrong dest tile
 	if(!dest->isClear(curr)) //wrong dest tile
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed!
 		iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed!
 		env->apply(&iw);
 		env->apply(&iw);
 	}
 	}
-	else if(env->moveHero(parameters.caster->id, parameters.caster->convertFromVisitablePos(parameters.pos), true))
+	else if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true))
 	{
 	{
 		SetMovePoints smp;
 		SetMovePoints smp;
-		smp.hid = parameters.caster->id;
-		if(movementCost < static_cast<int>(parameters.caster->movement))
-			smp.val = parameters.caster->movement - movementCost;
+		smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
+		if(movementCost < static_cast<int>(parameters.caster->getHeroCaster()->movement))
+			smp.val = parameters.caster->getHeroCaster()->movement - movementCost;
 		else
 		else
 			smp.val = 0;
 			smp.val = 0;
 		env->apply(&smp);
 		env->apply(&smp);
@@ -346,6 +354,12 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 {
 {
 	const CGTownInstance * destination = nullptr;
 	const CGTownInstance * destination = nullptr;
 	const int moveCost = movementCost(parameters);
 	const int moveCost = movementCost(parameters);
+	
+	if(!parameters.caster->getHeroCaster())
+	{
+		env->complain("Not a hero caster!");
+		return ESpellCastResult::ERROR;
+	}
 
 
 	if(parameters.caster->getSpellSchoolLevel(owner) < 2)
 	if(parameters.caster->getSpellSchoolLevel(owner) < 2)
 	{
 	{
@@ -355,13 +369,13 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 		if(nullptr == destination)
 		if(nullptr == destination)
 			return ESpellCastResult::ERROR;
 			return ESpellCastResult::ERROR;
 
 
-		if(static_cast<int>(parameters.caster->movement) < moveCost)
+		if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
 			return ESpellCastResult::ERROR;
 			return ESpellCastResult::ERROR;
 
 
 		if(destination->visitingHero)
 		if(destination->visitingHero)
 		{
 		{
 			InfoWindow iw;
 			InfoWindow iw;
-			iw.player = parameters.caster->tempOwner;
+			iw.player = parameters.caster->getCasterOwner();
 			iw.text.addTxt(MetaString::GENERAL_TXT, 123);
 			iw.text.addTxt(MetaString::GENERAL_TXT, 123);
 			env->apply(&iw);
 			env->apply(&iw);
 			return ESpellCastResult::CANCEL;
 			return ESpellCastResult::CANCEL;
@@ -397,7 +411,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 			return ESpellCastResult::ERROR;
 			return ESpellCastResult::ERROR;
 		}
 		}
 
 
-		const auto relations = env->getCb()->getPlayerRelations(destination->tempOwner, parameters.caster->tempOwner);
+		const auto relations = env->getCb()->getPlayerRelations(destination->tempOwner, parameters.caster->getCasterOwner());
 
 
 		if(relations == PlayerRelations::ENEMIES)
 		if(relations == PlayerRelations::ENEMIES)
 		{
 		{
@@ -405,7 +419,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 			return ESpellCastResult::ERROR;
 			return ESpellCastResult::ERROR;
 		}
 		}
 
 
-		if(static_cast<int>(parameters.caster->movement) < moveCost)
+		if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
 		{
 		{
 			env->complain("This hero has not enough movement points!");
 			env->complain("This hero has not enough movement points!");
 			return ESpellCastResult::ERROR;
 			return ESpellCastResult::ERROR;
@@ -423,11 +437,11 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 		return ESpellCastResult::ERROR;
 		return ESpellCastResult::ERROR;
 	}
 	}
 
 
-	if(env->moveHero(parameters.caster->id, parameters.caster->convertFromVisitablePos(destination->visitablePos()), true))
+	if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(destination->visitablePos()), true))
 	{
 	{
 		SetMovePoints smp;
 		SetMovePoints smp;
-		smp.hid = parameters.caster->id;
-		smp.val = std::max<ui32>(0, parameters.caster->movement - moveCost);
+		smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
+		smp.val = std::max<ui32>(0, parameters.caster->getHeroCaster()->movement - moveCost);
 		env->apply(&smp);
 		env->apply(&smp);
 	}
 	}
 	return ESpellCastResult::OK;
 	return ESpellCastResult::OK;
@@ -436,11 +450,17 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 {
 	std::vector<const CGTownInstance *>	towns = getPossibleTowns(env, parameters);
 	std::vector<const CGTownInstance *>	towns = getPossibleTowns(env, parameters);
+	
+	if(!parameters.caster->getHeroCaster())
+	{
+		env->complain("Not a hero caster!");
+		return ESpellCastResult::ERROR;
+	}
 
 
 	if(towns.empty())
 	if(towns.empty())
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 124);
 		iw.text.addTxt(MetaString::GENERAL_TXT, 124);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 		return ESpellCastResult::CANCEL;
@@ -448,10 +468,10 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 
 
 	const int moveCost = movementCost(parameters);
 	const int moveCost = movementCost(parameters);
 
 
-	if(static_cast<int>(parameters.caster->movement) < moveCost)
+	if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
 	{
 	{
 		InfoWindow iw;
 		InfoWindow iw;
-		iw.player = parameters.caster->tempOwner;
+		iw.player = parameters.caster->getCasterOwner();
 		iw.text.addTxt(MetaString::GENERAL_TXT, 125);
 		iw.text.addTxt(MetaString::GENERAL_TXT, 125);
 		env->apply(&iw);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 		return ESpellCastResult::CANCEL;
@@ -496,13 +516,13 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 		if(request.objects.empty())
 		if(request.objects.empty())
 		{
 		{
 			InfoWindow iw;
 			InfoWindow iw;
-			iw.player = parameters.caster->tempOwner;
+			iw.player = parameters.caster->getCasterOwner();
 			iw.text.addTxt(MetaString::GENERAL_TXT, 124);
 			iw.text.addTxt(MetaString::GENERAL_TXT, 124);
 			env->apply(&iw);
 			env->apply(&iw);
 			return ESpellCastResult::CANCEL;
 			return ESpellCastResult::CANCEL;
 		}
 		}
 
 
-		request.player = parameters.caster->getOwner();
+		request.player = parameters.caster->getCasterOwner();
 		request.title.addTxt(MetaString::JK_TXT, 40);
 		request.title.addTxt(MetaString::JK_TXT, 40);
 		request.description.addTxt(MetaString::JK_TXT, 41);
 		request.description.addTxt(MetaString::JK_TXT, 41);
 		request.icon.id = Component::EComponentType::SPELL;
 		request.icon.id = Component::EComponentType::SPELL;
@@ -520,13 +540,16 @@ const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment
 {
 {
 	if(pool.empty())
 	if(pool.empty())
 		return nullptr;
 		return nullptr;
+	
+	if(!parameters.caster->getHeroCaster())
+		return nullptr;
 
 
 	auto nearest = pool.cbegin(); //nearest town's iterator
 	auto nearest = pool.cbegin(); //nearest town's iterator
-	si32 dist = (*nearest)->pos.dist2dSQ(parameters.caster->pos);
+	si32 dist = (*nearest)->pos.dist2dSQ(parameters.caster->getHeroCaster()->pos);
 
 
 	for(auto i = nearest + 1; i != pool.cend(); ++i)
 	for(auto i = nearest + 1; i != pool.cend(); ++i)
 	{
 	{
-		si32 curDist = (*i)->pos.dist2dSQ(parameters.caster->pos);
+		si32 curDist = (*i)->pos.dist2dSQ(parameters.caster->getHeroCaster()->pos);
 
 
 		if(curDist < dist)
 		if(curDist < dist)
 		{
 		{
@@ -541,7 +564,7 @@ std::vector <const CGTownInstance*> TownPortalMechanics::getPossibleTowns(SpellC
 {
 {
 	std::vector <const CGTownInstance*> ret;
 	std::vector <const CGTownInstance*> ret;
 
 
-	const TeamState * team = env->getCb()->getPlayerTeam(parameters.caster->getOwner());
+	const TeamState * team = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner());
 
 
 	for(const auto & color : team->players)
 	for(const auto & color : team->players)
 	{
 	{
@@ -555,6 +578,9 @@ std::vector <const CGTownInstance*> TownPortalMechanics::getPossibleTowns(SpellC
 
 
 int32_t TownPortalMechanics::movementCost(const AdventureSpellCastParameters & parameters) const
 int32_t TownPortalMechanics::movementCost(const AdventureSpellCastParameters & parameters) const
 {
 {
+	if(parameters.caster != parameters.caster->getHeroCaster()) //if caster is not hero
+		return 0;
+	
 	return GameConstants::BASE_MOVEMENT_COST * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3);
 	return GameConstants::BASE_MOVEMENT_COST * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3);
 }
 }
 
 
@@ -568,11 +594,11 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env
 {
 {
 	ShowWorldViewEx pack;
 	ShowWorldViewEx pack;
 
 
-	pack.player = parameters.caster->getOwner();
+	pack.player = parameters.caster->getCasterOwner();
 
 
 	const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner);
 	const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner);
 
 
-	const auto fowMap = env->getCb()->getPlayerTeam(parameters.caster->getOwner())->fogOfWarMap;
+	const auto fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap;
 
 
 	for(const CGObjectInstance * obj : env->getMap()->objects)
 	for(const CGObjectInstance * obj : env->getMap()->objects)
 	{
 	{

+ 52 - 0
lib/spells/ExternalCaster.cpp

@@ -0,0 +1,52 @@
+/*
+ * ExternalCaster.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 "ExternalCaster.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+namespace spells
+{
+
+ExternalCaster::ExternalCaster()
+	: ProxyCaster(nullptr), schoolLevel(0)
+{
+}
+
+ExternalCaster::ExternalCaster(const Caster * actualCaster_, int schoolLevel_)
+	: ProxyCaster(actualCaster_), schoolLevel(schoolLevel_)
+{
+}
+
+void ExternalCaster::setActualCaster(const Caster * actualCaster_)
+{
+	actualCaster = actualCaster_;
+}
+
+void ExternalCaster::setSpellSchoolLevel(int level)
+{
+	schoolLevel = level;
+}
+
+void ExternalCaster::spendMana(ServerCallback * server, const int32_t spellCost) const
+{
+	//do nothing
+}
+
+int32_t ExternalCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const
+{
+	return schoolLevel;
+}
+
+}
+
+VCMI_LIB_NAMESPACE_END

+ 36 - 0
lib/spells/ExternalCaster.h

@@ -0,0 +1,36 @@
+/*
+ * ExternalCaster.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
+ *
+ */
+
+#pragma once
+
+#include "ProxyCaster.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+namespace spells
+{
+
+class DLL_LINKAGE ExternalCaster : public ProxyCaster
+{
+	int schoolLevel;
+public:
+	ExternalCaster();
+	ExternalCaster(const Caster * actualCaster_, int schoolLevel_);
+	
+	void setActualCaster(const Caster * actualCaster);
+	void setSpellSchoolLevel(int level);
+
+	int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override;
+	void spendMana(ServerCallback * server, const int32_t spellCost) const override;
+};
+
+} // namespace spells
+
+VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/spells/ISpellMechanics.h

@@ -350,7 +350,7 @@ public:
 class DLL_LINKAGE AdventureSpellCastParameters
 class DLL_LINKAGE AdventureSpellCastParameters
 {
 {
 public:
 public:
-	const CGHeroInstance * caster;
+	const spells::Caster * caster;
 	int3 pos;
 	int3 pos;
 };
 };
 
 

+ 8 - 0
lib/spells/ProxyCaster.cpp

@@ -118,6 +118,14 @@ void ProxyCaster::spendMana(ServerCallback * server, const int32_t spellCost) co
 		actualCaster->spendMana(server, spellCost);
 		actualCaster->spendMana(server, spellCost);
 }
 }
 
 
+const CGHeroInstance * ProxyCaster::getHeroCaster() const
+{
+	if(actualCaster)
+		return actualCaster->getHeroCaster();
+	
+	return nullptr;
+}
+
 }
 }
 
 
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/spells/ProxyCaster.h

@@ -35,6 +35,7 @@ public:
 	void getCasterName(MetaString & text) const override;
 	void getCasterName(MetaString & text) const override;
 	void getCastDescription(const Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void getCastDescription(const Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void spendMana(ServerCallback * server, const int32_t spellCost) const override;
 	void spendMana(ServerCallback * server, const int32_t spellCost) const override;
+	const CGHeroInstance * getHeroCaster() const override;
 
 
 protected:
 protected:
 	const Caster * actualCaster;
 	const Caster * actualCaster;

+ 14 - 1
server/CGameHandler.cpp

@@ -6299,6 +6299,19 @@ bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst,
 	return true;
 	return true;
 }
 }
 
 
+void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos)
+{
+	const CSpell * s = spellID.toSpell();
+	if(!s)
+		return;
+
+	AdventureSpellCastParameters p;
+	p.caster = caster;
+	p.pos = pos;
+
+	s->adventureCast(spellEnv, p);
+}
+
 bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2)
 bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2)
 {
 {
 	if(!sl1.army->hasStackAtSlot(sl1.slot))
 	if(!sl1.army->hasStackAtSlot(sl1.slot))
@@ -7360,4 +7373,4 @@ const ObjectInstanceID CGameHandler::putNewObject(Obj ID, int subID, int3 pos)
 	no.pos = pos;
 	no.pos = pos;
 	sendAndApply(&no);
 	sendAndApply(&no);
 	return no.id; //id field will be filled during applying on gs
 	return no.id; //id field will be filled during applying on gs
-}
+}

+ 2 - 1
server/CGameHandler.h

@@ -200,6 +200,8 @@ public:
 
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override;
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override;
 	void changeFogOfWar(std::unordered_set<int3, ShashInt3> &tiles, PlayerColor player, bool hide) override;
 	void changeFogOfWar(std::unordered_set<int3, ShashInt3> &tiles, PlayerColor player, bool hide) override;
+	
+	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override;
 
 
 	bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override;
 	bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override;
 	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override;
 	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override;
@@ -276,7 +278,6 @@ public:
 	void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging);
 	void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging);
 	const ObjectInstanceID putNewObject(Obj ID, int subID, int3 pos);
 	const ObjectInstanceID putNewObject(Obj ID, int subID, int3 pos);
 
 
-
 	template <typename Handler> void serialize(Handler &h, const int version)
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 	{
 		h & QID;
 		h & QID;