diff --git a/client/Client.h b/client/Client.h index c1aee94de..118b5bcb8 100644 --- a/client/Client.h +++ b/client/Client.h @@ -231,6 +231,7 @@ public: void changeObjPos(ObjectInstanceID objid, int3 newPos) override {}; void sendAndApply(CPackForClient * pack) 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(std::unordered_set & tiles, PlayerColor player, bool hide) override {} diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index d5d415fdc..37309fc08 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/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/BonusCaster.cpp ${MAIN_LIB_DIR}/spells/CSpellHandler.cpp + ${MAIN_LIB_DIR}/spells/ExternalCaster.cpp ${MAIN_LIB_DIR}/spells/ISpellMechanics.cpp ${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.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/BonusCaster.h ${MAIN_LIB_DIR}/spells/CSpellHandler.h + ${MAIN_LIB_DIR}/spells/ExternalCaster.h ${MAIN_LIB_DIR}/spells/ISpellMechanics.h + ${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.h ${MAIN_LIB_DIR}/spells/Problem.h ${MAIN_LIB_DIR}/spells/ProxyCaster.h ${MAIN_LIB_DIR}/spells/TargetCondition.h diff --git a/include/vcmi/spells/Caster.h b/include/vcmi/spells/Caster.h index 123865a97..46e347386 100644 --- a/include/vcmi/spells/Caster.h +++ b/include/vcmi/spells/Caster.h @@ -15,6 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class PlayerColor; struct MetaString; class ServerCallback; +class CGHeroInstance; namespace battle { @@ -65,6 +66,9 @@ public: virtual void getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) 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; }; } diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index a6c6eccba..c22f641bf 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -28,6 +28,11 @@ class CStackBasicDescriptor; class CGCreature; struct ShashInt3; +namespace spells +{ + class Caster; +} + #if SCRIPTING_ENABLED namespace scripting { @@ -132,6 +137,8 @@ public: 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(std::unordered_set &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 diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 6d9411ac2..1b9439ac4 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -418,6 +418,11 @@ int32_t CUnitState::getCasterUnitId() const return static_cast(unitId()); } +const CGHeroInstance * CUnitState::getHeroCaster() const +{ + return nullptr; +} + int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const { int32_t skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->getIndex())); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index ee130edb6..0a7a14ab9 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -192,6 +192,7 @@ public: int64_t getEffectValue(const spells::Spell * spell) const override; PlayerColor getCasterOwner() const override; + const CGHeroInstance * getHeroCaster() const override; void getCasterName(MetaString & text) const override; void getCastDescription(const spells::Spell * spell, const std::vector & attacked, MetaString & text) const override; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 126521780..e4fe431db 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -570,7 +570,7 @@ TExpType CGHeroInstance::calculateXp(TExpType exp) 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 @@ -669,6 +669,11 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std:: attacked.at(0)->addNameReplacement(text, true); } +const CGHeroInstance * CGHeroInstance::getHeroCaster() const +{ + return this; +} + void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) const { if(spellCost != 0) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index d49235b8c..51f633384 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -275,6 +275,7 @@ public: int64_t getEffectValue(const spells::Spell * spell) const override; PlayerColor getCasterOwner() const override; + const CGHeroInstance * getHeroCaster() const override; void getCasterName(MetaString & text) const override; void getCastDescription(const spells::Spell * spell, const std::vector & attacked, MetaString & text) const override; diff --git a/lib/mapObjects/CRewardableConstructor.cpp b/lib/mapObjects/CRewardableConstructor.cpp index cb3d0f741..1a9d1e0f6 100644 --- a/lib/mapObjects/CRewardableConstructor.cpp +++ b/lib/mapObjects/CRewardableConstructor.cpp @@ -132,6 +132,11 @@ void CRandomRewardObjectInfo::configureReward(CRewardableObject * object, CRando reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells); 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() ) { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 560960aa1..9cfa405fd 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -17,6 +17,8 @@ #include "../IGameCallback.h" #include "../CGameState.h" #include "../CPlayerState.h" +#include "../spells/CSpellHandler.h" +#include "../spells/ISpellMechanics.h" #include "CObjectClassesHandler.h" @@ -142,7 +144,8 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const iw.text = vi.message; vi.reward.loadComponents(iw.components, h); 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 grantReward(index, h, markAsVisit); @@ -359,8 +362,17 @@ void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, c 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); } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index ef5b06d90..74b7a413e 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -14,6 +14,7 @@ #include "../NetPacksBase.h" #include "../ResourceSet.h" +#include "../spells/ExternalCaster.h" VCMI_LIB_NAMESPACE_BEGIN @@ -169,6 +170,9 @@ public: std::vector artifacts; std::vector spells; std::vector creatures; + + /// actions that hero may execute and object caster. Pair of spellID and school level + std::pair spellCast; /// list of components that will be added to reward description. First entry in list will override displayed component std::vector extraComponents; @@ -191,7 +195,8 @@ public: movePoints(0), movePercentage(-1), primary(4, 0), - removeObject(false) + removeObject(false), + spellCast(SpellID::NONE, SecSkillLevel::NONE) {} template void serialize(Handler &h, const int version) @@ -213,6 +218,8 @@ public: h & spells; h & creatures; h & creaturesChange; + if(version >= 821) + h & spellCast; } }; @@ -317,6 +324,9 @@ protected: bool wasVisitedBefore(const CGHeroInstance * contextHero) const; bool onceVisitableObjectCleared; + + /// caster to cast adveture spells + mutable spells::ExternalCaster caster; public: EVisitMode getVisitMode() const; diff --git a/lib/mapObjects/JsonRandom.h b/lib/mapObjects/JsonRandom.h index ee0dd6299..1f323fa20 100644 --- a/lib/mapObjects/JsonRandom.h +++ b/lib/mapObjects/JsonRandom.h @@ -32,7 +32,7 @@ namespace JsonRandom }; 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 & valuesSet); + DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet = {}); DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng); @@ -41,8 +41,8 @@ namespace JsonRandom DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells); - DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells); + DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells = {}); + DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells = {}); DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index a00b26502..d7c0b7043 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -37,27 +37,28 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad return false; } - const CGHeroInstance * caster = parameters.caster; - - if(caster->inTownGarrison) + if(const CGHeroInstance * heroCaster = dynamic_cast(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); @@ -82,7 +83,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron for(const Bonus & b : bonuses) { GiveBonus gb; - gb.id = parameters.caster->id.getNum(); + gb.id = parameters.caster->getCasterUnitId(); gb.bonus = b; env->apply(&gb); } @@ -105,7 +106,7 @@ ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env, void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { AdvmapSpellCast asc; - asc.casterID = parameters.caster->id; + asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId()); asc.spellID = owner->id; env->apply(&asc); @@ -121,13 +122,7 @@ void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const Adventur switch(result) { 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; default: break; @@ -142,21 +137,28 @@ SummonBoatMechanics::SummonBoatMechanics(const CSpell * s): 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; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); 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); return ESpellCastResult::CANCEL; } - int3 summonPos = parameters.caster->bestLocation(); + int3 summonPos = parameters.caster->getHeroCaster()->bestLocation(); + if(summonPos.x < 0) { 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. env->apply(&iw); 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 { 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.addReplacement(parameters.caster->getNameTranslated()); + parameters.caster->getCasterName(iw.text); env->apply(&iw); return ESpellCastResult::OK; } @@ -186,7 +188,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment if(b->hero) 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 { nearest = b; @@ -205,7 +207,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment else if(schoolLevel < 2) //none or basic level -> cannot create boat :( { 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. env->apply(&iw); } @@ -213,7 +215,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment { NewObject no; no.ID = Obj::BOAT; - no.subID = parameters.caster->getBoatType(); + no.subID = parameters.caster->getHeroCaster()->getBoatType(); no.pos = summonPos + int3(1,0,0); 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 { 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.addReplacement(parameters.caster->getNameTranslated()); + parameters.caster->getCasterName(iw.text); env->apply(&iw); return ESpellCastResult::OK; } @@ -273,9 +275,15 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm env->complain("Destination is out of map!"); 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 * curr = env->getCb()->getTile(parameters.caster->getSightCenter()); + const TerrainTile * curr = env->getCb()->getTile(parameters.caster->getHeroCaster()->getSightCenter()); if(nullptr == dest) { @@ -289,7 +297,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm 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!"); return ESpellCastResult::ERROR; @@ -301,34 +309,34 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm std::stringstream cachingStr; 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; - 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.addReplacement(parameters.caster->getNameTranslated()); + parameters.caster->getCasterName(iw.text); env->apply(&iw); return ESpellCastResult::CANCEL; } 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); env->apply(&gb); if(!dest->isClear(curr)) //wrong dest tile { InfoWindow iw; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed! 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; - smp.hid = parameters.caster->id; - if(movementCost < static_cast(parameters.caster->movement)) - smp.val = parameters.caster->movement - movementCost; + smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); + if(movementCost < static_cast(parameters.caster->getHeroCaster()->movement)) + smp.val = parameters.caster->getHeroCaster()->movement - movementCost; else smp.val = 0; env->apply(&smp); @@ -346,6 +354,12 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment { const CGTownInstance * destination = nullptr; const int moveCost = movementCost(parameters); + + if(!parameters.caster->getHeroCaster()) + { + env->complain("Not a hero caster!"); + return ESpellCastResult::ERROR; + } if(parameters.caster->getSpellSchoolLevel(owner) < 2) { @@ -355,13 +369,13 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment if(nullptr == destination) return ESpellCastResult::ERROR; - if(static_cast(parameters.caster->movement) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movement) < moveCost) return ESpellCastResult::ERROR; if(destination->visitingHero) { InfoWindow iw; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); iw.text.addTxt(MetaString::GENERAL_TXT, 123); env->apply(&iw); return ESpellCastResult::CANCEL; @@ -397,7 +411,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment 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) { @@ -405,7 +419,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment return ESpellCastResult::ERROR; } - if(static_cast(parameters.caster->movement) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movement) < moveCost) { env->complain("This hero has not enough movement points!"); return ESpellCastResult::ERROR; @@ -423,11 +437,11 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment 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; - smp.hid = parameters.caster->id; - smp.val = std::max(0, parameters.caster->movement - moveCost); + smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); + smp.val = std::max(0, parameters.caster->getHeroCaster()->movement - moveCost); env->apply(&smp); } return ESpellCastResult::OK; @@ -436,11 +450,17 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { std::vector towns = getPossibleTowns(env, parameters); + + if(!parameters.caster->getHeroCaster()) + { + env->complain("Not a hero caster!"); + return ESpellCastResult::ERROR; + } if(towns.empty()) { InfoWindow iw; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); iw.text.addTxt(MetaString::GENERAL_TXT, 124); env->apply(&iw); return ESpellCastResult::CANCEL; @@ -448,10 +468,10 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons const int moveCost = movementCost(parameters); - if(static_cast(parameters.caster->movement) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movement) < moveCost) { InfoWindow iw; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); iw.text.addTxt(MetaString::GENERAL_TXT, 125); env->apply(&iw); return ESpellCastResult::CANCEL; @@ -496,13 +516,13 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons if(request.objects.empty()) { InfoWindow iw; - iw.player = parameters.caster->tempOwner; + iw.player = parameters.caster->getCasterOwner(); iw.text.addTxt(MetaString::GENERAL_TXT, 124); env->apply(&iw); return ESpellCastResult::CANCEL; } - request.player = parameters.caster->getOwner(); + request.player = parameters.caster->getCasterOwner(); request.title.addTxt(MetaString::JK_TXT, 40); request.description.addTxt(MetaString::JK_TXT, 41); request.icon.id = Component::EComponentType::SPELL; @@ -520,13 +540,16 @@ const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment { if(pool.empty()) return nullptr; + + if(!parameters.caster->getHeroCaster()) + return nullptr; 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) { - si32 curDist = (*i)->pos.dist2dSQ(parameters.caster->pos); + si32 curDist = (*i)->pos.dist2dSQ(parameters.caster->getHeroCaster()->pos); if(curDist < dist) { @@ -541,7 +564,7 @@ std::vector TownPortalMechanics::getPossibleTowns(SpellC { std::vector 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) { @@ -555,6 +578,9 @@ std::vector TownPortalMechanics::getPossibleTowns(SpellC 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); } @@ -568,11 +594,11 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env { ShowWorldViewEx pack; - pack.player = parameters.caster->getOwner(); + pack.player = parameters.caster->getCasterOwner(); 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) { diff --git a/lib/spells/ExternalCaster.cpp b/lib/spells/ExternalCaster.cpp new file mode 100644 index 000000000..a48735bb9 --- /dev/null +++ b/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 diff --git a/lib/spells/ExternalCaster.h b/lib/spells/ExternalCaster.h new file mode 100644 index 000000000..22e91fcb1 --- /dev/null +++ b/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 diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 851653254..4d6f828e6 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -350,7 +350,7 @@ public: class DLL_LINKAGE AdventureSpellCastParameters { public: - const CGHeroInstance * caster; + const spells::Caster * caster; int3 pos; }; diff --git a/lib/spells/ProxyCaster.cpp b/lib/spells/ProxyCaster.cpp index cc21d94db..1b72b3d34 100644 --- a/lib/spells/ProxyCaster.cpp +++ b/lib/spells/ProxyCaster.cpp @@ -118,6 +118,14 @@ void ProxyCaster::spendMana(ServerCallback * server, const int32_t spellCost) co actualCaster->spendMana(server, spellCost); } +const CGHeroInstance * ProxyCaster::getHeroCaster() const +{ + if(actualCaster) + return actualCaster->getHeroCaster(); + + return nullptr; +} + } VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/ProxyCaster.h b/lib/spells/ProxyCaster.h index 043cd15d2..da1422caa 100644 --- a/lib/spells/ProxyCaster.h +++ b/lib/spells/ProxyCaster.h @@ -35,6 +35,7 @@ public: void getCasterName(MetaString & text) const override; void getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; + const CGHeroInstance * getHeroCaster() const override; protected: const Caster * actualCaster; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 0e5816367..98dfb7c2f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -6299,6 +6299,19 @@ bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst, 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) { if(!sl1.army->hasStackAtSlot(sl1.slot)) @@ -7360,4 +7373,4 @@ const ObjectInstanceID CGameHandler::putNewObject(Obj ID, int subID, int3 pos) no.pos = pos; sendAndApply(&no); return no.id; //id field will be filled during applying on gs -} \ No newline at end of file +} diff --git a/server/CGameHandler.h b/server/CGameHandler.h index edd245a22..75c507001 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -200,6 +200,8 @@ public: void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; void changeFogOfWar(std::unordered_set &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; void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; @@ -276,7 +278,6 @@ public: void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); const ObjectInstanceID putNewObject(Obj ID, int subID, int3 pos); - template void serialize(Handler &h, const int version) { h & QID;