diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index b463ab6d5..767c3a3c9 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -117,6 +117,8 @@ "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", + + "vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 35c8b588b..df7f46f28 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -112,6 +112,8 @@ "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", + + "vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index e9db843e3..fc2d2a61c 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -45,6 +45,8 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapping/CMapDefines.h" #include "../../lib/pathfinder/CGPathNode.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/Problem.h" std::shared_ptr adventureInt; @@ -506,11 +508,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition) if(!shortcuts->optionMapViewActive()) return; - if(!LOCPLINT->cb->isVisible(targetPosition)) - { - if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR) - return; - } if(!LOCPLINT->makingTurn) return; @@ -519,23 +516,16 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition) if(spellBeingCasted) { assert(shortcuts->optionSpellcasting()); + assert(spellBeingCasted->id == SpellID::SCUTTLE_BOAT || spellBeingCasted->id == SpellID::DIMENSION_DOOR); - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: - if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::SCUTTLE_BOAT)) - performSpellcasting(targetPosition); - break; - case SpellID::DIMENSION_DOOR: - if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::DIMENSION_DOOR)) - performSpellcasting(targetPosition); - break; - default: - break; - } - + if(isValidAdventureSpellTarget(targetPosition)) + performSpellcasting(targetPosition); return; } + + if(!LOCPLINT->cb->isVisible(targetPosition)) + return; + //check if we can select this object bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES; @@ -610,21 +600,45 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition) return; bool isTargetPositionVisible = LOCPLINT->cb->isVisible(targetPosition); + const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr; + + if(spellBeingCasted) + { + switch(spellBeingCasted->id) + { + case SpellID::SCUTTLE_BOAT: + if(isValidAdventureSpellTarget(targetPosition)) + CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); + else + CCS->curh->set(Cursor::Map::POINTER); + return; + + case SpellID::DIMENSION_DOOR: + if(isValidAdventureSpellTarget(targetPosition)) + { + if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition)) + CCS->curh->set(Cursor::Map::T1_ATTACK); + else + CCS->curh->set(Cursor::Map::TELEPORT); + return; + } + else + CCS->curh->set(Cursor::Map::POINTER); + return; + default: + CCS->curh->set(Cursor::Map::POINTER); + return; + } + } if(!isTargetPositionVisible) { - GH.statusbar()->clear(); - - if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR) - { - CCS->curh->set(Cursor::Map::POINTER); - return; - } + CCS->curh->set(Cursor::Map::POINTER); + return; } auto objRelations = PlayerRelations::ALLIES; - const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr; if(objAtTile) { objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner); @@ -638,40 +652,6 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition) GH.statusbar()->write(tileTooltipText); } - if(spellBeingCasted) - { - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: - if(isValidAdventureSpellTarget(targetPosition, objAtTile, SpellID::SCUTTLE_BOAT)) - CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); - else - CCS->curh->set(Cursor::Map::POINTER); - return; - case SpellID::DIMENSION_DOOR: - if(isValidAdventureSpellTarget(targetPosition, objAtTile, SpellID::DIMENSION_DOOR)) - { - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS)) - { - auto isGuarded = LOCPLINT->cb->isTileGuardedAfterDimensionDoorUse(targetPosition, LOCPLINT->localState->getCurrentHero()); - if(isGuarded) - { - CCS->curh->set(Cursor::Map::T1_ATTACK); - return; - } - } - - CCS->curh->set(Cursor::Map::TELEPORT); - } - else - CCS->curh->set(Cursor::Map::POINTER); - return; - default: - CCS->curh->set(Cursor::Map::POINTER); - return; - } - } - if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown()) { if(objAtTile) @@ -945,34 +925,9 @@ void AdventureMapInterface::onScreenResize() activate(); } -bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId) +bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition) const { - int3 heroPosition = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - if (!isInScreenRange(heroPosition, targetPosition)) - { - return false; - } + spells::detail::ProblemImpl problem; - switch(spellId) - { - case SpellID::SCUTTLE_BOAT: - { - if(topObjectAtTarget && topObjectAtTarget->ID == Obj::BOAT) - return true; - else - return false; - } - case SpellID::DIMENSION_DOOR: - { - const TerrainTile * t = LOCPLINT->cb->getTileForDimensionDoor(targetPosition, LOCPLINT->localState->getCurrentHero()); - - if(t && t->isClear(LOCPLINT->cb->getTile(heroPosition))) - return true; - else - return false; - } - default: - logGlobal->warn("Called AdventureMapInterface::isValidAdventureSpellTarget with unknown Spell ID!"); - return false; - } + return spellBeingCasted->getAdventureMechanics().canBeCastAt(problem, LOCPLINT->cb.get(), LOCPLINT->localState->getCurrentHero(), targetPosition); } diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 36c19c334..cd63e5b24 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -93,7 +93,7 @@ private: void performSpellcasting(const int3 & castTarget); /// performs clientside validation of valid targets for adventure spells - bool isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId); + bool isValidAdventureSpellTarget(int3 targetPosition) const; /// dim interface if some windows opened void dim(Canvas & to); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 8c3ab46e9..d63cc2b72 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -40,6 +40,7 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" #include "../../lib/GameConstants.h" @@ -653,12 +654,25 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage; }); - if(mySpell->getTargetType() == spells::AimType::LOCATION) - adventureInt->enterCastingMode(mySpell); - else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) - owner->myInt->cb->castSpell(h, mySpell->id); + spells::detail::ProblemImpl problem; + if (mySpell->getAdventureMechanics().canBeCast(problem, LOCPLINT->cb.get(), owner->myHero)) + { + if(mySpell->getTargetType() == spells::AimType::LOCATION) + 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"); + } else - logGlobal->error("Invalid spell target type"); + { + std::vector texts; + problem.getAll(texts); + if(!texts.empty()) + LOCPLINT->showInfoDialog(texts.front()); + else + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); + } } } } diff --git a/config/gameConfig.json b/config/gameConfig.json index 19ef79356..71d0c3aee 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -395,8 +395,10 @@ "dimensionDoorOnlyToUncoveredTiles" : false, // if enabled, dimension door will hint regarding tile being incompatible terrain type, unlike H3 (water/land) "dimensionDoorExposesTerrainType" : false, + // if enabled, attempt to use dimension door on incompatible terrain (water/land) will result in spending of mana, movement and casts per day (H3 behavior) + "dimensionDoorFailureSpendsPoints" : true, // if enabled, dimension door will initiate a fight upon landing on tile adjacent to neutral creature - "dimensionDoorTriggersGuards" : false, + "dimensionDoorTriggersGuards" : true, // if enabled, dimension door can be used 1x per day, exception being 2x per day for XL+U or bigger maps (41472 tiles) + hero having expert air magic "dimensionDoorTournamentRulesLimit" : false }, diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 514740528..4385fad42 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -287,22 +287,9 @@ std::vector CGameInfoCallback::getGuardingCreatures (in return ret; } -bool CGameInfoCallback::isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) const +bool CGameInfoCallback::isTileGuardedUnchecked(int3 tile) const { - //for known tiles this is just potential convenience info, for tiles behind fog of war this info matches HotA but not H3 so make it accessible only with proper setting on - bool canAccessInfo = false; - - if(isVisible(tile)) - canAccessInfo = true; - else if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) - && isInScreenRange(castingHero->getSightCenter(), tile) - && castingHero->canCastThisSpell(static_cast(SpellID::DIMENSION_DOOR).toSpell())) - canAccessInfo = true; //TODO: check if available casts > 0, before adding that check make dimension door daily limit popup trigger on spell pick - - if(canAccessInfo) - return !gs->guardingCreatures(tile).empty(); - - return false; + return !gs->guardingCreatures(tile).empty(); } bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject) const @@ -528,40 +515,12 @@ const TerrainTile * CGameInfoCallback::getTile(int3 tile, bool verbose) const return nullptr; } -const TerrainTile * CGameInfoCallback::getTileForDimensionDoor(int3 tile, const CGHeroInstance * castingHero) const +const TerrainTile * CGameInfoCallback::getTileUnchecked(int3 tile) const { - auto outputTile = getTile(tile, false); + if (isInTheMap(tile)) + return &gs->map->getTile(tile); - if(outputTile != nullptr) - return outputTile; - - bool allowOnlyToUncoveredTiles = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES); - - if(!allowOnlyToUncoveredTiles) - { - if(castingHero->canCastThisSpell(static_cast(SpellID::DIMENSION_DOOR).toSpell()) - && isInScreenRange(castingHero->getSightCenter(), tile)) - { //TODO: check if available casts > 0, before adding that check make dimension door daily limit popup trigger on spell pick - //we are allowed to get basic blocked/water invisible nearby tile date when casting DD spell - TerrainTile targetTile = gs->map->getTile(tile); - auto obfuscatedTile = std::make_shared(); - obfuscatedTile->visitable = false; - obfuscatedTile->blocked = targetTile.blocked || targetTile.visitable; - - if(targetTile.blocked || targetTile.visitable) - obfuscatedTile->terType = VLC->terrainTypeHandler->getById(TerrainId::ROCK); - else if(!VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE)) - obfuscatedTile->terType = gs->map->getTile(castingHero->getSightCenter()).terType; - else - obfuscatedTile->terType = targetTile.isWater() - ? VLC->terrainTypeHandler->getById(TerrainId::WATER) - : VLC->terrainTypeHandler->getById(TerrainId::GRASS); - - outputTile = obfuscatedTile.get(); - } - } - - return outputTile; + return nullptr; } EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) const diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 70c69042b..04f264f59 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -193,11 +193,11 @@ public: //map virtual int3 guardingCreaturePosition (int3 pos) const; virtual std::vector getGuardingCreatures (int3 pos) const; - virtual bool isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) const; + virtual bool isTileGuardedUnchecked(int3 tile) const; virtual const CMapHeader * getMapHeader()const; virtual int3 getMapSize() const; //returns size of map - z is 1 for one - level map and 2 for two level map virtual const TerrainTile * getTile(int3 tile, bool verbose = true) const; - virtual const TerrainTile * getTileForDimensionDoor(int3 tile, const CGHeroInstance * castingHero) const; + virtual const TerrainTile * getTileUnchecked(int3 tile) const; virtual std::shared_ptr> getAllVisibleTiles() const; virtual bool isInTheMap(const int3 &pos) const; virtual void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 9c59b87e1..bef48d5c4 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -105,6 +105,7 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"}, {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, + {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"}, {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 10b42dc82..2af03c5c9 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -72,6 +72,7 @@ enum class EGameSettings COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, + DIMENSION_DOOR_FAILURE_SPENDS_POINTS, DIMENSION_DOOR_TRIGGERS_GUARDS, DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 98fd74c92..8820f111c 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -13,6 +13,7 @@ #include "AdventureSpellMechanics.h" #include "CSpellHandler.h" +#include "Problem.h" #include "../CGameInfoCallback.h" #include "../CPlayerState.h" @@ -32,38 +33,53 @@ AdventureSpellMechanics::AdventureSpellMechanics(const CSpell * s): { } -bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +bool AdventureSpellMechanics::canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const { if(!owner->isAdventure()) - { - env->complain("Attempt to cast non adventure spell in adventure mode"); return false; - } - if(const CGHeroInstance * heroCaster = dynamic_cast(parameters.caster)) + const auto * heroCaster = dynamic_cast(caster); + + if (heroCaster) { if(heroCaster->inTownGarrison) - { - env->complain("Attempt to cast an adventure spell in town garrison"); return false; - } const auto level = heroCaster->getSpellSchoolLevel(owner); const auto cost = owner->getCost(level); if(!heroCaster->canCastThisSpell(owner)) - { - env->complain("Hero cannot cast this spell!"); return false; - } if(heroCaster->mana < cost) - { - env->complain("Hero doesn't have enough spell points to cast this spell!"); return false; - } } + return canBeCastImpl(problem, cb, caster); +} + +bool AdventureSpellMechanics::canBeCastAt(spells::Problem & problem, const CGameInfoCallback * 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 CGameInfoCallback * cb, const spells::Caster * caster) const +{ + return true; +} + +bool AdventureSpellMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + return true; +} + +bool AdventureSpellMechanics::adventureCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + spells::detail::ProblemImpl problem; + + if (!canBeCastAt(problem, env->getCb(), parameters.caster, parameters.pos)) + return false; + ESpellCastResult result = beginCast(env, parameters); if(result == ESpellCastResult::OK) @@ -103,29 +119,31 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron ESpellCastResult AdventureSpellMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { - return ESpellCastResult::OK; + return ESpellCastResult::OK; +} + +void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + // no-op, only for implementation in derived classes } void AdventureSpellMechanics::performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { + const auto level = parameters.caster->getSpellSchoolLevel(owner); + const auto cost = owner->getCost(level); + AdvmapSpellCast asc; asc.casterID = ObjectInstanceID(parameters.caster->getCasterUnitId()); asc.spellID = owner->id; env->apply(&asc); ESpellCastResult result = applyAdventureEffects(env, parameters); - endCast(env, parameters, result); -} - -void AdventureSpellMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const ESpellCastResult result) const -{ - const auto level = parameters.caster->getSpellSchoolLevel(owner); - const auto cost = owner->getCost(level); switch(result) { case ESpellCastResult::OK: parameters.caster->spendMana(env, cost); + endCast(env, parameters); break; default: break; @@ -138,35 +156,34 @@ SummonBoatMechanics::SummonBoatMechanics(const CSpell * s): { } -ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +bool SummonBoatMechanics::canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const { - if(!parameters.caster->getHeroCaster()) + if(!caster->getHeroCaster()) + return false; + + if(caster->getHeroCaster()->boat) { - env->complain("Not a hero caster!"); - return ESpellCastResult::ERROR; - } - - if(parameters.caster->getHeroCaster()->boat) - { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 333);//%s is already in boat - parameters.caster->getCasterName(iw.text); - env->apply(&iw); - return ESpellCastResult::CANCEL; + MetaString message = MetaString::createFromTextID("core.genrltxt.333"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; } - int3 summonPos = parameters.caster->getHeroCaster()->bestLocation(); - + int3 summonPos = caster->getHeroCaster()->bestLocation(); + if(summonPos.x < 0) { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 334);//There is no place to put the boat. - env->apply(&iw); - return ESpellCastResult::CANCEL; + MetaString message = MetaString::createFromTextID("core.genrltxt.334"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; } + return true; +} + +ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); //check if spell works at all @@ -200,6 +217,8 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment } } + int3 summonPos = parameters.caster->getHeroCaster()->bestLocation(); + if(nullptr != nearest) //we found boat to summon { ChangeObjPos cop; @@ -214,6 +233,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment iw.player = parameters.caster->getCasterOwner(); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon. env->apply(&iw); + return ESpellCastResult::ERROR; } else //create boat { @@ -233,6 +253,29 @@ ScuttleBoatMechanics::ScuttleBoatMechanics(const CSpell * s): { } +bool ScuttleBoatMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * 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->isVisible(pos, caster->getCasterOwner())) + return false; + + const TerrainTile * t = cb->getTile(pos); + if(!t || t->visitableObjects.empty() || t->visitableObjects.back()->ID != Obj::BOAT) + return false; + + return true; +} + ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const { const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); @@ -247,36 +290,11 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen return ESpellCastResult::OK; } - if(!env->getMap()->isInTheMap(parameters.pos)) - { - env->complain("Invalid dst tile for scuttle!"); - return ESpellCastResult::ERROR; - } - - int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); - - if(!isInScreenRange(casterPosition, parameters.pos)) - { - env->complain("Attempting to cast Scuttle Boat outside screen range!"); - return ESpellCastResult::ERROR; - } - - if(!env->getCb()->isVisible(parameters.pos, parameters.caster->getCasterOwner())) - { - env->complain("Attempting to cast Scuttle Boat at invisible tile!"); - return ESpellCastResult::ERROR; - } - - const TerrainTile *t = &env->getMap()->getTile(parameters.pos); - if(t->visitableObjects.empty() || t->visitableObjects.back()->ID != Obj::BOAT) - { - env->complain("There is no boat to scuttle!"); - return ESpellCastResult::ERROR; - } + const TerrainTile & t = env->getMap()->getTile(parameters.pos); RemoveObject ro; ro.initiator = parameters.caster->getCasterOwner(); - ro.objectID = t->visitableObjects.back()->id; + ro.objectID = t.visitableObjects.back()->id; env->apply(&ro); return ESpellCastResult::OK; } @@ -287,71 +305,31 @@ DimensionDoorMechanics::DimensionDoorMechanics(const CSpell * s): { } -ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +bool DimensionDoorMechanics::canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const { - if(!env->getMap()->isInTheMap(parameters.pos)) + if(!caster->getHeroCaster()) + return false; + + if(caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough { - env->complain("Destination is out of map!"); - return ESpellCastResult::ERROR; - } - - if(!parameters.caster->getHeroCaster()) - { - env->complain("Not a hero caster!"); - return ESpellCastResult::ERROR; + problem.add(MetaString::createFromTextID("core.genrltxt.125")); + return false; } - int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter(); - - const TerrainTile * dest = env->getCb()->getTile(parameters.pos); - const TerrainTile * curr = env->getCb()->getTile(casterPosition); - - if(nullptr == dest) - { - env->complain("Destination tile doesn't exist!"); - return ESpellCastResult::ERROR; - } - - if(nullptr == curr) - { - env->complain("Source tile doesn't exist!"); - return ESpellCastResult::ERROR; - } - - if(parameters.caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough - { - env->complain("Hero needs movement points to cast Dimension Door!"); - return ESpellCastResult::ERROR; - } - - if(!isInScreenRange(casterPosition, parameters.pos)) - { - env->complain("Attempting to cast Dimension Door outside screen range!"); - return ESpellCastResult::ERROR; - } - - if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) - { - if(!env->getCb()->isVisible(parameters.pos, parameters.caster->getCasterOwner())) - { - env->complain("Attempting to cast Dimension Door inside Fog of War with limitation toggled on!"); - return ESpellCastResult::ERROR; - } - } - - const auto schoolLevel = parameters.caster->getSpellSchoolLevel(owner); - const int movementCost = GameConstants::BASE_MOVEMENT_COST * ((schoolLevel >= 3) ? 2 : 3); + const auto schoolLevel = caster->getSpellSchoolLevel(owner); std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; - int castsAlreadyPerformedThisTurn = parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size(); + int castsAlreadyPerformedThisTurn = caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size(); int castsLimit = owner->getLevelPower(schoolLevel); bool isTournamentRulesLimitEnabled = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT); if(isTournamentRulesLimitEnabled) { - bool meetsTournamentRulesTwoCastsRequirements = env->getMap()->width * env->getMap()->height * env->getMap()->levels() >= GameConstants::TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD + 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; @@ -359,21 +337,83 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn { - InfoWindow iw; - iw.player = parameters.caster->getCasterOwner(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today. - parameters.caster->getCasterName(iw.text); - env->apply(&iw); - return ESpellCastResult::CANCEL; + MetaString message = MetaString::createFromTextID("core.genrltxt.338"); + caster->getCasterName(message); + problem.add(std::move(message)); + return false; } - if(!dest->isClear(curr)) //wrong dest tile + return true; +} + +bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const +{ + if(!cb->isInTheMap(pos)) + return false; + + if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES)) + { + if(!cb->isVisible(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(VLC->settings()->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 movementCost = GameConstants::BASE_MOVEMENT_COST * ((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(); - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 70); //Dimension Door failed! - env->apply(&iw); - return ESpellCastResult::CANCEL; + + // tile is either blocked or not possible to move (e.g. water <-> land) + if(VLC->settings()->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; @@ -381,20 +421,27 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm 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); - if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true)) - { - 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), true); +} + ///TownPortalMechanics TownPortalMechanics::TownPortalMechanics(const CSpell * s): AdventureSpellMechanics(s) @@ -431,8 +478,8 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment env->apply(&iw); return ESpellCastResult::CANCEL; } - } - else if(env->getMap()->isInTheMap(parameters.pos)) + } + else if(env->getMap()->isInTheMap(parameters.pos)) { const TerrainTile & tile = env->getMap()->getTile(parameters.pos); @@ -488,6 +535,38 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment return ESpellCastResult::ERROR; } + const TerrainTile & from = env->getMap()->getTile(parameters.caster->getHeroCaster()->visitablePos()); + const TerrainTile & dest = env->getMap()->getTile(destination->visitablePos()); + + if(!dest.isClear(&from)) + { + InfoWindow iw; + iw.player = parameters.caster->getCasterOwner(); + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 135); + env->apply(&iw); + return ESpellCastResult::ERROR; + } + + return ESpellCastResult::OK; +} + +void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const +{ + const int moveCost = movementCost(parameters); + const CGTownInstance * destination = nullptr; + + if(parameters.caster->getSpellSchoolLevel(owner) < 2) + { + std::vector pool = getPossibleTowns(env, parameters); + destination = findNearestTown(env, parameters, pool); + } + else + { + const TerrainTile & tile = env->getMap()->getTile(parameters.pos); + auto * const topObj = tile.topVisitableObj(false); + destination = dynamic_cast(topObj); + } + if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(destination->visitablePos()), true)) { SetMovePoints smp; @@ -495,7 +574,6 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment smp.val = std::max(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost); env->apply(&smp); } - return ESpellCastResult::OK; } ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const diff --git a/lib/spells/AdventureSpellMechanics.h b/lib/spells/AdventureSpellMechanics.h index 8fd6ac6c4..02f8c01aa 100644 --- a/lib/spells/AdventureSpellMechanics.h +++ b/lib/spells/AdventureSpellMechanics.h @@ -18,64 +18,79 @@ class CGTownInstance; enum class ESpellCastResult { - OK, - CANCEL,//cast failed but it is not an error + OK, // cast successful + CANCEL, // cast failed but it is not an error, no mana has been spent PENDING, - ERROR//internal error occurred + ERROR// error occured, for example invalid request from player }; -class DLL_LINKAGE AdventureSpellMechanics : public IAdventureSpellMechanics +class AdventureSpellMechanics : public IAdventureSpellMechanics { public: AdventureSpellMechanics(const CSpell * s); + bool canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const final; + bool canBeCastAt(spells::Problem & problem, const CGameInfoCallback * 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 CGameInfoCallback * cb, const spells::Caster * caster) const; + virtual bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const; + void performCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; - void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const ESpellCastResult result) const; }; -class DLL_LINKAGE SummonBoatMechanics : public AdventureSpellMechanics +class SummonBoatMechanics final : public AdventureSpellMechanics { public: SummonBoatMechanics(const CSpell * s); protected: + bool canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const override; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE ScuttleBoatMechanics : public AdventureSpellMechanics +class ScuttleBoatMechanics final : public AdventureSpellMechanics { public: ScuttleBoatMechanics(const CSpell * s); protected: + bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE DimensionDoorMechanics : public AdventureSpellMechanics +class DimensionDoorMechanics final : public AdventureSpellMechanics { public: DimensionDoorMechanics(const CSpell * s); protected: + bool canBeCastImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const override; + bool canBeCastAtImpl(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster, const int3 & pos) const override; + ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; }; -class DLL_LINKAGE TownPortalMechanics : public AdventureSpellMechanics +class TownPortalMechanics final : public AdventureSpellMechanics { public: TownPortalMechanics(const CSpell * s); protected: ESpellCastResult applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; ESpellCastResult beginCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; + void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override; private: const CGTownInstance * findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector & pool) const; int32_t movementCost(const AdventureSpellCastParameters & parameters) const; std::vector getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const; }; -class DLL_LINKAGE ViewMechanics : public AdventureSpellMechanics +class ViewMechanics : public AdventureSpellMechanics { public: ViewMechanics(const CSpell * s); @@ -85,7 +100,7 @@ protected: virtual bool showTerrain(const int32_t spellLevel) const = 0; }; -class DLL_LINKAGE ViewAirMechanics : public ViewMechanics +class ViewAirMechanics final : public ViewMechanics { public: ViewAirMechanics(const CSpell * s); @@ -94,7 +109,7 @@ protected: bool showTerrain(const int32_t spellLevel) const override; }; -class DLL_LINKAGE ViewEarthMechanics : public ViewMechanics +class ViewEarthMechanics final : public ViewMechanics { public: ViewEarthMechanics(const CSpell * s); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index aaf101276..679f1ec62 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -491,6 +491,11 @@ void CSpell::setupMechanics() adventureMechanics = IAdventureSpellMechanics::createMechanics(this); } +const IAdventureSpellMechanics & CSpell::getAdventureMechanics() const +{ + return *adventureMechanics; +} + std::unique_ptr CSpell::battleMechanics(const spells::IBattleCast * event) const { return mechanics->create(event); diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index b4f079c90..f8826e23d 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -290,6 +290,7 @@ public://internal, for use only by Mechanics classes ///returns raw damage or healed HP int64_t calculateRawEffectValue(int32_t effectLevel, int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const; + const IAdventureSpellMechanics & getAdventureMechanics() const; std::unique_ptr battleMechanics(const spells::IBattleCast * event) const; private: void setIsOffensive(const bool val); diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 982b1f744..d0b442959 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -355,6 +355,9 @@ public: IAdventureSpellMechanics(const CSpell * s); virtual ~IAdventureSpellMechanics() = default; + virtual bool canBeCast(spells::Problem & problem, const CGameInfoCallback * cb, const spells::Caster * caster) const = 0; + virtual bool canBeCastAt(spells::Problem & problem, const CGameInfoCallback * 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);