浏览代码

Merge pull request #3686 from dydzio0614/dimension-door-changes

Dimension door changes
Ivan Savenko 1 年之前
父节点
当前提交
5b43720dda

+ 89 - 45
client/adventureMap/AdventureMapInterface.cpp

@@ -37,7 +37,7 @@
 #include "../CPlayerInterface.h"
 
 #include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
+#include "../../lib/GameSettings.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
@@ -501,42 +501,39 @@ const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPo
 	return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder);
 }
 
-void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
+void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 {
 	if(!shortcuts->optionMapViewActive())
 		return;
 
-	//FIXME: this line breaks H3 behavior for Dimension Door
-	if(!LOCPLINT->cb->isVisible(mapPos))
-		return;
+	if(!LOCPLINT->cb->isVisible(targetPosition))
+    {
+        if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR)
+		    return;
+    }
 	if(!LOCPLINT->makingTurn)
 		return;
 
-	const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos);
+	const CGObjectInstance *topBlocking = LOCPLINT->cb->isVisible(targetPosition) ? getActiveObject(targetPosition) : nullptr;
 
-	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
-
-	int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
 	if(spellBeingCasted)
 	{
 		assert(shortcuts->optionSpellcasting());
 
-		if (!isInScreenRange(selPos, mapPos))
-			return;
-
-		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
-
 		switch(spellBeingCasted->id)
 		{
-		case SpellID::SCUTTLE_BOAT: //Scuttle Boat
-			if(topBlocking && topBlocking->ID == Obj::BOAT)
-				performSpellcasting(mapPos);
+		case SpellID::SCUTTLE_BOAT:
+			if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::SCUTTLE_BOAT))
+				performSpellcasting(targetPosition);
 			break;
 		case SpellID::DIMENSION_DOOR:
-			if(!tile || tile->isClear(heroTile))
-				performSpellcasting(mapPos);
+			if(isValidAdventureSpellTarget(targetPosition, topBlocking, SpellID::DIMENSION_DOOR))
+				performSpellcasting(targetPosition);
+			break;
+		default:
 			break;
 		}
+
 		return;
 	}
 	//check if we can select this object
@@ -555,7 +552,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 	{
 		isHero = true;
 
-		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos);
+		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(targetPosition);
 		if(currentHero == topBlocking) //clicked selected hero
 		{
 			LOCPLINT->openHeroWindow(currentHero);
@@ -569,7 +566,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 		else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise
 		{
 			if(LOCPLINT->localState->hasPath(currentHero) &&
-			   LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving
+			   LOCPLINT->localState->getPath(currentHero).endPos() == targetPosition)//we'll be moving
 			{
 				assert(!CGI->mh->hasOngoingAnimations());
 				if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0)
@@ -585,7 +582,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 				}
 				else //remove old path and find a new one if we clicked on accessible tile
 				{
-					LOCPLINT->localState->setPath(currentHero, mapPos);
+					LOCPLINT->localState->setPath(currentHero, targetPosition);
 					onHeroChanged(currentHero);
 				}
 			}
@@ -603,23 +600,31 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 	}
 }
 
-void AdventureMapInterface::onTileHovered(const int3 &mapPos)
+void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 {
 	if(!shortcuts->optionMapViewActive())
 		return;
 
-	//may occur just at the start of game (fake move before full intiialization)
+	//may occur just at the start of game (fake move before full initialization)
 	if(!LOCPLINT->localState->getCurrentArmy())
 		return;
 
-	if(!LOCPLINT->cb->isVisible(mapPos))
+	bool isTargetPositionVisible = LOCPLINT->cb->isVisible(targetPosition);
+
+	if(!isTargetPositionVisible)
 	{
-		CCS->curh->set(Cursor::Map::POINTER);
-		GH.statusbar()->clear();
-		return;
+        GH.statusbar()->clear();
+
+        if(!spellBeingCasted || spellBeingCasted->id != SpellID::DIMENSION_DOOR)
+        {
+            CCS->curh->set(Cursor::Map::POINTER);
+            return;
+        }
 	}
+
 	auto objRelations = PlayerRelations::ALLIES;
-	const CGObjectInstance *objAtTile = getActiveObject(mapPos);
+
+	const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr;
 	if(objAtTile)
 	{
 		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
@@ -627,10 +632,10 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 		boost::replace_all(text,"\n"," ");
 		GH.statusbar()->write(text);
 	}
-	else
+	else if(isTargetPositionVisible)
 	{
-		std::string hlp = CGI->mh->getTerrainDescr(mapPos, false);
-		GH.statusbar()->write(hlp);
+		std::string tileTooltipText = CGI->mh->getTerrainDescr(targetPosition, false);
+		GH.statusbar()->write(tileTooltipText);
 	}
 
 	if(spellBeingCasted)
@@ -638,25 +643,32 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 		switch(spellBeingCasted->id)
 		{
 		case SpellID::SCUTTLE_BOAT:
-			{
-			int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-
-			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
+			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))
 			{
-				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
-				int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-				if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos))
-					CCS->curh->set(Cursor::Map::TELEPORT);
-				else
-					CCS->curh->set(Cursor::Map::POINTER);
-				return;
+				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;
 		}
 	}
 
@@ -684,7 +696,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 		std::array<Cursor::Map, 4> cursorVisit     = { Cursor::Map::T1_VISIT,      Cursor::Map::T2_VISIT,      Cursor::Map::T3_VISIT,      Cursor::Map::T4_VISIT,      };
 		std::array<Cursor::Map, 4> cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, };
 
-		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos);
+		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(targetPosition);
 		assert(pathNode);
 
 		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info
@@ -932,3 +944,35 @@ void AdventureMapInterface::onScreenResize()
 	if (widgetActive)
 		activate();
 }
+
+bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId)
+{
+	int3 heroPosition = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
+	if (!isInScreenRange(heroPosition, targetPosition))
+	{
+		return false;
+	}
+
+	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;
+	}
+}

+ 5 - 2
client/adventureMap/AdventureMapInterface.h

@@ -92,6 +92,9 @@ private:
 	/// casts current spell at specified location
 	void performSpellcasting(const int3 & castTarget);
 
+	/// performs clientside validation of valid targets for adventure spells
+	bool isValidAdventureSpellTarget(int3 targetPosition, const CGObjectInstance * topObjectAtTarget, SpellID spellId);
+
 	/// dim interface if some windows opened
 	void dim(Canvas & to);
 
@@ -170,10 +173,10 @@ public:
 	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
 
 	/// called by MapView whenever tile is clicked
-	void onTileLeftClicked(const int3 & mapPos);
+	void onTileLeftClicked(const int3 & targetPosition);
 
 	/// called by MapView whenever tile is hovered
-	void onTileHovered(const int3 & mapPos);
+	void onTileHovered(const int3 & targetPosition);
 
 	/// called by MapView whenever tile is clicked
 	void onTileRightClicked(const int3 & mapPos);

+ 7 - 1
client/mapView/MapRenderer.cpp

@@ -353,7 +353,10 @@ void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target,
 		size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101;
 		size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size();
 
-		target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0));
+		if (context.showSpellRange(coordinates))
+			target.drawColor(Rect(0,0,32,32), Colors::BLACK);
+		else
+			target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0));
 	}
 	else
 	{
@@ -363,6 +366,9 @@ void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target,
 
 uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates)
 {
+	if (context.showSpellRange(coordinates))
+		return 0xff - 2;
+
 	const NeighborTilesInfo neighborInfo(context, coordinates);
 	int retBitmapID = neighborInfo.getBitmapID();
 	if(retBitmapID < 0)

+ 1 - 1
client/mapView/mapHandler.h

@@ -64,7 +64,7 @@ public:
 	void removeMapObserver(IMapObjectObserver * observer);
 
 	/// returns string description for terrain interaction
-	std::string getTerrainDescr(const int3 & pos, bool rightClick) const;
+	std::string getTerrainDescr(const int3 & pos, bool rightClick) const; //TODO: possible to get info about invisible tiles from client without serverside validation
 
 	/// determines if the map is ready to handle new hero movement (not available during fading animations)
 	bool hasOngoingAnimations();

+ 13 - 1
config/gameConfig.json

@@ -312,7 +312,7 @@
 			// defines dice size of a luck roll, based on creature's luck
 			"goodLuckDice" : [ 24, 12, 8 ],
 			"badLuckDice" : [],
-			
+
 			// every 1 attack point damage influence in battle when attack points > defense points during creature attack
 			"attackPointDamageFactor": 0.05, 
 			// limit of damage increase that can be achieved by overpowering attack points
@@ -388,6 +388,18 @@
 			// if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied
 			"originalFlyRules" : false
 		},
+
+		"spells":
+		{
+			// if enabled, dimension work doesn't work into tiles under Fog of War
+			"dimensionDoorOnlyToUncoveredTiles" : false,
+			// if enabled, dimension door will hint regarding tile being incompatible terrain type, unlike H3 (water/land)
+			"dimensionDoorExposesTerrainType" : false,
+			// if enabled, dimension door will initiate a fight upon landing on tile adjacent to neutral creature
+			"dimensionDoorTriggersGuards" : false,
+			// 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
+		},
 		
 		"bonuses" : 
 		{

+ 58 - 4
lib/CGameInfoCallback.cpp

@@ -287,6 +287,24 @@ std::vector<const CGObjectInstance*> CGameInfoCallback::getGuardingCreatures (in
 	return ret;
 }
 
+bool CGameInfoCallback::isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) 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>(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;
+}
+
 bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject) const
 {
 	const auto * h = dynamic_cast<const CGHeroInstance *>(hero);
@@ -440,7 +458,7 @@ bool CGameInfoCallback::isVisible(const CGObjectInstance *obj) const
 // 		return armi;
 // }
 
-std::vector < const CGObjectInstance * > CGameInfoCallback::getBlockingObjs( int3 pos ) const
+std::vector <const CGObjectInstance *> CGameInfoCallback::getBlockingObjs( int3 pos ) const
 {
 	std::vector<const CGObjectInstance *> ret;
 	const TerrainTile *t = getTile(pos);
@@ -451,7 +469,7 @@ std::vector < const CGObjectInstance * > CGameInfoCallback::getBlockingObjs( int
 	return ret;
 }
 
-std::vector <const CGObjectInstance * > CGameInfoCallback::getVisitableObjs(int3 pos, bool verbose) const
+std::vector <const CGObjectInstance *> CGameInfoCallback::getVisitableObjs(int3 pos, bool verbose) const
 {
 	std::vector<const CGObjectInstance *> ret;
 	const TerrainTile *t = getTile(pos, verbose);
@@ -470,7 +488,7 @@ const CGObjectInstance * CGameInfoCallback::getTopObj (int3 pos) const
 	return vstd::backOrNull(getVisitableObjs(pos));
 }
 
-std::vector < const CGObjectInstance * > CGameInfoCallback::getFlaggableObjects(int3 pos) const
+std::vector <const CGObjectInstance *> CGameInfoCallback::getFlaggableObjects(int3 pos) const
 {
 	std::vector<const CGObjectInstance *> ret;
 	const TerrainTile *t = getTile(pos);
@@ -500,7 +518,7 @@ std::vector<const CGHeroInstance *> CGameInfoCallback::getAvailableHeroes(const
 	return ret;
 }
 
-const TerrainTile * CGameInfoCallback::getTile( int3 tile, bool verbose) const
+const TerrainTile * CGameInfoCallback::getTile(int3 tile, bool verbose) const
 {
 	if(isVisible(tile))
 		return &gs->map->getTile(tile);
@@ -510,6 +528,42 @@ const TerrainTile * CGameInfoCallback::getTile( int3 tile, bool verbose) const
 	return nullptr;
 }
 
+const TerrainTile * CGameInfoCallback::getTileForDimensionDoor(int3 tile, const CGHeroInstance * castingHero) const
+{
+    auto outputTile = getTile(tile, false);
+
+	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>(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<TerrainTile>();
+            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;
+}
+
 EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) const
 {
 	if(!isVisible(tile))

+ 2 - 0
lib/CGameInfoCallback.h

@@ -193,9 +193,11 @@ public:
 	//map
 	virtual int3 guardingCreaturePosition (int3 pos) const;
 	virtual std::vector<const CGObjectInstance*> getGuardingCreatures (int3 pos) const;
+	virtual bool isTileGuardedAfterDimensionDoorUse(int3 tile, const CGHeroInstance * castingHero) 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 std::shared_ptr<const boost::multi_array<TerrainTile*, 3>> getAllVisibleTiles() const;
 	virtual bool isInTheMap(const int3 &pos) const;
 	virtual void getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const;

+ 59 - 55
lib/GameSettings.cpp

@@ -50,61 +50,65 @@ void GameSettings::load(const JsonNode & input)
 	};
 
 	static const std::vector<SettingOption> optionPath = {
-		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                     },
-		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                    },
-		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"    },
-		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,  "combat",    "attackPointDamageFactorCap" },
-		{EGameSettings::COMBAT_BAD_LUCK_DICE,                   "combat",    "badLuckDice"                },
-		{EGameSettings::COMBAT_BAD_MORALE_DICE,                 "combat",    "badMoraleDice"              },
-		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR,     "combat",    "defensePointDamageFactor"   },
-		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat",    "defensePointDamageFactorCap"},
-		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                  "combat",    "goodLuckDice"               },
-		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                "combat",    "goodMoraleDice"             },
-		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,      "combat",    "oneHexTriggersObstacles"    },
-		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,   "creatures", "allowAllForDoubleMonth"     },
-		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,   "creatures", "allowRandomSpecialWeeks"    },
-		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,       "creatures", "dailyStackExperience"       },
-		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,            "creatures", "weeklyGrowthCap"            },
-		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"        },
-		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"      },
-		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"        },
-		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"             },
-		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"          },
-		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"          },
-		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"  },
-		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,         "heroes",    "startingStackChances"       },
-		{EGameSettings::HEROES_BACKPACK_CAP,                    "heroes",    "backpackSize"               },
-		{EGameSettings::HEROES_TAVERN_INVITE,                   "heroes",    "tavernInvite"               },
-		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"       },
-		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"           },
-		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"              },
-		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"             },
-		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"            },
-		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                   },
-		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,    "markets",   "blackMarketRestockPeriod"   },
-		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"      },
-		{EGameSettings::MODULE_COMMANDERS,                      "modules",   "commanders"                 },
-		{EGameSettings::MODULE_STACK_ARTIFACT,                  "modules",   "stackArtifact"              },
-		{EGameSettings::MODULE_STACK_EXPERIENCE,                "modules",   "stackExperience"            },
-		{EGameSettings::TEXTS_ARTIFACT,                         "textData",  "artifact"                   },
-		{EGameSettings::TEXTS_CREATURE,                         "textData",  "creature"                   },
-		{EGameSettings::TEXTS_FACTION,                          "textData",  "faction"                    },
-		{EGameSettings::TEXTS_HERO,                             "textData",  "hero"                       },
-		{EGameSettings::TEXTS_HERO_CLASS,                       "textData",  "heroClass"                  },
-		{EGameSettings::TEXTS_OBJECT,                           "textData",  "object"                     },
-		{EGameSettings::TEXTS_RIVER,                            "textData",  "river"                      },
-		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                       },
-		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                      },
-		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                    },
-		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"              },
-		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                   },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"   },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom"   },
-		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,               "pathfinder", "useWhirlpool"              },
-		{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES,          "pathfinder", "originalFlyRules"          },
-		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,           "towns",     "buildingsPerTurnCap"        },
-		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,        "towns",     "startingDwellingChances"    },
+		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                           },
+		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                          },
+		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"          },
+		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,  "combat",    "attackPointDamageFactorCap"       },
+		{EGameSettings::COMBAT_BAD_LUCK_DICE,                   "combat",    "badLuckDice"                      },
+		{EGameSettings::COMBAT_BAD_MORALE_DICE,                 "combat",    "badMoraleDice"                    },
+		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR,     "combat",    "defensePointDamageFactor"         },
+		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat",    "defensePointDamageFactorCap"      },
+		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                  "combat",    "goodLuckDice"                     },
+		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                "combat",    "goodMoraleDice"                   },
+		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,      "combat",    "oneHexTriggersObstacles"          },
+		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,   "creatures", "allowAllForDoubleMonth"           },
+		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,   "creatures", "allowRandomSpecialWeeks"          },
+		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,       "creatures", "dailyStackExperience"             },
+		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,            "creatures", "weeklyGrowthCap"                  },
+		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"              },
+		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"            },
+		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"              },
+		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"                   },
+		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"                },
+		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"                },
+		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"        },
+		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,         "heroes",    "startingStackChances"             },
+		{EGameSettings::HEROES_BACKPACK_CAP,                    "heroes",    "backpackSize"                     },
+		{EGameSettings::HEROES_TAVERN_INVITE,                   "heroes",    "tavernInvite"                     },
+		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"             },
+		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"                 },
+		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"                    },
+		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"                   },
+		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"                  },
+		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                         },
+		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,    "markets",   "blackMarketRestockPeriod"         },
+		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"            },
+		{EGameSettings::MODULE_COMMANDERS,                      "modules",   "commanders"                       },
+		{EGameSettings::MODULE_STACK_ARTIFACT,                  "modules",   "stackArtifact"                    },
+		{EGameSettings::MODULE_STACK_EXPERIENCE,                "modules",   "stackExperience"                  },
+		{EGameSettings::TEXTS_ARTIFACT,                         "textData",  "artifact"                         },
+		{EGameSettings::TEXTS_CREATURE,                         "textData",  "creature"                         },
+		{EGameSettings::TEXTS_FACTION,                          "textData",  "faction"                          },
+		{EGameSettings::TEXTS_HERO,                             "textData",  "hero"                             },
+		{EGameSettings::TEXTS_HERO_CLASS,                       "textData",  "heroClass"                        },
+		{EGameSettings::TEXTS_OBJECT,                           "textData",  "object"                           },
+		{EGameSettings::TEXTS_RIVER,                            "textData",  "river"                            },
+		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                             },
+		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                            },
+		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                          },
+		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"                    },
+		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                         },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"               },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"         },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom"         },
+		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,               "pathfinder", "useWhirlpool"                    },
+		{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_TRIGGERS_GUARDS,         "spells",    "dimensionDoorTriggersGuards"      },
+		{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,  "spells",    "dimensionDoorTournamentRulesLimit"},
+		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,           "towns",     "buildingsPerTurnCap"              },
+		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,        "towns",     "startingDwellingChances"          },
 	};
 
 	for(const auto & option : optionPath)

+ 4 - 0
lib/GameSettings.h

@@ -70,6 +70,10 @@ enum class EGameSettings
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_STARTING_DWELLING_CHANCES,
 	COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,
+	DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,
+	DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,
+	DIMENSION_DOOR_TRIGGERS_GUARDS,
+	DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,
 
 	OPTIONS_COUNT
 };

+ 1 - 0
lib/constants/NumericConstants.h

@@ -52,6 +52,7 @@ namespace GameConstants
 	constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement
 	constexpr int64_t PLAYER_RESOURCES_CAP = 1000 * 1000 * 1000;
 	constexpr int ALTAR_ARTIFACTS_SLOTS = 22;
+	constexpr int TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD = 144*144*2; //map tiles count threshold for 2 dimension door casts with tournament rules
 }
 
 VCMI_LIB_NAMESPACE_END

+ 55 - 9
lib/spells/AdventureSpellMechanics.cpp

@@ -17,6 +17,7 @@
 #include "../CGameInfoCallback.h"
 #include "../CPlayerState.h"
 #include "../CRandomGenerator.h"
+#include "../GameSettings.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/MiscObjects.h"
@@ -252,7 +253,20 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen
 		return ESpellCastResult::ERROR;
 	}
 
-	//TODO: test range, visibility
+	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)
 	{
@@ -287,8 +301,10 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 		return ESpellCastResult::ERROR;
 	}
 
+	int3 casterPosition = parameters.caster->getHeroCaster()->getSightCenter();
+
 	const TerrainTile * dest = env->getCb()->getTile(parameters.pos);
-	const TerrainTile * curr = env->getCb()->getTile(parameters.caster->getHeroCaster()->getSightCenter());
+	const TerrainTile * curr = env->getCb()->getTile(casterPosition);
 
 	if(nullptr == dest)
 	{
@@ -308,13 +324,40 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 		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);
 
 	std::stringstream cachingStr;
 	cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num;
 
-	if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn
+	int castsAlreadyPerformedThisTurn = parameters.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
+			&& schoolLevel == MasteryLevel::EXPERT;
+
+		castsLimit = meetsTournamentRulesTwoCastsRequirements ? 2 : 1;
+	}
+
+	if(castsAlreadyPerformedThisTurn >= castsLimit) //limit casts per turn
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
@@ -324,19 +367,22 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 		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);
-
 	if(!dest->isClear(curr)) //wrong dest tile
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
 		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 70); //Dimension Door failed!
 		env->apply(&iw);
+		return ESpellCastResult::CANCEL;
 	}
-	else if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true))
+
+	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);
+
+
+	if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true))
 	{
 		SetMovePoints smp;
 		smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());

+ 7 - 3
server/CGameHandler.cpp

@@ -1196,7 +1196,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 		sendAndApply(&tmh);
 
 		if (visitDest == VISIT_DEST && objectToVisit && objectToVisit->id == h->id)
-		{ // Hero should be always able to visit any object he staying on even if there guards around
+		{ // Hero should be always able to visit any object he is staying on even if there are guards around
 			visitObjectOnTile(t, h);
 		}
 		else if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos))
@@ -1254,7 +1254,11 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 		if (blockingVisit()) // e.g. hero on the other side of teleporter
 			return true;
 
-		doMove(TryMoveHero::TELEPORTATION, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE);
+		EGuardLook guardsCheck = VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS)
+			? CHECK_FOR_GUARDS
+			: IGNORE_GUARDS;
+
+		doMove(TryMoveHero::TELEPORTATION, guardsCheck, DONT_VISIT_DEST, LEAVING_TILE);
 
 		// visit town for town portal \ castle gates
 		// do not use generic visitObjectOnTile to avoid double-teleporting
@@ -1347,7 +1351,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne
 
 		if (oldOwner.isValidPlayer()) //old owner is real player
 		{
-			if (getPlayerState(oldOwner)->towns.empty() && getPlayerState(oldOwner)->status != EPlayerStatus::LOSER) //previous player lost last last town
+			if (getPlayerState(oldOwner)->towns.empty() && getPlayerState(oldOwner)->status != EPlayerStatus::LOSER) //previous player lost last town
 			{
 				InfoWindow iw;
 				iw.player = oldOwner;