1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-08 23:36:33 +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** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 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. If applicable, add screenshots to help explain your problem.
**Version** **Version**
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS] - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version] - Version: [VCMI version]

View File

@ -402,3 +402,9 @@ jobs:
run: | run: |
sudo apt install python3-jstyleson sudo apt install python3-jstyleson
python3 CI/validate_json.py 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); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest); 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 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); ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
} }
for(const auto & unit : allUnits) for(const auto & unit : allUnits)
{ {
if(!unit->isValidTarget(true)) if(!unit->isValidTarget(true))
@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
// Ensure ps.dest is not empty before accessing the first element
if (!ps.dest.empty())
{
logAi->trace( 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->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(), 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 #endif
} }
} }

View File

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

View File

@ -34,11 +34,6 @@
namespace NKAI 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 //one thread may be turn of AI and another will be handling a side effect for AI2
thread_local CCallback * cb = nullptr; thread_local CCallback * cb = nullptr;
thread_local AIGateway * ai = nullptr; thread_local AIGateway * ai = nullptr;
@ -553,7 +548,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
double fightRatio = ourStrength / (double)battleState.getEnemyStrength(); double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
// if we have no towns - things are already bad, so retreat is not an option. // 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); 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) else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
{ {
bool dangerUnknown = danger == 0; bool dangerUnknown = danger == 0;
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT); bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
answer = !dangerUnknown && !dangerTooHigh; answer = !dangerUnknown && !dangerTooHigh;
} }

View File

@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
return h == rhs.get(true); 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) if(dangerStrength)
{ {
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; return heroStrength > dangerStrength * safeAttackRatio;
} }
return true; //there's no danger 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) 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 WOOD_ORE_MINE_PRODUCTION = 2;
const int RESOURCE_MINE_PRODUCTION = 1; const int RESOURCE_MINE_PRODUCTION = 1;
const int ACTUAL_RESOURCE_COUNT = 7; 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; extern thread_local CCallback * cb;
@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj); bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property! 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, uint64_t dangerStrength, float safeAttackRatio);
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength); bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2); bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2); bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../CCallback.h" #include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/GameConstants.h" #include "../../../lib/GameConstants.h"
namespace NKAI namespace NKAI
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
uint64_t armyValue = 0; uint64_t armyValue = 0;
TemporaryArmy newArmyInstance; 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()) while(allowedFactions.size() < alignmentMap.size())
{ {
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
auto morale = slot.second->moraleVal(); auto morale = slot.second->moraleVal();
auto multiplier = 1.0f; auto multiplier = 1.0f;
const float BadMoraleChance = 0.083f; const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
const float HighMoraleChance = 0.04f; 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(); newValue += multiplier * slot.second->getPower();

View File

@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
for(int upgradeIndex : {1, 0}) for(int upgradeIndex : {1, 0})
{ {
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex)); BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building)) if(!vstd::contains(buildings, building))
continue; // no such building in town 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) 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_1});
otherBuildings.push_back({BuildingID::HORDE_2}); 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 & buildingSet : otherBuildings)
{ {
for(auto & buildingID : buildingSet) for(auto & buildingID : buildingSet)
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
auto towns = ai->cb->getTownsInfo(); auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns) for(const CGTownInstance* town : towns)
{ {
logAi->trace("Checking town %s", town->getNameTranslated()); logAi->trace("Checking town %s", town->getNameTranslated());
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
requiredResources += developmentInfo.requiredResources; requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost; totalDevelopmentCost += developmentInfo.townDevelopmentCost;
for(auto building : developmentInfo.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += developmentInfo.armyCost; armyCost += developmentInfo.armyCost;
for(auto bi : developmentInfo.toBuild) for(auto bi : developmentInfo.toBuild)
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
updateDailyIncome(); updateDailyIncome();
if(ai->cb->getDate(Date::DAY) == 1) goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
{
goldPressure = 1;
}
else
{
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
}
logAi->trace("Gold pressure: %f", goldPressure); logAi->trace("Gold pressure: %f", goldPressure);
} }
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
logAi->trace("checking %s", info.name); logAi->trace("checking %s", info.name);
logAi->trace("buildInfo %s", info.toString()); 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)) if(!town->hasBuilt(building))
{ {
auto canBuild = ai->cb->canBuildStructure(town, building); auto canBuild = ai->cb->canBuildStructure(town, building);
@ -281,6 +292,14 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++; prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost; 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; prerequisite.dailyIncome = info.dailyIncome;
return prerequisite; return prerequisite;

View File

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

View File

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

View File

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

View File

@ -56,9 +56,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
float evaluateHero(const CGHeroInstance * hero) const; float evaluateHero(const CGHeroInstance * hero) const;
bool canRecruitHero(const CGTownInstance * t = nullptr) const; bool canRecruitHero(const CGTownInstance * t = nullptr) const;
bool heroCapReached() const; bool heroCapReached(bool includeGarrisoned = true) const;
const CGHeroInstance * findHeroWithGrail() 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 getMagicStrength(const CGHeroInstance * hero) const;
float getFightingStrengthCached(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); auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
if (ai->cb->isVisible(node.coord))
blockers = ai->cb->getVisitableObjs(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)); auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); 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; continue;
ClusterMap::accessor cluster; ClusterMap::accessor cluster;
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); 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; continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f; 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 & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
ai->dangerHitMap->updateHitMap();
for(auto & developmentInfo : developmentInfos) 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) for (auto& buildingInfo : developmentInfo.toBuild)
{ {
@ -64,14 +85,16 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
composition.addNext(BuildThis(buildingInfo, developmentInfo)); composition.addNext(BuildThis(buildingInfo, developmentInfo));
composition.addNext(SaveResources(buildingInfo.buildCost)); composition.addNext(SaveResources(buildingInfo.buildCost));
tasks.push_back(sptr(composition)); tasks.push_back(sptr(composition));
} }
else else
{
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
} }
} }
} }
}
}
return tasks; return tasks;
} }

View File

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

View File

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

View File

@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
for(auto town : ai->cb->getTownsInfo()) for(auto town : ai->cb->getTownsInfo())
{ {
evaluateDefence(tasks, town, ai); 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; return tasks;
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); 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) 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))); 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; return;
} }
if(!threatNode.fastestDanger.hero) if(!threatNode.fastestDanger.hero)
{ {
logAi->trace("No threat found for town %s", town->getNameTranslated()); logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; 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(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{ {
#if NKAI_TRACE_LEVEL >= 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 // dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0 || town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{ {
tasks.push_back( tasks.push_back(
@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; 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)) 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)) else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{ {
if(town->garrisonHero) if(town->garrisonHero && town->garrisonHero != path.targetHero)
{
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 NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", 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 #endif
continue; continue;
} }
}
else if(path.turn() == 0) else if(path.turn() == 0)
{ {
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); 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 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) if(town->hasBuilt(BuildingID::TAVERN)
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) && 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()) else if(ai->heroManager->heroCapReached())
{ {
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
if(!heroToDismiss) if(!heroToDismiss)
continue; continue;

View File

@ -34,48 +34,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
Goals::TGoalVec tasks; Goals::TGoalVec tasks;
for (auto obj : ai->memory->visitableObjs) for (auto obj : ai->memory->visitableObjs)
{
if(!vstd::contains(ai->memory->alreadyVisited, obj))
{ {
switch (obj->ID.num) switch (obj->ID.num)
{ {
case Obj::REDWOOD_OBSERVATORY: case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE: 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)))); tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
break; break;
}
case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY: case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE: case Obj::SUBTERRANEAN_GATE:
case Obj::WHIRLPOOL: 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)))); 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()); logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif #endif
if (path.targetHero->getOwner() != ai->playerID)
continue;
if(path.containsHero(hero)) if(path.containsHero(hero))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
continue; 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(ai->arePathHeroesLocked(path))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
} }
auto danger = path.getTotalDanger(); 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 #if NKAI_TRACE_LEVEL >= 2
logAi->trace( logAi->trace(
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
continue; 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); auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero if(!upgrader->garrisonHero
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); 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.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); 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)) for(auto hero : cb->getAvailableHeroes(upgrader))
{ {
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader) auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->getFreeGold() >20000 && ai->getFreeGold() >20000
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
auto danger = path.getTotalDanger(); 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 #if NKAI_TRACE_LEVEL >= 2
logAi->trace( logAi->trace(

View File

@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
auto ourHeroes = ai->heroManager->getHeroRoles(); auto ourHeroes = ai->heroManager->getHeroRoles();
auto minScoreToHireMain = std::numeric_limits<float>::max(); auto minScoreToHireMain = std::numeric_limits<float>::max();
int currentArmyValue = 0;
for(auto hero : ourHeroes) for(auto hero : ourHeroes)
{ {
currentArmyValue += hero.first->getArmyCost();
if(hero.second != HeroRole::MAIN) if(hero.second != HeroRole::MAIN)
continue; continue;
@ -45,26 +47,38 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
minScoreToHireMain = newScore; 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) 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)) if(ai->heroManager->canRecruitHero(town))
{ {
auto availableHeroes = ai->cb->getAvailableHeroes(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()) for (auto obj : ai->objectClusterizer->getNearbyObjects())
{ {
if ((obj->ID == Obj::RESOURCE) 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)) for(auto hero : availableHeroes)
continue;
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{ {
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) for(auto town : towns)
{ {
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
continue;
ai->pathfinder->calculatePathInfo(paths, town->visitablePos()); ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
for(auto & path : paths) for(auto & path : paths)
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
if(town->visitingHero && town->visitingHero.get() != path.targetHero) if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue; continue;
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit()) if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
{ {
if(path.targetHero->mana == path.targetHero->manaLimit())
continue;
Composition stayAtTown; Composition stayAtTown;
stayAtTown.addNextSequence({ stayAtTown.addNextSequence({

View File

@ -17,8 +17,7 @@
namespace NKAI namespace NKAI
{ {
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter constexpr float 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
engineBase::engineBase() engineBase::engineBase()
{ {

View File

@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
{ {
objectDanger += evaluateDanger(hero->visitedTown.get()); 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) if(objectDanger)
@ -118,9 +127,9 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
auto fortLevel = town->fortLevel(); auto fortLevel = town->fortLevel();
if (fortLevel == CGTownInstance::EFortLevel::CASTLE) if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger += 10000; danger = std::max(danger * 2, danger + 10000);
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
danger += 4000; danger = std::max(ui64(danger * 1.4), danger + 4000);
} }
return danger; return danger;

View File

@ -34,13 +34,12 @@ using namespace Goals;
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph; std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
Nullkiller::Nullkiller() 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) 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 false;
} }
return cb->getStartInfo()->difficulty >= 3; return true;
} }
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway) void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
{ {
this->cb = cb; this->cb = cb;
this->gateway = gateway; 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; useObjectGraph = false;
openMap = false; openMap = false;
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
{ {
TGoalVec blockers; TGoalVec blockers;
if (task->asTask()->priority <= 0)
return;
for(auto & item : tasks) for(auto & item : tasks)
{ {
for(auto objid : item.affectedObjects) 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) if(item.task->asTask()->priority >= task->asTask()->priority)
return; return;
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
return taskptr(*bestTask); return taskptr(*bestTask);
} }
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
{ {
TaskPlan taskPlan; 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(); auto evaluator = this->priorityEvaluators->acquire();
for(size_t i = r.begin(); i != r.end(); i++) for(size_t i = r.begin(); i != r.end(); i++)
{ {
auto task = tasks[i]; auto task = tasks[i];
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
if(task->asTask()->priority <= 0) task->asTask()->priority = evaluator->evaluate(task, priorityTier);
task->asTask()->priority = evaluator->evaluate(task);
} }
}); });
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
if(lockReason != HeroLockedReason::NOT_LOCKED) if(lockReason != HeroLockedReason::NOT_LOCKED)
{ {
#if NKAI_TRACE_LEVEL >= 1 #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 #endif
return true; return true;
} }
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker); boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10; const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState(); resetAiState();
Goals::TGoalVec bestTasks; 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++) for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{ {
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
Goals::TTask bestTask = taskptr(Goals::Invalid()); Goals::TTask bestTask = taskptr(Goals::Invalid());
for(;i <= settings->getMaxPass(); i++) while(true)
{ {
bestTasks.clear(); bestTasks.clear();
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
decompose(bestTasks, sptr(BuildingBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(bestTasks); 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)) if(!executeTask(bestTask))
return; return;
@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
} }
} }
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
if(!isOpenMap()) if(!isOpenMap())
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); 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)); logAi->debug("Decision madel in %ld", timeElapsed(start));
@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
bestTask->priority); bestTask->priority);
} }
if(bestTask->priority < MIN_PRIORITY) if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
{ {
auto heroes = cb->getHeroesInfo(); auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
continue; 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(!executeTask(bestTask))
{ {
if(hasAnySuccess) if(hasAnySuccess)
@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
else else
return; return;
} }
hasAnySuccess = true; hasAnySuccess = true;
} }
hasAnySuccess |= handleTrading();
if(!hasAnySuccess) if(!hasAnySuccess)
{ {
logAi->trace("Nothing was done this turn. Ending turn."); 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; return;
} }
@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
lockedResources += 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; } ScanDepth getScanDepth() const { return scanDepth; }
bool isOpenMap() const { return openMap; } bool isOpenMap() const { return openMap; }
bool isObjectGraphAllowed() const { return useObjectGraph; } bool isObjectGraphAllowed() const { return useObjectGraph; }
bool handleTrading();
private: private:
void resetAiState(); void resetAiState();
void updateAiState(int pass, bool fast = false); void updateAiState(int pass, bool fast = false);
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) 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 executeTask(Goals::TTask task);
bool areAffectedObjectsPresent(Goals::TTask task) const; bool areAffectedObjectsPresent(Goals::TTask task) const;
HeroRole getTaskRole(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const;

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h" #include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h" #include "../../../lib/StartInfo.h"
@ -33,9 +35,7 @@
namespace NKAI namespace NKAI
{ {
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter constexpr float MIN_CRITICAL_VALUE = 2.0f;
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller* ai) EvaluationContext::EvaluationContext(const Nullkiller* ai)
: movementCost(0.0), : movementCost(0.0),
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
heroRole(HeroRole::SCOUT), heroRole(HeroRole::SCOUT),
turn(0), turn(0),
strategicalValue(0), strategicalValue(0),
conquestValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0), 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 creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1; auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree) 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) switch(art->aClass)
{ {
case CArtifact::EartClass::ART_TREASURE:
//FALL_THROUGH
case CArtifact::EartClass::ART_MINOR: case CArtifact::EartClass::ART_MINOR:
classValue = 1000; classValue = 1000;
break; break;
@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::SPELL_SCROLL:
//FALL_THROUGH
case Obj::ARTIFACT: case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType()); return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
case Obj::HERO: case Obj::HERO:
@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result; 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())); 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; 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 float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
{ {
auto rewardable = dynamic_cast<const CRewardableObject *>(hut); auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
for(auto stack : army->Slots()) 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; return value;
@ -786,7 +851,9 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); 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.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
evaluationContext.isExchange = true;
} }
}; };
@ -804,6 +871,7 @@ public:
evaluationContext.armyReward += upgradeValue; evaluationContext.armyReward += upgradeValue;
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
evaluationContext.isArmyUpgrade = true;
} }
}; };
@ -818,6 +886,25 @@ public:
int tilesDiscovered = task->value; int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); 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); Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); 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.movementCost += stayAtTown.getMovementWasted();
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
}
} }
}; };
@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger) if(enemyDanger.danger)
{ {
auto dangerRatio = enemyDanger.danger / (double)ourStrength; 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.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
} }
} }
@ -896,6 +981,10 @@ public:
else else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
evaluationContext.defenseValue = town->fortLevel();
evaluationContext.isDefend = true;
evaluationContext.threatTurns = treat.turn;
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
} }
@ -926,6 +1015,8 @@ public:
for(auto & node : path.nodes) for(auto & node : path.nodes)
{ {
vstd::amax(costsPerHero[node.targetHero], node.cost); vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
} }
for(auto pair : costsPerHero) for(auto pair : costsPerHero)
@ -952,10 +1043,18 @@ public:
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); 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.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()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn()); vstd::amax(evaluationContext.turn, path.turn());
} }
@ -996,6 +1095,7 @@ public:
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -1021,6 +1121,14 @@ public:
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task); Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); 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) if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{ {
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@ -1029,6 +1137,9 @@ public:
evaluationContext.movementCost += mpLeft; evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft;
evaluationContext.heroRole = defenderRole; 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.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; 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.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) if(bi.creatureID != CreatureID::NONE)
{ {
@ -1100,7 +1217,18 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{ {
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); 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) if(evaluationContext.goldReward)
{ {
@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
for(auto subgoal : parts) for(auto subgoal : parts)
{ {
context.goldCost += subgoal->goldCost; context.goldCost += subgoal->goldCost;
context.buildingCost += subgoal->buildingCost;
for(auto builder : evaluationContextBuilders) for(auto builder : evaluationContextBuilders)
{ {
@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
return context; return context;
} }
float PriorityEvaluator::evaluate(Goals::TSubgoal task) float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{ {
auto evaluationContext = buildEvaluationContext(task); auto evaluationContext = buildEvaluationContext(task);
@ -1185,6 +1314,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
double result = 0; double result = 0;
if (ai->settings->isUseFuzzy())
{
float fuzzyResult = 0;
try try
{ {
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
@ -1206,15 +1338,26 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
engine->process(); engine->process();
result = value->getValue(); fuzzyResult = value->getValue();
} }
catch (fl::Exception& fe) catch (fl::Exception& fe)
{ {
logAi->error("evaluate VisitTile: %s", fe.getWhat()); 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 #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(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
@ -1223,9 +1366,220 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
goldRewardPerTurn, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger, evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue, 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.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
result); result);

View File

@ -41,6 +41,7 @@ public:
float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(int resType) const;
float getResourceRequirementStrength(const TResources & res) const; float getResourceRequirementStrength(const TResources & res) const;
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const; float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
float getConquestValue(const CGObjectInstance* target) const;
float getTotalResourceRequirementStrength(int resType) const; float getTotalResourceRequirementStrength(int resType) const;
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
float getSkillReward(const CGObjectInstance * target, 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; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) 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 struct DLL_EXPORT EvaluationContext
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
int32_t goldCost; int32_t goldCost;
float skillReward; float skillReward;
float strategicalValue; float strategicalValue;
float conquestValue;
HeroRole heroRole; HeroRole heroRole;
uint8_t turn; uint8_t turn;
RewardEvaluator evaluator; RewardEvaluator evaluator;
float enemyHeroDangerRatio; 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); EvaluationContext(const Nullkiller * ai);
@ -91,7 +106,20 @@ public:
~PriorityEvaluator(); ~PriorityEvaluator();
void initVisitTile(); 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: private:
const Nullkiller * ai; const Nullkiller * ai;

View File

@ -11,6 +11,8 @@
#include <limits> #include <limits>
#include "Settings.h" #include "Settings.h"
#include "../../../lib/constants/StringConstants.h"
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
@ -22,56 +24,39 @@
namespace NKAI namespace NKAI
{ {
Settings::Settings() Settings::Settings(int difficultyLevel)
: maxRoamingHeroes(8), : maxRoamingHeroes(8),
mainHeroTurnDistanceLimit(10), mainHeroTurnDistanceLimit(10),
scoutHeroTurnDistanceLimit(5), scoutHeroTurnDistanceLimit(5),
maxGoldPressure(0.3f), maxGoldPressure(0.3f),
retreatThresholdRelative(0.3),
retreatThresholdAbsolute(10000),
safeAttackRatio(1.1),
maxpass(10), maxpass(10),
pathfinderBucketsCount(1),
pathfinderBucketSize(32),
allowObjectGraph(true), allowObjectGraph(true),
useTroopsFromGarrisons(false), 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["maxRoamingHeroes"].Integer();
{ mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer(); scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
} maxpass = node["maxpass"].Integer();
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber()) pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
{ maxGoldPressure = node["maxGoldPressure"].Float();
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer(); retreatThresholdRelative = node["retreatThresholdRelative"].Float();
} retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
safeAttackRatio = node["safeAttackRatio"].Float();
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber()) allowObjectGraph = node["allowObjectGraph"].Bool();
{ openMap = node["openMap"].Bool();
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer(); useFuzzy = node["useFuzzy"].Bool();
} useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
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();
}
} }
} }

View File

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

View File

@ -104,6 +104,7 @@ namespace Goals
bool isAbstract; SETTER(bool, isAbstract) bool isAbstract; SETTER(bool, isAbstract)
int value; SETTER(int, value) int value; SETTER(int, value)
ui64 goldCost; SETTER(ui64, goldCost) ui64 goldCost; SETTER(ui64, goldCost)
TResources buildingCost; SETTER(TResources, buildingCost)
int resID; SETTER(int, resID) int resID; SETTER(int, resID)
int objid; SETTER(int, objid) int objid; SETTER(int, objid)
int aid; SETTER(int, aid) 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()); throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
} }
if (hero->inTownGarrison)
ai->myCb->swapGarrisonHero(hero->visitedTown);
auto wait = cb->waitTillRealize; auto wait = cb->waitTillRealize;
cb->waitTillRealize = true; cb->waitTillRealize = true;

View File

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

View File

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

View File

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

View File

@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
{ {
return "Stay at town " + town->getNameTranslated() return "Stay at town " + town->getNameTranslated()
+ " hero " + hero->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) 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); ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,6 @@
#include "../../CCallback.h" #include "../../CCallback.h"
#include "../../lib/mapObjects/MapObjects.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) ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
: resources(Res), goal(Goal) : resources(Res), goal(Goal)
{ {

View File

@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
return false; return false;
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
return false; 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)) if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false; return false;
if(!cb->getAvailableHeroes(t).size()) 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.8" : "挑战性的",
"vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的",
"vcmi.adventureMap.monsterThreat.levels.10" : "致命的", "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.monsterLevel" : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物",
"vcmi.adventureMap.monsterMeleeType" : "近战", "vcmi.adventureMap.monsterMeleeType" : "近战",
"vcmi.adventureMap.monsterRangedType" : "远程", "vcmi.adventureMap.monsterRangedType" : "远程",
@ -188,9 +188,6 @@
"vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。",
"vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}",
"vcmi.server.errors.modsToDisable" : "{需要禁用的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.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。", "vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
@ -304,7 +301,7 @@
"vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器", "vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器",
"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)", "vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)",
"vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小", "vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小",
"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)", "vcmi.battleOptions.queueSizeBigButton.help": "设置回合顺序指示器为大尺寸(无法在游戏高度像素低于700时生效)",
"vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed1.hover": "",
"vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "",
"vcmi.battleOptions.animationsSpeed6.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "",
@ -384,7 +381,7 @@
"vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面",
"vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物",
"vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序", "vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序",
"vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。.", "vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。",
"vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序", "vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序",
"vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。", "vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。",
"vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序", "vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序",

View File

@ -86,12 +86,13 @@
"vcmi.spellBook.search" : "Hledat", "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.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.research" : "Prozkoumat toto kouzlo",
"vcmi.spellResearch.skip" : "Přeskočit toto kouzlo", "vcmi.spellResearch.skip" : "Přeskočit toto kouzlo",
"vcmi.spellResearch.abort" : "Přerušit", "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.serverConnecting" : "Připojování...",
"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", "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.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.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.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.deleteSaveGameTitle" : "Vyberte uloženou hru k odstranění",
"vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění", "vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění",
"vcmi.lobby.deleteFile" : "Chcete smazat následující soubor?", "vcmi.lobby.deleteFile" : "Chcete odstranit následující soubor?",
"vcmi.lobby.deleteFolder" : "Chcete smazat následující složku?", "vcmi.lobby.deleteFolder" : "Chcete odstranit následující složku?",
"vcmi.lobby.deleteMode" : "Přepnout do režimu mazání a zpět", "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.title" : "Online lobby VCMI",
"vcmi.lobby.login.username" : "Uživatelské jméno:", "vcmi.lobby.login.username" : "Uživatelské jméno:",
"vcmi.lobby.login.connecting" : "Připojování...", "vcmi.lobby.login.connecting" : "Připojování...",
@ -127,6 +166,7 @@
"vcmi.lobby.login.create" : "Nový účet", "vcmi.lobby.login.create" : "Nový účet",
"vcmi.lobby.login.login" : "Přihlásit se", "vcmi.lobby.login.login" : "Přihlásit se",
"vcmi.lobby.login.as" : "Přihlásit se jako %s", "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.rooms" : "Herní místnosti - %d",
"vcmi.lobby.header.channels" : "Kanály konverzace", "vcmi.lobby.header.channels" : "Kanály konverzace",
"vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name "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.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.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.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.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.", "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.DISINTEGRATE.description" : "Po smrti nezůstane žádné tělo",
"core.bonus.INVINCIBLE.name" : "Neporazitelný", "core.bonus.INVINCIBLE.name" : "Neporazitelný",
"core.bonus.INVINCIBLE.description" : "Nelze ovlivnit žádným efektem", "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.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.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.modsToEnable" : "{Following mods are required}",
"vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", "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.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!",
"vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.", "vcmi.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.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.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}",
"vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", "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.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.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.", "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.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.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.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.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.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.", "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.movementPointsHeroInfo" : "(Pontos de movimento: %REMAINING / %POINTS)",
"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!", "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.0" : "Vermelho",
"vcmi.capitalColors.1" : "Azul", "vcmi.capitalColors.1" : "Azul",
"vcmi.capitalColors.2" : "Bege", "vcmi.capitalColors.2" : "Bege",
@ -85,6 +92,7 @@
"vcmi.spellResearch.research" : "Pesquisar este Feitiço", "vcmi.spellResearch.research" : "Pesquisar este Feitiço",
"vcmi.spellResearch.skip" : "Pular este Feitiço", "vcmi.spellResearch.skip" : "Pular este Feitiço",
"vcmi.spellResearch.abort" : "Abortar", "vcmi.spellResearch.abort" : "Abortar",
"vcmi.spellResearch.noMoreSpells" : "Não há mais feitiços disponíveis para pesquisa.",
"vcmi.mainMenu.serverConnecting" : "Conectando...", "vcmi.mainMenu.serverConnecting" : "Conectando...",
"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:", "vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
@ -96,16 +104,60 @@
"vcmi.lobby.filepath" : "Caminho do arquivo", "vcmi.lobby.filepath" : "Caminho do arquivo",
"vcmi.lobby.creationDate" : "Data de criação", "vcmi.lobby.creationDate" : "Data de criação",
"vcmi.lobby.scenarioName" : "Nome do cenário", "vcmi.lobby.scenarioName" : "Nome do cenário",
"vcmi.lobby.mapPreview" : "Visualização do mapa", "vcmi.lobby.mapPreview" : "Prévia do mapa",
"vcmi.lobby.noPreview" : "sem visualização", "vcmi.lobby.noPreview" : "sem prévia",
"vcmi.lobby.noUnderground" : "sem subterrâneo", "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.backToLobby" : "Voltar para a sala de espera",
"vcmi.lobby.author" : "Autor", "vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Desvant.", "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.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.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.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.title" : "Sala de Espera Online do VCMI",
"vcmi.lobby.login.username" : "Nome de usuário:", "vcmi.lobby.login.username" : "Nome de usuário:",
@ -114,6 +166,7 @@
"vcmi.lobby.login.create" : "Nova Conta", "vcmi.lobby.login.create" : "Nova Conta",
"vcmi.lobby.login.login" : "Entrar", "vcmi.lobby.login.login" : "Entrar",
"vcmi.lobby.login.as" : "Entrar como %s", "vcmi.lobby.login.as" : "Entrar como %s",
"vcmi.lobby.login.spectator" : "Espectador",
"vcmi.lobby.header.rooms" : "Salas de Jogo - %d", "vcmi.lobby.header.rooms" : "Salas de Jogo - %d",
"vcmi.lobby.header.channels" : "Canais de Bate-papo", "vcmi.lobby.header.channels" : "Canais de Bate-papo",
"vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma "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.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.modsToEnable" : "{Os seguintes mods são necessários}",
"vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}", "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.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.", "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.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém",
"core.bonus.FLYING.name" : "Voo", "core.bonus.FLYING.name" : "Voo",
"core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)", "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.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo",
"core.bonus.GARGOYLE.name" : "Gárgula", "core.bonus.GARGOYLE.name" : "Gárgula",
"core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado", "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.name" : "Imune a Feitiços 1-${val}",
"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 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.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.name" : "Drenar Vida (${val}%)",
"core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado", "core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado",
"core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%", "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.DISINTEGRATE.description": "Nenhum corpo permanece após a morte",
"core.bonus.INVINCIBLE.name": "Invencível", "core.bonus.INVINCIBLE.name": "Invencível",
"core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada", "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.name" : "Sopro Prismático",
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Ataque de Sopro Prismático (três direções)", "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.castleMoat.name": "Fosso",
"spell.core.castleMoatTrigger.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.modsToEnable" : "{Se requieren los siguientes mods}",
"vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}", "vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}",
"vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?", "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.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", "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.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.modsToEnable" : "{Följande modd(ar) krävs}",
"vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}", "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.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.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'.", "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.modsToEnable" : "{Потрібні модифікації для завантаження гри}",
"vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}",
"vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "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.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами", "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.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer(); spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer(); spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer(); spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
}
// append any owned heroes / towns that were not present in loaded state // append any owned heroes / towns that were not present in loaded state
wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end()); wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end());
ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.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()); // ObjectInstanceID selectedObjectID(source["currentSelection"].Integer());
// const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID); // const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID);
// const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr); // const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr);

View File

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

View File

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

View File

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

View File

@ -362,17 +362,6 @@ void CMainMenu::update()
menu->switchToTab(menu->getActiveTab()); 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 // Handles mouse and key input
GH.handleEvents(); GH.handleEvents();
GH.windows().simpleRedraw(); GH.windows().simpleRedraw();

View File

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

View File

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

View File

@ -49,6 +49,7 @@ void ButtonBase::update()
// hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted // hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted
if (state == EButtonState::HIGHLIGHTED && image->size() < 4) if (state == EButtonState::HIGHLIGHTED && image->size() < 4)
image->setFrame(image->size()-1); image->setFrame(image->size()-1);
else
image->setFrame(stateToIndex[vstd::to_underlying(state)]); 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) void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
{ {
curHero = hero; curHero = hero;
if (!hero)
return;
for(auto slot : artWorn) for(auto slot : artWorn)
{ {

View File

@ -28,6 +28,7 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
CArtifactsOfHeroMain::~CArtifactsOfHeroMain() CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
{ {
if(curHero)
CArtifactsOfHeroBase::putBackPickedArtifact(); 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"); std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other");
int maxLen = 50; int maxLen = 50;
EFonts f = FONT_TINY; EFonts f = FONT_TINY;
Point pText = p + Point(3, 40); Point pText = p + Point(4, 38);
// 1px Black border // 1px Black border
bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen)); 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(); arts->putBackPickedArtifact();
close(); close();
LOCPLINT->cb->dismissHero(curHero); LOCPLINT->cb->dismissHero(curHero);
arts->setHero(nullptr);
}, nullptr); }, nullptr);
} }

View File

@ -117,7 +117,13 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
color = ""; color = "";
} }
else 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; currPos += symbolSize;
} }

View File

@ -1508,7 +1508,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
itemsVisible = items; itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled); 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) 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; itemsVisible = items;
init(titleWidget_, _title, _descr, searchBoxEnabled); 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) 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); 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); 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, "maxRoamingHeroes" : 8,
"maxpass" : 30, "maxpass" : 30,
"mainHeroTurnDistanceLimit" : 10, "mainHeroTurnDistanceLimit" : 10,
@ -6,5 +58,29 @@
"maxGoldPressure" : 0.3, "maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true, "useTroopsFromGarrisons" : true,
"openMap": 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 "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": "spells":
{ {
// if enabled, dimension work doesn't work into tiles under Fog of War // if enabled, dimension work doesn't work into tiles under Fog of War

View File

@ -106,7 +106,7 @@
"defaultTavern" : 5, "defaultTavern" : 5,
"affinity" : "might", "affinity" : "might",
"commander" : "medusaQueen", "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" } } "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
}, },
"warlock" : "warlock" :
@ -116,7 +116,7 @@
"defaultTavern" : 5, "defaultTavern" : 5,
"affinity" : "magic", "affinity" : "magic",
"commander" : "medusaQueen", "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" } } "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
}, },
"barbarian" : "barbarian" :

View File

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

View File

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

View File

@ -12,7 +12,7 @@
"properties" : { "properties" : {
"type" : { "type" : {
"type" : "string", "type" : "string",
"enum" : ["playerStart", "cpuStart", "treasure", "junction"] "enum" : ["playerStart", "cpuStart", "treasure", "junction", "sealed"]
}, },
"size" : { "type" : "number", "minimum" : 1 }, "size" : { "type" : "number", "minimum" : 1 },
"owner" : {}, "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) [![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.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.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/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) [![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. VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
<p> <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;"/> <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> </p>
## Links ## Links
* Homepage: https://vcmi.eu/ * Homepage: <https://vcmi.eu/>
* Forums: https://forum.vcmi.eu/ * Forums: <https://forum.vcmi.eu/>
* Bugtracker: https://github.com/vcmi/vcmi/issues * Bugtracker: <https://github.com/vcmi/vcmi/issues>
* Discord: https://discord.gg/chBT42V * Discord: <https://discord.gg/chBT42V>
* GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant * GPT Store: <https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant>
## Latest release ## 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. Please see corresponding installation guide articles for details for your platform.
## Installation guides ## Installation guides
- [Windows](players/Installation_Windows.md) - [Windows](players/Installation_Windows.md)
- [macOS](players/Installation_macOS.md) - [macOS](players/Installation_macOS.md)
- [Linux](players/Installation_Linux.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 ## Documentation and guidelines for developers
Development environment setup instructions: Development environment setup instructions:
- [Building VCMI for Android](developers/Building_Android.md) - [Building VCMI for Android](developers/Building_Android.md)
- [Building VCMI for iOS](developers/Building_iOS.md) - [Building VCMI for iOS](developers/Building_iOS.md)
- [Building VCMI for Linux](developers/Building_Linux.md) - [Building VCMI for Linux](developers/Building_Linux.md)
@ -78,6 +79,7 @@ Development environment setup instructions:
- [Conan](developers/Conan.md) - [Conan](developers/Conan.md)
Engine documentation: (NOTE: may be outdated) Engine documentation: (NOTE: may be outdated)
- [Development with Qt Creator](developers/Development_with_Qt_Creator.md) - [Development with Qt Creator](developers/Development_with_Qt_Creator.md)
- [Coding Guidelines](developers/Coding_Guidelines.md) - [Coding Guidelines](developers/Coding_Guidelines.md)
- [Bonus System](developers/Bonus_System.md) - [Bonus System](developers/Bonus_System.md)
@ -95,6 +97,6 @@ Engine documentation: (NOTE: may be outdated)
## Copyright and license ## Copyright and license
VCMI Project source code is licensed under GPL version 2 or later. 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) 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 **Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield
We have 3 battle AIs so far: We have 3 battle AIs so far:
* BattleAI - strongest * BattleAI - strongest
* StupidAI - for neutrals, should be simple so that experienced players can abuse it * 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. * 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 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 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. 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 ## 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 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 ### 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 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. 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: 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 * 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 * 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 * 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 * gathering goals, prioritizing and decomposing them
** execute selected best goals * execute selected best goals
Analyzer - a module gathering data from CCallback *. Its goal to make some statistics and avoid making any significant decissions. 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) * 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 * 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 * 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) * PriorityEvaluator - gathers information on task rewards, evaluates their priority using Fuzzy Light library (fuzzy logic)
### Goals ### Goals
Units of activity in AI. Can be AbstractGoal, Task, Marker and Behavior 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. 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 * AdventureSpellCast - town portal, water walk, air walk, summon boat
* BuildBoat - builds a boat in a specific shipyard * BuildBoat - builds a boat in a specific shipyard
* BuildThis - builds a building in a specified town * 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) * StayAtTown - stay at town for the rest of the day (to regain mana)
Behavior - a core game activity 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. * 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. * ClusterBehavior - uses information of ObjectClusterizer to unblock objects hidden behind various blockers. It kills guards, completes quests, captures garrisons.
* BuildingBehavior - develops our towns * 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 * 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: 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 * 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 * 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. 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 ### 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: 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 the attached node and its all red ancestors
- For every bonus - For every bonus
- Call propagator giving the new descendant - then attach appropriately bonuses to the red descendant of attached node (or the node itself). - 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: The following example shows an artifact providing a bonus based on the level of the hero that wears it:
```javascript ```json5
"core:greaterGnollsFlail": "core:greaterGnollsFlail":
{ {
"text" : { "description" : "This mighty flail increases the attack of all gnolls under the hero's command by twice the hero's level." }, "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 # 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. *Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box.
## Requirements ## 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 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: 4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it:
- install with Android Studio - install with Android Studio
- install with `sdkmanager` command line tool - 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) - download with Conan, see [#NDK and Conan](#ndk-and-conan)
5. Optional: 5. Optional:
- Ninja: download from your package manager or from https://github.com/ninja-build/ninja/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 - Ccache: download from your package manager or from <https://github.com/ccache/ccache/releases>
## Obtaining source code ## 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 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). 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: On the step where you need to replace **PROFILE**, choose:
- `android-32` to build for 32-bit architecture (armeabi-v7a) - `android-32` to build for 32-bit architecture (armeabi-v7a)
- `android-64` to build for 64-bit architecture (aarch64-v8a) - `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): 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) - 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. 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. 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 ### 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 ```sh
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm 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] > [!NOTE]
> The stock ffmpeg from Fedora repo is no good as it lacks a lots of codecs > 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 ```sh
sudo dnf install rpmdevtools sudo dnf install rpmdevtools
spectool -g -R ~/rpmbuild/SPECS/vcmi.spec 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 ```sh
sudo dnf install dnf-plugins-core sudo dnf install dnf-plugins-core
sudo dnf builddep ~/rpmbuild/SPECS/vcmi.spec 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 ```sh
rpmbuild -ba ~/rpmbuild/SPECS/vcmi.spec 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. 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: 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. 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: Good locations:
- `C:\VCMI` - `C:\VCMI`
Bad locations: Bad locations:
- `C:\Users\Michał\VCMI (non-ascii character)` - `C:\Users\Michał\VCMI (non-ascii character)`
- `C:\Program Files (x86)\VCMI (write protection)` - `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 #### 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. - 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) 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". - Extract archive by right clicking on it and choosing "7-zip -> Extract Here".
#### Move dependencies to target directory #### Move dependencies to target directory
Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside. Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside.
Move extracted `vcpkg` directory into your `%VCMI_DIR%` 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: From command line use:
```sh
git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
```
#### Build vcpkg and dependencies #### 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 ## Build VCMI
#### From GIT GUI #### From GIT GUI
- Open SourceTree - Open SourceTree
- File -> Clone - File -> Clone
- select `https://github.com/vcmi/vcmi/` as source - 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 - click Clone
#### From command line #### From command line
- `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source` - `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source`
### Generate solution for VCMI ### Generate solution for VCMI
- Create `%VCMI_DIR%/build` folder - Create `%VCMI_DIR%/build` folder
- Open a command line prompt at `%VCMI_DIR%/build` - Open a command line prompt at `%VCMI_DIR%/build`
- Execute `cd %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` - 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 ### Compile VCMI with Visual Studio
- Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio - Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio
- Select `Release` build type in the combobox - Select `Release` build type in the combobox
- If you want to use ccache: - 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! - VCMI will be built in `%VCMI_DIR%/build/bin` folder!
### Compile VCMI with MinGW via MSYS2 ### 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 - 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` - 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` - 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. 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: Make sure you have:
* Installed Heroes III from disk or using GOG installer * Installed Heroes III from disk or using GOG installer
* Copied `Data`, `Maps` and `Mp3` folders from Heroes III to: `%USERPROFILE%\Documents\My Games\vcmi\` * 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 ## 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). 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` * `-D CMAKE_BUILD_TYPE=Debug`
* Enables debug info and disables optimizations * Enables debug info and disables optimizations
* `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON` * `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON`
* Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. * 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
For clangd to find the JSON, create a file named `.clangd` with this content
``` ```
CompileFlags: CompileFlags:
CompilationDatabase: build 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 ### Main purposes of client
Client is responsible for: Client is responsible for:
- displaying state of game to human player - displaying state of game to human player
- capturing player's actions and sending requests to server - capturing player's actions and sending requests to server
- displaying changes in state of game indicated by 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>` `<other forward declarations>`
`<classes>` `<classes>`
##### New project part ##### 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> 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. 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 ## 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 /// @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 ### 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) - **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) - **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** - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz**
- iOS: pick ***dependencies-ios.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). 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 ### 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 #### 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. - 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 #### Building for Android
@ -105,11 +105,11 @@ After applying patch(es):
2. Run `make` 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`. 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 ##### 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. 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. 3. Now you can execute `conan install` to build all dependencies.
@ -172,7 +172,7 @@ cmake --preset ios-conan
`CMakeUserPresets.json` file: `CMakeUserPresets.json` file:
```json ```json5
{ {
"version": 3, "version": 3,
"cmakeMinimumRequired": { "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, - 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 - 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 ## Configuration

View File

@ -24,7 +24,7 @@ Some notes:
### Setup settings.json ### Setup settings.json
``` javascript ```json5
{ {
"logging" : { "logging" : {
"console" : { "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: 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 Format: %m
Threshold: info Threshold: info
@ -76,17 +76,18 @@ coloredOutputEnabled: true
colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red
**File** #### File
Format: %d %l %n \[%t\] - %m Format: %d %l %n \[%t\] - %m
**Loggers** #### Loggers
global -\> info global -\> info
### How to get a logger ### 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: 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 ```cpp
Logger * logger = CLogger::getLogger(CLoggerDomain("rmg")); Logger * logger = CLogger::getLogger(CLoggerDomain("rmg"));
``` ```

View File

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

View File

@ -5,12 +5,14 @@
For implementation details see files located at `lib/network` directory. 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: 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; int32_t messageSize;
byte messagePayload[messageSize]; byte messagePayload[messageSize];
``` ```
Networking can be used by: Networking can be used by:
- game client (vcmiclient / VCMI_Client.exe). Actual application that player interacts with directly using UI. - 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. - 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. - 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 ## Global lobby communication
For implementation details see: For implementation details see:
- game client: `client/globalLobby/GlobalLobbyClient.h - game client: `client/globalLobby/GlobalLobbyClient.h
- match server: `server/GlobalLobbyProcessor.h - match server: `server/GlobalLobbyProcessor.h
- lobby server: `client/globalLobby/GlobalLobbyClient.h - lobby server: `client/globalLobby/GlobalLobbyClient.h
In case of global lobby, message payload uses plaintext json format - utf-8 encoded string: In case of global lobby, message payload uses plaintext json format - utf-8 encoded string:
``` ```
int32_t messageSize; int32_t messageSize;
char jsonString[messageSize]; char jsonString[messageSize];
@ -43,6 +47,7 @@ Every message must be a struct (json object) that contains "type" field. Unlike
### Communication flow ### Communication flow
Notes: Notes:
- invalid message, such as corrupted json format or failure to validate message will result in no reply from server - 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 - 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` - lobby -> client: `accountCreated`
#### Login #### Login
- client -> lobby: `clientLogin` - client -> lobby: `clientLogin`
- lobby -> client: `loginSuccess` - lobby -> client: `loginSuccess`
- lobby -> client: `chatHistory` - lobby -> client: `chatHistory`
@ -59,10 +65,12 @@ Notes:
- lobby -> client: `activeGameRooms` - lobby -> client: `activeGameRooms`
#### Chat Message #### Chat Message
- client -> lobby: `sendChatMessage` - client -> lobby: `sendChatMessage`
- lobby -> every client: `chatMessage` - lobby -> every client: `chatMessage`
#### New Game Room #### New Game Room
- client starts match server instance - client starts match server instance
- match -> lobby: `serverLogin` - match -> lobby: `serverLogin`
- lobby -> match: `loginSuccess` - lobby -> match: `loginSuccess`
@ -73,19 +81,23 @@ Notes:
- lobby -> every client: `activeGameRooms` - lobby -> every client: `activeGameRooms`
#### Joining a game room #### Joining a game room
See [#Proxy mode](proxy-mode) See [#Proxy mode](proxy-mode)
#### Leaving a game room #### Leaving a game room
- client closes connection to match server - client closes connection to match server
- match -> lobby: `leaveGameRoom` - match -> lobby: `leaveGameRoom`
#### Sending an invite: #### Sending an invite
- client -> lobby: `sendInvite` - client -> lobby: `sendInvite`
- lobby -> target client: `inviteReceived` - 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 Note: there is no dedicated procedure to accept an invite. Instead, invited player will use same flow as when joining public game room
#### Logout #### Logout
- client closes connection - client closes connection
- lobby -> every client: `activeAccounts` - 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. 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: Currently, process to establish connection using proxy mode is:
- Player attempt to join open game room using `joinGameRoom` message - 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 - 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 - 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. 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. 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. This section dedicated to explain specific configurations of our servers for anyone who might need to improve it in future.
## Droplet configuration ### Droplet configuration
### Droplet and hosted services
Currently we using two droplets: Currently we using two droplets:

View File

@ -1,12 +1,16 @@
# Release Process # Release Process
## Versioning ## Versioning
For releases VCMI uses version numbering in form "1.X.Y", where: 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. - '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. - '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 ## Branches
Our branching strategy is very similar to GitFlow: 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. - `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. - `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`. - `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 ## Release process step-by-step
### Initial release setup (major releases only) ### Initial release setup (major releases only)
Should be done immediately after start of stabilization stage for previous release Should be done immediately after start of stabilization stage for previous release
- Create project named `Release 1.X` - Create project named `Release 1.X`
- Add all features and bugs that should be fixed as part of this release into this project - Add all features and bugs that should be fixed as part of this release into this project
### Start of stabilization stage (major releases only) ### 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. Should be done 2 weeks before planned release date. All major features should be finished at this point.
- Create `beta` branch from `develop` - 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 - Bump version and build ID for Android on `beta` branch
### Release preparation stage ### Release preparation stage
Should be done 1 week before release. Release date should be decided at this point. 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 - 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` - - Update downloads counter in `docs/readme.md`
### Release preparation stage ### Release preparation stage
Should be done 1 day before release. At this point beta branch is in full freeze. Should be done 1 day before release. At this point beta branch is in full freeze.
- Merge release preparation PR into `beta` - Merge release preparation PR into `beta`
- Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages - 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 - 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 - 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 - 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 - 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 - 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) - 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) - 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 ### Release publishing phase
Should be done on release date Should be done on release date
- Trigger builds for new release on Ubuntu PPA - Trigger builds for new release on Ubuntu PPA

View File

@ -1,6 +1,7 @@
# Ubuntu PPA # Ubuntu PPA
## Main links ## Main links
- [Team](https://launchpad.net/~vcmi) - [Team](https://launchpad.net/~vcmi)
- [Project](https://launchpad.net/vcmi) - [Project](https://launchpad.net/vcmi)
- [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) - [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi)
@ -14,22 +15,31 @@
## Automatic daily builds process ## Automatic daily builds process
### Code import ### Code import
- Launchpad performs regular (once per few hours) clone of our git repository. - 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. - 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) - If necessary, it is possible to trigger fresh clone immediately (Import Now button)
### Build dependencies ### Build dependencies
- All packages required for building of vcmi are defined in [debian/control](https://github.com/vcmi/vcmi/blob/develop/debian/control) file - 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 - Launchpad will automatically install build dependencies during build
- Dependencies of output .deb package are defined implicitly as dependencies of packages required for build - Dependencies of output .deb package are defined implicitly as dependencies of packages required for build
### Recipe building ### 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) - 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) - 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) ### Recipe content (build settings)
- Version of resulting .deb package is set in recipe content, e.g `{debupstream}+git{revtime}` for daily builds - 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 - 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 - 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` - Branch which is used for build is specified in recipe content, e.g. `lp:vcmi master`
## Workflow for creating a release build ## Workflow for creating a release build
- if necessary, push all required changes including `debian/changelog` update to `vcmi/master` branch - 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. - 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. - 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 - 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 - 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. - If any of builds have failed, open page with build info and check logs.
## People with access ## People with access
- [alexvins](https://github.com/alexvins) (https://launchpad.net/~alexvins)
- [ArseniyShestakov](https://github.com/ArseniyShestakov) (https://launchpad.net/~sxx) - [alexvins](https://github.com/alexvins) (<https://launchpad.net/~alexvins>)
- [IvanSavenko](https://github.com/IvanSavenko) (https://launchpad.net/~saven-ivan) - [ArseniyShestakov](https://github.com/ArseniyShestakov) (<https://launchpad.net/~sxx>)
- (Not member of VCMI, creator of PPA) (https://launchpad.net/~mantas) - [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 ## Format description
``` javascript ```json5
{ {
// Base path of all images in animation. Optional. // Base path of all images in animation. Optional.
// Can be used to avoid using long path to images // 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 ### Replacing a button
This json file will allow replacing .def file for a button with png images. Buttons require following images: 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 1. Active state. Button is active and can be pressed by player
2. Pressed state. Player pressed button but have not released it yet 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 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 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 "basepath" : "interface/MyButton", // all images are located in this directory
"images" : "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. 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 "basepath" : "myTown/myBuilding", // all images are located in this directory
"sequences" : "sequences" :

View File

@ -15,7 +15,7 @@ The limiters take no parameters:
Example: Example:
``` javascript ```json5
"limiters" : [ "SHOOTER_ONLY" ] "limiters" : [ "SHOOTER_ONLY" ]
``` ```
@ -30,7 +30,7 @@ Parameters:
- (optional) bonus sourceType and sourceId in struct - (optional) bonus sourceType and sourceId in struct
- example: (from Adele's bless): - example: (from Adele's bless):
``` javascript ```json5
"limiters" : [ "limiters" : [
{ {
"type" : "HAS_ANOTHER_BONUS_LIMITER", "type" : "HAS_ANOTHER_BONUS_LIMITER",
@ -64,6 +64,7 @@ Parameters:
If parameters is empty, level limiter works as CREATURES_ONLY limiter If parameters is empty, level limiter works as CREATURES_ONLY limiter
Parameters: Parameters:
- Minimal level - Minimal level
- Maximal level - Maximal level
@ -81,14 +82,14 @@ Parameters:
Example: Example:
``` javascript ```json5
"limiters": [ { "limiters": [ {
"type":"CREATURE_TYPE_LIMITER", "type":"CREATURE_TYPE_LIMITER",
"parameters": [ "angel", true ] "parameters": [ "angel", true ]
} ], } ],
``` ```
``` javascript ```json5
"limiters" : [ { "limiters" : [ {
"type" : "CREATURE_TERRAIN_LIMITER", "type" : "CREATURE_TERRAIN_LIMITER",
"parameters" : ["sand"] "parameters" : ["sand"]
@ -112,7 +113,7 @@ and operate on the remaining limiters in that list:
Example: Example:
``` javascript ```json5
"limiters" : [ "limiters" : [
"noneOf", "noneOf",
"IS_UNDEAD", "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) - addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert)
- Example (from Cloak Of The Undead King): - Example (from Cloak Of The Undead King):
```jsonc ```json5
{ {
"type" : "IMPROVED_NECROMANCY", "type" : "IMPROVED_NECROMANCY",
"subtype" : "creature.walkingDead", "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: 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" : { "disruptingRay" : {
"addInfo" : -2, "addInfo" : -2,
"subtype" : "spell.disruptingRay", "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: 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" : { "fortune" : {
"addInfo" : 3, "addInfo" : 3,
"subtype" : "spell.fortune", "subtype" : "spell.fortune",
@ -669,6 +669,7 @@ Affected unit can attack walls during siege battles (Cyclops)
### CATAPULT_EXTRA_SHOTS ### CATAPULT_EXTRA_SHOTS
Defines spell mastery level for spell used by CATAPULT bonus Defines spell mastery level for spell used by CATAPULT bonus
- subtype: affected spell - subtype: affected spell
- val: spell mastery level to use - val: spell mastery level to use

View File

@ -12,25 +12,27 @@ Check the files in *config/heroes/* for additional usage examples.
- Type: Complex - Type: Complex
- Parameters: valPer20, stepSize=1 - Parameters: valPer20, stepSize=1
- Effect: Updates val to - Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
` ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
Example: The following updater will cause a bonus to grow by 6 for every 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. 40 levels. At first level, rounding will cause the bonus to be 0.
` "updater" : {` ```json5
` "parameters" : [ 6, 2 ],` "updater" : {
` "type" : "GROWS_WITH_LEVEL"` "parameters" : [ 6, 2 ],
` }` "type" : "GROWS_WITH_LEVEL"
}
```
Example: The following updater will cause a bonus to grow by 3 for every 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. 20 levels. At first level, rounding will cause the bonus to be 1.
` "updater" : {` ```json5
` "parameters" : [ 3 ],` "updater" : {
` "type" : "GROWS_WITH_LEVEL"` "parameters" : [ 3 ],
` }` "type" : "GROWS_WITH_LEVEL"
}
```
Remarks: Remarks:
@ -42,13 +44,9 @@ Remarks:
## TIMES_HERO_LEVEL ## TIMES_HERO_LEVEL
- Type: Simple - 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 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 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 ## TIMES_STACK_LEVEL
- Type: Simple - Type: Simple
- Effect: Updates val to - Effect: Updates val to `val * stackLevel`
` val * stackLevel`
Usage: Usage:
@ -70,19 +66,17 @@ Remark: The stack level for war machines is 0.
## ARMY_MOVEMENT ## ARMY_MOVEMENT
- Type: Complex - Type: Complex
- Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, - Parameters: basePerSpeed, dividePerSpeed, additionalMultiplier, maxValue
maxValue - Effect: Updates val to `val+= max((floor(basePerSpeed / dividePerSpeed) * additionalMultiplier), maxValue)`
- Effect: Updates val to val+= max((floor(basePerSpeed / - 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).
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: - Example:
` "updater" : {` ```json5
` "parameters" : [ 20, 3, 10, 700 ],` "updater" : {
` "type" : "ARMY_MOVEMENT"` "parameters" : [ 20, 3, 10, 700 ],
` }` "type" : "ARMY_MOVEMENT"
}
```
## BONUS_OWNER_UPDATER ## BONUS_OWNER_UPDATER

View File

@ -3,13 +3,18 @@
Total value of Bonus is calculated using the following: 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: - 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. - 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: - 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. 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. All parameters but type are optional.
``` javascript ```json5
{ {
// Type of the bonus. See Bonus Types for full list // Type of the bonus. See Bonus Types for full list
"type": "BONUS_TYPE", "type": "BONUS_TYPE",
@ -81,7 +81,7 @@ See [Game Identifiers](Game_Identifiers.md) for full list of available identifie
### Example ### Example
``` javascript ```json5
"bonus" : "bonus" :
{ {
"type" : "HATE", "type" : "HATE",

View File

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

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