1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-06 23:26:26 +02:00

Merge branch 'develop' into video

This commit is contained in:
Ivan Savenko 2024-12-02 13:48:30 +02:00 committed by GitHub
commit 877f47e37f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
229 changed files with 9777 additions and 6403 deletions

View File

@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@ -33,6 +34,7 @@ If this something which worked well some time ago, please let us know about vers
If applicable, add screenshots to help explain your problem.
**Version**
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version]

View File

@ -402,3 +402,9 @@ jobs:
run: |
sudo apt install python3-jstyleson
python3 CI/validate_json.py
- name: Validate Markdown
uses: DavidAnson/markdownlint-cli2-action@v18
with:
config: 'CI/example.markdownlint-cli2.jsonc'
globs: '**/*.md'

View File

@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest);
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{
@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
}
for(const auto & unit : allUnits)
{
if(!unit->isValidTarget(true))
@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1
// Ensure ps.dest is not empty before accessing the first element
if (!ps.dest.empty())
{
logAi->trace(
"Spell affects %s (%d), dps: %2f",
"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce);
dpsReduce,
oldHealth,
newHealth);
}
else
{
// Handle the case where ps.dest is empty
logAi->trace(
"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
#endif
}
}

View File

@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
{
std::vector<const battle::Unit *> result;
for(int i = 0; i < turnOrder.size(); i++)
for(int i = 0; i < turnOrder.size(); i++, turn++)
{
auto & turnQueue = turnOrder[i];
HypotheticBattle turnBattle(env.get(), cb);

View File

@ -34,11 +34,6 @@
namespace NKAI
{
// our to enemy strength ratio constants
const float SAFE_ATTACK_CONSTANT = 1.1f;
const float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
//one thread may be turn of AI and another will be handling a side effect for AI2
thread_local CCallback * cb = nullptr;
thread_local AIGateway * ai = nullptr;
@ -553,7 +548,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
// if we have no towns - things are already bad, so retreat is not an option.
if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
{
return BattleAction::makeRetreat(battleState.ourSide);
}
@ -670,7 +665,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
{
bool dangerUnknown = danger == 0;
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
answer = !dangerUnknown && !dangerTooHigh;
}

View File

@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
return h == rhs.get(true);
}
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
{
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
if(dangerStrength)
{
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
return heroStrength > dangerStrength * safeAttackRatio;
}
return true; //there's no danger
}
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
{
return isSafeToVisit(h, h, dangerStrength);
return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
}
bool isObjectRemovable(const CGObjectInstance * obj)

View File

@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
const int WOOD_ORE_MINE_PRODUCTION = 2;
const int RESOURCE_MINE_PRODUCTION = 1;
const int ACTUAL_RESOURCE_COUNT = 7;
const int ALLOWED_ROAMING_HEROES = 8;
//implementation-dependent
extern const float SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern thread_local CCallback * cb;
@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h"
#include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/GameConstants.h"
namespace NKAI
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
uint64_t armyValue = 0;
TemporaryArmy newArmyInstance;
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
for(auto bonus : *bonusModifiers)
{
// army bonuses will change and object bonuses are temporary
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
{
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
}
}
while(allowedFactions.size() < alignmentMap.size())
{
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
auto morale = slot.second->moraleVal();
auto multiplier = 1.0f;
const float BadMoraleChance = 0.083f;
const float HighMoraleChance = 0.04f;
const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
if(morale < 0)
if(morale < 0 && !badMoraleDice.empty())
{
multiplier += morale * BadMoraleChance;
size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
multiplier -= 1.0 / badMoraleDice.at(diceIndex);
}
else if(morale > 0)
else if(morale > 0 && !highMoraleDice.empty())
{
multiplier += morale * HighMoraleChance;
size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
multiplier += 1.0 / highMoraleDice.at(diceIndex);
}
newValue += multiplier * slot.second->getPower();

View File

@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
for(int upgradeIndex : {1, 0})
{
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building))
continue; // no such building in town
@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
}
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
otherBuildings.push_back({ BuildingID::MARKETPLACE });
for(auto & buildingSet : otherBuildings)
{
for(auto & buildingID : buildingSet)
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns)
{
logAi->trace("Checking town %s", town->getNameTranslated());
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
for(auto building : developmentInfo.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += developmentInfo.armyCost;
for(auto bi : developmentInfo.toBuild)
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
updateDailyIncome();
if(ai->cb->getDate(Date::DAY) == 1)
{
goldPressure = 1;
}
else
{
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
}
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
logAi->trace("Gold pressure: %f", goldPressure);
}
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
logAi->trace("checking %s", info.name);
logAi->trace("buildInfo %s", info.toString());
int highestFort = 0;
for (auto twn : ai->cb->getTownsInfo())
{
highestFort = std::max(highestFort, (int)twn->fortLevel());
}
if(!town->hasBuilt(building))
{
auto canBuild = ai->cb->canBuildStructure(town, building);
@ -281,6 +292,14 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost;
bool haveSameOrBetterFort = false;
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
haveSameOrBetterFort = true;
if(!haveSameOrBetterFort)
prerequisite.dailyIncome = info.dailyIncome;
return prerequisite;

View File

@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
}
if(obj->ID == Obj::TOWN)
{
auto town = dynamic_cast<const CGTownInstance *>(obj);
@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
newThreat.hero = path.targetHero;
newThreat.turn = path.turn();
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
newThreat.danger = path.getHeroStrength();
if(newThreat.value() > node.maximumDanger.value())
@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
const auto& info = getTileThreat(tile);
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
}
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const

View File

@ -22,6 +22,7 @@ struct HitMapInfo
uint64_t danger;
uint8_t turn;
float threat;
HeroPtr hero;
HitMapInfo()
@ -33,6 +34,7 @@ struct HitMapInfo
{
danger = 0;
turn = 255;
threat = 0;
hero = HeroPtr();
}

View File

@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
{
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
}
void HeroManager::update()
@ -108,7 +108,7 @@ void HeroManager::update()
for(auto & hero : myHeroes)
{
scores[hero] = evaluateFightingStrength(hero);
knownFightingStrength[hero->id] = hero->getFightingStrength();
knownFightingStrength[hero->id] = hero->getHeroStrength();
}
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@ -147,7 +147,10 @@ void HeroManager::update()
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
{
if (heroRoles.find(hero) != heroRoles.end())
return heroRoles.at(hero);
else
return HeroRole::SCOUT;
}
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero);
}
bool HeroManager::heroCapReached() const
bool HeroManager::heroCapReached(bool includeGarrisoned) const
{
const bool includeGarnisoned = true;
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= ai->settings->getMaxRoamingHeroes()
return heroCount >= ai->settings->getMaxRoamingHeroes()
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
}
@ -204,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
auto cached = knownFightingStrength.find(hero->id);
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
}
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
return nullptr;
}
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
{
const CGHeroInstance * weakestHero = nullptr;
auto myHeroes = ai->cb->getHeroesInfo();
@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|| existingHero->getArmyStrength() >armyLimit
|| getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
{
continue;
}
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
{
weakestHero = existingHero;
}

View File

@ -56,9 +56,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
float evaluateHero(const CGHeroInstance * hero) const;
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
bool heroCapReached() const;
bool heroCapReached(bool includeGarrisoned = true) const;
const CGHeroInstance * findHeroWithGrail() const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
float getMagicStrength(const CGHeroInstance * hero) const;
float getFightingStrengthCached(const CGHeroInstance * hero) const;

View File

@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
if (ai->cb->isVisible(node.coord))
blockers = ai->cb->getVisitableObjs(node.coord);
if(guardPos.valid())
if(guardPos.valid() && ai->cb->isVisible(guardPos))
{
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY)
if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue;
ClusterMap::accessor cluster;
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY)
if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f;

View File

@ -49,7 +49,28 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
ai->dangerHitMap->updateHitMap();
for(auto & developmentInfo : developmentInfos)
{
bool emergencyDefense = false;
uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
{
closestThreat = std::min(closestThreat, threat.turn);
}
for (auto& buildingInfo : developmentInfo.toBuild)
{
if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
{
if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
{
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
emergencyDefense = true;
}
}
}
if (!emergencyDefense)
{
for (auto& buildingInfo : developmentInfo.toBuild)
{
@ -64,14 +85,16 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
composition.addNext(BuildThis(buildingInfo, developmentInfo));
composition.addNext(SaveResources(buildingInfo.buildCost));
tasks.push_back(sptr(composition));
}
else
{
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
}
}
}
}
}
return tasks;
}

View File

@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
{
Goals::TGoalVec tasks;
if(ai->cb->getDate(Date::DAY) == 1)
return tasks;
auto heroes = cb->getHeroesInfo();
if(heroes.empty())
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
return tasks;
}
ai->dangerHitMap->updateHitMap();
for(auto town : cb->getTownsInfo())
{
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
{
return tasks;
}
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
town,
ai->getFreeResources());
for(const CGHeroInstance * targetHero : heroes)
{
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
{
continue;
}
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
{
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(

View File

@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
logAi->trace("Path found %s", path.toString());
#endif
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
#endif
continue;
}
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
{
#if NKAI_TRACE_LEVEL >= 2
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
auto hero = path.targetHero;
auto danger = path.getTotalDanger();
if (hero->getOwner() != nullkiller->playerID)
continue;
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1)
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
continue;
}
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(

View File

@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
for(auto town : ai->cb->getTownsInfo())
{
evaluateDefence(tasks, town, ai);
//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
if (!tasks.empty())
break;
}
return tasks;
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true;
return false;
}
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
{
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true;
return false;
}
}
}
@ -162,7 +165,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
{
return;
}
if(!threatNode.fastestDanger.hero)
{
logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue;
}
if (!path.targetHero->canBeMergedWith(*town))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Can't merge armies of hero %s and town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{
#if NKAI_TRACE_LEVEL >= 1
@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
// dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{
tasks.push_back(
@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue;
}
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
{
if(ai->arePathHeroesLocked(path))
{
@ -343,15 +356,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
}
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
{
if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
if(town->garrisonHero && town->garrisonHero != path.targetHero)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
@ -360,7 +365,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
#endif
continue;
}
}
else if(path.turn() == 0)
{
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
{
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
return;
if(town->hasBuilt(BuildingID::TAVERN)
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
}
else if(ai->heroManager->heroCapReached())
{
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
if(!heroToDismiss)
continue;

View File

@ -34,48 +34,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
Goals::TGoalVec tasks;
for (auto obj : ai->memory->visitableObjs)
{
if(!vstd::contains(ai->memory->alreadyVisited, obj))
{
switch (obj->ID.num)
{
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
{
auto rObj = dynamic_cast<const CRewardableObject*>(obj);
if (!rObj->wasScouted(ai->playerID))
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
break;
}
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
case Obj::WHIRLPOOL:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
{
auto tObj = dynamic_cast<const CGTeleport*>(obj);
for (auto exit : cb->getTeleportChannelExits(tObj->channel))
{
if (exit != tObj->id)
{
if (!cb->isVisible(cb->getObjInstance(exit)))
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
}
break;
}
}
else
{
switch(obj->ID.num)
{
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
case Obj::WHIRLPOOL:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
break;
for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
{
if(!cb->getObj(exit))
{
// Always attempt to visit two-way teleports if one of channel exits is not visible
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
break;
}
}
break;
}
}
}

View File

@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif
if (path.targetHero->getOwner() != ai->playerID)
continue;
if(path.containsHero(hero))
{
#if NKAI_TRACE_LEVEL >= 2
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
continue;
}
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
if(ai->arePathHeroesLocked(path))
{
#if NKAI_TRACE_LEVEL >= 2
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
}
auto danger = path.getTotalDanger();
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
continue;
}
auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT
&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
armyToGetOrBuy.addArmyToBuy(
ai->armyManager->toSlotInfo(
ai->armyManager->getArmyAvailableToBuy(
path.heroArmy,
upgrader,
ai->getFreeResources(),
path.turn())));
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
{
for(auto hero : cb->getAvailableHeroes(upgrader))
{
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->getFreeGold() >20000
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
auto danger = path.getTotalDanger();
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(

View File

@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
auto ourHeroes = ai->heroManager->getHeroRoles();
auto minScoreToHireMain = std::numeric_limits<float>::max();
int currentArmyValue = 0;
for(auto hero : ourHeroes)
{
currentArmyValue += hero.first->getArmyCost();
if(hero.second != HeroRole::MAIN)
continue;
@ -45,26 +47,38 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
minScoreToHireMain = newScore;
}
}
// If we don't have any heros we might want to lower our expectations.
if (ourHeroes.empty())
minScoreToHireMain = 0;
const CGHeroInstance* bestHeroToHire = nullptr;
const CGTownInstance* bestTownToHireFrom = nullptr;
float bestScore = 0;
bool haveCapitol = false;
ai->dangerHitMap->updateHitMap();
int treasureSourcesCount = 0;
for(auto town : towns)
{
uint8_t closestThreat = UINT8_MAX;
for (auto threat : ai->dangerHitMap->getTownThreats(town))
{
closestThreat = std::min(closestThreat, threat.turn);
}
//Don't hire a hero where there already is one present
if (town->visitingHero && town->garrisonHero)
continue;
float visitability = 0;
for (auto checkHero : ourHeroes)
{
if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
visitability++;
}
if(ai->heroManager->canRecruitHero(town))
{
auto availableHeroes = ai->cb->getAvailableHeroes(town);
for(auto hero : availableHeroes)
{
auto score = ai->heroManager->evaluateHero(hero);
if(score > minScoreToHireMain)
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
break;
}
}
int treasureSourcesCount = 0;
for (auto obj : ai->objectClusterizer->getNearbyObjects())
{
if ((obj->ID == Obj::RESOURCE)
@ -81,14 +95,39 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
}
}
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
continue;
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
for(auto hero : availableHeroes)
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
auto score = ai->heroManager->evaluateHero(hero);
if(score > minScoreToHireMain)
{
score *= score / minScoreToHireMain;
}
score *= (hero->getArmyCost() + currentArmyValue);
if (hero->getFactionID() == town->getFactionID())
score *= 1.5;
if (vstd::isAlmostZero(visitability))
score *= 30 * town->getTownLevel();
else
score *= town->getTownLevel() / visitability;
if (score > bestScore)
{
bestScore = score;
bestHeroToHire = hero;
bestTownToHireFrom = town;
}
}
}
if (town->hasCapitol())
haveCapitol = true;
}
if (bestHeroToHire && bestTownToHireFrom)
{
if (ai->cb->getHeroesInfo().size() == 0
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
}
}

View File

@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
for(auto town : towns)
{
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
continue;
ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
for(auto & path : paths)
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue;
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
{
if(path.targetHero->mana == path.targetHero->manaLimit())
continue;
Composition stayAtTown;
stayAtTown.addNextSequence({

View File

@ -17,8 +17,7 @@
namespace NKAI
{
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
engineBase::engineBase()
{

View File

@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
{
objectDanger += evaluateDanger(hero->visitedTown.get());
}
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
}
if (objWithID<Obj::TOWN>(dangerousObject))
{
auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
auto hero = town->garrisonHero;
if (hero)
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
}
if(objectDanger)
@ -118,9 +127,9 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
auto fortLevel = town->fortLevel();
if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger += 10000;
danger = std::max(danger * 2, danger + 10000);
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
danger += 4000;
danger = std::max(ui64(danger * 1.4), danger + 4000);
}
return danger;

View File

@ -34,13 +34,12 @@ using namespace Goals;
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
Nullkiller::Nullkiller()
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
: activeHero(nullptr)
, scanDepth(ScanDepth::MAIN_FULL)
, useHeroChain(true)
, memory(std::make_unique<AIMemory>())
{
memory = std::make_unique<AIMemory>();
settings = std::make_unique<Settings>();
useObjectGraph = settings->isObjectGraphAllowed();
openMap = settings->isOpenMap() || useObjectGraph;
}
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
return false;
}
return cb->getStartInfo()->difficulty >= 3;
return true;
}
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
{
this->cb = cb;
this->gateway = gateway;
this->playerID = gateway->playerID;
playerID = gateway->playerID;
settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
if(openMap && !canUseOpenMap(cb, playerID))
if(canUseOpenMap(cb, playerID))
{
useObjectGraph = settings->isObjectGraphAllowed();
openMap = settings->isOpenMap() || useObjectGraph;
}
else
{
useObjectGraph = false;
openMap = false;
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
{
TGoalVec blockers;
if (task->asTask()->priority <= 0)
return;
for(auto & item : tasks)
{
for(auto objid : item.affectedObjects)
{
if(task == item.task || task->asTask()->isObjectAffected(objid))
if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
{
if(item.task->asTask()->priority >= task->asTask()->priority)
return;
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
return taskptr(*bestTask);
}
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
{
TaskPlan taskPlan;
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
{
auto evaluator = this->priorityEvaluators->acquire();
for(size_t i = r.begin(); i != r.end(); i++)
{
auto task = tasks[i];
if(task->asTask()->priority <= 0)
task->asTask()->priority = evaluator->evaluate(task);
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
task->asTask()->priority = evaluator->evaluate(task, priorityTier);
}
});
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
if(lockReason != HeroLockedReason::NOT_LOCKED)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString());
#endif
return true;
}
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState();
Goals::TGoalVec bestTasks;
#if NKAI_TRACE_LEVEL >= 1
float totalHeroStrength = 0;
int totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{
auto start = std::chrono::high_resolution_clock::now();
@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
Goals::TTask bestTask = taskptr(Goals::Invalid());
for(;i <= settings->getMaxPass(); i++)
while(true)
{
bestTasks.clear();
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
decompose(bestTasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(bestTasks);
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
if(bestTask->priority > 0)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask))
return;
@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
}
}
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
if(!isOpenMap())
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
TTaskVec selectedTasks;
#if NKAI_TRACE_LEVEL >= 1
int prioOfTask = 0;
#endif
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
{
decompose(bestTasks, sptr(StartupBehavior()), 1);
#if NKAI_TRACE_LEVEL >= 1
prioOfTask = prio;
#endif
selectedTasks = buildPlan(bestTasks, prio);
if (!selectedTasks.empty() || settings->isUseFuzzy())
break;
}
auto selectedTasks = buildPlan(bestTasks);
std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b)
{
return a->priority > b->priority;
});
logAi->debug("Decision madel in %ld", timeElapsed(start));
@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
bestTask->priority);
}
if(bestTask->priority < MIN_PRIORITY)
if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
continue;
}
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask))
{
if(hasAnySuccess)
@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
else
return;
}
hasAnySuccess = true;
}
hasAnySuccess |= handleTrading();
if(!hasAnySuccess)
{
logAi->trace("Nothing was done this turn. Ending turn.");
#if NKAI_TRACE_LEVEL >= 1
totalHeroStrength = 0;
totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
return;
}
@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
lockedResources += res;
}
bool Nullkiller::handleTrading()
{
bool haveTraded = false;
bool shouldTryToTrade = true;
int marketId = -1;
for (auto town : cb->getTownsInfo())
{
if (town->hasBuiltSomeTradeBuilding())
{
marketId = town->id;
}
}
if (marketId == -1)
return false;
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
{
if (const auto* m = dynamic_cast<const IMarket*>(obj))
{
while (shouldTryToTrade)
{
shouldTryToTrade = false;
buildAnalyzer->update();
TResources required = buildAnalyzer->getTotalResourcesRequired();
TResources income = buildAnalyzer->getDailyIncome();
TResources available = cb->getResourceAmount();
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("Available %s", available.toString());
logAi->debug("Required %s", required.toString());
#endif
int mostWanted = -1;
int mostExpendable = -1;
float minRatio = std::numeric_limits<float>::max();
float maxRatio = std::numeric_limits<float>::min();
for (int i = 0; i < required.size(); ++i)
{
if (required[i] <= 0)
continue;
float ratio = static_cast<float>(available[i]) / required[i];
if (ratio < minRatio) {
minRatio = ratio;
mostWanted = i;
}
}
for (int i = 0; i < required.size(); ++i)
{
float ratio = available[i];
if (required[i] > 0)
ratio = static_cast<float>(available[i]) / required[i];
else
ratio = available[i];
bool okToSell = false;
if (i == GameResID::GOLD)
{
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
okToSell = true;
}
else
{
if (required[i] <= 0 && income[i] > 0)
okToSell = true;
}
if (ratio > maxRatio && okToSell) {
maxRatio = ratio;
mostExpendable = i;
}
}
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
#endif
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
return false;
int toGive;
int toGet;
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
//TODO trade only as much as needed
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
{
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
#endif
haveTraded = true;
shouldTryToTrade = true;
}
}
}
}
return haveTraded;
}
}

View File

@ -120,13 +120,14 @@ public:
ScanDepth getScanDepth() const { return scanDepth; }
bool isOpenMap() const { return openMap; }
bool isObjectGraphAllowed() const { return useObjectGraph; }
bool handleTrading();
private:
void resetAiState();
void updateAiState(int pass, bool fast = false);
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
bool executeTask(Goals::TTask task);
bool areAffectedObjectsPresent(Goals::TTask task) const;
HeroRole getTaskRole(Goals::TTask task) const;

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h"
@ -33,9 +35,7 @@
namespace NKAI
{
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
constexpr float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller* ai)
: movementCost(0.0),
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
heroRole(HeroRole::SCOUT),
turn(0),
strategicalValue(0),
conquestValue(0),
evaluator(ai),
enemyHeroDangerRatio(0),
armyGrowth(0)
threat(0),
armyGrowth(0),
armyInvolvement(0),
defenseValue(0),
isDefend(false),
threatTurns(INT_MAX),
involvesSailing(false),
isTradeBuilding(false),
isExchange(false),
isArmyUpgrade(false),
isHero(false),
isEnemy(false),
explorePriority(0)
{
}
@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
auto creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree)
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
}
}
@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
switch(art->aClass)
{
case CArtifact::EartClass::ART_TREASURE:
//FALL_THROUGH
case CArtifact::EartClass::ART_MINOR:
classValue = 1000;
break;
@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::SPELL_SCROLL:
//FALL_THROUGH
case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
case Obj::HERO:
@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result;
}
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
{
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
}
@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
return 0;
}
float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
{
if (!target)
return 0;
if (target->getOwner() == ai->playerID)
return 0;
switch (target->ID)
{
case Obj::TOWN:
{
if (ai->buildAnalyzer->getDevelopmentInfo().empty())
return 10.0f;
auto town = dynamic_cast<const CGTownInstance*>(target);
if (town->getOwner() == ai->playerID)
{
auto armyIncome = townArmyGrowth(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
}
auto fortLevel = town->fortLevel();
auto booster = 1.0f;
if (town->hasCapitol())
return booster * 1.5;
if (fortLevel < CGTownInstance::CITADEL)
return booster * (town->hasFort() ? 1.0 : 0.8);
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
}
case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
: 0;
case Obj::KEYMASTER:
return 0.6f;
default:
return 0;
}
}
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
{
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
for(auto stack : army->Slots())
{
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
}
return value;
@ -786,7 +851,9 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
evaluationContext.isExchange = true;
}
};
@ -804,6 +871,7 @@ public:
evaluationContext.armyReward += upgradeValue;
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
evaluationContext.isArmyUpgrade = true;
}
};
@ -818,6 +886,25 @@ public:
int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
{
switch (obj->ID.num)
{
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
evaluationContext.explorePriority = 1;
break;
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
evaluationContext.explorePriority = 2;
break;
}
}
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
evaluationContext.explorePriority = 1;
if (evaluationContext.explorePriority == 0)
evaluationContext.explorePriority = 3;
}
};
@ -832,8 +919,13 @@ public:
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
if (evaluationContext.armyReward == 0)
evaluationContext.isDefend = true;
else
{
evaluationContext.movementCost += stayAtTown.getMovementWasted();
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
}
}
};
@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger)
{
auto dangerRatio = enemyDanger.danger / (double)ourStrength;
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
if(isAI)
{
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
}
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
}
}
@ -896,6 +981,10 @@ public:
else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
evaluationContext.defenseValue = town->fortLevel();
evaluationContext.isDefend = true;
evaluationContext.threatTurns = treat.turn;
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
}
@ -926,6 +1015,8 @@ public:
for(auto & node : path.nodes)
{
vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
}
for(auto pair : costsPerHero)
@ -952,10 +1043,18 @@ public:
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
if (target->ID == Obj::HERO)
evaluationContext.isHero = true;
if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
evaluationContext.isEnemy = true;
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
evaluationContext.armyInvolvement += army->getArmyCost();
if(evaluationContext.danger > 0)
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
}
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn());
}
@ -996,6 +1095,7 @@ public:
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -1021,6 +1121,14 @@ public:
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
for (auto obj : swapCommand.getAffectedObjects())
{
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
}
if (garrisonHero)
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@ -1029,6 +1137,9 @@ public:
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
evaluationContext.heroRole = defenderRole;
evaluationContext.isDefend = true;
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
}
}
};
@ -1072,8 +1183,14 @@ public:
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD];
int32_t cost = bi.buildCost[EGameResID::GOLD];
evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1;
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
evaluationContext.isTradeBuilding = true;
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
if(bi.creatureID != CreatureID::NONE)
{
@ -1100,7 +1217,18 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
{
evaluationContext.armyInvolvement += hero->getArmyCost();
}
}
int sameTownBonus = 0;
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
{
if (buildThis.town->getFaction() == town->getFaction())
sameTownBonus += town->getTownLevel();
}
evaluationContext.armyReward *= sameTownBonus;
if(evaluationContext.goldReward)
{
@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
for(auto subgoal : parts)
{
context.goldCost += subgoal->goldCost;
context.buildingCost += subgoal->buildingCost;
for(auto builder : evaluationContextBuilders)
{
@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
return context;
}
float PriorityEvaluator::evaluate(Goals::TSubgoal task)
float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{
auto evaluationContext = buildEvaluationContext(task);
@ -1185,6 +1314,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
double result = 0;
if (ai->settings->isUseFuzzy())
{
float fuzzyResult = 0;
try
{
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
@ -1206,15 +1338,26 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
engine->process();
result = value->getValue();
fuzzyResult = value->getValue();
}
catch (fl::Exception& fe)
{
logAi->error("evaluate VisitTile: %s", fe.getWhat());
}
result = fuzzyResult;
}
else
{
float score = 0;
float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25;
bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
arriveNextWeek = true;
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
priorityTier,
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
@ -1223,9 +1366,220 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
goldRewardPerTurn,
evaluationContext.goldCost,
evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
evaluationContext.explorePriority,
evaluationContext.isDefend);
#endif
switch (priorityTier)
{
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
{
if (evaluationContext.turn > 0)
return 0;
if(evaluationContext.conquestValue > 0)
score = 1000;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
{
if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
score = evaluationContext.armyInvolvement;
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score *= evaluationContext.closestWayRatio;
break;
}
case PriorityTier::KILL: //Take towns / kill heroes that are further away
{
if (evaluationContext.turn > 0 && evaluationContext.isHero)
return 0;
if (arriveNextWeek && evaluationContext.isEnemy)
return 0;
if (evaluationContext.conquestValue > 0)
score = 1000;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::UPGRADE:
{
if (!evaluationContext.isArmyUpgrade)
return 0;
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HIGH_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
return 0;
if (evaluationContext.buildingCost.marketValue() > 0)
return 0;
if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
return 0;
if (evaluationContext.explorePriority == 3)
return 0;
if (evaluationContext.isArmyUpgrade)
return 0;
if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
score -= evaluationContext.goldCost;
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
if (score > 0)
{
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
}
break;
}
case PriorityTier::LOW_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 3)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
{
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
return 0;
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
score = 1000;
score *= evaluationContext.closestWayRatio;
score /= (evaluationContext.turn + 1);
break;
}
case PriorityTier::BUILDINGS: //For buildings and buying army
{
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
//If we already have locked resources, we don't look at other buildings
if (ai->getLockedResources().marketValue() > 0)
return 0;
score += evaluationContext.conquestValue * 1000;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
if (evaluationContext.buildingCost.marketValue() > 0)
{
if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
{
logAi->trace("Should make sure to build market-place instead of %s", task->toString());
for (auto town : ai->cb->getTownsInfo())
{
if (!town->hasBuiltSomeTradeBuilding())
return 0;
}
}
score += 1000;
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
auto income = ai->buildAnalyzer->getDailyIncome();
if(ai->buildAnalyzer->isGoldPressureHigh())
score /= evaluationContext.buildingCost.marketValue();
if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
{
TResources needed = evaluationContext.buildingCost - resourcesAvailable;
needed.positive();
int turnsTo = needed.maxPurchasableCount(income);
if (turnsTo == INT_MAX)
return 0;
else
score /= turnsTo;
}
}
else
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
return 0;
}
break;
}
}
result = score;
//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
if (std::isnan(result))
return 0;
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
priorityTier,
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT],
goldRewardPerTurn,
evaluationContext.goldCost,
evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
result);

View File

@ -41,6 +41,7 @@ public:
float getResourceRequirementStrength(int resType) const;
float getResourceRequirementStrength(const TResources & res) const;
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
float getConquestValue(const CGObjectInstance* target) const;
float getTotalResourceRequirementStrength(int resType) const;
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
@ -48,7 +49,7 @@ public:
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) const;
uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
};
struct DLL_EXPORT EvaluationContext
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
int32_t goldCost;
float skillReward;
float strategicalValue;
float conquestValue;
HeroRole heroRole;
uint8_t turn;
RewardEvaluator evaluator;
float enemyHeroDangerRatio;
float threat;
float armyInvolvement;
int defenseValue;
bool isDefend;
int threatTurns;
TResources buildingCost;
bool involvesSailing;
bool isTradeBuilding;
bool isExchange;
bool isArmyUpgrade;
bool isHero;
bool isEnemy;
int explorePriority;
EvaluationContext(const Nullkiller * ai);
@ -91,7 +106,20 @@ public:
~PriorityEvaluator();
void initVisitTile();
float evaluate(Goals::TSubgoal task);
float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
enum PriorityTier : int32_t
{
BUILDINGS = 0,
INSTAKILL,
INSTADEFEND,
KILL,
UPGRADE,
HIGH_PRIO_EXPLORE,
HUNTER_GATHER,
LOW_PRIO_EXPLORE,
DEFEND
};
private:
const Nullkiller * ai;

View File

@ -11,6 +11,8 @@
#include <limits>
#include "Settings.h"
#include "../../../lib/constants/StringConstants.h"
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
@ -22,56 +24,39 @@
namespace NKAI
{
Settings::Settings()
Settings::Settings(int difficultyLevel)
: maxRoamingHeroes(8),
mainHeroTurnDistanceLimit(10),
scoutHeroTurnDistanceLimit(5),
maxGoldPressure(0.3f),
retreatThresholdRelative(0.3),
retreatThresholdAbsolute(10000),
safeAttackRatio(1.1),
maxpass(10),
pathfinderBucketsCount(1),
pathfinderBucketSize(32),
allowObjectGraph(true),
useTroopsFromGarrisons(false),
openMap(true)
openMap(true),
useFuzzy(false)
{
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
const JsonNode & node = rootNode[difficultyName];
if(node.Struct()["maxRoamingHeroes"].isNumber())
{
maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
}
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
{
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
}
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
{
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
}
if(node.Struct()["maxpass"].isNumber())
{
maxpass = node.Struct()["maxpass"].Integer();
}
if(node.Struct()["maxGoldPressure"].isNumber())
{
maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
}
if(!node.Struct()["allowObjectGraph"].isNull())
{
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
}
if(!node.Struct()["openMap"].isNull())
{
openMap = node.Struct()["openMap"].Bool();
}
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
{
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
}
maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
maxpass = node["maxpass"].Integer();
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
maxGoldPressure = node["maxGoldPressure"].Float();
retreatThresholdRelative = node["retreatThresholdRelative"].Float();
retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
safeAttackRatio = node["safeAttackRatio"].Float();
allowObjectGraph = node["allowObjectGraph"].Bool();
openMap = node["openMap"].Bool();
useFuzzy = node["useFuzzy"].Bool();
useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
}
}

View File

@ -25,21 +25,33 @@ namespace NKAI
int mainHeroTurnDistanceLimit;
int scoutHeroTurnDistanceLimit;
int maxpass;
int pathfinderBucketsCount;
int pathfinderBucketSize;
float maxGoldPressure;
float retreatThresholdRelative;
float retreatThresholdAbsolute;
float safeAttackRatio;
bool allowObjectGraph;
bool useTroopsFromGarrisons;
bool openMap;
bool useFuzzy;
public:
Settings();
explicit Settings(int difficultyLevel);
int getMaxPass() const { return maxpass; }
float getMaxGoldPressure() const { return maxGoldPressure; }
float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
float getSafeAttackRatio() const { return safeAttackRatio; }
int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
int getPathfinderBucketSize() const { return pathfinderBucketSize; }
bool isObjectGraphAllowed() const { return allowObjectGraph; }
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
bool isOpenMap() const { return openMap; }
bool isUseFuzzy() const { return useFuzzy; }
};
}

View File

@ -104,6 +104,7 @@ namespace Goals
bool isAbstract; SETTER(bool, isAbstract)
int value; SETTER(int, value)
ui64 goldCost; SETTER(ui64, goldCost)
TResources buildingCost; SETTER(TResources, buildingCost)
int resID; SETTER(int, resID)
int objid; SETTER(int, objid)
int aid; SETTER(int, aid)

View File

@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
}
if (hero->inTownGarrison)
ai->myCb->swapGarrisonHero(hero->visitedTown);
auto wait = cb->waitTillRealize;
cb->waitTillRealize = true;

View File

@ -89,12 +89,15 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
auto upperArmy = town->getUpperArmy();
if(!town->garrisonHero)
{
if (!garrisonHero->canBeMergedWith(*town))
{
while (upperArmy->stacksCount() != 0)
{
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
}
}
}
cb->swapGarrisonHero(town);

View File

@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
{
hero = path.targetHero;
tile = path.targetTile();
closestWayRatio = 1;
if(obj)
{
@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid);
ai->nullkiller->objectClusterizer->reset();
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);

View File

@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
ai->nullkiller->heroManager->update();
ai->nullkiller->objectClusterizer->reset();
}
}

View File

@ -44,6 +44,7 @@ namespace Goals
}
std::string toString() const override;
const CGHeroInstance* getHero() const override { return heroToBuy; }
void accept(AIGateway * ai) override;
};
}

View File

@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
{
return "Stay at town " + town->getNameTranslated()
+ " hero " + hero->getNameTranslated()
+ ", mana: " + std::to_string(hero->mana);
+ ", mana: " + std::to_string(hero->mana)
+ " / " + std::to_string(hero->manaLimit());
}
void StayAtTown::accept(AIGateway * ai)
{
if(hero->visitedTown != town)
{
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
}
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
}

View File

@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
continue;
}
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
{
bestGoal = goal;
bestValue = ourValue;

View File

@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
AISharedStorage::AISharedStorage(int3 sizes)
AISharedStorage::AISharedStorage(int3 sizes, int numChains)
{
if(!shared){
shared.reset(new boost::multi_array<AIPathNode, 4>(
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
nodes = shared;
foreach_tile_pos([&](const int3 & pos)
{
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
for(auto i = 0; i < numChains; i++)
{
auto & node = get(pos)[i];
@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
}
}
int AINodeStorage::getBucketCount() const
{
return ai->settings->getPathfinderBucketsCount();
}
int AINodeStorage::getBucketSize() const
{
return ai->settings->getPathfinderBucketSize();
}
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
{
accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
const EPathfindingLayer layer,
const ChainActor * actor)
{
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
auto chains = nodes.get(pos);
if(blocked(pos, layer))
@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
return std::nullopt;
}
for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
{
AIPathNode & node = chains[i + bucketOffset];
@ -486,8 +496,8 @@ public:
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
{
existingChains.reserve(AIPathfinding::NUM_CHAINS);
newChains.reserve(AIPathfinding::NUM_CHAINS);
existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
}
void execute(const tbb::blocked_range<size_t>& r)
@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(node->action == EPathNodeAction::BATTLE
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|| node->action == EPathNodeAction::DISEMBARK
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
{
continue;
@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
// do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID
&& hero.first->inTownGarrison
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
{
continue;
}
@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
continue;
}
if (targetTown->visitingHero
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|| targetTown->getUpperArmy()->stacksCount()))
continue;
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
if(nodeOptional)
@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.heroArmy = node.actor->creatureSet;
path.armyLoss = node.armyLoss;
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
for (auto pathNode : path.nodes)
{
path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
}
if(path.targetObjectDanger > 0)
{
@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
uint64_t AIPath::getHeroStrength() const
{
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
}
uint64_t AIPath::getTotalDanger() const

View File

@ -29,9 +29,6 @@ namespace NKAI
{
namespace AIPathfinding
{
const int BUCKET_COUNT = 3;
const int BUCKET_SIZE = 7;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int CHAIN_MAX_DEPTH = 4;
}
@ -157,7 +154,7 @@ public:
static boost::mutex locker;
static uint32_t version;
AISharedStorage(int3 mapSize);
AISharedStorage(int3 sizes, int numChains);
~AISharedStorage();
STRONG_INLINE
@ -197,6 +194,9 @@ public:
bool selectFirstActor();
bool selectNextActor();
int getBucketCount() const;
int getBucketSize() const;
std::vector<CGPathNode *> getInitialNodes() override;
virtual void calculateNeighbours(
@ -298,7 +298,7 @@ public:
inline int getBucket(const ChainActor * actor) const
{
return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
return ((uintptr_t)actor * 395) % getBucketCount();
}
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);

View File

@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
initialMovement = hero->movementPointsRemaining();
initialTurn = 0;
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
heroFightingStrength = hero->getFightingStrength();
heroFightingStrength = hero->getHeroStrength();
tiCache.reset(new TurnInfo(hero));
}

View File

@ -25,11 +25,9 @@ using crstring = const std::string &;
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
const int ACTUAL_RESOURCE_COUNT = 7;
const int ALLOWED_ROAMING_HEROES = 8;
//implementation-dependent
extern const double SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern thread_local CCallback * cb;
extern thread_local VCAI * ai;

View File

@ -14,8 +14,6 @@
#include "../../CCallback.h"
#include "../../lib/mapObjects/MapObjects.h"
#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol
ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
: resources(Res), goal(Goal)
{

View File

@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
return false;
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
return false;
if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
return false;
if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false;
if(!cb->getAvailableHeroes(t).size())

View File

@ -0,0 +1,280 @@
{
"config" : {
"default" : true,
// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md
"MD001": false,
// MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md
"MD003": {
"style": "atx"
},
// MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md
"MD004": false,
// FIXME: enable and consider fixing
//{
// "style": "consistent"
//},
// MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md
"MD005": true,
// MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md
"MD007": {
// Spaces for indent
"indent": 2,
// Whether to indent the first level of the list
"start_indented": false,
// Spaces for first level indent (when start_indented is set)
"start_indent": 0
},
// MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md
"MD009": {
// Spaces for line break
"br_spaces": 2,
// Allow spaces for empty lines in list items
"list_item_empty_lines": false,
// Include unnecessary breaks
"strict": false
},
// MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md
"MD010": {
// Include code blocks
"code_blocks": false,
// Fenced code languages to ignore
"ignore_code_languages": [],
// Number of spaces for each hard tab
"spaces_per_tab": 4
},
// MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md
"MD011": true,
// MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md
"MD012": {
// Consecutive blank lines
"maximum": 1
},
// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md
"MD013": false,
// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md
"MD014": true,
// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md
"MD018": true,
// MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md
"MD019": true,
// MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md
"MD020": true,
// MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md
"MD021": true,
// MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md
"MD022": {
// Blank lines above heading
"lines_above": 1,
// Blank lines below heading
"lines_below": 1
},
// MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md
"MD023": true,
// MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md
"MD024": false,
// FIXME: false positives?
//{
// // Only check sibling headings
// "allow_different_nesting": true,
// // Only check sibling headings
// "siblings_only": true
//},
// MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md
"MD025": {
// Heading level
"level": 1,
// RegExp for matching title in front matter
"front_matter_title": "^\\s*title\\s*[:=]"
},
// MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md
"MD026": {
// Punctuation characters
"punctuation": ".,;:!。,;:!"
},
// MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md
"MD027": true,
// MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md
"MD028": true,
// MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md
"MD029": false,
// FIXME: false positives or broken formatting
//{
// // List style
// "style": "ordered"
//},
// MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md
"MD030": {
// Spaces for single-line unordered list items
"ul_single": 1,
// Spaces for single-line ordered list items
"ol_single": 1,
// Spaces for multi-line unordered list items
"ul_multi": 1,
// Spaces for multi-line ordered list items
"ol_multi": 1
},
// MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md
"MD031": {
// Include list items
"list_items": false
},
// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md
"MD032": true,
// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md
"MD033": false,
// FIXME: enable and consider fixing
//{
// // Allowed elements
// "allowed_elements": []
//},
// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md
"MD034": true,
// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md
"MD035": {
// Horizontal rule style
"style": "consistent"
},
// MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md
"MD036": false,
// FIXME: enable and consider fixing
// {
// // Punctuation characters
// "punctuation": ".,;:!?。,;:!?"
// },
// MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md
"MD037": true,
// MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md
"MD038": true,
// MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md
"MD039": true,
// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
"MD040": false,
// FIXME: enable and consider fixing
//{
//// List of languages
// "allowed_languages": [ "cpp", "json5", "sh" ],
//// Require language only
// "language_only": true
//},
// MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md
"MD041": {
// Heading level
"level": 1,
// RegExp for matching title in front matter
"front_matter_title": "^\\s*title\\s*[:=]"
},
// MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md
"MD042": true,
// MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md
"MD043": false,
// MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md
"MD044": false,
// MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md
"MD045": false,
// MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md
"MD046": {
// Block style
"style": "fenced"
},
// MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md
"MD047": true,
// MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md
"MD048": {
// Code fence style
"style": "backtick"
},
// MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md
"MD049": {
// Emphasis style
"style": "asterisk"
},
// MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md
"MD050": {
// Strong style
"style": "asterisk"
},
// MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md
"MD051": true,
// MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md
"MD052": {
// Include shortcut syntax
"shortcut_syntax": false
},
// MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md
"MD053": {
// Ignored definitions
"ignored_definitions": [
"//"
]
},
// MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md
"MD054": {
// Allow autolinks
"autolink": true,
// Allow inline links and images
"inline": true,
// Allow full reference links and images
"full": true,
// Allow collapsed reference links and images
"collapsed": true,
// Allow shortcut reference links and images
"shortcut": true,
// Allow URLs as inline links
"url_inline": true
},
// MD058 - Tables should be surrounded by blank lines
"MD058" : true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的",
"vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的",
"vcmi.adventureMap.monsterThreat.levels.10" : "致命的",
"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜",
"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜",
"vcmi.adventureMap.monsterLevel" : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物",
"vcmi.adventureMap.monsterMeleeType" : "近战",
"vcmi.adventureMap.monsterRangedType" : "远程",
@ -188,9 +188,6 @@
"vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。",
"vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}",
"vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}",
"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
"vcmi.server.errors.modDependencyLoop" : "读取mod包 {'%s'}失败!\n 这个mod可能存在循环(软)依赖!",
"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
@ -304,7 +301,7 @@
"vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器",
"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)",
"vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小",
"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)",
"vcmi.battleOptions.queueSizeBigButton.help": "设置回合顺序指示器为大尺寸(无法在游戏高度像素低于700时生效)",
"vcmi.battleOptions.animationsSpeed1.hover": "",
"vcmi.battleOptions.animationsSpeed5.hover": "",
"vcmi.battleOptions.animationsSpeed6.hover": "",
@ -384,7 +381,7 @@
"vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面",
"vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物",
"vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序",
"vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。.",
"vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。",
"vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序",
"vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。",
"vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序",

View File

@ -86,12 +86,13 @@
"vcmi.spellBook.search" : "Hledat",
"vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu kouzel.",
"vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu dalších kouzel.",
"vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vraťte se zítra.",
"vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu kouzel?",
"vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?",
"vcmi.spellResearch.research" : "Prozkoumat toto kouzlo",
"vcmi.spellResearch.skip" : "Přeskočit toto kouzlo",
"vcmi.spellResearch.abort" : "Přerušit",
"vcmi.spellResearch.noMoreSpells" : "Žádná další kouzla k výzkumu nejsou dostupná.",
"vcmi.mainMenu.serverConnecting" : "Připojování...",
"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
@ -113,13 +114,51 @@
"vcmi.lobby.handicap.resource" : "Dává hráčům odpovídající zdroje navíc k běžným startovním zdrojům. Jsou povoleny záporné hodnoty, ale jsou omezeny na celkovou hodnotu 0 (hráč nikdy nezačíná se zápornými zdroji).",
"vcmi.lobby.handicap.income" : "Mění různé příjmy hráče podle procent. Výsledek je zaokrouhlen nahoru.",
"vcmi.lobby.handicap.growth" : "Mění rychlost růstu jednotel v městech vlastněných hráčem. Výsledek je zaokrouhlen nahoru.",
"vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry (např. z předchozích verzí).\n\nChcete je odstranit?",
"vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry.\n\nBylo nalezeno %d uložených her, které již nejsou podporovány, pravděpodobně kvůli rozdílům mezi verzemi VCMI.\n\nChcete je odstranit?",
"vcmi.lobby.deleteSaveGameTitle" : "Vyberte uloženou hru k odstranění",
"vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění",
"vcmi.lobby.deleteFile" : "Chcete smazat následující soubor?",
"vcmi.lobby.deleteFolder" : "Chcete smazat následující složku?",
"vcmi.lobby.deleteFile" : "Chcete odstranit následující soubor?",
"vcmi.lobby.deleteFolder" : "Chcete odstranit následující složku?",
"vcmi.lobby.deleteMode" : "Přepnout do režimu mazání a zpět",
"vcmi.broadcast.failedLoadGame" : "Nepodařilo se načíst hru",
"vcmi.broadcast.command" : "Použijte '!help' pro zobrazení dostupných příkazů",
"vcmi.broadcast.simturn.end" : "Současné tahy byly ukončeny",
"vcmi.broadcast.simturn.endBetween" : "Současné tahy mezi hráči %s a %s byly ukončeny",
"vcmi.broadcast.serverProblem" : "Server narazil na problém",
"vcmi.broadcast.gameTerminated" : "Hra byla ukončena",
"vcmi.broadcast.gameSavedAs" : "Hra byla uložena jako",
"vcmi.broadcast.noCheater" : "Nejsou zaznamenáni žádní podvodníci!",
"vcmi.broadcast.playerCheater" : "Hráč %s je podvodník!",
"vcmi.broadcast.statisticFile" : "Soubory se statistikou lze nalézt v adresáři %s",
"vcmi.broadcast.help.commands" : "Dostupné příkazy pro hostitele:",
"vcmi.broadcast.help.exit" : "'!exit' - okamžitě ukončí aktuální hru",
"vcmi.broadcast.help.kick" : "'!kick <hráč>' - vyhodí vybraného hráče ze hry",
"vcmi.broadcast.help.save" : "'!save <název_souboru>' - uloží hru pod zadaným názvem",
"vcmi.broadcast.help.statistic" : "'!statistic' - uloží statistiky hry jako soubor CSV",
"vcmi.broadcast.help.commandsAll" : "Dostupné příkazy pro všechny hráče:",
"vcmi.broadcast.help.help" : "'!help' - zobrazí tuto nápovědu",
"vcmi.broadcast.help.cheaters" : "'!cheaters' - zobrazí seznam hráčů, kteří během hry použili cheaty",
"vcmi.broadcast.help.vote" : "'!vote' - umožňuje změnit některá nastavení hry, pokud všichni hráči souhlasí",
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - povolí současné tahy na určený počet dní nebo dokud nenastane kontakt",
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - vynutí současné tahy na určený počet dní s blokováním kontaktů hráčů",
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - ukončí současné tahy po skončení aktuálního tahu",
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prodlouží základní časovač pro všechny hráče o určený počet sekund",
"vcmi.broadcast.vote.noActive" : "Žádné aktivní hlasování!",
"vcmi.broadcast.vote.yes" : "ano",
"vcmi.broadcast.vote.no" : "ne",
"vcmi.broadcast.vote.notRecognized" : "Hlasovací příkaz nebyl rozpoznán!",
"vcmi.broadcast.vote.success.untilContacts" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní nebo dokud nenastane kontakt",
"vcmi.broadcast.vote.success.contactsBlocked" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní. Kontakty jsou blokovány",
"vcmi.broadcast.vote.success.nextDay" : "Hlasování bylo úspěšné. Současné tahy skončí následující den",
"vcmi.broadcast.vote.success.timer" : "Hlasování bylo úspěšné. Časovač pro všechny hráče byl prodloužen o %s sekund",
"vcmi.broadcast.vote.aborted" : "Hráč hlasoval proti změně. Hlasování bylo ukončeno",
"vcmi.broadcast.vote.start.untilContacts" : "Bylo zahájeno hlasování o povolení současných tahů na %s dní",
"vcmi.broadcast.vote.start.contactsBlocked" : "Bylo zahájeno hlasování o vynucení současných tahů na %s dní",
"vcmi.broadcast.vote.start.nextDay" : "Bylo zahájeno hlasování o ukončení současných tahů od následujícího dne",
"vcmi.broadcast.vote.start.timer" : "Bylo zahájeno hlasování o prodloužení časovače pro všechny hráče o %s sekund",
"vcmi.broadcast.vote.hint" : "Napište '!vote yes', pokud souhlasíte se změnou, nebo '!vote no', pokud jste proti",
"vcmi.lobby.login.title" : "Online lobby VCMI",
"vcmi.lobby.login.username" : "Uživatelské jméno:",
"vcmi.lobby.login.connecting" : "Připojování...",
@ -127,6 +166,7 @@
"vcmi.lobby.login.create" : "Nový účet",
"vcmi.lobby.login.login" : "Přihlásit se",
"vcmi.lobby.login.as" : "Přihlásit se jako %s",
"vcmi.lobby.login.spectator" : "Divák",
"vcmi.lobby.header.rooms" : "Herní místnosti - %d",
"vcmi.lobby.header.channels" : "Kanály konverzace",
"vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name
@ -187,10 +227,9 @@
"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",
"vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}",
"vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}",
"vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n",
"vcmi.server.errors.modDependencyLoop" : "Nelze načíst modifikaci {'%s'}!\n Modifikace může být součástí (nepřímé) závislostní smyčky.",
"vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!",
"vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.",
"vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!",
"vcmi.dimensionDoor.seaToLandError" : "Pomocí dimenzní brány není možné se teleportovat z moře na pevninu nebo naopak.",
@ -720,6 +759,30 @@
"core.bonus.DISINTEGRATE.description" : "Po smrti nezůstane žádné tělo",
"core.bonus.INVINCIBLE.name" : "Neporazitelný",
"core.bonus.INVINCIBLE.description" : "Nelze ovlivnit žádným efektem",
"core.bonus.MECHANICAL.description" : "Imunita vůči mnoha efektům, opravitelné",
"core.bonus.MECHANICAL.name" : "Mechanický",
"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech",
"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Útok trojitým dechem (útok přes 3 směry)"
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)",
"spell.core.castleMoat.name" : "Hradní příkop",
"spell.core.castleMoatTrigger.name" : "Hradní příkop",
"spell.core.catapultShot.name" : "Výstřel z katapultu",
"spell.core.cyclopsShot.name" : "Obléhací střela",
"spell.core.dungeonMoat.name" : "Vařící olej",
"spell.core.dungeonMoatTrigger.name" : "Vařící olej",
"spell.core.fireWallTrigger.name" : "Ohnivá zeď",
"spell.core.firstAid.name" : "První pomoc",
"spell.core.fortressMoat.name" : "Vařící dehet",
"spell.core.fortressMoatTrigger.name" : "Vařící dehet",
"spell.core.infernoMoat.name" : "Láva",
"spell.core.infernoMoatTrigger.name" : "Láva",
"spell.core.landMineTrigger.name" : "Pozemní mina",
"spell.core.necropolisMoat.name" : "Hřbitov",
"spell.core.necropolisMoatTrigger.name" : "Hřbitov",
"spell.core.rampartMoat.name" : "Ostružiní",
"spell.core.rampartMoatTrigger.name" : "Ostružiní",
"spell.core.strongholdMoat.name" : "Dřevěné bodce",
"spell.core.strongholdMoatTrigger.name" : "Dřevěné bodce",
"spell.core.summonDemons.name" : "Přivolání démonů",
"spell.core.towerMoat.name" : "Pozemní mina"
}

View File

@ -188,9 +188,6 @@
"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
"vcmi.server.errors.modsToEnable" : "{Following mods are required}",
"vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}",
"vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n",
"vcmi.server.errors.modDependencyLoop" : "Failed to load mod {'%s'}!\n It maybe in a (soft) dependency loop.",
"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.",

View File

@ -188,9 +188,7 @@
"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
"vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}",
"vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}",
"vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n",
"vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.",
"vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!",
"vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.",

View File

@ -182,9 +182,7 @@
"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
"vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}",
"vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}",
"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
"vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
"vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.",

View File

@ -28,6 +28,13 @@
"vcmi.adventureMap.movementPointsHeroInfo" : "(Pontos de movimento: %REMAINING / %POINTS)",
"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!",
"vcmi.bonusSource.artifact" : "Artefato",
"vcmi.bonusSource.creature" : "Habilidade",
"vcmi.bonusSource.spell" : "Feitiço",
"vcmi.bonusSource.hero" : "Herói",
"vcmi.bonusSource.commander" : "Comandante",
"vcmi.bonusSource.other" : "Outro",
"vcmi.capitalColors.0" : "Vermelho",
"vcmi.capitalColors.1" : "Azul",
"vcmi.capitalColors.2" : "Bege",
@ -85,6 +92,7 @@
"vcmi.spellResearch.research" : "Pesquisar este Feitiço",
"vcmi.spellResearch.skip" : "Pular este Feitiço",
"vcmi.spellResearch.abort" : "Abortar",
"vcmi.spellResearch.noMoreSpells" : "Não há mais feitiços disponíveis para pesquisa.",
"vcmi.mainMenu.serverConnecting" : "Conectando...",
"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
@ -96,16 +104,60 @@
"vcmi.lobby.filepath" : "Caminho do arquivo",
"vcmi.lobby.creationDate" : "Data de criação",
"vcmi.lobby.scenarioName" : "Nome do cenário",
"vcmi.lobby.mapPreview" : "Visualização do mapa",
"vcmi.lobby.noPreview" : "sem visualização",
"vcmi.lobby.mapPreview" : "Prévia do mapa",
"vcmi.lobby.noPreview" : "sem prévia",
"vcmi.lobby.noUnderground" : "sem subterrâneo",
"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
"vcmi.lobby.sortDate" : "Ordenar mapas por data de alteração",
"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
"vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Desvant.",
"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
"vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?",
"vcmi.lobby.deleteSaveGameTitle" : "Selecione um Jogo Salvo para excluir",
"vcmi.lobby.deleteMapTitle" : "Selecione um Cenário para excluir",
"vcmi.lobby.deleteFile" : "Deseja excluir o seguinte arquivo?",
"vcmi.lobby.deleteFolder" : "Deseja excluir a seguinte pasta?",
"vcmi.lobby.deleteMode" : "Alternar para o modo de exclusão e voltar",
"vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo",
"vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis",
"vcmi.broadcast.simturn.end" : "Os turnos simultâneos terminaram",
"vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s terminaram",
"vcmi.broadcast.serverProblem" : "O servidor encontrou um problema",
"vcmi.broadcast.gameTerminated" : "o jogo foi encerrado",
"vcmi.broadcast.gameSavedAs" : "jogo salvo como",
"vcmi.broadcast.noCheater" : "Nenhum trapaçeiro registrado!",
"vcmi.broadcast.playerCheater" : "O jogador %s é um trapaçeiro!",
"vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s",
"vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:",
"vcmi.broadcast.help.exit" : "'!exit' - termina imediatamente o jogo atual",
"vcmi.broadcast.help.kick" : "'!kick <player>' - expulsa o jogador especificado do jogo",
"vcmi.broadcast.help.save" : "'!save <filename>' - salva o jogo com o nome de arquivo especificado",
"vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv",
"vcmi.broadcast.help.commandsAll" : "Comandos disponíveis para todos os jogadores:",
"vcmi.broadcast.help.help" : "'!help' - exibe esta ajuda",
"vcmi.broadcast.help.cheaters" : "'!cheaters' - lista os jogadores que usaram comandos de trapaça durante o jogo",
"vcmi.broadcast.help.vote" : "'!vote' - permite mudar algumas configurações do jogo se todos os jogadores votarem a favor",
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato",
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores",
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar",
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o temporizador base para todos os jogadores por um número determinado de segundos",
"vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!",
"vcmi.broadcast.vote.yes" : "sim",
"vcmi.broadcast.vote.no" : "não",
"vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!",
"vcmi.broadcast.vote.success.untilContacts" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato",
"vcmi.broadcast.vote.success.contactsBlocked" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados",
"vcmi.broadcast.vote.success.nextDay" : "Votação bem-sucedida. Os turnos simultâneos terminarão no próximo dia",
"vcmi.broadcast.vote.success.timer" : "Votação bem-sucedida. O temporizador para todos os jogadores foi prolongado por %s segundos",
"vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada",
"vcmi.broadcast.vote.start.untilContacts" : "Iniciada votação para permitir turnos simultâneos por mais %s dias",
"vcmi.broadcast.vote.start.contactsBlocked" : "Iniciada votação para forçar turnos simultâneos por mais %s dias",
"vcmi.broadcast.vote.start.nextDay" : "Iniciada votação para terminar os turnos simultâneos a partir do próximo dia",
"vcmi.broadcast.vote.start.timer" : "Iniciada votação para prolongar o temporizador para todos os jogadores por %s segundos",
"vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra",
"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
"vcmi.lobby.login.username" : "Nome de usuário:",
@ -114,6 +166,7 @@
"vcmi.lobby.login.create" : "Nova Conta",
"vcmi.lobby.login.login" : "Entrar",
"vcmi.lobby.login.as" : "Entrar como %s",
"vcmi.lobby.login.spectator" : "Espectador",
"vcmi.lobby.header.rooms" : "Salas de Jogo - %d",
"vcmi.lobby.header.channels" : "Canais de Bate-papo",
"vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma
@ -174,9 +227,9 @@
"vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.",
"vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}",
"vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}",
"vcmi.server.errors.modNoDependency" : "Falha ao carregar o mod {'%s'}!\n Ele depende do mod {'%s'}, que não está ativo!\n",
"vcmi.server.errors.modConflict" : "Falha ao carregar o mod {'%s'}!\n Conflito com o mod ativo {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!",
"vcmi.server.errors.wrongIdentified" : "Você foi identificado como jogador %s, enquanto se espera %s",
"vcmi.server.errors.notAllowed" : "Você não tem permissão para realizar esta ação!",
"vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.",
@ -610,7 +663,7 @@
"core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém",
"core.bonus.FLYING.name" : "Voo",
"core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)",
"core.bonus.FREE_SHOOTING.name" : "Tiro Livre",
"core.bonus.FREE_SHOOTING.name" : "Tiro Curto",
"core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo",
"core.bonus.GARGOYLE.name" : "Gárgula",
"core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado",
@ -629,7 +682,7 @@
"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imune a Feitiços 1-${val}",
"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 1-${val}",
"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado",
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos",
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a mais de ${val} hexágonos de distância",
"core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)",
"core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado",
"core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%",
@ -706,9 +759,10 @@
"core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte",
"core.bonus.INVINCIBLE.name": "Invencível",
"core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada",
"core.bonus.MECHANICAL.name": "Mecânico",
"core.bonus.MECHANICAL.description": "Imunidade a muitos efeitos, reparável",
"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Sopro Prismático",
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Ataque de Sopro Prismático (três direções)",
"vcmi.server.errors.modDependencyLoop" : "Falha ao carregar o mod {'%s'}!\n Ele pode estar em um ciclo de dependência.",
"spell.core.castleMoat.name": "Fosso",
"spell.core.castleMoatTrigger.name": "Fosso",

View File

@ -79,8 +79,6 @@
"vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}",
"vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}",
"vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?",
"vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n",
"vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n",
"vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.",
"vcmi.settingsMainWindow.generalTab.hover" : "General",

View File

@ -188,9 +188,7 @@
"vcmi.server.errors.existingProcess" : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.",
"vcmi.server.errors.modsToEnable" : "{Följande modd(ar) krävs}",
"vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}",
"vcmi.server.errors.modNoDependency" : "Misslyckades med att ladda modd {'%s'}!\n Den är beroende av modd {'%s'} som inte är aktiverad!\n",
"vcmi.server.errors.modDependencyLoop": "Misslyckades med att ladda modd {'%s'}!\n Den kanske är i en (mjuk) beroendeloop.",
"vcmi.server.errors.modConflict" : "Misslyckades med att ladda modd {'%s'}!\n Konflikter med aktiverad modd {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!",
"vcmi.dimensionDoor.seaToLandError" : "Det går inte att teleportera sig från hav till land eller tvärtom med trollformeln 'Dimensionsdörr'.",

View File

@ -139,8 +139,6 @@
"vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}",
"vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}",
"vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?",
"vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n",
"vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",

View File

@ -396,16 +396,19 @@ void PlayerLocalState::deserialize(const JsonNode & source)
}
}
if (!source["spellbook"].isNull())
{
spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
}
// append any owned heroes / towns that were not present in loaded state
wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end());
ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end());
//FIXME: broken, anything that is selected in here will be overwritten on NewTurn pack
//FIXME: broken, anything that is selected in here will be overwritten on PlayerStartsTurn pack
// ObjectInstanceID selectedObjectID(source["currentSelection"].Integer());
// const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID);
// const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr);

View File

@ -18,6 +18,7 @@
#include "../gui/CursorHandler.h"
#include "../gui/EventDispatcher.h"
#include "../gui/ShortcutHandler.h"
#include "../render/IScreenHandler.h"
#include "../../lib/CConfigHandler.h"
@ -198,9 +199,10 @@ void InputSourceGameController::tryToConvertCursor()
assert(CCS->curh);
if(CCS->curh->getShowType() == Cursor::ShowType::HARDWARE)
{
int scalingFactor = GH.screenHandler().getScalingFactor();
const Point & cursorPosition = GH.getCursorPosition();
CCS->curh->changeCursor(Cursor::ShowType::SOFTWARE);
CCS->curh->cursorMove(cursorPosition.x, cursorPosition.y);
CCS->curh->cursorMove(cursorPosition.x * scalingFactor, cursorPosition.y * scalingFactor);
GH.input().setCursorPosition(cursorPosition);
}
}
@ -225,12 +227,13 @@ void InputSourceGameController::doCursorMove(int deltaX, int deltaY)
return;
const Point & screenSize = GH.screenDimensions();
const Point & cursorPosition = GH.getCursorPosition();
int scalingFactor = GH.screenHandler().getScalingFactor();
int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x);
int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y);
Point targetPosition{newX, newY};
GH.input().setCursorPosition(targetPosition);
if(CCS && CCS->curh)
CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
CCS->curh->cursorMove(GH.getCursorPosition().x * scalingFactor, GH.getCursorPosition().y * scalingFactor);
}
int InputSourceGameController::getMoveDis(float planDis)

View File

@ -27,7 +27,7 @@
#include "../widgets/ObjectLists.h"
#include "../../lib/modding/CModHandler.h"
#include "../../lib/modding/CModInfo.h"
#include "../../lib/modding/ModDescription.h"
#include "../../lib/texts/CGeneralTextHandler.h"
#include "../../lib/texts/MetaString.h"
@ -128,14 +128,14 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
GlobalLobbyRoomModInfo modInfo;
modInfo.status = modEntry.second;
if (modEntry.second == ModVerificationStatus::EXCESSIVE)
modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString();
modInfo.version = CGI->modh->getModInfo(modEntry.first).getVersion().toString();
else
modInfo.version = roomDescription.modList.at(modEntry.first).version.toString();
if (modEntry.second == ModVerificationStatus::NOT_INSTALLED)
modInfo.modName = roomDescription.modList.at(modEntry.first).name;
else
modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name;
modInfo.modName = CGI->modh->getModInfo(modEntry.first).getName();
modVerificationList.push_back(modInfo);
}

View File

@ -263,7 +263,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now?
}
si8 playerLimit = opts->getMaxPlayersCount();
si8 playerLimit = opts->getPlayerLimit();
si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount();
si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount();

View File

@ -362,17 +362,6 @@ void CMainMenu::update()
menu->switchToTab(menu->getActiveTab());
}
static bool warnedAboutModDependencies = false;
if (!warnedAboutModDependencies)
{
warnedAboutModDependencies = true;
auto errorMessages = CGI->modh->getModLoadErrors();
if (!errorMessages.empty())
CInfoWindow::showInfoDialog(errorMessages, std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
}
// Handles mouse and key input
GH.handleEvents();
GH.windows().simpleRedraw();

View File

@ -548,7 +548,10 @@ size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates)
return iconIndex;
}
if (MapRendererBaseContext::isVisible(coordinates))
return MapRendererWorldViewContext::overlayImageIndex(coordinates);
else
return std::numeric_limits<size_t>::max();
}
MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState)

View File

@ -118,6 +118,12 @@ size_t CTrueTypeFont::getStringWidthScaled(const std::string & text) const
{
int width;
TTF_SizeUTF8(font.get(), text.c_str(), &width, nullptr);
if (outline)
width += getScalingFactor();
if (dropShadow || outline)
width += getScalingFactor();
return width;
}

View File

@ -49,6 +49,7 @@ void ButtonBase::update()
// hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted
if (state == EButtonState::HIGHLIGHTED && image->size() < 4)
image->setFrame(image->size()-1);
else
image->setFrame(stateToIndex[vstd::to_underlying(state)]);
}

View File

@ -140,6 +140,8 @@ void CArtifactsOfHeroBase::gestureArtPlace(CComponentHolder & artPlace, const Po
void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
{
curHero = hero;
if (!hero)
return;
for(auto slot : artWorn)
{

View File

@ -28,6 +28,7 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
{
if(curHero)
CArtifactsOfHeroBase::putBackPickedArtifact();
}

View File

@ -279,7 +279,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other");
int maxLen = 50;
EFonts f = FONT_TINY;
Point pText = p + Point(3, 40);
Point pText = p + Point(4, 38);
// 1px Black border
bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));

View File

@ -312,6 +312,7 @@ void CHeroWindow::dismissCurrent()
arts->putBackPickedArtifact();
close();
LOCPLINT->cb->dismissHero(curHero);
arts->setHero(nullptr);
}, nullptr);
}

View File

@ -117,7 +117,13 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
color = "";
}
else
printableString.append(text.data() + currPos, symbolSize);
{
std::string character = "";
character.append(text.data() + currPos, symbolSize);
if(fontPtr->getStringWidth(printableString + character) > maxLineWidth)
break;
printableString += character;
}
currPos += symbolSize;
}

View File

@ -1508,7 +1508,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled);
list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements)
list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
}
CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled)
@ -1528,7 +1528,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled);
list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements)
list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
}
void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled)
@ -1653,7 +1653,7 @@ void CObjectListWindow::keyPressed(EShortcut key)
}
vstd::abetween<int>(sel, 0, itemsVisible.size()-1);
list->scrollTo(sel - 4); // -4 is for centering (list have 9 elements)
list->scrollTo(sel);
changeSelection(sel);
}

View File

@ -1,4 +1,56 @@
{
"pawn" : {
"maxRoamingHeroes" : 8,
"maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10,
"scoutHeroTurnDistanceLimit" : 5,
"maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true,
"openMap": false,
"allowObjectGraph": false,
"pathfinderBucketsCount" : 1, // old value: 3,
"pathfinderBucketSize" : 32, // old value: 7,
"retreatThresholdRelative" : 0.3,
"retreatThresholdAbsolute" : 10000,
"safeAttackRatio" : 1.1,
"useFuzzy" : false
},
"knight" : {
"maxRoamingHeroes" : 8,
"maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10,
"scoutHeroTurnDistanceLimit" : 5,
"maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true,
"openMap": false,
"allowObjectGraph": false,
"pathfinderBucketsCount" : 1, // old value: 3,
"pathfinderBucketSize" : 32, // old value: 7,
"retreatThresholdRelative" : 0.3,
"retreatThresholdAbsolute" : 10000,
"safeAttackRatio" : 1.1,
"useFuzzy" : false
},
"rook" : {
"maxRoamingHeroes" : 8,
"maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10,
"scoutHeroTurnDistanceLimit" : 5,
"maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true,
"openMap": false,
"allowObjectGraph": false,
"pathfinderBucketsCount" : 1, // old value: 3,
"pathfinderBucketSize" : 32, // old value: 7,
"retreatThresholdRelative" : 0.3,
"retreatThresholdAbsolute" : 10000,
"safeAttackRatio" : 1.1,
"useFuzzy" : false
},
"queen" : {
"maxRoamingHeroes" : 8,
"maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10,
@ -6,5 +58,29 @@
"maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true,
"openMap": true,
"allowObjectGraph": true
"allowObjectGraph": false,
"pathfinderBucketsCount" : 1, // old value: 3,
"pathfinderBucketSize" : 32, // old value: 7,
"retreatThresholdRelative" : 0.3,
"retreatThresholdAbsolute" : 10000,
"safeAttackRatio" : 1.1,
"useFuzzy" : false
},
"king" : {
"maxRoamingHeroes" : 8,
"maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10,
"scoutHeroTurnDistanceLimit" : 5,
"maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true,
"openMap": true,
"allowObjectGraph": false,
"pathfinderBucketsCount" : 1, // old value: 3,
"pathfinderBucketSize" : 32, // old value: 7,
"retreatThresholdRelative" : 0.3,
"retreatThresholdAbsolute" : 10000,
"safeAttackRatio" : 1.1,
"useFuzzy" : false
}
}

View File

@ -489,6 +489,20 @@
"originalFlyRules" : true
},
"resources" : {
// H3 mechanics - AI receives bonus (or malus, on easy) to his resource income
// AI will receive specified values as percentage of his weekly income
// So, "gems" : 200 will give AI player 200% of his daily income of gems over week, or, in other words,
// giving AI player 2 additional gems per week for every owned Gem Pond
"weeklyBonusesAI" : {
"pawn" : { "gold" : -175 },
"knight": {},
"rook" : {},
"queen" : { "wood" : 275 , "mercury" : 100, "ore" : 275, "sulfur" : 100, "crystal" : 100, "gems" : 100, "gold" : 175},
"king" : { "wood" : 375 , "mercury" : 200, "ore" : 375, "sulfur" : 200, "crystal" : 200, "gems" : 200, "gold" : 350}
}
},
"spells":
{
// if enabled, dimension work doesn't work into tiles under Fog of War

View File

@ -106,7 +106,7 @@
"defaultTavern" : 5,
"affinity" : "might",
"commander" : "medusaQueen",
"mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } },
"mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } },
"animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
},
"warlock" :
@ -116,7 +116,7 @@
"defaultTavern" : 5,
"affinity" : "magic",
"commander" : "medusaQueen",
"mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } },
"mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } },
"animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
},
"barbarian" :

View File

@ -133,6 +133,14 @@
"originalFlyRules" : { "type" : "boolean" }
}
},
"resources": {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"weeklyBonusesAI" : { "type" : "object" }
}
},
"spells": {
"type" : "object",
"additionalProperties" : false,

View File

@ -620,7 +620,6 @@
"defaultRepositoryURL",
"extraRepositoryURL",
"extraRepositoryEnabled",
"enableInstalledMods",
"autoCheckRepositories",
"ignoreSslErrors",
"updateOnStartup",
@ -647,10 +646,6 @@
"type" : "boolean",
"default" : false
},
"enableInstalledMods" : {
"type" : "boolean",
"default" : true
},
"ignoreSslErrors" : {
"type" : "boolean",
"default" : false

View File

@ -12,7 +12,7 @@
"properties" : {
"type" : {
"type" : "string",
"enum" : ["playerStart", "cpuStart", "treasure", "junction"]
"enum" : ["playerStart", "cpuStart", "treasure", "junction", "sealed"]
},
"size" : { "type" : "number", "minimum" : 1 },
"owner" : {},

View File

@ -1,11 +1,11 @@
# VCMI Project
[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.7/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.7)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
# VCMI Project
VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
<p>
@ -15,14 +15,13 @@ VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving
<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Quick%20Hero%20Select%20Bastion.jpg?raw=true" alt="New widget for Hero selection, featuring Pavillon Town" style="height:120px;"/>
</p>
## Links
* Homepage: https://vcmi.eu/
* Forums: https://forum.vcmi.eu/
* Bugtracker: https://github.com/vcmi/vcmi/issues
* Discord: https://discord.gg/chBT42V
* GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant
* Homepage: <https://vcmi.eu/>
* Forums: <https://forum.vcmi.eu/>
* Bugtracker: <https://github.com/vcmi/vcmi/issues>
* Discord: <https://discord.gg/chBT42V>
* GPT Store: <https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant>
## Latest release
@ -31,6 +30,7 @@ Loading saves made with different major version of VCMI is usually **not** suppo
Please see corresponding installation guide articles for details for your platform.
## Installation guides
- [Windows](players/Installation_Windows.md)
- [macOS](players/Installation_macOS.md)
- [Linux](players/Installation_Linux.md)
@ -70,6 +70,7 @@ See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md
## Documentation and guidelines for developers
Development environment setup instructions:
- [Building VCMI for Android](developers/Building_Android.md)
- [Building VCMI for iOS](developers/Building_iOS.md)
- [Building VCMI for Linux](developers/Building_Linux.md)
@ -78,6 +79,7 @@ Development environment setup instructions:
- [Conan](developers/Conan.md)
Engine documentation: (NOTE: may be outdated)
- [Development with Qt Creator](developers/Development_with_Qt_Creator.md)
- [Coding Guidelines](developers/Coding_Guidelines.md)
- [Bonus System](developers/Bonus_System.md)
@ -95,6 +97,6 @@ Engine documentation: (NOTE: may be outdated)
## Copyright and license
VCMI Project source code is licensed under GPL version 2 or later.
VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets
VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: <https://github.com/vcmi/vcmi-assets>
Copyright (C) 2007-2024 VCMI Team (check AUTHORS file for the contributors list)

View File

@ -6,16 +6,17 @@ There are two types of AI: adventure and battle.
**Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield
We have 3 battle AIs so far:
* BattleAI - strongest
* StupidAI - for neutrals, should be simple so that experienced players can abuse it
* Empty AI - should do nothing at all. If needed another battle AI can be introduced.
Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition.
Each side in a battle is represented by an CArmedInstance object. CHeroInstance and CGDwelling, CGMonster and more are subclasses of CArmedInstance. CArmedInstance contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface battle::Unit *.
Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is `activeStack(battle::Unit * stack)`. It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as `cb`. `CPlayerSpecificCallback` it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition.
Each side in a battle is represented by an `CArmedInstance` object. `CHeroInstance` and `CGDwelling`, `CGMonster` and more are subclasses of `CArmedInstance`. `CArmedInstance` contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface `battle::Unit *`.
Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retaliation, shooter or not, initial count of shots and so on.
When you call unit->getAttack() it summarizes all these bonuses and returns the resulting value.
When you call `unit->getAttack()` it summarizes all these bonuses and returns the resulting value.
One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change.
One important class is `HypotheticBattle`. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around `CPlayerSpecificCallback` or another `HypotheticBattle` so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (`CStackWithBonuses`). So if you need to emulate an attack you can call `hypotheticbattle.getforupdate()` and it will return the `CStackWithBonuses` which you can safely change.
## BattleAI
@ -38,17 +39,20 @@ BattleAI itself handles all the rest and issues actual commands
Adventure AI responsible for moving heroes on map, gathering things, developing town. Main idea is to gather all possible tasks on map, prioritize them and select the best one for each heroes. Initially was a fork of VCAI
### Parts
Gateway - a callback for server used to invoke AI actions when server thinks it is time to do something. Through this callback AI is informed about various events like hero level up, tile revialed, blocking dialogs and so on. In order to do this Gaateway implements specific interface. The interface is exactly the same for human and AI
Another important actor for server interaction is CCallback * cb. This one is used to retrieve gamestate information and ask server to do things like hero moving, spell casting and so on. Each AI has own instance of Gateway and it is a root object which holds all AI state. Gateway has an event method yourTurn which invokes makeTurn in another thread. The last passes control to Nullkiller engine.
Nullkiller engine - place where actual AI logic is organized. It contains a main loop for gathering and prioritizing things. Its algorithm:
* reset AI state, it avoids keeping some memory about the game in general to reduce amount of things serialized into savefile state. The only serialized things are in nullkiller->memory. This helps reducing save incompatibility. It should be mostly enough for AI to analyze data avaialble in CCallback
* main loop, loop iteration is called a pass
** update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work
** gathering goals, prioritizing and decomposing them
** execute selected best goals
* update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work
* gathering goals, prioritizing and decomposing them
* execute selected best goals
Analyzer - a module gathering data from CCallback *. Its goal to make some statistics and avoid making any significant decissions.
* HeroAnalyser - decides upong which hero suits better to be main (army carrier and fighter) and which is better to be a scout (gathering unguarded resources, exploring)
* BuildAnalyzer - prepares information on what we can build in our towns, and what resources we need to do this
* DangerHitMapAnalyser - checks if enemy hero can rich each tile, how fast and what is their army strangth
@ -61,9 +65,11 @@ Analyzer - a module gathering data from CCallback *. Its goal to make some stati
* PriorityEvaluator - gathers information on task rewards, evaluates their priority using Fuzzy Light library (fuzzy logic)
### Goals
Units of activity in AI. Can be AbstractGoal, Task, Marker and Behavior
Task - simple thing which can be done right away in order to gain some reward. Or a composition of simple things in case if more than one action is needed to gain the reward.
* AdventureSpellCast - town portal, water walk, air walk, summon boat
* BuildBoat - builds a boat in a specific shipyard
* BuildThis - builds a building in a specified town
@ -78,6 +84,7 @@ Task - simple thing which can be done right away in order to gain some reward. O
* StayAtTown - stay at town for the rest of the day (to regain mana)
Behavior - a core game activity
* CaptureObjectsBehavior - generally it is about visiting map objects which give reward. It can capture any object, even those which are behind monsters and so on. But due to performance considerations it is not allowed to handle monsters and quests now.
* ClusterBehavior - uses information of ObjectClusterizer to unblock objects hidden behind various blockers. It kills guards, completes quests, captures garrisons.
* BuildingBehavior - develops our towns
@ -89,6 +96,7 @@ Behavior - a core game activity
* DefenceBehavior - defend towns by eliminating treatening heroes or hiding in town garrison
AbstractGoal - some goals can not be completed because it is not clear how to do this. They express desire to do something, not exact plan. DeepDecomposer is used to refine such goals until they are turned into such plan or discarded. Some examples:
* CaptureObject - you need to visit some object (flag a shipyard for instance) but do not know how
* CompleteQuest - you need to bypass bordergate or borderguard or questguard but do not know how
AbstractGoal usually comes in form of composition with some elementar task blocked by abstract objective. For instance CaptureObject(Shipyard), ExecuteHeroChain(visit x, build boat, visit enemy town). When such composition is decomposed it can turn into either a pair of herochains or into another abstract composition if path to shipyard is also blocked with something.

View File

@ -26,6 +26,7 @@ There are two basic types of operations that can be performed on the graph:
### Adding a new node
When node is attached to a new black parent (the only possibility - adding parent is the same as adding a child to it), the propagation system is triggered and works as follows:
- For the attached node and its all red ancestors
- For every bonus
- Call propagator giving the new descendant - then attach appropriately bonuses to the red descendant of attached node (or the node itself).
@ -54,7 +55,7 @@ Updaters are objects attached to bonuses. They can modify a bonus (typically by
The following example shows an artifact providing a bonus based on the level of the hero that wears it:
```javascript
```json5
"core:greaterGnollsFlail":
{
"text" : { "description" : "This mighty flail increases the attack of all gnolls under the hero's command by twice the hero's level." },

View File

@ -1,26 +1,26 @@
# Building Android
The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is https://github.com/vcmi/vcmi-android/blob/master/building.txt (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole.
The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is <https://github.com/vcmi/vcmi-android/blob/master/building.txt> (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole.
*Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box.
## Requirements
1. CMake 3.20+: download from your package manager or from https://cmake.org/download/
1. CMake 3.20+: download from your package manager or from <https://cmake.org/download/>
2. JDK 11, not necessarily from Oracle
3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/
3. Android command line tools or Android Studio for your OS: <https://developer.android.com/studio/>
4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it:
- install with Android Studio
- install with `sdkmanager` command line tool
- download from https://developer.android.com/ndk/downloads
- download from <https://developer.android.com/ndk/downloads>
- download with Conan, see [#NDK and Conan](#ndk-and-conan)
5. Optional:
- Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases
- Ccache: download from your package manager or from https://github.com/ccache/ccache/releases
- Ninja: download from your package manager or from <https://github.com/ninja-build/ninja/releases>
- Ccache: download from your package manager or from <https://github.com/ccache/ccache/releases>
## Obtaining source code
Clone https://github.com/vcmi/vcmi with submodules. Example for command line:
Clone <https://github.com/vcmi/vcmi> with submodules. Example for command line:
```
git clone --recurse-submodules https://github.com/vcmi/vcmi.git
@ -31,6 +31,7 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git
We use Conan package manager to build/consume dependencies, find detailed usage instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Сonan.md).
On the step where you need to replace **PROFILE**, choose:
- `android-32` to build for 32-bit architecture (armeabi-v7a)
- `android-64` to build for 64-bit architecture (aarch64-v8a)
@ -38,7 +39,7 @@ On the step where you need to replace **PROFILE**, choose:
Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html):
- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose _android-**X**-ndk_ where _**X**_ is either `32` or `64`.
- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose *android-**X**-ndk* where ***X*** is either `32` or `64`.
- to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will work only when consuming the pre-built binaries)
```

View File

@ -41,7 +41,7 @@ NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedo
On Arch-based distributions, there is a development package available for VCMI on the AUR.
It can be found at https://aur.archlinux.org/packages/vcmi-git/
It can be found at <https://aur.archlinux.org/packages/vcmi-git/>
Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki.
@ -109,9 +109,9 @@ This will generate `vcmiclient`, `vcmiserver`, `vcmilauncher` as well as .so lib
### RPM package
The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview
The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: <http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview>
0. Enable RPMFusion free repo to access to ffmpeg libs:
1. Enable RPMFusion free repo to access to ffmpeg libs:
```sh
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
@ -120,30 +120,31 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
> [!NOTE]
> The stock ffmpeg from Fedora repo is no good as it lacks a lots of codecs
1. Perform a git clone from a tagged branch for the right Fedora version from https://github.com/rpmfusion/vcmi; for example for Fedora 38: <pre>git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git</pre>
2. Perform a git clone from a tagged branch for the right Fedora version from <https://github.com/rpmfusion/vcmi>; for example for Fedora 38: <pre>git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git</pre>
2. Copy all files to ~/rpmbuild/SPECS with command: <pre>cp vcmi/* ~/rpmbuild/SPECS</pre>
3. Copy all files to ~/rpmbuild/SPECS with command: <pre>cp vcmi/* ~/rpmbuild/SPECS</pre>
3. Fetch all sources by using spectool:
4. Fetch all sources by using spectool:
```sh
sudo dnf install rpmdevtools
spectool -g -R ~/rpmbuild/SPECS/vcmi.spec
```
4. Fetch all dependencies required to build the RPM:
5. Fetch all dependencies required to build the RPM:
```sh
sudo dnf install dnf-plugins-core
sudo dnf builddep ~/rpmbuild/SPECS/vcmi.spec
```
4. Go to ~/rpmbuild/SPECS and open terminal in this folder and type:
6. Go to ~/rpmbuild/SPECS and open terminal in this folder and type:
```sh
rpmbuild -ba ~/rpmbuild/SPECS/vcmi.spec
```
5. Generated RPM is in folder ~/rpmbuild/RPMS
7. Generated RPM is in folder ~/rpmbuild/RPMS
If you want to package the generated RPM above for different processor architectures and operating systems you can use the tool mock.
Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type:

View File

@ -24,9 +24,11 @@ Warning! Replace `%VCMI_DIR%` with path you've chosen for VCMI installation in t
It is recommended to avoid non-ascii characters in the path to your working folders. The folder should not be write-protected by system.
Good locations:
- `C:\VCMI`
Bad locations:
- `C:\Users\Michał\VCMI (non-ascii character)`
- `C:\Program Files (x86)\VCMI (write protection)`
@ -38,13 +40,14 @@ You have two options: to use pre-built libraries or build your own. We strongly
#### Download and unpack archive
Vcpkg Archives are available at our GitHub: https://github.com/vcmi/vcmi-deps-windows/releases
Vcpkg Archives are available at our GitHub: <https://github.com/vcmi/vcmi-deps-windows/releases>
- Download latest version available.
EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z)
- Extract archive by right clicking on it and choosing "7-zip -> Extract Here".
#### Move dependencies to target directory
Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside.
Move extracted `vcpkg` directory into your `%VCMI_DIR%`
@ -65,7 +68,9 @@ Be aware that building Vcpkg might take a lot of time depend on your CPU model a
From command line use:
```sh
git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
```
#### Build vcpkg and dependencies
@ -85,6 +90,7 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
## Build VCMI
#### From GIT GUI
- Open SourceTree
- File -> Clone
- select `https://github.com/vcmi/vcmi/` as source
@ -94,15 +100,18 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
- click Clone
#### From command line
- `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source`
### Generate solution for VCMI
- Create `%VCMI_DIR%/build` folder
- Open a command line prompt at `%VCMI_DIR%/build`
- Execute `cd %VCMI_DIR%/build`
- Create solution (Visual Studio 2022 64-bit) `cmake %VCMI_DIR%/source -DCMAKE_TOOLCHAIN_FILE=%VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake -G "Visual Studio 17 2022" -A x64`
### Compile VCMI with Visual Studio
- Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio
- Select `Release` build type in the combobox
- If you want to use ccache:
@ -113,7 +122,8 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
- VCMI will be built in `%VCMI_DIR%/build/bin` folder!
### Compile VCMI with MinGW via MSYS2
- Install MSYS2 from https://www.msys2.org/
- Install MSYS2 from <https://www.msys2.org/>
- Start the `MSYS MinGW x64`-shell
- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static mingw-w64-x86_64-qt5-tools mingw-w64-x86_64-tbb`
- Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release`
@ -134,8 +144,10 @@ Vcpkg might be very unstable due to limited popularity and fact of using bleedin
Pre-built version we provide is always manually tested with all supported versions of MSVC for both Release and Debug builds and all known quirks are listed below.
#$# Build is successful but can not start new game
### Build is successful but can not start new game
Make sure you have:
* Installed Heroes III from disk or using GOG installer
* Copied `Data`, `Maps` and `Mp3` folders from Heroes III to: `%USERPROFILE%\Documents\My Games\vcmi\`

View File

@ -91,7 +91,7 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h
## Packaging project into DMG file
After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C `<configuration name> with the same configuration that you used to build the project.
After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C <configuration name>` with the same configuration that you used to build the project.
If you use Conan, it's expected that you use **conan-generated** directory at step 4 of [Conan package manager](Conan.md).

View File

@ -3,9 +3,7 @@
* `-D CMAKE_BUILD_TYPE=Debug`
* Enables debug info and disables optimizations
* `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON`
* Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server.
For clangd to find the JSON, create a file named `.clangd` with this content
* Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. For clangd to find the JSON, create a file named `.clangd` with this content
```
CompileFlags:
CompilationDatabase: build

View File

@ -29,6 +29,7 @@ Most of VCMI configuration files uses Json format and located in "config" direct
### Main purposes of client
Client is responsible for:
- displaying state of game to human player
- capturing player's actions and sending requests to server
- displaying changes in state of game indicated by server
@ -94,7 +95,6 @@ Forward declarations of the lib in headers of other parts of the project need to
`<other forward declarations>`
`<classes>`
##### New project part
If you're creating new project part, place `VCMI_LIB_USING_NAMESPACE` in its `StdInc.h` to be able to use lib classes without explicit namespace in implementation files. Example: <https://github.com/vcmi/vcmi/blob/develop/launcher/StdInc.h>

View File

@ -4,7 +4,7 @@
VCMI implementation bases on C++17 standard. Any feature is acceptable as long as it's will pass build on our CI, but there is list below on what is already being used.
Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at http://en.cppreference.com/w/cpp/compiler_support
Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at <http://en.cppreference.com/w/cpp/compiler_support>
## Style Guidelines
@ -749,7 +749,7 @@ If you need a more detailed description for a method you can use such style:
/// @return Description of the return value
```
A good essay about writing comments: http://ardalis.com/when-to-comment-your-code
A good essay about writing comments: <http://ardalis.com/when-to-comment-your-code>
### Casing

View File

@ -27,7 +27,7 @@ The following platforms are supported and known to work, others might require ch
- **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04)
- **Android**: libraries are built with NDK r25c (25.2.9519653)
2. Download the binaries archive and unpack it to `~/.conan` directory from https://github.com/vcmi/vcmi-dependencies/releases/latest
2. Download the binaries archive and unpack it to `~/.conan` directory from <https://github.com/vcmi/vcmi-dependencies/releases/latest>
- macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz**
- iOS: pick ***dependencies-ios.txz***
@ -65,7 +65,7 @@ If you use `--build=never` and this command fails, then it means that you can't
VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this).
_Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`.
*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`.
### Using our prebuilt binaries for macOS/iOS
@ -86,7 +86,7 @@ This subsection describes platform specifics to build libraries from source prop
#### Building for macOS/iOS
- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your _host_ profile or pass `-s compiler.cppstd=11` on the command line.
- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your *host* profile or pass `-s compiler.cppstd=11` on the command line.
- If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards.
#### Building for Android
@ -105,11 +105,11 @@ After applying patch(es):
2. Run `make`
3. Copy file `qtbase/jar/QtAndroid.jar` from the build directory to the **package directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH/jar`.
_Note_: if you plan to build Qt from source again, then you don't need to perform the above _After applying patch(es)_ steps after building.
*Note*: if you plan to build Qt from source again, then you don't need to perform the above *After applying patch(es)* steps after building.
##### Using recipes for system libraries
1. Clone/download https://github.com/kambala-decapitator/conan-system-libs
1. Clone/download <https://github.com/kambala-decapitator/conan-system-libs>
2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android. Do it for each library you need.
3. Now you can execute `conan install` to build all dependencies.
@ -172,7 +172,7 @@ cmake --preset ios-conan
`CMakeUserPresets.json` file:
```json
```json5
{
"version": 3,
"cmakeMinimumRequired": {

View File

@ -6,7 +6,7 @@ Qt Creator is the recommended IDE for VCMI development on Linux distributions, b
- Almost no manual configuration when used with CMake. Project configuration is read from CMake text files,
- Easy to setup and use with multiple different compiler toolchains: GCC, Visual Studio, Clang
You can install Qt Creator from repository, but better to stick to latest version from Qt website: https://www.qt.io/download-qt-installer-oss
You can install Qt Creator from repository, but better to stick to latest version from Qt website: <https://www.qt.io/download-qt-installer-oss>
## Configuration

View File

@ -24,7 +24,7 @@ Some notes:
### Setup settings.json
``` javascript
```json5
{
"logging" : {
"console" : {
@ -68,7 +68,7 @@ The following code shows how the logging system can be configured:
If `configureDefault` or `configure` won't be called, then logs aren't written either to the console or to the file. The default logging setups a system like this:
**Console**
#### Console
Format: %m
Threshold: info
@ -76,17 +76,18 @@ coloredOutputEnabled: true
colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red
**File**
#### File
Format: %d %l %n \[%t\] - %m
**Loggers**
#### Loggers
global -\> info
### How to get a logger
There exist only one logger object per domain. A logger object cannot be copied. You can get access to a logger object by using the globally defined ones like `logGlobal` or `logAi`, etc... or by getting one manually:
```cpp
Logger * logger = CLogger::getLogger(CLoggerDomain("rmg"));
```

View File

@ -2,7 +2,7 @@
## Configuration
``` javascript
```json5
{
//general purpose script, Lua or ERM, runs on server
"myScript":

View File

@ -5,12 +5,14 @@
For implementation details see files located at `lib/network` directory.
VCMI uses connection using TCP to communicate with server, even in single-player games. However, even though TCP is stream-based protocol, VCMI uses atomic messages for communication. Each message is a serialized stream of bytes, preceded by 4-byte message size:
```
int32_t messageSize;
byte messagePayload[messageSize];
```
Networking can be used by:
- game client (vcmiclient / VCMI_Client.exe). Actual application that player interacts with directly using UI.
- match server (vcmiserver / VCMI_Server.exe / part of game client). This app controls game logic and coordinates multiplayer games.
- lobby server (vcmilobby). This app provides access to global lobby through which players can play game over Internet.
@ -28,11 +30,13 @@ For gameplay, VCMI serializes data into a binary stream. See [Serialization](Ser
## Global lobby communication
For implementation details see:
- game client: `client/globalLobby/GlobalLobbyClient.h
- match server: `server/GlobalLobbyProcessor.h
- lobby server: `client/globalLobby/GlobalLobbyClient.h
In case of global lobby, message payload uses plaintext json format - utf-8 encoded string:
```
int32_t messageSize;
char jsonString[messageSize];
@ -43,6 +47,7 @@ Every message must be a struct (json object) that contains "type" field. Unlike
### Communication flow
Notes:
- invalid message, such as corrupted json format or failure to validate message will result in no reply from server
- in addition to specified messages, match server will send `operationFailed` message on failure to apply player request
@ -52,6 +57,7 @@ Notes:
- lobby -> client: `accountCreated`
#### Login
- client -> lobby: `clientLogin`
- lobby -> client: `loginSuccess`
- lobby -> client: `chatHistory`
@ -59,10 +65,12 @@ Notes:
- lobby -> client: `activeGameRooms`
#### Chat Message
- client -> lobby: `sendChatMessage`
- lobby -> every client: `chatMessage`
#### New Game Room
- client starts match server instance
- match -> lobby: `serverLogin`
- lobby -> match: `loginSuccess`
@ -73,19 +81,23 @@ Notes:
- lobby -> every client: `activeGameRooms`
#### Joining a game room
See [#Proxy mode](proxy-mode)
#### Leaving a game room
- client closes connection to match server
- match -> lobby: `leaveGameRoom`
#### Sending an invite:
#### Sending an invite
- client -> lobby: `sendInvite`
- lobby -> target client: `inviteReceived`
Note: there is no dedicated procedure to accept an invite. Instead, invited player will use same flow as when joining public game room
#### Logout
- client closes connection
- lobby -> every client: `activeAccounts`
@ -94,6 +106,7 @@ Note: there is no dedicated procedure to accept an invite. Instead, invited play
In order to connect players located behind NAT, VCMI lobby can operate in "proxy" mode. In this mode, connection will be act as proxy and will transmit gameplay data from client to a match server, without any data processing on lobby server.
Currently, process to establish connection using proxy mode is:
- Player attempt to join open game room using `joinGameRoom` message
- Lobby server validates requests and on success - notifies match server about new player in lobby using control connection
- Match server receives request, establishes new connection to game lobby, sends `serverProxyLogin` message to lobby server and immediately transfers this connection to VCMIServer class to use as connection for gameplay communication

View File

@ -94,13 +94,11 @@ Reserve accounts for other code hosting services:
2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret.
3. Centralized way to post news about game updates to all social media.
# Project Servers Configuration
## Project Servers Configuration
This section dedicated to explain specific configurations of our servers for anyone who might need to improve it in future.
## Droplet configuration
### Droplet and hosted services
### Droplet configuration
Currently we using two droplets:

View File

@ -1,12 +1,16 @@
# Release Process
## Versioning
For releases VCMI uses version numbering in form "1.X.Y", where:
- 'X' indicates major release. Different major versions are generally not compatible with each other. Save format is different, network protocol is different, mod format likely different.
- 'Y' indicates hotfix release. Despite its name this is usually not urgent, but planned release. Different hotfixes for same major version are fully compatible with each other.
## Branches
Our branching strategy is very similar to GitFlow:
- `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `1.X.Y` when corresponding version is released. State of master branch represents state of latest public release.
- `beta` branch is for stabilization of ongoing release. Beta branch is created when new major release enters stabilization stage and is used for both major release itself as well as for subsequent hotfixes. Only changes that are safe, have minimal chance of regressions and improve player experience should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta.
- `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta`.
@ -14,12 +18,14 @@ Our branching strategy is very similar to GitFlow:
## Release process step-by-step
### Initial release setup (major releases only)
Should be done immediately after start of stabilization stage for previous release
- Create project named `Release 1.X`
- Add all features and bugs that should be fixed as part of this release into this project
### Start of stabilization stage (major releases only)
Should be done 2 weeks before planned release date. All major features should be finished at this point.
- Create `beta` branch from `develop`
@ -34,6 +40,7 @@ Should be done 2 weeks before planned release date. All major features should be
- Bump version and build ID for Android on `beta` branch
### Release preparation stage
Should be done 1 week before release. Release date should be decided at this point.
- Make sure to announce codebase freeze deadline (1 day before release) to all developers
@ -45,21 +52,23 @@ Should be done 1 week before release. Release date should be decided at this poi
- - Update downloads counter in `docs/readme.md`
### Release preparation stage
Should be done 1 day before release. At this point beta branch is in full freeze.
- Merge release preparation PR into `beta`
- Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages
- Create draft release page, specify `1.x.y` as tag for `master` after publishing
- Check that artifacts for all platforms have been built by CI on `master` branch
- Download and rename all build artifacts to use form "VCMI-1.X.Y-Platform.xxx"
- Download and rename all build artifacts to use form `VCMI-1.X.Y-Platform.xxx`
- Attach build artifacts for all platforms to release page
- Manually extract Windows installer, remove `$PLUGINSDIR` directory which contains installer files and repackage data as .zip archive
- Attach produced zip archive to release page as an alternative Windows installer
- Upload built AAB to Google Play and send created release draft for review (usually takes several hours)
- Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates)
- (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu
- (major releases only) Prepare pull request with release update for web site <https://github.com/vcmi/VCMI.eu>
### Release publishing phase
Should be done on release date
- Trigger builds for new release on Ubuntu PPA

View File

@ -1,6 +1,7 @@
# Ubuntu PPA
## Main links
- [Team](https://launchpad.net/~vcmi)
- [Project](https://launchpad.net/vcmi)
- [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi)
@ -14,22 +15,31 @@
## Automatic daily builds process
### Code import
- Launchpad performs regular (once per few hours) clone of our git repository.
- This process can be observed on [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) page.
- If necessary, it is possible to trigger fresh clone immediately (Import Now button)
### Build dependencies
- All packages required for building of vcmi are defined in [debian/control](https://github.com/vcmi/vcmi/blob/develop/debian/control) file
- Launchpad will automatically install build dependencies during build
- Dependencies of output .deb package are defined implicitly as dependencies of packages required for build
### Recipe building
- Every 24 hours Launchpad triggers daily builds on all recipes that have build schedule enable. For vcmi this is [Daily recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-daily)
- Alternatively, builds can be triggered manually using "request build(s) link on recipe page. VCMI uses this for [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable)
### Recipe content (build settings)
- Version of resulting .deb package is set in recipe content, e.g `{debupstream}+git{revtime}` for daily builds
- Base version (referred as `debupstream` on Launchpad is taken from source code, [debian/changelog](https://github.com/vcmi/vcmi/blob/develop/debian/changelog) file
- CMake configuration settings are taken from source code, [debian/rules](https://github.com/vcmi/vcmi/blob/develop/debian/rules) file
- Branch which is used for build is specified in recipe content, e.g. `lp:vcmi master`
## Workflow for creating a release build
- if necessary, push all required changes including `debian/changelog` update to `vcmi/master` branch
- Go to [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) and run repository import.
- Wait for import to finish, which usually happens within a minute. Press F5 to actually see changes.
@ -37,8 +47,10 @@
- Wait for builds to finish. This takes quite a while, usually - over a hour, even more for arm builds
- Once built, all successfully built packages are automatically copied to PPA linked to the recipe
- If any of builds have failed, open page with build info and check logs.
## People with access
- [alexvins](https://github.com/alexvins) (https://launchpad.net/~alexvins)
- [ArseniyShestakov](https://github.com/ArseniyShestakov) (https://launchpad.net/~sxx)
- [IvanSavenko](https://github.com/IvanSavenko) (https://launchpad.net/~saven-ivan)
- (Not member of VCMI, creator of PPA) (https://launchpad.net/~mantas)
- [alexvins](https://github.com/alexvins) (<https://launchpad.net/~alexvins>)
- [ArseniyShestakov](https://github.com/ArseniyShestakov) (<https://launchpad.net/~sxx>)
- [IvanSavenko](https://github.com/IvanSavenko) (<https://launchpad.net/~saven-ivan>)
- (Not member of VCMI, creator of PPA) (<https://launchpad.net/~mantas>)

View File

@ -8,7 +8,7 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def
## Format description
``` javascript
```json5
{
// Base path of all images in animation. Optional.
// Can be used to avoid using long path to images
@ -58,12 +58,14 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def
### Replacing a button
This json file will allow replacing .def file for a button with png images. Buttons require following images:
1. Active state. Button is active and can be pressed by player
2. Pressed state. Player pressed button but have not released it yet
3. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image
4. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off
```javascript
```json5
{
"basepath" : "interface/MyButton", // all images are located in this directory
"images" :
@ -80,7 +82,8 @@ This json file will allow replacing .def file for a button with png images. Butt
This json file allows defining one animation sequence, for example for adventure map objects or for town buildings.
```javascript
```json5
{
"basepath" : "myTown/myBuilding", // all images are located in this directory
"sequences" :

View File

@ -15,7 +15,7 @@ The limiters take no parameters:
Example:
``` javascript
```json5
"limiters" : [ "SHOOTER_ONLY" ]
```
@ -30,7 +30,7 @@ Parameters:
- (optional) bonus sourceType and sourceId in struct
- example: (from Adele's bless):
``` javascript
```json5
"limiters" : [
{
"type" : "HAS_ANOTHER_BONUS_LIMITER",
@ -64,6 +64,7 @@ Parameters:
If parameters is empty, level limiter works as CREATURES_ONLY limiter
Parameters:
- Minimal level
- Maximal level
@ -81,14 +82,14 @@ Parameters:
Example:
``` javascript
```json5
"limiters": [ {
"type":"CREATURE_TYPE_LIMITER",
"parameters": [ "angel", true ]
} ],
```
``` javascript
```json5
"limiters" : [ {
"type" : "CREATURE_TERRAIN_LIMITER",
"parameters" : ["sand"]
@ -112,7 +113,7 @@ and operate on the remaining limiters in that list:
Example:
``` javascript
```json5
"limiters" : [
"noneOf",
"IS_UNDEAD",

View File

@ -128,7 +128,7 @@ Allows to raise different creatures than Skeletons after battle.
- addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert)
- Example (from Cloak Of The Undead King):
```jsonc
```json5
{
"type" : "IMPROVED_NECROMANCY",
"subtype" : "creature.walkingDead",
@ -256,7 +256,7 @@ Gives creature under effect of this spell additional bonus, which is hardcoded a
Modifies 'val' parameter of spell effects that give bonuses by specified value. For example, Aenain makes Disrupting Ray decrease target's defense by additional 2 points:
```jsonc
```json5
"disruptingRay" : {
"addInfo" : -2,
"subtype" : "spell.disruptingRay",
@ -271,7 +271,7 @@ Modifies 'val' parameter of spell effects that give bonuses by specified value.
Changes 'val' parameter of spell effects that give bonuses to a specified value. For example, Fortune cast by Melody always modifies luck by +3:
```jsonc
```json5
"fortune" : {
"addInfo" : 3,
"subtype" : "spell.fortune",
@ -669,6 +669,7 @@ Affected unit can attack walls during siege battles (Cyclops)
### CATAPULT_EXTRA_SHOTS
Defines spell mastery level for spell used by CATAPULT bonus
- subtype: affected spell
- val: spell mastery level to use

View File

@ -12,25 +12,27 @@ Check the files in *config/heroes/* for additional usage examples.
- Type: Complex
- Parameters: valPer20, stepSize=1
- Effect: Updates val to
` ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
- Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
Example: The following updater will cause a bonus to grow by 6 for every
40 levels. At first level, rounding will cause the bonus to be 0.
` "updater" : {`
` "parameters" : [ 6, 2 ],`
` "type" : "GROWS_WITH_LEVEL"`
` }`
```json5
"updater" : {
"parameters" : [ 6, 2 ],
"type" : "GROWS_WITH_LEVEL"
}
```
Example: The following updater will cause a bonus to grow by 3 for every
20 levels. At first level, rounding will cause the bonus to be 1.
` "updater" : {`
` "parameters" : [ 3 ],`
` "type" : "GROWS_WITH_LEVEL"`
` }`
```json5
"updater" : {
"parameters" : [ 3 ],
"type" : "GROWS_WITH_LEVEL"
}
```
Remarks:
@ -42,13 +44,9 @@ Remarks:
## TIMES_HERO_LEVEL
- Type: Simple
- Effect: Updates val to
- Effect: Updates val to `val * heroLevel`
` val * heroLevel`
Usage:
` "updater" : "TIMES_HERO_LEVEL"`
Usage: `"updater" : "TIMES_HERO_LEVEL"`
Remark: This updater is redundant, in the sense that GROWS_WITH_LEVEL
can also express the desired scaling by setting valPer20 to 20\*val. It
@ -57,9 +55,7 @@ has been added for convenience.
## TIMES_STACK_LEVEL
- Type: Simple
- Effect: Updates val to
` val * stackLevel`
- Effect: Updates val to `val * stackLevel`
Usage:
@ -70,19 +66,17 @@ Remark: The stack level for war machines is 0.
## ARMY_MOVEMENT
- Type: Complex
- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier,
maxValue
- Effect: Updates val to val+= max((floor(basePerSpeed /
dividePerSpeed)\* additionalMultiplier), maxValue)
- Remark: this updater is designed for MOVEMENT bonus to match H3 army
movement rules (in the example - actual movement updater, which
produces values same as in default movement.txt).
- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, maxValue
- Effect: Updates val to `val+= max((floor(basePerSpeed / dividePerSpeed) * additionalMultiplier), maxValue)`
- Remark: this updater is designed for MOVEMENT bonus to match H3 army movement rules (in the example - actual movement updater, which produces values same as in default movement.txt).
- Example:
` "updater" : {`
` "parameters" : [ 20, 3, 10, 700 ],`
` "type" : "ARMY_MOVEMENT"`
` }`
```json5
"updater" : {
"parameters" : [ 20, 3, 10, 700 ],
"type" : "ARMY_MOVEMENT"
}
```
## BONUS_OWNER_UPDATER

View File

@ -3,13 +3,18 @@
Total value of Bonus is calculated using the following:
- For each bonus source type we calculate new source value (for all bonus value types except PERCENT_TO_SOURCE and PERCENT_TO_TARGET_TYPE) using the following:
` newVal = (val * (100 + PERCENT_TO_SOURCE) / 100))`
```
newVal = (val * (100 + PERCENT_TO_SOURCE) / 100))
```
- PERCENT_TO_TARGET_TYPE applies as PERCENT_TO_SOURCE to targetSourceType of bonus.
- All bonus value types summarized and then used as subject of the following formula:
` clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN)`
```
clamp(((BASE_NUMBER * (100 + PERCENT_TO_BASE) / 100) + ADDITIVE_VALUE) * (100 + PERCENT_TO_ALL) / 100), INDEPENDENT_MAX, INDEPENDENT_MIN)
```
Semantics of INDEPENDENT_MAX and INDEPENDENT_MIN are wrapped, and first means than bonus total value will be at least INDEPENDENT_MAX, and second means than bonus value will be at most INDEPENDENT_MIN.

View File

@ -4,7 +4,7 @@
All parameters but type are optional.
``` javascript
```json5
{
// Type of the bonus. See Bonus Types for full list
"type": "BONUS_TYPE",
@ -81,7 +81,7 @@ See [Game Identifiers](Game_Identifiers.md) for full list of available identifie
### Example
``` javascript
```json5
"bonus" :
{
"type" : "HATE",

View File

@ -22,7 +22,7 @@ Includes:
Function of all of these objects can be enabled by this:
``` javascript
```json5
"function" : "castleGates"
```
@ -58,31 +58,31 @@ CBuilding class.
#### unlock guild level
``` javascript
```json5
"guildLevels" : 1
```
#### unlock hero recruitment
``` javascript
```json5
"allowsHeroPurchase" : true
```
#### unlock ship purchase
``` javascript
```json5
"allowsShipPurchase" : true
```
#### unlock building purchase
``` javascript
```json5
"allowsBuildingPurchase" : true
```
#### unlocks creatures
``` javascript
```json5
"dwelling" : { "level" : 1, "creature" : "archer" }
```
@ -92,31 +92,31 @@ Turn into town bonus? What about creature-specific bonuses from hordes?
#### gives resources
``` javascript
```json5
"provides" : { "gold" : 500 }
```
#### gives guild spells
``` javascript
```json5
"guildSpells" : [5, 0, 0, 0, 0]
```
#### gives thieves guild
``` javascript
```json5
"thievesGuildLevels" : 1
```
#### gives fortifications
``` javascript
```json5
"fortificationLevels" : 1
```
#### gives war machine
``` javascript
```json5
"warMachine" : "ballista"
```
@ -134,7 +134,7 @@ Includes:
- bonus to scouting range
- bonus to player
``` javascript
```json5
"bonuses" :
{
"moraleToDefenders" :
@ -162,12 +162,12 @@ Possible issue - with removing of fixed ID's buildings in different town
may no longer share same ID. However Capitol must be unique across all
town. Should be fixed somehow.
``` javascript
```json5
"onePerPlayer" : true
```
#### chance to be built on start
``` javascript
```json5
"prebuiltChance" : 75
```

Some files were not shown because too many files have changed in this diff Show More