mirror of
https://github.com/vcmi/vcmi.git
synced 2025-05-29 22:57:49 +02:00
Merge branch 'develop' into video
This commit is contained in:
commit
877f47e37f
.github
AI
BattleAI
Nullkiller
AIGateway.cppAIUtility.cppAIUtility.h
Analyzers
ArmyManager.cppBuildAnalyzer.cppDangerHitMapAnalyzer.cppDangerHitMapAnalyzer.hHeroManager.cppHeroManager.hObjectClusterizer.cpp
Behaviors
BuildingBehavior.cppBuyArmyBehavior.cppCaptureObjectsBehavior.cppDefenceBehavior.cppExplorationBehavior.cppGatherArmyBehavior.cppRecruitHeroBehavior.cppStayAtTownBehavior.cpp
Engine
FuzzyEngines.cppFuzzyHelper.cppNullkiller.cppNullkiller.hPriorityEvaluator.cppPriorityEvaluator.hSettings.cppSettings.h
Goals
AbstractGoal.hAdventureSpellCast.cppExchangeSwapTownHeroes.cppExecuteHeroChain.cppRecruitHero.cppRecruitHero.hStayAtTown.cpp
Helpers
Pathfinding
VCAI
CI
ChangeLog.mdMods/vcmi/Content/config
chinese.jsonczech.jsonenglish.jsongerman.jsonpolish.jsonportuguese.jsonspanish.jsonswedish.jsonukrainian.json
client
config
docs
Readme.md
developers
AI.mdBonus_System.mdBuilding_Android.mdBuilding_Linux.mdBuilding_Windows.mdBuilding_iOS.mdBuilding_macOS.mdCMake.mdCode_Structure.mdCoding_Guidelines.mdConan.mdDevelopment_with_Qt_Creator.mdLogging_API.mdLua_Scripting_System.mdNetworking.mdRMG_Description.mdSerialization.md
maintainers
modders
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@ -24,7 +25,7 @@ Steps to reproduce the behavior:
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
A clear description what is currently happening
|
||||
A clear description what is currently happening
|
||||
|
||||
**Did it work earlier?**
|
||||
If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
|
||||
@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version**
|
||||
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
|
||||
- Version: [VCMI version]
|
||||
|
||||
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
|
||||
- Version: [VCMI version]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
6
.github/workflows/github.yml
vendored
6
.github/workflows/github.yml
vendored
@ -402,3 +402,9 @@ jobs:
|
||||
run: |
|
||||
sudo apt install python3-jstyleson
|
||||
python3 CI/validate_json.py
|
||||
|
||||
- name: Validate Markdown
|
||||
uses: DavidAnson/markdownlint-cli2-action@v18
|
||||
with:
|
||||
config: 'CI/example.markdownlint-cli2.jsonc'
|
||||
globs: '**/*.md'
|
||||
|
@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
|
||||
cast.castEval(state->getServerCallback(), ps.dest);
|
||||
|
||||
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
|
||||
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
|
||||
|
||||
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
|
||||
{
|
||||
@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
|
||||
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
|
||||
}
|
||||
|
||||
for(const auto & unit : allUnits)
|
||||
{
|
||||
if(!unit->isValidTarget(true))
|
||||
@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
|
||||
|
||||
#if BATTLE_TRACE_LEVEL >= 1
|
||||
logAi->trace(
|
||||
"Spell affects %s (%d), dps: %2f",
|
||||
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
||||
unit->getCount(),
|
||||
dpsReduce);
|
||||
// Ensure ps.dest is not empty before accessing the first element
|
||||
if (!ps.dest.empty())
|
||||
{
|
||||
logAi->trace(
|
||||
"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
|
||||
ps.spell->getNameTranslated(),
|
||||
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
|
||||
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
||||
unit->getCount(),
|
||||
dpsReduce,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
|
||||
{
|
||||
std::vector<const battle::Unit *> result;
|
||||
|
||||
for(int i = 0; i < turnOrder.size(); i++)
|
||||
for(int i = 0; i < turnOrder.size(); i++, turn++)
|
||||
{
|
||||
auto & turnQueue = turnOrder[i];
|
||||
HypotheticBattle turnBattle(env.get(), cb);
|
||||
|
@ -34,11 +34,6 @@
|
||||
namespace NKAI
|
||||
{
|
||||
|
||||
// our to enemy strength ratio constants
|
||||
const float SAFE_ATTACK_CONSTANT = 1.1f;
|
||||
const float RETREAT_THRESHOLD = 0.3f;
|
||||
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
|
||||
|
||||
//one thread may be turn of AI and another will be handling a side effect for AI2
|
||||
thread_local CCallback * cb = nullptr;
|
||||
thread_local AIGateway * ai = nullptr;
|
||||
@ -553,7 +548,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
|
||||
double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
|
||||
|
||||
// if we have no towns - things are already bad, so retreat is not an option.
|
||||
if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
|
||||
if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
|
||||
{
|
||||
return BattleAction::makeRetreat(battleState.ourSide);
|
||||
}
|
||||
@ -670,7 +665,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
|
||||
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
|
||||
{
|
||||
bool dangerUnknown = danger == 0;
|
||||
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
|
||||
bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
|
||||
|
||||
answer = !dangerUnknown && !dangerTooHigh;
|
||||
}
|
||||
|
@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
|
||||
return h == rhs.get(true);
|
||||
}
|
||||
|
||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
|
||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
|
||||
{
|
||||
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
|
||||
const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
|
||||
|
||||
if(dangerStrength)
|
||||
{
|
||||
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
|
||||
return heroStrength > dangerStrength * safeAttackRatio;
|
||||
}
|
||||
|
||||
return true; //there's no danger
|
||||
}
|
||||
|
||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
|
||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
|
||||
{
|
||||
return isSafeToVisit(h, h, dangerStrength);
|
||||
return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
|
||||
}
|
||||
|
||||
bool isObjectRemovable(const CGObjectInstance * obj)
|
||||
|
@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
|
||||
const int WOOD_ORE_MINE_PRODUCTION = 2;
|
||||
const int RESOURCE_MINE_PRODUCTION = 1;
|
||||
const int ACTUAL_RESOURCE_COUNT = 7;
|
||||
const int ALLOWED_ROAMING_HEROES = 8;
|
||||
|
||||
//implementation-dependent
|
||||
extern const float SAFE_ATTACK_CONSTANT;
|
||||
extern const int GOLD_RESERVE;
|
||||
|
||||
extern thread_local CCallback * cb;
|
||||
|
||||
@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
|
||||
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
|
||||
|
||||
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
|
||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
|
||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
|
||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
|
||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
|
||||
|
||||
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
|
||||
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "../Engine/Nullkiller.h"
|
||||
#include "../../../CCallback.h"
|
||||
#include "../../../lib/mapObjects/MapObjects.h"
|
||||
#include "../../../lib/IGameSettings.h"
|
||||
#include "../../../lib/GameConstants.h"
|
||||
|
||||
namespace NKAI
|
||||
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
||||
uint64_t armyValue = 0;
|
||||
|
||||
TemporaryArmy newArmyInstance;
|
||||
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
|
||||
|
||||
for(auto bonus : *bonusModifiers)
|
||||
{
|
||||
// army bonuses will change and object bonuses are temporary
|
||||
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
|
||||
{
|
||||
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
|
||||
}
|
||||
}
|
||||
|
||||
while(allowedFactions.size() < alignmentMap.size())
|
||||
{
|
||||
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
||||
auto morale = slot.second->moraleVal();
|
||||
auto multiplier = 1.0f;
|
||||
|
||||
const float BadMoraleChance = 0.083f;
|
||||
const float HighMoraleChance = 0.04f;
|
||||
const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
|
||||
const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
|
||||
|
||||
if(morale < 0)
|
||||
if(morale < 0 && !badMoraleDice.empty())
|
||||
{
|
||||
multiplier += morale * BadMoraleChance;
|
||||
size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
|
||||
multiplier -= 1.0 / badMoraleDice.at(diceIndex);
|
||||
}
|
||||
else if(morale > 0)
|
||||
else if(morale > 0 && !highMoraleDice.empty())
|
||||
{
|
||||
multiplier += morale * HighMoraleChance;
|
||||
size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
|
||||
multiplier += 1.0 / highMoraleDice.at(diceIndex);
|
||||
}
|
||||
|
||||
newValue += multiplier * slot.second->getPower();
|
||||
|
@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
|
||||
for(int upgradeIndex : {1, 0})
|
||||
{
|
||||
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
|
||||
|
||||
if(!vstd::contains(buildings, building))
|
||||
continue; // no such building in town
|
||||
|
||||
@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
|
||||
|
||||
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
|
||||
{
|
||||
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
|
||||
otherBuildings.push_back({BuildingID::HORDE_1});
|
||||
otherBuildings.push_back({BuildingID::HORDE_2});
|
||||
}
|
||||
|
||||
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
|
||||
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
|
||||
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
|
||||
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
|
||||
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
|
||||
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
|
||||
otherBuildings.push_back({ BuildingID::MARKETPLACE });
|
||||
|
||||
for(auto & buildingSet : otherBuildings)
|
||||
{
|
||||
for(auto & buildingID : buildingSet)
|
||||
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
|
||||
|
||||
auto towns = ai->cb->getTownsInfo();
|
||||
|
||||
float economyDevelopmentCost = 0;
|
||||
|
||||
for(const CGTownInstance* town : towns)
|
||||
{
|
||||
logAi->trace("Checking town %s", town->getNameTranslated());
|
||||
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
|
||||
|
||||
requiredResources += developmentInfo.requiredResources;
|
||||
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
|
||||
for(auto building : developmentInfo.toBuild)
|
||||
{
|
||||
if (building.dailyIncome[EGameResID::GOLD] > 0)
|
||||
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
|
||||
}
|
||||
armyCost += developmentInfo.armyCost;
|
||||
|
||||
for(auto bi : developmentInfo.toBuild)
|
||||
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
|
||||
|
||||
updateDailyIncome();
|
||||
|
||||
if(ai->cb->getDate(Date::DAY) == 1)
|
||||
{
|
||||
goldPressure = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
|
||||
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
||||
}
|
||||
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
||||
|
||||
logAi->trace("Gold pressure: %f", goldPressure);
|
||||
}
|
||||
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
||||
logAi->trace("checking %s", info.name);
|
||||
logAi->trace("buildInfo %s", info.toString());
|
||||
|
||||
int highestFort = 0;
|
||||
for (auto twn : ai->cb->getTownsInfo())
|
||||
{
|
||||
highestFort = std::max(highestFort, (int)twn->fortLevel());
|
||||
}
|
||||
|
||||
if(!town->hasBuilt(building))
|
||||
{
|
||||
auto canBuild = ai->cb->canBuildStructure(town, building);
|
||||
@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
||||
prerequisite.baseCreatureID = info.baseCreatureID;
|
||||
prerequisite.prerequisitesCount++;
|
||||
prerequisite.armyCost = info.armyCost;
|
||||
prerequisite.dailyIncome = info.dailyIncome;
|
||||
bool haveSameOrBetterFort = false;
|
||||
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
|
||||
haveSameOrBetterFort = true;
|
||||
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
|
||||
haveSameOrBetterFort = true;
|
||||
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
|
||||
haveSameOrBetterFort = true;
|
||||
if(!haveSameOrBetterFort)
|
||||
prerequisite.dailyIncome = info.dailyIncome;
|
||||
|
||||
return prerequisite;
|
||||
}
|
||||
|
@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
|
||||
|
||||
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
|
||||
}
|
||||
|
||||
if(obj->ID == Obj::TOWN)
|
||||
{
|
||||
auto town = dynamic_cast<const CGTownInstance *>(obj);
|
||||
@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
|
||||
|
||||
newThreat.hero = path.targetHero;
|
||||
newThreat.turn = path.turn();
|
||||
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
|
||||
newThreat.danger = path.getHeroStrength();
|
||||
|
||||
if(newThreat.value() > node.maximumDanger.value())
|
||||
@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
|
||||
|
||||
const auto& info = getTileThreat(tile);
|
||||
|
||||
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
|
||||
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
|
||||
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
|
||||
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
|
||||
}
|
||||
|
||||
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
|
||||
|
@ -22,6 +22,7 @@ struct HitMapInfo
|
||||
|
||||
uint64_t danger;
|
||||
uint8_t turn;
|
||||
float threat;
|
||||
HeroPtr hero;
|
||||
|
||||
HitMapInfo()
|
||||
@ -33,6 +34,7 @@ struct HitMapInfo
|
||||
{
|
||||
danger = 0;
|
||||
turn = 255;
|
||||
threat = 0;
|
||||
hero = HeroPtr();
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
|
||||
|
||||
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
|
||||
{
|
||||
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
|
||||
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
|
||||
}
|
||||
|
||||
void HeroManager::update()
|
||||
@ -108,7 +108,7 @@ void HeroManager::update()
|
||||
for(auto & hero : myHeroes)
|
||||
{
|
||||
scores[hero] = evaluateFightingStrength(hero);
|
||||
knownFightingStrength[hero->id] = hero->getFightingStrength();
|
||||
knownFightingStrength[hero->id] = hero->getHeroStrength();
|
||||
}
|
||||
|
||||
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
|
||||
@ -147,7 +147,10 @@ void HeroManager::update()
|
||||
|
||||
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
|
||||
{
|
||||
return heroRoles.at(hero);
|
||||
if (heroRoles.find(hero) != heroRoles.end())
|
||||
return heroRoles.at(hero);
|
||||
else
|
||||
return HeroRole::SCOUT;
|
||||
}
|
||||
|
||||
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
|
||||
@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
|
||||
return evaluateFightingStrength(hero);
|
||||
}
|
||||
|
||||
bool HeroManager::heroCapReached() const
|
||||
bool HeroManager::heroCapReached(bool includeGarrisoned) const
|
||||
{
|
||||
const bool includeGarnisoned = true;
|
||||
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
|
||||
int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
|
||||
|
||||
return heroCount >= ALLOWED_ROAMING_HEROES
|
||||
|| heroCount >= ai->settings->getMaxRoamingHeroes()
|
||||
return heroCount >= ai->settings->getMaxRoamingHeroes()
|
||||
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|
||||
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
|
||||
}
|
||||
@ -204,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
|
||||
auto cached = knownFightingStrength.find(hero->id);
|
||||
|
||||
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
|
||||
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
|
||||
return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
|
||||
}
|
||||
|
||||
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
|
||||
@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
|
||||
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
|
||||
{
|
||||
const CGHeroInstance * weakestHero = nullptr;
|
||||
auto myHeroes = ai->cb->getHeroesInfo();
|
||||
@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|
||||
|| existingHero->getArmyStrength() >armyLimit
|
||||
|| getHeroRole(existingHero) == HeroRole::MAIN
|
||||
|| existingHero->movementPointsRemaining()
|
||||
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|
||||
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
|
||||
if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
|
||||
{
|
||||
weakestHero = existingHero;
|
||||
}
|
||||
|
@ -56,9 +56,9 @@ public:
|
||||
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
|
||||
float evaluateHero(const CGHeroInstance * hero) const;
|
||||
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
|
||||
bool heroCapReached() const;
|
||||
bool heroCapReached(bool includeGarrisoned = true) const;
|
||||
const CGHeroInstance * findHeroWithGrail() const;
|
||||
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
|
||||
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
|
||||
float getMagicStrength(const CGHeroInstance * hero) const;
|
||||
float getFightingStrengthCached(const CGHeroInstance * hero) const;
|
||||
|
||||
|
@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
|
||||
{
|
||||
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
|
||||
|
||||
blockers = ai->cb->getVisitableObjs(node.coord);
|
||||
if (ai->cb->isVisible(node.coord))
|
||||
blockers = ai->cb->getVisitableObjs(node.coord);
|
||||
|
||||
if(guardPos.valid())
|
||||
if(guardPos.valid() && ai->cb->isVisible(guardPos))
|
||||
{
|
||||
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
|
||||
|
||||
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
|
||||
|
||||
heroesProcessed.insert(path.targetHero);
|
||||
|
||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
|
||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
|
||||
|
||||
if(priority < MIN_PRIORITY)
|
||||
if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
|
||||
continue;
|
||||
else if (priority <= 0)
|
||||
continue;
|
||||
|
||||
ClusterMap::accessor cluster;
|
||||
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
|
||||
|
||||
heroesProcessed.insert(path.targetHero);
|
||||
|
||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
|
||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
|
||||
|
||||
if(priority < MIN_PRIORITY)
|
||||
if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
|
||||
continue;
|
||||
else if (priority <= 0)
|
||||
continue;
|
||||
|
||||
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
||||
|
@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
|
||||
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
|
||||
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
|
||||
|
||||
ai->dangerHitMap->updateHitMap();
|
||||
|
||||
for(auto & developmentInfo : developmentInfos)
|
||||
{
|
||||
for(auto & buildingInfo : developmentInfo.toBuild)
|
||||
bool emergencyDefense = false;
|
||||
uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
|
||||
for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
|
||||
{
|
||||
if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
|
||||
closestThreat = std::min(closestThreat, threat.turn);
|
||||
}
|
||||
for (auto& buildingInfo : developmentInfo.toBuild)
|
||||
{
|
||||
if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
|
||||
{
|
||||
if(buildingInfo.notEnoughRes)
|
||||
if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
|
||||
{
|
||||
if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
|
||||
continue;
|
||||
|
||||
Composition composition;
|
||||
|
||||
composition.addNext(BuildThis(buildingInfo, developmentInfo));
|
||||
composition.addNext(SaveResources(buildingInfo.buildCost));
|
||||
|
||||
tasks.push_back(sptr(composition));
|
||||
}
|
||||
else
|
||||
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
||||
emergencyDefense = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!emergencyDefense)
|
||||
{
|
||||
for (auto& buildingInfo : developmentInfo.toBuild)
|
||||
{
|
||||
if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
|
||||
{
|
||||
if (buildingInfo.notEnoughRes)
|
||||
{
|
||||
if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
|
||||
continue;
|
||||
|
||||
Composition composition;
|
||||
|
||||
composition.addNext(BuildThis(buildingInfo, developmentInfo));
|
||||
composition.addNext(SaveResources(buildingInfo.buildCost));
|
||||
tasks.push_back(sptr(composition));
|
||||
}
|
||||
else
|
||||
{
|
||||
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
Goals::TGoalVec tasks;
|
||||
|
||||
if(ai->cb->getDate(Date::DAY) == 1)
|
||||
return tasks;
|
||||
|
||||
auto heroes = cb->getHeroesInfo();
|
||||
|
||||
if(heroes.empty())
|
||||
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
return tasks;
|
||||
}
|
||||
|
||||
ai->dangerHitMap->updateHitMap();
|
||||
|
||||
for(auto town : cb->getTownsInfo())
|
||||
{
|
||||
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
|
||||
|
||||
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
|
||||
{
|
||||
return tasks;
|
||||
}
|
||||
|
||||
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
|
||||
town,
|
||||
ai->getFreeResources());
|
||||
|
||||
for(const CGHeroInstance * targetHero : heroes)
|
||||
{
|
||||
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
|
||||
{
|
||||
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
|
||||
|
@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
logAi->trace("Path found %s", path.toString());
|
||||
#endif
|
||||
|
||||
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
auto hero = path.targetHero;
|
||||
auto danger = path.getTotalDanger();
|
||||
|
||||
if (hero->getOwner() != nullkiller->playerID)
|
||||
continue;
|
||||
|
||||
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
|
||||
&& (path.getTotalDanger() == 0 || path.turn() > 0)
|
||||
&& path.exchangeCount > 1)
|
||||
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
continue;
|
||||
}
|
||||
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace(
|
||||
|
@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
|
||||
for(auto town : ai->cb->getTownsInfo())
|
||||
{
|
||||
evaluateDefence(tasks, town, ai);
|
||||
//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
|
||||
if (!tasks.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return tasks;
|
||||
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
||||
|
||||
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
|
||||
{
|
||||
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
|
||||
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
|
||||
|
||||
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
||||
if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!threatNode.fastestDanger.hero)
|
||||
{
|
||||
logAi->trace("No threat found for town %s", town->getNameTranslated());
|
||||
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!path.targetHero->canBeMergedWith(*town))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->trace("Can't merge armies of hero %s and town %s",
|
||||
path.targetHero->getObjectName(),
|
||||
town->getObjectName());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
// dismiss creatures we are not able to pick to be able to hide in garrison
|
||||
if(town->garrisonHero
|
||||
|| town->getUpperArmy()->stacksCount() == 0
|
||||
|| path.targetHero->canBeMergedWith(*town)
|
||||
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
|
||||
{
|
||||
tasks.push_back(
|
||||
@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
continue;
|
||||
}
|
||||
|
||||
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
|
||||
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
|
||||
{
|
||||
if(ai->arePathHeroesLocked(path))
|
||||
{
|
||||
@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
}
|
||||
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
|
||||
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
||||
path.targetHero->getObjectName(),
|
||||
town->getObjectName());
|
||||
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
||||
path.targetHero->getObjectName(),
|
||||
town->getObjectName());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if(path.turn() == 0)
|
||||
{
|
||||
@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
|
||||
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
|
||||
{
|
||||
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
|
||||
return;
|
||||
|
||||
if(town->hasBuilt(BuildingID::TAVERN)
|
||||
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
|
||||
{
|
||||
@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
|
||||
}
|
||||
else if(ai->heroManager->heroCapReached())
|
||||
{
|
||||
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
|
||||
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
|
||||
|
||||
if(!heroToDismiss)
|
||||
continue;
|
||||
|
@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
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::PILLAR_OF_FIRE:
|
||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
|
||||
{
|
||||
auto rObj = dynamic_cast<const CRewardableObject*>(obj);
|
||||
if (!rObj->wasScouted(ai->playerID))
|
||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
|
||||
break;
|
||||
}
|
||||
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||
case Obj::MONOLITH_TWO_WAY:
|
||||
case Obj::SUBTERRANEAN_GATE:
|
||||
case Obj::WHIRLPOOL:
|
||||
auto tObj = dynamic_cast<const CGTeleport *>(obj);
|
||||
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
|
||||
{
|
||||
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)
|
||||
auto tObj = dynamic_cast<const CGTeleport*>(obj);
|
||||
for (auto exit : cb->getTeleportChannelExits(tObj->channel))
|
||||
{
|
||||
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;
|
||||
if (exit != tObj->id)
|
||||
{
|
||||
if (!cb->isVisible(cb->getObjInstance(exit)))
|
||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
|
||||
#endif
|
||||
|
||||
if (path.targetHero->getOwner() != ai->playerID)
|
||||
continue;
|
||||
|
||||
if(path.containsHero(hero))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
continue;
|
||||
}
|
||||
|
||||
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ai->arePathHeroesLocked(path))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
}
|
||||
|
||||
auto danger = path.getTotalDanger();
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace(
|
||||
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
continue;
|
||||
}
|
||||
|
||||
auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
|
||||
|
||||
if(heroRole == HeroRole::SCOUT
|
||||
&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
|
||||
|
||||
if(!upgrader->garrisonHero
|
||||
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
|
||||
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
|
||||
|
||||
armyToGetOrBuy.addArmyToBuy(
|
||||
ai->armyManager->toSlotInfo(
|
||||
ai->armyManager->getArmyAvailableToBuy(
|
||||
path.heroArmy,
|
||||
upgrader,
|
||||
ai->getFreeResources(),
|
||||
path.turn())));
|
||||
|
||||
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
|
||||
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
|
||||
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
|
||||
@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
{
|
||||
for(auto hero : cb->getAvailableHeroes(upgrader))
|
||||
{
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
|
||||
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||
|
||||
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
|
||||
&& ai->getFreeGold() >20000
|
||||
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
|
||||
auto danger = path.getTotalDanger();
|
||||
|
||||
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
|
||||
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace(
|
||||
|
@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
auto ourHeroes = ai->heroManager->getHeroRoles();
|
||||
auto minScoreToHireMain = std::numeric_limits<float>::max();
|
||||
int currentArmyValue = 0;
|
||||
|
||||
for(auto hero : ourHeroes)
|
||||
{
|
||||
currentArmyValue += hero.first->getArmyCost();
|
||||
if(hero.second != HeroRole::MAIN)
|
||||
continue;
|
||||
|
||||
@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
minScoreToHireMain = newScore;
|
||||
}
|
||||
}
|
||||
// If we don't have any heros we might want to lower our expectations.
|
||||
if (ourHeroes.empty())
|
||||
minScoreToHireMain = 0;
|
||||
|
||||
const CGHeroInstance* bestHeroToHire = nullptr;
|
||||
const CGTownInstance* bestTownToHireFrom = nullptr;
|
||||
float bestScore = 0;
|
||||
bool haveCapitol = false;
|
||||
|
||||
ai->dangerHitMap->updateHitMap();
|
||||
int treasureSourcesCount = 0;
|
||||
|
||||
for(auto town : towns)
|
||||
{
|
||||
uint8_t closestThreat = UINT8_MAX;
|
||||
for (auto threat : ai->dangerHitMap->getTownThreats(town))
|
||||
{
|
||||
closestThreat = std::min(closestThreat, threat.turn);
|
||||
}
|
||||
//Don't hire a hero where there already is one present
|
||||
if (town->visitingHero && town->garrisonHero)
|
||||
continue;
|
||||
float visitability = 0;
|
||||
for (auto checkHero : ourHeroes)
|
||||
{
|
||||
if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
|
||||
visitability++;
|
||||
}
|
||||
if(ai->heroManager->canRecruitHero(town))
|
||||
{
|
||||
auto availableHeroes = ai->cb->getAvailableHeroes(town);
|
||||
|
||||
for(auto hero : availableHeroes)
|
||||
|
||||
for (auto obj : ai->objectClusterizer->getNearbyObjects())
|
||||
{
|
||||
auto score = ai->heroManager->evaluateHero(hero);
|
||||
|
||||
if(score > minScoreToHireMain)
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int treasureSourcesCount = 0;
|
||||
|
||||
for(auto obj : ai->objectClusterizer->getNearbyObjects())
|
||||
{
|
||||
if((obj->ID == Obj::RESOURCE)
|
||||
if ((obj->ID == Obj::RESOURCE)
|
||||
|| obj->ID == Obj::TREASURE_CHEST
|
||||
|| obj->ID == Obj::CAMPFIRE
|
||||
|| isWeeklyRevisitable(ai, obj)
|
||||
|| obj->ID ==Obj::ARTIFACT)
|
||||
|| obj->ID == Obj::ARTIFACT)
|
||||
{
|
||||
auto tile = obj->visitablePos();
|
||||
auto closestTown = ai->dangerHitMap->getClosestTown(tile);
|
||||
|
||||
if(town == closestTown)
|
||||
if (town == closestTown)
|
||||
treasureSourcesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
|
||||
continue;
|
||||
|
||||
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||
for(auto hero : availableHeroes)
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
|
||||
auto score = ai->heroManager->evaluateHero(hero);
|
||||
if(score > minScoreToHireMain)
|
||||
{
|
||||
score *= score / minScoreToHireMain;
|
||||
}
|
||||
score *= (hero->getArmyCost() + currentArmyValue);
|
||||
if (hero->getFactionID() == town->getFactionID())
|
||||
score *= 1.5;
|
||||
if (vstd::isAlmostZero(visitability))
|
||||
score *= 30 * town->getTownLevel();
|
||||
else
|
||||
score *= town->getTownLevel() / visitability;
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestHeroToHire = hero;
|
||||
bestTownToHireFrom = town;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (town->hasCapitol())
|
||||
haveCapitol = true;
|
||||
}
|
||||
if (bestHeroToHire && bestTownToHireFrom)
|
||||
{
|
||||
if (ai->cb->getHeroesInfo().size() == 0
|
||||
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
|
@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
for(auto town : towns)
|
||||
{
|
||||
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
|
||||
continue;
|
||||
|
||||
ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
|
||||
|
||||
for(auto & path : paths)
|
||||
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
||||
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
|
||||
continue;
|
||||
|
||||
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
|
||||
continue;
|
||||
|
||||
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
||||
if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
||||
{
|
||||
if(path.targetHero->mana == path.targetHero->manaLimit())
|
||||
continue;
|
||||
|
||||
Composition stayAtTown;
|
||||
|
||||
stayAtTown.addNextSequence({
|
||||
|
@ -17,8 +17,7 @@
|
||||
namespace NKAI
|
||||
{
|
||||
|
||||
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
|
||||
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
|
||||
constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
|
||||
|
||||
engineBase::engineBase()
|
||||
{
|
||||
|
@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
|
||||
{
|
||||
objectDanger += evaluateDanger(hero->visitedTown.get());
|
||||
}
|
||||
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
|
||||
}
|
||||
if (objWithID<Obj::TOWN>(dangerousObject))
|
||||
{
|
||||
auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
|
||||
auto hero = town->garrisonHero;
|
||||
|
||||
if (hero)
|
||||
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
|
||||
}
|
||||
|
||||
if(objectDanger)
|
||||
@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
|
||||
{
|
||||
auto fortLevel = town->fortLevel();
|
||||
|
||||
if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
||||
danger += 10000;
|
||||
if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
||||
danger = std::max(danger * 2, danger + 10000);
|
||||
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
|
||||
danger += 4000;
|
||||
danger = std::max(ui64(danger * 1.4), danger + 4000);
|
||||
}
|
||||
|
||||
return danger;
|
||||
|
@ -34,13 +34,12 @@ using namespace Goals;
|
||||
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
|
||||
|
||||
Nullkiller::Nullkiller()
|
||||
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
|
||||
: activeHero(nullptr)
|
||||
, scanDepth(ScanDepth::MAIN_FULL)
|
||||
, useHeroChain(true)
|
||||
, memory(std::make_unique<AIMemory>())
|
||||
{
|
||||
memory = std::make_unique<AIMemory>();
|
||||
settings = std::make_unique<Settings>();
|
||||
|
||||
useObjectGraph = settings->isObjectGraphAllowed();
|
||||
openMap = settings->isOpenMap() || useObjectGraph;
|
||||
}
|
||||
|
||||
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
||||
@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
||||
return false;
|
||||
}
|
||||
|
||||
return cb->getStartInfo()->difficulty >= 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
||||
{
|
||||
this->cb = cb;
|
||||
this->gateway = gateway;
|
||||
|
||||
playerID = gateway->playerID;
|
||||
this->playerID = gateway->playerID;
|
||||
|
||||
if(openMap && !canUseOpenMap(cb, playerID))
|
||||
settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
|
||||
|
||||
if(canUseOpenMap(cb, playerID))
|
||||
{
|
||||
useObjectGraph = settings->isObjectGraphAllowed();
|
||||
openMap = settings->isOpenMap() || useObjectGraph;
|
||||
}
|
||||
else
|
||||
{
|
||||
useObjectGraph = false;
|
||||
openMap = false;
|
||||
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
|
||||
{
|
||||
TGoalVec blockers;
|
||||
|
||||
if (task->asTask()->priority <= 0)
|
||||
return;
|
||||
|
||||
for(auto & item : tasks)
|
||||
{
|
||||
for(auto objid : item.affectedObjects)
|
||||
{
|
||||
if(task == item.task || task->asTask()->isObjectAffected(objid))
|
||||
if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
|
||||
{
|
||||
if(item.task->asTask()->priority >= task->asTask()->priority)
|
||||
return;
|
||||
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
|
||||
return taskptr(*bestTask);
|
||||
}
|
||||
|
||||
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
|
||||
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
|
||||
{
|
||||
TaskPlan taskPlan;
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
|
||||
{
|
||||
auto evaluator = this->priorityEvaluators->acquire();
|
||||
|
||||
for(size_t i = r.begin(); i != r.end(); i++)
|
||||
{
|
||||
auto task = tasks[i];
|
||||
|
||||
if(task->asTask()->priority <= 0)
|
||||
task->asTask()->priority = evaluator->evaluate(task);
|
||||
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
|
||||
task->asTask()->priority = evaluator->evaluate(task, priorityTier);
|
||||
}
|
||||
});
|
||||
|
||||
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
|
||||
if(lockReason != HeroLockedReason::NOT_LOCKED)
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
|
||||
logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
|
||||
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
|
||||
|
||||
const int MAX_DEPTH = 10;
|
||||
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
|
||||
|
||||
resetAiState();
|
||||
|
||||
Goals::TGoalVec bestTasks;
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
float totalHeroStrength = 0;
|
||||
int totalTownLevel = 0;
|
||||
for (auto heroInfo : cb->getHeroesInfo())
|
||||
{
|
||||
totalHeroStrength += heroInfo->getTotalStrength();
|
||||
}
|
||||
for (auto townInfo : cb->getTownsInfo())
|
||||
{
|
||||
totalTownLevel += townInfo->getTownLevel();
|
||||
}
|
||||
logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
|
||||
#endif
|
||||
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
|
||||
|
||||
Goals::TTask bestTask = taskptr(Goals::Invalid());
|
||||
|
||||
for(;i <= settings->getMaxPass(); i++)
|
||||
while(true)
|
||||
{
|
||||
bestTasks.clear();
|
||||
|
||||
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
||||
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
|
||||
decompose(bestTasks, sptr(BuildingBehavior()), 1);
|
||||
|
||||
bestTask = choseBestTask(bestTasks);
|
||||
|
||||
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
|
||||
if(bestTask->priority > 0)
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
|
||||
#endif
|
||||
if(!executeTask(bestTask))
|
||||
return;
|
||||
|
||||
@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
|
||||
}
|
||||
}
|
||||
|
||||
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
||||
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
|
||||
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
|
||||
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
|
||||
@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
|
||||
if(!isOpenMap())
|
||||
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
|
||||
|
||||
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
|
||||
TTaskVec selectedTasks;
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
int prioOfTask = 0;
|
||||
#endif
|
||||
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
|
||||
{
|
||||
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
prioOfTask = prio;
|
||||
#endif
|
||||
selectedTasks = buildPlan(bestTasks, prio);
|
||||
if (!selectedTasks.empty() || settings->isUseFuzzy())
|
||||
break;
|
||||
}
|
||||
|
||||
auto selectedTasks = buildPlan(bestTasks);
|
||||
std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b)
|
||||
{
|
||||
return a->priority > b->priority;
|
||||
});
|
||||
|
||||
logAi->debug("Decision madel in %ld", timeElapsed(start));
|
||||
|
||||
@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
|
||||
bestTask->priority);
|
||||
}
|
||||
|
||||
if(bestTask->priority < MIN_PRIORITY)
|
||||
if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
|
||||
{
|
||||
auto heroes = cb->getHeroesInfo();
|
||||
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
|
||||
@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
|
||||
#endif
|
||||
if(!executeTask(bestTask))
|
||||
{
|
||||
if(hasAnySuccess)
|
||||
@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
hasAnySuccess = true;
|
||||
}
|
||||
|
||||
hasAnySuccess |= handleTrading();
|
||||
|
||||
if(!hasAnySuccess)
|
||||
{
|
||||
logAi->trace("Nothing was done this turn. Ending turn.");
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
totalHeroStrength = 0;
|
||||
totalTownLevel = 0;
|
||||
for (auto heroInfo : cb->getHeroesInfo())
|
||||
{
|
||||
totalHeroStrength += heroInfo->getTotalStrength();
|
||||
}
|
||||
for (auto townInfo : cb->getTownsInfo())
|
||||
{
|
||||
totalTownLevel += townInfo->getTownLevel();
|
||||
}
|
||||
logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
|
||||
lockedResources += res;
|
||||
}
|
||||
|
||||
bool Nullkiller::handleTrading()
|
||||
{
|
||||
bool haveTraded = false;
|
||||
bool shouldTryToTrade = true;
|
||||
int marketId = -1;
|
||||
for (auto town : cb->getTownsInfo())
|
||||
{
|
||||
if (town->hasBuiltSomeTradeBuilding())
|
||||
{
|
||||
marketId = town->id;
|
||||
}
|
||||
}
|
||||
if (marketId == -1)
|
||||
return false;
|
||||
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
|
||||
{
|
||||
if (const auto* m = dynamic_cast<const IMarket*>(obj))
|
||||
{
|
||||
while (shouldTryToTrade)
|
||||
{
|
||||
shouldTryToTrade = false;
|
||||
buildAnalyzer->update();
|
||||
TResources required = buildAnalyzer->getTotalResourcesRequired();
|
||||
TResources income = buildAnalyzer->getDailyIncome();
|
||||
TResources available = cb->getResourceAmount();
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->debug("Available %s", available.toString());
|
||||
logAi->debug("Required %s", required.toString());
|
||||
#endif
|
||||
int mostWanted = -1;
|
||||
int mostExpendable = -1;
|
||||
float minRatio = std::numeric_limits<float>::max();
|
||||
float maxRatio = std::numeric_limits<float>::min();
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
if (required[i] <= 0)
|
||||
continue;
|
||||
float ratio = static_cast<float>(available[i]) / required[i];
|
||||
|
||||
if (ratio < minRatio) {
|
||||
minRatio = ratio;
|
||||
mostWanted = i;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
float ratio = available[i];
|
||||
if (required[i] > 0)
|
||||
ratio = static_cast<float>(available[i]) / required[i];
|
||||
else
|
||||
ratio = available[i];
|
||||
|
||||
bool okToSell = false;
|
||||
|
||||
if (i == GameResID::GOLD)
|
||||
{
|
||||
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
|
||||
okToSell = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (required[i] <= 0 && income[i] > 0)
|
||||
okToSell = true;
|
||||
}
|
||||
|
||||
if (ratio > maxRatio && okToSell) {
|
||||
maxRatio = ratio;
|
||||
mostExpendable = i;
|
||||
}
|
||||
}
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
|
||||
#endif
|
||||
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
|
||||
return false;
|
||||
|
||||
int toGive;
|
||||
int toGet;
|
||||
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
|
||||
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
|
||||
//TODO trade only as much as needed
|
||||
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
|
||||
{
|
||||
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
|
||||
#endif
|
||||
haveTraded = true;
|
||||
shouldTryToTrade = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return haveTraded;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -120,13 +120,14 @@ public:
|
||||
ScanDepth getScanDepth() const { return scanDepth; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
||||
bool handleTrading();
|
||||
|
||||
private:
|
||||
void resetAiState();
|
||||
void updateAiState(int pass, bool fast = false);
|
||||
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
|
||||
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
|
||||
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
|
||||
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
|
||||
bool executeTask(Goals::TTask task);
|
||||
bool areAffectedObjectsPresent(Goals::TTask task) const;
|
||||
HeroRole getTaskRole(Goals::TTask task) const;
|
||||
|
@ -15,6 +15,8 @@
|
||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||
#include "../../../lib/mapObjects/MapObjects.h"
|
||||
#include "../../../lib/mapping/CMapDefines.h"
|
||||
#include "../../../lib/RoadHandler.h"
|
||||
#include "../../../lib/CCreatureHandler.h"
|
||||
#include "../../../lib/VCMI_Lib.h"
|
||||
#include "../../../lib/StartInfo.h"
|
||||
@ -33,11 +35,9 @@
|
||||
namespace NKAI
|
||||
{
|
||||
|
||||
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
|
||||
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
|
||||
const float MIN_CRITICAL_VALUE = 2.0f;
|
||||
constexpr float MIN_CRITICAL_VALUE = 2.0f;
|
||||
|
||||
EvaluationContext::EvaluationContext(const Nullkiller * ai)
|
||||
EvaluationContext::EvaluationContext(const Nullkiller* ai)
|
||||
: movementCost(0.0),
|
||||
manaCost(0),
|
||||
danger(0),
|
||||
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
|
||||
heroRole(HeroRole::SCOUT),
|
||||
turn(0),
|
||||
strategicalValue(0),
|
||||
conquestValue(0),
|
||||
evaluator(ai),
|
||||
enemyHeroDangerRatio(0),
|
||||
armyGrowth(0)
|
||||
threat(0),
|
||||
armyGrowth(0),
|
||||
armyInvolvement(0),
|
||||
defenseValue(0),
|
||||
isDefend(false),
|
||||
threatTurns(INT_MAX),
|
||||
involvesSailing(false),
|
||||
isTradeBuilding(false),
|
||||
isExchange(false),
|
||||
isArmyUpgrade(false),
|
||||
isHero(false),
|
||||
isEnemy(false),
|
||||
explorePriority(0)
|
||||
{
|
||||
}
|
||||
|
||||
@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
|
||||
auto creature = creLevel.second.back().toCreature();
|
||||
auto creaturesAreFree = creature->getLevel() == 1;
|
||||
if(!creaturesAreFree)
|
||||
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
|
||||
cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
|
||||
|
||||
switch(art->aClass)
|
||||
{
|
||||
case CArtifact::EartClass::ART_TREASURE:
|
||||
//FALL_THROUGH
|
||||
case CArtifact::EartClass::ART_MINOR:
|
||||
classValue = 1000;
|
||||
break;
|
||||
@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
|
||||
case Obj::CREATURE_GENERATOR3:
|
||||
case Obj::CREATURE_GENERATOR4:
|
||||
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
|
||||
case Obj::SPELL_SCROLL:
|
||||
//FALL_THROUGH
|
||||
case Obj::ARTIFACT:
|
||||
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
|
||||
case Obj::HERO:
|
||||
@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
|
||||
float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
|
||||
{
|
||||
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
|
||||
}
|
||||
@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
|
||||
return 0;
|
||||
}
|
||||
|
||||
float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
|
||||
{
|
||||
if (!target)
|
||||
return 0;
|
||||
if (target->getOwner() == ai->playerID)
|
||||
return 0;
|
||||
switch (target->ID)
|
||||
{
|
||||
case Obj::TOWN:
|
||||
{
|
||||
if (ai->buildAnalyzer->getDevelopmentInfo().empty())
|
||||
return 10.0f;
|
||||
|
||||
auto town = dynamic_cast<const CGTownInstance*>(target);
|
||||
|
||||
if (town->getOwner() == ai->playerID)
|
||||
{
|
||||
auto armyIncome = townArmyGrowth(town);
|
||||
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
|
||||
|
||||
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
|
||||
}
|
||||
|
||||
auto fortLevel = town->fortLevel();
|
||||
auto booster = 1.0f;
|
||||
|
||||
if (town->hasCapitol())
|
||||
return booster * 1.5;
|
||||
|
||||
if (fortLevel < CGTownInstance::CITADEL)
|
||||
return booster * (town->hasFort() ? 1.0 : 0.8);
|
||||
else
|
||||
return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
|
||||
}
|
||||
|
||||
case Obj::HERO:
|
||||
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
|
||||
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
|
||||
: 0;
|
||||
|
||||
case Obj::KEYMASTER:
|
||||
return 0.6f;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
|
||||
{
|
||||
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
|
||||
@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
|
||||
|
||||
for(auto stack : army->Slots())
|
||||
{
|
||||
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
|
||||
value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
|
||||
}
|
||||
|
||||
return value;
|
||||
@ -786,7 +851,9 @@ public:
|
||||
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
|
||||
|
||||
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
|
||||
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
|
||||
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
|
||||
evaluationContext.isExchange = true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -804,6 +871,7 @@ public:
|
||||
|
||||
evaluationContext.armyReward += upgradeValue;
|
||||
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
|
||||
evaluationContext.isArmyUpgrade = true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -818,22 +886,46 @@ public:
|
||||
int tilesDiscovered = task->value;
|
||||
|
||||
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
|
||||
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
|
||||
{
|
||||
switch (obj->ID.num)
|
||||
{
|
||||
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||
case Obj::MONOLITH_TWO_WAY:
|
||||
case Obj::SUBTERRANEAN_GATE:
|
||||
evaluationContext.explorePriority = 1;
|
||||
break;
|
||||
case Obj::REDWOOD_OBSERVATORY:
|
||||
case Obj::PILLAR_OF_FIRE:
|
||||
evaluationContext.explorePriority = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
|
||||
evaluationContext.explorePriority = 1;
|
||||
if (evaluationContext.explorePriority == 0)
|
||||
evaluationContext.explorePriority = 3;
|
||||
}
|
||||
};
|
||||
|
||||
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
|
||||
{
|
||||
public:
|
||||
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
|
||||
void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
|
||||
{
|
||||
if(task->goalType != Goals::STAY_AT_TOWN)
|
||||
if (task->goalType != Goals::STAY_AT_TOWN)
|
||||
return;
|
||||
|
||||
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
|
||||
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
|
||||
|
||||
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
|
||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
|
||||
evaluationContext.movementCost += stayAtTown.getMovementWasted();
|
||||
if (evaluationContext.armyReward == 0)
|
||||
evaluationContext.isDefend = true;
|
||||
else
|
||||
{
|
||||
evaluationContext.movementCost += stayAtTown.getMovementWasted();
|
||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
|
||||
if(enemyDanger.danger)
|
||||
{
|
||||
auto dangerRatio = enemyDanger.danger / (double)ourStrength;
|
||||
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
|
||||
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
|
||||
|
||||
if(isAI)
|
||||
{
|
||||
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
|
||||
}
|
||||
|
||||
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
|
||||
vstd::amax(evaluationContext.threat, enemyDanger.threat);
|
||||
}
|
||||
}
|
||||
|
||||
@ -896,6 +981,10 @@ public:
|
||||
else
|
||||
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
|
||||
|
||||
evaluationContext.defenseValue = town->fortLevel();
|
||||
evaluationContext.isDefend = true;
|
||||
evaluationContext.threatTurns = treat.turn;
|
||||
|
||||
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
|
||||
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
|
||||
}
|
||||
@ -926,6 +1015,8 @@ public:
|
||||
for(auto & node : path.nodes)
|
||||
{
|
||||
vstd::amax(costsPerHero[node.targetHero], node.cost);
|
||||
if (node.layer == EPathfindingLayer::SAIL)
|
||||
evaluationContext.involvesSailing = true;
|
||||
}
|
||||
|
||||
for(auto pair : costsPerHero)
|
||||
@ -952,10 +1043,18 @@ public:
|
||||
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
|
||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
|
||||
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
|
||||
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||
if (target->ID == Obj::HERO)
|
||||
evaluationContext.isHero = true;
|
||||
if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
|
||||
evaluationContext.isEnemy = true;
|
||||
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
|
||||
evaluationContext.armyInvolvement += army->getArmyCost();
|
||||
if(evaluationContext.danger > 0)
|
||||
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
|
||||
}
|
||||
|
||||
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
|
||||
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
|
||||
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
|
||||
vstd::amax(evaluationContext.turn, path.turn());
|
||||
}
|
||||
@ -996,6 +1095,7 @@ public:
|
||||
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
|
||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
|
||||
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
|
||||
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
|
||||
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
|
||||
evaluationContext.movementCost += objInfo.second.movementCost / boost;
|
||||
@ -1021,6 +1121,14 @@ public:
|
||||
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
|
||||
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
|
||||
|
||||
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
|
||||
for (auto obj : swapCommand.getAffectedObjects())
|
||||
{
|
||||
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
|
||||
}
|
||||
if (garrisonHero)
|
||||
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
|
||||
|
||||
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
|
||||
{
|
||||
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
|
||||
@ -1029,6 +1137,9 @@ public:
|
||||
evaluationContext.movementCost += mpLeft;
|
||||
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
|
||||
evaluationContext.heroRole = defenderRole;
|
||||
evaluationContext.isDefend = true;
|
||||
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
|
||||
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1072,8 +1183,14 @@ public:
|
||||
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
|
||||
evaluationContext.heroRole = HeroRole::MAIN;
|
||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
|
||||
evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD];
|
||||
int32_t cost = bi.buildCost[EGameResID::GOLD];
|
||||
evaluationContext.goldCost += cost;
|
||||
evaluationContext.closestWayRatio = 1;
|
||||
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
|
||||
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
|
||||
evaluationContext.isTradeBuilding = true;
|
||||
|
||||
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
|
||||
|
||||
if(bi.creatureID != CreatureID::NONE)
|
||||
{
|
||||
@ -1100,7 +1217,18 @@ public:
|
||||
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
|
||||
{
|
||||
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
|
||||
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
|
||||
{
|
||||
evaluationContext.armyInvolvement += hero->getArmyCost();
|
||||
}
|
||||
}
|
||||
int sameTownBonus = 0;
|
||||
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
|
||||
{
|
||||
if (buildThis.town->getFaction() == town->getFaction())
|
||||
sameTownBonus += town->getTownLevel();
|
||||
}
|
||||
evaluationContext.armyReward *= sameTownBonus;
|
||||
|
||||
if(evaluationContext.goldReward)
|
||||
{
|
||||
@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
||||
for(auto subgoal : parts)
|
||||
{
|
||||
context.goldCost += subgoal->goldCost;
|
||||
context.buildingCost += subgoal->buildingCost;
|
||||
|
||||
for(auto builder : evaluationContextBuilders)
|
||||
{
|
||||
@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
||||
return context;
|
||||
}
|
||||
|
||||
float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
{
|
||||
auto evaluationContext = buildEvaluationContext(task);
|
||||
|
||||
@ -1185,36 +1314,256 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
|
||||
double result = 0;
|
||||
|
||||
try
|
||||
if (ai->settings->isUseFuzzy())
|
||||
{
|
||||
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
||||
heroRoleVariable->setValue(evaluationContext.heroRole);
|
||||
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
||||
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
||||
goldRewardVariable->setValue(goldRewardPerTurn);
|
||||
armyRewardVariable->setValue(evaluationContext.armyReward);
|
||||
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
||||
skillRewardVariable->setValue(evaluationContext.skillReward);
|
||||
dangerVariable->setValue(evaluationContext.danger);
|
||||
rewardTypeVariable->setValue(rewardType);
|
||||
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
||||
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
||||
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
|
||||
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
|
||||
turnVariable->setValue(evaluationContext.turn);
|
||||
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
|
||||
float fuzzyResult = 0;
|
||||
try
|
||||
{
|
||||
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
||||
heroRoleVariable->setValue(evaluationContext.heroRole);
|
||||
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
||||
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
||||
goldRewardVariable->setValue(goldRewardPerTurn);
|
||||
armyRewardVariable->setValue(evaluationContext.armyReward);
|
||||
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
||||
skillRewardVariable->setValue(evaluationContext.skillReward);
|
||||
dangerVariable->setValue(evaluationContext.danger);
|
||||
rewardTypeVariable->setValue(rewardType);
|
||||
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
||||
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
||||
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
|
||||
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
|
||||
turnVariable->setValue(evaluationContext.turn);
|
||||
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
|
||||
|
||||
engine->process();
|
||||
engine->process();
|
||||
|
||||
result = value->getValue();
|
||||
fuzzyResult = value->getValue();
|
||||
}
|
||||
catch (fl::Exception& fe)
|
||||
{
|
||||
logAi->error("evaluate VisitTile: %s", fe.getWhat());
|
||||
}
|
||||
result = fuzzyResult;
|
||||
}
|
||||
catch(fl::Exception & fe)
|
||||
else
|
||||
{
|
||||
logAi->error("evaluate VisitTile: %s", fe.getWhat());
|
||||
float score = 0;
|
||||
float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25;
|
||||
|
||||
bool arriveNextWeek = false;
|
||||
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
|
||||
arriveNextWeek = true;
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
|
||||
priorityTier,
|
||||
task->toString(),
|
||||
evaluationContext.armyLossPersentage,
|
||||
(int)evaluationContext.turn,
|
||||
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
evaluationContext.armyGrowth,
|
||||
evaluationContext.skillReward,
|
||||
evaluationContext.danger,
|
||||
evaluationContext.threatTurns,
|
||||
evaluationContext.threat,
|
||||
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
|
||||
evaluationContext.strategicalValue,
|
||||
evaluationContext.conquestValue,
|
||||
evaluationContext.closestWayRatio,
|
||||
evaluationContext.enemyHeroDangerRatio,
|
||||
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("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("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,
|
||||
@ -1223,9 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
evaluationContext.armyGrowth,
|
||||
evaluationContext.skillReward,
|
||||
evaluationContext.danger,
|
||||
evaluationContext.threatTurns,
|
||||
evaluationContext.threat,
|
||||
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
|
||||
evaluationContext.strategicalValue,
|
||||
evaluationContext.conquestValue,
|
||||
evaluationContext.closestWayRatio,
|
||||
evaluationContext.enemyHeroDangerRatio,
|
||||
result);
|
||||
|
@ -41,6 +41,7 @@ public:
|
||||
float getResourceRequirementStrength(int resType) const;
|
||||
float getResourceRequirementStrength(const TResources & res) const;
|
||||
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
|
||||
float getConquestValue(const CGObjectInstance* target) const;
|
||||
float getTotalResourceRequirementStrength(int resType) const;
|
||||
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
|
||||
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
|
||||
@ -48,7 +49,7 @@ public:
|
||||
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
|
||||
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
|
||||
uint64_t townArmyGrowth(const CGTownInstance * town) const;
|
||||
uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
|
||||
float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
|
||||
};
|
||||
|
||||
struct DLL_EXPORT EvaluationContext
|
||||
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
|
||||
int32_t goldCost;
|
||||
float skillReward;
|
||||
float strategicalValue;
|
||||
float conquestValue;
|
||||
HeroRole heroRole;
|
||||
uint8_t turn;
|
||||
RewardEvaluator evaluator;
|
||||
float enemyHeroDangerRatio;
|
||||
float threat;
|
||||
float armyInvolvement;
|
||||
int defenseValue;
|
||||
bool isDefend;
|
||||
int threatTurns;
|
||||
TResources buildingCost;
|
||||
bool involvesSailing;
|
||||
bool isTradeBuilding;
|
||||
bool isExchange;
|
||||
bool isArmyUpgrade;
|
||||
bool isHero;
|
||||
bool isEnemy;
|
||||
int explorePriority;
|
||||
|
||||
EvaluationContext(const Nullkiller * ai);
|
||||
|
||||
@ -91,7 +106,20 @@ public:
|
||||
~PriorityEvaluator();
|
||||
void initVisitTile();
|
||||
|
||||
float evaluate(Goals::TSubgoal task);
|
||||
float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
|
||||
|
||||
enum PriorityTier : int32_t
|
||||
{
|
||||
BUILDINGS = 0,
|
||||
INSTAKILL,
|
||||
INSTADEFEND,
|
||||
KILL,
|
||||
UPGRADE,
|
||||
HIGH_PRIO_EXPLORE,
|
||||
HUNTER_GATHER,
|
||||
LOW_PRIO_EXPLORE,
|
||||
DEFEND
|
||||
};
|
||||
|
||||
private:
|
||||
const Nullkiller * ai;
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include <limits>
|
||||
|
||||
#include "Settings.h"
|
||||
|
||||
#include "../../../lib/constants/StringConstants.h"
|
||||
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||
@ -22,56 +24,39 @@
|
||||
|
||||
namespace NKAI
|
||||
{
|
||||
Settings::Settings()
|
||||
Settings::Settings(int difficultyLevel)
|
||||
: maxRoamingHeroes(8),
|
||||
mainHeroTurnDistanceLimit(10),
|
||||
scoutHeroTurnDistanceLimit(5),
|
||||
maxGoldPressure(0.3f),
|
||||
maxGoldPressure(0.3f),
|
||||
retreatThresholdRelative(0.3),
|
||||
retreatThresholdAbsolute(10000),
|
||||
safeAttackRatio(1.1),
|
||||
maxpass(10),
|
||||
pathfinderBucketsCount(1),
|
||||
pathfinderBucketSize(32),
|
||||
allowObjectGraph(true),
|
||||
useTroopsFromGarrisons(false),
|
||||
openMap(true)
|
||||
openMap(true),
|
||||
useFuzzy(false)
|
||||
{
|
||||
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
||||
const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
|
||||
const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
||||
const JsonNode & node = rootNode[difficultyName];
|
||||
|
||||
if(node.Struct()["maxRoamingHeroes"].isNumber())
|
||||
{
|
||||
maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
|
||||
{
|
||||
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
|
||||
{
|
||||
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["maxpass"].isNumber())
|
||||
{
|
||||
maxpass = node.Struct()["maxpass"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["maxGoldPressure"].isNumber())
|
||||
{
|
||||
maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
|
||||
}
|
||||
|
||||
if(!node.Struct()["allowObjectGraph"].isNull())
|
||||
{
|
||||
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
|
||||
}
|
||||
|
||||
if(!node.Struct()["openMap"].isNull())
|
||||
{
|
||||
openMap = node.Struct()["openMap"].Bool();
|
||||
}
|
||||
|
||||
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
|
||||
{
|
||||
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
|
||||
}
|
||||
maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
|
||||
mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
|
||||
scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
|
||||
maxpass = node["maxpass"].Integer();
|
||||
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
|
||||
pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
|
||||
maxGoldPressure = node["maxGoldPressure"].Float();
|
||||
retreatThresholdRelative = node["retreatThresholdRelative"].Float();
|
||||
retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
|
||||
safeAttackRatio = node["safeAttackRatio"].Float();
|
||||
allowObjectGraph = node["allowObjectGraph"].Bool();
|
||||
openMap = node["openMap"].Bool();
|
||||
useFuzzy = node["useFuzzy"].Bool();
|
||||
useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
|
||||
}
|
||||
}
|
||||
|
@ -25,21 +25,33 @@ namespace NKAI
|
||||
int mainHeroTurnDistanceLimit;
|
||||
int scoutHeroTurnDistanceLimit;
|
||||
int maxpass;
|
||||
int pathfinderBucketsCount;
|
||||
int pathfinderBucketSize;
|
||||
float maxGoldPressure;
|
||||
float retreatThresholdRelative;
|
||||
float retreatThresholdAbsolute;
|
||||
float safeAttackRatio;
|
||||
bool allowObjectGraph;
|
||||
bool useTroopsFromGarrisons;
|
||||
bool openMap;
|
||||
bool useFuzzy;
|
||||
|
||||
public:
|
||||
Settings();
|
||||
explicit Settings(int difficultyLevel);
|
||||
|
||||
int getMaxPass() const { return maxpass; }
|
||||
float getMaxGoldPressure() const { return maxGoldPressure; }
|
||||
float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
|
||||
float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
|
||||
float getSafeAttackRatio() const { return safeAttackRatio; }
|
||||
int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
|
||||
int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
|
||||
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
||||
int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
|
||||
int getPathfinderBucketSize() const { return pathfinderBucketSize; }
|
||||
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
||||
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
bool isUseFuzzy() const { return useFuzzy; }
|
||||
};
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ namespace Goals
|
||||
bool isAbstract; SETTER(bool, isAbstract)
|
||||
int value; SETTER(int, value)
|
||||
ui64 goldCost; SETTER(ui64, goldCost)
|
||||
TResources buildingCost; SETTER(TResources, buildingCost)
|
||||
int resID; SETTER(int, resID)
|
||||
int objid; SETTER(int, objid)
|
||||
int aid; SETTER(int, aid)
|
||||
|
@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
|
||||
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
|
||||
}
|
||||
|
||||
if (hero->inTownGarrison)
|
||||
ai->myCb->swapGarrisonHero(hero->visitedTown);
|
||||
|
||||
auto wait = cb->waitTillRealize;
|
||||
|
||||
cb->waitTillRealize = true;
|
||||
|
@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
|
||||
|
||||
if(!town->garrisonHero)
|
||||
{
|
||||
while(upperArmy->stacksCount() != 0)
|
||||
if (!garrisonHero->canBeMergedWith(*town))
|
||||
{
|
||||
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
||||
while (upperArmy->stacksCount() != 0)
|
||||
{
|
||||
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
|
||||
{
|
||||
hero = path.targetHero;
|
||||
tile = path.targetTile();
|
||||
closestWayRatio = 1;
|
||||
|
||||
if(obj)
|
||||
{
|
||||
@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
|
||||
|
||||
ai->nullkiller->setActive(chainPath.targetHero, tile);
|
||||
ai->nullkiller->setTargetObject(objid);
|
||||
ai->nullkiller->objectClusterizer->reset();
|
||||
|
||||
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
|
||||
|
||||
|
@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
|
||||
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
|
||||
|
||||
ai->nullkiller->heroManager->update();
|
||||
ai->nullkiller->objectClusterizer->reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ namespace Goals
|
||||
}
|
||||
|
||||
std::string toString() const override;
|
||||
const CGHeroInstance* getHero() const override { return heroToBuy; }
|
||||
void accept(AIGateway * ai) override;
|
||||
};
|
||||
}
|
||||
|
@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
|
||||
{
|
||||
return "Stay at town " + town->getNameTranslated()
|
||||
+ " hero " + hero->getNameTranslated()
|
||||
+ ", mana: " + std::to_string(hero->mana);
|
||||
+ ", mana: " + std::to_string(hero->mana)
|
||||
+ " / " + std::to_string(hero->manaLimit());
|
||||
}
|
||||
|
||||
void StayAtTown::accept(AIGateway * ai)
|
||||
{
|
||||
if(hero->visitedTown != town)
|
||||
{
|
||||
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
|
||||
}
|
||||
|
||||
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
|
||||
continue;
|
||||
}
|
||||
|
||||
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
|
||||
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
|
||||
{
|
||||
bestGoal = goal;
|
||||
bestValue = ourValue;
|
||||
|
@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
|
||||
|
||||
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
|
||||
|
||||
AISharedStorage::AISharedStorage(int3 sizes)
|
||||
AISharedStorage::AISharedStorage(int3 sizes, int numChains)
|
||||
{
|
||||
if(!shared){
|
||||
shared.reset(new boost::multi_array<AIPathNode, 4>(
|
||||
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
|
||||
boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
|
||||
|
||||
nodes = shared;
|
||||
|
||||
foreach_tile_pos([&](const int3 & pos)
|
||||
{
|
||||
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
|
||||
for(auto i = 0; i < numChains; i++)
|
||||
{
|
||||
auto & node = get(pos)[i];
|
||||
|
||||
@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
|
||||
}
|
||||
}
|
||||
|
||||
int AINodeStorage::getBucketCount() const
|
||||
{
|
||||
return ai->settings->getPathfinderBucketsCount();
|
||||
}
|
||||
|
||||
int AINodeStorage::getBucketSize() const
|
||||
{
|
||||
return ai->settings->getPathfinderBucketSize();
|
||||
}
|
||||
|
||||
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
|
||||
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
|
||||
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
|
||||
{
|
||||
accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
|
||||
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
|
||||
@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
||||
const EPathfindingLayer layer,
|
||||
const ChainActor * actor)
|
||||
{
|
||||
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
|
||||
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
|
||||
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
|
||||
int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
|
||||
auto chains = nodes.get(pos);
|
||||
|
||||
if(blocked(pos, layer))
|
||||
@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
|
||||
for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
|
||||
{
|
||||
AIPathNode & node = chains[i + bucketOffset];
|
||||
|
||||
@ -486,8 +496,8 @@ public:
|
||||
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
|
||||
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
|
||||
{
|
||||
existingChains.reserve(AIPathfinding::NUM_CHAINS);
|
||||
newChains.reserve(AIPathfinding::NUM_CHAINS);
|
||||
existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||
newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||
}
|
||||
|
||||
void execute(const tbb::blocked_range<size_t>& r)
|
||||
@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
|
||||
if(node->action == EPathNodeAction::BATTLE
|
||||
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|
||||
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|
||||
|| node->action == EPathNodeAction::DISEMBARK
|
||||
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
|
||||
{
|
||||
continue;
|
||||
@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
|
||||
// do not allow our own heroes in garrison to act on map
|
||||
if(hero.first->getOwner() == ai->playerID
|
||||
&& hero.first->inTownGarrison
|
||||
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
|
||||
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetTown->visitingHero
|
||||
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|
||||
|| targetTown->getUpperArmy()->stacksCount()))
|
||||
continue;
|
||||
|
||||
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
|
||||
|
||||
if(nodeOptional)
|
||||
@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
|
||||
path.heroArmy = node.actor->creatureSet;
|
||||
path.armyLoss = node.armyLoss;
|
||||
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
|
||||
for (auto pathNode : path.nodes)
|
||||
{
|
||||
path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
|
||||
}
|
||||
|
||||
if(path.targetObjectDanger > 0)
|
||||
{
|
||||
@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
|
||||
|
||||
uint64_t AIPath::getHeroStrength() const
|
||||
{
|
||||
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
||||
return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
||||
}
|
||||
|
||||
uint64_t AIPath::getTotalDanger() const
|
||||
|
@ -29,9 +29,6 @@ namespace NKAI
|
||||
{
|
||||
namespace AIPathfinding
|
||||
{
|
||||
const int BUCKET_COUNT = 3;
|
||||
const int BUCKET_SIZE = 7;
|
||||
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
|
||||
const int CHAIN_MAX_DEPTH = 4;
|
||||
}
|
||||
|
||||
@ -157,7 +154,7 @@ public:
|
||||
static boost::mutex locker;
|
||||
static uint32_t version;
|
||||
|
||||
AISharedStorage(int3 mapSize);
|
||||
AISharedStorage(int3 sizes, int numChains);
|
||||
~AISharedStorage();
|
||||
|
||||
STRONG_INLINE
|
||||
@ -197,6 +194,9 @@ public:
|
||||
bool selectFirstActor();
|
||||
bool selectNextActor();
|
||||
|
||||
int getBucketCount() const;
|
||||
int getBucketSize() const;
|
||||
|
||||
std::vector<CGPathNode *> getInitialNodes() override;
|
||||
|
||||
virtual void calculateNeighbours(
|
||||
@ -298,7 +298,7 @@ public:
|
||||
|
||||
inline int getBucket(const ChainActor * actor) const
|
||||
{
|
||||
return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
|
||||
return ((uintptr_t)actor * 395) % getBucketCount();
|
||||
}
|
||||
|
||||
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
|
||||
|
@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
|
||||
initialMovement = hero->movementPointsRemaining();
|
||||
initialTurn = 0;
|
||||
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
|
||||
heroFightingStrength = hero->getFightingStrength();
|
||||
heroFightingStrength = hero->getHeroStrength();
|
||||
tiCache.reset(new TurnInfo(hero));
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,9 @@ using crstring = const std::string &;
|
||||
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
|
||||
|
||||
const int ACTUAL_RESOURCE_COUNT = 7;
|
||||
const int ALLOWED_ROAMING_HEROES = 8;
|
||||
|
||||
//implementation-dependent
|
||||
extern const double SAFE_ATTACK_CONSTANT;
|
||||
extern const int GOLD_RESERVE;
|
||||
|
||||
extern thread_local CCallback * cb;
|
||||
extern thread_local VCAI * ai;
|
||||
|
@ -14,8 +14,6 @@
|
||||
#include "../../CCallback.h"
|
||||
#include "../../lib/mapObjects/MapObjects.h"
|
||||
|
||||
#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol
|
||||
|
||||
ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
|
||||
: resources(Res), goal(Goal)
|
||||
{
|
||||
|
@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
|
||||
return false;
|
||||
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
|
||||
return false;
|
||||
if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
|
||||
return false;
|
||||
if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
|
||||
return false;
|
||||
if(!cb->getAvailableHeroes(t).size())
|
||||
|
280
CI/example.markdownlint-cli2.jsonc
Normal file
280
CI/example.markdownlint-cli2.jsonc
Normal 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
|
||||
|
||||
}
|
||||
}
|
1590
ChangeLog.md
1590
ChangeLog.md
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@
|
||||
"vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的",
|
||||
"vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的",
|
||||
"vcmi.adventureMap.monsterThreat.levels.10" : "致命的",
|
||||
"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜",
|
||||
"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的",
|
||||
"vcmi.adventureMap.monsterLevel" : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物",
|
||||
"vcmi.adventureMap.monsterMeleeType" : "近战",
|
||||
"vcmi.adventureMap.monsterRangedType" : "远程",
|
||||
@ -188,9 +188,6 @@
|
||||
"vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。",
|
||||
"vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}",
|
||||
"vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}",
|
||||
"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
|
||||
"vcmi.server.errors.modDependencyLoop" : "读取mod包 {'%s'}失败!\n 这个mod可能存在循环(软)依赖!",
|
||||
"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
|
||||
@ -304,7 +301,7 @@
|
||||
"vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器",
|
||||
"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)",
|
||||
"vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小",
|
||||
"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)",
|
||||
"vcmi.battleOptions.queueSizeBigButton.help": "设置回合顺序指示器为大尺寸(无法在游戏高度像素低于700时生效)",
|
||||
"vcmi.battleOptions.animationsSpeed1.hover": "",
|
||||
"vcmi.battleOptions.animationsSpeed5.hover": "",
|
||||
"vcmi.battleOptions.animationsSpeed6.hover": "",
|
||||
@ -384,7 +381,7 @@
|
||||
"vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面",
|
||||
"vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物",
|
||||
"vcmi.heroWindow.sortBackpackByCost.hover" : "按价格排序",
|
||||
"vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。.",
|
||||
"vcmi.heroWindow.sortBackpackByCost.help" : "将行囊里的宝物按价格排序。",
|
||||
"vcmi.heroWindow.sortBackpackBySlot.hover" : "按装备槽排序",
|
||||
"vcmi.heroWindow.sortBackpackBySlot.help" : "将行囊里的宝物按装备槽排序。",
|
||||
"vcmi.heroWindow.sortBackpackByClass.hover" : "按类型排序",
|
||||
|
@ -86,12 +86,13 @@
|
||||
|
||||
"vcmi.spellBook.search" : "Hledat",
|
||||
|
||||
"vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu kouzel.",
|
||||
"vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu dalších kouzel.",
|
||||
"vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vraťte se zítra.",
|
||||
"vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu kouzel?",
|
||||
"vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?",
|
||||
"vcmi.spellResearch.research" : "Prozkoumat toto kouzlo",
|
||||
"vcmi.spellResearch.skip" : "Přeskočit toto kouzlo",
|
||||
"vcmi.spellResearch.abort" : "Přerušit",
|
||||
"vcmi.spellResearch.noMoreSpells" : "Žádná další kouzla k výzkumu nejsou dostupná.",
|
||||
|
||||
"vcmi.mainMenu.serverConnecting" : "Připojování...",
|
||||
"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
|
||||
@ -113,13 +114,51 @@
|
||||
"vcmi.lobby.handicap.resource" : "Dává hráčům odpovídající zdroje navíc k běžným startovním zdrojům. Jsou povoleny záporné hodnoty, ale jsou omezeny na celkovou hodnotu 0 (hráč nikdy nezačíná se zápornými zdroji).",
|
||||
"vcmi.lobby.handicap.income" : "Mění různé příjmy hráče podle procent. Výsledek je zaokrouhlen nahoru.",
|
||||
"vcmi.lobby.handicap.growth" : "Mění rychlost růstu jednotel v městech vlastněných hráčem. Výsledek je zaokrouhlen nahoru.",
|
||||
"vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry (např. z předchozích verzí).\n\nChcete je odstranit?",
|
||||
"vcmi.lobby.deleteUnsupportedSave" : "Nalezeny nepodporované uložené hry.\n\nBylo nalezeno %d uložených her, které již nejsou podporovány, pravděpodobně kvůli rozdílům mezi verzemi VCMI.\n\nChcete je odstranit?",
|
||||
"vcmi.lobby.deleteSaveGameTitle" : "Vyberte uloženou hru k odstranění",
|
||||
"vcmi.lobby.deleteMapTitle" : "Vyberte scénář k odstranění",
|
||||
"vcmi.lobby.deleteFile" : "Chcete smazat následující soubor?",
|
||||
"vcmi.lobby.deleteFolder" : "Chcete smazat následující složku?",
|
||||
"vcmi.lobby.deleteFile" : "Chcete odstranit následující soubor?",
|
||||
"vcmi.lobby.deleteFolder" : "Chcete odstranit následující složku?",
|
||||
"vcmi.lobby.deleteMode" : "Přepnout do režimu mazání a zpět",
|
||||
|
||||
"vcmi.broadcast.failedLoadGame" : "Nepodařilo se načíst hru",
|
||||
"vcmi.broadcast.command" : "Použijte '!help' pro zobrazení dostupných příkazů",
|
||||
"vcmi.broadcast.simturn.end" : "Současné tahy byly ukončeny",
|
||||
"vcmi.broadcast.simturn.endBetween" : "Současné tahy mezi hráči %s a %s byly ukončeny",
|
||||
"vcmi.broadcast.serverProblem" : "Server narazil na problém",
|
||||
"vcmi.broadcast.gameTerminated" : "Hra byla ukončena",
|
||||
"vcmi.broadcast.gameSavedAs" : "Hra byla uložena jako",
|
||||
"vcmi.broadcast.noCheater" : "Nejsou zaznamenáni žádní podvodníci!",
|
||||
"vcmi.broadcast.playerCheater" : "Hráč %s je podvodník!",
|
||||
"vcmi.broadcast.statisticFile" : "Soubory se statistikou lze nalézt v adresáři %s",
|
||||
"vcmi.broadcast.help.commands" : "Dostupné příkazy pro hostitele:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - okamžitě ukončí aktuální hru",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <hráč>' - vyhodí vybraného hráče ze hry",
|
||||
"vcmi.broadcast.help.save" : "'!save <název_souboru>' - uloží hru pod zadaným názvem",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - uloží statistiky hry jako soubor CSV",
|
||||
"vcmi.broadcast.help.commandsAll" : "Dostupné příkazy pro všechny hráče:",
|
||||
"vcmi.broadcast.help.help" : "'!help' - zobrazí tuto nápovědu",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - zobrazí seznam hráčů, kteří během hry použili cheaty",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - umožňuje změnit některá nastavení hry, pokud všichni hráči souhlasí",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - povolí současné tahy na určený počet dní nebo dokud nenastane kontakt",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - vynutí současné tahy na určený počet dní s blokováním kontaktů hráčů",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - ukončí současné tahy po skončení aktuálního tahu",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prodlouží základní časovač pro všechny hráče o určený počet sekund",
|
||||
"vcmi.broadcast.vote.noActive" : "Žádné aktivní hlasování!",
|
||||
"vcmi.broadcast.vote.yes" : "ano",
|
||||
"vcmi.broadcast.vote.no" : "ne",
|
||||
"vcmi.broadcast.vote.notRecognized" : "Hlasovací příkaz nebyl rozpoznán!",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní nebo dokud nenastane kontakt",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "Hlasování bylo úspěšné. Současné tahy poběží ještě %s dní. Kontakty jsou blokovány",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "Hlasování bylo úspěšné. Současné tahy skončí následující den",
|
||||
"vcmi.broadcast.vote.success.timer" : "Hlasování bylo úspěšné. Časovač pro všechny hráče byl prodloužen o %s sekund",
|
||||
"vcmi.broadcast.vote.aborted" : "Hráč hlasoval proti změně. Hlasování bylo ukončeno",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "Bylo zahájeno hlasování o povolení současných tahů na %s dní",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "Bylo zahájeno hlasování o vynucení současných tahů na %s dní",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "Bylo zahájeno hlasování o ukončení současných tahů od následujícího dne",
|
||||
"vcmi.broadcast.vote.start.timer" : "Bylo zahájeno hlasování o prodloužení časovače pro všechny hráče o %s sekund",
|
||||
"vcmi.broadcast.vote.hint" : "Napište '!vote yes', pokud souhlasíte se změnou, nebo '!vote no', pokud jste proti",
|
||||
|
||||
"vcmi.lobby.login.title" : "Online lobby VCMI",
|
||||
"vcmi.lobby.login.username" : "Uživatelské jméno:",
|
||||
"vcmi.lobby.login.connecting" : "Připojování...",
|
||||
@ -127,6 +166,7 @@
|
||||
"vcmi.lobby.login.create" : "Nový účet",
|
||||
"vcmi.lobby.login.login" : "Přihlásit se",
|
||||
"vcmi.lobby.login.as" : "Přihlásit se jako %s",
|
||||
"vcmi.lobby.login.spectator" : "Divák",
|
||||
"vcmi.lobby.header.rooms" : "Herní místnosti - %d",
|
||||
"vcmi.lobby.header.channels" : "Kanály konverzace",
|
||||
"vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name
|
||||
@ -187,10 +227,9 @@
|
||||
"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",
|
||||
"vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}",
|
||||
"vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n",
|
||||
"vcmi.server.errors.modDependencyLoop" : "Nelze načíst modifikaci {'%s'}!\n Modifikace může být součástí (nepřímé) závislostní smyčky.",
|
||||
"vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!",
|
||||
"vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.",
|
||||
"vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Pomocí dimenzní brány není možné se teleportovat z moře na pevninu nebo naopak.",
|
||||
|
||||
@ -286,42 +325,42 @@
|
||||
"vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nPokud je tato možnost aktivována, posouvání mapy bude plynulé.",
|
||||
"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Přeskočit efekty mizení",
|
||||
"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Přeskočit efekty mizení}\n\nKdyž je povoleno, přeskočí se efekty mizení objektů a podobné efekty (sběr surovin, nalodění atd.). V některých případech zrychlí uživatelské rozhraní na úkor estetiky. Obzvláště užitečné v PvP hrách. Pro maximální rychlost pohybu je toto nastavení aktivní bez ohledu na další volby.",
|
||||
"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed1.help": "Nastavit posouvání mapy na velmi pomalé",
|
||||
"vcmi.adventureOptions.mapScrollSpeed5.help": "Nastavit posouvání mapy na velmi rychlé",
|
||||
"vcmi.adventureOptions.mapScrollSpeed6.help": "Nastavit posouvání mapy na okamžité",
|
||||
"vcmi.adventureOptions.mapScrollSpeed1.hover" : "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed5.hover" : "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed6.hover" : "",
|
||||
"vcmi.adventureOptions.mapScrollSpeed1.help" : "Nastavit posouvání mapy na velmi pomalé",
|
||||
"vcmi.adventureOptions.mapScrollSpeed5.help" : "Nastavit posouvání mapy na velmi rychlé",
|
||||
"vcmi.adventureOptions.mapScrollSpeed6.help" : "Nastavit posouvání mapy na okamžité",
|
||||
"vcmi.adventureOptions.hideBackground.hover" : "Skrýt pozadí",
|
||||
"vcmi.adventureOptions.hideBackground.help" : "{Skrýt pozadí}\n\nSkryje mapu dobrodružství na pozadí a místo ní zobrazí texturu.",
|
||||
|
||||
"vcmi.battleOptions.queueSizeLabel.hover": "Zobrazit frontu pořadí tahů",
|
||||
"vcmi.battleOptions.queueSizeNoneButton.hover": "VYPNUTO",
|
||||
"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
|
||||
"vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ",
|
||||
"vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ",
|
||||
"vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů.",
|
||||
"vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
|
||||
"vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit MALOU frontu pořadí tahů.",
|
||||
"vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).",
|
||||
"vcmi.battleOptions.animationsSpeed1.hover": "",
|
||||
"vcmi.battleOptions.animationsSpeed5.hover": "",
|
||||
"vcmi.battleOptions.animationsSpeed6.hover": "",
|
||||
"vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé.",
|
||||
"vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé.",
|
||||
"vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité.",
|
||||
"vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí",
|
||||
"vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.",
|
||||
"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců",
|
||||
"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.",
|
||||
"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Zobrazit okno statistik hrdinů",
|
||||
"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.",
|
||||
"vcmi.battleOptions.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu",
|
||||
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.",
|
||||
"vcmi.battleOptions.endWithAutocombat.hover": "Přeskočit bitvu",
|
||||
"vcmi.battleOptions.endWithAutocombat.help": "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.",
|
||||
"vcmi.battleOptions.showQuickSpell.hover": "Zobrazit rychlý panel kouzel",
|
||||
"vcmi.battleOptions.showQuickSpell.help": "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.",
|
||||
"vcmi.battleOptions.queueSizeLabel.hover" : "Zobrazit frontu pořadí tahů",
|
||||
"vcmi.battleOptions.queueSizeNoneButton.hover" : "VYPNUTO",
|
||||
"vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO",
|
||||
"vcmi.battleOptions.queueSizeSmallButton.hover" : "MALÁ",
|
||||
"vcmi.battleOptions.queueSizeBigButton.hover" : "VELKÁ",
|
||||
"vcmi.battleOptions.queueSizeNoneButton.help" : "Nezobrazovat frontu pořadí tahů.",
|
||||
"vcmi.battleOptions.queueSizeAutoButton.help" : "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
|
||||
"vcmi.battleOptions.queueSizeSmallButton.help" : "Zobrazit MALOU frontu pořadí tahů.",
|
||||
"vcmi.battleOptions.queueSizeBigButton.help" : "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).",
|
||||
"vcmi.battleOptions.animationsSpeed1.hover" : "",
|
||||
"vcmi.battleOptions.animationsSpeed5.hover" : "",
|
||||
"vcmi.battleOptions.animationsSpeed6.hover" : "",
|
||||
"vcmi.battleOptions.animationsSpeed1.help" : "Nastavit rychlost animací na velmi pomalé.",
|
||||
"vcmi.battleOptions.animationsSpeed5.help" : "Nastavit rychlost animací na velmi rychlé.",
|
||||
"vcmi.battleOptions.animationsSpeed6.help" : "Nastavit rychlost animací na okamžité.",
|
||||
"vcmi.battleOptions.movementHighlightOnHover.hover" : "Zvýraznění pohybu při najetí",
|
||||
"vcmi.battleOptions.movementHighlightOnHover.help" : "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.",
|
||||
"vcmi.battleOptions.rangeLimitHighlightOnHover.hover" : "Zobrazit omezení dostřelu střelců",
|
||||
"vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Zobrazit omezení dostřelu střelců při najetí}\n\nZobrazit dostřel střelce při najetí na něj.",
|
||||
"vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Zobrazit okno statistik hrdinů",
|
||||
"vcmi.battleOptions.showStickyHeroInfoWindows.help" : "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.",
|
||||
"vcmi.battleOptions.skipBattleIntroMusic.hover" : "Přeskočit úvodní hudbu",
|
||||
"vcmi.battleOptions.skipBattleIntroMusic.help" : "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.",
|
||||
"vcmi.battleOptions.endWithAutocombat.hover" : "Přeskočit bitvu",
|
||||
"vcmi.battleOptions.endWithAutocombat.help" : "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.",
|
||||
"vcmi.battleOptions.showQuickSpell.hover" : "Zobrazit rychlý panel kouzel",
|
||||
"vcmi.battleOptions.showQuickSpell.help" : "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.",
|
||||
|
||||
"vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit objekt",
|
||||
"vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit objekt}\n\nPokud hrdina právě stojí na objektu na mapě, může toto místo znovu navštívit.",
|
||||
@ -365,8 +404,8 @@
|
||||
"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).",
|
||||
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek",
|
||||
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Zobrazit týdenní přírůstek jednotek}\n\nZobrazit týdenní přírůstek jednotek místo dostupného počtu ke koupení v přehledu města (levý spodní okraj obrazovky města).",
|
||||
"vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktní informace o jednotkách",
|
||||
"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).",
|
||||
"vcmi.otherOptions.compactTownCreatureInfo.hover" : "Kompaktní informace o jednotkách",
|
||||
"vcmi.otherOptions.compactTownCreatureInfo.help" : "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).",
|
||||
|
||||
"vcmi.townHall.missingBase" : "Nejdříve musí být postavena základní budova %s",
|
||||
"vcmi.townHall.noCreaturesToRecruit" : "Nejsou k dispozici žádné jednotky k najmutí!",
|
||||
@ -568,158 +607,182 @@
|
||||
|
||||
"mapObject.core.hillFort.object.description" : "Zde můžeš vylepšit jednotky. Vylepšení jednotek úrovně 1 až 4 je zde levnější než v jejich domovském městě.",
|
||||
|
||||
"core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý útok",
|
||||
"core.bonus.ADDITIONAL_ATTACK.description": "Útočí dvakrát",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.name": "Další odvetné útoky",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.description": "Může odvetně zaútočit ${val} krát navíc",
|
||||
"core.bonus.AIR_IMMUNITY.name": "Odolnost vůči vzdušné magii",
|
||||
"core.bonus.AIR_IMMUNITY.description": "Imunní vůči všem kouzlům školy vzdušné magie",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.name": "Útok na všechny kolem",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.description": "Útočí na všechny sousední nepřátele",
|
||||
"core.bonus.BLOCKS_RETALIATION.name": "Žádná odveta",
|
||||
"core.bonus.BLOCKS_RETALIATION.description": "Nepřítel nemůže odvetně zaútočit",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Žádná střelecká odveta",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Nepřítel nemůže odvetně zaútočit střeleckým útokem",
|
||||
"core.bonus.CATAPULT.name": "Katapult",
|
||||
"core.bonus.CATAPULT.description": "Útočí na ochranné hradby",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Snížit cenu kouzel (${val})",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Snižuje náklady na kouzla pro hrdinu o ${val}",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tlumič magie (${val})",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zvyšuje náklady na kouzla nepřítele o ${val}",
|
||||
"core.bonus.CHARGE_IMMUNITY.name": "Odolnost vůči Nájezdu",
|
||||
"core.bonus.CHARGE_IMMUNITY.description": "Imunní vůči Nájezdu Jezdců a Šampionů",
|
||||
"core.bonus.DARKNESS.name": "Závoj temnoty",
|
||||
"core.bonus.DARKNESS.description": "Vytváří závoj temnoty s poloměrem ${val}",
|
||||
"core.bonus.DEATH_STARE.name": "Smrtící pohled (${val}%)",
|
||||
"core.bonus.DEATH_STARE.description": "Má ${val}% šanci zabít jednu jednotku",
|
||||
"core.bonus.DEFENSIVE_STANCE.name": "Obranný bonus",
|
||||
"core.bonus.DEFENSIVE_STANCE.description": "+${val} k obraně při bránění",
|
||||
"core.bonus.DESTRUCTION.name": "Zničení",
|
||||
"core.bonus.DESTRUCTION.description": "Má ${val}% šanci zabít další jednotky po útoku",
|
||||
"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Smrtelný úder",
|
||||
"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Má ${val}% šanci způsobit dvojnásobné základní poškození při útoku",
|
||||
"core.bonus.DRAGON_NATURE.name": "Dračí povaha",
|
||||
"core.bonus.DRAGON_NATURE.description": "Jednotka má Dračí povahu",
|
||||
"core.bonus.EARTH_IMMUNITY.name": "Odolnost vůči zemské magii",
|
||||
"core.bonus.EARTH_IMMUNITY.description": "Imunní vůči všem kouzlům školy zemské magie",
|
||||
"core.bonus.ENCHANTER.name": "Zaklínač",
|
||||
"core.bonus.ENCHANTER.description": "Může každé kolo sesílat masové kouzlo ${subtype.spell}",
|
||||
"core.bonus.ENCHANTED.name": "Očarovaný",
|
||||
"core.bonus.ENCHANTED.description": "Je pod trvalým účinkem kouzla ${subtype.spell}",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignorování útoku (${val}%)",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Při útoku je ignorováno ${val}% útočníkovy síly",
|
||||
"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorování obrany (${val}%)",
|
||||
"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Pří útoku nebude bráno v potaz ${val}% bodů obrany obránce",
|
||||
"core.bonus.FIRE_IMMUNITY.name": "Odolnost vůči ohnivé magii",
|
||||
"core.bonus.FIRE_IMMUNITY.description": "Imunní vůči všem kouzlům školy ohnivé magie",
|
||||
"core.bonus.FIRE_SHIELD.name": "Ohnivý štít (${val}%)",
|
||||
"core.bonus.FIRE_SHIELD.description": "Odrazí část zranění při útoku z blízka",
|
||||
"core.bonus.FIRST_STRIKE.name": "První úder",
|
||||
"core.bonus.FIRST_STRIKE.description": "Tato jednotka útočí dříve, než je napadena",
|
||||
"core.bonus.FEAR.name": "Strach",
|
||||
"core.bonus.FEAR.description": "Vyvolává strach u nepřátelské jednotky",
|
||||
"core.bonus.FEARLESS.name": "Nebojácnost",
|
||||
"core.bonus.FEARLESS.description": "Imunní vůči schopnosti Strach",
|
||||
"core.bonus.FEROCITY.name": "Zuřivost",
|
||||
"core.bonus.FEROCITY.description": "Útočí ${val} krát navíc, pokud někoho zabije",
|
||||
"core.bonus.FLYING.name": "Létání",
|
||||
"core.bonus.FLYING.description": "Při pohybu létá (ignoruje překážky)",
|
||||
"core.bonus.FREE_SHOOTING.name": "Střelba zblízka",
|
||||
"core.bonus.FREE_SHOOTING.description": "Může použít výstřely i při útoku zblízka",
|
||||
"core.bonus.GARGOYLE.name": "Chrlič",
|
||||
"core.bonus.GARGOYLE.description": "Nemůže být oživen ani vyléčen",
|
||||
"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Snižuje poškození (${val}%)",
|
||||
"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Snižuje poškození od útoků z dálky a blízka",
|
||||
"core.bonus.HATE.name": "Nenávidí ${subtype.creature}",
|
||||
"core.bonus.HATE.description": "Způsobuje ${val}% více poškození vůči ${subtype.creature}",
|
||||
"core.bonus.HEALER.name": "Léčitel",
|
||||
"core.bonus.HEALER.description": "Léčí spojenecké jednotky",
|
||||
"core.bonus.HP_REGENERATION.name": "Regenerace",
|
||||
"core.bonus.HP_REGENERATION.description": "Každé kolo regeneruje ${val} bodů zdraví",
|
||||
"core.bonus.JOUSTING.name": "Nájezd šampionů",
|
||||
"core.bonus.JOUSTING.description": "+${val}% poškození za každé projité pole",
|
||||
"core.bonus.KING.name": "Král",
|
||||
"core.bonus.KING.description": "Zranitelný proti zabijákovi úrovně ${val} a vyšší",
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odolnost kouzel 1-${val}",
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odolnost vůči kouzlům úrovní 1-${val}",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.name": "Omezený dostřel",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description": "Není schopen zasáhnout jednotky vzdálenější než ${val} polí",
|
||||
"core.bonus.LIFE_DRAIN.name": "Vysávání života (${val}%)",
|
||||
"core.bonus.LIFE_DRAIN.description": "Vysává ${val}% způsobeného poškození",
|
||||
"core.bonus.MANA_CHANNELING.name": "Kanál magie ${val}%",
|
||||
"core.bonus.MANA_CHANNELING.description": "Poskytuje vašemu hrdinovi ${val}% many použité nepřítelem",
|
||||
"core.bonus.MANA_DRAIN.name": "Vysávání many",
|
||||
"core.bonus.MANA_DRAIN.description": "Vysává ${val} many každý tah",
|
||||
"core.bonus.MAGIC_MIRROR.name": "Magické zrcadlo (${val}%)",
|
||||
"core.bonus.MAGIC_MIRROR.description": "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku",
|
||||
"core.bonus.MAGIC_RESISTANCE.name": "Magická odolnost (${val}%)",
|
||||
"core.bonus.MAGIC_RESISTANCE.description": "Má ${val}% šanci odolat nepřátelskému kouzlu",
|
||||
"core.bonus.MIND_IMMUNITY.name": "Imunita vůči kouzlům mysli",
|
||||
"core.bonus.MIND_IMMUNITY.description": "Imunní vůči kouzlům mysli",
|
||||
"core.bonus.NO_DISTANCE_PENALTY.name": "Žádná penalizace vzdálenosti",
|
||||
"core.bonus.NO_DISTANCE_PENALTY.description": "Způsobuje plné poškození na jakoukoliv vzdálenost",
|
||||
"core.bonus.NO_MELEE_PENALTY.name": "Bez penalizace útoku zblízka",
|
||||
"core.bonus.NO_MELEE_PENALTY.description": "Jednotka není penalizována za útok zblízka",
|
||||
"core.bonus.NO_MORALE.name": "Neutrální morálka",
|
||||
"core.bonus.NO_MORALE.description": "Jednotka je imunní vůči efektům morálky",
|
||||
"core.bonus.NO_WALL_PENALTY.name": "Bez penalizace hradbami",
|
||||
"core.bonus.NO_WALL_PENALTY.description": "Plné poškození během obléhání",
|
||||
"core.bonus.NON_LIVING.name": "Neživý",
|
||||
"core.bonus.NON_LIVING.description": "Imunní vůči mnohým efektům",
|
||||
"core.bonus.RANDOM_SPELLCASTER.name": "Náhodný kouzelník",
|
||||
"core.bonus.RANDOM_SPELLCASTER.description": "Může seslat náhodné kouzlo",
|
||||
"core.bonus.RANGED_RETALIATION.name": "Střelecká odveta",
|
||||
"core.bonus.RANGED_RETALIATION.description": "Může provést protiútok na dálku",
|
||||
"core.bonus.RECEPTIVE.name": "Vnímavý",
|
||||
"core.bonus.RECEPTIVE.description": "Nemá imunitu na přátelská kouzla",
|
||||
"core.bonus.REBIRTH.name": "Znovuzrození (${val}%)",
|
||||
"core.bonus.REBIRTH.description": "${val}% jednotek povstane po smrti",
|
||||
"core.bonus.RETURN_AFTER_STRIKE.name": "Útok a návrat",
|
||||
"core.bonus.RETURN_AFTER_STRIKE.description": "Navrátí se po útoku na zblízka",
|
||||
"core.bonus.REVENGE.name": "Pomsta",
|
||||
"core.bonus.REVENGE.description": "Způsobuje extra poškození na základě ztrát útočníka v bitvě",
|
||||
"core.bonus.SHOOTER.name": "Střelec",
|
||||
"core.bonus.SHOOTER.description": "Jednotka může střílet",
|
||||
"core.bonus.SHOOTS_ALL_ADJACENT.name": "Střílí všude kolem",
|
||||
"core.bonus.SHOOTS_ALL_ADJACENT.description": "Střelecký útok této jednotky zasáhne všechny cíle v malé oblasti",
|
||||
"core.bonus.SOUL_STEAL.name": "Zloděj duší",
|
||||
"core.bonus.SOUL_STEAL.description": "Získává ${val} nové jednotky za každého zabitého nepřítele",
|
||||
"core.bonus.SPELLCASTER.name": "Kouzelník",
|
||||
"core.bonus.SPELLCASTER.description": "Může seslat kouzlo ${subtype.spell}",
|
||||
"core.bonus.SPELL_AFTER_ATTACK.name": "Sesílá po útoku",
|
||||
"core.bonus.SPELL_AFTER_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} po útoku",
|
||||
"core.bonus.SPELL_BEFORE_ATTACK.name": "Sesílá před útokem",
|
||||
"core.bonus.SPELL_BEFORE_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} před útokem",
|
||||
"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Magická odolnost",
|
||||
"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Poškození kouzly sníženo o ${val}%.",
|
||||
"core.bonus.SPELL_IMMUNITY.name": "Imunita vůči kouzlům",
|
||||
"core.bonus.SPELL_IMMUNITY.description": "Imunní vůči ${subtype.spell}",
|
||||
"core.bonus.SPELL_LIKE_ATTACK.name": "Útok kouzlem",
|
||||
"core.bonus.SPELL_LIKE_ATTACK.description": "Útočí kouzlem ${subtype.spell}",
|
||||
"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura odporu",
|
||||
"core.bonus.SPELL_RESISTANCE_AURA.description": "Jednotky poblíž získají ${val}% magickou odolnost",
|
||||
"core.bonus.SUMMON_GUARDIANS.name": "Přivolání ochránců",
|
||||
"core.bonus.SUMMON_GUARDIANS.description": "Na začátku bitvy přivolá ${subtype.creature} (${val}%)",
|
||||
"core.bonus.SYNERGY_TARGET.name": "Synergizovatelný",
|
||||
"core.bonus.SYNERGY_TARGET.description": "Tato jednotka je náchylná k synergickým efektům",
|
||||
"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Dech",
|
||||
"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Útok dechem (dosah 2 polí)",
|
||||
"core.bonus.THREE_HEADED_ATTACK.name": "Tříhlavý útok",
|
||||
"core.bonus.THREE_HEADED_ATTACK.description": "Útočí na tři sousední jednotky",
|
||||
"core.bonus.TRANSMUTATION.name": "Transmutace",
|
||||
"core.bonus.TRANSMUTATION.description": "${val}% šance na přeměnu napadené jednotky na jiný typ",
|
||||
"core.bonus.UNDEAD.name": "Nemrtvý",
|
||||
"core.bonus.UNDEAD.description": "Jednotka je nemrtvá",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.name": "Neomezené odvetné útoky",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.description": "Může provést neomezený počet odvetných útoků",
|
||||
"core.bonus.WATER_IMMUNITY.name": "Odolnost vůči vodní magii",
|
||||
"core.bonus.WATER_IMMUNITY.description": "Imunní vůči všem kouzlům školy vodní magie",
|
||||
"core.bonus.WIDE_BREATH.name": "Široký dech",
|
||||
"core.bonus.WIDE_BREATH.description": "Široký útok dechem (více polí)",
|
||||
"core.bonus.DISINTEGRATE.name": "Rozpad",
|
||||
"core.bonus.DISINTEGRATE.description": "Po smrti nezůstane žádné tělo",
|
||||
"core.bonus.INVINCIBLE.name": "Neporazitelný",
|
||||
"core.bonus.INVINCIBLE.description": "Nelze ovlivnit žádným efektem",
|
||||
"core.bonus.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.ADDITIONAL_ATTACK.name" : "Dvojitý útok",
|
||||
"core.bonus.ADDITIONAL_ATTACK.description" : "Útočí dvakrát",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.name" : "Další odvetné útoky",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.description" : "Může odvetně zaútočit ${val} krát navíc",
|
||||
"core.bonus.AIR_IMMUNITY.name" : "Odolnost vůči vzdušné magii",
|
||||
"core.bonus.AIR_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vzdušné magie",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.name" : "Útok na všechny kolem",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.description" : "Útočí na všechny sousední nepřátele",
|
||||
"core.bonus.BLOCKS_RETALIATION.name" : "Žádná odveta",
|
||||
"core.bonus.BLOCKS_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Žádná střelecká odveta",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.description" : "Nepřítel nemůže odvetně zaútočit střeleckým útokem",
|
||||
"core.bonus.CATAPULT.name" : "Katapult",
|
||||
"core.bonus.CATAPULT.description" : "Útočí na ochranné hradby",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Snížit cenu kouzel (${val})",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Snižuje náklady na kouzla pro hrdinu o ${val}",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Tlumič magie (${val})",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Zvyšuje náklady na kouzla nepřítele o ${val}",
|
||||
"core.bonus.CHARGE_IMMUNITY.name" : "Odolnost vůči Nájezdu",
|
||||
"core.bonus.CHARGE_IMMUNITY.description" : "Imunní vůči Nájezdu Jezdců a Šampionů",
|
||||
"core.bonus.DARKNESS.name" : "Závoj temnoty",
|
||||
"core.bonus.DARKNESS.description" : "Vytváří závoj temnoty s poloměrem ${val}",
|
||||
"core.bonus.DEATH_STARE.name" : "Smrtící pohled (${val}%)",
|
||||
"core.bonus.DEATH_STARE.description" : "Má ${val}% šanci zabít jednu jednotku",
|
||||
"core.bonus.DEFENSIVE_STANCE.name" : "Obranný bonus",
|
||||
"core.bonus.DEFENSIVE_STANCE.description" : "+${val} k obraně při bránění",
|
||||
"core.bonus.DESTRUCTION.name" : "Zničení",
|
||||
"core.bonus.DESTRUCTION.description" : "Má ${val}% šanci zabít další jednotky po útoku",
|
||||
"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Smrtelný úder",
|
||||
"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Má ${val}% šanci způsobit dvojnásobné základní poškození při útoku",
|
||||
"core.bonus.DRAGON_NATURE.name" : "Dračí povaha",
|
||||
"core.bonus.DRAGON_NATURE.description" : "Jednotka má Dračí povahu",
|
||||
"core.bonus.EARTH_IMMUNITY.name" : "Odolnost vůči zemské magii",
|
||||
"core.bonus.EARTH_IMMUNITY.description" : "Imunní vůči všem kouzlům školy zemské magie",
|
||||
"core.bonus.ENCHANTER.name" : "Zaklínač",
|
||||
"core.bonus.ENCHANTER.description" : "Může každé kolo sesílat masové kouzlo ${subtype.spell}",
|
||||
"core.bonus.ENCHANTED.name" : "Očarovaný",
|
||||
"core.bonus.ENCHANTED.description" : "Je pod trvalým účinkem kouzla ${subtype.spell}",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorování útoku (${val}%)",
|
||||
"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Při útoku je ignorováno ${val}% útočníkovy síly",
|
||||
"core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorování obrany (${val}%)",
|
||||
"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Pří útoku nebude bráno v potaz ${val}% bodů obrany obránce",
|
||||
"core.bonus.FIRE_IMMUNITY.name" : "Odolnost vůči ohnivé magii",
|
||||
"core.bonus.FIRE_IMMUNITY.description" : "Imunní vůči všem kouzlům školy ohnivé magie",
|
||||
"core.bonus.FIRE_SHIELD.name" : "Ohnivý štít (${val}%)",
|
||||
"core.bonus.FIRE_SHIELD.description" : "Odrazí část zranění při útoku z blízka",
|
||||
"core.bonus.FIRST_STRIKE.name" : "První úder",
|
||||
"core.bonus.FIRST_STRIKE.description" : "Tato jednotka útočí dříve, než je napadena",
|
||||
"core.bonus.FEAR.name" : "Strach",
|
||||
"core.bonus.FEAR.description" : "Vyvolává strach u nepřátelské jednotky",
|
||||
"core.bonus.FEARLESS.name" : "Nebojácnost",
|
||||
"core.bonus.FEARLESS.description" : "Imunní vůči schopnosti Strach",
|
||||
"core.bonus.FEROCITY.name" : "Zuřivost",
|
||||
"core.bonus.FEROCITY.description" : "Útočí ${val} krát navíc, pokud někoho zabije",
|
||||
"core.bonus.FLYING.name" : "Létání",
|
||||
"core.bonus.FLYING.description" : "Při pohybu létá (ignoruje překážky)",
|
||||
"core.bonus.FREE_SHOOTING.name" : "Střelba zblízka",
|
||||
"core.bonus.FREE_SHOOTING.description" : "Může použít výstřely i při útoku zblízka",
|
||||
"core.bonus.GARGOYLE.name" : "Chrlič",
|
||||
"core.bonus.GARGOYLE.description" : "Nemůže být oživen ani vyléčen",
|
||||
"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Snižuje poškození (${val}%)",
|
||||
"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Snižuje poškození od útoků z dálky a blízka",
|
||||
"core.bonus.HATE.name" : "Nenávidí ${subtype.creature}",
|
||||
"core.bonus.HATE.description" : "Způsobuje ${val}% více poškození vůči ${subtype.creature}",
|
||||
"core.bonus.HEALER.name" : "Léčitel",
|
||||
"core.bonus.HEALER.description" : "Léčí spojenecké jednotky",
|
||||
"core.bonus.HP_REGENERATION.name" : "Regenerace",
|
||||
"core.bonus.HP_REGENERATION.description" : "Každé kolo regeneruje ${val} bodů zdraví",
|
||||
"core.bonus.JOUSTING.name" : "Nájezd šampionů",
|
||||
"core.bonus.JOUSTING.description" : "+${val}% poškození za každé projité pole",
|
||||
"core.bonus.KING.name" : "Král",
|
||||
"core.bonus.KING.description" : "Zranitelný proti zabijákovi úrovně ${val} a vyšší",
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Odolnost kouzel 1-${val}",
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Odolnost vůči kouzlům úrovní 1-${val}",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dostřel",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Není schopen zasáhnout jednotky vzdálenější než ${val} polí",
|
||||
"core.bonus.LIFE_DRAIN.name" : "Vysávání života (${val}%)",
|
||||
"core.bonus.LIFE_DRAIN.description" : "Vysává ${val}% způsobeného poškození",
|
||||
"core.bonus.MANA_CHANNELING.name" : "Kanál magie ${val}%",
|
||||
"core.bonus.MANA_CHANNELING.description" : "Poskytuje vašemu hrdinovi ${val}% many použité nepřítelem",
|
||||
"core.bonus.MANA_DRAIN.name" : "Vysávání many",
|
||||
"core.bonus.MANA_DRAIN.description" : "Vysává ${val} many každý tah",
|
||||
"core.bonus.MAGIC_MIRROR.name" : "Magické zrcadlo (${val}%)",
|
||||
"core.bonus.MAGIC_MIRROR.description" : "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku",
|
||||
"core.bonus.MAGIC_RESISTANCE.name" : "Magická odolnost (${val}%)",
|
||||
"core.bonus.MAGIC_RESISTANCE.description" : "Má ${val}% šanci odolat nepřátelskému kouzlu",
|
||||
"core.bonus.MIND_IMMUNITY.name" : "Imunita vůči kouzlům mysli",
|
||||
"core.bonus.MIND_IMMUNITY.description" : "Imunní vůči kouzlům mysli",
|
||||
"core.bonus.NO_DISTANCE_PENALTY.name" : "Žádná penalizace vzdálenosti",
|
||||
"core.bonus.NO_DISTANCE_PENALTY.description" : "Způsobuje plné poškození na jakoukoliv vzdálenost",
|
||||
"core.bonus.NO_MELEE_PENALTY.name" : "Bez penalizace útoku zblízka",
|
||||
"core.bonus.NO_MELEE_PENALTY.description" : "Jednotka není penalizována za útok zblízka",
|
||||
"core.bonus.NO_MORALE.name" : "Neutrální morálka",
|
||||
"core.bonus.NO_MORALE.description" : "Jednotka je imunní vůči efektům morálky",
|
||||
"core.bonus.NO_WALL_PENALTY.name" : "Bez penalizace hradbami",
|
||||
"core.bonus.NO_WALL_PENALTY.description" : "Plné poškození během obléhání",
|
||||
"core.bonus.NON_LIVING.name" : "Neživý",
|
||||
"core.bonus.NON_LIVING.description" : "Imunní vůči mnohým efektům",
|
||||
"core.bonus.RANDOM_SPELLCASTER.name" : "Náhodný kouzelník",
|
||||
"core.bonus.RANDOM_SPELLCASTER.description" : "Může seslat náhodné kouzlo",
|
||||
"core.bonus.RANGED_RETALIATION.name" : "Střelecká odveta",
|
||||
"core.bonus.RANGED_RETALIATION.description" : "Může provést protiútok na dálku",
|
||||
"core.bonus.RECEPTIVE.name" : "Vnímavý",
|
||||
"core.bonus.RECEPTIVE.description" : "Nemá imunitu na přátelská kouzla",
|
||||
"core.bonus.REBIRTH.name" : "Znovuzrození (${val}%)",
|
||||
"core.bonus.REBIRTH.description" : "${val}% jednotek povstane po smrti",
|
||||
"core.bonus.RETURN_AFTER_STRIKE.name" : "Útok a návrat",
|
||||
"core.bonus.RETURN_AFTER_STRIKE.description" : "Navrátí se po útoku na zblízka",
|
||||
"core.bonus.REVENGE.name" : "Pomsta",
|
||||
"core.bonus.REVENGE.description" : "Způsobuje extra poškození na základě ztrát útočníka v bitvě",
|
||||
"core.bonus.SHOOTER.name" : "Střelec",
|
||||
"core.bonus.SHOOTER.description" : "Jednotka může střílet",
|
||||
"core.bonus.SHOOTS_ALL_ADJACENT.name" : "Střílí všude kolem",
|
||||
"core.bonus.SHOOTS_ALL_ADJACENT.description" : "Střelecký útok této jednotky zasáhne všechny cíle v malé oblasti",
|
||||
"core.bonus.SOUL_STEAL.name" : "Zloděj duší",
|
||||
"core.bonus.SOUL_STEAL.description" : "Získává ${val} nové jednotky za každého zabitého nepřítele",
|
||||
"core.bonus.SPELLCASTER.name" : "Kouzelník",
|
||||
"core.bonus.SPELLCASTER.description" : "Může seslat kouzlo ${subtype.spell}",
|
||||
"core.bonus.SPELL_AFTER_ATTACK.name" : "Sesílá po útoku",
|
||||
"core.bonus.SPELL_AFTER_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} po útoku",
|
||||
"core.bonus.SPELL_BEFORE_ATTACK.name" : "Sesílá před útokem",
|
||||
"core.bonus.SPELL_BEFORE_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} před útokem",
|
||||
"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Magická odolnost",
|
||||
"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Poškození kouzly sníženo o ${val}%.",
|
||||
"core.bonus.SPELL_IMMUNITY.name" : "Imunita vůči kouzlům",
|
||||
"core.bonus.SPELL_IMMUNITY.description" : "Imunní vůči ${subtype.spell}",
|
||||
"core.bonus.SPELL_LIKE_ATTACK.name" : "Útok kouzlem",
|
||||
"core.bonus.SPELL_LIKE_ATTACK.description" : "Útočí kouzlem ${subtype.spell}",
|
||||
"core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura odporu",
|
||||
"core.bonus.SPELL_RESISTANCE_AURA.description" : "Jednotky poblíž získají ${val}% magickou odolnost",
|
||||
"core.bonus.SUMMON_GUARDIANS.name" : "Přivolání ochránců",
|
||||
"core.bonus.SUMMON_GUARDIANS.description" : "Na začátku bitvy přivolá ${subtype.creature} (${val}%)",
|
||||
"core.bonus.SYNERGY_TARGET.name" : "Synergizovatelný",
|
||||
"core.bonus.SYNERGY_TARGET.description" : "Tato jednotka je náchylná k synergickým efektům",
|
||||
"core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Dech",
|
||||
"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Útok dechem (dosah 2 polí)",
|
||||
"core.bonus.THREE_HEADED_ATTACK.name" : "Tříhlavý útok",
|
||||
"core.bonus.THREE_HEADED_ATTACK.description" : "Útočí na tři sousední jednotky",
|
||||
"core.bonus.TRANSMUTATION.name" : "Transmutace",
|
||||
"core.bonus.TRANSMUTATION.description" : "${val}% šance na přeměnu napadené jednotky na jiný typ",
|
||||
"core.bonus.UNDEAD.name" : "Nemrtvý",
|
||||
"core.bonus.UNDEAD.description" : "Jednotka je nemrtvá",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.name" : "Neomezené odvetné útoky",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.description" : "Může provést neomezený počet odvetných útoků",
|
||||
"core.bonus.WATER_IMMUNITY.name" : "Odolnost vůči vodní magii",
|
||||
"core.bonus.WATER_IMMUNITY.description" : "Imunní vůči všem kouzlům školy vodní magie",
|
||||
"core.bonus.WIDE_BREATH.name" : "Široký dech",
|
||||
"core.bonus.WIDE_BREATH.description" : "Široký útok dechem (více polí)",
|
||||
"core.bonus.DISINTEGRATE.name" : "Rozpad",
|
||||
"core.bonus.DISINTEGRATE.description" : "Po smrti nezůstane žádné tělo",
|
||||
"core.bonus.INVINCIBLE.name" : "Neporazitelný",
|
||||
"core.bonus.INVINCIBLE.description" : "Nelze ovlivnit žádným efektem",
|
||||
"core.bonus.MECHANICAL.description" : "Imunita vůči mnoha efektům, opravitelné",
|
||||
"core.bonus.MECHANICAL.name" : "Mechanický",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)",
|
||||
|
||||
"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"
|
||||
}
|
@ -188,9 +188,6 @@
|
||||
"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
|
||||
"vcmi.server.errors.modsToEnable" : "{Following mods are required}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}",
|
||||
"vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n",
|
||||
"vcmi.server.errors.modDependencyLoop" : "Failed to load mod {'%s'}!\n It maybe in a (soft) dependency loop.",
|
||||
"vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.",
|
||||
|
@ -188,9 +188,7 @@
|
||||
"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
|
||||
"vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}",
|
||||
"vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n",
|
||||
"vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.",
|
||||
"vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.",
|
||||
|
@ -182,9 +182,7 @@
|
||||
"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
|
||||
"vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}",
|
||||
"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
|
||||
"vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
|
||||
"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.",
|
||||
|
@ -28,6 +28,13 @@
|
||||
"vcmi.adventureMap.movementPointsHeroInfo" : "(Pontos de movimento: %REMAINING / %POINTS)",
|
||||
"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!",
|
||||
|
||||
"vcmi.bonusSource.artifact" : "Artefato",
|
||||
"vcmi.bonusSource.creature" : "Habilidade",
|
||||
"vcmi.bonusSource.spell" : "Feitiço",
|
||||
"vcmi.bonusSource.hero" : "Herói",
|
||||
"vcmi.bonusSource.commander" : "Comandante",
|
||||
"vcmi.bonusSource.other" : "Outro",
|
||||
|
||||
"vcmi.capitalColors.0" : "Vermelho",
|
||||
"vcmi.capitalColors.1" : "Azul",
|
||||
"vcmi.capitalColors.2" : "Bege",
|
||||
@ -85,6 +92,7 @@
|
||||
"vcmi.spellResearch.research" : "Pesquisar este Feitiço",
|
||||
"vcmi.spellResearch.skip" : "Pular este Feitiço",
|
||||
"vcmi.spellResearch.abort" : "Abortar",
|
||||
"vcmi.spellResearch.noMoreSpells" : "Não há mais feitiços disponíveis para pesquisa.",
|
||||
|
||||
"vcmi.mainMenu.serverConnecting" : "Conectando...",
|
||||
"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
|
||||
@ -96,16 +104,60 @@
|
||||
"vcmi.lobby.filepath" : "Caminho do arquivo",
|
||||
"vcmi.lobby.creationDate" : "Data de criação",
|
||||
"vcmi.lobby.scenarioName" : "Nome do cenário",
|
||||
"vcmi.lobby.mapPreview" : "Visualização do mapa",
|
||||
"vcmi.lobby.noPreview" : "sem visualização",
|
||||
"vcmi.lobby.mapPreview" : "Prévia do mapa",
|
||||
"vcmi.lobby.noPreview" : "sem prévia",
|
||||
"vcmi.lobby.noUnderground" : "sem subterrâneo",
|
||||
"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
|
||||
"vcmi.lobby.sortDate" : "Ordenar mapas por data de alteração",
|
||||
"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
|
||||
"vcmi.lobby.author" : "Autor",
|
||||
"vcmi.lobby.handicap" : "Desvant.",
|
||||
"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
|
||||
"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
|
||||
"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
|
||||
"vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?",
|
||||
"vcmi.lobby.deleteSaveGameTitle" : "Selecione um Jogo Salvo para excluir",
|
||||
"vcmi.lobby.deleteMapTitle" : "Selecione um Cenário para excluir",
|
||||
"vcmi.lobby.deleteFile" : "Deseja excluir o seguinte arquivo?",
|
||||
"vcmi.lobby.deleteFolder" : "Deseja excluir a seguinte pasta?",
|
||||
"vcmi.lobby.deleteMode" : "Alternar para o modo de exclusão e voltar",
|
||||
|
||||
"vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo",
|
||||
"vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis",
|
||||
"vcmi.broadcast.simturn.end" : "Os turnos simultâneos terminaram",
|
||||
"vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s terminaram",
|
||||
"vcmi.broadcast.serverProblem" : "O servidor encontrou um problema",
|
||||
"vcmi.broadcast.gameTerminated" : "o jogo foi encerrado",
|
||||
"vcmi.broadcast.gameSavedAs" : "jogo salvo como",
|
||||
"vcmi.broadcast.noCheater" : "Nenhum trapaçeiro registrado!",
|
||||
"vcmi.broadcast.playerCheater" : "O jogador %s é um trapaçeiro!",
|
||||
"vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s",
|
||||
"vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:",
|
||||
"vcmi.broadcast.help.exit" : "'!exit' - termina imediatamente o jogo atual",
|
||||
"vcmi.broadcast.help.kick" : "'!kick <player>' - expulsa o jogador especificado do jogo",
|
||||
"vcmi.broadcast.help.save" : "'!save <filename>' - salva o jogo com o nome de arquivo especificado",
|
||||
"vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv",
|
||||
"vcmi.broadcast.help.commandsAll" : "Comandos disponíveis para todos os jogadores:",
|
||||
"vcmi.broadcast.help.help" : "'!help' - exibe esta ajuda",
|
||||
"vcmi.broadcast.help.cheaters" : "'!cheaters' - lista os jogadores que usaram comandos de trapaça durante o jogo",
|
||||
"vcmi.broadcast.help.vote" : "'!vote' - permite mudar algumas configurações do jogo se todos os jogadores votarem a favor",
|
||||
"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato",
|
||||
"vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores",
|
||||
"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar",
|
||||
"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o temporizador base para todos os jogadores por um número determinado de segundos",
|
||||
"vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!",
|
||||
"vcmi.broadcast.vote.yes" : "sim",
|
||||
"vcmi.broadcast.vote.no" : "não",
|
||||
"vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!",
|
||||
"vcmi.broadcast.vote.success.untilContacts" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato",
|
||||
"vcmi.broadcast.vote.success.contactsBlocked" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados",
|
||||
"vcmi.broadcast.vote.success.nextDay" : "Votação bem-sucedida. Os turnos simultâneos terminarão no próximo dia",
|
||||
"vcmi.broadcast.vote.success.timer" : "Votação bem-sucedida. O temporizador para todos os jogadores foi prolongado por %s segundos",
|
||||
"vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada",
|
||||
"vcmi.broadcast.vote.start.untilContacts" : "Iniciada votação para permitir turnos simultâneos por mais %s dias",
|
||||
"vcmi.broadcast.vote.start.contactsBlocked" : "Iniciada votação para forçar turnos simultâneos por mais %s dias",
|
||||
"vcmi.broadcast.vote.start.nextDay" : "Iniciada votação para terminar os turnos simultâneos a partir do próximo dia",
|
||||
"vcmi.broadcast.vote.start.timer" : "Iniciada votação para prolongar o temporizador para todos os jogadores por %s segundos",
|
||||
"vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra",
|
||||
|
||||
"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
|
||||
"vcmi.lobby.login.username" : "Nome de usuário:",
|
||||
@ -114,6 +166,7 @@
|
||||
"vcmi.lobby.login.create" : "Nova Conta",
|
||||
"vcmi.lobby.login.login" : "Entrar",
|
||||
"vcmi.lobby.login.as" : "Entrar como %s",
|
||||
"vcmi.lobby.login.spectator" : "Espectador",
|
||||
"vcmi.lobby.header.rooms" : "Salas de Jogo - %d",
|
||||
"vcmi.lobby.header.channels" : "Canais de Bate-papo",
|
||||
"vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma
|
||||
@ -174,9 +227,9 @@
|
||||
"vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.",
|
||||
"vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}",
|
||||
"vcmi.server.errors.modNoDependency" : "Falha ao carregar o mod {'%s'}!\n Ele depende do mod {'%s'}, que não está ativo!\n",
|
||||
"vcmi.server.errors.modConflict" : "Falha ao carregar o mod {'%s'}!\n Conflito com o mod ativo {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!",
|
||||
"vcmi.server.errors.wrongIdentified" : "Você foi identificado como jogador %s, enquanto se espera %s",
|
||||
"vcmi.server.errors.notAllowed" : "Você não tem permissão para realizar esta ação!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.",
|
||||
|
||||
@ -610,7 +663,7 @@
|
||||
"core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém",
|
||||
"core.bonus.FLYING.name" : "Voo",
|
||||
"core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)",
|
||||
"core.bonus.FREE_SHOOTING.name" : "Tiro Livre",
|
||||
"core.bonus.FREE_SHOOTING.name" : "Tiro Curto",
|
||||
"core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo",
|
||||
"core.bonus.GARGOYLE.name" : "Gárgula",
|
||||
"core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado",
|
||||
@ -629,7 +682,7 @@
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imune a Feitiços 1-${val}",
|
||||
"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 1-${val}",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos",
|
||||
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a mais de ${val} hexágonos de distância",
|
||||
"core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)",
|
||||
"core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado",
|
||||
"core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%",
|
||||
@ -706,9 +759,10 @@
|
||||
"core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte",
|
||||
"core.bonus.INVINCIBLE.name": "Invencível",
|
||||
"core.bonus.INVINCIBLE.description": "Não pode ser afetado por nada",
|
||||
"core.bonus.MECHANICAL.name": "Mecânico",
|
||||
"core.bonus.MECHANICAL.description": "Imunidade a muitos efeitos, reparável",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Sopro Prismático",
|
||||
"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Ataque de Sopro Prismático (três direções)",
|
||||
"vcmi.server.errors.modDependencyLoop" : "Falha ao carregar o mod {'%s'}!\n Ele pode estar em um ciclo de dependência.",
|
||||
|
||||
"spell.core.castleMoat.name": "Fosso",
|
||||
"spell.core.castleMoatTrigger.name": "Fosso",
|
||||
|
@ -79,8 +79,6 @@
|
||||
"vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}",
|
||||
"vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?",
|
||||
"vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n",
|
||||
"vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.",
|
||||
|
||||
"vcmi.settingsMainWindow.generalTab.hover" : "General",
|
||||
|
@ -188,9 +188,7 @@
|
||||
"vcmi.server.errors.existingProcess" : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.",
|
||||
"vcmi.server.errors.modsToEnable" : "{Följande modd(ar) krävs}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}",
|
||||
"vcmi.server.errors.modNoDependency" : "Misslyckades med att ladda modd {'%s'}!\n Den är beroende av modd {'%s'} som inte är aktiverad!\n",
|
||||
"vcmi.server.errors.modDependencyLoop": "Misslyckades med att ladda modd {'%s'}!\n Den kanske är i en (mjuk) beroendeloop.",
|
||||
"vcmi.server.errors.modConflict" : "Misslyckades med att ladda modd {'%s'}!\n Konflikter med aktiverad modd {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Det går inte att teleportera sig från hav till land eller tvärtom med trollformeln 'Dimensionsdörr'.",
|
||||
|
@ -139,8 +139,6 @@
|
||||
"vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}",
|
||||
"vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}",
|
||||
"vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?",
|
||||
"vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n",
|
||||
"vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n",
|
||||
"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
|
||||
|
||||
"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
|
||||
|
@ -396,16 +396,19 @@ void PlayerLocalState::deserialize(const JsonNode & source)
|
||||
}
|
||||
}
|
||||
|
||||
spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
|
||||
spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
|
||||
spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
|
||||
spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
|
||||
if (!source["spellbook"].isNull())
|
||||
{
|
||||
spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
|
||||
spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
|
||||
spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
|
||||
spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
|
||||
}
|
||||
|
||||
// append any owned heroes / towns that were not present in loaded state
|
||||
wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end());
|
||||
ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end());
|
||||
|
||||
//FIXME: broken, anything that is selected in here will be overwritten on NewTurn pack
|
||||
//FIXME: broken, anything that is selected in here will be overwritten on PlayerStartsTurn pack
|
||||
// ObjectInstanceID selectedObjectID(source["currentSelection"].Integer());
|
||||
// const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID);
|
||||
// const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr);
|
||||
|
@ -18,6 +18,7 @@
|
||||
#include "../gui/CursorHandler.h"
|
||||
#include "../gui/EventDispatcher.h"
|
||||
#include "../gui/ShortcutHandler.h"
|
||||
#include "../render/IScreenHandler.h"
|
||||
|
||||
#include "../../lib/CConfigHandler.h"
|
||||
|
||||
@ -198,9 +199,10 @@ void InputSourceGameController::tryToConvertCursor()
|
||||
assert(CCS->curh);
|
||||
if(CCS->curh->getShowType() == Cursor::ShowType::HARDWARE)
|
||||
{
|
||||
int scalingFactor = GH.screenHandler().getScalingFactor();
|
||||
const Point & cursorPosition = GH.getCursorPosition();
|
||||
CCS->curh->changeCursor(Cursor::ShowType::SOFTWARE);
|
||||
CCS->curh->cursorMove(cursorPosition.x, cursorPosition.y);
|
||||
CCS->curh->cursorMove(cursorPosition.x * scalingFactor, cursorPosition.y * scalingFactor);
|
||||
GH.input().setCursorPosition(cursorPosition);
|
||||
}
|
||||
}
|
||||
@ -225,12 +227,13 @@ void InputSourceGameController::doCursorMove(int deltaX, int deltaY)
|
||||
return;
|
||||
const Point & screenSize = GH.screenDimensions();
|
||||
const Point & cursorPosition = GH.getCursorPosition();
|
||||
int scalingFactor = GH.screenHandler().getScalingFactor();
|
||||
int newX = std::min(std::max(cursorPosition.x + deltaX, 0), screenSize.x);
|
||||
int newY = std::min(std::max(cursorPosition.y + deltaY, 0), screenSize.y);
|
||||
Point targetPosition{newX, newY};
|
||||
GH.input().setCursorPosition(targetPosition);
|
||||
if(CCS && CCS->curh)
|
||||
CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
|
||||
CCS->curh->cursorMove(GH.getCursorPosition().x * scalingFactor, GH.getCursorPosition().y * scalingFactor);
|
||||
}
|
||||
|
||||
int InputSourceGameController::getMoveDis(float planDis)
|
||||
|
@ -27,7 +27,7 @@
|
||||
#include "../widgets/ObjectLists.h"
|
||||
|
||||
#include "../../lib/modding/CModHandler.h"
|
||||
#include "../../lib/modding/CModInfo.h"
|
||||
#include "../../lib/modding/ModDescription.h"
|
||||
#include "../../lib/texts/CGeneralTextHandler.h"
|
||||
#include "../../lib/texts/MetaString.h"
|
||||
|
||||
@ -128,14 +128,14 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
|
||||
GlobalLobbyRoomModInfo modInfo;
|
||||
modInfo.status = modEntry.second;
|
||||
if (modEntry.second == ModVerificationStatus::EXCESSIVE)
|
||||
modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString();
|
||||
modInfo.version = CGI->modh->getModInfo(modEntry.first).getVersion().toString();
|
||||
else
|
||||
modInfo.version = roomDescription.modList.at(modEntry.first).version.toString();
|
||||
|
||||
if (modEntry.second == ModVerificationStatus::NOT_INSTALLED)
|
||||
modInfo.modName = roomDescription.modList.at(modEntry.first).name;
|
||||
else
|
||||
modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name;
|
||||
modInfo.modName = CGI->modh->getModInfo(modEntry.first).getName();
|
||||
|
||||
modVerificationList.push_back(modInfo);
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
|
||||
humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now?
|
||||
}
|
||||
|
||||
si8 playerLimit = opts->getMaxPlayersCount();
|
||||
si8 playerLimit = opts->getPlayerLimit();
|
||||
si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount();
|
||||
si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount();
|
||||
|
||||
|
@ -362,17 +362,6 @@ void CMainMenu::update()
|
||||
menu->switchToTab(menu->getActiveTab());
|
||||
}
|
||||
|
||||
static bool warnedAboutModDependencies = false;
|
||||
|
||||
if (!warnedAboutModDependencies)
|
||||
{
|
||||
warnedAboutModDependencies = true;
|
||||
auto errorMessages = CGI->modh->getModLoadErrors();
|
||||
|
||||
if (!errorMessages.empty())
|
||||
CInfoWindow::showInfoDialog(errorMessages, std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
|
||||
}
|
||||
|
||||
// Handles mouse and key input
|
||||
GH.handleEvents();
|
||||
GH.windows().simpleRedraw();
|
||||
|
@ -548,7 +548,10 @@ size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates)
|
||||
return iconIndex;
|
||||
}
|
||||
|
||||
return MapRendererWorldViewContext::overlayImageIndex(coordinates);
|
||||
if (MapRendererBaseContext::isVisible(coordinates))
|
||||
return MapRendererWorldViewContext::overlayImageIndex(coordinates);
|
||||
else
|
||||
return std::numeric_limits<size_t>::max();
|
||||
}
|
||||
|
||||
MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState)
|
||||
|
@ -118,6 +118,12 @@ size_t CTrueTypeFont::getStringWidthScaled(const std::string & text) const
|
||||
{
|
||||
int width;
|
||||
TTF_SizeUTF8(font.get(), text.c_str(), &width, nullptr);
|
||||
|
||||
if (outline)
|
||||
width += getScalingFactor();
|
||||
if (dropShadow || outline)
|
||||
width += getScalingFactor();
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,8 @@ void ButtonBase::update()
|
||||
// hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted
|
||||
if (state == EButtonState::HIGHLIGHTED && image->size() < 4)
|
||||
image->setFrame(image->size()-1);
|
||||
image->setFrame(stateToIndex[vstd::to_underlying(state)]);
|
||||
else
|
||||
image->setFrame(stateToIndex[vstd::to_underlying(state)]);
|
||||
}
|
||||
|
||||
if (isActive())
|
||||
|
@ -140,6 +140,8 @@ void CArtifactsOfHeroBase::gestureArtPlace(CComponentHolder & artPlace, const Po
|
||||
void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
|
||||
{
|
||||
curHero = hero;
|
||||
if (!hero)
|
||||
return;
|
||||
|
||||
for(auto slot : artWorn)
|
||||
{
|
||||
|
@ -28,7 +28,8 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
|
||||
|
||||
CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
|
||||
{
|
||||
CArtifactsOfHeroBase::putBackPickedArtifact();
|
||||
if(curHero)
|
||||
CArtifactsOfHeroBase::putBackPickedArtifact();
|
||||
}
|
||||
|
||||
void CArtifactsOfHeroMain::keyPressed(EShortcut key)
|
||||
|
@ -279,7 +279,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
|
||||
std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other");
|
||||
int maxLen = 50;
|
||||
EFonts f = FONT_TINY;
|
||||
Point pText = p + Point(3, 40);
|
||||
Point pText = p + Point(4, 38);
|
||||
|
||||
// 1px Black border
|
||||
bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));
|
||||
|
@ -312,6 +312,7 @@ void CHeroWindow::dismissCurrent()
|
||||
arts->putBackPickedArtifact();
|
||||
close();
|
||||
LOCPLINT->cb->dismissHero(curHero);
|
||||
arts->setHero(nullptr);
|
||||
}, nullptr);
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,13 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
|
||||
color = "";
|
||||
}
|
||||
else
|
||||
printableString.append(text.data() + currPos, symbolSize);
|
||||
{
|
||||
std::string character = "";
|
||||
character.append(text.data() + currPos, symbolSize);
|
||||
if(fontPtr->getStringWidth(printableString + character) > maxLineWidth)
|
||||
break;
|
||||
printableString += character;
|
||||
}
|
||||
|
||||
currPos += symbolSize;
|
||||
}
|
||||
|
@ -1508,7 +1508,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
|
||||
itemsVisible = items;
|
||||
|
||||
init(titleWidget_, _title, _descr, searchBoxEnabled);
|
||||
list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements)
|
||||
list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
|
||||
}
|
||||
|
||||
CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, std::function<void(int)> Callback, size_t initialSelection, std::vector<std::shared_ptr<IImage>> images, bool searchBoxEnabled)
|
||||
@ -1528,7 +1528,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
|
||||
itemsVisible = items;
|
||||
|
||||
init(titleWidget_, _title, _descr, searchBoxEnabled);
|
||||
list->scrollTo(initialSelection - 4); // -4 is for centering (list have 9 elements)
|
||||
list->scrollTo(std::min(static_cast<int>(initialSelection + 4), static_cast<int>(items.size() - 1))); // 4 is for centering (list have 9 elements)
|
||||
}
|
||||
|
||||
void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled)
|
||||
@ -1653,7 +1653,7 @@ void CObjectListWindow::keyPressed(EShortcut key)
|
||||
}
|
||||
|
||||
vstd::abetween<int>(sel, 0, itemsVisible.size()-1);
|
||||
list->scrollTo(sel - 4); // -4 is for centering (list have 9 elements)
|
||||
list->scrollTo(sel);
|
||||
changeSelection(sel);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,86 @@
|
||||
{
|
||||
"maxRoamingHeroes" : 8,
|
||||
"maxpass" : 30,
|
||||
"mainHeroTurnDistanceLimit" : 10,
|
||||
"scoutHeroTurnDistanceLimit" : 5,
|
||||
"maxGoldPressure" : 0.3,
|
||||
"useTroopsFromGarrisons" : true,
|
||||
"openMap": true,
|
||||
"allowObjectGraph": true
|
||||
"pawn" : {
|
||||
"maxRoamingHeroes" : 8,
|
||||
"maxpass" : 30,
|
||||
"mainHeroTurnDistanceLimit" : 10,
|
||||
"scoutHeroTurnDistanceLimit" : 5,
|
||||
"maxGoldPressure" : 0.3,
|
||||
"useTroopsFromGarrisons" : true,
|
||||
"openMap": false,
|
||||
"allowObjectGraph": false,
|
||||
"pathfinderBucketsCount" : 1, // old value: 3,
|
||||
"pathfinderBucketSize" : 32, // old value: 7,
|
||||
"retreatThresholdRelative" : 0.3,
|
||||
"retreatThresholdAbsolute" : 10000,
|
||||
"safeAttackRatio" : 1.1,
|
||||
"useFuzzy" : false
|
||||
},
|
||||
|
||||
"knight" : {
|
||||
"maxRoamingHeroes" : 8,
|
||||
"maxpass" : 30,
|
||||
"mainHeroTurnDistanceLimit" : 10,
|
||||
"scoutHeroTurnDistanceLimit" : 5,
|
||||
"maxGoldPressure" : 0.3,
|
||||
"useTroopsFromGarrisons" : true,
|
||||
"openMap": false,
|
||||
"allowObjectGraph": false,
|
||||
"pathfinderBucketsCount" : 1, // old value: 3,
|
||||
"pathfinderBucketSize" : 32, // old value: 7,
|
||||
"retreatThresholdRelative" : 0.3,
|
||||
"retreatThresholdAbsolute" : 10000,
|
||||
"safeAttackRatio" : 1.1,
|
||||
"useFuzzy" : false
|
||||
},
|
||||
|
||||
"rook" : {
|
||||
"maxRoamingHeroes" : 8,
|
||||
"maxpass" : 30,
|
||||
"mainHeroTurnDistanceLimit" : 10,
|
||||
"scoutHeroTurnDistanceLimit" : 5,
|
||||
"maxGoldPressure" : 0.3,
|
||||
"useTroopsFromGarrisons" : true,
|
||||
"openMap": false,
|
||||
"allowObjectGraph": false,
|
||||
"pathfinderBucketsCount" : 1, // old value: 3,
|
||||
"pathfinderBucketSize" : 32, // old value: 7,
|
||||
"retreatThresholdRelative" : 0.3,
|
||||
"retreatThresholdAbsolute" : 10000,
|
||||
"safeAttackRatio" : 1.1,
|
||||
"useFuzzy" : false
|
||||
},
|
||||
|
||||
"queen" : {
|
||||
"maxRoamingHeroes" : 8,
|
||||
"maxpass" : 30,
|
||||
"mainHeroTurnDistanceLimit" : 10,
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
}
|
||||
}
|
@ -488,6 +488,20 @@
|
||||
// if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied
|
||||
"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":
|
||||
{
|
||||
|
@ -106,7 +106,7 @@
|
||||
"defaultTavern" : 5,
|
||||
"affinity" : "might",
|
||||
"commander" : "medusaQueen",
|
||||
"mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } },
|
||||
"mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } },
|
||||
"animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
|
||||
},
|
||||
"warlock" :
|
||||
@ -116,7 +116,7 @@
|
||||
"defaultTavern" : 5,
|
||||
"affinity" : "magic",
|
||||
"commander" : "medusaQueen",
|
||||
"mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } },
|
||||
"mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } },
|
||||
"animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } }
|
||||
},
|
||||
"barbarian" :
|
||||
|
@ -133,6 +133,14 @@
|
||||
"originalFlyRules" : { "type" : "boolean" }
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type" : "object",
|
||||
"additionalProperties" : false,
|
||||
"properties" : {
|
||||
"weeklyBonusesAI" : { "type" : "object" }
|
||||
}
|
||||
},
|
||||
|
||||
"spells": {
|
||||
"type" : "object",
|
||||
"additionalProperties" : false,
|
||||
|
@ -620,7 +620,6 @@
|
||||
"defaultRepositoryURL",
|
||||
"extraRepositoryURL",
|
||||
"extraRepositoryEnabled",
|
||||
"enableInstalledMods",
|
||||
"autoCheckRepositories",
|
||||
"ignoreSslErrors",
|
||||
"updateOnStartup",
|
||||
@ -647,10 +646,6 @@
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
},
|
||||
"enableInstalledMods" : {
|
||||
"type" : "boolean",
|
||||
"default" : true
|
||||
},
|
||||
"ignoreSslErrors" : {
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
|
@ -12,7 +12,7 @@
|
||||
"properties" : {
|
||||
"type" : {
|
||||
"type" : "string",
|
||||
"enum" : ["playerStart", "cpuStart", "treasure", "junction"]
|
||||
"enum" : ["playerStart", "cpuStart", "treasure", "junction", "sealed"]
|
||||
},
|
||||
"size" : { "type" : "number", "minimum" : 1 },
|
||||
"owner" : {},
|
||||
|
@ -1,11 +1,11 @@
|
||||
# VCMI Project
|
||||
|
||||
[](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.7)
|
||||
[](https://github.com/vcmi/vcmi/releases)
|
||||
|
||||
# VCMI Project
|
||||
|
||||
VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
|
||||
|
||||
<p>
|
||||
@ -15,14 +15,13 @@ VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving
|
||||
<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Quick%20Hero%20Select%20Bastion.jpg?raw=true" alt="New widget for Hero selection, featuring Pavillon Town" style="height:120px;"/>
|
||||
</p>
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
* Homepage: https://vcmi.eu/
|
||||
* Forums: https://forum.vcmi.eu/
|
||||
* Bugtracker: https://github.com/vcmi/vcmi/issues
|
||||
* Discord: https://discord.gg/chBT42V
|
||||
* GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant
|
||||
* Homepage: <https://vcmi.eu/>
|
||||
* Forums: <https://forum.vcmi.eu/>
|
||||
* Bugtracker: <https://github.com/vcmi/vcmi/issues>
|
||||
* Discord: <https://discord.gg/chBT42V>
|
||||
* GPT Store: <https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant>
|
||||
|
||||
## Latest release
|
||||
|
||||
@ -31,6 +30,7 @@ Loading saves made with different major version of VCMI is usually **not** suppo
|
||||
Please see corresponding installation guide articles for details for your platform.
|
||||
|
||||
## Installation guides
|
||||
|
||||
- [Windows](players/Installation_Windows.md)
|
||||
- [macOS](players/Installation_macOS.md)
|
||||
- [Linux](players/Installation_Linux.md)
|
||||
@ -70,6 +70,7 @@ See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md
|
||||
## Documentation and guidelines for developers
|
||||
|
||||
Development environment setup instructions:
|
||||
|
||||
- [Building VCMI for Android](developers/Building_Android.md)
|
||||
- [Building VCMI for iOS](developers/Building_iOS.md)
|
||||
- [Building VCMI for Linux](developers/Building_Linux.md)
|
||||
@ -78,6 +79,7 @@ Development environment setup instructions:
|
||||
- [Conan](developers/Conan.md)
|
||||
|
||||
Engine documentation: (NOTE: may be outdated)
|
||||
|
||||
- [Development with Qt Creator](developers/Development_with_Qt_Creator.md)
|
||||
- [Coding Guidelines](developers/Coding_Guidelines.md)
|
||||
- [Bonus System](developers/Bonus_System.md)
|
||||
@ -95,6 +97,6 @@ Engine documentation: (NOTE: may be outdated)
|
||||
## Copyright and license
|
||||
|
||||
VCMI Project source code is licensed under GPL version 2 or later.
|
||||
VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets
|
||||
VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: <https://github.com/vcmi/vcmi-assets>
|
||||
|
||||
Copyright (C) 2007-2024 VCMI Team (check AUTHORS file for the contributors list)
|
||||
|
@ -6,18 +6,19 @@ There are two types of AI: adventure and battle.
|
||||
**Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield
|
||||
|
||||
We have 3 battle AIs so far:
|
||||
|
||||
* BattleAI - strongest
|
||||
* StupidAI - for neutrals, should be simple so that experienced players can abuse it
|
||||
* Empty AI - should do nothing at all. If needed another battle AI can be introduced.
|
||||
|
||||
Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition.
|
||||
Each side in a battle is represented by an CArmedInstance object. CHeroInstance and CGDwelling, CGMonster and more are subclasses of CArmedInstance. CArmedInstance contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface battle::Unit *.
|
||||
Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is `activeStack(battle::Unit * stack)`. It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as `cb`. `CPlayerSpecificCallback` it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition.
|
||||
Each side in a battle is represented by an `CArmedInstance` object. `CHeroInstance` and `CGDwelling`, `CGMonster` and more are subclasses of `CArmedInstance`. `CArmedInstance` contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface `battle::Unit *`.
|
||||
Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retaliation, shooter or not, initial count of shots and so on.
|
||||
When you call unit->getAttack() it summarizes all these bonuses and returns the resulting value.
|
||||
When you call `unit->getAttack()` it summarizes all these bonuses and returns the resulting value.
|
||||
|
||||
One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change.
|
||||
One important class is `HypotheticBattle`. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around `CPlayerSpecificCallback` or another `HypotheticBattle` so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (`CStackWithBonuses`). So if you need to emulate an attack you can call `hypotheticbattle.getforupdate()` and it will return the `CStackWithBonuses` which you can safely change.
|
||||
|
||||
## BattleAI
|
||||
## BattleAI
|
||||
|
||||
BattleAI's most important classes are the following:
|
||||
|
||||
@ -38,17 +39,20 @@ BattleAI itself handles all the rest and issues actual commands
|
||||
Adventure AI responsible for moving heroes on map, gathering things, developing town. Main idea is to gather all possible tasks on map, prioritize them and select the best one for each heroes. Initially was a fork of VCAI
|
||||
|
||||
### Parts
|
||||
|
||||
Gateway - a callback for server used to invoke AI actions when server thinks it is time to do something. Through this callback AI is informed about various events like hero level up, tile revialed, blocking dialogs and so on. In order to do this Gaateway implements specific interface. The interface is exactly the same for human and AI
|
||||
Another important actor for server interaction is CCallback * cb. This one is used to retrieve gamestate information and ask server to do things like hero moving, spell casting and so on. Each AI has own instance of Gateway and it is a root object which holds all AI state. Gateway has an event method yourTurn which invokes makeTurn in another thread. The last passes control to Nullkiller engine.
|
||||
|
||||
Nullkiller engine - place where actual AI logic is organized. It contains a main loop for gathering and prioritizing things. Its algorithm:
|
||||
|
||||
* reset AI state, it avoids keeping some memory about the game in general to reduce amount of things serialized into savefile state. The only serialized things are in nullkiller->memory. This helps reducing save incompatibility. It should be mostly enough for AI to analyze data avaialble in CCallback
|
||||
* main loop, loop iteration is called a pass
|
||||
** update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work
|
||||
** gathering goals, prioritizing and decomposing them
|
||||
** execute selected best goals
|
||||
* update AI state, some state is lazy and updates once per day to avoid performance hit, some state is recalculated each loop iteration. At this stage analysers and pathfidner work
|
||||
* gathering goals, prioritizing and decomposing them
|
||||
* execute selected best goals
|
||||
|
||||
Analyzer - a module gathering data from CCallback *. Its goal to make some statistics and avoid making any significant decissions.
|
||||
|
||||
* HeroAnalyser - decides upong which hero suits better to be main (army carrier and fighter) and which is better to be a scout (gathering unguarded resources, exploring)
|
||||
* BuildAnalyzer - prepares information on what we can build in our towns, and what resources we need to do this
|
||||
* DangerHitMapAnalyser - checks if enemy hero can rich each tile, how fast and what is their army strangth
|
||||
@ -61,9 +65,11 @@ Analyzer - a module gathering data from CCallback *. Its goal to make some stati
|
||||
* PriorityEvaluator - gathers information on task rewards, evaluates their priority using Fuzzy Light library (fuzzy logic)
|
||||
|
||||
### Goals
|
||||
|
||||
Units of activity in AI. Can be AbstractGoal, Task, Marker and Behavior
|
||||
|
||||
Task - simple thing which can be done right away in order to gain some reward. Or a composition of simple things in case if more than one action is needed to gain the reward.
|
||||
|
||||
* AdventureSpellCast - town portal, water walk, air walk, summon boat
|
||||
* BuildBoat - builds a boat in a specific shipyard
|
||||
* BuildThis - builds a building in a specified town
|
||||
@ -78,6 +84,7 @@ Task - simple thing which can be done right away in order to gain some reward. O
|
||||
* StayAtTown - stay at town for the rest of the day (to regain mana)
|
||||
|
||||
Behavior - a core game activity
|
||||
|
||||
* CaptureObjectsBehavior - generally it is about visiting map objects which give reward. It can capture any object, even those which are behind monsters and so on. But due to performance considerations it is not allowed to handle monsters and quests now.
|
||||
* ClusterBehavior - uses information of ObjectClusterizer to unblock objects hidden behind various blockers. It kills guards, completes quests, captures garrisons.
|
||||
* BuildingBehavior - develops our towns
|
||||
@ -89,6 +96,7 @@ Behavior - a core game activity
|
||||
* DefenceBehavior - defend towns by eliminating treatening heroes or hiding in town garrison
|
||||
|
||||
AbstractGoal - some goals can not be completed because it is not clear how to do this. They express desire to do something, not exact plan. DeepDecomposer is used to refine such goals until they are turned into such plan or discarded. Some examples:
|
||||
|
||||
* CaptureObject - you need to visit some object (flag a shipyard for instance) but do not know how
|
||||
* CompleteQuest - you need to bypass bordergate or borderguard or questguard but do not know how
|
||||
AbstractGoal usually comes in form of composition with some elementar task blocked by abstract objective. For instance CaptureObject(Shipyard), ExecuteHeroChain(visit x, build boat, visit enemy town). When such composition is decomposed it can turn into either a pair of herochains or into another abstract composition if path to shipyard is also blocked with something.
|
||||
|
@ -6,8 +6,8 @@ The bonus system of VCMI is a set of mechanisms that make handling of different
|
||||
|
||||
Each bonus originates from some node in the bonus system, and may have propagator and limiter objects attached to it. Bonuses are shared around as follows:
|
||||
|
||||
1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated.
|
||||
2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes.
|
||||
1. Bonuses with propagator are propagated to "matching" descendants in the red DAG - which descendants match is determined by the propagator. Bonuses without a propagator will not be propagated.
|
||||
2. Bonuses without limiters are inherited by all descendants in the black DAG. If limiters are present, they can restrict inheritance to certain nodes.
|
||||
|
||||
Inheritance is the default means of sharing bonuses. A typical example is an artefact granting a bonus to attack/defense stat, which is inherited by the hero wearing it, and then by creatures in the hero's army.
|
||||
A common limiter is by creature - e.g. the hero Eric has a specialty that grants bonuses to attack, defense and speed, but only to griffins.
|
||||
@ -15,9 +15,9 @@ Propagation is used when bonuses need to be shared in a different direction than
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added.
|
||||
- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance.
|
||||
- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current.
|
||||
- Propagation is done by copying bonuses to the target nodes. This happens when bonuses are added.
|
||||
- Inheritance is done on-the-fly when needed, by traversing the black DAG. Results are cached to improve performance.
|
||||
- Whenever a node changes (e.g. bonus added), a global counter gets increased which is used to check whether cached results are still current.
|
||||
|
||||
## Operations on the graph
|
||||
|
||||
@ -26,6 +26,7 @@ There are two basic types of operations that can be performed on the graph:
|
||||
### Adding a new node
|
||||
|
||||
When node is attached to a new black parent (the only possibility - adding parent is the same as adding a child to it), the propagation system is triggered and works as follows:
|
||||
|
||||
- For the attached node and its all red ancestors
|
||||
- For every bonus
|
||||
- Call propagator giving the new descendant - then attach appropriately bonuses to the red descendant of attached node (or the node itself).
|
||||
@ -54,7 +55,7 @@ Updaters are objects attached to bonuses. They can modify a bonus (typically by
|
||||
|
||||
The following example shows an artifact providing a bonus based on the level of the hero that wears it:
|
||||
|
||||
```javascript
|
||||
```json5
|
||||
"core:greaterGnollsFlail":
|
||||
{
|
||||
"text" : { "description" : "This mighty flail increases the attack of all gnolls under the hero's command by twice the hero's level." },
|
||||
|
@ -1,26 +1,26 @@
|
||||
# Building Android
|
||||
|
||||
The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is https://github.com/vcmi/vcmi-android/blob/master/building.txt (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole.
|
||||
The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is <https://github.com/vcmi/vcmi-android/blob/master/building.txt> (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole.
|
||||
|
||||
*Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. CMake 3.20+: download from your package manager or from https://cmake.org/download/
|
||||
1. CMake 3.20+: download from your package manager or from <https://cmake.org/download/>
|
||||
2. JDK 11, not necessarily from Oracle
|
||||
3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/
|
||||
3. Android command line tools or Android Studio for your OS: <https://developer.android.com/studio/>
|
||||
4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it:
|
||||
- install with Android Studio
|
||||
- install with `sdkmanager` command line tool
|
||||
- download from https://developer.android.com/ndk/downloads
|
||||
- download from <https://developer.android.com/ndk/downloads>
|
||||
- download with Conan, see [#NDK and Conan](#ndk-and-conan)
|
||||
5. Optional:
|
||||
- Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases
|
||||
- Ccache: download from your package manager or from https://github.com/ccache/ccache/releases
|
||||
- Ninja: download from your package manager or from <https://github.com/ninja-build/ninja/releases>
|
||||
- Ccache: download from your package manager or from <https://github.com/ccache/ccache/releases>
|
||||
|
||||
## Obtaining source code
|
||||
|
||||
Clone https://github.com/vcmi/vcmi with submodules. Example for command line:
|
||||
Clone <https://github.com/vcmi/vcmi> with submodules. Example for command line:
|
||||
|
||||
```
|
||||
git clone --recurse-submodules https://github.com/vcmi/vcmi.git
|
||||
@ -31,6 +31,7 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git
|
||||
We use Conan package manager to build/consume dependencies, find detailed usage instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Сonan.md).
|
||||
|
||||
On the step where you need to replace **PROFILE**, choose:
|
||||
|
||||
- `android-32` to build for 32-bit architecture (armeabi-v7a)
|
||||
- `android-64` to build for 64-bit architecture (aarch64-v8a)
|
||||
|
||||
@ -38,7 +39,7 @@ On the step where you need to replace **PROFILE**, choose:
|
||||
|
||||
Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html):
|
||||
|
||||
- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose _android-**X**-ndk_ where _**X**_ is either `32` or `64`.
|
||||
- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose *android-**X**-ndk* where ***X*** is either `32` or `64`.
|
||||
- to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will work only when consuming the pre-built binaries)
|
||||
|
||||
```
|
||||
|
@ -11,15 +11,15 @@ Older distributions and compilers might work, but they aren't tested by Github C
|
||||
|
||||
To compile, the following packages (and their development counterparts) are needed to build:
|
||||
|
||||
- CMake
|
||||
- SDL2 with devel packages: mixer, image, ttf
|
||||
- zlib and zlib-devel
|
||||
- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale
|
||||
- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules
|
||||
- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names.
|
||||
- Optional:
|
||||
- if you want to build scripting modules: LuaJIT
|
||||
- to speed up recompilation: Ccache
|
||||
- CMake
|
||||
- SDL2 with devel packages: mixer, image, ttf
|
||||
- zlib and zlib-devel
|
||||
- Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale
|
||||
- Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules
|
||||
- Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names.
|
||||
- Optional:
|
||||
- if you want to build scripting modules: LuaJIT
|
||||
- to speed up recompilation: Ccache
|
||||
|
||||
### On Debian-based systems (e.g. Ubuntu)
|
||||
|
||||
@ -41,7 +41,7 @@ NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedo
|
||||
|
||||
On Arch-based distributions, there is a development package available for VCMI on the AUR.
|
||||
|
||||
It can be found at https://aur.archlinux.org/packages/vcmi-git/
|
||||
It can be found at <https://aur.archlinux.org/packages/vcmi-git/>
|
||||
|
||||
Information about building packages from the Arch User Repository (AUR) can be found at the Arch wiki.
|
||||
|
||||
@ -109,9 +109,9 @@ This will generate `vcmiclient`, `vcmiserver`, `vcmilauncher` as well as .so lib
|
||||
|
||||
### RPM package
|
||||
|
||||
The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview
|
||||
The first step is to prepare a RPM build environment. On Fedora systems you can follow this guide: <http://fedoraproject.org/wiki/How_to_create_an_RPM_package#SPEC_file_overview>
|
||||
|
||||
0. Enable RPMFusion free repo to access to ffmpeg libs:
|
||||
1. Enable RPMFusion free repo to access to ffmpeg libs:
|
||||
|
||||
```sh
|
||||
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
|
||||
@ -120,33 +120,34 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
|
||||
> [!NOTE]
|
||||
> The stock ffmpeg from Fedora repo is no good as it lacks a lots of codecs
|
||||
|
||||
1. Perform a git clone from a tagged branch for the right Fedora version from https://github.com/rpmfusion/vcmi; for example for Fedora 38: <pre>git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git</pre>
|
||||
2. Perform a git clone from a tagged branch for the right Fedora version from <https://github.com/rpmfusion/vcmi>; for example for Fedora 38: <pre>git clone -b f38 --single-branch https://github.com/rpmfusion/vcmi.git</pre>
|
||||
|
||||
2. Copy all files to ~/rpmbuild/SPECS with command: <pre>cp vcmi/* ~/rpmbuild/SPECS</pre>
|
||||
3. Copy all files to ~/rpmbuild/SPECS with command: <pre>cp vcmi/* ~/rpmbuild/SPECS</pre>
|
||||
|
||||
3. Fetch all sources by using spectool:
|
||||
4. Fetch all sources by using spectool:
|
||||
|
||||
```sh
|
||||
sudo dnf install rpmdevtools
|
||||
spectool -g -R ~/rpmbuild/SPECS/vcmi.spec
|
||||
```
|
||||
|
||||
4. Fetch all dependencies required to build the RPM:
|
||||
5. Fetch all dependencies required to build the RPM:
|
||||
|
||||
```sh
|
||||
sudo dnf install dnf-plugins-core
|
||||
sudo dnf builddep ~/rpmbuild/SPECS/vcmi.spec
|
||||
```
|
||||
|
||||
4. Go to ~/rpmbuild/SPECS and open terminal in this folder and type:
|
||||
6. Go to ~/rpmbuild/SPECS and open terminal in this folder and type:
|
||||
|
||||
```sh
|
||||
rpmbuild -ba ~/rpmbuild/SPECS/vcmi.spec
|
||||
```
|
||||
|
||||
5. Generated RPM is in folder ~/rpmbuild/RPMS
|
||||
7. Generated RPM is in folder ~/rpmbuild/RPMS
|
||||
|
||||
If you want to package the generated RPM above for different processor architectures and operating systems you can use the tool mock.
|
||||
Moreover, it is necessary to install mock-rpmfusion_free due to the packages ffmpeg-devel and ffmpeg-libs which aren't available in the standard RPM repositories(at least for Fedora). Go to ~/rpmbuild/SRPMS in terminal and type:
|
||||
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:
|
||||
|
||||
```sh
|
||||
mock -r fedora-38-aarch64-rpmfusion_free path_to_source_RPM
|
||||
|
@ -12,8 +12,8 @@ Windows builds can be made in more than one way and with more than one tool. Thi
|
||||
- CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users".
|
||||
- To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html)
|
||||
- Optional:
|
||||
- To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page)
|
||||
- To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases)
|
||||
- To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page)
|
||||
- To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases)
|
||||
|
||||
### Choose an installation directory
|
||||
|
||||
@ -21,12 +21,14 @@ Create a directory for VCMI development, eg. `C:\VCMI` We will call this directo
|
||||
|
||||
Warning! Replace `%VCMI_DIR%` with path you've chosen for VCMI installation in the following commands.
|
||||
|
||||
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:
|
||||
|
||||
- `C:\VCMI`
|
||||
|
||||
Bad locations:
|
||||
|
||||
- `C:\Users\Michał\VCMI (non-ascii character)`
|
||||
- `C:\Program Files (x86)\VCMI (write protection)`
|
||||
|
||||
@ -38,13 +40,14 @@ You have two options: to use pre-built libraries or build your own. We strongly
|
||||
|
||||
#### Download and unpack archive
|
||||
|
||||
Vcpkg Archives are available at our GitHub: https://github.com/vcmi/vcmi-deps-windows/releases
|
||||
Vcpkg Archives are available at our GitHub: <https://github.com/vcmi/vcmi-deps-windows/releases>
|
||||
|
||||
- Download latest version available.
|
||||
EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z)
|
||||
- Extract archive by right clicking on it and choosing "7-zip -> Extract Here".
|
||||
|
||||
#### Move dependencies to target directory
|
||||
|
||||
Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside.
|
||||
Move extracted `vcpkg` directory into your `%VCMI_DIR%`
|
||||
|
||||
@ -57,19 +60,21 @@ Be aware that building Vcpkg might take a lot of time depend on your CPU model a
|
||||
|
||||
#### Clone vcpkg
|
||||
|
||||
1. open SourceTree
|
||||
2. File -\> Clone
|
||||
3. select **<https://github.com/microsoft/vcpkg/>** as source
|
||||
4. select **%VCMI_DIR%/vcpkg** as destination
|
||||
5. click **Clone**
|
||||
1. open SourceTree
|
||||
2. File -\> Clone
|
||||
3. select **<https://github.com/microsoft/vcpkg/>** as source
|
||||
4. select **%VCMI_DIR%/vcpkg** as destination
|
||||
5. click **Clone**
|
||||
|
||||
From command line use:
|
||||
|
||||
git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
|
||||
```
|
||||
|
||||
#### Build vcpkg and dependencies
|
||||
|
||||
- Run
|
||||
- Run
|
||||
`%VCMI_DIR%/vcpkg/bootstrap-vcpkg.bat`
|
||||
- For 32-bit build run:
|
||||
`%VCMI_DIR%/vcpkg/vcpkg.exe install tbb:x64-windows fuzzylite:x64-windows sdl2:x64-windows sdl2-image:x64-windows sdl2-ttf:x64-windows sdl2-mixer[mpg123]:x64-windows boost:x64-windows qt5-base:x64-windows ffmpeg:x64-windows luajit:x64-windows`
|
||||
@ -85,6 +90,7 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
|
||||
## Build VCMI
|
||||
|
||||
#### From GIT GUI
|
||||
|
||||
- Open SourceTree
|
||||
- File -> Clone
|
||||
- select `https://github.com/vcmi/vcmi/` as source
|
||||
@ -94,26 +100,30 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
|
||||
- click Clone
|
||||
|
||||
#### From command line
|
||||
|
||||
- `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source`
|
||||
|
||||
### Generate solution for VCMI
|
||||
|
||||
- Create `%VCMI_DIR%/build` folder
|
||||
- Open a command line prompt at `%VCMI_DIR%/build`
|
||||
- Execute `cd %VCMI_DIR%/build`
|
||||
- Execute `cd %VCMI_DIR%/build`
|
||||
- Create solution (Visual Studio 2022 64-bit) `cmake %VCMI_DIR%/source -DCMAKE_TOOLCHAIN_FILE=%VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake -G "Visual Studio 17 2022" -A x64`
|
||||
|
||||
### Compile VCMI with Visual Studio
|
||||
|
||||
- Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio
|
||||
- Select `Release` build type in the combobox
|
||||
- If you want to use ccache:
|
||||
- Select `Manage Configurations...` in the combobox
|
||||
- Specify the following CMake variable: `ENABLE_CCACHE=ON`
|
||||
- See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details
|
||||
- Select `Manage Configurations...` in the combobox
|
||||
- Specify the following CMake variable: `ENABLE_CCACHE=ON`
|
||||
- See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details
|
||||
- Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer.
|
||||
- VCMI will be built in `%VCMI_DIR%/build/bin` folder!
|
||||
|
||||
### Compile VCMI with MinGW via MSYS2
|
||||
- Install MSYS2 from https://www.msys2.org/
|
||||
|
||||
- Install MSYS2 from <https://www.msys2.org/>
|
||||
- Start the `MSYS MinGW x64`-shell
|
||||
- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static mingw-w64-x86_64-qt5-tools mingw-w64-x86_64-tbb`
|
||||
- Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release`
|
||||
@ -134,8 +144,10 @@ Vcpkg might be very unstable due to limited popularity and fact of using bleedin
|
||||
|
||||
Pre-built version we provide is always manually tested with all supported versions of MSVC for both Release and Debug builds and all known quirks are listed below.
|
||||
|
||||
#$# Build is successful but can not start new game
|
||||
### Build is successful but can not start new game
|
||||
|
||||
Make sure you have:
|
||||
|
||||
* Installed Heroes III from disk or using GOG installer
|
||||
* Copied `Data`, `Maps` and `Mp3` folders from Heroes III to: `%USERPROFILE%\Documents\My Games\vcmi\`
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
2. Xcode: <https://developer.apple.com/xcode/>
|
||||
3. CMake 3.21+: `brew install --cask cmake` or get from <https://cmake.org/download/>
|
||||
4. Optional:
|
||||
- CCache to speed up recompilation: `brew install ccache`
|
||||
- CCache to speed up recompilation: `brew install ccache`
|
||||
|
||||
## Obtaining source code
|
||||
|
||||
|
@ -91,7 +91,7 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h
|
||||
|
||||
## Packaging project into DMG file
|
||||
|
||||
After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C `<configuration name> with the same configuration that you used to build the project.
|
||||
After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C <configuration name>` with the same configuration that you used to build the project.
|
||||
|
||||
If you use Conan, it's expected that you use **conan-generated** directory at step 4 of [Conan package manager](Conan.md).
|
||||
|
||||
|
@ -1,23 +1,21 @@
|
||||
# CMake options
|
||||
|
||||
* `-D CMAKE_BUILD_TYPE=Debug`
|
||||
* Enables debug info and disables optimizations
|
||||
* Enables debug info and disables optimizations
|
||||
* `-D CMAKE_EXPORT_COMPILE_COMMANDS=ON`
|
||||
* Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server.
|
||||
|
||||
For clangd to find the JSON, create a file named `.clangd` with this content
|
||||
```
|
||||
CompileFlags:
|
||||
CompilationDatabase: build
|
||||
```
|
||||
and place it here:
|
||||
```
|
||||
.
|
||||
├── vcmi -> contains sources and is under git control
|
||||
├── build -> contains build output, makefiles, object files,...
|
||||
└── .clangd
|
||||
```
|
||||
* Creates `compile_commands.json` for [clangd](https://clangd.llvm.org/) language server. For clangd to find the JSON, create a file named `.clangd` with this content
|
||||
```
|
||||
CompileFlags:
|
||||
CompilationDatabase: build
|
||||
```
|
||||
and place it here:
|
||||
```
|
||||
.
|
||||
├── vcmi -> contains sources and is under git control
|
||||
├── build -> contains build output, makefiles, object files,...
|
||||
└── .clangd
|
||||
```
|
||||
* `-D ENABLE_CCACHE:BOOL=ON`
|
||||
* Speeds up recompilation
|
||||
* Speeds up recompilation
|
||||
* `-G Ninja`
|
||||
* Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag
|
||||
* Use Ninja build system instead of Make, which speeds up the build and doesn't require a `-j` flag
|
||||
|
@ -29,9 +29,10 @@ Most of VCMI configuration files uses Json format and located in "config" direct
|
||||
### Main purposes of client
|
||||
|
||||
Client is responsible for:
|
||||
- displaying state of game to human player
|
||||
- capturing player's actions and sending requests to server
|
||||
- displaying changes in state of game indicated by server
|
||||
|
||||
- displaying state of game to human player
|
||||
- capturing player's actions and sending requests to server
|
||||
- displaying changes in state of game indicated by server
|
||||
|
||||
### Rendering of graphics
|
||||
|
||||
@ -44,9 +45,9 @@ In rendering, Interface object system is quite helpful. Its base is CIntObject c
|
||||
|
||||
Server is responsible for:
|
||||
|
||||
- maintaining state of the game
|
||||
- handling requests from all clients participating in game
|
||||
- informing all clients about changes in state of the game that are
|
||||
- maintaining state of the game
|
||||
- handling requests from all clients participating in game
|
||||
- informing all clients about changes in state of the game that are
|
||||
visible to them
|
||||
|
||||
## Lib
|
||||
@ -59,11 +60,11 @@ iOS platform pioneered single process build, where server is a static library an
|
||||
|
||||
Lib contains code responsible for:
|
||||
|
||||
- handling most of Heroes III files (.lod, .txt setting files)
|
||||
- storing information common to server and client like state of the game
|
||||
- managing armies, buildings, artifacts, spells, bonuses and other game objects
|
||||
- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server)
|
||||
- networking and serialization
|
||||
- handling most of Heroes III files (.lod, .txt setting files)
|
||||
- storing information common to server and client like state of the game
|
||||
- managing armies, buildings, artifacts, spells, bonuses and other game objects
|
||||
- handling general game mechanics and related actions (only adventure map objects; it's an unwanted remnant of past development - all game mechanics should be handled by the server)
|
||||
- networking and serialization
|
||||
|
||||
#### Serialization
|
||||
|
||||
@ -94,7 +95,6 @@ Forward declarations of the lib in headers of other parts of the project need to
|
||||
`<other forward declarations>`
|
||||
`<classes>`
|
||||
|
||||
|
||||
##### New project part
|
||||
|
||||
If you're creating new project part, place `VCMI_LIB_USING_NAMESPACE` in its `StdInc.h` to be able to use lib classes without explicit namespace in implementation files. Example: <https://github.com/vcmi/vcmi/blob/develop/launcher/StdInc.h>
|
||||
@ -121,4 +121,4 @@ VCMI includes [FuzzyLite](http://code.google.com/p/fuzzy-lite/) library to make
|
||||
|
||||
### Duels
|
||||
|
||||
### ERM parser
|
||||
### ERM parser
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
VCMI implementation bases on C++17 standard. Any feature is acceptable as long as it's will pass build on our CI, but there is list below on what is already being used.
|
||||
|
||||
Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at http://en.cppreference.com/w/cpp/compiler_support
|
||||
Any compiler supporting C++17 should work, but this has not been thoroughly tested. You can find information about extensions and compiler support at <http://en.cppreference.com/w/cpp/compiler_support>
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
@ -20,7 +20,7 @@ Inside a code block put the opening brace on the next line after the current sta
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
code();
|
||||
@ -30,7 +30,7 @@ if(a)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a) {
|
||||
code();
|
||||
code();
|
||||
@ -41,14 +41,14 @@ Avoid using unnecessary open/close braces, vertical space is usually limited:
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
code();
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a) {
|
||||
code();
|
||||
}
|
||||
@ -58,7 +58,7 @@ Unless there are either multiple hierarchical conditions being used or that the
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
if(b)
|
||||
@ -68,7 +68,7 @@ if(a)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
if(b)
|
||||
code();
|
||||
@ -78,7 +78,7 @@ If there are brackets inside the body, outside brackets are required.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
for(auto elem : list)
|
||||
@ -90,7 +90,7 @@ if(a)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
for(auto elem : list)
|
||||
{
|
||||
@ -102,7 +102,7 @@ If "else" branch has brackets then "if" should also have brackets even if it is
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
code();
|
||||
@ -118,7 +118,7 @@ else
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
code();
|
||||
else
|
||||
@ -134,7 +134,7 @@ If you intentionally want to avoid usage of "else if" and keep if body indent ma
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
code();
|
||||
@ -148,7 +148,7 @@ else
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
code();
|
||||
else
|
||||
@ -160,7 +160,7 @@ When defining a method, use a new line for the brace, like this:
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
void method()
|
||||
{
|
||||
}
|
||||
@ -168,7 +168,7 @@ void method()
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
void Method() {
|
||||
}
|
||||
```
|
||||
@ -179,14 +179,14 @@ Use white space in expressions liberally, except in the presence of parenthesis.
|
||||
|
||||
**Good:**
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a + 5 > method(blah('a') + 4))
|
||||
foo += 24;
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a+5>method(blah('a')+4))
|
||||
foo+=24;
|
||||
```
|
||||
@ -199,13 +199,13 @@ Use a space before and after the address or pointer character in a pointer decla
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
CIntObject * images[100];
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
CIntObject* images[100]; or
|
||||
CIntObject *images[100];
|
||||
```
|
||||
@ -214,14 +214,14 @@ Do not use spaces before parentheses.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
code();
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if (a)
|
||||
code();
|
||||
```
|
||||
@ -230,7 +230,7 @@ Do not use extra spaces around conditions inside parentheses.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a && b)
|
||||
code();
|
||||
|
||||
@ -240,7 +240,7 @@ if(a && (b || c))
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if( a && b )
|
||||
code();
|
||||
|
||||
@ -252,14 +252,14 @@ Do not use more than one space between operators.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if((a && b) || (c + 1 == d))
|
||||
code();
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if((a && b) || (c + 1 == d))
|
||||
code();
|
||||
|
||||
@ -273,14 +273,14 @@ When allocating objects, don't use parentheses for creating stack-based objects
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
std::vector<int> v;
|
||||
CGBoat btn = new CGBoat();
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
std::vector<int> v(); // shouldn't compile anyway
|
||||
CGBoat btn = new CGBoat;
|
||||
```
|
||||
@ -289,14 +289,14 @@ Avoid overuse of parentheses:
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a && (b + 1))
|
||||
return c == d;
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if((a && (b + 1)))
|
||||
return (c == d);
|
||||
```
|
||||
@ -305,7 +305,7 @@ if((a && (b + 1)))
|
||||
|
||||
Base class list must be on same line with class name.
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
class CClass : public CClassBaseOne, public CClassBaseOne
|
||||
{
|
||||
int id;
|
||||
@ -321,7 +321,7 @@ When 'private:', 'public:' and other labels are not on the line after opening br
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
class CClass
|
||||
{
|
||||
int id;
|
||||
@ -333,7 +333,7 @@ public:
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
class CClass
|
||||
{
|
||||
int id;
|
||||
@ -344,7 +344,7 @@ public:
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
class CClass
|
||||
{
|
||||
protected:
|
||||
@ -357,7 +357,7 @@ public:
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
class CClass
|
||||
{
|
||||
|
||||
@ -373,7 +373,7 @@ public:
|
||||
|
||||
Constructor member and base class initialization must be on new line, indented with tab with leading colon.
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
CClass::CClass()
|
||||
: CClassBaseOne(true, nullptr), id(0), bool parameters(false)
|
||||
{
|
||||
@ -387,7 +387,7 @@ Switch statements have the case at the same indentation as the switch.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
switch(alignment)
|
||||
{
|
||||
case EAlignment::EVIL:
|
||||
@ -407,7 +407,7 @@ default:
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
switch(alignment)
|
||||
{
|
||||
case EAlignment::EVIL:
|
||||
@ -447,7 +447,7 @@ break;
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = [this, a, &b](int3 & tile, int index) -> bool
|
||||
{
|
||||
do_that();
|
||||
@ -456,7 +456,7 @@ auto lambda = [this, a, &b](int3 & tile, int index) -> bool
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = [this,a,&b](int3 & tile, int index)->bool{do_that();};
|
||||
```
|
||||
|
||||
@ -464,7 +464,7 @@ Empty parameter list is required even if function takes no arguments.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = []()
|
||||
{
|
||||
do_that();
|
||||
@ -473,7 +473,7 @@ auto lambda = []()
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = []
|
||||
{
|
||||
do_that();
|
||||
@ -484,7 +484,7 @@ Do not use inline lambda expressions inside if-else, for and other conditions.
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = []()
|
||||
{
|
||||
do_that();
|
||||
@ -497,7 +497,7 @@ if(lambda)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if([]()
|
||||
{
|
||||
do_that();
|
||||
@ -511,7 +511,7 @@ Do not pass inline lambda expressions as parameter unless it's the last paramete
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
auto lambda = []()
|
||||
{
|
||||
do_that();
|
||||
@ -521,7 +521,7 @@ obj->someMethod(lambda, true);
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
obj->someMethod([]()
|
||||
{
|
||||
do_that();
|
||||
@ -530,7 +530,7 @@ obj->someMethod([]()
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
obj->someMethod(true, []()
|
||||
{
|
||||
do_that();
|
||||
@ -543,7 +543,7 @@ Serialization of each element must be on it's own line since this make debugging
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
template <typename Handler> void serialize(Handler & h, const int version)
|
||||
{
|
||||
h & identifier;
|
||||
@ -555,7 +555,7 @@ template <typename Handler> void serialize(Handler & h, const int version)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
template <typename Handler> void serialize(Handler & h, const int version)
|
||||
{
|
||||
h & identifier & description & name & dependencies;
|
||||
@ -566,7 +566,7 @@ Save backward compatibility code is exception when extra brackets are always use
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
template <typename Handler> void serialize(Handler & h, const int version)
|
||||
{
|
||||
h & identifier;
|
||||
@ -586,7 +586,7 @@ template <typename Handler> void serialize(Handler & h, const int version)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
template <typename Handler> void serialize(Handler & h, const int version)
|
||||
{
|
||||
h & identifier;
|
||||
@ -604,7 +604,7 @@ template <typename Handler> void serialize(Handler & h, const int version)
|
||||
|
||||
For any new files, please paste the following info block at the very top of the source file:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
/*
|
||||
* Name_of_File.h, part of VCMI engine
|
||||
*
|
||||
@ -622,13 +622,13 @@ The above notice have to be included both in header and source files (.h/.cpp).
|
||||
|
||||
For any header or source file code must be in following order:
|
||||
|
||||
1. Licensing information
|
||||
2. pragma once preprocessor directive
|
||||
3. include directives
|
||||
4. Forward declarations
|
||||
5. All other code
|
||||
1. Licensing information
|
||||
2. pragma once preprocessor directive
|
||||
3. include directives
|
||||
4. Forward declarations
|
||||
5. All other code
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
/*
|
||||
* Name_of_File.h, part of VCMI engine
|
||||
*
|
||||
@ -652,7 +652,7 @@ If you comment on the same line with code there must be one single space between
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
code(); //Do something
|
||||
@ -665,7 +665,7 @@ else // Do something.
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if(a)
|
||||
{
|
||||
code();//Do something
|
||||
@ -680,7 +680,7 @@ If you add single-line comment on own line slashes must have same indent as code
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
// Do something
|
||||
if(a)
|
||||
{
|
||||
@ -692,7 +692,7 @@ if(a)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
// Do something
|
||||
if(a)
|
||||
{
|
||||
@ -706,7 +706,7 @@ Avoid comments inside multi-line if-else conditions. If your conditions are too
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
bool isMyHeroAlive = a && b || (c + 1 > 15);
|
||||
bool canMyHeroMove = myTurn && hero.movePoints > 0;
|
||||
if(isMyHeroAlive && canMyHeroMove)
|
||||
@ -717,7 +717,7 @@ if(isMyHeroAlive && canMyHeroMove)
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
if((a && b || (c + 1 > 15)) //Check if hero still alive
|
||||
&& myTurn && hero.movePoints > 0) //Check if hero can move
|
||||
{
|
||||
@ -727,7 +727,7 @@ if((a && b || (c + 1 > 15)) //Check if hero still alive
|
||||
|
||||
You should write a comment before the class definition which describes shortly the class. 1-2 sentences are enough. Methods and class data members should be commented if they aren't self-describing only. Getters/Setters, simple methods where the purpose is clear or similar methods shouldn't be commented, because vertical space is usually limited. The style of documentation comments should be the three slashes-style: ///.
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
/// Returns true if a debug/trace log message will be logged, false if not.
|
||||
/// Useful if performance is important and concatenating the log message is a expensive task.
|
||||
bool isDebugEnabled() const;
|
||||
@ -738,7 +738,7 @@ The above example doesn't follow a strict scheme on how to comment a method. It
|
||||
|
||||
If you need a more detailed description for a method you can use such style:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
/// <A short one line description>
|
||||
///
|
||||
/// <Longer description>
|
||||
@ -749,7 +749,7 @@ If you need a more detailed description for a method you can use such style:
|
||||
/// @return Description of the return value
|
||||
```
|
||||
|
||||
A good essay about writing comments: http://ardalis.com/when-to-comment-your-code
|
||||
A good essay about writing comments: <http://ardalis.com/when-to-comment-your-code>
|
||||
|
||||
### Casing
|
||||
|
||||
@ -775,7 +775,7 @@ Outdated. There is separate entry for [Logging API](Logging_API.md)
|
||||
|
||||
If you want to trace the control flow of VCMI, then you should use the macro LOG_TRACE or LOG_TRACE_PARAMS. The first one prints a message when the function is entered or leaved. The name of the function will also be logged. In addition to this the second macro, let's you specify parameters which you want to print. You should print traces with parameters like this:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
LOG_TRACE_PARAMS(logGlobal, "hero '%s', spellId '%d', pos '%s'.", hero, spellId, pos);
|
||||
```
|
||||
|
||||
@ -797,14 +797,14 @@ Do not use uncommon abbreviations for class, method, parameter and global object
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
CArt * getRandomArt(...)
|
||||
class CIntObject
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
CArtifact * getRandomArtifact(...)
|
||||
class CInterfaceObject
|
||||
```
|
||||
@ -827,7 +827,7 @@ The header StdInc.h should be included in every compilation unit. It has to be i
|
||||
|
||||
Do not declare enumerations in global namespace. It is better to use strongly typed enum or to wrap them in class or namespace to avoid polluting global namespace:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
enum class EAlignment
|
||||
{
|
||||
GOOD,
|
||||
@ -848,7 +848,7 @@ namespace EAlignment
|
||||
|
||||
If the comment duplicates the name of commented member, it's better if it wouldn't exist at all. It just increases maintenance cost. Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
size_t getHeroesCount(); //gets count of heroes (surprise?)
|
||||
```
|
||||
|
||||
@ -862,16 +862,16 @@ Don't return const objects or primitive types from functions -- it's pointless.
|
||||
|
||||
Bad:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
const std::vector<CGObjectInstance *> guardingCreatures(int3 pos) const;
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
``` cpp
|
||||
```cpp
|
||||
std::vector<const CGObjectInstance *> guardingCreatures(int3 pos) const;
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines)
|
||||
[Mono project coding guidelines](http://www.mono-project.com/Coding_Guidelines)
|
||||
|
@ -27,7 +27,7 @@ The following platforms are supported and known to work, others might require ch
|
||||
- **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04)
|
||||
- **Android**: libraries are built with NDK r25c (25.2.9519653)
|
||||
|
||||
2. Download the binaries archive and unpack it to `~/.conan` directory from https://github.com/vcmi/vcmi-dependencies/releases/latest
|
||||
2. Download the binaries archive and unpack it to `~/.conan` directory from <https://github.com/vcmi/vcmi-dependencies/releases/latest>
|
||||
|
||||
- macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz**
|
||||
- iOS: pick ***dependencies-ios.txz***
|
||||
@ -65,7 +65,7 @@ If you use `--build=never` and this command fails, then it means that you can't
|
||||
|
||||
VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this).
|
||||
|
||||
_Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`.
|
||||
*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`.
|
||||
|
||||
### Using our prebuilt binaries for macOS/iOS
|
||||
|
||||
@ -86,7 +86,7 @@ This subsection describes platform specifics to build libraries from source prop
|
||||
|
||||
#### Building for macOS/iOS
|
||||
|
||||
- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your _host_ profile or pass `-s compiler.cppstd=11` on the command line.
|
||||
- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your *host* profile or pass `-s compiler.cppstd=11` on the command line.
|
||||
- If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards.
|
||||
|
||||
#### Building for Android
|
||||
@ -105,11 +105,11 @@ After applying patch(es):
|
||||
2. Run `make`
|
||||
3. Copy file `qtbase/jar/QtAndroid.jar` from the build directory to the **package directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH/jar`.
|
||||
|
||||
_Note_: if you plan to build Qt from source again, then you don't need to perform the above _After applying patch(es)_ steps after building.
|
||||
*Note*: if you plan to build Qt from source again, then you don't need to perform the above *After applying patch(es)* steps after building.
|
||||
|
||||
##### Using recipes for system libraries
|
||||
|
||||
1. Clone/download https://github.com/kambala-decapitator/conan-system-libs
|
||||
1. Clone/download <https://github.com/kambala-decapitator/conan-system-libs>
|
||||
2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android. Do it for each library you need.
|
||||
3. Now you can execute `conan install` to build all dependencies.
|
||||
|
||||
@ -172,7 +172,7 @@ cmake --preset ios-conan
|
||||
|
||||
`CMakeUserPresets.json` file:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"version": 3,
|
||||
"cmakeMinimumRequired": {
|
||||
|
@ -6,7 +6,7 @@ Qt Creator is the recommended IDE for VCMI development on Linux distributions, b
|
||||
- Almost no manual configuration when used with CMake. Project configuration is read from CMake text files,
|
||||
- Easy to setup and use with multiple different compiler toolchains: GCC, Visual Studio, Clang
|
||||
|
||||
You can install Qt Creator from repository, but better to stick to latest version from Qt website: https://www.qt.io/download-qt-installer-oss
|
||||
You can install Qt Creator from repository, but better to stick to latest version from Qt website: <https://www.qt.io/download-qt-installer-oss>
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -21,4 +21,4 @@ The build dir should be set to something like /trunk/build for the debug build a
|
||||
There is a problem with QtCreator when debugging both vcmiclient and vcmiserver. If you debug the vcmiclient, start a game, attach the vcmiserver process to the gdb debugger(Debug \> Start Debugging \> Attach to Running External Application...) then breakpoints which are set for vcmiserver will be ignored. This looks like a bug, in any case it's not intuitively. Two workarounds are available luckily:
|
||||
|
||||
1. Run vcmiclient (no debug mode), then attach server process to the debugger
|
||||
2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately(it works!)
|
||||
2. Open two instances of QtCreator and debug vcmiserver and vcmiclient separately (it works!)
|
||||
|
@ -2,14 +2,14 @@
|
||||
|
||||
## Features
|
||||
|
||||
- A logger belongs to a "domain", this enables us to change log level settings more selectively
|
||||
- The log format can be customized
|
||||
- The color of a log entry can be customized based on logger domain and logger level
|
||||
- Logger settings can be changed in the settings.json file
|
||||
- No std::endl at the end of a log entry required
|
||||
- Thread-safe
|
||||
- Macros for tracing the application flow
|
||||
- Provides stream-like and function-like logging
|
||||
- A logger belongs to a "domain", this enables us to change log level settings more selectively
|
||||
- The log format can be customized
|
||||
- The color of a log entry can be customized based on logger domain and logger level
|
||||
- Logger settings can be changed in the settings.json file
|
||||
- No std::endl at the end of a log entry required
|
||||
- Thread-safe
|
||||
- Macros for tracing the application flow
|
||||
- Provides stream-like and function-like logging
|
||||
|
||||
## Class diagram
|
||||
|
||||
@ -17,14 +17,14 @@
|
||||
|
||||
Some notes:
|
||||
|
||||
- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json.
|
||||
- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important.
|
||||
- There are two methods `configure` and `configureDefault` of the class `CBasicLogConfigurator` to initialize and setup the logging system. The latter one setups default logging and isn't dependent on VCMI's filesystem, whereas the first one setups logging based on the user's settings which can be configured in the settings.json.
|
||||
- The methods `isDebugEnabled` and `isTraceEnabled` return true if a log record of level debug respectively trace will be logged. This can be useful if composing the log message is a expensive task and performance is important.
|
||||
|
||||
## Usage
|
||||
|
||||
### Setup settings.json
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
{
|
||||
"logging" : {
|
||||
"console" : {
|
||||
@ -68,7 +68,7 @@ The following code shows how the logging system can be configured:
|
||||
|
||||
If `configureDefault` or `configure` won't be called, then logs aren't written either to the console or to the file. The default logging setups a system like this:
|
||||
|
||||
**Console**
|
||||
#### Console
|
||||
|
||||
Format: %m
|
||||
Threshold: info
|
||||
@ -76,17 +76,18 @@ coloredOutputEnabled: true
|
||||
|
||||
colorMapping: trace -\> gray, debug -\> white, info -\> green, warn -\> yellow, error -\> red
|
||||
|
||||
**File**
|
||||
#### File
|
||||
|
||||
Format: %d %l %n \[%t\] - %m
|
||||
|
||||
**Loggers**
|
||||
#### Loggers
|
||||
|
||||
global -\> info
|
||||
|
||||
### How to get a logger
|
||||
|
||||
There exist only one logger object per domain. A logger object cannot be copied. You can get access to a logger object by using the globally defined ones like `logGlobal` or `logAi`, etc... or by getting one manually:
|
||||
|
||||
```cpp
|
||||
Logger * logger = CLogger::getLogger(CLoggerDomain("rmg"));
|
||||
```
|
||||
@ -104,22 +105,22 @@ Don't include a '\n' or std::endl at the end of your log message, a new line wil
|
||||
|
||||
The following list shows several log levels from the highest one to the lowest one:
|
||||
|
||||
- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination)
|
||||
- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally"
|
||||
- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc...
|
||||
- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D'
|
||||
- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',...
|
||||
- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination)
|
||||
- warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally"
|
||||
- info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc...
|
||||
- debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D'
|
||||
- trace -\> for logging the control flow, the execution progress or fine-grained events, e.g. hero movement completed, entering CMapEditManager::updateTerrainViews: posx '10', posy '5', width '10', height '10', mapLevel '0',...
|
||||
|
||||
The following colors are available for console output:
|
||||
|
||||
- default
|
||||
- green
|
||||
- red
|
||||
- magenta
|
||||
- yellow
|
||||
- white
|
||||
- gray
|
||||
- teal
|
||||
- default
|
||||
- green
|
||||
- red
|
||||
- magenta
|
||||
- yellow
|
||||
- white
|
||||
- gray
|
||||
- teal
|
||||
|
||||
### How to trace execution
|
||||
|
||||
@ -143,10 +144,10 @@ The program execution can be traced by using the macros TRACE_BEGIN, TRACE_END a
|
||||
|
||||
A domain is a specific part of the software. In VCMI there exist several domains:
|
||||
|
||||
- network
|
||||
- ai
|
||||
- bonus
|
||||
- network
|
||||
- network
|
||||
- ai
|
||||
- bonus
|
||||
- network
|
||||
|
||||
In addition to these domains, there exist always a super domain called "global". Sub-domains can be created with "ai.battle" or "ai.adventure" for example. The dot between the "ai" and "battle" is important and notes the parent-child relationship of those two domains. A few examples how the log level will be inherited:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Configuration
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
{
|
||||
//general purpose script, Lua or ERM, runs on server
|
||||
"myScript":
|
||||
@ -87,75 +87,75 @@ VCMI uses LuaJIT, which is Lua 5.1 API, see [upstream documentation](https://www
|
||||
|
||||
Following libraries are supported
|
||||
|
||||
- base
|
||||
- table
|
||||
- string
|
||||
- math
|
||||
- bit
|
||||
- base
|
||||
- table
|
||||
- string
|
||||
- math
|
||||
- bit
|
||||
|
||||
## ERM
|
||||
|
||||
### Features
|
||||
|
||||
- no strict limit on function/variable numbers (technical limit 32 bit integer except 0))
|
||||
- TODO semi compare
|
||||
- DONE macros
|
||||
- no strict limit on function/variable numbers (technical limit 32 bit integer except 0))
|
||||
- TODO semi compare
|
||||
- DONE macros
|
||||
|
||||
### Bugs
|
||||
|
||||
- TODO Broken XOR support (clashes with \`X\` option)
|
||||
- TODO Broken XOR support (clashes with \`X\` option)
|
||||
|
||||
### Triggers
|
||||
|
||||
- TODO **!?AE** Equip/Unequip artifact
|
||||
- WIP **!?BA** when any battle occurs
|
||||
- WIP **!?BF** when a battlefield is prepared for a battle
|
||||
- TODO **!?BG** at every action taken by any stack or by the hero
|
||||
- TODO **!?BR** at every turn of a battle
|
||||
- *!?CM (client only) click the mouse button.*
|
||||
- TODO **!?CO** Commander triggers
|
||||
- TODO **!?DL** Custom dialogs
|
||||
- DONE **!?FU** function
|
||||
- TODO **!?GE** "global" event
|
||||
- TODO **!?GM** Saving/Loading
|
||||
- TODO **!?HE** when the hero \# is attacked by an enemy hero or
|
||||
- TODO **!?AE** Equip/Unequip artifact
|
||||
- WIP **!?BA** when any battle occurs
|
||||
- WIP **!?BF** when a battlefield is prepared for a battle
|
||||
- TODO **!?BG** at every action taken by any stack or by the hero
|
||||
- TODO **!?BR** at every turn of a battle
|
||||
- *!?CM (client only) click the mouse button.*
|
||||
- TODO **!?CO** Commander triggers
|
||||
- TODO **!?DL** Custom dialogs
|
||||
- DONE **!?FU** function
|
||||
- TODO **!?GE** "global" event
|
||||
- TODO **!?GM** Saving/Loading
|
||||
- TODO **!?HE** when the hero \# is attacked by an enemy hero or
|
||||
visited by an allied hero
|
||||
- TODO **!?HL** hero gains a level
|
||||
- TODO **!?HM** every step a hero \# takes
|
||||
- *!?IP Multiplayer support.*
|
||||
- TODO **!?LE** (!$LE) An Event on the map
|
||||
- WIP **!?MF** stack taking physical damage(before an action)
|
||||
- TODO **!?MG** casting on the adventure map
|
||||
- *!?MM scroll text during a battle*
|
||||
- TODO **!?MR** Magic resistance
|
||||
- TODO **!?MW** Wandering Monsters
|
||||
- WIP **!?OB** (!$OB) visiting objects
|
||||
- DONE **!?PI** Post Instruction.
|
||||
- TODO **!?SN** Sound and ERA extensions
|
||||
- *!?TH town hall*
|
||||
- TODO **!?TL** Real-Time Timer
|
||||
- TODO **!?TM** timed events
|
||||
- TODO **!?HL** hero gains a level
|
||||
- TODO **!?HM** every step a hero \# takes
|
||||
- *!?IP Multiplayer support.*
|
||||
- TODO **!?LE** (!$LE) An Event on the map
|
||||
- WIP **!?MF** stack taking physical damage(before an action)
|
||||
- TODO **!?MG** casting on the adventure map
|
||||
- *!?MM scroll text during a battle*
|
||||
- TODO **!?MR** Magic resistance
|
||||
- TODO **!?MW** Wandering Monsters
|
||||
- WIP **!?OB** (!$OB) visiting objects
|
||||
- DONE **!?PI** Post Instruction.
|
||||
- TODO **!?SN** Sound and ERA extensions
|
||||
- *!?TH town hall*
|
||||
- TODO **!?TL** Real-Time Timer
|
||||
- TODO **!?TM** timed events
|
||||
|
||||
### Receivers
|
||||
|
||||
#### VCMI
|
||||
|
||||
- **!!MC:S@varName@** - declare new "normal" variable (technically
|
||||
- **!!MC:S@varName@** - declare new "normal" variable (technically
|
||||
v-var with string key)
|
||||
- TODO Identifier resolver
|
||||
- WIP Bonus system
|
||||
- TODO Identifier resolver
|
||||
- WIP Bonus system
|
||||
|
||||
#### ERA
|
||||
|
||||
- DONE !!if !!el !!en
|
||||
- TODO !!br !!co
|
||||
- TODO !!SN:X
|
||||
- DONE !!if !!el !!en
|
||||
- TODO !!br !!co
|
||||
- TODO !!SN:X
|
||||
|
||||
#### WoG
|
||||
|
||||
- TODO !!AR Артефакт (ресурс) в определенной позиции
|
||||
- TODO !!BA Битва
|
||||
- !!BA:A$ return 1 for battle evaluation
|
||||
- !!BA:A$ return 1 for battle evaluation
|
||||
- TODO !!BF Препятствия на поле боя
|
||||
- TODO !!BG Действий монстров в бою
|
||||
- TODO !!BH Действия героя в бою
|
||||
@ -201,4 +201,4 @@ Following libraries are supported
|
||||
- *!#VC Контроль переменных*
|
||||
- WIP !!VR Установка переменных
|
||||
|
||||
### Persistence
|
||||
### Persistence
|
||||
|
@ -5,12 +5,14 @@
|
||||
For implementation details see files located at `lib/network` directory.
|
||||
|
||||
VCMI uses connection using TCP to communicate with server, even in single-player games. However, even though TCP is stream-based protocol, VCMI uses atomic messages for communication. Each message is a serialized stream of bytes, preceded by 4-byte message size:
|
||||
|
||||
```
|
||||
int32_t messageSize;
|
||||
byte messagePayload[messageSize];
|
||||
```
|
||||
|
||||
Networking can be used by:
|
||||
|
||||
- game client (vcmiclient / VCMI_Client.exe). Actual application that player interacts with directly using UI.
|
||||
- match server (vcmiserver / VCMI_Server.exe / part of game client). This app controls game logic and coordinates multiplayer games.
|
||||
- lobby server (vcmilobby). This app provides access to global lobby through which players can play game over Internet.
|
||||
@ -28,11 +30,13 @@ For gameplay, VCMI serializes data into a binary stream. See [Serialization](Ser
|
||||
## Global lobby communication
|
||||
|
||||
For implementation details see:
|
||||
|
||||
- game client: `client/globalLobby/GlobalLobbyClient.h
|
||||
- match server: `server/GlobalLobbyProcessor.h
|
||||
- lobby server: `client/globalLobby/GlobalLobbyClient.h
|
||||
|
||||
In case of global lobby, message payload uses plaintext json format - utf-8 encoded string:
|
||||
|
||||
```
|
||||
int32_t messageSize;
|
||||
char jsonString[messageSize];
|
||||
@ -43,6 +47,7 @@ Every message must be a struct (json object) that contains "type" field. Unlike
|
||||
### Communication flow
|
||||
|
||||
Notes:
|
||||
|
||||
- invalid message, such as corrupted json format or failure to validate message will result in no reply from server
|
||||
- in addition to specified messages, match server will send `operationFailed` message on failure to apply player request
|
||||
|
||||
@ -51,7 +56,8 @@ Notes:
|
||||
- client -> lobby: `clientRegister`
|
||||
- lobby -> client: `accountCreated`
|
||||
|
||||
#### Login
|
||||
#### Login
|
||||
|
||||
- client -> lobby: `clientLogin`
|
||||
- lobby -> client: `loginSuccess`
|
||||
- lobby -> client: `chatHistory`
|
||||
@ -59,10 +65,12 @@ Notes:
|
||||
- lobby -> client: `activeGameRooms`
|
||||
|
||||
#### Chat Message
|
||||
|
||||
- client -> lobby: `sendChatMessage`
|
||||
- lobby -> every client: `chatMessage`
|
||||
|
||||
#### New Game Room
|
||||
|
||||
- client starts match server instance
|
||||
- match -> lobby: `serverLogin`
|
||||
- lobby -> match: `loginSuccess`
|
||||
@ -73,19 +81,23 @@ Notes:
|
||||
- lobby -> every client: `activeGameRooms`
|
||||
|
||||
#### Joining a game room
|
||||
|
||||
See [#Proxy mode](proxy-mode)
|
||||
|
||||
#### Leaving a game room
|
||||
|
||||
- client closes connection to match server
|
||||
- match -> lobby: `leaveGameRoom`
|
||||
|
||||
#### Sending an invite:
|
||||
#### Sending an invite
|
||||
|
||||
- client -> lobby: `sendInvite`
|
||||
- lobby -> target client: `inviteReceived`
|
||||
|
||||
Note: there is no dedicated procedure to accept an invite. Instead, invited player will use same flow as when joining public game room
|
||||
|
||||
#### Logout
|
||||
|
||||
- client closes connection
|
||||
- lobby -> every client: `activeAccounts`
|
||||
|
||||
@ -94,6 +106,7 @@ Note: there is no dedicated procedure to accept an invite. Instead, invited play
|
||||
In order to connect players located behind NAT, VCMI lobby can operate in "proxy" mode. In this mode, connection will be act as proxy and will transmit gameplay data from client to a match server, without any data processing on lobby server.
|
||||
|
||||
Currently, process to establish connection using proxy mode is:
|
||||
|
||||
- Player attempt to join open game room using `joinGameRoom` message
|
||||
- Lobby server validates requests and on success - notifies match server about new player in lobby using control connection
|
||||
- Match server receives request, establishes new connection to game lobby, sends `serverProxyLogin` message to lobby server and immediately transfers this connection to VCMIServer class to use as connection for gameplay communication
|
||||
@ -101,4 +114,4 @@ Currently, process to establish connection using proxy mode is:
|
||||
- Game client receives message and establishes own side of proxy connection - connects to lobby, sends `clientProxyLogin` message and transfers to ServerHandler class to use as connection for gameplay communication
|
||||
- Lobby server accepts new connection and moves it into a proxy mode - all packages that will be received by one side of this connection will be re-sent to another side without any processing.
|
||||
|
||||
Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection
|
||||
Once the game is over (or if one side disconnects) lobby server will close another side of the connection and erase proxy connection
|
||||
|
@ -74,4 +74,4 @@ For every zone, a few random obstacle sets are selected. [Details](https://githu
|
||||
|
||||
### Filling space
|
||||
|
||||
Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly.
|
||||
Tiles which need to be `blocked` but are not `used` are filled with obstacles. Largest obstacles which cover the most tiles are picked first, other than that they are chosen randomly.
|
||||
|
@ -140,7 +140,7 @@ CLoadFile/CSaveFile classes allow to read data to file and store data to file. T
|
||||
|
||||
#### Networking
|
||||
|
||||
See [Networking](Networking.md)
|
||||
See [Networking](Networking.md)
|
||||
|
||||
### Additional features
|
||||
|
||||
@ -259,4 +259,4 @@ Foo *loadedA, *loadedB;
|
||||
|
||||
The feature recognizes pointers by addresses. Therefore it allows mixing pointers to base and derived classes. However, it does not allow serializing classes with multiple inheritance using a "non-first" base (other bases have a certain address offset from the actual object).
|
||||
|
||||
Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them.
|
||||
Pointer cycles are properly handled. This feature makes sense for savegames and is turned on for them.
|
||||
|
@ -9,30 +9,30 @@ So far we using following services:
|
||||
### Most important
|
||||
|
||||
- VCMI.eu domain paid until July of 2019.
|
||||
- Owner: Tow
|
||||
- Our main domain used by services.
|
||||
- Owner: Tow
|
||||
- Our main domain used by services.
|
||||
- VCMI.download paid until November of 2026.
|
||||
- Owner: SXX
|
||||
- Intended to be used for all assets downloads.
|
||||
- Domain registered on GANDI and **can be renewed by anyone without access to account**.
|
||||
- Owner: SXX
|
||||
- Intended to be used for all assets downloads.
|
||||
- Domain registered on GANDI and **can be renewed by anyone without access to account**.
|
||||
- [DigitalOcean](https://cloud.digitalocean.com/) team.
|
||||
- Our hosting sponsor.
|
||||
- Administrator access: SXX, Warmonger.
|
||||
- User access: AVS, Tow.
|
||||
- Our hosting sponsor.
|
||||
- Administrator access: SXX, Warmonger.
|
||||
- User access: AVS, Tow.
|
||||
- [CloudFlare](https://www.cloudflare.com/a/overview) account.
|
||||
- Access through shared login / password.
|
||||
- All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there.
|
||||
- Access through shared login / password.
|
||||
- All of our infrastructure is behind CloudFlare and all our web. We manage our DNS there.
|
||||
- [Google Apps (G Suite)](https://admin.google.com/) account.
|
||||
- It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day.
|
||||
- One administrative email used for other services registration.
|
||||
- "noreply" email used for outgoing mail on Wiki and Bug Tracker.
|
||||
- "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email.
|
||||
- Administrator access: Tow, SXX.
|
||||
- It's only for vcmi.eu domain and limited to 5 users. Each account has limit of 500 emails / day.
|
||||
- One administrative email used for other services registration.
|
||||
- "noreply" email used for outgoing mail on Wiki and Bug Tracker.
|
||||
- "forum" email used for outgoing mail on Forums. Since we authenticate everyone through forum it's should be separate email.
|
||||
- Administrator access: Tow, SXX.
|
||||
- [Google Play Console](https://play.google.com/apps/publish/) account.
|
||||
- Hold ownership over VCMI Android App.
|
||||
- Owner: SXX
|
||||
- Administrator access: Warmonger, AVS, Ivan.
|
||||
- Release manager access: Fay.
|
||||
- Hold ownership over VCMI Android App.
|
||||
- Owner: SXX
|
||||
- Administrator access: Warmonger, AVS, Ivan.
|
||||
- Release manager access: Fay.
|
||||
|
||||
Not all services let us safely share login credentials, but at least when possible at least two of core developers must have access to them in case of emergency.
|
||||
|
||||
@ -41,20 +41,20 @@ Not all services let us safely share login credentials, but at least when possib
|
||||
We want to notify players about updates on as many social services as possible.
|
||||
|
||||
- Facebook page: <https://www.facebook.com/VCMIOfficial>
|
||||
- Administrator access: SXX, Warmonger
|
||||
- Administrator access: SXX, Warmonger
|
||||
- Twitter account: <https://twitter.com/VCMIOfficial>
|
||||
- Administrator access: SXX.
|
||||
- User access via TweetDeck:
|
||||
- VK / VKontakte page: <https://vk.com/VCMIOfficial>
|
||||
- Owner: SXX
|
||||
- Administrator access: AVS
|
||||
- Administrator access: SXX.
|
||||
- User access via TweetDeck:
|
||||
- VK / VKontakte page: <https://vk.com/VCMIOfficial>
|
||||
- Owner: SXX
|
||||
- Administrator access: AVS
|
||||
- Steam group: <https://steamcommunity.com/groups/VCMI>
|
||||
- Administrator access: SXX
|
||||
- Moderator access: Dydzio
|
||||
- Reddit: <https://reddit.com/r/vcmi/>
|
||||
- Administrator access: SXX
|
||||
- ModDB entry: <http://www.moddb.com/engines/vcmi>
|
||||
- Administrator access: SXX
|
||||
- Administrator access: SXX
|
||||
- Moderator access: Dydzio
|
||||
- Reddit: <https://reddit.com/r/vcmi/>
|
||||
- Administrator access: SXX
|
||||
- ModDB entry: <http://www.moddb.com/engines/vcmi>
|
||||
- Administrator access: SXX
|
||||
|
||||
### Communication channels
|
||||
|
||||
@ -70,48 +70,46 @@ We want to notify players about updates on as many social services as possible.
|
||||
### Other services
|
||||
|
||||
- Launchpad PPA: <https://launchpad.net/~vcmi>
|
||||
- Member access: AVS
|
||||
- Administrator access: Ivan, SXX
|
||||
- Member access: AVS
|
||||
- Administrator access: Ivan, SXX
|
||||
- Snapcraft Dashboard: <https://dashboard.snapcraft.io/>
|
||||
- Administrator access: SXX
|
||||
- Administrator access: SXX
|
||||
- Coverity Scan page: <https://scan.coverity.com/projects/vcmi>
|
||||
- Administrator access: SXX, Warmonger, AVS
|
||||
- Administrator access: SXX, Warmonger, AVS
|
||||
- OpenHub page: <https://www.openhub.net/p/VCMI>
|
||||
- Administrator access: Tow
|
||||
- Administrator access: Tow
|
||||
- Docker Hub organization: <https://hub.docker.com/u/vcmi/>
|
||||
- Administrator access: SXX
|
||||
- Administrator access: SXX
|
||||
|
||||
Reserve accounts for other code hosting services:
|
||||
|
||||
- GitLab organization: <https://gitlab.com/vcmi/>
|
||||
- Administrator access: SXX
|
||||
- Administrator access: SXX
|
||||
- BitBucket organization: <https://bitbucket.org/vcmi/>
|
||||
- Administrator access: SXX
|
||||
- Administrator access: SXX
|
||||
|
||||
## What's to improve
|
||||
|
||||
1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access.
|
||||
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.
|
||||
1. Encourage Tow to transfer VCMI.eu to GANDI so it's can be also renewed without access.
|
||||
2. Use 2FA on CloudFlare and just ask everyone to get FreeOTP and then use shared secret.
|
||||
3. Centralized way to post news about game updates to all social media.
|
||||
|
||||
# Project Servers Configuration
|
||||
## Project Servers Configuration
|
||||
|
||||
This section dedicated to explain specific configurations of our servers for anyone who might need to improve it in future.
|
||||
|
||||
## Droplet configuration
|
||||
|
||||
### Droplet and hosted services
|
||||
### Droplet configuration
|
||||
|
||||
Currently we using two droplets:
|
||||
|
||||
- First one serve all of our web services:
|
||||
- [Forum](https://forum.vcmi.eu/)
|
||||
- [Bug tracker](https://bugs.vcmi.eu/)
|
||||
- [Wiki](https://wiki.vcmi.eu/)
|
||||
- [Slack invite page](https://slack.vcmi.eu/)
|
||||
- [Forum](https://forum.vcmi.eu/)
|
||||
- [Bug tracker](https://bugs.vcmi.eu/)
|
||||
- [Wiki](https://wiki.vcmi.eu/)
|
||||
- [Slack invite page](https://slack.vcmi.eu/)
|
||||
- Second serve downloads:
|
||||
- [Legacy download page](http://download.vcmi.eu/)
|
||||
- [Build download page](https://builds.vcmi.download/)
|
||||
- [Legacy download page](http://download.vcmi.eu/)
|
||||
- [Build download page](https://builds.vcmi.download/)
|
||||
|
||||
To keep everything secure we should always keep binary downloads separate from any web services.
|
||||
|
||||
@ -131,4 +129,4 @@ We only expose floating IP that can be detached from droplet in case of emergenc
|
||||
- Address: beholder.vcmi.eu (67.207.75.182)
|
||||
- Port 22 serve SFTP for file uploads as well as CI artifacts uploads.
|
||||
|
||||
If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls).
|
||||
If new services added firewall rules can be adjusted in [DO control panel](https://cloud.digitalocean.com/networking/firewalls).
|
||||
|
@ -1,12 +1,16 @@
|
||||
# Release Process
|
||||
|
||||
## Versioning
|
||||
|
||||
For releases VCMI uses version numbering in form "1.X.Y", where:
|
||||
|
||||
- 'X' indicates major release. Different major versions are generally not compatible with each other. Save format is different, network protocol is different, mod format likely different.
|
||||
- 'Y' indicates hotfix release. Despite its name this is usually not urgent, but planned release. Different hotfixes for same major version are fully compatible with each other.
|
||||
|
||||
## Branches
|
||||
|
||||
Our branching strategy is very similar to GitFlow:
|
||||
|
||||
- `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `1.X.Y` when corresponding version is released. State of master branch represents state of latest public release.
|
||||
- `beta` branch is for stabilization of ongoing release. Beta branch is created when new major release enters stabilization stage and is used for both major release itself as well as for subsequent hotfixes. Only changes that are safe, have minimal chance of regressions and improve player experience should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta.
|
||||
- `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta`.
|
||||
@ -14,12 +18,14 @@ Our branching strategy is very similar to GitFlow:
|
||||
## Release process step-by-step
|
||||
|
||||
### Initial release setup (major releases only)
|
||||
|
||||
Should be done immediately after start of stabilization stage for previous release
|
||||
|
||||
- Create project named `Release 1.X`
|
||||
- Add all features and bugs that should be fixed as part of this release into this project
|
||||
|
||||
### Start of stabilization stage (major releases only)
|
||||
|
||||
Should be done 2 weeks before planned release date. All major features should be finished at this point.
|
||||
|
||||
- Create `beta` branch from `develop`
|
||||
@ -34,6 +40,7 @@ Should be done 2 weeks before planned release date. All major features should be
|
||||
- Bump version and build ID for Android on `beta` branch
|
||||
|
||||
### Release preparation stage
|
||||
|
||||
Should be done 1 week before release. Release date should be decided at this point.
|
||||
|
||||
- Make sure to announce codebase freeze deadline (1 day before release) to all developers
|
||||
@ -45,21 +52,23 @@ Should be done 1 week before release. Release date should be decided at this poi
|
||||
- - Update downloads counter in `docs/readme.md`
|
||||
|
||||
### Release preparation stage
|
||||
|
||||
Should be done 1 day before release. At this point beta branch is in full freeze.
|
||||
|
||||
- Merge release preparation PR into `beta`
|
||||
- Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages
|
||||
- Create draft release page, specify `1.x.y` as tag for `master` after publishing
|
||||
- Check that artifacts for all platforms have been built by CI on `master` branch
|
||||
- Download and rename all build artifacts to use form "VCMI-1.X.Y-Platform.xxx"
|
||||
- Download and rename all build artifacts to use form `VCMI-1.X.Y-Platform.xxx`
|
||||
- Attach build artifacts for all platforms to release page
|
||||
- Manually extract Windows installer, remove `$PLUGINSDIR` directory which contains installer files and repackage data as .zip archive
|
||||
- Attach produced zip archive to release page as an alternative Windows installer
|
||||
- Upload built AAB to Google Play and send created release draft for review (usually takes several hours)
|
||||
- Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates)
|
||||
- (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu
|
||||
- (major releases only) Prepare pull request with release update for web site <https://github.com/vcmi/VCMI.eu>
|
||||
|
||||
### Release publishing phase
|
||||
|
||||
Should be done on release date
|
||||
|
||||
- Trigger builds for new release on Ubuntu PPA
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Ubuntu PPA
|
||||
|
||||
## Main links
|
||||
|
||||
- [Team](https://launchpad.net/~vcmi)
|
||||
- [Project](https://launchpad.net/vcmi)
|
||||
- [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi)
|
||||
@ -14,31 +15,42 @@
|
||||
## Automatic daily builds process
|
||||
|
||||
### Code import
|
||||
|
||||
- Launchpad performs regular (once per few hours) clone of our git repository.
|
||||
- This process can be observed on [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) page.
|
||||
- If necessary, it is possible to trigger fresh clone immediately (Import Now button)
|
||||
|
||||
### Build dependencies
|
||||
|
||||
- All packages required for building of vcmi are defined in [debian/control](https://github.com/vcmi/vcmi/blob/develop/debian/control) file
|
||||
- Launchpad will automatically install build dependencies during build
|
||||
- Dependencies of output .deb package are defined implicitly as dependencies of packages required for build
|
||||
|
||||
### Recipe building
|
||||
|
||||
- Every 24 hours Launchpad triggers daily builds on all recipes that have build schedule enable. For vcmi this is [Daily recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-daily)
|
||||
- Alternatively, builds can be triggered manually using "request build(s) link on recipe page. VCMI uses this for [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable)
|
||||
|
||||
### Recipe content (build settings)
|
||||
|
||||
- Version of resulting .deb package is set in recipe content, e.g `{debupstream}+git{revtime}` for daily builds
|
||||
- Base version (referred as `debupstream` on Launchpad is taken from source code, [debian/changelog](https://github.com/vcmi/vcmi/blob/develop/debian/changelog) file
|
||||
- CMake configuration settings are taken from source code, [debian/rules](https://github.com/vcmi/vcmi/blob/develop/debian/rules) file
|
||||
- Branch which is used for build is specified in recipe content, e.g. `lp:vcmi master`
|
||||
|
||||
## Workflow for creating a release build
|
||||
|
||||
- if necessary, push all required changes including `debian/changelog` update to `vcmi/master` branch
|
||||
- Go to [Sources](https://code.launchpad.net/~vcmi/vcmi/+git/vcmi) and run repository import.
|
||||
- Wait for import to finish, which usually happens within a minute. Press F5 to actually see changes.
|
||||
- Go to [Stable recipe](https://code.launchpad.net/~vcmi/+recipe/vcmi-stable) and request new 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
|
||||
- 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
|
||||
- [alexvins](https://github.com/alexvins) (https://launchpad.net/~alexvins)
|
||||
- [ArseniyShestakov](https://github.com/ArseniyShestakov) (https://launchpad.net/~sxx)
|
||||
- [IvanSavenko](https://github.com/IvanSavenko) (https://launchpad.net/~saven-ivan)
|
||||
- (Not member of VCMI, creator of PPA) (https://launchpad.net/~mantas)
|
||||
|
||||
- [alexvins](https://github.com/alexvins) (<https://launchpad.net/~alexvins>)
|
||||
- [ArseniyShestakov](https://github.com/ArseniyShestakov) (<https://launchpad.net/~sxx>)
|
||||
- [IvanSavenko](https://github.com/IvanSavenko) (<https://launchpad.net/~saven-ivan>)
|
||||
- (Not member of VCMI, creator of PPA) (<https://launchpad.net/~mantas>)
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def this format allows:
|
||||
|
||||
- Overriding individual frames from json file (e.g. icons)
|
||||
- Modern graphics formats (targa, png - all formats supported by VCMI image loader)
|
||||
- Does not requires any special tools - all you need is text editor and images.
|
||||
- Overriding individual frames from json file (e.g. icons)
|
||||
- Modern graphics formats (targa, png - all formats supported by VCMI image loader)
|
||||
- Does not requires any special tools - all you need is text editor and images.
|
||||
|
||||
## Format description
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
{
|
||||
// Base path of all images in animation. Optional.
|
||||
// Can be used to avoid using long path to images
|
||||
@ -58,12 +58,14 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def
|
||||
### Replacing a button
|
||||
|
||||
This json file will allow replacing .def file for a button with png images. Buttons require following images:
|
||||
|
||||
1. Active state. Button is active and can be pressed by player
|
||||
2. Pressed state. Player pressed button but have not released it yet
|
||||
3. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image
|
||||
4. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off
|
||||
|
||||
```javascript
|
||||
```json5
|
||||
{
|
||||
"basepath" : "interface/MyButton", // all images are located in this directory
|
||||
|
||||
"images" :
|
||||
@ -80,7 +82,8 @@ This json file will allow replacing .def file for a button with png images. Butt
|
||||
|
||||
This json file allows defining one animation sequence, for example for adventure map objects or for town buildings.
|
||||
|
||||
```javascript
|
||||
```json5
|
||||
{
|
||||
"basepath" : "myTown/myBuilding", // all images are located in this directory
|
||||
|
||||
"sequences" :
|
||||
|
@ -4,14 +4,14 @@ Bonus may have any of these durations. They acts in disjunction.
|
||||
|
||||
## List of all bonus duration types
|
||||
|
||||
- PERMANENT
|
||||
- ONE_BATTLE: at the end of battle
|
||||
- ONE_DAY: at the end of day
|
||||
- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days)
|
||||
- N_TURNS: used during battles, after battle bonus is always removed
|
||||
- N_DAYS
|
||||
- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack
|
||||
- UNTIL_ATTACK: removed after attack and counterattacks are performed
|
||||
- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance
|
||||
- COMMANDER_KILLED
|
||||
- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed
|
||||
- PERMANENT
|
||||
- ONE_BATTLE: at the end of battle
|
||||
- ONE_DAY: at the end of day
|
||||
- ONE_WEEK: at the end of week (bonus lasts till the end of week, NOT 7 days)
|
||||
- N_TURNS: used during battles, after battle bonus is always removed
|
||||
- N_DAYS
|
||||
- UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack
|
||||
- UNTIL_ATTACK: removed after attack and counterattacks are performed
|
||||
- STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance
|
||||
- COMMANDER_KILLED
|
||||
- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed
|
||||
|
@ -15,7 +15,7 @@ The limiters take no parameters:
|
||||
|
||||
Example:
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
"limiters" : [ "SHOOTER_ONLY" ]
|
||||
```
|
||||
|
||||
@ -25,12 +25,12 @@ Example:
|
||||
|
||||
Parameters:
|
||||
|
||||
- Bonus type
|
||||
- (optional) bonus subtype
|
||||
- (optional) bonus sourceType and sourceId in struct
|
||||
- example: (from Adele's bless):
|
||||
- Bonus type
|
||||
- (optional) bonus subtype
|
||||
- (optional) bonus sourceType and sourceId in struct
|
||||
- example: (from Adele's bless):
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
"limiters" : [
|
||||
{
|
||||
"type" : "HAS_ANOTHER_BONUS_LIMITER",
|
||||
@ -50,20 +50,21 @@ Parameters:
|
||||
|
||||
Parameters:
|
||||
|
||||
- Creature id (string)
|
||||
- (optional) include upgrades - default is false
|
||||
- Creature id (string)
|
||||
- (optional) include upgrades - default is false
|
||||
|
||||
### CREATURE_ALIGNMENT_LIMITER
|
||||
|
||||
Parameters:
|
||||
|
||||
- Alignment identifier
|
||||
- Alignment identifier
|
||||
|
||||
### CREATURE_LEVEL_LIMITER
|
||||
|
||||
If parameters is empty, level limiter works as CREATURES_ONLY limiter
|
||||
|
||||
Parameters:
|
||||
|
||||
- Minimal level
|
||||
- Maximal level
|
||||
|
||||
@ -71,24 +72,24 @@ Parameters:
|
||||
|
||||
Parameters:
|
||||
|
||||
- Faction identifier
|
||||
- Faction identifier
|
||||
|
||||
### CREATURE_TERRAIN_LIMITER
|
||||
|
||||
Parameters:
|
||||
|
||||
- Terrain identifier
|
||||
- Terrain identifier
|
||||
|
||||
Example:
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
"limiters": [ {
|
||||
"type":"CREATURE_TYPE_LIMITER",
|
||||
"parameters": [ "angel", true ]
|
||||
} ],
|
||||
```
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
"limiters" : [ {
|
||||
"type" : "CREATURE_TERRAIN_LIMITER",
|
||||
"parameters" : ["sand"]
|
||||
@ -106,13 +107,13 @@ Parameters:
|
||||
The following limiters must be specified as the first element of a list,
|
||||
and operate on the remaining limiters in that list:
|
||||
|
||||
- allOf (default when no aggregate limiter is specified)
|
||||
- anyOf
|
||||
- noneOf
|
||||
- allOf (default when no aggregate limiter is specified)
|
||||
- anyOf
|
||||
- noneOf
|
||||
|
||||
Example:
|
||||
|
||||
``` javascript
|
||||
```json5
|
||||
"limiters" : [
|
||||
"noneOf",
|
||||
"IS_UNDEAD",
|
||||
@ -121,4 +122,4 @@ Example:
|
||||
"parameters" : [ "SIEGE_WEAPON" ]
|
||||
}
|
||||
]
|
||||
```
|
||||
```
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
## Available propagators
|
||||
|
||||
- BATTLE_WIDE: Affects both sides during battle
|
||||
- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero)
|
||||
- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion.
|
||||
- HERO: Bonus will be transferred to hero (for example from stacks in his army).
|
||||
- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies.
|
||||
- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map.
|
||||
- BATTLE_WIDE: Affects both sides during battle
|
||||
- VISITED_TOWN_AND_VISITOR: Used with Legion artifacts (town visited by hero)
|
||||
- PLAYER_PROPAGATOR: Bonus will affect all objects owned by player. Used by Statue of Legion.
|
||||
- HERO: Bonus will be transferred to hero (for example from stacks in his army).
|
||||
- TEAM_PROPAGATOR: Bonus will affect all objects owned by player and his allies.
|
||||
- GLOBAL_EFFECT: This effect will influence all creatures, heroes and towns on the map.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user