1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-08 22:26:51 +02:00

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
}
```
This commit is contained in:
Ivan Savenko
2025-01-24 17:46:18 +00:00
parent b36f5e4026
commit ec970c7b22
18 changed files with 80 additions and 37 deletions

View File

@@ -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()

View File

@@ -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)
{

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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" },

View File

@@ -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,

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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
{

View File

@@ -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();

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
};