diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9405dfe8d..5947d1b94 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -251,7 +251,6 @@ set(lib_MAIN_SRCS serializer/SerializerReflection.cpp spells/AbilityCaster.cpp - spells/AdventureSpellMechanics.cpp spells/BattleSpellMechanics.cpp spells/BonusCaster.cpp spells/CSpellHandler.cpp @@ -264,6 +263,13 @@ set(lib_MAIN_SRCS spells/TargetCondition.cpp spells/ViewSpellInt.cpp + spells/adventure/AdventureSpellMechanics.cpp + spells/adventure/DimensionDoorMechanics.cpp + spells/adventure/ScuttleBoatMechanics.cpp + spells/adventure/SummonBoatMechanics.cpp + spells/adventure/TownPortalMechanics.cpp + spells/adventure/ViewWorldMechanics.cpp + spells/effects/Catapult.cpp spells/effects/Clone.cpp spells/effects/Damage.cpp @@ -700,7 +706,6 @@ set(lib_MAIN_HEADERS serializer/SerializerReflection.h spells/AbilityCaster.h - spells/AdventureSpellMechanics.h spells/BattleSpellMechanics.h spells/BonusCaster.h spells/CSpellHandler.h @@ -713,6 +718,13 @@ set(lib_MAIN_HEADERS spells/TargetCondition.h spells/ViewSpellInt.h + spells/adventure/AdventureSpellMechanics.h + spells/adventure/DimensionDoorMechanics.h + spells/adventure/ScuttleBoatMechanics.h + spells/adventure/SummonBoatMechanics.h + spells/adventure/TownPortalMechanics.h + spells/adventure/ViewWorldMechanics.h + spells/effects/Catapult.h spells/effects/Clone.h spells/effects/Damage.h diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp deleted file mode 100644 index e57f7c3b0..000000000 --- a/lib/spells/AdventureSpellMechanics.cpp +++ /dev/null @@ -1,782 +0,0 @@ -/* - * AdventureSpellMechanics.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 "AdventureSpellMechanics.h" - -#include "CSpellHandler.h" -#include "Problem.h" - -#include "../CPlayerState.h" -#include "../IGameSettings.h" -#include "../callback/IGameInfoCallback.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../mapObjects/CGTownInstance.h" -#include "../mapObjects/MiscObjects.h" -#include "../mapping/CMap.h" -#include "../networkPacks/PacksForClient.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -///AdventureSpellMechanics -AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s): - IAdventureSpellMechanics(s) -{ -} - -bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const -{ - if(!owner->isAdventure()) - return false; - - const auto * heroCaster = dynamic_cast(caster); - - if (heroCaster) - { - if(heroCaster->isGarrisoned()) - return false; - - const auto level = heroCaster->getSpellSchoolLevel(owner); - const auto cost = owner->getCost(level); - - if(!heroCaster->canCastThisSpell(owner)) - return false; - - if(heroCaster->mana < cost) - return false; - } - - return canBeCastImpl(problem, cb, caster); -} - -bool AdventureSpellMechanics::canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const -{ - return canBeCast(problem, cb, caster) && canBeCastAtImpl(problem, cb, caster, pos); -} - -bool AdventureSpellMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const -{ - return true; -} - -bool AdventureSpellMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const -{ - return true; -} - -bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - spells::detail::ProblemImpl problem; - - if (!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos)) - return false; - - ESpellCastResult result = beginCast(env, parameters); - - if(result == ESpellCastResult::OK) - performCast(env, parameters); - - return result != ESpellCastResult::ERROR; -} - -ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - if(owner->hasEffects()) - { - //todo: cumulative effects support - const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - - std::vector bonuses; - - owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner)); - - for(const Bonus & b : bonuses) - { - GiveBonus gb; - gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); - gb.bonus = b; - env->apply(gb); - } - - return ESpellCastResult::OK; - } - else - { - //There is no generic algorithm of adventure cast - env->complain("Unimplemented adventure spell"); - return ESpellCastResult::ERROR; - } -} - -ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - return ESpellCastResult::OK; -} - -void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - // no-op, only for implementation in derived classes -} - -void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const auto level = parameters.caster->getSpellSchoolLevel(owner); - const auto cost = owner->getCost(level); - - AdvmapSpellCast asc; - asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId()); - asc.spellID = owner->id; - env->apply(asc); - - ESpellCastResult result = applyAdventureEffects(env, parameters); - - switch(result) - { - case ESpellCastResult::OK: - parameters.caster->spendMana(env, cost); - endCast(env, parameters); - break; - default: - break; - } -} - -///SummonBoatMechanics -SummonBoatMechanics::SummonBoatMechanics(const CSpell * s): - AdventureSpellMechanics(s) -{ -} - -bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const -{ - if(!caster->getHeroCaster()) - return false; - - if(caster->getHeroCaster()->inBoat()) - { - MetaString message = MetaString::createFromTextID("core.genrltxt.333"); - caster->getCasterName(message); - problem.add(std::move(message)); - return false; - } - - int3 summonPos = caster->getHeroCaster()->bestLocation(); - - if(summonPos.x < 0) - { - MetaString message = MetaString::createFromTextID("core.genrltxt.334"); - caster->getCasterName(message); - problem.add(std::move(message)); - return false; - } - - return true; -} - -ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - - //check if spell works at all - if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. - parameters.caster->getCasterName(iw.text); - env->apply(iw); - return ESpellCastResult::OK; - } - - //try to find unoccupied boat to summon - const CGBoat * nearest = nullptr; - double dist = 0; - for(const auto & b : env->getMap()->getObjects()) - { - if(b->getBoardedHero() || b->layer != EPathfindingLayer::SAIL) - continue; //we're looking for unoccupied boat - - double nDist = b->visitablePos().dist2d(parameters.caster->getHeroCaster()->visitablePos()); - if(!nearest || nDist < dist) //it's first boat or closer than previous - { - nearest = b; - dist = nDist; - } - } - - int3 summonPos = parameters.caster->getHeroCaster()->bestLocation(); - - if(nullptr != nearest) //we found boat to summon - { - ChangeObjPos cop; - cop.objid = nearest->id; - cop.nPos = summonPos; - cop.initiator = parameters.caster->getCasterOwner(); - env->apply(cop); - } - else if(schoolLevel < 2) //none or basic level -> cannot create boat :( - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon. - env->apply(iw); - return ESpellCastResult::ERROR; - } - else //create boat - { - env->createBoat(summonPos, BoatId::NECROPOLIS, parameters.caster->getCasterOwner()); - } - return ESpellCastResult::OK; -} - -///ScuttleBoatMechanics -ScuttleBoatMechanics::ScuttleBoatMechanics(const CSpell * s): - AdventureSpellMechanics(s) -{ -} - -bool ScuttleBoatMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const -{ - if(!cb->isInTheMap(pos)) - return false; - - if (caster->getHeroCaster()) - { - int3 casterPosition = caster->getHeroCaster()->getSightCenter(); - - if(!isInScreenRange(casterPosition, pos)) - return false; - } - - if(!cb->isVisibleFor(pos, caster->getCasterOwner())) - return false; - - const TerrainTile * t = cb->getTile(pos); - if(!t || t->visitableObjects.empty()) - return false; - - const CGObjectInstance * topObject = cb->getObj(t->visitableObjects.back()); - if (topObject->ID != Obj::BOAT) - return false; - - return true; -} - -ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - //check if spell works at all - if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed - parameters.caster->getCasterName(iw.text); - env->apply(iw); - return ESpellCastResult::OK; - } - - const TerrainTile & t = env->getMap()->getTile(parameters.pos); - - RemoveObject ro; - ro.initiator = parameters.caster->getCasterOwner(); - ro.objectID = t.visitableObjects.back(); - env->apply(ro); - return ESpellCastResult::OK; -} - -///DimensionDoorMechanics -DimensionDoorMechanics::DimensionDoorMechanics(const CSpell * s): - AdventureSpellMechanics(s) -{ -} - -bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const -{ - if(!caster->getHeroCaster()) - return false; - - if(caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough - { - problem.add(MetaString::createFromTextID("core.genrltxt.125")); - return false; - } - - const auto schoolLevel = caster->getSpellSchoolLevel(owner); - - std::stringstream cachingStr; - cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; - - int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), cachingStr.str())->size(); - int castsLimit = owner->getLevelPower(schoolLevel); - - bool isTournamentRulesLimitEnabled = cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT); - if(isTournamentRulesLimitEnabled) - { - int3 mapSize = cb->getMapSize(); - - bool meetsTournamentRulesTwoCastsRequirements = mapSize.x * mapSize.y * mapSize.z >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD - && schoolLevel == MasteryLevel::EXPERT; - - castsLimit = meetsTournamentRulesTwoCastsRequirements ? 2 : 1; - } - - if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn - { - MetaString message = MetaString::createFromTextID("core.genrltxt.338"); - caster->getCasterName(message); - problem.add(std::move(message)); - return false; - } - - return true; -} - -bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const -{ - if(!cb->isInTheMap(pos)) - return false; - - if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) - { - if(!cb->isVisibleFor(pos, caster->getCasterOwner())) - return false; - } - - int3 casterPosition = caster->getHeroCaster()->getSightCenter(); - - const TerrainTile * dest = cb->getTileUnchecked(pos); - const TerrainTile * curr = cb->getTileUnchecked(casterPosition); - - if(!dest) - return false; - - if(!curr) - return false; - - if(!isInScreenRange(casterPosition, pos)) - return false; - - if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE)) - { - if(!dest->isClear(curr)) - return false; - } - else - { - if (dest->blocked()) - return false; - } - - return true; -} - -ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - const int baseCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); - const int movementCost = baseCost * ((schoolLevel >= 3) ? 2 : 3); - - int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); - const TerrainTile * dest = env->getCb()->getTile(parameters.pos); - const TerrainTile * curr = env->getCb()->getTile(casterPosition); - - if(!dest->isClear(curr)) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - - // tile is either blocked or not possible to move (e.g. water <-> land) - if(env->getCb()->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS)) - { - // SOD: DD to such "wrong" terrain results in mana and move points spending, but fails to move hero - iw.text = MetaString::createFromTextID("core.genrltxt.70"); // Dimension Door failed! - env->apply(iw); - // no return - resources will be spent - } - else - { - // HotA: game will show error message without taking mana or move points, even when DD into terra incognita - iw.text = MetaString::createFromTextID("vcmi.dimensionDoor.seaToLandError"); - env->apply(iw); - return ESpellCastResult::CANCEL; - } - } - - GiveBonus gb; - gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); - gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); - env->apply(gb); - - SetMovePoints smp; - smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); - if(movementCost < static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining())) - smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementCost; - else - smp.val = 0; - env->apply(smp); - - return ESpellCastResult::OK; -} - -void DimensionDoorMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); - const TerrainTile * dest = env->getCb()->getTile(parameters.pos); - const TerrainTile * curr = env->getCb()->getTile(casterPosition); - - if(dest->isClear(curr)) - env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), EMovementMode::DIMENSION_DOOR); -} - -///TownPortalMechanics -TownPortalMechanics::TownPortalMechanics(const CSpell * s): - AdventureSpellMechanics(s) -{ -} - -ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const CGTownInstance * destination = nullptr; - const int moveCost = movementCost(env, parameters); - - if(!parameters.caster->getHeroCaster()) - { - env->complain("Not a hero caster!"); - return ESpellCastResult::ERROR; - } - - if(parameters.caster->getSpellSchoolLevel(owner) < 2) - { - std::vector pool = getPossibleTowns(env, parameters); - destination = findNearestTown(env, parameters, pool); - - if(nullptr == destination) - return ESpellCastResult::ERROR; - - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) - return ESpellCastResult::ERROR; - - if(destination->getVisitingHero()) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 123); - env->apply(iw); - return ESpellCastResult::CANCEL; - } - } - else if(env->getMap()->isInTheMap(parameters.pos)) - { - const TerrainTile & tile = env->getMap()->getTile(parameters.pos); - - ObjectInstanceID topObjID = tile.topVisitableObj(false); - const CGObjectInstance * topObj = env->getMap()->getObject(topObjID); - - if(!topObj) - { - env->complain("Destination tile is not visitable" + parameters.pos.toString()); - return ESpellCastResult::ERROR; - } - else if(topObj->ID == Obj::HERO) - { - env->complain("Can't teleport to occupied town at " + parameters.pos.toString()); - return ESpellCastResult::ERROR; - } - else if(topObj->ID != Obj::TOWN) - { - env->complain("No town at destination tile " + parameters.pos.toString()); - return ESpellCastResult::ERROR; - } - - destination = dynamic_cast(topObj); - - if(nullptr == destination) - { - env->complain("[Internal error] invalid town object at " + parameters.pos.toString()); - return ESpellCastResult::ERROR; - } - - const auto relations = env->getCb()->getPlayerRelations(destination->tempOwner, parameters.caster->getCasterOwner()); - - if(relations == PlayerRelations::ENEMIES) - { - env->complain("Can't teleport to enemy!"); - return ESpellCastResult::ERROR; - } - - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) - { - env->complain("This hero has not enough movement points!"); - return ESpellCastResult::ERROR; - } - - if(destination->getVisitingHero()) - { - env->complain("[Internal error] Can't teleport to occupied town"); - return ESpellCastResult::ERROR; - } - } - else - { - env->complain("Invalid destination tile"); - return ESpellCastResult::ERROR; - } - - const TerrainTile & from = env->getMap()->getTile(parameters.caster->getHeroCaster()->visitablePos()); - const TerrainTile & dest = env->getMap()->getTile(destination->visitablePos()); - - if(!dest.entrableTerrain(&from)) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 135); - env->apply(iw); - return ESpellCastResult::ERROR; - } - - return ESpellCastResult::OK; -} - -void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - const int moveCost = movementCost(env, parameters); - const CGTownInstance * destination = nullptr; - - if(parameters.caster->getSpellSchoolLevel(owner) < 2) - { - std::vector pool = getPossibleTowns(env, parameters); - destination = findNearestTown(env, parameters, pool); - } - else - { - const TerrainTile & tile = env->getMap()->getTile(parameters.pos); - ObjectInstanceID topObjID = tile.topVisitableObj(false); - const CGObjectInstance * topObj = env->getMap()->getObject(topObjID); - - destination = dynamic_cast(topObj); - } - - if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(destination->visitablePos()), EMovementMode::TOWN_PORTAL)) - { - SetMovePoints smp; - smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); - smp.val = std::max(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost); - env->apply(smp); - } -} - -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->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); - env->apply(iw); - return ESpellCastResult::CANCEL; - } - - const int moveCost = movementCost(env, parameters); - - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 125); - env->apply(iw); - return ESpellCastResult::CANCEL; - } - - if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) - { - auto queryCallback = [this, env, parameters](std::optional reply) -> void - { - if(reply.has_value()) - { - ObjectInstanceID townId(*reply); - - const CGObjectInstance * o = env->getCb()->getObj(townId, true); - if(o == nullptr) - { - env->complain("Invalid object instance selected"); - return; - } - - if(!dynamic_cast(o)) - { - env->complain("Object instance is not town"); - return; - } - - AdventureSpellCastParameters p; - p.caster = parameters.caster; - p.pos = o->visitablePos(); - performCast(env, p); - } - }; - - MapObjectSelectDialog request; - - for(const auto * t : towns) - { - if(t->getVisitingHero() == nullptr) //empty town - request.objects.push_back(t->id); - } - - if(request.objects.empty()) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); - env->apply(iw); - return ESpellCastResult::CANCEL; - } - - request.player = parameters.caster->getCasterOwner(); - request.title.appendLocalString(EMetaText::JK_TXT, 40); - request.description.appendLocalString(EMetaText::JK_TXT, 41); - request.icon = Component(ComponentType::SPELL, owner->id); - - env->genericQuery(&request, request.player, queryCallback); - - return ESpellCastResult::PENDING; - } - - return ESpellCastResult::OK; -} - -const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const -{ - if(pool.empty()) - return nullptr; - - if(!parameters.caster->getHeroCaster()) - return nullptr; - - auto nearest = pool.cbegin(); //nearest town's iterator - si32 dist = (*nearest)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); - - for(auto i = nearest + 1; i != pool.cend(); ++i) - { - si32 curDist = (*i)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); - - if(curDist < dist) - { - nearest = i; - dist = curDist; - } - } - return *nearest; -} - -std::vector TownPortalMechanics::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - std::vector ret; - - const TeamState * team = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner()); - - for(const auto & color : team->players) - { - for(auto currTown : env->getCb()->getPlayerState(color)->getTowns()) - { - ret.push_back(currTown); - } - } - return ret; -} - -int32_t TownPortalMechanics::movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - if(parameters.caster != parameters.caster->getHeroCaster()) //if caster is not hero - return 0; - - int baseMovementCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); - return baseMovementCost * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3); -} - -///ViewMechanics -ViewMechanics::ViewMechanics(const CSpell * s): - AdventureSpellMechanics(s) -{ -} - -ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const -{ - ShowWorldViewEx pack; - - pack.player = parameters.caster->getCasterOwner(); - - const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner); - - const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; - - for(const auto & obj : env->getMap()->getObjects()) - { - //deleted object remain as empty pointer - if(obj && filterObject(obj, spellLevel)) - { - ObjectPosInfo posInfo(obj); - - if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0) - pack.objectPositions.push_back(posInfo); - } - } - pack.showTerrain = showTerrain(spellLevel); - - env->apply(pack); - - return ESpellCastResult::OK; -} - -///ViewAirMechanics -ViewAirMechanics::ViewAirMechanics(const CSpell * s): - ViewMechanics(s) -{ -} - -bool ViewAirMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const -{ - return (obj->ID == Obj::ARTIFACT) || (spellLevel > 1 && obj->ID == Obj::HERO) || (spellLevel > 2 && obj->ID == Obj::TOWN); -} - -bool ViewAirMechanics::showTerrain(const int32_t spellLevel) const -{ - return false; -} - -///ViewEarthMechanics -ViewEarthMechanics::ViewEarthMechanics(const CSpell * s): - ViewMechanics(s) -{ -} - -bool ViewEarthMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const -{ - return (obj->ID == Obj::RESOURCE) || (spellLevel > 1 && obj->ID == Obj::MINE); -} - -bool ViewEarthMechanics::showTerrain(const int32_t spellLevel) const -{ - return spellLevel > 2; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/AdventureSpellMechanics.h b/lib/spells/AdventureSpellMechanics.h deleted file mode 100644 index 247dcd9f4..000000000 --- a/lib/spells/AdventureSpellMechanics.h +++ /dev/null @@ -1,123 +0,0 @@ -/* - * AdventureSpellMechanics.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 "ISpellMechanics.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; - -enum class ESpellCastResult -{ - OK, // cast successful - CANCEL, // cast failed but it is not an error, no mana has been spent - PENDING, - ERROR// error occurred, for example invalid request from player -}; - -class AdventureSpellMechanics : public IAdventureSpellMechanics -{ -public: - AdventureSpellMechanics(const CSpell * s); - - bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final; - bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; - - bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override final; -protected: - ///actual adventure cast implementation - virtual ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; - virtual ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; - virtual void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; - virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const; - virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const; - - void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; -}; - -class SummonBoatMechanics final : public AdventureSpellMechanics -{ -public: - SummonBoatMechanics(const CSpell * s); -protected: - bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override; - - ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; -}; - -class ScuttleBoatMechanics final : public AdventureSpellMechanics -{ -public: - ScuttleBoatMechanics(const CSpell * s); -protected: - bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; - - ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; -}; - -class DimensionDoorMechanics final : public AdventureSpellMechanics -{ -public: - DimensionDoorMechanics(const CSpell * s); -protected: - bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override; - bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; - - ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; - void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; -}; - -class TownPortalMechanics final : public AdventureSpellMechanics -{ -public: - TownPortalMechanics(const CSpell * s); -protected: - ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; - ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; - void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; -private: - const CGTownInstance * findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const; - int32_t movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; - std::vector getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; -}; - -class ViewMechanics : public AdventureSpellMechanics -{ -public: - ViewMechanics(const CSpell * s); -protected: - ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; - virtual bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const = 0; - virtual bool showTerrain(const int32_t spellLevel) const = 0; -}; - -class ViewAirMechanics final : public ViewMechanics -{ -public: - ViewAirMechanics(const CSpell * s); -protected: - bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override; - bool showTerrain(const int32_t spellLevel) const override; -}; - -class ViewEarthMechanics final : public ViewMechanics -{ -public: - ViewEarthMechanics(const CSpell * s); -protected: - bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override; - bool showTerrain(const int32_t spellLevel) const override; -}; - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index d337de05e..192cbe064 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -26,7 +26,13 @@ #include "TargetCondition.h" #include "Problem.h" -#include "AdventureSpellMechanics.h" +#include "adventure/AdventureSpellMechanics.h" +#include "adventure/DimensionDoorMechanics.h" +#include "adventure/ScuttleBoatMechanics.h" +#include "adventure/SummonBoatMechanics.h" +#include "adventure/TownPortalMechanics.h" +#include "adventure/ViewWorldMechanics.h" + #include "BattleSpellMechanics.h" #include "effects/Effects.h" diff --git a/lib/spells/adventure/AdventureSpellMechanics.cpp b/lib/spells/adventure/AdventureSpellMechanics.cpp new file mode 100644 index 000000000..f5d5f6a46 --- /dev/null +++ b/lib/spells/adventure/AdventureSpellMechanics.cpp @@ -0,0 +1,144 @@ +/* + * AdventureSpellMechanics.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 "AdventureSpellMechanics.h" + +#include "../CSpellHandler.h" +#include "../Problem.h" + +#include "../../mapObjects/CGHeroInstance.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s) + : IAdventureSpellMechanics(s) +{ +} + +bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +{ + if(!owner->isAdventure()) + return false; + + const auto * heroCaster = dynamic_cast(caster); + + if(heroCaster) + { + if(heroCaster->isGarrisoned()) + return false; + + const auto level = heroCaster->getSpellSchoolLevel(owner); + const auto cost = owner->getCost(level); + + if(!heroCaster->canCastThisSpell(owner)) + return false; + + if(heroCaster->mana < cost) + return false; + } + + return canBeCastImpl(problem, cb, caster); +} + +bool AdventureSpellMechanics::canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + return canBeCast(problem, cb, caster) && canBeCastAtImpl(problem, cb, caster, pos); +} + +bool AdventureSpellMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +{ + return true; +} + +bool AdventureSpellMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + return true; +} + +bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + spells::detail::ProblemImpl problem; + + if(!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos)) + return false; + + ESpellCastResult result = beginCast(env, parameters); + + if(result == ESpellCastResult::OK) + performCast(env, parameters); + + return result != ESpellCastResult::ERROR; +} + +ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + if(owner->hasEffects()) + { + //todo: cumulative effects support + const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); + + std::vector bonuses; + + owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner)); + + for(const Bonus & b : bonuses) + { + GiveBonus gb; + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); + gb.bonus = b; + env->apply(gb); + } + + return ESpellCastResult::OK; + } + else + { + //There is no generic algorithm of adventure cast + env->complain("Unimplemented adventure spell"); + return ESpellCastResult::ERROR; + } +} + +ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + return ESpellCastResult::OK; +} + +void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + // no-op, only for implementation in derived classes +} + +void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const auto level = parameters.caster->getSpellSchoolLevel(owner); + const auto cost = owner->getCost(level); + + AdvmapSpellCast asc; + asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId()); + asc.spellID = owner->id; + env->apply(asc); + + ESpellCastResult result = applyAdventureEffects(env, parameters); + + switch(result) + { + case ESpellCastResult::OK: + parameters.caster->spendMana(env, cost); + endCast(env, parameters); + break; + default: + break; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/AdventureSpellMechanics.h b/lib/spells/adventure/AdventureSpellMechanics.h new file mode 100644 index 000000000..ae352272e --- /dev/null +++ b/lib/spells/adventure/AdventureSpellMechanics.h @@ -0,0 +1,46 @@ +/* + * AdventureSpellMechanics.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 "../ISpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +enum class ESpellCastResult +{ + OK, // cast successful + CANCEL, // cast failed but it is not an error, no mana has been spent + PENDING, + ERROR // error occurred, for example invalid request from player +}; + +class AdventureSpellMechanics : public IAdventureSpellMechanics +{ +public: + AdventureSpellMechanics(const CSpell * s); + + bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final; + bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; + + bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override final; + +protected: + ///actual adventure cast implementation + virtual ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + virtual ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + virtual void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const; + virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const; + + void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/DimensionDoorMechanics.cpp b/lib/spells/adventure/DimensionDoorMechanics.cpp new file mode 100644 index 000000000..6198c5ff8 --- /dev/null +++ b/lib/spells/adventure/DimensionDoorMechanics.cpp @@ -0,0 +1,166 @@ +/* + * DimensionDoorMechanics.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 "DimensionDoorMechanics.h" + +#include "../CSpellHandler.h" + +#include "../../IGameSettings.h" +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/CMapDefines.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +DimensionDoorMechanics::DimensionDoorMechanics(const CSpell * s) + : AdventureSpellMechanics(s) +{ +} + +bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +{ + if(!caster->getHeroCaster()) + return false; + + if(caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough + { + problem.add(MetaString::createFromTextID("core.genrltxt.125")); + return false; + } + + const auto schoolLevel = caster->getSpellSchoolLevel(owner); + + std::stringstream cachingStr; + cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; + + int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), cachingStr.str())->size(); + int castsLimit = owner->getLevelPower(schoolLevel); + + bool isTournamentRulesLimitEnabled = cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT); + if(isTournamentRulesLimitEnabled) + { + int3 mapSize = cb->getMapSize(); + + bool meetsTournamentRulesTwoCastsRequirements = mapSize.x * mapSize.y * mapSize.z >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD && schoolLevel == MasteryLevel::EXPERT; + + castsLimit = meetsTournamentRulesTwoCastsRequirements ? 2 : 1; + } + + if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn + { + MetaString message = MetaString::createFromTextID("core.genrltxt.338"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; + } + + return true; +} + +bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if(!cb->isInTheMap(pos)) + return false; + + if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) + { + if(!cb->isVisibleFor(pos, caster->getCasterOwner())) + return false; + } + + int3 casterPosition = caster->getHeroCaster()->getSightCenter(); + + const TerrainTile * dest = cb->getTileUnchecked(pos); + const TerrainTile * curr = cb->getTileUnchecked(casterPosition); + + if(!dest) + return false; + + if(!curr) + return false; + + if(!isInScreenRange(casterPosition, pos)) + return false; + + if(cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE)) + { + if(!dest->isClear(curr)) + return false; + } + else + { + if(dest->blocked()) + return false; + } + + return true; +} + +ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); + const int baseCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); + const int movementCost = baseCost * ((schoolLevel >= 3) ? 2 : 3); + + int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); + const TerrainTile * dest = env->getCb()->getTile(parameters.pos); + const TerrainTile * curr = env->getCb()->getTile(casterPosition); + + if(!dest->isClear(curr)) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + + // tile is either blocked or not possible to move (e.g. water <-> land) + if(env->getCb()->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS)) + { + // SOD: DD to such "wrong" terrain results in mana and move points spending, but fails to move hero + iw.text = MetaString::createFromTextID("core.genrltxt.70"); // Dimension Door failed! + env->apply(iw); + // no return - resources will be spent + } + else + { + // HotA: game will show error message without taking mana or move points, even when DD into terra incognita + iw.text = MetaString::createFromTextID("vcmi.dimensionDoor.seaToLandError"); + env->apply(iw); + return ESpellCastResult::CANCEL; + } + } + + GiveBonus gb; + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); + gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); + env->apply(gb); + + SetMovePoints smp; + smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); + if(movementCost < static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining())) + smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementCost; + else + smp.val = 0; + env->apply(smp); + + return ESpellCastResult::OK; +} + +void DimensionDoorMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); + const TerrainTile * dest = env->getCb()->getTile(parameters.pos); + const TerrainTile * curr = env->getCb()->getTile(casterPosition); + + if(dest->isClear(curr)) + env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), EMovementMode::DIMENSION_DOOR); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/DimensionDoorMechanics.h b/lib/spells/adventure/DimensionDoorMechanics.h new file mode 100644 index 000000000..cf5e32b6f --- /dev/null +++ b/lib/spells/adventure/DimensionDoorMechanics.h @@ -0,0 +1,30 @@ +/* + * DimensionDoorMechanics.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 "AdventureSpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DimensionDoorMechanics final : public AdventureSpellMechanics +{ +public: + DimensionDoorMechanics(const CSpell * s); + +protected: + bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override; + bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; + + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/ScuttleBoatMechanics.cpp b/lib/spells/adventure/ScuttleBoatMechanics.cpp new file mode 100644 index 000000000..24b8bfe8b --- /dev/null +++ b/lib/spells/adventure/ScuttleBoatMechanics.cpp @@ -0,0 +1,78 @@ +/* + * ScuttleBoatMechanics.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 "ScuttleBoatMechanics.h" + +#include "../CSpellHandler.h" + +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/CMap.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ScuttleBoatMechanics::ScuttleBoatMechanics(const CSpell * s) + : AdventureSpellMechanics(s) +{ +} + +bool ScuttleBoatMechanics::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if(!cb->isInTheMap(pos)) + return false; + + if(caster->getHeroCaster()) + { + int3 casterPosition = caster->getHeroCaster()->getSightCenter(); + + if(!isInScreenRange(casterPosition, pos)) + return false; + } + + if(!cb->isVisibleFor(pos, caster->getCasterOwner())) + return false; + + const TerrainTile * t = cb->getTile(pos); + if(!t || t->visitableObjects.empty()) + return false; + + const CGObjectInstance * topObject = cb->getObj(t->visitableObjects.back()); + if(topObject->ID != Obj::BOAT) + return false; + + return true; +} + +ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); + //check if spell works at all + if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed + parameters.caster->getCasterName(iw.text); + env->apply(iw); + return ESpellCastResult::OK; + } + + const TerrainTile & t = env->getMap()->getTile(parameters.pos); + + RemoveObject ro; + ro.initiator = parameters.caster->getCasterOwner(); + ro.objectID = t.visitableObjects.back(); + env->apply(ro); + return ESpellCastResult::OK; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/ScuttleBoatMechanics.h b/lib/spells/adventure/ScuttleBoatMechanics.h new file mode 100644 index 000000000..dec0b4037 --- /dev/null +++ b/lib/spells/adventure/ScuttleBoatMechanics.h @@ -0,0 +1,28 @@ +/* + * ScuttleBoatMechanics.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 "AdventureSpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ScuttleBoatMechanics final : public AdventureSpellMechanics +{ +public: + ScuttleBoatMechanics(const CSpell * s); + +protected: + bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; + + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/SummonBoatMechanics.cpp b/lib/spells/adventure/SummonBoatMechanics.cpp new file mode 100644 index 000000000..6d4a0341d --- /dev/null +++ b/lib/spells/adventure/SummonBoatMechanics.cpp @@ -0,0 +1,111 @@ +/* + * SummonBoatMechanics.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 "SummonBoatMechanics.h" + +#include "../CSpellHandler.h" + +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapObjects/MiscObjects.h" +#include "../../mapping/CMap.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +SummonBoatMechanics::SummonBoatMechanics(const CSpell * s) + : AdventureSpellMechanics(s) +{ +} + +bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +{ + if(!caster->getHeroCaster()) + return false; + + if(caster->getHeroCaster()->inBoat()) + { + MetaString message = MetaString::createFromTextID("core.genrltxt.333"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; + } + + int3 summonPos = caster->getHeroCaster()->bestLocation(); + + if(summonPos.x < 0) + { + MetaString message = MetaString::createFromTextID("core.genrltxt.334"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; + } + + return true; +} + +ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); + + //check if spell works at all + if(env->getRNG()->nextInt(0, 99) >= owner->getLevelPower(schoolLevel)) //power is % chance of success + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 336); //%s tried to summon a boat, but failed. + parameters.caster->getCasterName(iw.text); + env->apply(iw); + return ESpellCastResult::OK; + } + + //try to find unoccupied boat to summon + const CGBoat * nearest = nullptr; + double dist = 0; + for(const auto & b : env->getMap()->getObjects()) + { + if(b->getBoardedHero() || b->layer != EPathfindingLayer::SAIL) + continue; //we're looking for unoccupied boat + + double nDist = b->visitablePos().dist2d(parameters.caster->getHeroCaster()->visitablePos()); + if(!nearest || nDist < dist) //it's first boat or closer than previous + { + nearest = b; + dist = nDist; + } + } + + int3 summonPos = parameters.caster->getHeroCaster()->bestLocation(); + + if(nullptr != nearest) //we found boat to summon + { + ChangeObjPos cop; + cop.objid = nearest->id; + cop.nPos = summonPos; + cop.initiator = parameters.caster->getCasterOwner(); + env->apply(cop); + } + else if(schoolLevel < 2) //none or basic level -> cannot create boat :( + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon. + env->apply(iw); + return ESpellCastResult::ERROR; + } + else //create boat + { + env->createBoat(summonPos, BoatId::NECROPOLIS, parameters.caster->getCasterOwner()); + } + return ESpellCastResult::OK; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/SummonBoatMechanics.h b/lib/spells/adventure/SummonBoatMechanics.h new file mode 100644 index 000000000..553abaa9c --- /dev/null +++ b/lib/spells/adventure/SummonBoatMechanics.h @@ -0,0 +1,28 @@ +/* + * SummonBoatMechanics.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 "AdventureSpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class SummonBoatMechanics final : public AdventureSpellMechanics +{ +public: + SummonBoatMechanics(const CSpell * s); + +protected: + bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override; + + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/TownPortalMechanics.cpp b/lib/spells/adventure/TownPortalMechanics.cpp new file mode 100644 index 000000000..e67df29d7 --- /dev/null +++ b/lib/spells/adventure/TownPortalMechanics.cpp @@ -0,0 +1,299 @@ +/* + * TownPortalMechanics.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 "TownPortalMechanics.h" + +#include "../CSpellHandler.h" + +#include "../../CPlayerState.h" +#include "../../IGameSettings.h" +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapObjects/CGTownInstance.h" +#include "../../mapping/CMap.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +TownPortalMechanics::TownPortalMechanics(const CSpell * s): + AdventureSpellMechanics(s) +{ +} + +ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const CGTownInstance * destination = nullptr; + const int moveCost = movementCost(env, parameters); + + if(!parameters.caster->getHeroCaster()) + { + env->complain("Not a hero caster!"); + return ESpellCastResult::ERROR; + } + + if(parameters.caster->getSpellSchoolLevel(owner) < 2) + { + std::vector pool = getPossibleTowns(env, parameters); + destination = findNearestTown(env, parameters, pool); + + if(nullptr == destination) + return ESpellCastResult::ERROR; + + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + return ESpellCastResult::ERROR; + + if(destination->getVisitingHero()) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 123); + env->apply(iw); + return ESpellCastResult::CANCEL; + } + } + else if(env->getMap()->isInTheMap(parameters.pos)) + { + const TerrainTile & tile = env->getMap()->getTile(parameters.pos); + + ObjectInstanceID topObjID = tile.topVisitableObj(false); + const CGObjectInstance * topObj = env->getMap()->getObject(topObjID); + + if(!topObj) + { + env->complain("Destination tile is not visitable" + parameters.pos.toString()); + return ESpellCastResult::ERROR; + } + else if(topObj->ID == Obj::HERO) + { + env->complain("Can't teleport to occupied town at " + parameters.pos.toString()); + return ESpellCastResult::ERROR; + } + else if(topObj->ID != Obj::TOWN) + { + env->complain("No town at destination tile " + parameters.pos.toString()); + return ESpellCastResult::ERROR; + } + + destination = dynamic_cast(topObj); + + if(nullptr == destination) + { + env->complain("[Internal error] invalid town object at " + parameters.pos.toString()); + return ESpellCastResult::ERROR; + } + + const auto relations = env->getCb()->getPlayerRelations(destination->tempOwner, parameters.caster->getCasterOwner()); + + if(relations == PlayerRelations::ENEMIES) + { + env->complain("Can't teleport to enemy!"); + return ESpellCastResult::ERROR; + } + + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + { + env->complain("This hero has not enough movement points!"); + return ESpellCastResult::ERROR; + } + + if(destination->getVisitingHero()) + { + env->complain("[Internal error] Can't teleport to occupied town"); + return ESpellCastResult::ERROR; + } + } + else + { + env->complain("Invalid destination tile"); + return ESpellCastResult::ERROR; + } + + const TerrainTile & from = env->getMap()->getTile(parameters.caster->getHeroCaster()->visitablePos()); + const TerrainTile & dest = env->getMap()->getTile(destination->visitablePos()); + + if(!dest.entrableTerrain(&from)) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 135); + env->apply(iw); + return ESpellCastResult::ERROR; + } + + return ESpellCastResult::OK; +} + +void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const int moveCost = movementCost(env, parameters); + const CGTownInstance * destination = nullptr; + + if(parameters.caster->getSpellSchoolLevel(owner) < 2) + { + std::vector pool = getPossibleTowns(env, parameters); + destination = findNearestTown(env, parameters, pool); + } + else + { + const TerrainTile & tile = env->getMap()->getTile(parameters.pos); + ObjectInstanceID topObjID = tile.topVisitableObj(false); + const CGObjectInstance * topObj = env->getMap()->getObject(topObjID); + + destination = dynamic_cast(topObj); + } + + if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(destination->visitablePos()), EMovementMode::TOWN_PORTAL)) + { + SetMovePoints smp; + smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); + smp.val = std::max(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost); + env->apply(smp); + } +} + +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->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); + env->apply(iw); + return ESpellCastResult::CANCEL; + } + + const int moveCost = movementCost(env, parameters); + + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 125); + env->apply(iw); + return ESpellCastResult::CANCEL; + } + + if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) + { + auto queryCallback = [this, env, parameters](std::optional reply) -> void + { + if(reply.has_value()) + { + ObjectInstanceID townId(*reply); + + const CGObjectInstance * o = env->getCb()->getObj(townId, true); + if(o == nullptr) + { + env->complain("Invalid object instance selected"); + return; + } + + if(!dynamic_cast(o)) + { + env->complain("Object instance is not town"); + return; + } + + AdventureSpellCastParameters p; + p.caster = parameters.caster; + p.pos = o->visitablePos(); + performCast(env, p); + } + }; + + MapObjectSelectDialog request; + + for(const auto * t : towns) + { + if(t->getVisitingHero() == nullptr) //empty town + request.objects.push_back(t->id); + } + + if(request.objects.empty()) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124); + env->apply(iw); + return ESpellCastResult::CANCEL; + } + + request.player = parameters.caster->getCasterOwner(); + request.title.appendLocalString(EMetaText::JK_TXT, 40); + request.description.appendLocalString(EMetaText::JK_TXT, 41); + request.icon = Component(ComponentType::SPELL, owner->id); + + env->genericQuery(&request, request.player, queryCallback); + + return ESpellCastResult::PENDING; + } + + return ESpellCastResult::OK; +} + +const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const +{ + if(pool.empty()) + return nullptr; + + if(!parameters.caster->getHeroCaster()) + return nullptr; + + auto nearest = pool.cbegin(); //nearest town's iterator + si32 dist = (*nearest)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); + + for(auto i = nearest + 1; i != pool.cend(); ++i) + { + si32 curDist = (*i)->visitablePos().dist2dSQ(parameters.caster->getHeroCaster()->visitablePos()); + + if(curDist < dist) + { + nearest = i; + dist = curDist; + } + } + return *nearest; +} + +std::vector TownPortalMechanics::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + std::vector ret; + + const TeamState * team = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner()); + + for(const auto & color : team->players) + { + for(auto currTown : env->getCb()->getPlayerState(color)->getTowns()) + { + ret.push_back(currTown); + } + } + return ret; +} + +int32_t TownPortalMechanics::movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + if(parameters.caster != parameters.caster->getHeroCaster()) //if caster is not hero + return 0; + + int baseMovementCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); + return baseMovementCost * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/TownPortalMechanics.h b/lib/spells/adventure/TownPortalMechanics.h new file mode 100644 index 000000000..e0d227678 --- /dev/null +++ b/lib/spells/adventure/TownPortalMechanics.h @@ -0,0 +1,35 @@ +/* + * TownPortalMechanics.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 "AdventureSpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; + +class TownPortalMechanics final : public AdventureSpellMechanics +{ +public: + TownPortalMechanics(const CSpell * s); + +protected: + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + +private: + const CGTownInstance * findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const; + int32_t movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + std::vector getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/ViewWorldMechanics.cpp b/lib/spells/adventure/ViewWorldMechanics.cpp new file mode 100644 index 000000000..7d7c45aa9 --- /dev/null +++ b/lib/spells/adventure/ViewWorldMechanics.cpp @@ -0,0 +1,90 @@ +/* + * ViewWorldMechanics.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 "ViewWorldMechanics.h" + +#include "../CSpellHandler.h" + +#include "../../CPlayerState.h" +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/CMap.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ViewMechanics::ViewMechanics(const CSpell * s): + AdventureSpellMechanics(s) +{ +} + +ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + ShowWorldViewEx pack; + + pack.player = parameters.caster->getCasterOwner(); + + const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner); + + const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; + + for(const auto & obj : env->getMap()->getObjects()) + { + //deleted object remain as empty pointer + if(obj && filterObject(obj, spellLevel)) + { + ObjectPosInfo posInfo(obj); + + if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0) + pack.objectPositions.push_back(posInfo); + } + } + pack.showTerrain = showTerrain(spellLevel); + + env->apply(pack); + + return ESpellCastResult::OK; +} + +///ViewAirMechanics +ViewAirMechanics::ViewAirMechanics(const CSpell * s) + : ViewMechanics(s) +{ +} + +bool ViewAirMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const +{ + return (obj->ID == Obj::ARTIFACT) || (spellLevel > 1 && obj->ID == Obj::HERO) || (spellLevel > 2 && obj->ID == Obj::TOWN); +} + +bool ViewAirMechanics::showTerrain(const int32_t spellLevel) const +{ + return false; +} + +///ViewEarthMechanics +ViewEarthMechanics::ViewEarthMechanics(const CSpell * s) + : ViewMechanics(s) +{ +} + +bool ViewEarthMechanics::filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const +{ + return (obj->ID == Obj::RESOURCE) || (spellLevel > 1 && obj->ID == Obj::MINE); +} + +bool ViewEarthMechanics::showTerrain(const int32_t spellLevel) const +{ + return spellLevel > 2; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/ViewWorldMechanics.h b/lib/spells/adventure/ViewWorldMechanics.h new file mode 100644 index 000000000..2e82babf2 --- /dev/null +++ b/lib/spells/adventure/ViewWorldMechanics.h @@ -0,0 +1,48 @@ +/* + * ViewWorldMechanics.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 "AdventureSpellMechanics.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ViewMechanics : public AdventureSpellMechanics +{ +public: + ViewMechanics(const CSpell * s); + +protected: + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + virtual bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const = 0; + virtual bool showTerrain(const int32_t spellLevel) const = 0; +}; + +class ViewAirMechanics final : public ViewMechanics +{ +public: + ViewAirMechanics(const CSpell * s); + +protected: + bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override; + bool showTerrain(const int32_t spellLevel) const override; +}; + +class ViewEarthMechanics final : public ViewMechanics +{ +public: + ViewEarthMechanics(const CSpell * s); + +protected: + bool filterObject(const CGObjectInstance * obj, const int32_t spellLevel) const override; + bool showTerrain(const int32_t spellLevel) const override; +}; + +VCMI_LIB_NAMESPACE_END