浏览代码

Support for configuring minimal cost for moving between tiles

- Added `movementCostBase` parameter to game config that defines minimal
amount of movement points that will be spent when moving from one tile
on another while offroad (and cost of Fly / Town Portal spells)
- Added `BASE_TILE_MOVEMENT_COST` bonus type that allows modifying
`movementCostBase` on per-hero basis

Example usage for hota-like pathfinding skill
```json
"tileCostReduction" : {
	"type" : "BASE_TILE_MOVEMENT_COST",
	"val" : -15
}
```
Ivan Savenko 8 月之前
父节点
当前提交
ec970c7b22

+ 4 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -20,6 +20,7 @@
 #include "../../../lib/pathfinder/CPathfinder.h"
 #include "../../../lib/pathfinder/PathfinderUtil.h"
 #include "../../../lib/pathfinder/PathfinderOptions.h"
+#include "../../../lib/IGameSettings.h"
 #include "../../../lib/CPlayerState.h"
 
 namespace NKAI
@@ -1097,7 +1098,9 @@ struct TownPortalFinder
 
 		// TODO: Copy/Paste from TownPortalMechanics
 		townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal));
-		movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3);
+
+		int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+		movementNeeded = baseCost * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3);
 	}
 
 	bool actorCanCastTownPortal()

+ 3 - 1
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -17,6 +17,7 @@
 #include "../../../lib/pathfinder/CPathfinder.h"
 #include "../../../lib/pathfinder/PathfinderOptions.h"
 #include "../../../lib/pathfinder/PathfinderUtil.h"
+#include "../../../lib/IGameSettings.h"
 #include "../../../lib/CPlayerState.h"
 
 AINodeStorage::AINodeStorage(const int3 & Sizes)
@@ -246,7 +247,8 @@ void AINodeStorage::calculateTownPortalTeleportations(
 
 		// TODO: Copy/Paste from TownPortalMechanics
 		auto skillLevel = hero->getSpellSchoolLevel(townPortal);
-		auto movementCost = GameConstants::BASE_MOVEMENT_COST * (skillLevel >= 3 ? 2 : 3);
+		int baseCost = hero->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+		auto movementCost = baseCost * (skillLevel >= 3 ? 2 : 3);
 
 		if(hero->movementPointsRemaining() < movementCost)
 		{

+ 4 - 0
config/gameConfig.json

@@ -306,6 +306,10 @@
 			"tavernInvite"            : false,
 			// minimal primary skills for heroes
 			"minimalPrimarySkills": [ 0, 0, 1, 1],
+			
+			/// minimal movement cost from one tile to another while offroad. Also see BASE_TILE_MOVEMENT_COST bonus type
+			/// Also affects cost of Town Portal spell and movement cost when using Fly spell
+			"movementCostBase" : 100,
 			/// movement points hero can get on start of the turn when on land, depending on speed of slowest creature (0-based list)
 			"movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ],
 			/// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list)

+ 1 - 0
config/schemas/gameSettings.json

@@ -45,6 +45,7 @@
 				"backpackSize" :              { "type" : "number" },
 				"tavernInvite" :              { "type" : "boolean" },
 				"minimalPrimarySkills" :      { "type" : "array" },
+				"movementCostBase"  :         { "type" : "number" },
 				"movementPointsLand" :        { "type" : "array" },
 				"movementPointsSea" :         { "type" : "array" }
 			}

+ 7 - 1
docs/modders/Bonus/Bonus_Types.md

@@ -156,10 +156,16 @@ Allows affected heroes to learn spells from each other during hero exchange
 
 ### ROUGH_TERRAIN_DISCOUNT
 
-Reduces movement points penalty when moving on terrains with movement cost over 100 points. Can not reduce movement cost below 100 points
+Reduces movement point penalty when moving on terrain with movement cost higher than base movement cost Cannot reduce movement cost lower than base movement cost. See the `movementCostBase` parameter in the game config and the `BASE_TILE_MOVEMENT_COST` bonus type. Used for the Pathfinding skill
 
 - val: penalty reduction, in movement points per tile.
 
+### BASE_TILE_MOVEMENT_COST
+
+Change the minimum cost required to move from one tile to another while off-road by the specified value. Has no effect on road movement. Used for pathfinding in HotA
+
+- val: positive value increases the minimum cost, negative value decreases it.
+
 ### WANDERING_CREATURES_JOIN_BONUS
 
 Increases probability for wandering monsters to join army of affected heroes

+ 1 - 0
lib/GameSettings.cpp

@@ -78,6 +78,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,             "heroes",    "retreatOnWinWithoutTroops"            },
 		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,                   "heroes",    "startingStackChances"                 },
 		{EGameSettings::HEROES_TAVERN_INVITE,                             "heroes",    "tavernInvite"                         },
+		{EGameSettings::HEROES_MOVEMENT_COST_BASE,                        "heroes",    "movementCostBase"                     },
 		{EGameSettings::HEROES_MOVEMENT_POINTS_LAND,                      "heroes",    "movementPointsLand"                   },
 		{EGameSettings::HEROES_MOVEMENT_POINTS_SEA,                       "heroes",    "movementPointsSea"                    },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,                     "mapFormat", "armageddonsBlade"                     },

+ 1 - 0
lib/IGameSettings.h

@@ -51,6 +51,7 @@ enum class EGameSettings
 	HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,
 	HEROES_STARTING_STACKS_CHANCES,
 	HEROES_TAVERN_INVITE,
+	HEROES_MOVEMENT_COST_BASE,
 	HEROES_MOVEMENT_POINTS_LAND,
 	HEROES_MOVEMENT_POINTS_SEA,
 	MAP_FORMAT_ARMAGEDDONS_BLADE,

+ 1 - 1
lib/RoadHandler.cpp

@@ -84,6 +84,6 @@ RoadType::RoadType():
 	id(Road::NO_ROAD),
 	identifier("empty"),
 	modScope("core"),
-	movementCost(GameConstants::BASE_MOVEMENT_COST)
+	movementCost(0)
 {}
 VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/bonuses/BonusEnum.h

@@ -182,6 +182,7 @@ class JsonNode;
 	BONUS_NAME(INVINCIBLE) /* cannot be target of attacks or spells */ \
 	BONUS_NAME(MECHANICAL) /*eg. factory creatures, cannot be rised or healed, only neutral morale, repairable by engineer */ \
 	BONUS_NAME(PRISM_HEX_ATTACK_BREATH) /*eg. dragons*/	\
+	BONUS_NAME(BASE_TILE_MOVEMENT_COST) /*minimal cost for moving offroad*/	\
 	/* end of list */
 
 

+ 0 - 1
lib/constants/NumericConstants.h

@@ -49,7 +49,6 @@ namespace GameConstants
 	constexpr int SPELLS_QUANTITY=70;
 	constexpr int CREATURES_COUNT = 197;
 
-	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

+ 0 - 19
lib/mapObjects/CGHeroInstance.cpp

@@ -73,25 +73,6 @@ void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler)
 		handler.serializeInt("powerRank", powerRank.value());
 }
 
-ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
-{
-	int64_t ret = GameConstants::BASE_MOVEMENT_COST;
-
-	//if there is road both on dest and src tiles - use src road movement cost
-	if(dest.hasRoad() && from.hasRoad())
-	{
-		ret = from.getRoad()->movementCost;
-	}
-	else if(!ti->hasNoTerrainPenalty(from.getTerrainID())) //no special movement bonus
-	{
-		ret = VLC->terrainTypeHandler->getById(from.getTerrainID())->moveCost;
-		ret -= ti->getRoughTerrainDiscountValue();
-		if(ret < GameConstants::BASE_MOVEMENT_COST)
-			ret = GameConstants::BASE_MOVEMENT_COST;
-	}
-	return static_cast<ui32>(ret);
-}
-
 FactionID CGHeroInstance::getFactionID() const
 {
 	return getHeroClass()->faction;

+ 0 - 3
lib/mapObjects/CGHeroInstance.h

@@ -218,9 +218,6 @@ public:
 	void setSecSkillLevel(const SecondarySkill & which, int val, bool abs); // abs == 0 - changes by value; 1 - sets to value
 	void levelUp(const std::vector<SecondarySkill> & skills);
 
-	/// returns base movement cost for movement between specific tiles. Does not accounts for diagonal movement or last tile exception
-	ui32 getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const;
-
 	void setMovementPoints(int points);
 	int movementPointsRemaining() const;
 	int movementPointsLimit(bool onLand) const;

+ 30 - 3
lib/pathfinder/CPathfinder.cpp

@@ -16,8 +16,10 @@
 #include "TurnInfo.h"
 
 #include "../gameState/CGameState.h"
+#include "../IGameSettings.h"
 #include "../CPlayerState.h"
 #include "../TerrainHandler.h"
+#include "../RoadHandler.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/MiscObjects.h"
@@ -658,14 +660,17 @@ int CPathfinderHelper::getMovementCost(
 	
 	bool isAirLayer = (hero->boat && hero->boat->layer == EPathfindingLayer::AIR) || ti->hasFlyingMovement();
 
-	int movementCost = hero->getTileMovementCost(*dt, *ct, ti);
+	int movementCost = getTileMovementCost(*dt, *ct, ti);
 	if(isSailLayer)
 	{
 		if(ct->hasFavorableWinds())
 			movementCost = static_cast<int>(movementCost * 2.0 / 3);
 	}
 	else if(isAirLayer)
-		vstd::amin(movementCost, GameConstants::BASE_MOVEMENT_COST + ti->getFlyingMovementValue());
+	{
+		int baseCost = getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+		vstd::amin(movementCost, baseCost + ti->getFlyingMovementValue());
+	}
 	else if(isWaterLayer && ti->hasWaterWalking())
 		movementCost = static_cast<int>(movementCost * (100.0 + ti->getWaterWalkingValue()) / 100.0);
 
@@ -685,7 +690,7 @@ int CPathfinderHelper::getMovementCost(
 	const int pointsLeft = remainingMovePoints - movementCost;
 	if(checkLast && pointsLeft > 0)
 	{
-		int minimalNextMoveCost = hero->getTileMovementCost(*dt, *ct, ti);
+		int minimalNextMoveCost = getTileMovementCost(*dt, *ct, ti);
 
 		if (pointsLeft < minimalNextMoveCost)
 			return remainingMovePoints;
@@ -694,4 +699,26 @@ int CPathfinderHelper::getMovementCost(
 	return movementCost;
 }
 
+ui32 CPathfinderHelper::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
+{
+	//if there is road both on dest and src tiles - use src road movement cost
+	if(dest.hasRoad() && from.hasRoad())
+		return from.getRoad()->movementCost;
+
+	int baseMovementCost = ti->getMovementCostBase();
+	int terrainMoveCost = from.getTerrain()->moveCost;
+	int terrainDiscout = ti->getRoughTerrainDiscountValue();
+
+	int costWithPathfinding = std::max(baseMovementCost, terrainMoveCost - terrainDiscout);
+
+	//if hero can move without penalty - either all-native army, or creatures like Nomads in army
+	if(ti->hasNoTerrainPenalty(from.getTerrainID()))
+	{
+		int baseCost = getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+		return std::min(baseCost, costWithPathfinding);
+	}
+
+	return costWithPathfinding;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 3 - 0
lib/pathfinder/CPathfinder.h

@@ -66,6 +66,9 @@ private:
 
 class DLL_LINKAGE CPathfinderHelper : private CGameInfoCallback
 {
+	/// returns base movement cost for movement between specific tiles. Does not accounts for diagonal movement or last tile exception
+	ui32 getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const;
+
 public:
 	enum EPatrolState
 	{

+ 12 - 0
lib/pathfinder/TurnInfo.cpp

@@ -69,6 +69,11 @@ int TurnInfo::getRoughTerrainDiscountValue() const
 	return roughTerrainDiscountValue;
 }
 
+int TurnInfo::getMovementCostBase() const
+{
+	return moveCostBaseValue;
+}
+
 int TurnInfo::getMovePointsLimitLand() const
 {
 	return movePointsLimitLand;
@@ -123,6 +128,13 @@ TurnInfo::TurnInfo(TurnInfoCache * sharedCache, const CGHeroInstance * target, i
 		roughTerrainDiscountValue = bonuses->valOfBonuses(daySelector);
 	}
 
+	{
+		static const CSelector selector = Selector::type()(BonusType::BASE_TILE_MOVEMENT_COST);
+		const auto & bonuses = sharedCache->baseTileMovementCost.getBonusList(target, selector);
+		int baseMovementCost = target->cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+		moveCostBaseValue = bonuses->valOfBonuses(daySelector, baseMovementCost);
+	}
+
 	{
 		static const CSelector selector = Selector::typeSubtype(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementSea);
 		const auto & vectorSea = target->cb->getSettings().getValue(EGameSettings::HEROES_MOVEMENT_POINTS_SEA).Vector();

+ 3 - 0
lib/pathfinder/TurnInfo.h

@@ -33,6 +33,7 @@ struct TurnInfoCache
 	TurnInfoBonusList noTerrainPenalty;
 	TurnInfoBonusList freeShipBoarding;
 	TurnInfoBonusList roughTerrainDiscount;
+	TurnInfoBonusList baseTileMovementCost;
 	TurnInfoBonusList movementPointsLimitLand;
 	TurnInfoBonusList movementPointsLimitWater;
 
@@ -57,6 +58,7 @@ private:
 	int flyingMovementValue;
 	int waterWalkingValue;
 	int roughTerrainDiscountValue;
+	int moveCostBaseValue;
 	int movePointsLimitLand;
 	int movePointsLimitWater;
 
@@ -73,6 +75,7 @@ public:
 	int getFlyingMovementValue() const;
 	int getWaterWalkingValue() const;
 	int getRoughTerrainDiscountValue() const;
+	int getMovementCostBase() const;
 	int getMovePointsLimitLand() const;
 	int getMovePointsLimitWater() const;
 

+ 8 - 6
lib/spells/AdventureSpellMechanics.cpp

@@ -384,7 +384,8 @@ bool DimensionDoorMechanics::canBeCastAtImpl(spells::Problem & problem, const CG
 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);
+	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);
@@ -447,7 +448,7 @@ TownPortalMechanics::TownPortalMechanics(const CSpell * s):
 ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 	const CGTownInstance * destination = nullptr;
-	const int moveCost = movementCost(parameters);
+	const int moveCost = movementCost(env, parameters);
 	
 	if(!parameters.caster->getHeroCaster())
 	{
@@ -548,7 +549,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 
 void TownPortalMechanics::endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
-	const int moveCost = movementCost(parameters);
+	const int moveCost = movementCost(env, parameters);
 	const CGTownInstance * destination = nullptr;
 
 	if(parameters.caster->getSpellSchoolLevel(owner) < 2)
@@ -591,7 +592,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 		return ESpellCastResult::CANCEL;
 	}
 
-	const int moveCost = movementCost(parameters);
+	const int moveCost = movementCost(env, parameters);
 
 	if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
 	{
@@ -700,12 +701,13 @@ std::vector <const CGTownInstance*> TownPortalMechanics::getPossibleTowns(SpellC
 	return ret;
 }
 
-int32_t TownPortalMechanics::movementCost(const AdventureSpellCastParameters & parameters) const
+int32_t TownPortalMechanics::movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const
 {
 	if(parameters.caster != parameters.caster->getHeroCaster()) //if caster is not hero
 		return 0;
 	
-	return GameConstants::BASE_MOVEMENT_COST * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3);
+	int baseMovementCost = env->getCb()->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+	return baseMovementCost * ((parameters.caster->getSpellSchoolLevel(owner) >= 3) ? 2 : 3);
 }
 
 ///ViewMechanics

+ 1 - 1
lib/spells/AdventureSpellMechanics.h

@@ -86,7 +86,7 @@ protected:
 	void endCast(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const override;
 private:
 	const CGTownInstance * findNearestTown(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters, const std::vector <const CGTownInstance*> & pool) const;
-	int32_t movementCost(const AdventureSpellCastParameters & parameters) const;
+	int32_t movementCost(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
 	std::vector <const CGTownInstance*> getPossibleTowns(SpellCastEnvironment * env, const AdventureSpellCastParameters & parameters) const;
 };