diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index efaf14a4e..69aee6b37 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -12,6 +12,8 @@ #include "../Engine/Nullkiller.h" #include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/IGameSettings.h" +#include "../../../lib/spells/ISpellMechanics.h" +#include "../../../lib/spells/adventure/TownPortalEffect.h" namespace NKAI { @@ -210,32 +212,31 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const float HeroManager::getMagicStrength(const CGHeroInstance * hero) const { - auto hasFly = hero->spellbookContainsSpell(SpellID::FLY); - auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL); auto manaLimit = hero->manaLimit(); auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER); - auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0; auto score = 0.0f; + // FIXME: this will not cover spells give by scrolls / tomes. Intended? for(auto spellId : hero->getSpellsInSpellbook()) { auto spell = spellId.toSpell(); auto schoolLevel = hero->getSpellSchoolLevel(spell); + auto townPortalEffect = spell->getAdventureMechanics().getEffectAs(hero); score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f; + + if (spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT)) + score += 0.3; + + if(townPortalEffect != nullptr && schoolLevel != 0) + score += 0.6f; } vstd::amin(score, 1); score *= std::min(1.0f, spellPower / 10.0f); - if(hasFly) - score += 0.3f; - - if(hasTownPortal && hasEarth) - score += 0.6f; - vstd::amin(score, 1); score *= std::min(1.0f, manaLimit / 100.0f); diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 8c7c0294f..35b632058 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "AdventureSpellCast.h" #include "../AIGateway.h" +#include "../../../lib/spells/ISpellMechanics.h" +#include "../../../lib/spells/adventure/TownPortalEffect.h" namespace NKAI { @@ -39,8 +41,9 @@ void AdventureSpellCast::accept(AIGateway * ai) if(hero->mana < hero->getSpellCost(spell)) throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated()); + auto townPortalEffect = spell->getAdventureMechanics().getEffectAs(hero); - if(town && spellID == SpellID::TOWN_PORTAL) + if(town && townPortalEffect) { ai->selectedObject = town->id; @@ -61,7 +64,7 @@ void AdventureSpellCast::accept(AIGateway * ai) cb->waitTillRealize = true; cb->castSpell(hero, spellID, tile); - if(town && spellID == SpellID::TOWN_PORTAL) + if(town && townPortalEffect) { // visit town ai->moveHeroToTile(town->visitablePos(), hero); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 95560d259..c3e3d1043 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -17,6 +17,8 @@ #include "../../../lib/pathfinder/CPathfinder.h" #include "../../../lib/pathfinder/PathfinderUtil.h" #include "../../../lib/pathfinder/PathfinderOptions.h" +#include "../../../lib/spells/ISpellMechanics.h" +#include "../../../lib/spells/adventure/TownPortalEffect.h" #include "../../../lib/IGameSettings.h" #include "../../../lib/CPlayerState.h" @@ -1059,32 +1061,27 @@ std::vector AINodeStorage::calculateTeleportations( struct TownPortalFinder { const std::vector & initialNodes; - MasteryLevel::Type townPortalSkillLevel; - uint64_t movementNeeded; const ChainActor * actor; const CGHeroInstance * hero; std::vector targetTowns; AINodeStorage * nodeStorage; - - SpellID spellID; const CSpell * townPortal; + uint64_t movementNeeded; + SpellID spellID; + bool townSelectionAllowed; - TownPortalFinder( - const ChainActor * actor, - const std::vector & initialNodes, - std::vector targetTowns, - AINodeStorage * nodeStorage) - :actor(actor), initialNodes(initialNodes), hero(actor->hero), - targetTowns(targetTowns), nodeStorage(nodeStorage) + TownPortalFinder(const ChainActor * actor, const std::vector & initialNodes, const std::vector & targetTowns, AINodeStorage * nodeStorage, SpellID spellID) + : initialNodes(initialNodes) + , actor(actor) + , hero(actor->hero) + , targetTowns(targetTowns) + , nodeStorage(nodeStorage) + , townPortal(spellID.toSpell()) + , spellID(spellID) { - spellID = SpellID::TOWN_PORTAL; - townPortal = spellID.toSpell(); - - // TODO: Copy/Paste from TownPortalMechanics - townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal)); - - int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); - movementNeeded = baseCost * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3); + auto townPortalEffect = townPortal->getAdventureMechanics().getEffectAs(hero); + movementNeeded = townPortalEffect->getMovementPointsRequired(); + townSelectionAllowed = townPortalEffect->townSelectionAllowed(); } bool actorCanCastTownPortal() @@ -1105,7 +1102,7 @@ struct TownPortalFinder continue; } - if(townPortalSkillLevel < MasteryLevel::ADVANCED) + if(!townSelectionAllowed) { const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int { @@ -1151,7 +1148,7 @@ struct TownPortalFinder DO_NOT_SAVE_TO_COMMITTED_TILES); node->theNodeBefore = bestNode; - node->addSpecialAction(std::make_shared(targetTown)); + node->addSpecialAction(std::make_shared(targetTown, spellID)); } return nodeOptional; @@ -1177,10 +1174,21 @@ void AINodeStorage::calculateTownPortal( return; // no towns no need to run loop further } - TownPortalFinder townPortalFinder(actor, initialNodes, towns, this); - - if(townPortalFinder.actorCanCastTownPortal()) + for (const auto & spell : LIBRARY->spellh->objects) { + if (!spell || !spell->isAdventure()) + continue; + + auto townPortalEffect = spell->getAdventureMechanics().getEffectAs(actor->hero); + + if (!townPortalEffect) + continue; + + TownPortalFinder townPortalFinder(actor, initialNodes, towns, this, spell->id); + + if(!townPortalFinder.actorCanCastTownPortal()) + continue; + for(const CGTownInstance * targetTown : towns) { if(targetTown->getVisitingHero() diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp index cb0cfa102..4c9ce19b8 100644 --- a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp @@ -28,12 +28,12 @@ namespace AIPathfinding manaCost = hero->getSpellCost(spellToCast.toSpell()); } - WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero) - :AdventureCastAction(SpellID::WATER_WALK, hero, DayFlags::WATER_WALK_CAST) + WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero, SpellID spellToCast) + :AdventureCastAction(spellToCast, hero, DayFlags::WATER_WALK_CAST) { } - AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero) - : AdventureCastAction(SpellID::FLY, hero, DayFlags::FLY_CAST) + AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero, SpellID spellToCast) + : AdventureCastAction(spellToCast, hero, DayFlags::FLY_CAST) { } diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h index acbaf28fe..a88324e3e 100644 --- a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -45,14 +45,16 @@ namespace AIPathfinding class WaterWalkingAction : public AdventureCastAction { + SpellID spellToCast; public: - WaterWalkingAction(const CGHeroInstance * hero); + WaterWalkingAction(const CGHeroInstance * hero, SpellID spellToCast); }; class AirWalkingAction : public AdventureCastAction { + SpellID spellToCast; public: - AirWalkingAction(const CGHeroInstance * hero); + AirWalkingAction(const CGHeroInstance * hero, SpellID spellToCast); }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index 548792412..50bb18f51 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp @@ -92,7 +92,7 @@ namespace AIPathfinding void SummonBoatAction::execute(AIGateway * ai, const CGHeroInstance * hero) const { - Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai); + Goals::AdventureSpellCast(hero, usedSpell).accept(ai); } const ChainActor * SummonBoatAction::getActor(const ChainActor * sourceActor) const @@ -139,10 +139,8 @@ namespace AIPathfinding int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const { - SpellID summonBoat = SpellID::SUMMON_BOAT; - // FIXME: this should be hero->getSpellCost, however currently queries to bonus system are too slow - return summonBoat.toSpell()->getCost(0); + return usedSpell.toSpell()->getCost(0); } } diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 76857c3ca..b9034f910 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -24,7 +24,13 @@ namespace AIPathfinding class SummonBoatAction : public VirtualBoatAction { + SpellID usedSpell; public: + SummonBoatAction(SpellID usedSpell) + : usedSpell(usedSpell) + { + } + void execute(AIGateway * ai, const CGHeroInstance * hero) const override; virtual void applyOnDestination( diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp index f92a3a0b2..22434c547 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp @@ -20,7 +20,7 @@ using namespace AIPathfinding; void TownPortalAction::execute(AIGateway * ai, const CGHeroInstance * hero) const { - auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL); + auto goal = Goals::AdventureSpellCast(hero, usedSpell); goal.town = target; goal.tile = target->visitablePos(); diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h index 34c6c3c10..aa11e5930 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h @@ -22,10 +22,12 @@ namespace AIPathfinding { private: const CGTownInstance * target; + SpellID usedSpell; public: - TownPortalAction(const CGTownInstance * target) + TownPortalAction(const CGTownInstance * target, SpellID usedSpell) :target(target) + ,usedSpell(usedSpell) { } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index 8d95d6224..e4f37bb0d 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -12,6 +12,8 @@ #include "../../Engine/Nullkiller.h" #include "../../../../lib/pathfinder/CPathfinder.h" #include "../../../../lib/pathfinder/TurnInfo.h" +#include "../../../../lib/spells/ISpellMechanics.h" +#include "../../../../lib/spells/adventure/SummonBoatEffect.h" namespace NKAI { @@ -109,19 +111,22 @@ namespace AIPathfinding void AILayerTransitionRule::setup() { - SpellID waterWalk = SpellID::WATER_WALK; - SpellID airWalk = SpellID::FLY; - for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) { - if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell())) + for (const auto & spell : LIBRARY->spellh->objects) { - waterWalkingActions[hero] = std::make_shared(hero); - } + if (!spell || !spell->isAdventure()) + continue; - if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell())) - { - airWalkingActions[hero] = std::make_shared(hero); + if(spell->getAdventureMechanics().givesBonus(hero, BonusType::WATER_WALKING) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get())) + { + waterWalkingActions[hero] = std::make_shared(hero, spell->id); + } + + if(spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get())) + { + airWalkingActions[hero] = std::make_shared(hero, spell->id); + } } } @@ -159,13 +164,21 @@ namespace AIPathfinding for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) { - auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); - - if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) + for (const auto & spell : LIBRARY->spellh->objects) { - // TODO: For lower school level we might need to check the existence of some boat - summonableVirtualBoats[hero] = std::make_shared(); + if (!spell || !spell->isAdventure()) + continue; + + auto effect = spell->getAdventureMechanics().getEffectAs(hero); + + if (!effect || !hero->canCastThisSpell(spell.get())) + continue; + + if (effect->canCreateNewBoat() && effect->getSuccessChance(hero) == 100) + { + // TODO: For lower school level we might need to check the existence of some boat + summonableVirtualBoats[hero] = std::make_shared(spell->id); + } } } } diff --git a/AI/VCAI/Goals/AdventureSpellCast.cpp b/AI/VCAI/Goals/AdventureSpellCast.cpp index 7365e0309..147376cbb 100644 --- a/AI/VCAI/Goals/AdventureSpellCast.cpp +++ b/AI/VCAI/Goals/AdventureSpellCast.cpp @@ -13,6 +13,8 @@ #include "../FuzzyHelper.h" #include "../AIhelper.h" #include "../../../lib/mapObjects/CGTownInstance.h" +#include "../../../lib/spells/ISpellMechanics.h" +#include "../../../lib/spells/adventure/TownPortalEffect.h" using namespace Goals; @@ -39,7 +41,9 @@ TSubgoal AdventureSpellCast::whatToDoToAchieve() if(hero->mana < hero->getSpellCost(spell)) throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated()); - if(spellID == SpellID::TOWN_PORTAL && town && town->getVisitingHero()) + auto townPortalEffect = spell->getAdventureMechanics().getEffectAs(hero.h); + + if(townPortalEffect && town && town->getVisitingHero()) throw cannotFulfillGoalException("The town is already occupied by " + town->getVisitingHero()->getNameTranslated()); return iAmElementar(); @@ -47,7 +51,9 @@ TSubgoal AdventureSpellCast::whatToDoToAchieve() void AdventureSpellCast::accept(VCAI * ai) { - if(town && spellID == SpellID::TOWN_PORTAL) + auto townPortalEffect = spellID.toSpell()->getAdventureMechanics().getEffectAs(hero.h); + + if(town && townPortalEffect) { ai->selectedObject = town->id; } @@ -57,7 +63,7 @@ void AdventureSpellCast::accept(VCAI * ai) cb->waitTillRealize = true; cb->castSpell(hero.h, spellID, tile); - if(town && spellID == SpellID::TOWN_PORTAL) + if(town && townPortalEffect) { // visit town ai->moveHeroToTile(town->visitablePos(), hero); diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index 93987cd00..ba7fe1ca1 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -15,6 +15,8 @@ #include "../../../lib/pathfinder/CPathfinder.h" #include "../../../lib/pathfinder/PathfinderOptions.h" #include "../../../lib/pathfinder/PathfinderUtil.h" +#include "../../../lib/spells/ISpellMechanics.h" +#include "../../../lib/spells/adventure/TownPortalEffect.h" #include "../../../lib/IGameSettings.h" #include "../../../lib/CPlayerState.h" @@ -225,12 +227,21 @@ void AINodeStorage::calculateTownPortalTeleportations( const PathNodeInfo & source, std::vector & neighbours) { - SpellID spellID = SpellID::TOWN_PORTAL; - const CSpell * townPortal = spellID.toSpell(); auto srcNode = getAINode(source.node); - if(hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal)) + for (const auto & spell : LIBRARY->spellh->objects) { + if (!spell || !spell->isAdventure()) + continue; + + auto townPortalEffect = spell->getAdventureMechanics().getEffectAs(hero); + + if (!townPortalEffect) + continue; + + if(!hero->canCastThisSpell(spell.get()) || hero->mana < hero->getSpellCost(spell.get())) + continue; + auto towns = cb->getTownsInfo(false); vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool @@ -238,22 +249,15 @@ void AINodeStorage::calculateTownPortalTeleportations( return cb->getPlayerRelations(hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES; }); - if(!towns.size()) + if(towns.empty()) + return; + + if(hero->movementPointsRemaining() < townPortalEffect->getMovementPointsRequired()) { return; } - // TODO: Copy/Paste from TownPortalMechanics - auto skillLevel = hero->getSpellSchoolLevel(townPortal); - int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE); - auto movementCost = baseCost * (skillLevel >= 3 ? 2 : 3); - - if(hero->movementPointsRemaining() < movementCost) - { - return; - } - - if(skillLevel < MasteryLevel::ADVANCED) + if(!townPortalEffect->townSelectionAllowed()) { const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int { @@ -279,7 +283,7 @@ void AINodeStorage::calculateTownPortalTeleportations( AIPathNode * node = nodeOptional.value(); node->theNodeBefore = source.node; - node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown)); + node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown, spell->id)); node->moveRemains = source.node->moveRemains; neighbours.push_back(node); diff --git a/AI/VCAI/Pathfinding/Actions/BoatActions.cpp b/AI/VCAI/Pathfinding/Actions/BoatActions.cpp index 50df68395..22f6b39cd 100644 --- a/AI/VCAI/Pathfinding/Actions/BoatActions.cpp +++ b/AI/VCAI/Pathfinding/Actions/BoatActions.cpp @@ -23,7 +23,7 @@ namespace AIPathfinding Goals::TSubgoal SummonBoatAction::whatToDo(const HeroPtr & hero) const { - return Goals::sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT)); + return Goals::sptr(Goals::AdventureSpellCast(hero, usedSpell)); } void SummonBoatAction::applyOnDestination( @@ -53,8 +53,6 @@ namespace AIPathfinding uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const { - SpellID summonBoat = SpellID::SUMMON_BOAT; - - return hero->getSpellCost(summonBoat.toSpell()); + return hero->getSpellCost(usedSpell.toSpell()); } } diff --git a/AI/VCAI/Pathfinding/Actions/BoatActions.h b/AI/VCAI/Pathfinding/Actions/BoatActions.h index 793a74486..f358d60f8 100644 --- a/AI/VCAI/Pathfinding/Actions/BoatActions.h +++ b/AI/VCAI/Pathfinding/Actions/BoatActions.h @@ -34,9 +34,11 @@ namespace AIPathfinding class SummonBoatAction : public VirtualBoatAction { + SpellID usedSpell; public: - SummonBoatAction() + SummonBoatAction(SpellID usedSpell) :VirtualBoatAction(AINodeStorage::CAST_CHAIN) + ,usedSpell(usedSpell) { } diff --git a/AI/VCAI/Pathfinding/Actions/TownPortalAction.cpp b/AI/VCAI/Pathfinding/Actions/TownPortalAction.cpp index 6063f12c5..5c58ffb3a 100644 --- a/AI/VCAI/Pathfinding/Actions/TownPortalAction.cpp +++ b/AI/VCAI/Pathfinding/Actions/TownPortalAction.cpp @@ -19,5 +19,5 @@ Goals::TSubgoal TownPortalAction::whatToDo(const HeroPtr & hero) const { const CGTownInstance * targetTown = target; // const pointer is not allowed in settown - return Goals::sptr(Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL).settown(targetTown).settile(targetTown->visitablePos())); + return Goals::sptr(Goals::AdventureSpellCast(hero, spellToUse).settown(targetTown).settile(targetTown->visitablePos())); } diff --git a/AI/VCAI/Pathfinding/Actions/TownPortalAction.h b/AI/VCAI/Pathfinding/Actions/TownPortalAction.h index eba21d392..0fe3bfcb9 100644 --- a/AI/VCAI/Pathfinding/Actions/TownPortalAction.h +++ b/AI/VCAI/Pathfinding/Actions/TownPortalAction.h @@ -20,10 +20,12 @@ namespace AIPathfinding { private: const CGTownInstance * target; + SpellID spellToUse; public: - TownPortalAction(const CGTownInstance * target) + TownPortalAction(const CGTownInstance * target, SpellID spellToUse) :target(target) + ,spellToUse(spellToUse) { } diff --git a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp index d7b044ffd..34d12f2ff 100644 --- a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -10,6 +10,9 @@ #include "StdInc.h" #include "AILayerTransitionRule.h" +#include "../../../../lib/spells/ISpellMechanics.h" +#include "../../../../lib/spells/adventure/SummonBoatEffect.h" + namespace AIPathfinding { AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) @@ -74,13 +77,22 @@ namespace AIPathfinding } auto hero = nodeStorage->getHero(); - auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); - if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) + for (const auto & spell : LIBRARY->spellh->objects) { - // TODO: For lower school level we might need to check the existence of some boat - summonableVirtualBoat.reset(new SummonBoatAction()); + if (!spell || !spell->isAdventure()) + continue; + + auto effect = spell->getAdventureMechanics().getEffectAs(hero); + + if (!effect || !hero->canCastThisSpell(spell.get())) + continue; + + if (effect->canCreateNewBoat() && effect->getSuccessChance(hero) == 100) + { + // TODO: For lower school level we might need to check the existence of some boat + summonableVirtualBoat.reset(new SummonBoatAction(spell->id)); + } } } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index b33d0cd94..d87151d2e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1234,11 +1234,10 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus return; adventureInt->onHeroChanged(hero); - if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain) - { - //recalculate paths because hero has lost bonus influencing pathfinding - localState->erasePath(hero); - } + + //recalculate paths because hero has lost or gained bonus influencing pathfinding + if (bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING || bonus.type == BonusType::ROUGH_TERRAIN_DISCOUNT || bonus.type == BonusType::NO_TERRAIN_PENALTY) + localState->verifyPath(hero); } void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) @@ -1602,9 +1601,6 @@ void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID sp if(ENGINE->windows().topWindow()) ENGINE->windows().popWindows(1); - if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) - localState->erasePath(caster); - auto castSoundPath = spellID.toSpell()->getCastSound(); if(!castSoundPath.empty()) ENGINE->sound().playSound(castSoundPath); diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 1445b5b7a..af11f3496 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -91,6 +91,16 @@ void PlayerLocalState::verifyPath(const CGHeroInstance * h) setPath(h, getPath(h).endPos()); } +SpellID PlayerLocalState::getCurrentSpell() const +{ + return currentSpell; +} + +void PlayerLocalState::setCurrentSpell(SpellID castedSpell) +{ + currentSpell = castedSpell; +} + const CGHeroInstance * PlayerLocalState::getCurrentHero() const { if(currentSelection && currentSelection->ID == Obj::HERO) diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index 6d9e30f5f..15f481cd9 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -49,6 +49,8 @@ class PlayerLocalState PlayerSpellbookSetting spellbookSettings; + SpellID currentSpell; + void syncronizeState(); public: @@ -89,6 +91,11 @@ public: const CGTownInstance * getCurrentTown() const; const CArmedInstance * getCurrentArmy() const; + // returns currently cast spell, if any + SpellID getCurrentSpell() const; + + void setCurrentSpell(SpellID castedSpell); + void serialize(JsonNode & dest) const; void deserialize(const JsonNode & source); diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 363889b48..a00847ac7 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -47,6 +47,7 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/pathfinder/CGPathNode.h" #include "../../lib/pathfinder/TurnInfo.h" +#include "../../lib/spells/adventure/AdventureSpellEffect.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" @@ -515,7 +516,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition) if(spellBeingCasted) { assert(shortcuts->optionSpellcasting()); - assert(spellBeingCasted->id == SpellID::SCUTTLE_BOAT || spellBeingCasted->id == SpellID::DIMENSION_DOOR); if(isValidAdventureSpellTarget(targetPosition)) performSpellcasting(targetPosition); @@ -613,31 +613,16 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition) if(spellBeingCasted) { - switch(spellBeingCasted->id.toEnum()) - { - case SpellID::SCUTTLE_BOAT: - if(isValidAdventureSpellTarget(targetPosition)) - ENGINE->cursor().set(Cursor::Map::SCUTTLE_BOAT); - else - ENGINE->cursor().set(Cursor::Map::POINTER); - return; + const auto * hero = GAME->interface()->localState->getCurrentHero(); + const auto * spellEffect = spellBeingCasted->getAdventureMechanics().getEffectAs(hero); + spells::detail::ProblemImpl problem; - case SpellID::DIMENSION_DOOR: - if(isValidAdventureSpellTarget(targetPosition)) - { - if(GAME->interface()->cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && GAME->interface()->cb->isTileGuardedUnchecked(targetPosition)) - ENGINE->cursor().set(Cursor::Map::T1_ATTACK); - else - ENGINE->cursor().set(Cursor::Map::TELEPORT); - return; - } - else - ENGINE->cursor().set(Cursor::Map::POINTER); - return; - default: + if(spellEffect && spellEffect->canBeCastAtImpl(problem, GAME->interface()->cb.get(), hero, targetPosition)) + ENGINE->cursor().set(spellEffect->getCursorForTarget(GAME->interface()->cb.get(), hero, targetPosition)); + else ENGINE->cursor().set(Cursor::Map::POINTER); - return; - } + + return; } if(!isTargetPositionVisible) @@ -839,11 +824,8 @@ void AdventureMapInterface::onTileRightClicked(const int3 &mapPos) void AdventureMapInterface::enterCastingMode(const CSpell * sp) { - assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR); spellBeingCasted = sp; - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = true; - + GAME->interface()->localState->setCurrentSpell(sp->id); setState(EAdventureState::CASTING_SPELL); } @@ -852,9 +834,7 @@ void AdventureMapInterface::exitCastingMode() assert(spellBeingCasted); spellBeingCasted = nullptr; setState(EAdventureState::MAKING_TURN); - - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = false; + GAME->interface()->localState->setCurrentSpell(SpellID::NONE); } void AdventureMapInterface::hotkeyAbortCastingMode() diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 536f02ee1..6ce4a5ac2 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -117,9 +117,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): cellUnitMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); cellUnitMaxMovementHighlight = ENGINE->renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - attackCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY); - spellCursors = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY); - rangedFullDamageLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY); shootingRangeLimitImages = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY); @@ -861,24 +858,7 @@ void BattleFieldController::show(Canvas & to) renderBattlefield(to); if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID) - { - auto combatCursorIndex = ENGINE->cursor().get(); - if (combatCursorIndex) - { - auto combatImageIndex = static_cast(*combatCursorIndex); - to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetCombat(combatImageIndex)); - return; - } - - auto spellCursorIndex = ENGINE->cursor().get(); - if (spellCursorIndex) - { - auto spellImageIndex = static_cast(*spellCursorIndex); - to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffsetSpellcast()); - return; - } - - } + to.draw(ENGINE->cursor().getCurrentImage(), hexPositionAbsolute(getHoveredHex()).center() - ENGINE->cursor().getPivotOffset()); } bool BattleFieldController::receiveEvent(const Point & position, int eventType) const diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index 1b34a54cf..26e799058 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -37,9 +37,6 @@ class BattleFieldController : public CIntObject std::shared_ptr rangedFullDamageLimitImages; std::shared_ptr shootingRangeLimitImages; - std::shared_ptr attackCursors; - std::shared_ptr spellCursors; - /// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack std::unique_ptr backgroundWithHexes; diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index cf3d66107..ca9a5cb71 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -364,6 +364,8 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) void BattleInterface::spellCast(const BattleSpellCast * sc) { + waitForAnimations(); + // Do not deactivate anything in tactics mode // This is battlefield setup spells if(!tacticsMode) diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index d7a0bae75..e1c1a4b7d 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -850,7 +850,7 @@ void BattleWindow::showAll(Canvas & to) CIntObject::showAll(to); if (ENGINE->screenDimensions().x != 800 || ENGINE->screenDimensions().y !=600) - to.drawBorder(Rect(pos.x-1, pos.y, pos.w+2, pos.h+1), Colors::BRIGHT_YELLOW); + to.drawBorder(Rect(pos.x-1, pos.y - (queue && queue->embedded ? 1 : 0), pos.w+2, pos.h+1 + (queue && queue->embedded ? 1 : 0)), Colors::BRIGHT_YELLOW); } void BattleWindow::show(Canvas & to) diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index 064d0374e..8856e9f94 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -21,6 +21,7 @@ #include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/json/JsonUtils.h" std::unique_ptr CursorHandler::createCursor() { @@ -42,7 +43,6 @@ CursorHandler::CursorHandler() , showing(false) , pos(0,0) , dndObject(nullptr) - , type(Cursor::Type::DEFAULT) { showType = dynamic_cast(cursor.get()) ? Cursor::ShowType::SOFTWARE : Cursor::ShowType::HARDWARE; } @@ -51,49 +51,152 @@ CursorHandler::~CursorHandler() = default; void CursorHandler::init() { - cursors = + JsonNode cursorConfig = JsonUtils::assembleFromFiles("config/cursors.json"); + std::vector animations; + + for (const auto & cursorEntry : cursorConfig.Struct()) { - ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR"), EImageBlitMode::COLORKEY), - ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"), EImageBlitMode::COLORKEY), - ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT"), EImageBlitMode::COLORKEY), - ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"), EImageBlitMode::COLORKEY) - }; + CursorParameters parameters; + parameters.cursorID = cursorEntry.first; + parameters.image = ImagePath::fromJson(cursorEntry.second["image"]); + parameters.animation = AnimationPath::fromJson(cursorEntry.second["animation"]); + parameters.animationFrameIndex = cursorEntry.second["frame"].Integer(); + parameters.isAnimated = cursorEntry.second["animated"].Bool(); + parameters.pivot.x = cursorEntry.second["pivotX"].Integer(); + parameters.pivot.y = cursorEntry.second["pivotY"].Integer(); + + cursors.push_back(parameters); + } set(Cursor::Map::POINTER); } -void CursorHandler::changeGraphic(Cursor::Type type, size_t index) +void CursorHandler::set(const std::string & index) { assert(dndObject == nullptr); - if (type == this->type && index == this->frame) + if (index == currentCursorID) return; - this->type = type; - this->frame = index; + currentCursorID = index; + currentCursorIndex = 0; + frameTime = 0; + for (size_t i = 0; i < cursors.size(); ++i) + { + if (cursors[i].cursorID == index) + { + currentCursorIndex = i; + break; + } + } + + const auto & currentCursor = cursors.at(currentCursorIndex); + + if (currentCursor.image.empty()) + { + if (!loadedAnimations.count(currentCursor.animation)) + loadedAnimations[currentCursor.animation] = ENGINE->renderHandler().loadAnimation(currentCursor.animation, EImageBlitMode::COLORKEY); + + if (currentCursor.isAnimated) + cursorImage = loadedAnimations[currentCursor.animation]->getImage(0); + else + cursorImage = loadedAnimations[currentCursor.animation]->getImage(currentCursor.animationFrameIndex); + } + else + { + if (!loadedImages.count(currentCursor.image)) + loadedImages[currentCursor.image] = ENGINE->renderHandler().loadImage(currentCursor.image, EImageBlitMode::COLORKEY); + cursorImage = loadedImages[currentCursor.image]; + } cursor->setImage(getCurrentImage(), getPivotOffset()); } -void CursorHandler::set(Cursor::Default index) -{ - changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); -} - void CursorHandler::set(Cursor::Map index) { - changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); + constexpr std::array mapCursorNames = + { + "mapPointer", + "mapHourglass", + "mapHero", + "mapTown", + "mapTurn1Move", + "mapTurn1Attack", + "mapTurn1Sail", + "mapTurn1Disembark", + "mapTurn1Exchange", + "mapTurn1Visit", + "mapTurn2Move", + "mapTurn2Attack", + "mapTurn2Sail", + "mapTurn2Disembark", + "mapTurn2Exchange", + "mapTurn2Visit", + "mapTurn3Move", + "mapTurn3Attack", + "mapTurn3Sail", + "mapTurn3Disembark", + "mapTurn3Exchange", + "mapTurn3Visit", + "mapTurn4Move", + "mapTurn4Attack", + "mapTurn4Sail", + "mapTurn4Disembark", + "mapTurn4Exchange", + "mapTurn4Visit", + "mapTurn1SailVisit", + "mapTurn2SailVisit", + "mapTurn3SailVisit", + "mapTurn4SailVisit", + "mapScrollNorth", + "mapScrollNorthEast", + "mapScrollEast", + "mapScrollSouthEast", + "mapScrollSouth", + "mapScrollSouthWest", + "mapScrollWest", + "mapScrollNorthWest", + "UNUSED", + "mapDimensionDoor", + "mapScuttleBoat" + }; + + set(mapCursorNames.at(static_cast(index))); } void CursorHandler::set(Cursor::Combat index) { - changeGraphic(Cursor::Type::COMBAT, static_cast(index)); + constexpr std::array combatCursorNames = + { + "combatBlocked", + "combatMove", + "combatFly", + "combatShoot", + "combatHero", + "combatQuery", + "combatPointer", + "combatHitNorthEast", + "combatHitEast", + "combatHitSouthEast", + "combatHitSouthWest", + "combatHitWest", + "combatHitNorthWest", + "combatHitNorth", + "combatHitSouth", + "combatShootPenalty", + "combatShootCatapult", + "combatHeal", + "combatSacrifice", + "combatTeleport" + }; + + set(combatCursorNames.at(static_cast(index))); } void CursorHandler::set(Cursor::Spellcast index) { - //Note: this is animated cursor, ignore specified frame and only change type - changeGraphic(Cursor::Type::SPELLBOOK, frame); + //Note: this is animated cursor, ignore requested frame and only change type + set("castSpell"); } void CursorHandler::dragAndDropCursor(std::shared_ptr image) @@ -116,120 +219,12 @@ void CursorHandler::cursorMove(const int & x, const int & y) cursor->setCursorPosition(pos); } -Point CursorHandler::getPivotOffsetDefault(size_t index) -{ - return {0, 0}; -} - -Point CursorHandler::getPivotOffsetMap(size_t index) -{ - static const std::array offsets = {{ - { 0, 0}, // POINTER = 0, - { 0, 0}, // HOURGLASS = 1, - { 12, 10}, // HERO = 2, - { 12, 12}, // TOWN = 3, - - { 15, 13}, // T1_MOVE = 4, - { 13, 13}, // T1_ATTACK = 5, - { 16, 32}, // T1_SAIL = 6, - { 13, 20}, // T1_DISEMBARK = 7, - { 8, 9}, // T1_EXCHANGE = 8, - { 14, 16}, // T1_VISIT = 9, - - { 15, 13}, // T2_MOVE = 10, - { 13, 13}, // T2_ATTACK = 11, - { 16, 32}, // T2_SAIL = 12, - { 13, 20}, // T2_DISEMBARK = 13, - { 8, 9}, // T2_EXCHANGE = 14, - { 14, 16}, // T2_VISIT = 15, - - { 15, 13}, // T3_MOVE = 16, - { 13, 13}, // T3_ATTACK = 17, - { 16, 32}, // T3_SAIL = 18, - { 13, 20}, // T3_DISEMBARK = 19, - { 8, 9}, // T3_EXCHANGE = 20, - { 14, 16}, // T3_VISIT = 21, - - { 15, 13}, // T4_MOVE = 22, - { 13, 13}, // T4_ATTACK = 23, - { 16, 32}, // T4_SAIL = 24, - { 13, 20}, // T4_DISEMBARK = 25, - { 8, 9}, // T4_EXCHANGE = 26, - { 14, 16}, // T4_VISIT = 27, - - { 16, 32}, // T1_SAIL_VISIT = 28, - { 16, 32}, // T2_SAIL_VISIT = 29, - { 16, 32}, // T3_SAIL_VISIT = 30, - { 16, 32}, // T4_SAIL_VISIT = 31, - - { 6, 1}, // SCROLL_NORTH = 32, - { 16, 2}, // SCROLL_NORTHEAST = 33, - { 21, 6}, // SCROLL_EAST = 34, - { 16, 16}, // SCROLL_SOUTHEAST = 35, - { 6, 21}, // SCROLL_SOUTH = 36, - { 1, 16}, // SCROLL_SOUTHWEST = 37, - { 1, 5}, // SCROLL_WEST = 38, - { 2, 1}, // SCROLL_NORTHWEST = 39, - - { 0, 0}, // POINTER_COPY = 40, - { 14, 16}, // TELEPORT = 41, - { 20, 20}, // SCUTTLE_BOAT = 42 - }}; - - assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index] * ENGINE->screenHandler().getScalingFactor(); -} - -Point CursorHandler::getPivotOffsetCombat(size_t index) -{ - static const std::array offsets = {{ - { 12, 12 }, // BLOCKED = 0, - { 10, 14 }, // MOVE = 1, - { 14, 14 }, // FLY = 2, - { 12, 12 }, // SHOOT = 3, - { 12, 12 }, // HERO = 4, - { 8, 12 }, // QUERY = 5, - { 0, 0 }, // POINTER = 6, - { 21, 0 }, // HIT_NORTHEAST = 7, - { 31, 5 }, // HIT_EAST = 8, - { 21, 21 }, // HIT_SOUTHEAST = 9, - { 0, 21 }, // HIT_SOUTHWEST = 10, - { 0, 5 }, // HIT_WEST = 11, - { 0, 0 }, // HIT_NORTHWEST = 12, - { 6, 0 }, // HIT_NORTH = 13, - { 6, 31 }, // HIT_SOUTH = 14, - { 14, 0 }, // SHOOT_PENALTY = 15, - { 12, 12 }, // SHOOT_CATAPULT = 16, - { 12, 12 }, // HEAL = 17, - { 12, 12 }, // SACRIFICE = 18, - { 14, 20 }, // TELEPORT = 19 - }}; - - assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index] * ENGINE->screenHandler().getScalingFactor(); -} - -Point CursorHandler::getPivotOffsetSpellcast() -{ - return Point(18, 28) * ENGINE->screenHandler().getScalingFactor(); -} - Point CursorHandler::getPivotOffset() { if (dndObject) return dndObject->dimensions() / 2; - switch (type) { - case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame); - case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame); - case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame); - case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast(); - }; - - assert(0); - return {0, 0}; + return cursors.at(currentCursorIndex).pivot; } std::shared_ptr CursorHandler::getCurrentImage() @@ -237,15 +232,17 @@ std::shared_ptr CursorHandler::getCurrentImage() if (dndObject) return dndObject; - return cursors[static_cast(type)]->getImage(frame); + return cursorImage; } -void CursorHandler::updateSpellcastCursor() +void CursorHandler::updateAnimatedCursor() { static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame frameTime += ENGINE->framerate().getElapsedMilliseconds() / 1000.f; - size_t newFrame = frame; + int32_t newFrame = currentFrame; + const auto & animationName = cursors.at(currentCursorIndex).animation; + const auto & animation = loadedAnimations.at(animationName); while (frameTime >= frameDisplayDuration) { @@ -253,12 +250,12 @@ void CursorHandler::updateSpellcastCursor() newFrame++; } - auto & animation = cursors.at(static_cast(type)); - while (newFrame >= animation->size()) newFrame -= animation->size(); - changeGraphic(Cursor::Type::SPELLBOOK, newFrame); + currentFrame = newFrame; + cursorImage = animation->getImage(currentFrame); + cursor->setImage(getCurrentImage(), getPivotOffset()); } void CursorHandler::render() @@ -271,8 +268,8 @@ void CursorHandler::update() if(!showing) return; - if (type == Cursor::Type::SPELLBOOK) - updateSpellcastCursor(); + if (cursors.at(currentCursorIndex).isAnimated) + updateAnimatedCursor(); cursor->update(); } diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 0b270a5a1..3ccd41ced 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -18,25 +18,18 @@ class CAnimation; namespace Cursor { - enum class Type { + enum class Type : int8_t { ADVENTURE, // set of various cursors for adventure map COMBAT, // set of various cursors for combat - DEFAULT, // default arrow and hourglass cursors SPELLBOOK // animated cursor for spellcasting }; - enum class ShowType { + enum class ShowType : int8_t { SOFTWARE, HARDWARE }; - enum class Default { - POINTER = 0, - //ARROW_COPY = 1, // probably unused - HOURGLASS = 2, - }; - - enum class Combat { + enum class Combat : int8_t { BLOCKED = 0, MOVE = 1, FLY = 2, @@ -61,7 +54,7 @@ namespace Cursor COUNT }; - enum class Map { + enum class Map : int8_t { POINTER = 0, HOURGLASS = 1, HERO = 2, @@ -109,7 +102,7 @@ namespace Cursor COUNT }; - enum class Spellcast { + enum class Spellcast : int8_t { SPELL = 0, }; } @@ -117,26 +110,33 @@ namespace Cursor /// handles mouse cursor class CursorHandler final { + struct CursorParameters + { + std::string cursorID; + ImagePath image; + AnimationPath animation; + Point pivot; + int animationFrameIndex; + bool isAnimated; + }; + + std::vector cursors; + std::map> loadedAnimations; + std::map> loadedImages; + std::shared_ptr dndObject; //if set, overrides currentCursor - - std::array, 4> cursors; - - bool showing; + std::shared_ptr cursorImage; //if set, overrides currentCursor /// Current cursor - Cursor::Type type; - Cursor::ShowType showType; - size_t frame; - float frameTime; + std::string currentCursorID; Point pos; + float frameTime; + int32_t currentCursorIndex; + int32_t currentFrame; + Cursor::ShowType showType; + bool showing; - void changeGraphic(Cursor::Type type, size_t index); - - Point getPivotOffset(); - - void updateSpellcastCursor(); - - std::shared_ptr getCurrentImage(); + void updateAnimatedCursor(); std::unique_ptr cursor; @@ -154,31 +154,13 @@ public: void dragAndDropCursor(const AnimationPath & path, size_t index); /// Changes cursor to specified index - void set(Cursor::Default index); void set(Cursor::Map index); void set(Cursor::Combat index); void set(Cursor::Spellcast index); + void set(const std::string & index); - /// Returns current index of cursor - template - std::optional get() - { - bool typeValid = true; - - typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; - typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; - - if (typeValid) - return static_cast(frame); - return std::nullopt; - } - - Point getPivotOffsetSpellcast(); - Point getPivotOffsetDefault(size_t index); - Point getPivotOffsetMap(size_t index); - Point getPivotOffsetCombat(size_t index); + std::shared_ptr getCurrentImage(); + Point getPivotOffset(); void render(); void update(); diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index 7e9ba4f60..5da7cd5c6 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -19,14 +19,16 @@ #include "../GameInstance.h" #include "../../lib/Point.h" +#include "../../lib/battle/CPlayerBattleCallback.h" +#include "../../lib/battle/IBattleState.h" #include "../../lib/callback/CCallback.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/spells/CSpellHandler.h" #include "../../lib/mapping/CMap.h" #include "../../lib/pathfinder/CGPathNode.h" -#include "../../lib/battle/CPlayerBattleCallback.h" -#include "../../lib/battle/IBattleState.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/adventure/AdventureSpellEffect.h" MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState) : viewState(viewState) @@ -367,15 +369,14 @@ bool MapRendererAdventureContext::showTextOverlay() const bool MapRendererAdventureContext::showSpellRange(const int3 & position) const { - if (!settingSpellRange) - return false; - auto hero = GAME->interface()->localState->getCurrentHero(); + auto spell = GAME->interface()->localState->getCurrentSpell(); - if (!hero) + if (!hero || !spell.hasValue()) return false; - return !isInScreenRange(hero->getSightCenter(), position); + const auto * spellEffect = spell.toSpell()->getAdventureMechanics().getEffectAs(hero); + return !spellEffect->isTargetInRange(GAME->interface()->cb.get(), hero, position); } MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState) diff --git a/client/mapView/MapRendererContext.h b/client/mapView/MapRendererContext.h index cc71fbc5d..ece349d22 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -72,7 +72,6 @@ public: bool settingShowGrid = false; bool settingShowVisitable = false; bool settingShowBlocked = false; - bool settingSpellRange= false; bool settingTextOverlay = false; bool settingsAdventureObjectAnimation = true; bool settingsAdventureTerrainAnimation = true; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 7088c77da..b0ece0265 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -233,7 +233,6 @@ void MapViewController::updateState() adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); - adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); adventureContext->settingTextOverlay = (ENGINE->isKeyboardAltDown() || ENGINE->input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool(); } } diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 56e79ca69..59e149082 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -137,7 +137,7 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim auto it = animationLayouts.find(actualPath); - if (it != animationLayouts.end()) + if (it != animationLayouts.end() && (settings["video"]["useHdTextures"].Bool() || scalingFactor == 1)) return it->second; AnimationLayoutMap result; @@ -312,9 +312,9 @@ std::shared_ptr RenderHandler::loadScaledImage(const ImageLocato std::shared_ptr img = nullptr; - if(CResourceHandler::get()->existsResource(imagePathSprites)) + if(CResourceHandler::get()->existsResource(imagePathSprites) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)) img = std::make_shared(imagePathSprites, optimizeImage); - else if(CResourceHandler::get()->existsResource(imagePathData)) + else if(CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)) img = std::make_shared(imagePathData, optimizeImage); else if(CResourceHandler::get()->existsResource(imagePath)) img = std::make_shared(imagePath, optimizeImage); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 708b19276..3e849ab11 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -42,6 +42,7 @@ #include "../../lib/callback/CCallback.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/adventure/AdventureSpellEffect.h" #include "../../lib/spells/Problem.h" #include "../../lib/spells/SpellSchoolHandler.h" #include "../../lib/texts/CGeneralTextHandler.h" @@ -752,12 +753,12 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) spells::detail::ProblemImpl problem; if (mySpell->getAdventureMechanics().canBeCast(problem, GAME->interface()->cb.get(), owner->myHero)) { - if(mySpell->getTargetType() == spells::AimType::LOCATION) + const auto * rangeEffect = mySpell->getAdventureMechanics().getEffectAs(owner->myHero); + + if(rangeEffect != nullptr) adventureInt->enterCastingMode(mySpell); - else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) - owner->myInt->cb->castSpell(h, mySpell->id); else - logGlobal->error("Invalid spell target type"); + owner->myInt->cb->castSpell(h, mySpell->id); } else { diff --git a/config/cursors.json b/config/cursors.json new file mode 100644 index 000000000..56bf286eb --- /dev/null +++ b/config/cursors.json @@ -0,0 +1,73 @@ +/// This file can be modified via mods by creating file config/cursors.json +/// File in mod only needs to contain new or modified entries +/// Format: +/// - "image" - path to image (e.g. png file) with image for this cursor. Overrides "animation" key. +/// - "animation" - path to animation (.def or .json) with image for this cursor. Also requires "frame" or "animated" key +/// - "frame" - index of frame inside animation file that should be used for this cursor +/// - "animated" - if set to true, cursor will be animated using entire animation file +/// - "pivotX" and "pivotY" - position inside image that should act as pointer tip. Mouse clicks would generate click event on pixel below this point +{ + "mapPointer" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRADVNTR", "frame" : 0 }, + "mapHourglass" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRADVNTR", "frame" : 1 }, + "mapHero" : { "pivotX" : 12, "pivotY" : 10, "animation" : "CRADVNTR", "frame" : 2 }, + "mapTown" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRADVNTR", "frame" : 3 }, + "mapTurn1Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 4 }, + "mapTurn1Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 5 }, + "mapTurn1Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 6 }, + "mapTurn1Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 7 }, + "mapTurn1Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 8 }, + "mapTurn1Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 9 }, + "mapTurn2Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 10 }, + "mapTurn2Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 11 }, + "mapTurn2Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 12 }, + "mapTurn2Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 13 }, + "mapTurn2Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 14 }, + "mapTurn2Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 15 }, + "mapTurn3Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 16 }, + "mapTurn3Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 17 }, + "mapTurn3Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 18 }, + "mapTurn3Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 19 }, + "mapTurn3Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 20 }, + "mapTurn3Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 21 }, + "mapTurn4Move" : { "pivotX" : 15, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 22 }, + "mapTurn4Attack" : { "pivotX" : 13, "pivotY" : 13, "animation" : "CRADVNTR", "frame" : 23 }, + "mapTurn4Sail" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 24 }, + "mapTurn4Disembark" : { "pivotX" : 13, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 25 }, + "mapTurn4Exchange" : { "pivotX" : 8, "pivotY" : 9, "animation" : "CRADVNTR", "frame" : 26 }, + "mapTurn4Visit" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 27 }, + "mapTurn1SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 28 }, + "mapTurn2SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 29 }, + "mapTurn3SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 30 }, + "mapTurn4SailVisit" : { "pivotX" : 16, "pivotY" : 32, "animation" : "CRADVNTR", "frame" : 31 }, + "mapScrollNorth" : { "pivotX" : 6, "pivotY" : 1, "animation" : "CRADVNTR", "frame" : 32 }, + "mapScrollNorthEast" : { "pivotX" : 16, "pivotY" : 2, "animation" : "CRADVNTR", "frame" : 33 }, + "mapScrollEast" : { "pivotX" : 21, "pivotY" : 6, "animation" : "CRADVNTR", "frame" : 34 }, + "mapScrollSouthEast" : { "pivotX" : 16, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 35 }, + "mapScrollSouth" : { "pivotX" : 6, "pivotY" : 21, "animation" : "CRADVNTR", "frame" : 36 }, + "mapScrollSouthWest" : { "pivotX" : 1, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 37 }, + "mapScrollWest" : { "pivotX" : 1, "pivotY" : 5, "animation" : "CRADVNTR", "frame" : 38 }, + "mapScrollNorthWest" : { "pivotX" : 2, "pivotY" : 1, "animation" : "CRADVNTR", "frame" : 39 }, + "mapDimensionDoor" : { "pivotX" : 14, "pivotY" : 16, "animation" : "CRADVNTR", "frame" : 41 }, + "mapScuttleBoat" : { "pivotX" : 20, "pivotY" : 20, "animation" : "CRADVNTR", "frame" : 42 }, + "combatBlocked" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 0}, + "combatMove" : { "pivotX" : 10, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" : 1}, + "combatFly" : { "pivotX" : 14, "pivotY" : 14, "animation" : "CRCOMBAT", "frame" : 2}, + "combatShoot" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 3}, + "combatHero" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 4}, + "combatQuery" : { "pivotX" : 8, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 5}, + "combatPointer" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 6}, + "combatHitNorthEast" : { "pivotX" : 21, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 7}, + "combatHitEast" : { "pivotX" : 31, "pivotY" : 5, "animation" : "CRCOMBAT", "frame" : 8}, + "combatHitSouthEast" : { "pivotX" : 21, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" : 9}, + "combatHitSouthWest" : { "pivotX" : 0, "pivotY" : 21, "animation" : "CRCOMBAT", "frame" : 10}, + "combatHitWest" : { "pivotX" : 0, "pivotY" : 5, "animation" : "CRCOMBAT", "frame" : 11}, + "combatHitNorthWest" : { "pivotX" : 0, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 12}, + "combatHitNorth" : { "pivotX" : 6, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 13}, + "combatHitSouth" : { "pivotX" : 6, "pivotY" : 31, "animation" : "CRCOMBAT", "frame" : 14}, + "combatShootPenalty" : { "pivotX" : 14, "pivotY" : 0, "animation" : "CRCOMBAT", "frame" : 15}, + "combatShootCatapult" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 16}, + "combatHeal" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 17}, + "combatSacrifice" : { "pivotX" : 12, "pivotY" : 12, "animation" : "CRCOMBAT", "frame" : 18}, + "combatTeleport" : { "pivotX" : 14, "pivotY" : 20, "animation" : "CRCOMBAT", "frame" : 19}, + "castSpell" : { "pivotX" : 18, "pivotY" : 28, "animation" : "CRSPELL", "animated" : true } +} diff --git a/config/schemas/settings.json b/config/schemas/settings.json index af3a3d5da..60a220bea 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -220,7 +220,8 @@ "fontUpscalingFilter", "downscalingFilter", "allowPortrait", - "asyncUpscaling" + "asyncUpscaling", + "useHdTextures" ], "properties" : { "resolution" : { @@ -316,6 +317,10 @@ "asyncUpscaling" : { "type" : "boolean", "default" : true + }, + "useHdTextures" : { + "type" : "boolean", + "default" : true } } }, diff --git a/config/schemas/spell.json b/config/schemas/spell.json index a61b83267..172c5df1d 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -50,6 +50,335 @@ } } }, + "adventureEffect" : { + "type" : "object", + "required" : [ "type" ], + "properties" : { + "type" : { + "type" : "string", + "enum" : [ "generic", "dimensionDoor", "removeObject", "summonBoat", "townPortal", "viewWorld" ] + }, + "castsPerDay" : { "type" : "number" }, + "castsPerDayXL" : { "type" : "number" }, + "bonuses" : { "additionalProperties" : { "$ref" : "bonusInstance.json" }}, + }, + "allOf" : [ + { + "if" : { "properties" : { "type" : { "const" : "dimensionDoor" } } }, + "then" : { + "properties" : { + "castsPerDay" : {}, + "castsPerDayXL" : {}, + "bonuses" : {}, + "type" : {}, + "rangeX" : { "type" : "number" }, + "rangeY" : { "type" : "number" }, + "ignoreFow" : { "type" : "boolean" }, + "cursor" : { "type" : "string" }, + "cursorGuarded" : { "type" : "string" }, + "movementPointsRequired" : { "type" : "number" }, + "movementPointsTaken" : { "type" : "number" }, + "waterLandFailureTakesPoints" : { "type" : "boolean" }, + "exposeFow" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "removeObject" } } }, + "then" : { + "properties" : { + "castsPerDay" : {}, + "castsPerDayXL" : {}, + "bonuses" : {}, + "type" : {}, + "rangeX" : { "type" : "number" }, + "rangeY" : { "type" : "number" }, + "ignoreFow" : { "type" : "boolean" }, + "cursor" : { "type" : "string" }, + "objects" : { "additionalProperties" : { "type" : "boolean" } } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "summonBoat" } } }, + "then" : { + "properties" : { + "castsPerDay" : {}, + "castsPerDayXL" : {}, + "bonuses" : {}, + "type" : {}, + "useExistingBoat" : { "type" : "boolean" }, + "createdBoat" : { "type" : "string" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "townPortal" } } }, + "then" : { + "properties" : { + "castsPerDay" : {}, + "castsPerDayXL" : {}, + "bonuses" : {}, + "type" : {}, + "movementPointsRequired" : { "type" : "number" }, + "movementPointsTaken" : { "type" : "number" }, + "allowTownSelection" : { "type" : "boolean" }, + "skipOccupiedTowns" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "viewWorld" } } }, + "then" : { + "properties" : { + "castsPerDay" : {}, + "castsPerDayXL" : {}, + "bonuses" : {}, + "type" : {}, + "showTerrain" : { "type" : "boolean" }, + "objects" : { "additionalProperties" : { "type" : "boolean" } } + }, + "additionalProperties" : false + } + } + ] + }, + "battleEffect" : { + "type" : "object", + "required" : [ "type" ], + "properties" : { + "type" : { + "type" : "string", + "enum" : [ + "core:catapult", + "core:clone", + "core:damage", + "core:demonSummon", + "core:dispel", + "core:heal", + "core:moat", + "core:obstacle", + "core:removeObstacle", + "core:sacrifice", + "core:summon", + "core:teleport", + "core:timed" + ] + }, + "indirect" : { "type" : "boolean" }, + "optional" : { "type" : "boolean" } + }, + "allOf" : [ + { + "if" : { "properties" : { "type" : { "const" : "core:catapult" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "targetsToAttack" : { "type" : "number" }, + "chanceToHitKeep" : { "type" : "number" }, + "chanceToHitGate" : { "type" : "number" }, + "chanceToHitTower" : { "type" : "number" }, + "chanceToHitWall" : { "type" : "number" }, + "chanceToNormalHit" : { "type" : "number" }, + "chanceToCrit" : { "type" : "number" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:clone" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "maxTier" : { "type" : "number" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:damage" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "killByPercentage" : { "type" : "boolean" }, + "killByCount" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:demonSummon" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "id" : { "type" : "string" }, + "permanent" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:dispel" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "dispelPositive" : { "type" : "boolean" }, + "dispelNegative" : { "type" : "boolean" }, + "dispelNeutral" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "enum" : [ "core:heal", "core:sacrifice" ] } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "healLevel" : { "type" : "string" }, + "healPower" : { "type" : "string" }, + "minFullUnits" : { "type" : "number" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:moat" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "hidden" : { "type" : "boolean" }, + "trap" : { "type" : "boolean" }, + "removeOnTrigger" : { "type" : "boolean" }, + "dispellable" : { "type" : "boolean" }, + "moatDamage" : { "type" : "number" }, + "moatHexes" : { }, + "triggerAbility" : { "type" : "string" }, + "defender" : { }, + "bonus" : { "additionalProperties" : { "$ref" : "bonusInstance.json" } } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:obstacle" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "hidden" : { "type" : "boolean" }, + "passable" : { "type" : "boolean" }, + "trap" : { "type" : "boolean" }, + "removeOnTrigger" : { "type" : "boolean" }, + "hideNative" : { "type" : "boolean" }, + "patchCount" : { "type" : "number" }, + "turnsRemaining" : { "type" : "number" }, + "triggerAbility" : { "type" : "string" }, + "attacker" : { }, + "defender" : { }, + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:removeObstacle" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "removeAbsolute" : { "type" : "boolean" }, + "removeUsual" : { "type" : "boolean" }, + "removeAllSpells" : { "type" : "boolean" }, + "removeSpells" : { } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:summon" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "id" : { "type" : "string" }, + "permanent" : { "type" : "boolean" }, + "exclusive" : { "type" : "boolean" }, + "summonByHealth" : { "type" : "boolean" }, + "summonSameUnit" : { "type" : "boolean" }, + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:teleport" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "triggerObstacles" : { "type" : "boolean" }, + "isWallPassable" : { "type" : "boolean" }, + "isMoatPassable" : { "type" : "boolean" } + }, + "additionalProperties" : false + } + }, + { + "if" : { "properties" : { "type" : { "const" : "core:timed" } } }, + "then" : { + "properties" : { + "type" : {}, + "indirect" : {}, + "optional" : {}, + "ignoreImmunity" : { "type" : "boolean" }, + "chainLength" : { "type" : "number" }, + "chainFactor" : { "type" : "number" }, + "cumulative" : { "type" : "boolean" }, + "bonus" : { "additionalProperties" : { "$ref" : "bonusInstance.json" } } + }, + "additionalProperties" : false + } + }, + ] + }, "flags" : { "type" : "object", "additionalProperties" : { @@ -94,9 +423,12 @@ "battleEffects" : { "type" : "object", "additionalProperties" : { - "type" : "object" + "$ref" : "#/definitions/battleEffect" } }, + "adventureEffect" : { + "$ref" : "#/definitions/adventureEffect" + }, "targetModifier" : { "type" : "object", "additionalProperties" : false, diff --git a/config/spells/adventure.json b/config/spells/adventure.json index ef79c679e..fd340a304 100644 --- a/config/spells/adventure.json +++ b/config/spells/adventure.json @@ -6,9 +6,24 @@ "sounds": { "cast": "SUMMBOAT" }, - "levels" :{ + "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "summonBoat", + "castsPerDay" : 0, + "useExistingBoat" : true + } + }, + "advanced":{ + "adventureEffect" : { + "createdBoat" : "boatNecropolis" + } + }, + "expert":{ + "adventureEffect" : { + "createdBoat" : "boatNecropolis" + } } }, "flags" : { @@ -25,7 +40,26 @@ }, "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "removeObject", + "castsPerDay" : 0, + "cursor" : "mapScuttleBoat", // defined in config/cursors.json + "rangeX" : 9, + "rangeY" : 8, + "ignoreFow" : false, + "objects" : { + "boat" : true + } + } + }, + "advanced":{ + "adventureEffect" : { + } + }, + "expert":{ + "adventureEffect" : { + } } }, "flags" : { @@ -43,51 +77,58 @@ "levels" : { "base":{ "range" : "0", - "effects" : { - "visionsMonsters" : { - "type" : "VISIONS", - "subtype" : "visionsMonsters", - "duration" : "ONE_DAY", - "val" : 1, - "valueType" : "INDEPENDENT_MAX" + "adventureEffect" : { + "type" : "generic", + "castsPerDay" : 0, + "bonuses" : { + "visionsMonsters" : { + "type" : "VISIONS", + "subtype" : "visionsMonsters", + "duration" : "ONE_DAY", + "val" : 1, + "valueType" : "INDEPENDENT_MAX" + } } - } + } }, "advanced":{ - "effects" : { - "visionsMonsters" : { - "val" : 2 - }, - "visionsHeroes" :{ - "type" : "VISIONS", - "subtype" : "visionsHeroes", - "duration" : "ONE_DAY", - "val" : 2, - "valueType" : "INDEPENDENT_MAX" + "adventureEffect" : { + "bonuses" : { + "visionsMonsters" : { + "val" : 2 + }, + "visionsHeroes" :{ + "type" : "VISIONS", + "subtype" : "visionsHeroes", + "duration" : "ONE_DAY", + "val" : 2, + "valueType" : "INDEPENDENT_MAX" + } } - - } + } }, "expert":{ - "effects" : { - "visionsMonsters" : { - "val" : 3 - }, - "visionsHeroes" :{ - "type" : "VISIONS", - "subtype" : "visionsHeroes", - "duration" : "ONE_DAY", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - }, - "visionsTowns" :{ - "type" : "VISIONS", - "subtype" : "visionsTowns", - "duration" : "ONE_DAY", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" + "adventureEffect" : { + "bonuses" : { + "visionsMonsters" : { + "val" : 3 + }, + "visionsHeroes" :{ + "type" : "VISIONS", + "subtype" : "visionsHeroes", + "duration" : "ONE_DAY", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + }, + "visionsTowns" :{ + "type" : "VISIONS", + "subtype" : "visionsTowns", + "duration" : "ONE_DAY", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + } } - } + } } }, "flags" : { @@ -103,7 +144,33 @@ }, "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "viewWorld", + "castsPerDay" : 0, + "objects" : { + "resource" : true + } + } + }, + "advanced":{ + "adventureEffect" : { + "objects" : { + "resource" : true, + "mine" : true, + "abandonedMine" : true + } + } + }, + "expert":{ + "adventureEffect" : { + "objects" : { + "resource" : true, + "mine" : true, + "abandonedMine" : true + }, + "showTerrain" : true + } } }, "flags" : { @@ -120,28 +187,36 @@ "levels" : { "base":{ "range" : "0", - "effects" : { - "stealth" : { - "type" : "DISGUISED", - "duration" : "ONE_DAY", - "val" : 1, - "valueType" : "INDEPENDENT_MAX" + "adventureEffect" : { + "type" : "generic", + "castsPerDay" : 0, + "bonuses" : { + "stealth" : { + "type" : "DISGUISED", + "duration" : "ONE_DAY", + "val" : 1, + "valueType" : "INDEPENDENT_MAX" + } } - } + } }, "advanced":{ - "effects" : { - "stealth" : { - "val" : 2 - } - } + "adventureEffect" : { + "bonuses" : { + "stealth" : { + "val" : 2 + } + } + } }, "expert":{ - "effects" : { - "stealth" : { - "val" : 3 + "adventureEffect" : { + "bonuses" : { + "stealth" : { + "val" : 3 + } } - } + } } }, "flags" : { @@ -157,7 +232,31 @@ }, "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "viewWorld", + "castsPerDay" : 0, + "objects" : { + "artifact" : true + } + } + }, + "advanced":{ + "adventureEffect" : { + "objects" : { + "artifact" : true, + "hero" : true + } + } + }, + "expert":{ + "adventureEffect" : { + "objects" : { + "artifact" : true, + "hero" : true, + "town" : true + } + } } }, "flags" : { @@ -174,28 +273,38 @@ "levels" : { "base":{ "range" : "0", - "effects" : { - "fly" : { - "type" : "FLYING_MOVEMENT", - "duration" : "ONE_DAY", - "val" : 40, - "valueType" : "INDEPENDENT_MIN" + "adventureEffect" : { + "type" : "generic", + "castsPerDay" : 0, + "bonuses" : { + "fly" : { + "type" : "FLYING_MOVEMENT", + "duration" : "ONE_DAY", + "val" : 40, + "valueType" : "INDEPENDENT_MIN" + } } - } + } }, "advanced":{ - "effects" : { - "fly" : { - "val" : 20 + "adventureEffect" : { + "type" : "generic", + "bonuses" : { + "fly" : { + "val" : 20 + } } - } + } }, "expert":{ - "effects" : { - "fly" : { - "val" : 0 + "adventureEffect" : { + "type" : "generic", + "bonuses" : { + "fly" : { + "val" : 0 + } } - } + } } }, "flags" : { @@ -212,28 +321,38 @@ "levels" : { "base":{ "range" : "0", - "effects" : { - "waterWalk" : { - "type" : "WATER_WALKING", - "duration" : "ONE_DAY", - "val" : 40, - "valueType" : "INDEPENDENT_MIN" + "adventureEffect" : { + "type" : "generic", + "castsPerDay" : 0, + "bonuses" : { + "waterWalk" : { + "type" : "WATER_WALKING", + "duration" : "ONE_DAY", + "val" : 40, + "valueType" : "INDEPENDENT_MIN" + } } - } + } }, "advanced":{ - "effects" : { - "waterWalk" : { - "val" : 20 + "adventureEffect" : { + "type" : "generic", + "bonuses" : { + "waterWalk" : { + "val" : 20 + } } - } + } }, "expert":{ - "effects" : { - "waterWalk" : { - "val" : 0 + "adventureEffect" : { + "type" : "generic", + "bonuses" : { + "waterWalk" : { + "val" : 0 + } } - } + } } }, "flags" : { @@ -250,7 +369,31 @@ }, "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "dimensionDoor", + "movementPointsRequired" : 0, + "movementPointsTaken" : 300, + "waterLandFailureTakesPoints" : true, + "cursor" : "mapDimensionDoor", // defined in config/cursors.json + "cursorGuarded" : "mapTurn1Attack", // defined in config/cursors.json + "castsPerDay" : 2, + "rangeX" : 9, + "rangeY" : 8, + "ignoreFow" : true, + "exposeFow" : true + } + }, + "advanced":{ + "adventureEffect" : { + "castsPerDay" : 3 + } + }, + "expert":{ + "adventureEffect" : { + "castsPerDay" : 4, + "movementPointsTaken" : 200 + } } }, "flags" : { @@ -266,7 +409,29 @@ }, "levels" : { "base":{ - "range" : "X" + "range" : "X", + "adventureEffect" : { + "type" : "townPortal", + "castsPerDay" : 2, + "allowTownSelection" : false, + "skipOccupiedTowns" : false, + "movementPointsRequired" : 300, + "movementPointsTaken" : 300 + } + }, + "advanced":{ + "adventureEffect" : { + "allowTownSelection" : true, + "movementPointsRequired" : 200, + "movementPointsTaken" : 200 + } + }, + "expert":{ + "adventureEffect" : { + "allowTownSelection" : true, + "movementPointsRequired" : 200, + "movementPointsTaken" : 200 + } } }, "flags" : { diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index f66b332ed..74b43269f 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -241,8 +241,6 @@ TODO "firstEffect": {[bonus format]}, "secondEffect": {[bonus format]} //... - - }, // DEPRECATED, please use "battleEffects" with timed effect and "cumulative" set to true instead @@ -260,6 +258,11 @@ TODO "mod:secondEffect": {[effect format]} //... } + + /// See Configurable adventure map effects section below for detailed description + "adventureEffect" : { + [effect format] + } } ``` @@ -672,3 +675,179 @@ Value of all bonuses can be affected by following bonuses: - range 0: any single obstacle - range X: all obstacles + +## Configurable adventure map effects + +Currently, VCMI does not allow completely new spell effects for adventure maps. However, it is possible to: + +- modify the parameters of all H3 spells. +- create spells with similar effects to H3 spells +- create a spell that gives bonuses to the hero who cast the spell. + +Unlike combat effects, adventure map spells can only have one special effect, such as the Dimension Door or Town Portal effect. The number of bonuses granted by an adventure map spell is unlimited. + +The AI has a limited understanding of adventure map spells and may use the following spells: + +- Spells that give `WATER_WALKING` or `FLYING_MOVEMENT` bonuses +- Spells with the Summon Boat effect, provided the spell can create new boats with a 100% success chance. +- Any spells with the Town Portal effect. + +### Common format + +All properties in this section can be used for all non-generic adventure map spell effects. + +Parameters: + +- `type` - the type of spell effect used for this spell, or `generic` if a custom mechanic is not used. +- `castsPerDay` - Optional. Defines how many times a hero can cast this spell per day; set to zero or omit for unlimited use. +- `castsPerDayXL` - Optional. An alternative cast-per-day limit that is only active on maps that are at least XL+U in size. If this value is not set or is set to zero, the game will use the value of the `castsPerDay` variable. +- `bonuses` - A list of bonuses that will be given to the hero when this spell is cast successfully. When used with effects that can fail (e.g. Summon Boat), the bonuses will only apply to a successful cast. + +Example: + +```json +"adventureEffect" : { + "type" : "generic", + "castsPerDay" : 0, + "castsPerDayXL" : 0, + "bonuses" : { + "fly" : { + "type" : "FLYING_MOVEMENT", + "duration" : "ONE_DAY", + "val" : 40, + "valueType" : "INDEPENDENT_MIN" + } + } +} +``` + +### Dimension Door + +The effect instantly teleports the hero to the selected location. + +Parameters: + +- `movementPointsRequired` - The amount of movement points the hero must have to cast this spell. +- `movementPointsTaken` - The amount of movement points that will be taken if the spell is cast successfully. If the hero does not have enough movement points, they will be reduced to zero after casting. +- `waterLandFailureTakesPoints` - If set to true, mana and movement points will be spent on an attempt to teleport to an inaccessible location (e.g. teleporting to land while in a boat). +- `cursor` - Identifier of the cursor that will be shown when hovering over a valid destination tile. See `config/cursors.json` for more details. +- `cursorGuarded` - alternative cursor that appears if using the teleport spell on a target would result in combat. This is only used if the game rule 'dimensionDoorTriggersGuards' is active. +- `exposeFow` - If this is set to true, using this spell will reveal information behind fog of war, such as whether teleportation is possible or if the location is guarded. +- `ignoreFow` - If this is set to true, it is possible to use the spell to teleport into terra incognita. +- `rangeX` - maximum distance to teleport in the X dimension (left-right axis). +- `rangeY` - maximum distance to teleport in the Y dimension (top-bottom axis). + +Example: + +```json +"adventureEffect" : { + "type" : "dimensionDoor", + "movementPointsRequired" : 0, + "movementPointsTaken" : 300, + "waterLandFailureTakesPoints" : true, + "cursor" : "mapDimensionDoor", + "cursorGuarded" : "mapTurn1Attack", + "castsPerDay" : 2, + "rangeX" : 9, + "rangeY" : 8, + "ignoreFow" : true, + "exposeFow" : true +} +``` + +### Remove Object + +The effect completely removes the targeted object from the map. The Scuttle Boat spell is an example of this effect. The success chance is defined as [spell effect power](#spell-power). + +Parameters: + +- `objects` - a list of map objects that can be removed by this spell. +- `cursor` - identifier of the cursor that will be displayed when hovering over a valid target object. See `config/cursors.json` for more details. +- `ignoreFow` - If set to true, it is possible to use this spell to remove objects behind terra incognita. +- `rangeX` - maximum distance to remove objects in the X dimension (left-right axis). +- `rangeY` - maximum distance to remove objects in the Y dimension (top-bottom axis). + +Example: + +```json +"adventureEffect" : { + "type" : "removeObject", + "castsPerDay" : 0, + "cursor" : "mapScuttleBoat", + "rangeX" : 9, + "rangeY" : 8, + "ignoreFow" : false, + "objects" : { + "boat" : true + } +} +``` + +### Summon Boat + +The effect moves or creates a boat next to the hero who cast the spell. The success chance is defined as [spell effect power](#spell-power). + +Parameters: + +- `useExistingBoat` - If this is set to true, the spell can move existing boats to the hero's location. +- `createdBoat` - Optional identifier of the boat type that can be created by this spell. If this is not set, the spell cannot create new boats. + +Note that if the spell can both create new boats and use existing ones, it would prefer to move existing boats and only create new ones if there are no suitable ones to move. + +Example: + +```json +"adventureEffect" : { + "type" : "summonBoat", + "castsPerDay" : 0, + "useExistingBoat" : true, + "createdBoat" : "boatNecropolis" +} +``` + +### Town Portal + +Effect moves hero to a location of owned or allied town. + +Parameters: + +- `movementPointsRequired` - amount of movement points that hero must have to cast this spell +- `movementPointsTaken` - amount of movement points that will be taken on sucessful cast of the spell. If hero does not have enough movement points, they will be reduced to zero after cast +- `allowTownSelection` - if set to true, player will be able to select town to teleport to among all friendly non-occupied towns. +- `skipOccupiedTowns` - if set to true, hero will teleport to nearest non-occupied town, ignoring any closer towns that are occupied by a visiting hero. No effect if `allowTownSelection` is set. + +Example: + +```json +"adventureEffect" : { + "type" : "townPortal", + "castsPerDay" : 2, + "allowTownSelection" : false, + "skipOccupiedTowns" : false, + "movementPointsRequired" : 300, + "movementPointsTaken" : 300 +} +``` + +### View World + +Effect shows World View menu with specified objects behind FoW revealed to the player + +Parameters: + +- `objects` - list of object types that will be revealed on World View. Note that only following objects have assotiated icon, any objects not from this list will not be visible: `resource`, `mine`, `abandonedMine`, `artifact`, `hero`, `town`. +- `showTerrain` - if set to true, terrain of the entire map (but not objects on it) will be revealed to the player. + +Example: + +```json +"adventureEffect" : { + "type" : "viewWorld", + "objects" : { + "resource" : true, + "mine" : true, + "abandonedMine" : true + }, + "showTerrain" : true +} +``` diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 59658a3ae..bdeee78f6 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -268,12 +268,13 @@ set(lib_MAIN_SRCS spells/TargetCondition.cpp spells/ViewSpellInt.cpp + spells/adventure/AdventureSpellEffect.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/adventure/DimensionDoorEffect.cpp + spells/adventure/RemoveObjectEffect.cpp + spells/adventure/SummonBoatEffect.cpp + spells/adventure/TownPortalEffect.cpp + spells/adventure/ViewWorldEffect.cpp spells/effects/Catapult.cpp spells/effects/Clone.cpp @@ -731,11 +732,12 @@ set(lib_MAIN_HEADERS 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/adventure/AdventureSpellEffect.h + spells/adventure/DimensionDoorEffect.h + spells/adventure/RemoveObjectEffect.h + spells/adventure/SummonBoatEffect.h + spells/adventure/TownPortalEffect.h + spells/adventure/ViewWorldEffect.h spells/effects/Catapult.h spells/effects/Clone.h diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index a3132be39..0986d11f1 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -68,10 +68,6 @@ const std::vector GameSettings::settingProperties = {EGameSettings::CREATURES_JOINING_PERCENTAGE, "creatures", "joiningPercentage" }, {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, - {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, - {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, - {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles" }, - {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit" }, {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 693c30fd0..40e0561d3 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -41,10 +41,6 @@ enum class EGameSettings CREATURES_JOINING_PERCENTAGE, CREATURES_WEEKLY_GROWTH_CAP, CREATURES_WEEKLY_GROWTH_PERCENT, - DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, - DIMENSION_DOOR_FAILURE_SPENDS_POINTS, - DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, - DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, DIMENSION_DOOR_TRIGGERS_GUARDS, DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, DWELLINGS_ACCUMULATE_WHEN_OWNED, diff --git a/lib/callback/CDynLibHandler.cpp b/lib/callback/CDynLibHandler.cpp index 50e669fa6..6892a791c 100644 --- a/lib/callback/CDynLibHandler.cpp +++ b/lib/callback/CDynLibHandler.cpp @@ -67,6 +67,10 @@ VCMI_LIB_NAMESPACE_BEGIN getName = reinterpret_cast(dlsym(dll, "GetAiName")); getAI = reinterpret_cast(dlsym(dll, methodName.c_str())); } + else + { + logGlobal->error("Cannot open dynamic library '%s'. Reason: %s", libpath.string(), dlerror()); + } #endif // VCMI_WINDOWS if (!dll) diff --git a/lib/callback/CGameInfoCallback.h b/lib/callback/CGameInfoCallback.h index b214cdc8b..6ab6893ad 100644 --- a/lib/callback/CGameInfoCallback.h +++ b/lib/callback/CGameInfoCallback.h @@ -72,7 +72,7 @@ public: //map int3 guardingCreaturePosition (int3 pos) const override; std::vector getGuardingCreatures (int3 pos) const override; - bool isTileGuardedUnchecked(int3 tile) const; + bool isTileGuardedUnchecked(int3 tile) const override; const TerrainTile * getTile(int3 tile, bool verbose = true) const override; const TerrainTile * getTileUnchecked(int3 tile) const override; void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; diff --git a/lib/callback/EditorCallback.cpp b/lib/callback/EditorCallback.cpp index b29365fa0..ef9558cfc 100644 --- a/lib/callback/EditorCallback.cpp +++ b/lib/callback/EditorCallback.cpp @@ -62,6 +62,11 @@ const TerrainTile * EditorCallback::getTileUnchecked(int3) const THROW_EDITOR_UNSUPPORTED; } +bool EditorCallback::isTileGuardedUnchecked(int3 tile) const +{ + THROW_EDITOR_UNSUPPORTED; +} + const CGObjectInstance * EditorCallback::getTopObj(int3) const { THROW_EDITOR_UNSUPPORTED; diff --git a/lib/callback/EditorCallback.h b/lib/callback/EditorCallback.h index 157b38785..da0c7a097 100644 --- a/lib/callback/EditorCallback.h +++ b/lib/callback/EditorCallback.h @@ -32,6 +32,7 @@ public: const TerrainTile * getTile(int3 tile, bool verbose) const override; const TerrainTile * getTileUnchecked(int3 tile) const override; + bool isTileGuardedUnchecked(int3 tile) const override; const CGObjectInstance * getTopObj(int3 pos) const override; EDiggingStatus getTileDigStatus(int3 tile, bool verbose) const override; void calculatePaths(const std::shared_ptr & config) const override; diff --git a/lib/callback/IGameInfoCallback.h b/lib/callback/IGameInfoCallback.h index 342ac152b..84d86cbe2 100644 --- a/lib/callback/IGameInfoCallback.h +++ b/lib/callback/IGameInfoCallback.h @@ -147,6 +147,8 @@ public: virtual bool checkForVisitableDir(const int3 & src, const int3 & dst) const = 0; /// Returns all wandering monsters that guard specified tile virtual std::vector getGuardingCreatures (int3 pos) const = 0; + /// Returns if tile is guarded by wandering monsters without checking whether player has access to the tile. AVOID USAGE. + virtual bool isTileGuardedUnchecked(int3 tile) const = 0; /// Returns all tiles within specified range with specific tile visibility mode virtual void getTilesInRange(std::unordered_set & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional player = std::optional(), int3::EDistanceFormula formula = int3::DIST_2D) const = 0; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index def1e1a43..2294055da 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -856,16 +856,16 @@ public: NONE = -1, // Adventure map spells - SUMMON_BOAT = 0, - SCUTTLE_BOAT = 1, - VISIONS = 2, - VIEW_EARTH = 3, - DISGUISE = 4, - VIEW_AIR = 5, - FLY = 6, - WATER_WALK = 7, - DIMENSION_DOOR = 8, - TOWN_PORTAL = 9, + SUMMON_BOAT [[deprecated("check for spell mechanics instead of spell ID")]] = 0, + SCUTTLE_BOAT [[deprecated("check for spell mechanics instead of spell ID")]] = 1, + VISIONS [[deprecated("check for spell mechanics instead of spell ID")]] = 2, + VIEW_EARTH [[deprecated("check for spell mechanics instead of spell ID")]] = 3, + DISGUISE [[deprecated("check for spell mechanics instead of spell ID")]] = 4, + VIEW_AIR [[deprecated("check for spell mechanics instead of spell ID")]] = 5, + FLY [[deprecated("check for spell mechanics instead of spell ID")]] = 6, + WATER_WALK [[deprecated("check for spell mechanics instead of spell ID")]] = 7, + DIMENSION_DOOR [[deprecated("check for spell mechanics instead of spell ID")]] = 8, + TOWN_PORTAL [[deprecated("check for spell mechanics instead of spell ID")]] = 9, // Combat spells QUICKSAND = 10, diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index a3c04f2fb..154a6fcec 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -25,6 +25,7 @@ #include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" #include "../spells/CSpellHandler.h" +#include "spells/ISpellMechanics.h" VCMI_LIB_NAMESPACE_BEGIN @@ -502,7 +503,9 @@ CPathfinderHelper::CPathfinderHelper(const IGameInfoCallback & gameInfo, const C turn(-1), owner(Hero->tempOwner), hero(Hero), - options(Options) + options(Options), + canCastFly(false), + canCastWaterWalk(false) { turnsInfo.reserve(16); updateTurnInfo(); @@ -510,11 +513,20 @@ CPathfinderHelper::CPathfinderHelper(const IGameInfoCallback & gameInfo, const C whirlpoolProtection = Hero->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION); - SpellID flySpell = SpellID::FLY; - canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); + if (options.canUseCast) + { + for (const auto & spell : LIBRARY->spellh->objects) + { + if (!spell || !spell->isAdventure()) + continue; - SpellID waterWalk = SpellID::WATER_WALK; - canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell()); + if(spell->getAdventureMechanics().givesBonus(hero, BonusType::WATER_WALKING) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get())) + canCastWaterWalk = true; + + if(spell->getAdventureMechanics().givesBonus(hero, BonusType::FLYING_MOVEMENT) && hero->canCastThisSpell(spell.get()) && hero->mana >= hero->getSpellCost(spell.get())) + canCastFly = true; + } + } } CPathfinderHelper::~CPathfinderHelper() = default; diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 7f55193d2..141daf29a 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -536,12 +536,6 @@ CSpell::TargetInfo::TargetInfo(const CSpell * spell, const int level, spells::Mo clearAffected = levelInfo.clearAffected; } -bool DLL_LINKAGE isInScreenRange(const int3 & center, const int3 & pos) -{ - int3 diff = pos - center; - return diff.x >= -9 && diff.x <= 9 && diff.y >= -8 && diff.y <= 8; -} - ///CSpellHandler std::vector CSpellHandler::loadLegacyData() { @@ -997,6 +991,8 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c levelObject.cumulativeEffects.push_back(b); } + levelObject.adventureEffect = levelNode["adventureEffect"]; + if(!levelNode["battleEffects"].Struct().empty()) { levelObject.battleEffects = levelNode["battleEffects"]; diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 06d5fc106..025bd6378 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -101,6 +101,7 @@ public: std::vector> cumulativeEffects; //deprecated JsonNode battleEffects; + JsonNode adventureEffect; }; /** \brief Low level accessor. Don`t use it if absolutely necessary @@ -289,8 +290,6 @@ private: std::unique_ptr adventureMechanics;//(!) do not serialize }; -bool DLL_LINKAGE isInScreenRange(const int3 ¢er, const int3 &pos); //for spells like Dimension Door - class DLL_LINKAGE CSpellHandler: public CHandlerBase { std::vector spellRangeInHexes(std::string rng) const; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index f586ee45a..af9457ca9 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -27,11 +27,6 @@ #include "Problem.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" @@ -648,28 +643,10 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s) std::unique_ptr IAdventureSpellMechanics::createMechanics(const CSpell * s) { - switch(s->id.toEnum()) - { - case SpellID::SUMMON_BOAT: - return std::make_unique(s); - case SpellID::SCUTTLE_BOAT: - return std::make_unique(s); - case SpellID::DIMENSION_DOOR: - return std::make_unique(s); - case SpellID::FLY: - case SpellID::WATER_WALK: - case SpellID::VISIONS: - case SpellID::DISGUISE: - return std::make_unique(s); //implemented using bonus system - case SpellID::TOWN_PORTAL: - return std::make_unique(s); - case SpellID::VIEW_EARTH: - return std::make_unique(s); - case SpellID::VIEW_AIR: - return std::make_unique(s); - default: - return s->isCombat() ? std::unique_ptr() : std::make_unique(s); - } + if (s->isCombat()) + return nullptr; + + return std::make_unique(s); } VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 3ef011a72..3439d6c67 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -30,6 +30,7 @@ class JsonNode; class CStack; class CGObjectInstance; class CGHeroInstance; +class IAdventureSpellEffect; namespace spells { @@ -354,11 +355,20 @@ public: virtual bool canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const = 0; virtual bool canBeCastAt(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const = 0; - virtual bool adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const = 0; static std::unique_ptr createMechanics(const CSpell * s); + + virtual bool givesBonus(const spells::Caster * caster, BonusType which) const = 0; + + template + const EffectType * getEffectAs(const spells::Caster * caster) const + { + return dynamic_cast(getEffect(caster)); + } protected: + virtual const IAdventureSpellEffect * getEffect(const spells::Caster * caster) const = 0; + const CSpell * owner; }; diff --git a/lib/spells/adventure/AdventureSpellEffect.cpp b/lib/spells/adventure/AdventureSpellEffect.cpp new file mode 100644 index 000000000..cf7958412 --- /dev/null +++ b/lib/spells/adventure/AdventureSpellEffect.cpp @@ -0,0 +1,46 @@ +/* + * AdventureSpellEffect.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 "AdventureSpellEffect.h" + +#include "../../json/JsonNode.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../callback/IGameInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +AdventureSpellRangedEffect::AdventureSpellRangedEffect(const JsonNode & config) + : rangeX(config["rangeX"].Integer()) + , rangeY(config["rangeY"].Integer()) + , ignoreFow(config["ignoreFow"].Bool()) +{ +} + +bool AdventureSpellRangedEffect::isTargetInRange(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if(!cb->isInTheMap(pos)) + return false; + + if(caster->getHeroCaster()) + { + int3 center = caster->getHeroCaster()->getSightCenter(); + + int3 diff = pos - center; + return diff.x >= -rangeX && diff.x <= rangeX && diff.y >= -rangeY && diff.y <= rangeY; + } + + if(!ignoreFow && !cb->isVisibleFor(pos, caster->getCasterOwner())) + return false; + + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/AdventureSpellEffect.h b/lib/spells/adventure/AdventureSpellEffect.h new file mode 100644 index 000000000..c71c0e07b --- /dev/null +++ b/lib/spells/adventure/AdventureSpellEffect.h @@ -0,0 +1,60 @@ +/* + * AdventureSpellEffect.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 : int8_t +{ + 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; + +class IAdventureSpellEffect +{ +public: + virtual ~IAdventureSpellEffect() = default; + + virtual ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const {return ESpellCastResult::OK;}; + virtual ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const {return ESpellCastResult::OK;}; + virtual void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const {}; + virtual bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const {return true;}; + virtual bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return true;}; + virtual std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const {return {};}; +}; + +class AdventureSpellEffect final : public IAdventureSpellEffect +{ +public: + AdventureSpellEffect() = default; +}; + +class DLL_LINKAGE AdventureSpellRangedEffect : public IAdventureSpellEffect +{ + int rangeX; + int rangeY; + bool ignoreFow; + +public: + AdventureSpellRangedEffect(const JsonNode & config); + + bool isTargetInRange(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const; + std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override = 0; //must be implemented in derived classes + bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override = 0; //must be implemented in derived classes +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/AdventureSpellMechanics.cpp b/lib/spells/adventure/AdventureSpellMechanics.cpp index f5d5f6a46..cb51c356e 100644 --- a/lib/spells/adventure/AdventureSpellMechanics.cpp +++ b/lib/spells/adventure/AdventureSpellMechanics.cpp @@ -11,17 +11,86 @@ #include "StdInc.h" #include "AdventureSpellMechanics.h" +#include "AdventureSpellEffect.h" +#include "DimensionDoorEffect.h" +#include "RemoveObjectEffect.h" +#include "SummonBoatEffect.h" +#include "TownPortalEffect.h" +#include "ViewWorldEffect.h" + #include "../CSpellHandler.h" #include "../Problem.h" +#include "../../json/JsonBonus.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../networkPacks/PacksForClient.h" +#include "../../callback/IGameInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN +std::unique_ptr AdventureSpellMechanics::createAdventureEffect(const CSpell * s, const JsonNode & node) +{ + const std::string & typeID = node["type"].String(); + + if(typeID == "generic") + return std::make_unique(); + if(typeID == "dimensionDoor") + return std::make_unique(s, node); + if(typeID == "removeObject") + return std::make_unique(s, node); + if(typeID == "summonBoat") + return std::make_unique(s, node); + if(typeID == "townPortal") + return std::make_unique(s, node); + if(typeID == "viewWorld") + return std::make_unique(s, node); + + return std::make_unique(); +} + AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s) : IAdventureSpellMechanics(s) { + for(int level = 0; level < GameConstants::SPELL_SCHOOL_LEVELS; level++) + { + const JsonNode & config = s->getLevelInfo(level).adventureEffect; + + levelOptions[level].effect = createAdventureEffect(s, config); + levelOptions[level].castsPerDay = config["castsPerDay"].Integer(); + levelOptions[level].castsPerDayXL = config["castsPerDayXL"].Integer(); + + levelOptions[level].bonuses = s->getLevelInfo(level).effects; + + for(const auto & elem : config["bonuses"].Struct()) + { + auto b = JsonUtils::parseBonus(elem.second); + b->sid = BonusSourceID(s->id); + b->source = BonusSource::SPELL_EFFECT; + levelOptions[level].bonuses.push_back(b); + } + } +} + +AdventureSpellMechanics::~AdventureSpellMechanics() = default; + +const AdventureSpellMechanics::LevelOptions & AdventureSpellMechanics::getLevel(const spells::Caster * caster) const +{ + int schoolLevel = caster->getSpellSchoolLevel(owner); + return levelOptions.at(schoolLevel); +} + +const IAdventureSpellEffect * AdventureSpellMechanics::getEffect(const spells::Caster * caster) const +{ + return getLevel(caster).effect.get(); +} + +bool AdventureSpellMechanics::givesBonus(const spells::Caster * caster, BonusType which) const +{ + for (const auto & bonus : getLevel(caster).bonuses) + if (bonus->type == which) + return true; + + return false; } bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const @@ -44,24 +113,30 @@ bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const IGameIn if(heroCaster->mana < cost) return false; + + 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(); + int3 mapSize = cb->getMapSize(); + bool mapSizeIsAtLeastXL = mapSize.x * mapSize.y * mapSize.z >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD; + bool useAlternativeLimit = mapSizeIsAtLeastXL && getLevel(caster).castsPerDayXL != 0; + int castsLimit = useAlternativeLimit ? getLevel(caster).castsPerDayXL : getLevel(caster).castsPerDay; + + if(castsLimit > 0 && castsLimit <= castsAlreadyPerformedThisTurn ) //limit casts per turn + { + MetaString message = MetaString::createFromTextID("core.genrltxt.338"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; + } } - return canBeCastImpl(problem, cb, caster); + return getLevel(caster).effect->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; + return canBeCast(problem, cb, caster) && getLevel(caster).effect->canBeCastAtImpl(problem, cb, caster, pos); } bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const @@ -71,7 +146,7 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad if(!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos)) return false; - ESpellCastResult result = beginCast(env, parameters); + ESpellCastResult result = getLevel(parameters.caster).effect->beginCast(env, parameters, *this); if(result == ESpellCastResult::OK) performCast(env, parameters); @@ -79,43 +154,21 @@ bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const Ad return result != ESpellCastResult::ERROR; } -ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +void AdventureSpellMechanics::giveBonuses(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { - if(owner->hasEffects()) + for(const auto & b : getLevel(parameters.caster).bonuses) { - //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; + GiveBonus gb; + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); + gb.bonus = *b; + gb.bonus.duration = parameters.caster->getEnchantPower(owner); + env->apply(gb); } - 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 + 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); } void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const @@ -128,16 +181,13 @@ void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const Adve asc.spellID = owner->id; env->apply(asc); - ESpellCastResult result = applyAdventureEffects(env, parameters); + ESpellCastResult result = getLevel(parameters.caster).effect->applyAdventureEffects(env, parameters); - switch(result) + if (result == ESpellCastResult::OK) { - case ESpellCastResult::OK: - parameters.caster->spendMana(env, cost); - endCast(env, parameters); - break; - default: - break; + giveBonuses(env, parameters); + parameters.caster->spendMana(env, cost); + getLevel(parameters.caster).effect->endCast(env, parameters); } } diff --git a/lib/spells/adventure/AdventureSpellMechanics.h b/lib/spells/adventure/AdventureSpellMechanics.h index ae352272e..5508f9f2f 100644 --- a/lib/spells/adventure/AdventureSpellMechanics.h +++ b/lib/spells/adventure/AdventureSpellMechanics.h @@ -14,33 +14,36 @@ 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 IAdventureSpellEffect; -class AdventureSpellMechanics : public IAdventureSpellMechanics +class AdventureSpellMechanics final : public IAdventureSpellMechanics, boost::noncopyable { + struct LevelOptions + { + std::unique_ptr effect; + std::vector> bonuses; + int castsPerDay; + int castsPerDayXL; + }; + + std::array levelOptions; + + const LevelOptions & getLevel(const spells::Caster * caster) const; + void giveBonuses(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + std::unique_ptr createAdventureEffect(const CSpell * s, const JsonNode & node); + 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; + ~AdventureSpellMechanics(); void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; + +private: + 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 final; + const IAdventureSpellEffect * getEffect(const spells::Caster * caster) const final; + bool givesBonus(const spells::Caster * caster, BonusType which) const final; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/DimensionDoorEffect.cpp b/lib/spells/adventure/DimensionDoorEffect.cpp new file mode 100644 index 000000000..11f99ed30 --- /dev/null +++ b/lib/spells/adventure/DimensionDoorEffect.cpp @@ -0,0 +1,141 @@ +/* + * DimensionDoorEffect.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 "DimensionDoorEffect.h" + +#include "../CSpellHandler.h" + +#include "../../IGameSettings.h" +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/TerrainTile.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +DimensionDoorEffect::DimensionDoorEffect(const CSpell * s, const JsonNode & config) + : AdventureSpellRangedEffect(config) + , cursor(config["cursor"].String()) + , cursorGuarded(config["cursorGuarded"].String()) + , movementPointsRequired(config["movementPointsRequired"].Integer()) + , movementPointsTaken(config["movementPointsTaken"].Integer()) + , waterLandFailureTakesPoints(config["waterLandFailureTakesPoints"].Bool()) + , exposeFow(config["exposeFow"].Bool()) +{ +} + +std::string DimensionDoorEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if(!cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS)) + return cursor; + + if (!exposeFow && !cb->isVisibleFor(pos, caster->getCasterOwner())) + return cursor; + + if (!cb->isTileGuardedUnchecked(pos)) + return cursor; + + return cursorGuarded; +} + +bool DimensionDoorEffect::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +{ + if(!caster->getHeroCaster()) + return false; + + if(caster->getHeroCaster()->movementPointsRemaining() <= movementPointsRequired) + { + problem.add(MetaString::createFromTextID("core.genrltxt.125")); + return false; + } + + return true; +} + +bool DimensionDoorEffect::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if (!isTargetInRange(cb, caster, pos)) + 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(exposeFow) + { + if(!dest->isClear(curr)) + return false; + } + else + { + if(dest->blocked()) + return false; + } + + return true; +} + +ESpellCastResult DimensionDoorEffect::applyAdventureEffects(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)) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + + // tile is either blocked or not possible to move (e.g. water <-> land) + if(waterLandFailureTakesPoints) + { + // 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; + } + } + + SetMovePoints smp; + smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); + if(movementPointsTaken < static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining())) + smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementPointsTaken; + else + smp.val = 0; + env->apply(smp); + + return ESpellCastResult::OK; +} + +void DimensionDoorEffect::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/DimensionDoorEffect.h b/lib/spells/adventure/DimensionDoorEffect.h new file mode 100644 index 000000000..16f2111ad --- /dev/null +++ b/lib/spells/adventure/DimensionDoorEffect.h @@ -0,0 +1,37 @@ +/* + * DimensionDoorEffect.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 "AdventureSpellEffect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DimensionDoorEffect final : public AdventureSpellRangedEffect +{ + std::string cursor; + std::string cursorGuarded; + int movementPointsRequired; + int movementPointsTaken; + bool waterLandFailureTakesPoints; + bool exposeFow; + +public: + DimensionDoorEffect(const CSpell * s, const JsonNode & config); + +private: + bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final; + bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final; + std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/DimensionDoorMechanics.cpp b/lib/spells/adventure/DimensionDoorMechanics.cpp deleted file mode 100644 index 72046d965..000000000 --- a/lib/spells/adventure/DimensionDoorMechanics.cpp +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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/TerrainTile.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 deleted file mode 100644 index cf5e32b6f..000000000 --- a/lib/spells/adventure/DimensionDoorMechanics.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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/RemoveObjectEffect.cpp b/lib/spells/adventure/RemoveObjectEffect.cpp new file mode 100644 index 000000000..2e1a5595f --- /dev/null +++ b/lib/spells/adventure/RemoveObjectEffect.cpp @@ -0,0 +1,84 @@ +/* + * RemoveObjectEffect.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 "RemoveObjectEffect.h" + +#include "../CSpellHandler.h" + +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/CMap.h" +#include "../../networkPacks/PacksForClient.h" +#include "../../modding/IdentifierStorage.h" + +VCMI_LIB_NAMESPACE_BEGIN + +RemoveObjectEffect::RemoveObjectEffect(const CSpell * s, const JsonNode & config) + : AdventureSpellRangedEffect(config) + , owner(s) + , failMessage(MetaString::createFromTextID("core.genrltxt.337")) //%s tried to scuttle the boat, but failed + , cursor(config["cursor"].String()) +{ + for(const auto & objectNode : config["objects"].Struct()) + { + if(objectNode.second.Bool()) + { + LIBRARY->identifiers()->requestIdentifier(objectNode.second.getModScope(), "object", objectNode.first, [this](si32 index) + { + removedObjects.push_back(MapObjectID(index)); + }); + } + } +} + +std::string RemoveObjectEffect::getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + return cursor; +} + +bool RemoveObjectEffect::canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if (!isTargetInRange(cb, caster, pos)) + return false; + + const TerrainTile * t = cb->getTileUnchecked(pos); + if(!t || t->visitableObjects.empty()) + return false; + + const CGObjectInstance * topObject = cb->getObj(t->visitableObjects.back()); + + return vstd::contains(removedObjects, topObject->ID); +} + +ESpellCastResult RemoveObjectEffect::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 = failMessage; + 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/RemoveObjectEffect.h b/lib/spells/adventure/RemoveObjectEffect.h new file mode 100644 index 000000000..4141d166b --- /dev/null +++ b/lib/spells/adventure/RemoveObjectEffect.h @@ -0,0 +1,33 @@ +/* + * RemoveObjectEffect.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 "AdventureSpellEffect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class RemoveObjectEffect final : public AdventureSpellRangedEffect +{ + const CSpell * owner; + std::vector removedObjects; + MetaString failMessage; + std::string cursor; + +public: + RemoveObjectEffect(const CSpell * s, const JsonNode & config); + +private: + bool canBeCastAtImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final; + std::string getCursorForTarget(const IGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const final; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/ScuttleBoatMechanics.cpp b/lib/spells/adventure/ScuttleBoatMechanics.cpp deleted file mode 100644 index 24b8bfe8b..000000000 --- a/lib/spells/adventure/ScuttleBoatMechanics.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 deleted file mode 100644 index dec0b4037..000000000 --- a/lib/spells/adventure/ScuttleBoatMechanics.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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/SummonBoatEffect.cpp similarity index 53% rename from lib/spells/adventure/SummonBoatMechanics.cpp rename to lib/spells/adventure/SummonBoatEffect.cpp index 6d4a0341d..6216ece26 100644 --- a/lib/spells/adventure/SummonBoatMechanics.cpp +++ b/lib/spells/adventure/SummonBoatEffect.cpp @@ -1,5 +1,5 @@ /* - * SummonBoatMechanics.cpp, part of VCMI engine + * SummonBoatEffect.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -10,23 +10,44 @@ #include "StdInc.h" -#include "SummonBoatMechanics.h" +#include "SummonBoatEffect.h" #include "../CSpellHandler.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../mapObjects/MiscObjects.h" #include "../../mapping/CMap.h" +#include "../../modding/IdentifierStorage.h" #include "../../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN -SummonBoatMechanics::SummonBoatMechanics(const CSpell * s) - : AdventureSpellMechanics(s) +SummonBoatEffect::SummonBoatEffect(const CSpell * s, const JsonNode & config) + : owner(s) + , useExistingBoat(config["useExistingBoat"].Bool()) { + if (!config["createdBoat"].isNull()) + { + LIBRARY->identifiers()->requestIdentifier("core:boat", config["createdBoat"], [=](int32_t boatTypeID) + { + createdBoat = BoatId(boatTypeID); + }); + } + } -bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const +bool SummonBoatEffect::canCreateNewBoat() const +{ + return createdBoat != BoatId::NONE; +} + +int SummonBoatEffect::getSuccessChance(const spells::Caster * caster) const +{ + const auto schoolLevel = caster->getSpellSchoolLevel(owner); + return owner->getLevelPower(schoolLevel); +} + +bool SummonBoatEffect::canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const { if(!caster->getHeroCaster()) return false; @@ -52,12 +73,10 @@ bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const IGameIn return true; } -ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +ESpellCastResult SummonBoatEffect::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 + if(env->getRNG()->nextInt(0, 99) >= getSuccessChance(parameters.caster)) //power is % chance of success { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -69,17 +88,21 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment //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 + if (useExistingBoat) + { + double dist = 0; + for(const auto & b : env->getMap()->getObjects()) { - nearest = b; - dist = nDist; + 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; + } } } @@ -93,7 +116,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment cop.initiator = parameters.caster->getCasterOwner(); env->apply(cop); } - else if(schoolLevel < 2) //none or basic level -> cannot create boat :( + else if(!canCreateNewBoat()) //none or basic level -> cannot create boat :( { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -103,7 +126,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment } else //create boat { - env->createBoat(summonPos, BoatId::NECROPOLIS, parameters.caster->getCasterOwner()); + env->createBoat(summonPos, createdBoat, parameters.caster->getCasterOwner()); } return ESpellCastResult::OK; } diff --git a/lib/spells/adventure/SummonBoatEffect.h b/lib/spells/adventure/SummonBoatEffect.h new file mode 100644 index 000000000..7dd1307ad --- /dev/null +++ b/lib/spells/adventure/SummonBoatEffect.h @@ -0,0 +1,34 @@ +/* + * SummonBoatEffect.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 "AdventureSpellEffect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE SummonBoatEffect final : public IAdventureSpellEffect +{ + const CSpell * owner; + BoatId createdBoat = BoatId::NONE; + bool useExistingBoat; + +public: + SummonBoatEffect(const CSpell * s, const JsonNode & config); + + bool canCreateNewBoat() const; + int getSuccessChance(const spells::Caster * caster) const; + +private: + bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const final; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const final; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/TownPortalMechanics.cpp b/lib/spells/adventure/TownPortalEffect.cpp similarity index 78% rename from lib/spells/adventure/TownPortalMechanics.cpp rename to lib/spells/adventure/TownPortalEffect.cpp index e67df29d7..f4f985a74 100644 --- a/lib/spells/adventure/TownPortalMechanics.cpp +++ b/lib/spells/adventure/TownPortalEffect.cpp @@ -1,5 +1,5 @@ /* - * TownPortalMechanics.cpp, part of VCMI engine + * TownPortalEffect.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,7 +9,9 @@ */ #include "StdInc.h" -#include "TownPortalMechanics.h" +#include "TownPortalEffect.h" + +#include "AdventureSpellMechanics.h" #include "../CSpellHandler.h" @@ -23,15 +25,18 @@ VCMI_LIB_NAMESPACE_BEGIN -TownPortalMechanics::TownPortalMechanics(const CSpell * s): - AdventureSpellMechanics(s) +TownPortalEffect::TownPortalEffect(const CSpell * s, const JsonNode & config) + : owner(s) + , movementPointsRequired(config["movementPointsRequired"].Integer()) + , movementPointsTaken(config["movementPointsTaken"].Integer()) + , allowTownSelection(config["allowTownSelection"].Bool()) + , skipOccupiedTowns(config["skipOccupiedTowns"].Bool()) { } -ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +ESpellCastResult TownPortalEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { const CGTownInstance * destination = nullptr; - const int moveCost = movementCost(env, parameters); if(!parameters.caster->getHeroCaster()) { @@ -39,7 +44,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment return ESpellCastResult::ERROR; } - if(parameters.caster->getSpellSchoolLevel(owner) < 2) + if(!allowTownSelection) { std::vector pool = getPossibleTowns(env, parameters); destination = findNearestTown(env, parameters, pool); @@ -47,7 +52,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment if(nullptr == destination) return ESpellCastResult::ERROR; - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsRequired) return ESpellCastResult::ERROR; if(destination->getVisitingHero()) @@ -98,7 +103,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment return ESpellCastResult::ERROR; } - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsRequired) { env->complain("This hero has not enough movement points!"); return ESpellCastResult::ERROR; @@ -131,9 +136,8 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment return ESpellCastResult::OK; } -void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +void TownPortalEffect::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { - const int moveCost = movementCost(env, parameters); const CGTownInstance * destination = nullptr; if(parameters.caster->getSpellSchoolLevel(owner) < 2) @@ -154,12 +158,15 @@ void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpe { SetMovePoints smp; smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId()); - smp.val = std::max(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost); + if(movementPointsTaken < static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining())) + smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementPointsTaken; + else + smp.val = 0; env->apply(smp); } } -ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +ESpellCastResult TownPortalEffect::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const { std::vector towns = getPossibleTowns(env, parameters); @@ -178,9 +185,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons return ESpellCastResult::CANCEL; } - const int moveCost = movementCost(env, parameters); - - if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost) + if(static_cast(parameters.caster->getHeroCaster()->movementPointsRemaining()) < movementPointsTaken) { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -191,7 +196,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) { - auto queryCallback = [this, env, parameters](std::optional reply) -> void + auto queryCallback = [&mechanics, env, parameters](std::optional reply) -> void { if(reply.has_value()) { @@ -213,7 +218,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons AdventureSpellCastParameters p; p.caster = parameters.caster; p.pos = o->visitablePos(); - performCast(env, p); + mechanics.performCast(env, p); } }; @@ -247,7 +252,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons return ESpellCastResult::OK; } -const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const +const CGTownInstance * TownPortalEffect::findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const { if(pool.empty()) return nullptr; @@ -271,7 +276,7 @@ const CGTownInstance * TownPortalMechanics::findNearestTown(SpellCastEnvironment return *nearest; } -std::vector TownPortalMechanics::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +std::vector TownPortalEffect::getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { std::vector ret; @@ -281,19 +286,11 @@ std::vector TownPortalMechanics::getPossibleTowns(SpellC { for(auto currTown : env->getCb()->getPlayerState(color)->getTowns()) { - ret.push_back(currTown); + if (!skipOccupiedTowns || currTown->getVisitingHero() == nullptr) + 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/TownPortalEffect.h similarity index 61% rename from lib/spells/adventure/TownPortalMechanics.h rename to lib/spells/adventure/TownPortalEffect.h index e0d227678..5011b26f0 100644 --- a/lib/spells/adventure/TownPortalMechanics.h +++ b/lib/spells/adventure/TownPortalEffect.h @@ -1,5 +1,5 @@ /* - * TownPortalMechanics.h, part of VCMI engine + * TownPortalEffect.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -10,25 +10,31 @@ #pragma once -#include "AdventureSpellMechanics.h" +#include "AdventureSpellEffect.h" VCMI_LIB_NAMESPACE_BEGIN class CGTownInstance; -class TownPortalMechanics final : public AdventureSpellMechanics +class DLL_LINKAGE TownPortalEffect final : public IAdventureSpellEffect { -public: - TownPortalMechanics(const CSpell * s); + const CSpell * owner; + int movementPointsRequired; + int movementPointsTaken; + bool allowTownSelection; + bool skipOccupiedTowns; -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; +public: + TownPortalEffect(const CSpell * s, const JsonNode & config); + + int getMovementPointsRequired() const { return movementPointsRequired; } + bool townSelectionAllowed() const { return allowTownSelection; } private: + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const AdventureSpellMechanics & mechanics) const override; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; 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; }; diff --git a/lib/spells/adventure/ViewWorldEffect.cpp b/lib/spells/adventure/ViewWorldEffect.cpp new file mode 100644 index 000000000..52fa6773e --- /dev/null +++ b/lib/spells/adventure/ViewWorldEffect.cpp @@ -0,0 +1,68 @@ +/* + * ViewWorldEffect.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 "ViewWorldEffect.h" + +#include "../CSpellHandler.h" + +#include "../../CPlayerState.h" +#include "../../callback/IGameInfoCallback.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../mapping/CMap.h" +#include "../../modding/IdentifierStorage.h" +#include "../../networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ViewWorldEffect::ViewWorldEffect(const CSpell * s, const JsonNode & config) +{ + showTerrain = config["showTerrain"].Bool(); + + for(const auto & objectNode : config["objects"].Struct()) + { + if(objectNode.second.Bool()) + { + LIBRARY->identifiers()->requestIdentifier(objectNode.second.getModScope(), "object", objectNode.first, [this](si32 index) + { + filteredObjects.push_back(MapObjectID(index)); + }); + } + } +} + +ESpellCastResult ViewWorldEffect::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + ShowWorldViewEx pack; + + pack.player = parameters.caster->getCasterOwner(); + pack.showTerrain = showTerrain; + + 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 && vstd::contains(filteredObjects, obj->ID)) + { + ObjectPosInfo posInfo(obj); + + if(fowMap[posInfo.pos.z][posInfo.pos.x][posInfo.pos.y] == 0) + pack.objectPositions.push_back(posInfo); + } + } + + env->apply(pack); + + return ESpellCastResult::OK; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/SummonBoatMechanics.h b/lib/spells/adventure/ViewWorldEffect.h similarity index 55% rename from lib/spells/adventure/SummonBoatMechanics.h rename to lib/spells/adventure/ViewWorldEffect.h index 553abaa9c..c86db98af 100644 --- a/lib/spells/adventure/SummonBoatMechanics.h +++ b/lib/spells/adventure/ViewWorldEffect.h @@ -1,5 +1,5 @@ /* - * SummonBoatMechanics.h, part of VCMI engine + * ViewWorldEffect.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -10,17 +10,17 @@ #pragma once -#include "AdventureSpellMechanics.h" +#include "AdventureSpellEffect.h" VCMI_LIB_NAMESPACE_BEGIN -class SummonBoatMechanics final : public AdventureSpellMechanics +class ViewWorldEffect final : public IAdventureSpellEffect { -public: - SummonBoatMechanics(const CSpell * s); + std::vector filteredObjects; + bool showTerrain = false; -protected: - bool canBeCastImpl(spells::Problem & problem, const IGameInfoCallback * cb, const spells::Caster * caster) const override; +public: + ViewWorldEffect(const CSpell * s, const JsonNode & config); ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; }; diff --git a/lib/spells/adventure/ViewWorldMechanics.cpp b/lib/spells/adventure/ViewWorldMechanics.cpp deleted file mode 100644 index 7d7c45aa9..000000000 --- a/lib/spells/adventure/ViewWorldMechanics.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 deleted file mode 100644 index 2e82babf2..000000000 --- a/lib/spells/adventure/ViewWorldMechanics.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index d12e825d9..9e7848de5 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -78,6 +78,7 @@ public: MOCK_CONST_METHOD1(guardingCreaturePosition, int3(int3 pos)); MOCK_CONST_METHOD2(checkForVisitableDir, bool(const int3 & src, const int3 & dst)); MOCK_CONST_METHOD1(getGuardingCreatures, std::vector(int3 pos)); + MOCK_CONST_METHOD1(isTileGuardedUnchecked, bool(int3 pos)); MOCK_METHOD2(pickAllowedArtsSet, void(std::vector & out, vstd::RNG & rand));