1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-05-21 22:33:43 +02:00

Merge remote-tracking branch 'origin/beta' into fix_rmg_teams

This commit is contained in:
Tomasz Zieliński 2023-08-14 18:59:45 +02:00
commit c59014ce18
217 changed files with 5923 additions and 3597 deletions

View File

@ -251,7 +251,14 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack)
void CBattleAI::yourTacticPhase(int distance) void CBattleAI::yourTacticPhase(int distance)
{ {
cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
}
uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start)
{
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
} }
void CBattleAI::activeStack( const CStack * stack ) void CBattleAI::activeStack( const CStack * stack )
@ -261,6 +268,8 @@ void CBattleAI::activeStack( const CStack * stack )
BattleAction result = BattleAction::makeDefend(stack); BattleAction result = BattleAction::makeDefend(stack);
setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
auto start = std::chrono::high_resolution_clock::now();
try try
{ {
if(stack->creatureId() == CreatureID::CATAPULT) if(stack->creatureId() == CreatureID::CATAPULT)
@ -276,6 +285,8 @@ void CBattleAI::activeStack( const CStack * stack )
attemptCastingSpell(); attemptCastingSpell();
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
if(cb->battleIsFinished() || !stack->alive()) if(cb->battleIsFinished() || !stack->alive())
{ {
//spellcast may finish battle or kill active stack //spellcast may finish battle or kill active stack
@ -312,6 +323,8 @@ void CBattleAI::activeStack( const CStack * stack )
movesSkippedByDefense = 0; movesSkippedByDefense = 0;
} }
logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
cb->battleMakeUnitAction(result); cb->battleMakeUnitAction(result);
} }
@ -494,7 +507,12 @@ void CBattleAI::attemptCastingSpell()
{ {
spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell);
for(auto & target : temp.findPotentialTargets()) if(!spell->isDamage() && spell->getTargetType() == spells::AimType::LOCATION)
continue;
const bool FAST = true;
for(auto & target : temp.findPotentialTargets(FAST))
{ {
PossibleSpellcast ps; PossibleSpellcast ps;
ps.dest = target; ps.dest = target;
@ -826,7 +844,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas
ps.value = totalGain; ps.value = totalGain;
} }
void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
side = Side; side = Side;
@ -863,7 +881,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
if(!bs.canFlee || !bs.canSurrender) if(!bs.canFlee && !bs.canSurrender)
{ {
return std::nullopt; return std::nullopt;
} }

View File

@ -59,7 +59,8 @@ class CBattleAI : public CBattleGameInterface
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
//Previous setting of cb //Previous setting of cb
bool wasWaitingForRealize, wasUnlockingGs; bool wasWaitingForRealize;
bool wasUnlockingGs;
int movesSkippedByDefense; int movesSkippedByDefense;
public: public:
@ -82,7 +83,7 @@ public:
BattleAction selectStackAction(const CStack * stack); BattleAction selectStackAction(const CStack * stack);
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack); std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack

View File

@ -600,6 +600,8 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
if(unit->isTurret()) if(unit->isTurret())
continue; continue;
auto unitSpeed = unit->speed(turn);
if(turnBattle.battleCanShoot(unit)) if(turnBattle.battleCanShoot(unit))
{ {
for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
@ -614,7 +616,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
{ {
bool reachable = unitReachability.distances[hex] <= unit->speed(turn); bool reachable = unitReachability.distances[hex] <= unitSpeed;
if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
{ {
@ -624,7 +626,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
{ {
for(BattleHex neighbor : hex.neighbouringTiles()) for(BattleHex neighbor : hex.neighbouringTiles())
{ {
reachable = unitReachability.distances[neighbor] <= unit->speed(turn); reachable = unitReachability.distances[neighbor] <= unitSpeed;
if(reachable) break; if(reachable) break;
} }

View File

@ -84,17 +84,6 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
{ {
return lhs.damageDiff() > rhs.damageDiff(); return lhs.damageDiff() > rhs.damageDiff();
}); });
if (!possibleAttacks.empty())
{
auto & bestAp = possibleAttacks[0];
logGlobal->debug("Battle AI best: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
bestAp.attack.attacker->unitType()->getJsonKey(),
state.battleGetUnitByPos(bestAp.dest)->unitType()->getJsonKey(),
(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
}
} }
int64_t PotentialTargets::bestActionValue() const int64_t PotentialTargets::bestActionValue() const

View File

@ -41,7 +41,7 @@ void CEmptyAI::activeStack(const CStack * stack)
void CEmptyAI::yourTacticPhase(int distance) void CEmptyAI::yourTacticPhase(int distance)
{ {
cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
} }
void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)

View File

@ -29,7 +29,7 @@ namespace NKAI
{ {
// our to enemy strength ratio constants // our to enemy strength ratio constants
const float SAFE_ATTACK_CONSTANT = 1.2f; const float SAFE_ATTACK_CONSTANT = 1.1f;
const float RETREAT_THRESHOLD = 0.3f; const float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
LOG_TRACE(logAi); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
validateObject(details.id); //enemy hero may have left visible area
auto hero = cb->getHero(details.id); auto hero = cb->getHero(details.id);
if(!hero)
validateObject(details.id); //enemy hero may have left visible area
const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0));
@ -777,28 +779,21 @@ void AIGateway::makeTurn()
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex); boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn"); setThreadName("AIGateway::makeTurn");
cb->sendMessage("vcmieagles");
retrieveVisitableObjs();
if(cb->getDate(Date::DAY_OF_WEEK) == 1) if(cb->getDate(Date::DAY_OF_WEEK) == 1)
{ {
std::vector<const CGObjectInstance *> objs; for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs)
retrieveVisitableObjs(objs, true);
for(const CGObjectInstance * obj : objs)
{ {
if(isWeeklyRevisitable(obj)) if(isWeeklyRevisitable(obj))
{ {
addVisitableObj(obj);
nullkiller->memory->markObjectUnvisited(obj); nullkiller->memory->markObjectUnvisited(obj);
} }
} }
} }
cb->sendMessage("vcmieagles");
if(cb->getDate(Date::DAY) == 1)
{
retrieveVisitableObjs();
}
#if NKAI_TRACE_LEVEL == 0 #if NKAI_TRACE_LEVEL == 0
try try
{ {
@ -809,7 +804,7 @@ void AIGateway::makeTurn()
for (auto h : cb->getHeroesInfo()) for (auto h : cb->getHeroesInfo())
{ {
if (h->movementPointsRemaining()) if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
} }
#if NKAI_TRACE_LEVEL == 0 #if NKAI_TRACE_LEVEL == 0
} }
@ -872,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
for(auto army : armies)
{
// move first stack at first slot if empty to avoid can not take away last creature
if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0)
{
cb->mergeOrSwapStacks(
army,
army,
SlotID(0),
army->Slots().begin()->first);
}
}
//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
{ {
@ -1059,20 +1067,25 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
int count = d->creatures[i].first; int count = d->creatures[i].first;
CreatureID creID = d->creatures[i].second.back(); CreatureID creID = d->creatures[i].second.back();
if(!recruiter->getSlotFor(creID).validSlot())
{
continue;
}
vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
if(count > 0) if(count > 0)
cb->recruitCreatures(d, recruiter, creID, count, i); cb->recruitCreatures(d, recruiter, creID, count, i);
} }
} }
void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_BATTLE); status.setBattle(ONGOING_BATTLE);
const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
} }
void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
@ -1083,12 +1096,16 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
bool won = br->winner == myCb->battleGetMySide(); bool won = br->winner == myCb->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
battlename.clear(); battlename.clear();
status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0; if (queryID != -1)
requestActionASAP([=]()
{ {
answerQuery(queryID, confirmAction); status.addQuery(queryID, "Combat result dialog");
}); const int confirmAction = 0;
requestActionASAP([=]()
{
answerQuery(queryID, confirmAction);
});
}
CAdventureAI::battleEnd(br, queryID); CAdventureAI::battleEnd(br, queryID);
} }
@ -1098,26 +1115,13 @@ void AIGateway::waitTillFree()
status.waitTillFree(); status.waitTillFree();
} }
void AIGateway::retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned) const
{
foreach_tile_pos([&](const int3 & pos)
{
for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
{
if(includeOwned || obj->tempOwner != playerID)
out.push_back(obj);
}
});
}
void AIGateway::retrieveVisitableObjs() void AIGateway::retrieveVisitableObjs()
{ {
foreach_tile_pos([&](const int3 & pos) foreach_tile_pos([&](const int3 & pos)
{ {
for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
{ {
if(obj->tempOwner != playerID) addVisitableObj(obj);
addVisitableObj(obj);
} }
}); });
} }
@ -1175,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
if(startHpos == dst) if(startHpos == dst)
{ {
//FIXME: this assertion fails also if AI moves onto defeated guarded object //FIXME: this assertion fails also if AI moves onto defeated guarded object
assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object //assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
cb->moveHero(*h, h->convertFromVisitablePos(dst)); cb->moveHero(*h, h->convertFromVisitablePos(dst));
afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared

View File

@ -169,7 +169,7 @@ public:
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();
@ -195,7 +195,6 @@ public:
void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
void retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
void retrieveVisitableObjs(); void retrieveVisitableObjs();
virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const; virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;

View File

@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
if(dynamic_cast<const CGDwelling *>(obj)) if(dynamic_cast<const CGDwelling *>(obj))
return true; return true;
if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
return true;
switch(obj->ID) switch(obj->ID)
{ {
case Obj::STABLES:
case Obj::MAGIC_WELL:
case Obj::HILL_FORT: case Obj::HILL_FORT:
return true; return true;
case Obj::BORDER_GATE: case Obj::BORDER_GATE:

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../CCallback.h" #include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/GameConstants.h"
namespace NKAI namespace NKAI
{ {
@ -33,6 +34,45 @@ public:
} }
}; };
void ArmyUpgradeInfo::addArmyToBuy(std::vector<SlotInfo> army)
{
for(auto slot : army)
{
resultingArmy.push_back(slot);
upgradeValue += slot.power;
upgradeCost += slot.creature->getFullRecruitCost() * slot.count;
}
}
void ArmyUpgradeInfo::addArmyToGet(std::vector<SlotInfo> army)
{
for(auto slot : army)
{
resultingArmy.push_back(slot);
upgradeValue += slot.power;
}
}
std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
{
std::vector<SlotInfo> result;
for(auto i : army)
{
SlotInfo slot;
slot.creature = VLC->creh->objects[i.cre->getId()];
slot.count = i.count;
slot.power = evaluateStackPower(i.cre, i.count);
result.push_back(slot);
}
return result;
}
uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
{ {
return howManyReinforcementsCanGet(hero, hero, source); return howManyReinforcementsCanGet(hero, hero, source);
@ -130,13 +170,13 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
std::vector<SlotInfo> newArmy; std::vector<SlotInfo> newArmy;
uint64_t newValue = 0; uint64_t newValue = 0;
newArmyInstance.clear(); newArmyInstance.clearSlots();
for(auto & slot : sortedSlots) for(auto & slot : sortedSlots)
{ {
if(vstd::contains(allowedFactions, slot.creature->getFaction())) if(vstd::contains(allowedFactions, slot.creature->getFaction()))
{ {
auto slotID = newArmyInstance.getSlotFor(slot.creature); auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
if(slotID.validSlot()) if(slotID.validSlot())
{ {
@ -238,7 +278,8 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
ui64 ArmyManager::howManyReinforcementsCanBuy( ui64 ArmyManager::howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, const CGDwelling * dwelling,
const TResources & availableResources) const const TResources & availableResources,
uint8_t turn) const
{ {
ui64 aivalue = 0; ui64 aivalue = 0;
auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources); auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
@ -259,17 +300,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
std::vector<creInfo> ArmyManager::getArmyAvailableToBuy( std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
const CCreatureSet * hero, const CCreatureSet * hero,
const CGDwelling * dwelling, const CGDwelling * dwelling,
TResources availableRes) const TResources availableRes,
uint8_t turn) const
{ {
std::vector<creInfo> creaturesInDwellings; std::vector<creInfo> creaturesInDwellings;
int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
const CGTownInstance * town = dwelling->ID == Obj::TOWN
? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--) for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{ {
auto ci = infoFromDC(dwelling->creatures[i]); auto ci = infoFromDC(dwelling->creatures[i]);
if(!ci.count || ci.creID == -1) if(ci.creID == -1) continue;
continue;
if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
{
ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth();
}
if(!ci.count) continue;
SlotID dst = hero->getSlotFor(ci.creID); SlotID dst = hero->getSlotFor(ci.creID);
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@ -282,8 +335,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
if(!ci.count) if(!ci.count) continue;
continue;
ci.level = i; //this is important for Dungeon Summoning Portal ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci); creaturesInDwellings.push_back(ci);
@ -307,7 +359,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier,
return newArmy > oldArmy ? newArmy - oldArmy : 0; return newArmy > oldArmy ? newArmy - oldArmy : 0;
} }
uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const
{ {
return creature->getAIValue() * count; return creature->getAIValue() * count;
} }

View File

@ -34,6 +34,9 @@ struct ArmyUpgradeInfo
std::vector<SlotInfo> resultingArmy; std::vector<SlotInfo> resultingArmy;
uint64_t upgradeValue = 0; uint64_t upgradeValue = 0;
TResources upgradeCost; TResources upgradeCost;
void addArmyToBuy(std::vector<SlotInfo> army);
void addArmyToGet(std::vector<SlotInfo> army);
}; };
class DLL_EXPORT IArmyManager //: public: IAbstractManager class DLL_EXPORT IArmyManager //: public: IAbstractManager
@ -45,20 +48,33 @@ public:
virtual ui64 howManyReinforcementsCanBuy( virtual ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, const CGDwelling * dwelling,
const TResources & availableResources) const = 0; const TResources & availableResources,
uint8_t turn = 0) const = 0;
virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0; virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual ui64 howManyReinforcementsCanGet(
const IBonusBearer * armyCarrier,
const CCreatureSet * target,
const CCreatureSet * source) const = 0;
virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0; virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const = 0; virtual std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const = 0;
virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(
const CCreatureSet * hero,
const CGDwelling * dwelling,
TResources availableRes,
uint8_t turn = 0) const = 0;
virtual uint64_t evaluateStackPower(const Creature * creature, int count) const = 0;
virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
virtual ArmyUpgradeInfo calculateCreaturesUpgrade( virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
const CCreatureSet * army, const CCreatureSet * army,
const CGObjectInstance * upgrader, const CGObjectInstance * upgrader,
const TResources & availableResources) const = 0; const TResources & availableResources) const = 0;
virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0; virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
}; };
@ -74,20 +90,30 @@ private:
public: public:
ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
void update() override; void update() override;
ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
ui64 howManyReinforcementsCanBuy( ui64 howManyReinforcementsCanBuy(
const CCreatureSet * targetArmy, const CCreatureSet * targetArmy,
const CGDwelling * dwelling, const CGDwelling * dwelling,
const TResources & availableResources) const override; const TResources & availableResources,
uint8_t turn = 0) const override;
ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override; ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override; std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override; std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const override; std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const override;
std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
std::vector<creInfo> getArmyAvailableToBuy(
const CCreatureSet * hero,
const CGDwelling * dwelling,
TResources availableRes,
uint8_t turn = 0) const override;
std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
uint64_t evaluateStackPower(const CCreature * creature, int count) const override; uint64_t evaluateStackPower(const Creature * creature, int count) const override;
SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
ArmyUpgradeInfo calculateCreaturesUpgrade( ArmyUpgradeInfo calculateCreaturesUpgrade(
const CCreatureSet * army, const CCreatureSet * army,

View File

@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
logAi->trace("Checking other buildings"); logAi->trace("Checking other buildings");
std::vector<std::vector<BuildingID>> otherBuildings = { std::vector<std::vector<BuildingID>> otherBuildings = {
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL},
{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
}; };
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{ {
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
} }
for(auto & buildingSet : otherBuildings) for(auto & buildingSet : otherBuildings)
{ {
for(auto & buildingID : buildingSet) for(auto & buildingID : buildingSet)
{ {
if(!developmentInfo.town->hasBuilt(buildingID)) if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
{ {
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
@ -163,8 +166,8 @@ void BuildAnalyzer::update()
} }
else else
{ {
goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
} }
logAi->trace("Gold preasure: %f", goldPreasure); logAi->trace("Gold preasure: %f", goldPreasure);
@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CCreature * creature = nullptr; const CCreature * creature = nullptr;
CreatureID baseCreatureID; CreatureID baseCreatureID;
int creatureLevel = -1;
int creatureUpgrade = 0;
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{ {
int level = toBuild - BuildingID::DWELL_FIRST; creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN); creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN }
? creatures.at(level / GameConstants::CREATURES_PER_TOWN) else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(0);
}
else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(1);
}
if(creatureLevel >= 0)
{
auto creatures = townInfo->creatures.at(creatureLevel);
auto creatureID = creatures.size() > creatureUpgrade
? creatures.at(creatureUpgrade)
: creatures.front(); : creatures.front();
baseCreatureID = creatures.front(); baseCreatureID = creatures.front();
@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo(
} }
else else
{ {
creatureGrows = creature->getGrowth(); if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST)
{
creatureGrows = creature->getGrowth();
if(town->hasBuilt(BuildingID::CASTLE)) if(town->hasBuilt(BuildingID::CASTLE))
creatureGrows *= 2; creatureGrows *= 2;
else if(town->hasBuilt(BuildingID::CITADEL)) else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2; creatureGrows += creatureGrows / 2;
}
else
{
creatureGrows = creature->getHorde();
}
} }
armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows); armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);

View File

@ -17,20 +17,29 @@ namespace NKAI
HitMapInfo HitMapInfo::NoTreat; HitMapInfo HitMapInfo::NoTreat;
double HitMapInfo::value() const
{
return danger / std::sqrt(turn / 3.0f + 1);
}
void DangerHitMapAnalyzer::updateHitMap() void DangerHitMapAnalyzer::updateHitMap()
{ {
if(upToDate) if(hitMapUpToDate)
return; return;
logAi->trace("Update danger hitmap"); logAi->trace("Update danger hitmap");
upToDate = true; hitMapUpToDate = true;
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
auto cb = ai->cb.get(); auto cb = ai->cb.get();
auto mapSize = ai->cb->getMapSize(); auto mapSize = ai->cb->getMapSize();
hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
enemyHeroAccessibleObjects.clear(); enemyHeroAccessibleObjects.clear();
townTreats.clear();
std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes; std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
@ -44,6 +53,13 @@ void DangerHitMapAnalyzer::updateHitMap()
} }
} }
auto ourTowns = cb->getTownsInfo();
for(auto town : ourTowns)
{
townTreats[town->id]; // insert empty list
}
foreach_tile_pos([&](const int3 & pos){ foreach_tile_pos([&](const int3 & pos){
hitMap[pos.x][pos.y][pos.z].reset(); hitMap[pos.x][pos.y][pos.z].reset();
}); });
@ -67,34 +83,53 @@ void DangerHitMapAnalyzer::updateHitMap()
if(path.getFirstBlockedAction()) if(path.getFirstBlockedAction())
continue; continue;
auto tileDanger = path.getHeroStrength();
auto turn = path.turn();
auto & node = hitMap[pos.x][pos.y][pos.z]; auto & node = hitMap[pos.x][pos.y][pos.z];
if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) HitMapInfo newTreat;
|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
newTreat.hero = path.targetHero;
newTreat.turn = path.turn();
newTreat.danger = path.getHeroStrength();
if(newTreat.value() > node.maximumDanger.value())
{ {
node.maximumDanger.danger = tileDanger; node.maximumDanger = newTreat;
node.maximumDanger.turn = turn;
node.maximumDanger.hero = path.targetHero;
} }
if(turn < node.fastestDanger.turn if(newTreat.turn < node.fastestDanger.turn
|| (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)) || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger))
{ {
node.fastestDanger.danger = tileDanger; node.fastestDanger = newTreat;
node.fastestDanger.turn = turn;
node.fastestDanger.hero = path.targetHero;
} }
if(turn == 0) auto objects = cb->getVisitableObjs(pos, false);
for(auto obj : objects)
{ {
auto objects = cb->getVisitableObjs(pos, false); if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID)
for(auto obj : objects)
{ {
if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) auto & treats = townTreats[obj->id];
enemyHeroAccessibleObjects[path.targetHero].insert(obj); auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool
{
return i.hero.hid == path.targetHero->id;
});
if(treat == treats.end())
{
treats.emplace_back();
treat = std::prev(treats.end(), 1);
}
if(newTreat.value() > treat->value())
{
*treat = newTreat;
}
if(newTreat.turn == 0)
{
if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES)
enemyHeroAccessibleObjects.emplace_back(path.targetHero, obj);
}
} }
} }
} }
@ -104,6 +139,122 @@ void DangerHitMapAnalyzer::updateHitMap()
logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
} }
void DangerHitMapAnalyzer::calculateTileOwners()
{
if(tileOwnersUpToDate) return;
tileOwnersUpToDate = true;
auto cb = ai->cb.get();
auto mapSize = ai->cb->getMapSize();
if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
std::map<const CGHeroInstance *, HeroRole> townHeroes;
std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
PathfinderSettings pathfinderSettings;
pathfinderSettings.mainTurnDistanceLimit = 5;
auto addTownHero = [&](const CGTownInstance * town)
{
auto townHero = new CGHeroInstance();
CRandomGenerator rng;
auto visitablePos = town->visitablePos();
townHero->setOwner(ai->playerID); // lets avoid having multiple colors
townHero->initHero(rng, static_cast<HeroTypeID>(0));
townHero->pos = townHero->convertFromVisitablePos(visitablePos);
townHero->initObj(rng);
heroTownMap[townHero] = town;
townHeroes[townHero] = HeroRole::MAIN;
};
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->ID == Obj::TOWN)
{
addTownHero(dynamic_cast<const CGTownInstance *>(obj));
}
}
for(auto town : cb->getTownsInfo())
{
addTownHero(town);
}
ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
pforeachTilePos(mapSize, [&](const int3 & pos)
{
float ourDistance = std::numeric_limits<float>::max();
float enemyDistance = std::numeric_limits<float>::max();
const CGTownInstance * enemyTown = nullptr;
const CGTownInstance * ourTown = nullptr;
for(AIPath & path : ai->pathfinder->getPathInfo(pos))
{
if(!path.targetHero || path.getFirstBlockedAction())
continue;
auto town = heroTownMap[path.targetHero];
if(town->getOwner() == ai->playerID)
{
if(ourDistance > path.movementCost())
{
ourDistance = path.movementCost();
ourTown = town;
}
}
else
{
if(enemyDistance > path.movementCost())
{
enemyDistance = path.movementCost();
enemyTown = town;
}
}
}
if(ourDistance == enemyDistance)
{
hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
}
else if(!enemyTown || ourDistance < enemyDistance)
{
hitMap[pos.x][pos.y][pos.z].closestTown = ourTown;
}
else
{
hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown;
}
});
}
const std::vector<HitMapInfo> & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const
{
static const std::vector<HitMapInfo> empty = {};
auto result = townTreats.find(town->id);
return result == townTreats.end() ? empty : result->second;
}
PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const
{
auto town = hitMap[tile.x][tile.y][tile.z].closestTown;
return town ? town->getOwner() : PlayerColor::NEUTRAL;
}
const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const
{
return hitMap[tile.x][tile.y][tile.z].closestTown;
}
uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
{ {
int3 tile = path.targetTile(); int3 tile = path.targetTile();
@ -130,21 +281,22 @@ const HitMapNode & DangerHitMapAnalyzer::getTileTreat(const int3 & tile) const
const std::set<const CGObjectInstance *> empty = {}; const std::set<const CGObjectInstance *> empty = {};
const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const
{ {
auto result = enemyHeroAccessibleObjects.find(enemy); std::set<const CGObjectInstance *> result;
if(result == enemyHeroAccessibleObjects.end()) for(auto & obj : enemyHeroAccessibleObjects)
{ {
return empty; if(obj.hero == enemy)
result.insert(obj.obj);
} }
return result->second; return result;
} }
void DangerHitMapAnalyzer::reset() void DangerHitMapAnalyzer::reset()
{ {
upToDate = false; hitMapUpToDate = false;
} }
} }

View File

@ -35,6 +35,8 @@ struct HitMapInfo
turn = 255; turn = 255;
hero = HeroPtr(); hero = HeroPtr();
} }
double value() const;
}; };
struct HitMapNode struct HitMapNode
@ -42,6 +44,8 @@ struct HitMapNode
HitMapInfo maximumDanger; HitMapInfo maximumDanger;
HitMapInfo fastestDanger; HitMapInfo fastestDanger;
const CGTownInstance * closestTown = nullptr;
HitMapNode() = default; HitMapNode() = default;
void reset() void reset()
@ -51,23 +55,41 @@ struct HitMapNode
} }
}; };
struct EnemyHeroAccessibleObject
{
const CGHeroInstance * hero;
const CGObjectInstance * obj;
EnemyHeroAccessibleObject(const CGHeroInstance * hero, const CGObjectInstance * obj)
:hero(hero), obj(obj)
{
}
};
class DangerHitMapAnalyzer class DangerHitMapAnalyzer
{ {
private: private:
boost::multi_array<HitMapNode, 3> hitMap; boost::multi_array<HitMapNode, 3> hitMap;
std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects; tbb::concurrent_vector<EnemyHeroAccessibleObject> enemyHeroAccessibleObjects;
bool upToDate; bool hitMapUpToDate = false;
bool tileOwnersUpToDate = false;
const Nullkiller * ai; const Nullkiller * ai;
std::map<ObjectInstanceID, std::vector<HitMapInfo>> townTreats;
public: public:
DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
void updateHitMap(); void updateHitMap();
void calculateTileOwners();
uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
const HitMapNode & getTileTreat(const int3 & tile) const; const HitMapNode & getTileTreat(const int3 & tile) const;
const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; std::set<const CGObjectInstance *> getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
void reset(); void reset();
void resetTileOwners() { tileOwnersUpToDate = false; }
PlayerColor getTileOwner(const int3 & tile) const;
const CGTownInstance * getClosestTown(const int3 & tile) const;
const std::vector<HitMapInfo> & getTownTreats(const CGTownInstance * town) const;
}; };
} }

View File

@ -125,6 +125,7 @@ void HeroManager::update()
} }
std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
heroRoles.clear();
for(auto hero : myHeroes) for(auto hero : myHeroes)
{ {
@ -180,6 +181,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero); return evaluateFightingStrength(hero);
} }
bool HeroManager::heroCapReached() const
{
const bool includeGarnisoned = true;
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
}
bool HeroManager::canRecruitHero(const CGTownInstance * town) const bool HeroManager::canRecruitHero(const CGTownInstance * town) const
{ {
if(!town) if(!town)
@ -191,13 +201,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
return false; return false;
const bool includeGarnisoned = true; if(heroCapReached())
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
if(heroCount >= ALLOWED_ROAMING_HEROES)
return false;
if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false; return false;
if(!cb->getAvailableHeroes(town).size()) if(!cb->getAvailableHeroes(town).size())
@ -225,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
return nullptr; return nullptr;
} }
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
{
const CGHeroInstance * weakestHero = nullptr;
auto myHeroes = ai->cb->getHeroesInfo();
for(auto existingHero : myHeroes)
{
if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE
|| existingHero->getArmyStrength() >armyLimit
|| getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
{
continue;
}
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
}
return weakestHero;
}
SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap) SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
:scoreMap(scoreMap) :scoreMap(scoreMap)
{ {

View File

@ -31,7 +31,9 @@ public:
virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
virtual float evaluateHero(const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
virtual bool heroCapReached() const = 0;
virtual const CGHeroInstance * findHeroWithGrail() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0;
virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
}; };
class DLL_EXPORT ISecondarySkillRule class DLL_EXPORT ISecondarySkillRule
@ -71,7 +73,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
float evaluateHero(const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override;
bool canRecruitHero(const CGTownInstance * t = nullptr) const override; bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
bool heroCapReached() const override;
const CGHeroInstance * findHeroWithGrail() const override; const CGHeroInstance * findHeroWithGrail() const override;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
private: private:
float evaluateFightingStrength(const CGHeroInstance * hero) const; float evaluateFightingStrength(const CGHeroInstance * hero) const;

View File

@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize()
auto obj = objs[i]; auto obj = objs[i];
if(!shouldVisitObject(obj)) if(!shouldVisitObject(obj))
return; {
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
continue;
}
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());

View File

@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
tasks.reserve(paths.size()); tasks.reserve(paths.size());
const AIPath * closestWay = nullptr; std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
std::vector<ExecuteHeroChain *> waysToVisitObj; std::vector<ExecuteHeroChain *> waysToVisitObj;
for(auto & path : paths) for(auto & path : paths)
@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT auto & closestWay = closestWaysByRole[heroRole];
&& (!closestWay || closestWay->movementCost() > path.movementCost()))
if(!closestWay || closestWay->movementCost() > path.movementCost())
{ {
closestWay = &path; closestWay = &path;
} }
@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
} }
} }
if(closestWay) for(auto way : waysToVisitObj)
{ {
for(auto way : waysToVisitObj) auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
auto closestWay = closestWaysByRole[heroRole];
if(closestWay)
{ {
way->closestWayRatio way->closestWayRatio
= closestWay->movementCost() / way->getPath().movementCost(); = closestWay->movementCost() / way->getPath().movementCost();
@ -209,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
{ {
captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL) if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
captureObjects(ai->nullkiller->objectClusterizer->getFarObjects()); captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
} }

View File

@ -49,37 +49,119 @@ Goals::TGoalVec DefenceBehavior::decompose() const
return tasks; return tasks;
} }
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector<AIPath> & paths)
{ {
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
if(town->garrisonHero) for(const AIPath & path : paths)
{ {
if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{ {
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{ {
#if NKAI_TRACE_LEVEL >= 1
logAi->trace( logAi->trace(
"Extracting hero %s from garrison of town %s", "Hero %s can eliminate danger for town %s using path %s.",
town->garrisonHero->getNameTranslated(), path.targetHero->getObjectName(),
town->getNameTranslated()); town->getObjectName(),
path.toString());
#endif
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); return true;
return;
} }
} }
}
return false;
}
void handleCounterAttack(
const CGTownInstance * town,
const HitMapInfo & treat,
const HitMapInfo & maximumDanger,
Goals::TGoalVec & tasks)
{
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn))
{
auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
for(int i = 0; i < heroCapturingPaths.size(); i++)
{
AIPath & path = heroCapturingPaths[i];
TSubgoal goal = goals[i];
if(!goal || goal->invalid() || !goal->isElementar()) continue;
Composition composition;
composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
}
}
bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks)
{
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
logAi->trace( logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town", "Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(), town->garrisonHero->getNameTranslated(),
town->getNameTranslated()); town->getNameTranslated());
return true;
}
if(!town->visitingHero)
{
if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true;
}
else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{
auto armyDismissLimit = 1000;
auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit);
if(heroToDismiss)
{
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true;
}
}
}
return false;
}
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
{
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks))
{
return; return;
} }
@ -107,105 +189,17 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
town->getNameTranslated(), town->getNameTranslated(),
treat.danger, treat.danger,
std::to_string(treat.turn), std::to_string(treat.turn),
treat.hero->getNameTranslated()); treat.hero ? treat.hero->getNameTranslated() : std::string("<no hero>"));
bool treatIsUnderControl = false; handleCounterAttack(town, treat, treatNode.maximumDanger, tasks);
for(AIPath & path : paths) if(isTreatUnderControl(town, treat, paths))
{ {
if(town->visitingHero && path.targetHero != town->visitingHero.get())
continue;
if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)
&& isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
{
Composition composition;
composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get()));
tasks.push_back(Goals::sptr(composition));
}
bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{
if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace(
"Hero %s can eliminate danger for town %s using path %s.",
path.targetHero->getObjectName(),
town->getObjectName(),
path.toString());
#endif
treatIsUnderControl = true;
break;
}
}
}
if(treatIsUnderControl)
continue; continue;
if(!town->visitingHero
&& town->hasBuilt(BuildingID::TAVERN)
&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
auto heroesInTavern = cb->getAvailableHeroes(town);
for(auto hero : heroesInTavern)
{
if(hero->getTotalStrength() > treat.danger)
{
auto myHeroes = cb->getHeroesInfo();
if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
#endif
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
continue;
}
else
{
const CGHeroInstance * weakestHero = nullptr;
for(auto existingHero : myHeroes)
{
if(ai->nullkiller->isHeroLocked(existingHero)
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
continue;
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
{
weakestHero = existingHero;
}
if(weakestHero)
{
tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero)));
}
}
}
}
}
} }
evaluateRecruitingHero(tasks, treat, town);
if(paths.empty()) if(paths.empty())
{ {
logAi->trace("No ways to defend town %s", town->getNameTranslated()); logAi->trace("No ways to defend town %s", town->getNameTranslated());
@ -229,6 +223,22 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
path.movementCost(), path.movementCost(),
path.toString()); path.toString());
#endif #endif
auto townDefenseStrength = town->garrisonHero
? town->garrisonHero->getTotalStrength()
: (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength());
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
if(path.turn() <= treat.turn - 2) if(path.turn() <= treat.turn - 2)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@ -275,9 +285,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
tasks.push_back( tasks.push_back(
Goals::sptr(Composition() Goals::sptr(Composition()
.addNext(DefendTown(town, treat, path)) .addNext(DefendTown(town, treat, path))
.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get())) .addNextSequence({
.addNext(ExecuteHeroChain(path, town)) sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)))); sptr(ExecuteHeroChain(path, town)),
sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
})));
continue; continue;
} }
@ -313,15 +325,58 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
} }
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
Composition composition; Composition composition;
composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town)); composition.addNext(DefendTown(town, treat, path));
TGoalVec sequence;
if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1)
{
composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE));
tasks.push_back(Goals::sptr(composition));
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Locking hero %s in garrison of %s",
town->garrisonHero.get()->getObjectName(),
town->getObjectName());
#endif
continue;
}
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
{
if(ai->nullkiller->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());
#endif
continue;
}
}
else if(path.turn() == 0)
{
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
}
}
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Move %s to defend town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
sequence.push_back(sptr(ExecuteHeroChain(path, town)));
composition.addNextSequence(sequence);
auto firstBlockedAction = path.getFirstBlockedAction(); auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction) if(firstBlockedAction)
@ -350,4 +405,70 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
logAi->debug("Found %d tasks", tasks.size()); logAi->debug("Found %d tasks", tasks.size());
} }
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const
{
if(town->hasBuilt(BuildingID::TAVERN)
&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
auto heroesInTavern = cb->getAvailableHeroes(town);
for(auto hero : heroesInTavern)
{
if(hero->getTotalStrength() < treat.danger)
continue;
auto myHeroes = cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
#endif
bool needSwap = false;
const CGHeroInstance * heroToDismiss = nullptr;
if(town->visitingHero)
{
if(!town->garrisonHero)
needSwap = true;
else
{
if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength())
{
if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength())
continue;
heroToDismiss = town->visitingHero.get();
}
else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength())
continue;
else
{
needSwap = true;
heroToDismiss = town->garrisonHero.get();
}
}
}
else if(ai->nullkiller->heroManager->heroCapReached())
{
heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
if(!heroToDismiss)
continue;
}
TGoalVec sequence;
Goals::Composition recruitHeroComposition;
if(needSwap)
sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
if(heroToDismiss)
sequence.push_back(sptr(DismissHero(heroToDismiss)));
sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence)));
}
}
}
} }

View File

@ -15,8 +15,12 @@
namespace NKAI namespace NKAI
{ {
struct HitMapInfo;
namespace Goals namespace Goals
{ {
class DefenceBehavior : public CGoal<DefenceBehavior> class DefenceBehavior : public CGoal<DefenceBehavior>
{ {
public: public:
@ -35,6 +39,7 @@ namespace Goals
private: private:
void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
}; };
} }

View File

@ -12,10 +12,13 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../Goals/ExecuteHeroChain.h" #include "../Goals/ExecuteHeroChain.h"
#include "../Goals/Composition.h" #include "../Goals/Composition.h"
#include "../Goals/RecruitHero.h"
#include "../Markers/HeroExchange.h" #include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h" #include "../Markers/ArmyUpgrade.h"
#include "GatherArmyBehavior.h" #include "GatherArmyBehavior.h"
#include "CaptureObjectsBehavior.h"
#include "../AIUtility.h" #include "../AIUtility.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
namespace NKAI namespace NKAI
{ {
@ -78,20 +81,18 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
for(const AIPath & path : paths) for(const AIPath & path : paths)
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path found %s", path.toString()); logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif #endif
if(path.containsHero(hero)) continue; if(path.containsHero(hero))
if(path.turn() == 0 && hero->inTownGarrison)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); logAi->trace("Selfcontaining path. Ignore");
#endif #endif
continue; continue;
} }
if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@ -109,10 +110,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
HeroExchange heroExchange(hero, path); HeroExchange heroExchange(hero, path);
float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); uint64_t armyValue = heroExchange.getReinforcementArmyStrength();
float armyRatio = (float)armyValue / hero->getArmyStrength();
// avoid transferring very small amount of army // avoid transferring very small amount of army
if(armyValue < 0.1f && armyValue < 20000) if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500)
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Army value is too small."); logAi->trace("Army value is too small.");
@ -172,7 +174,28 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
exchangePath.closestWayRatio = 1; exchangePath.closestWayRatio = 1;
composition.addNext(heroExchange); composition.addNext(heroExchange);
composition.addNext(exchangePath);
if(hero->inTownGarrison && path.turn() == 0)
{
auto lockReason = ai->nullkiller->getHeroLockedReason(hero);
if(path.targetHero->visitedTown == hero->visitedTown)
{
composition.addNextSequence({
sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))});
}
else
{
composition.addNextSequence({
sptr(ExchangeSwapTownHeroes(hero->visitedTown)),
sptr(exchangePath),
sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))});
}
}
else
{
composition.addNext(exchangePath);
}
auto blockedAction = path.getFirstBlockedAction(); auto blockedAction = path.getFirstBlockedAction();
@ -212,18 +235,42 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
#endif #endif
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj; std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Found %d paths", paths.size()); logAi->trace("Found %d paths", paths.size());
#endif #endif
bool hasMainAround = false;
for(const AIPath & path : paths) for(const AIPath & path : paths)
{ {
auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
hasMainAround = true;
}
for(int i = 0; i < paths.size(); i++)
{
auto & path = paths[i];
auto visitGoal = goals[i];
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path found %s", path.toString()); logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif #endif
if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
if(visitGoal->invalid())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Not valid way.");
#endif
continue;
}
if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Town has visiting hero."); logAi->trace("Ignore path. Town has visiting hero.");
@ -261,18 +308,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) if(!upgrader->garrisonHero
&& (
hasMainAround
|| ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN))
{ {
upgrade.upgradeValue += ArmyUpgradeInfo armyToGetOrBuy;
ai->nullkiller->armyManager->howManyReinforcementsCanGet(
armyToGetOrBuy.addArmyToGet(
ai->nullkiller->armyManager->getBestArmy(
path.targetHero, path.targetHero,
path.heroArmy, path.heroArmy,
upgrader->getUpperArmy()); upgrader->getUpperArmy()));
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
armyToGetOrBuy.addArmyToBuy(
ai->nullkiller->armyManager->toSlotInfo(
ai->nullkiller->armyManager->getArmyAvailableToBuy(
path.heroArmy,
upgrader,
ai->nullkiller->getFreeResources(),
path.turn())));
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
if(!upgrade.upgradeValue
&& armyToGetOrBuy.upgradeValue > 20000
&& ai->nullkiller->heroManager->canRecruitHero(town)
&& path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
{
for(auto hero : cb->getAvailableHeroes(town))
{
auto scoutReinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town)
+ ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->nullkiller->getFreeGold() >20000
&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)
{
Composition recruitHero;
recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero));
}
}
}
} }
auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // avoid small upgrades
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Army value is too small (%f)", armyValue); logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
@ -297,11 +384,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
if(isSafe) if(isSafe)
{ {
ExecuteHeroChain newWay(path, upgrader); tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal)));
newWay.closestWayRatio = 1;
tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay)));
} }
} }

View File

@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
} }
} }
int treasureSourcesCount = 0;
for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
{
if((obj->ID == Obj::RESOURCE)
|| obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE
|| isWeeklyRevisitable(obj)
|| obj->ID ==Obj::ARTIFACT)
{
auto tile = obj->visitablePos();
auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile);
if(town == closestTown)
treasureSourcesCount++;
}
}
if(treasureSourcesCount < 5)
continue;
if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))

View File

@ -52,6 +52,7 @@ set(Nullkiller_SRCS
Behaviors/BuildingBehavior.cpp Behaviors/BuildingBehavior.cpp
Behaviors/GatherArmyBehavior.cpp Behaviors/GatherArmyBehavior.cpp
Behaviors/ClusterBehavior.cpp Behaviors/ClusterBehavior.cpp
Helpers/ArmyFormation.cpp
AIGateway.cpp AIGateway.cpp
) )
@ -114,6 +115,7 @@ set(Nullkiller_HEADERS
Behaviors/BuildingBehavior.h Behaviors/BuildingBehavior.h
Behaviors/GatherArmyBehavior.h Behaviors/GatherArmyBehavior.h
Behaviors/ClusterBehavior.h Behaviors/ClusterBehavior.h
Helpers/ArmyFormation.h
AIGateway.h AIGateway.h
) )

View File

@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
case Obj::MINE: case Obj::MINE:
case Obj::ABANDONED_MINE: case Obj::ABANDONED_MINE:
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
}
case Obj::CRYPT: //crypt case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA: case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship case Obj::DERELICT_SHIP: //derelict ship
// case Obj::PYRAMID: {
return estimateBankDanger(dynamic_cast<const CBank *>(obj)); const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
}
case Obj::PYRAMID: case Obj::PYRAMID:
{ {
if(obj->subID == 0) if(obj->subID == 0)

View File

@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
armyManager.reset(new ArmyManager(cb.get(), this)); armyManager.reset(new ArmyManager(cb.get(), this));
heroManager.reset(new HeroManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this));
decomposer.reset(new DeepDecomposer()); decomposer.reset(new DeepDecomposer());
armyFormation.reset(new ArmyFormation(cb, this));
} }
Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@ -117,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
void Nullkiller::resetAiState() void Nullkiller::resetAiState()
{ {
lockedResources = TResources(); lockedResources = TResources();
scanDepth = ScanDepth::FULL; scanDepth = ScanDepth::MAIN_FULL;
playerID = ai->playerID; playerID = ai->playerID;
lockedHeroes.clear(); lockedHeroes.clear();
dangerHitMap->reset(); dangerHitMap->reset();
@ -133,10 +134,14 @@ void Nullkiller::updateAiState(int pass, bool fast)
activeHero = nullptr; activeHero = nullptr;
setTargetObject(-1); setTargetObject(-1);
decomposer->reset();
buildAnalyzer->update();
if(!fast) if(!fast)
{ {
memory->removeInvisibleObjects(cb.get()); memory->removeInvisibleObjects(cb.get());
dangerHitMap->calculateTileOwners();
dangerHitMap->updateHitMap(); dangerHitMap->updateHitMap();
boost::this_thread::interruption_point(); boost::this_thread::interruption_point();
@ -156,11 +161,15 @@ void Nullkiller::updateAiState(int pass, bool fast)
PathfinderSettings cfg; PathfinderSettings cfg;
cfg.useHeroChain = useHeroChain; cfg.useHeroChain = useHeroChain;
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
if(scanDepth != ScanDepth::FULL) if(scanDepth == ScanDepth::SMALL)
{ {
cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
}
if(scanDepth != ScanDepth::ALL_FULL)
{
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
} }
boost::this_thread::interruption_point(); boost::this_thread::interruption_point();
@ -173,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast)
} }
armyManager->update(); armyManager->update();
buildAnalyzer->update();
decomposer->reset();
logAi->debug("AI state updated in %ld", timeElapsed(start)); logAi->debug("AI state updated in %ld", timeElapsed(start));
} }
@ -222,7 +229,7 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker); boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10; const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7; const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState(); resetAiState();
@ -231,8 +238,8 @@ void Nullkiller::makeTurn()
updateAiState(i); updateAiState(i);
Goals::TTask bestTask = taskptr(Goals::Invalid()); Goals::TTask bestTask = taskptr(Goals::Invalid());
do for(;i <= MAXPASS; i++)
{ {
Goals::TTaskVec fastTasks = { Goals::TTaskVec fastTasks = {
choseBestTask(sptr(BuyArmyBehavior()), 1), choseBestTask(sptr(BuyArmyBehavior()), 1),
@ -246,7 +253,11 @@ void Nullkiller::makeTurn()
executeTask(bestTask); executeTask(bestTask);
updateAiState(i, true); updateAiState(i, true);
} }
} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); else
{
break;
}
}
Goals::TTaskVec bestTasks = { Goals::TTaskVec bestTasks = {
bestTask, bestTask,
@ -265,7 +276,6 @@ void Nullkiller::makeTurn()
bestTask = choseBestTask(bestTasks); bestTask = choseBestTask(bestTasks);
HeroPtr hero = bestTask->getHero(); HeroPtr hero = bestTask->getHero();
HeroRole heroRole = HeroRole::MAIN; HeroRole heroRole = HeroRole::MAIN;
if(hero.validAndSet()) if(hero.validAndSet())
@ -274,26 +284,50 @@ void Nullkiller::makeTurn()
if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
useHeroChain = false; useHeroChain = false;
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::FULL) && scanDepth == ScanDepth::MAIN_FULL)
{ {
useHeroChain = false; useHeroChain = false;
scanDepth = ScanDepth::SMALL; scanDepth = ScanDepth::SMALL;
logAi->trace( logAi->trace(
"Goal %s has too low priority %f so increasing scan depth", "Goal %s has low priority %f so decreasing scan depth to gain performance.",
bestTask->toString(), bestTask->toString(),
bestTask->priority); bestTask->priority);
} }
if(bestTask->priority < MIN_PRIORITY) if(bestTask->priority < MIN_PRIORITY)
{ {
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
bestTask->toString(),
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
}
logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString());
return; return;
} }
executeTask(bestTask); executeTask(bestTask);
if(i == MAXPASS)
{
logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", bestTask->toString());
}
} }
} }

View File

@ -18,6 +18,7 @@
#include "../Analyzers/ArmyManager.h" #include "../Analyzers/ArmyManager.h"
#include "../Analyzers/HeroManager.h" #include "../Analyzers/HeroManager.h"
#include "../Analyzers/ObjectClusterizer.h" #include "../Analyzers/ObjectClusterizer.h"
#include "../Helpers/ArmyFormation.h"
namespace NKAI namespace NKAI
{ {
@ -39,9 +40,11 @@ enum class HeroLockedReason
enum class ScanDepth enum class ScanDepth
{ {
FULL = 0, MAIN_FULL = 0,
SMALL = 1 SMALL = 1,
ALL_FULL = 2
}; };
class Nullkiller class Nullkiller
@ -67,6 +70,7 @@ public:
std::unique_ptr<AIMemory> memory; std::unique_ptr<AIMemory> memory;
std::unique_ptr<FuzzyHelper> dangerEvaluator; std::unique_ptr<FuzzyHelper> dangerEvaluator;
std::unique_ptr<DeepDecomposer> decomposer; std::unique_ptr<DeepDecomposer> decomposer;
std::unique_ptr<ArmyFormation> armyFormation;
PlayerColor playerID; PlayerColor playerID;
std::shared_ptr<CCallback> cb; std::shared_ptr<CCallback> cb;

View File

@ -23,6 +23,7 @@
#include "../Goals/ExecuteHeroChain.h" #include "../Goals/ExecuteHeroChain.h"
#include "../Goals/BuildThis.h" #include "../Goals/BuildThis.h"
#include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/ExchangeSwapTownHeroes.h"
#include "../Goals/DismissHero.h"
#include "../Markers/UnlockCluster.h" #include "../Markers/UnlockCluster.h"
#include "../Markers/HeroExchange.h" #include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h" #include "../Markers/ArmyUpgrade.h"
@ -33,6 +34,7 @@ namespace NKAI
#define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller * ai) EvaluationContext::EvaluationContext(const Nullkiller * ai)
: movementCost(0.0), : movementCost(0.0),
@ -49,10 +51,16 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
turn(0), turn(0),
strategicalValue(0), strategicalValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0) enemyHeroDangerRatio(0),
armyGrowth(0)
{ {
} }
void EvaluationContext::addNonCriticalStrategicalValue(float value)
{
vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
}
PriorityEvaluator::~PriorityEvaluator() PriorityEvaluator::~PriorityEvaluator()
{ {
delete engine; delete engine;
@ -64,6 +72,7 @@ void PriorityEvaluator::initVisitTile()
std::string str = std::string((char *)file.first.get(), file.second); std::string str = std::string((char *)file.first.get(), file.second);
engine = fl::FllImporter().fromString(str); engine = fl::FllImporter().fromString(str);
armyLossPersentageVariable = engine->getInputVariable("armyLoss"); armyLossPersentageVariable = engine->getInputVariable("armyLoss");
armyGrowthVariable = engine->getInputVariable("armyGrowth");
heroRoleVariable = engine->getInputVariable("heroRole"); heroRoleVariable = engine->getInputVariable("heroRole");
dangerVariable = engine->getInputVariable("danger"); dangerVariable = engine->getInputVariable("danger");
turnVariable = engine->getInputVariable("turn"); turnVariable = engine->getInputVariable("turn");
@ -99,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
auto town = cb->getTown(target->id); auto town = cb->getTown(target->id);
auto fortLevel = town->fortLevel(); auto fortLevel = town->fortLevel();
if(town->hasCapitol()) return booster * 2000; if(town->hasCapitol())
return booster * 2000;
// probably well developed town will have city hall // probably well developed town will have city hall
if(fortLevel == CGTownInstance::CASTLE) return booster * 750; if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
@ -153,18 +163,18 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
{ {
result += (c.data.type->getAIValue() * c.data.count) * c.chance; result += (c.data.type->getAIValue() * c.data.count) * c.chance;
} }
else /*else
{ {
//we will need to discard the weakest stack //we will need to discard the weakest stack
result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance; result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance;
} }*/
} }
result /= 100; //divide by total chance result /= 100; //divide by total chance
return result; return result;
} }
uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold) uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold)
{ {
auto dwelling = dynamic_cast<const CGDwelling *>(target); auto dwelling = dynamic_cast<const CGDwelling *>(target);
uint64_t score = 0; uint64_t score = 0;
@ -185,6 +195,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool
return score; return score;
} }
uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor)
{
auto dwelling = dynamic_cast<const CGDwelling *>(target);
uint64_t score = 0;
if(dwelling->getOwner() == myColor)
return 0;
for(auto & creLevel : dwelling->creatures)
{
if(creLevel.second.size())
{
auto creature = creLevel.second.back().toCreature();
score += creature->getAIValue() * creature->getGrowth();
}
}
return score;
}
int getDwellingArmyCost(const CGObjectInstance * target) int getDwellingArmyCost(const CGObjectInstance * target)
{ {
auto dwelling = dynamic_cast<const CGDwelling *>(target); auto dwelling = dynamic_cast<const CGDwelling *>(target);
@ -247,23 +278,13 @@ uint64_t RewardEvaluator::getArmyReward(
{ {
const float enemyArmyEliminationRewardRatio = 0.5f; const float enemyArmyEliminationRewardRatio = 0.5f;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID);
if(!target) if(!target)
return 0; return 0;
switch(target->ID) switch(target->ID)
{ {
case Obj::TOWN:
{
auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2;
if(fortLevel < CGTownInstance::CITADEL)
return town->hasFort() ? booster * 500 : 0;
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
}
case Obj::HILL_FORT: case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
case Obj::CREATURE_BANK: case Obj::CREATURE_BANK:
@ -272,7 +293,7 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingScore(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT: case Obj::CRYPT:
case Obj::SHIPWRECK: case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR: case Obj::SHIPWRECK_SURVIVOR:
@ -283,7 +304,7 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::DRAGON_UTOPIA: case Obj::DRAGON_UTOPIA:
return 10000; return 10000;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength() ? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
: 0; : 0;
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
@ -293,6 +314,47 @@ uint64_t RewardEvaluator::getArmyReward(
} }
} }
uint64_t RewardEvaluator::getArmyGrowth(
const CGObjectInstance * target,
const CGHeroInstance * hero,
const CCreatureSet * army) const
{
if(!target)
return 0;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
if(relations != PlayerRelations::ENEMIES)
return 0;
switch(target->ID)
{
case Obj::TOWN:
{
auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto neutral = !town->getOwner().isValidPlayer();
auto booster = isAnotherAi(town, *ai->cb) || neutral ? 1 : 2;
if(fortLevel < CGTownInstance::CITADEL)
return town->hasFort() ? booster * 500 : 0;
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
}
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4:
return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner());
case Obj::ARTIFACT:
// it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well.
return 0;
default:
return 0;
}
}
int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const
{ {
if(!target) if(!target)
@ -338,7 +400,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
2. The formula quickly approaches 1.0 as hero level increases, 2. The formula quickly approaches 1.0 as hero level increases,
but higher level always means higher value and the minimal value for level 1 hero is 0.5 but higher level always means higher value and the minimal value for level 1 hero is 0.5
*/ */
return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level)))); return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level))));
} }
float RewardEvaluator::getResourceRequirementStrength(int resType) const float RewardEvaluator::getResourceRequirementStrength(int resType) const
@ -366,10 +428,26 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const
return 0; return 0;
float ratio = dailyIncome[resType] == 0 float ratio = dailyIncome[resType] == 0
? (float)requiredResources[resType] / 50.0f ? (float)requiredResources[resType] / 10.0f
: (float)requiredResources[resType] / dailyIncome[resType] / 50.0f; : (float)requiredResources[resType] / dailyIncome[resType] / 20.0f;
return std::min(ratio, 1.0f); return std::min(ratio, 2.0f);
}
uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
{
uint64_t result = 0;
for(auto creatureInfo : town->creatures)
{
if(creatureInfo.second.empty())
continue;
auto creature = creatureInfo.second.back().toCreature();
result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
}
return result;
} }
float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
@ -407,18 +485,28 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::TOWN: case Obj::TOWN:
{ {
if(ai->buildAnalyzer->getDevelopmentInfo().empty()) if(ai->buildAnalyzer->getDevelopmentInfo().empty())
return 1; return 10.0f;
auto town = dynamic_cast<const CGTownInstance *>(target); auto town = dynamic_cast<const CGTownInstance *>(target);
auto fortLevel = town->fortLevel();
auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1;
if(town->hasCapitol()) return 1; 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 = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f;
if(town->hasCapitol())
return booster * 1.5;
if(fortLevel < CGTownInstance::CITADEL) if(fortLevel < CGTownInstance::CITADEL)
return booster * (town->hasFort() ? 0.6 : 0.4); return booster * (town->hasFort() ? 1.0 : 0.8);
else else
return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
} }
case Obj::HERO: case Obj::HERO:
@ -463,15 +551,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::GARDEN_OF_REVELATION: case Obj::GARDEN_OF_REVELATION:
case Obj::MARLETTO_TOWER: case Obj::MARLETTO_TOWER:
case Obj::MERCENARY_CAMP: case Obj::MERCENARY_CAMP:
case Obj::SHRINE_OF_MAGIC_GESTURE:
case Obj::SHRINE_OF_MAGIC_INCANTATION:
case Obj::TREE_OF_KNOWLEDGE: case Obj::TREE_OF_KNOWLEDGE:
return 1; return 1;
case Obj::LEARNING_STONE: case Obj::LEARNING_STONE:
return 1.0f / std::sqrt(hero->level); return 1.0f / std::sqrt(hero->level);
case Obj::ARENA: case Obj::ARENA:
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 2; return 2;
case Obj::SHRINE_OF_MAGIC_INCANTATION:
return 0.2f;
case Obj::SHRINE_OF_MAGIC_GESTURE:
return 0.3f;
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 0.5f;
case Obj::LIBRARY_OF_ENLIGHTENMENT: case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8; return 8;
case Obj::WITCH_HUT: case Obj::WITCH_HUT:
@ -513,12 +604,13 @@ int32_t getArmyCost(const CArmedInstance * army)
return value; return value;
} }
/// Gets aproximated reward in gold. Daily income is multiplied by 5
int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
{ {
if(!target) if(!target)
return 0; return 0;
auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
const int dailyIncomeMultiplier = 5; const int dailyIncomeMultiplier = 5;
const float enemyArmyEliminationGoldRewardRatio = 0.2f; const float enemyArmyEliminationGoldRewardRatio = 0.2f;
const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
@ -559,7 +651,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
//Objectively saves us 2500 to hire hero //Objectively saves us 2500 to hire hero
return GameConstants::HERO_GOLD_COST; return GameConstants::HERO_GOLD_COST;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target)) ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
: 0; : 0;
default: default:
@ -579,7 +671,8 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(); uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength(); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
} }
}; };
@ -596,7 +689,7 @@ public:
uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
evaluationContext.armyReward += upgradeValue; evaluationContext.armyReward += upgradeValue;
evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
} }
}; };
@ -621,23 +714,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
class DefendTownEvaluator : public IEvaluationContextBuilder class DefendTownEvaluator : public IEvaluationContextBuilder
{ {
private:
uint64_t townArmyIncome(const CGTownInstance * town) const
{
uint64_t result = 0;
for(auto creatureInfo : town->creatures)
{
if(creatureInfo.second.empty())
continue;
auto creature = creatureInfo.second.back().toCreature();
result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
}
return result;
}
public: public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{ {
@ -648,22 +724,34 @@ public:
const CGTownInstance * town = defendTown.town; const CGTownInstance * town = defendTown.town;
auto & treat = defendTown.getTreat(); auto & treat = defendTown.getTreat();
auto armyIncome = townArmyIncome(town); auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
strategicalValue = 1;
float multiplier = 1; float multiplier = 1;
if(treat.turn < defendTown.getTurn()) if(treat.turn < defendTown.getTurn())
multiplier /= 1 + (defendTown.getTurn() - treat.turn); multiplier /= 1 + (defendTown.getTurn() - treat.turn);
evaluationContext.armyReward += armyIncome * multiplier; multiplier /= 1.0f + treat.turn / 5.0f;
if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
{
auto ourSpeed = defendTown.hero->movementPointsLimit(true);
auto enemySpeed = treat.hero->movementPointsLimit(true);
if(enemySpeed > ourSpeed) multiplier *= 0.7f;
}
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town);
evaluationContext.armyGrowth += armyGrowth * multiplier;
evaluationContext.goldReward += dailyIncome * 5 * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier;
evaluationContext.strategicalValue += strategicalValue * multiplier;
if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue);
else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
} }
@ -709,18 +797,22 @@ public:
auto army = path.heroArmy; auto army = path.heroArmy;
const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) if(heroRole == HeroRole::MAIN)
evaluationContext.heroRole = heroRole;
if (target)
{ {
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
} }
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn()); vstd::amax(evaluationContext.turn, path.turn());
} }
@ -760,7 +852,7 @@ public:
evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost; evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost; evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -798,6 +890,31 @@ public:
} }
}; };
class DismissHeroContextBuilder : public IEvaluationContextBuilder
{
private:
const Nullkiller * ai;
public:
DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::DISMISS_HERO)
return;
Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
auto role = ai->heroManager->getHeroRole(dismissedHero);
auto mpLeft = dismissedHero->movementPointsRemaining();
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[role] += mpLeft;
evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
}
};
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{ {
public: public:
@ -813,39 +930,47 @@ public:
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
evaluationContext.closestWayRatio = 1;
if(bi.creatureID != CreatureID::NONE) if(bi.creatureID != CreatureID::NONE)
{ {
evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0; evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
if(bi.baseCreatureID == bi.creatureID) if(bi.baseCreatureID == bi.creatureID)
{ {
evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount; evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
evaluationContext.armyReward += bi.armyStrength; evaluationContext.armyReward += bi.armyStrength;
} }
else else
{ {
auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount; evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
} }
} }
else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
{ {
evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f; evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
} }
else else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
}
if(evaluationContext.goldReward)
{ {
auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount; evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
} }
if(bi.notEnoughRes && bi.prerequisitesCount == 1) if(bi.notEnoughRes && bi.prerequisitesCount == 1)
{ {
evaluationContext.strategicalValue /= 2; evaluationContext.strategicalValue /= 3;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5;
evaluationContext.turn += 5;
} }
} }
}; };
@ -872,6 +997,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>()); evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>()); evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>()); evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
} }
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
@ -909,6 +1035,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
+ (evaluationContext.armyReward > 0 ? 1 : 0) + (evaluationContext.armyReward > 0 ? 1 : 0)
+ (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.skillReward > 0 ? 1 : 0)
+ (evaluationContext.strategicalValue > 0 ? 1 : 0); + (evaluationContext.strategicalValue > 0 ? 1 : 0);
float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10);
double result = 0; double result = 0;
@ -918,8 +1046,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
heroRoleVariable->setValue(evaluationContext.heroRole); heroRoleVariable->setValue(evaluationContext.heroRole);
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(evaluationContext.goldReward); goldRewardVariable->setValue(goldRewardPerTurn);
armyRewardVariable->setValue(evaluationContext.armyReward); armyRewardVariable->setValue(evaluationContext.armyReward);
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
skillRewardVariable->setValue(evaluationContext.skillReward); skillRewardVariable->setValue(evaluationContext.skillReward);
dangerVariable->setValue(evaluationContext.danger); dangerVariable->setValue(evaluationContext.danger);
rewardTypeVariable->setValue(rewardType); rewardTypeVariable->setValue(rewardType);
@ -940,13 +1069,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
} }
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.goldReward, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.danger, evaluationContext.danger,

View File

@ -33,6 +33,7 @@ public:
RewardEvaluator(const Nullkiller * ai) : ai(ai) {} RewardEvaluator(const Nullkiller * ai) : ai(ai) {}
uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const; uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const;
uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const;
float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(int resType) const;
@ -43,6 +44,7 @@ public:
int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) const;
}; };
struct DLL_EXPORT EvaluationContext struct DLL_EXPORT EvaluationContext
@ -54,6 +56,7 @@ struct DLL_EXPORT EvaluationContext
float closestWayRatio; float closestWayRatio;
float armyLossPersentage; float armyLossPersentage;
float armyReward; float armyReward;
uint64_t armyGrowth;
int32_t goldReward; int32_t goldReward;
int32_t goldCost; int32_t goldCost;
float skillReward; float skillReward;
@ -64,6 +67,8 @@ struct DLL_EXPORT EvaluationContext
float enemyHeroDangerRatio; float enemyHeroDangerRatio;
EvaluationContext(const Nullkiller * ai); EvaluationContext(const Nullkiller * ai);
void addNonCriticalStrategicalValue(float value);
}; };
class IEvaluationContextBuilder class IEvaluationContextBuilder
@ -95,6 +100,7 @@ private:
fl::InputVariable * turnVariable; fl::InputVariable * turnVariable;
fl::InputVariable * goldRewardVariable; fl::InputVariable * goldRewardVariable;
fl::InputVariable * armyRewardVariable; fl::InputVariable * armyRewardVariable;
fl::InputVariable * armyGrowthVariable;
fl::InputVariable * dangerVariable; fl::InputVariable * dangerVariable;
fl::InputVariable * skillRewardVariable; fl::InputVariable * skillRewardVariable;
fl::InputVariable * strategicalValueVariable; fl::InputVariable * strategicalValueVariable;

View File

@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai)
throw cannotFulfillGoalException("No creatures to buy."); throw cannotFulfillGoalException("No creatures to buy.");
} }
if(town->visitingHero) if(town->visitingHero && !town->garrisonHero)
{ {
ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get()); ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
} }

View File

@ -31,9 +31,17 @@ std::string Composition::toString() const
{ {
std::string result = "Composition"; std::string result = "Composition";
for(auto goal : subtasks) for(auto step : subtasks)
{ {
result += " " + goal->toString(); result += "[";
for(auto goal : step)
{
if(goal->isElementar())
result += goal->toString() + " => ";
else
result += goal->toString() + ", ";
}
result += "] ";
} }
return result; return result;
@ -41,17 +49,34 @@ std::string Composition::toString() const
void Composition::accept(AIGateway * ai) void Composition::accept(AIGateway * ai)
{ {
taskptr(*subtasks.back())->accept(ai); for(auto task : subtasks.back())
{
if(task->isElementar())
{
taskptr(*task)->accept(ai);
}
else
{
break;
}
}
} }
TGoalVec Composition::decompose() const TGoalVec Composition::decompose() const
{ {
return subtasks; TGoalVec result;
for(const TGoalVec & step : subtasks)
vstd::concatenate(result, step);
return result;
} }
Composition & Composition::addNext(const AbstractGoal & goal) Composition & Composition::addNextSequence(const TGoalVec & taskSequence)
{ {
return addNext(sptr(goal)); subtasks.push_back(taskSequence);
return *this;
} }
Composition & Composition::addNext(TSubgoal goal) Composition & Composition::addNext(TSubgoal goal)
@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
} }
else else
{ {
subtasks.push_back(goal); subtasks.push_back({goal});
} }
return *this; return *this;
} }
Composition & Composition::addNext(const AbstractGoal & goal)
{
return addNext(sptr(goal));
}
bool Composition::isElementar() const bool Composition::isElementar() const
{ {
return subtasks.back()->isElementar(); return subtasks.back().front()->isElementar();
} }
int Composition::getHeroExchangeCount() const int Composition::getHeroExchangeCount() const
{ {
return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0; auto result = 0;
for(auto task : subtasks.back())
{
if(task->isElementar())
{
result += taskptr(*task)->getHeroExchangeCount();
}
}
return result;
} }
} }

View File

@ -18,7 +18,7 @@ namespace Goals
class DLL_EXPORT Composition : public ElementarGoal<Composition> class DLL_EXPORT Composition : public ElementarGoal<Composition>
{ {
private: private:
TGoalVec subtasks; std::vector<TGoalVec> subtasks; // things we want to do now
public: public:
Composition() Composition()
@ -26,16 +26,12 @@ namespace Goals
{ {
} }
Composition(TGoalVec subtasks)
: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
{
}
virtual bool operator==(const Composition & other) const override; virtual bool operator==(const Composition & other) const override;
virtual std::string toString() const override; virtual std::string toString() const override;
void accept(AIGateway * ai) override; void accept(AIGateway * ai) override;
Composition & addNext(const AbstractGoal & goal); Composition & addNext(const AbstractGoal & goal);
Composition & addNext(TSubgoal goal); Composition & addNext(TSubgoal goal);
Composition & addNextSequence(const TGoalVec & taskSequence);
virtual TGoalVec decompose() const override; virtual TGoalVec decompose() const override;
virtual bool isElementar() const override; virtual bool isElementar() const override;
virtual int getHeroExchangeCount() const override; virtual int getHeroExchangeCount() const override;

View File

@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid); ai->nullkiller->setTargetObject(objid);
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN)
{
auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner());
if(relations == PlayerRelations::ENEMIES)
{
ai->nullkiller->armyFormation->rearrangeArmyForSiege(
dynamic_cast<const CGTownInstance *>(targetObject),
chainPath.targetHero);
}
}
std::set<int> blockedIndexes; std::set<int> blockedIndexes;
for(int i = chainPath.nodes.size() - 1; i >= 0; i--) for(int i = chainPath.nodes.size() - 1; i >= 0; i--)

View File

@ -24,7 +24,10 @@ using namespace Goals;
std::string RecruitHero::toString() const std::string RecruitHero::toString() const
{ {
return "Recruit hero at " + town->getNameTranslated(); if(heroToBuy)
return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated();
else
return "Recruit hero at " + town->getNameTranslated();
} }
void RecruitHero::accept(AIGateway * ai) void RecruitHero::accept(AIGateway * ai)
@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai)
throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
} }
auto heroToHire = heroes[0]; auto heroToHire = heroToBuy;
for(auto hero : heroes) if(!heroToHire)
{ {
if(objid == hero->id.getNum()) for(auto hero : heroes)
{ {
heroToHire = hero; if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength())
break; heroToHire = hero;
} }
if(hero->getTotalStrength() > heroToHire->getTotalStrength())
heroToHire = hero;
} }
if(!heroToHire)
throw cannotFulfillGoalException("No hero to hire!");
if(t->visitingHero) if(t->visitingHero)
{ {
cb->swapGarrisonHero(t); cb->swapGarrisonHero(t);

View File

@ -22,18 +22,20 @@ namespace Goals
{ {
class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero> class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
{ {
private:
const CGHeroInstance * heroToBuy;
public: public:
RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy) RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
: RecruitHero(townWithTavern) : ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy)
{ {
objid = heroToBuy->id.getNum(); town = townWithTavern;
priority = 1;
} }
RecruitHero(const CGTownInstance * townWithTavern) RecruitHero(const CGTownInstance * townWithTavern)
: ElementarGoal(Goals::RECRUIT_HERO) : RecruitHero(townWithTavern, nullptr)
{ {
priority = 1;
town = townWithTavern;
} }
virtual bool operator==(const RecruitHero & other) const override virtual bool operator==(const RecruitHero & other) const override

View File

@ -0,0 +1,68 @@
/*
* ArmyFormation.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "ArmyFormation.h"
#include "../../../lib/mapObjects/CGTownInstance.h"
namespace NKAI
{
void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
{
auto freeSlots = attacker->getFreeSlotsQueue();
while(!freeSlots.empty())
{
auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
{
return slot.second->getCount() == 1
? std::numeric_limits<int>::max()
: slot.second->getCreatureID().toCreature()->getAIValue();
});
if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
{
break;
}
cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
freeSlots.pop();
}
if(town->fortLevel() > CGTownInstance::FORT)
{
std::vector<CStackInstance *> stacks;
for(auto slot : attacker->Slots())
stacks.push_back(slot.second);
boost::sort(
stacks,
[](CStackInstance * slot1, CStackInstance * slot2) -> bool
{
auto cre1 = slot1->getCreatureID().toCreature();
auto cre2 = slot2->getCreatureID().toCreature();
auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING);
if(flying != 0) return flying < 0;
else return cre1->getAIValue() < cre2->getAIValue();
});
for(int i = 0; i < stacks.size(); i++)
{
auto pos = vstd::findKey(attacker->Slots(), stacks[i]);
if(pos.getNum() != i)
cb->swapCreatures(attacker, attacker, static_cast<SlotID>(i), pos);
}
}
}
}

View File

@ -0,0 +1,38 @@
/*
* ArmyFormation.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../AIUtility.h"
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI
{
struct HeroPtr;
class AIGateway;
class FuzzyHelper;
class Nullkiller;
class DLL_EXPORT ArmyFormation
{
private:
std::shared_ptr<CCallback> cb; //this is enough, but we downcast from CCallback
public:
ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
};
}

View File

@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
sethero(upgradePath.targetHero); sethero(upgradePath.targetHero);
} }
ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)
: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
{
sethero(targetMain);
}
bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const
{ {
return false; return false;

View File

@ -27,6 +27,7 @@ namespace Goals
public: public:
ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
virtual bool operator==(const ArmyUpgrade & other) const override; virtual bool operator==(const ArmyUpgrade & other) const override;
virtual std::string toString() const override; virtual std::string toString() const override;

View File

@ -18,8 +18,8 @@ namespace NKAI
using namespace Goals; using namespace Goals;
DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath) DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack)
: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()) : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
{ {
settown(town); settown(town);
sethero(defencePath.targetHero); sethero(defencePath.targetHero);

View File

@ -24,9 +24,10 @@ namespace Goals
uint64_t defenceArmyStrength; uint64_t defenceArmyStrength;
HitMapInfo treat; HitMapInfo treat;
uint8_t turn; uint8_t turn;
bool counterattack;
public: public:
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
virtual bool operator==(const DefendTown & other) const override; virtual bool operator==(const DefendTown & other) const override;
@ -37,6 +38,8 @@ namespace Goals
uint64_t getDefenceStrength() const { return defenceArmyStrength; } uint64_t getDefenceStrength() const { return defenceArmyStrength; }
uint8_t getTurn() const { return turn; } uint8_t getTurn() const { return turn; }
bool isCounterAttack() { return counterattack; }
}; };
} }

View File

@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const
std::string HeroExchange::toString() const std::string HeroExchange::toString() const
{ {
return "Hero exchange " + exchangePath.toString(); return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
} }
uint64_t HeroExchange::getReinforcementArmyStrength() const uint64_t HeroExchange::getReinforcementArmyStrength() const

View File

@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
for(auto & hero : heroes) for(auto & hero : heroes)
{ {
// do not allow our own heroes in garrison to act on map // do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) if(hero.first->getOwner() == ai->playerID
&& hero.first->inTownGarrison
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
{
continue; continue;
}
uint64_t mask = FirstActorMask << actors.size(); uint64_t mask = FirstActorMask << actors.size();
auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai); auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);

View File

@ -24,8 +24,8 @@
namespace NKAI namespace NKAI
{ {
const int SCOUT_TURN_DISTANCE_LIMIT = 3; const int SCOUT_TURN_DISTANCE_LIMIT = 5;
const int MAIN_TURN_DISTANCE_LIMIT = 5; const int MAIN_TURN_DISTANCE_LIMIT = 10;
namespace AIPathfinding namespace AIPathfinding
{ {
@ -258,7 +258,7 @@ public:
{ {
double ratio = (double)danger / (armyValue * hero->getFightingStrength()); double ratio = (double)danger / (armyValue * hero->getFightingStrength());
return (uint64_t)(armyValue * ratio * ratio * ratio); return (uint64_t)(armyValue * ratio * ratio);
} }
STRONG_INLINE STRONG_INLINE

View File

@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit); storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit);
storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit); storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit);
logAi->trace(
"Scout turn distance: %s, main %s",
std::to_string(pathfinderSettings.scoutTurnDistanceLimit),
std::to_string(pathfinderSettings.mainTurnDistanceLimit));
if(pathfinderSettings.useHeroChain) if(pathfinderSettings.useHeroChain)
{ {
storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs); storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);

View File

@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base)
armyCost = base->armyCost; armyCost = base->armyCost;
actorAction = base->actorAction; actorAction = base->actorAction;
tiCache = base->tiCache; tiCache = base->tiCache;
actorExchangeCount = base->actorExchangeCount;
} }
void HeroActor::setupSpecialActors() void HeroActor::setupSpecialActors()

View File

@ -18,14 +18,21 @@ static std::shared_ptr<CBattleCallback> cbc;
CStupidAI::CStupidAI() CStupidAI::CStupidAI()
: side(-1) : side(-1)
, wasWaitingForRealize(false)
, wasUnlockingGs(false)
{ {
print("created"); print("created");
} }
CStupidAI::~CStupidAI() CStupidAI::~CStupidAI()
{ {
print("destroyed"); print("destroyed");
if(cb)
{
//Restore previous state of CB - it may be shared with the main AI (like VCAI)
cb->waitTillRealize = wasWaitingForRealize;
cb->unlockGsWhenWaiting = wasUnlockingGs;
}
} }
void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
@ -33,6 +40,11 @@ void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
print("init called, saving ptr to IBattleCallback"); print("init called, saving ptr to IBattleCallback");
env = ENV; env = ENV;
cbc = cb = CB; cbc = cb = CB;
wasWaitingForRealize = CB->waitTillRealize;
wasUnlockingGs = CB->unlockGsWhenWaiting;
CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false;
} }
void CStupidAI::actionFinished(const BattleAction &action) void CStupidAI::actionFinished(const BattleAction &action)
@ -90,7 +102,7 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl
void CStupidAI::yourTacticPhase(int distance) void CStupidAI::yourTacticPhase(int distance)
{ {
cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
} }
void CStupidAI::activeStack( const CStack * stack ) void CStupidAI::activeStack( const CStack * stack )
@ -230,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
print("battleStacksEffectsSet called"); print("battleStacksEffectsSet called");
} }
void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{ {
print("battleStart called"); print("battleStart called");
side = Side; side = Side;

View File

@ -20,6 +20,9 @@ class CStupidAI : public CBattleGameInterface
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
bool wasWaitingForRealize;
bool wasUnlockingGs;
void print(const std::string &text) const; void print(const std::string &text) const;
public: public:
CStupidAI(); CStupidAI();
@ -41,7 +44,7 @@ public:
void battleSpellCast(const BattleSpellCast *sc) override; void battleSpellCast(const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
private: private:

View File

@ -819,7 +819,7 @@ void VCAI::makeTurn()
for (auto h : cb->getHeroesInfo()) for (auto h : cb->getHeroesInfo())
{ {
if (h->movementPointsRemaining()) if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
} }
} }
catch (boost::thread_interrupted & e) catch (boost::thread_interrupted & e)
@ -1359,7 +1359,7 @@ void VCAI::wander(HeroPtr h)
TimeCheck tc("looking for wander destination"); TimeCheck tc("looking for wander destination");
while(h->movementPointsRemaining()) for(int k = 0; k < 10 && h->movementPointsRemaining(); k++)
{ {
validateVisitableObjs(); validateVisitableObjs();
ah->updatePaths(getMyHeroes()); ah->updatePaths(getMyHeroes());
@ -1575,14 +1575,14 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
} }
void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_BATTLE); status.setBattle(ONGOING_BATTLE);
const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
} }
void VCAI::battleEnd(const BattleResult * br, QueryID queryID) void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
@ -1593,12 +1593,16 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
bool won = br->winner == myCb->battleGetMySide(); bool won = br->winner == myCb->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
battlename.clear(); battlename.clear();
status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0; if (queryID != -1)
requestActionASAP([=]()
{ {
answerQuery(queryID, confirmAction); status.addQuery(queryID, "Combat result dialog");
}); const int confirmAction = 0;
requestActionASAP([=]()
{
answerQuery(queryID, confirmAction);
});
}
CAdventureAI::battleEnd(br, queryID); CAdventureAI::battleEnd(br, queryID);
} }

View File

@ -201,7 +201,7 @@ public:
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();

View File

@ -279,6 +279,10 @@ if(MINGW OR MSVC)
endif(MSVC) endif(MSVC)
if(MINGW) if(MINGW)
# Temporary (?) workaround for failing builds on MinGW CI due to bug in TBB
set(CMAKE_CXX_EXTENSIONS ON)
set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp bcrypt) set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp bcrypt)
# Check for iconv (may be needed for Boost.Locale) # Check for iconv (may be needed for Boost.Locale)

View File

@ -240,7 +240,7 @@
"default-release" "default-release"
], ],
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Release" "CMAKE_BUILD_TYPE": "RelWithDebInfo"
} }
} }
], ],

View File

@ -1,6 +1,13 @@
# 1.2.1 -> 1.3.0 # 1.3.0 -> 1.3.1
(unreleased) (unreleased)
* Fixed crash on starting game with outdated mods
* Fixed Android mod manager crash
* Fixed framerate drops on hero movement with active hota mod
* Reverted FPS limit on mobile systems back to 60 fps
# 1.2.1 -> 1.3.0
### GENERAL: ### GENERAL:
* Implemented automatic interface scaling to any resolution supported by monitor * Implemented automatic interface scaling to any resolution supported by monitor
* Implemented UI scaling option to scale game interface * Implemented UI scaling option to scale game interface
@ -17,6 +24,7 @@
* Added H3:SOD cheat codes as alternative to vcmi cheats * Added H3:SOD cheat codes as alternative to vcmi cheats
* Fixed several possible crashes caused by autocombat activation * Fixed several possible crashes caused by autocombat activation
* Fixed artifact lock icon in localized versions of the game * Fixed artifact lock icon in localized versions of the game
* Fixed possible crash on changing hardware cursor
### TOUCHSCREEN SUPPORT: ### TOUCHSCREEN SUPPORT:
* VCMI will now properly recognizes touch screen input * VCMI will now properly recognizes touch screen input
@ -47,6 +55,10 @@
### AI PLAYER: ### AI PLAYER:
* Fixed potential crash on accessing market (VCAI) * Fixed potential crash on accessing market (VCAI)
* Fixed potentially infinite turns (VCAI) * Fixed potentially infinite turns (VCAI)
* Reworked object prioritizing
* Improved town defense against enemy heroes
* Improved town building (mage guild and horde)
* Various behavior fixes
### GAME MECHANICS ### GAME MECHANICS
* Hero retreating after end of 7th turn will now correctly appear in tavern * Hero retreating after end of 7th turn will now correctly appear in tavern
@ -72,6 +84,7 @@
* Game will now play correct music track on scenario selection window * Game will now play correct music track on scenario selection window
* Dracon woll now correctly start without spellbook in Dragon Slayer campaign * Dracon woll now correctly start without spellbook in Dragon Slayer campaign
* Fixed frequent crash on moving to next scenario during campaign * Fixed frequent crash on moving to next scenario during campaign
* Fixed inability to dismiss heroes on maps with "capture town" victory condition
### RANDOM MAP GENERATOR: ### RANDOM MAP GENERATOR:
* Improved zone placement, shape and connections * Improved zone placement, shape and connections
@ -86,6 +99,7 @@
* Support for "wide" connections * Support for "wide" connections
* Support for new "fictive" and "repulsive" connections * Support for new "fictive" and "repulsive" connections
* RMG will now run faster, utilizing many CPU cores * RMG will now run faster, utilizing many CPU cores
* Removed random seed number from random map description
### INTERFACE: ### INTERFACE:
* Adventure map is now scalable and can be used with any resolution without mods * Adventure map is now scalable and can be used with any resolution without mods
@ -105,6 +119,8 @@
* Last symbol of entered cheat/chat message will no longer trigger hotkey * Last symbol of entered cheat/chat message will no longer trigger hotkey
* Right-clicking map name in scenario selection will now show file name * Right-clicking map name in scenario selection will now show file name
* Right-clicking save game in save/load screen will now show file name and creation date * Right-clicking save game in save/load screen will now show file name and creation date
* Right-clicking in town fort window will now show creature information popup
* Implemented pasting from clipboard (Ctrl+V) for text input
### BATTLES: ### BATTLES:
* Implemented Tower moat (Land Mines) * Implemented Tower moat (Land Mines)
@ -139,6 +155,7 @@
* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance * Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses * MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
* Configurable objects can now be translated * Configurable objects can now be translated
* Fixed loading of custom battlefield identifiers for map objects
# 1.2.0 -> 1.2.1 # 1.2.0 -> 1.2.1

View File

@ -253,26 +253,6 @@ using TLockGuardRec = std::lock_guard<std::recursive_mutex>;
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
void inline handleException()
{
try
{
throw;
}
catch(const std::exception & ex)
{
logGlobal->error(ex.what());
}
catch(const std::string & ex)
{
logGlobal->error(ex);
}
catch(...)
{
logGlobal->error("Sorry, caught unknown exception type. No more info available.");
}
}
namespace vstd namespace vstd
{ {
// combine hashes. Present in boost but not in std // combine hashes. Present in boost but not in std

View File

@ -145,7 +145,7 @@
"vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.hover" : "隐藏完成任务",
"vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务",
"vcmi.randomMapTab.widgets.defaultTemplate" : "默认", "vcmi.randomMapTab.widgets.randomTemplate" : "(随机)",
"vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.templateLabel" : "模板",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系",

View File

@ -186,7 +186,7 @@
"vcmi.questLog.hideComplete.hover" : "Hide complete quests", "vcmi.questLog.hideComplete.hover" : "Hide complete quests",
"vcmi.questLog.hideComplete.help" : "Hide all completed quests", "vcmi.questLog.hideComplete.help" : "Hide all completed quests",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(default)", "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)",
"vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.templateLabel" : "Template",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments",

View File

@ -168,7 +168,6 @@
"vcmi.questLog.hideComplete.hover" : "Masquer les quêtes terminées", "vcmi.questLog.hideComplete.hover" : "Masquer les quêtes terminées",
"vcmi.questLog.hideComplete.help" : "Masquer toutes les quêtes terminées", "vcmi.questLog.hideComplete.help" : "Masquer toutes les quêtes terminées",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(par défaut)",
"vcmi.randomMapTab.widgets.templateLabel" : "Modèle", "vcmi.randomMapTab.widgets.templateLabel" : "Modèle",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuration...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuration...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alignements d'équipe", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alignements d'équipe",

View File

@ -29,6 +29,13 @@
"vcmi.capitalColors.5" : "Violett", "vcmi.capitalColors.5" : "Violett",
"vcmi.capitalColors.6" : "Türkis", "vcmi.capitalColors.6" : "Türkis",
"vcmi.capitalColors.7" : "Rosa", "vcmi.capitalColors.7" : "Rosa",
"vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen",
"vcmi.radialWheel.showUnitInformation" : "Informationen zur Kreatur anzeigen",
"vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen",
"vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen",
"vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee",
"vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot",
"vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n", "vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n",
"vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n",
@ -38,6 +45,9 @@
"vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel",
"vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei",
"vcmi.mainMenu.playerName" : "Spieler", "vcmi.mainMenu.playerName" : "Spieler",
"vcmi.lobby.filename" : "Dateiname",
"vcmi.lobby.creationDate" : "Erstellungsdatum",
"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
"vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:", "vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:",
@ -87,6 +97,10 @@
"vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.",
"vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand",
"vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.",
"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement",
"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln",
"vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links",
"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen",
"vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.hover": "",
"vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "",
"vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@ -113,10 +127,12 @@
"vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.",
"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen",
"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.",
"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen",
"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.",
"vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen",
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.",
"vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen",
"vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen",
"vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).",
"vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).",
"vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).",
@ -170,7 +186,7 @@
"vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests",
"vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(Standard)", "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)",
"vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.templateLabel" : "Template",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen",

View File

@ -30,6 +30,13 @@
"vcmi.capitalColors.6" : "Jasnoniebieski", "vcmi.capitalColors.6" : "Jasnoniebieski",
"vcmi.capitalColors.7" : "Różowy", "vcmi.capitalColors.7" : "Różowy",
"vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia",
"vcmi.radialWheel.showUnitInformation" : "Pokaż informacje o stworzeniu",
"vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie",
"vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo",
"vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii",
"vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca",
"vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n", "vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n",
"vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n",
"vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverConnecting" : "Łączenie...",
@ -39,6 +46,9 @@
"vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP",
"vcmi.mainMenu.playerName" : "Gracz", "vcmi.mainMenu.playerName" : "Gracz",
"vcmi.lobby.filename" : "Nazwa pliku",
"vcmi.lobby.creationDate" : "Data utworzenia",
"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
"vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:", "vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:",
"vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?",
@ -74,6 +84,8 @@
"vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund",
"vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS", "vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS",
"vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
"vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia",
"vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym",
"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
@ -85,6 +97,10 @@
"vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.", "vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.",
"vcmi.adventureOptions.borderScroll.hover" : "Przewijanie na brzegu mapy", "vcmi.adventureOptions.borderScroll.hover" : "Przewijanie na brzegu mapy",
"vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.", "vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.",
"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym",
"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem",
"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
"vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.hover": "",
"vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "",
"vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@ -111,10 +127,12 @@
"vcmi.battleOptions.movementHighlightOnHover.help": "{Pokaż możliwości ruchu po najechaniu}\n\nPodświetla zasięg ruchu jednostki gdy najedziesz na nią myszą.", "vcmi.battleOptions.movementHighlightOnHover.help": "{Pokaż możliwości ruchu po najechaniu}\n\nPodświetla zasięg ruchu jednostki gdy najedziesz na nią myszą.",
"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Pokaż limit zasięgu dla strzelców", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Pokaż limit zasięgu dla strzelców",
"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Pokaż limit zasięgu dla strzelców po najechaniu}\n\nPokazuje limity zasięgu jednostki strzeleckiej gdy najedziesz na nią myszą.", "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Pokaż limit zasięgu dla strzelców po najechaniu}\n\nPokazuje limity zasięgu jednostki strzeleckiej gdy najedziesz na nią myszą.",
"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Pokaż trwale statystyki bohaterów",
"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Pokaż trwale statystyki bohaterów}\n\nWłącza trwałe okna statystyk bohaterów pokazujące umiejętności pierwszorzędne i punkty magii.",
"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe",
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.",
"vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo",
"vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo",
"vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).",
"vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).",
"vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).", "vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).",
@ -168,7 +186,7 @@
"vcmi.questLog.hideComplete.hover" : "Ukryj ukończone misje", "vcmi.questLog.hideComplete.hover" : "Ukryj ukończone misje",
"vcmi.questLog.hideComplete.help" : "Ukrywa wszystkie misje, które zostały zakończone", "vcmi.questLog.hideComplete.help" : "Ukrywa wszystkie misje, które zostały zakończone",
"vcmi.randomMapTab.widgets.defaultTemplate" : "domyślny", "vcmi.randomMapTab.widgets.randomTemplate" : "(Losowy)",
"vcmi.randomMapTab.widgets.templateLabel" : "Szablon", "vcmi.randomMapTab.widgets.templateLabel" : "Szablon",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze",

View File

@ -119,7 +119,7 @@
"vcmi.questLog.hideComplete.hover" : "Скрыть завершенное", "vcmi.questLog.hideComplete.hover" : "Скрыть завершенное",
"vcmi.questLog.hideComplete.help" : "Скрыть все завершенные квесты", "vcmi.questLog.hideComplete.help" : "Скрыть все завершенные квесты",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(по умолчанию)", "vcmi.randomMapTab.widgets.randomTemplate" : "(Случайный)",
"vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Настройка...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Настройка...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд",

View File

@ -142,7 +142,6 @@
"vcmi.questLog.hideComplete.hover" : "Ocultar misiones completas", "vcmi.questLog.hideComplete.hover" : "Ocultar misiones completas",
"vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones que ya han sido completadas", "vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones que ya han sido completadas",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(predeterminado)",
"vcmi.randomMapTab.widgets.templateLabel" : "Plantilla", "vcmi.randomMapTab.widgets.templateLabel" : "Plantilla",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configurar...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configurar...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alineaciones de equipos", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alineaciones de equipos",

View File

@ -85,7 +85,7 @@
"vcmi.systemOptions.framerateButton.hover" : "Лічильник кадрів", "vcmi.systemOptions.framerateButton.hover" : "Лічильник кадрів",
"vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна", "vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна",
"vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук",
"vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\Використовувати вібрацію при використанні сенсорного екрану", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану",
"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
@ -186,7 +186,7 @@
"vcmi.questLog.hideComplete.hover" : "Приховати завершені квести", "vcmi.questLog.hideComplete.hover" : "Приховати завершені квести",
"vcmi.questLog.hideComplete.help" : "Приховує всі квести, які вже мають стан виконаних", "vcmi.questLog.hideComplete.help" : "Приховує всі квести, які вже мають стан виконаних",
"vcmi.randomMapTab.widgets.defaultTemplate" : "(за замовчуванням)", "vcmi.randomMapTab.widgets.randomTemplate" : "(Випадковий)",
"vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон",
"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Налаштувати...", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Налаштувати...",
"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд",

View File

@ -78,7 +78,7 @@
] ]
}, },
"version" : "1.2", "version" : "1.3",
"author" : "VCMI Team", "author" : "VCMI Team",
"contact" : "http://forum.vcmi.eu/index.php", "contact" : "http://forum.vcmi.eu/index.php",
"modType" : "Graphical", "modType" : "Graphical",

View File

@ -1,7 +1,6 @@
[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi) [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.0)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
# VCMI Project # VCMI Project
VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities.

View File

@ -10,8 +10,8 @@ android {
applicationId "is.xyz.vcmi" applicationId "is.xyz.vcmi"
minSdk 19 minSdk 19
targetSdk 31 targetSdk 31
versionCode 1201 versionCode 1310
versionName "1.2.1" versionName "1.3.1"
setProperty("archivesBaseName", "vcmi") setProperty("archivesBaseName", "vcmi")
} }
@ -42,7 +42,7 @@ android {
applicationLabel: '@string/app_name', applicationLabel: '@string/app_name',
] ]
ndk { ndk {
debugSymbolLevel 'symbol_table' debugSymbolLevel 'full'
} }
} }
daily { daily {

View File

@ -182,9 +182,16 @@ public class ActivityMods extends ActivityWithToolbar
public void onSuccess(ServerResponse<List<VCMIMod>> response) public void onSuccess(ServerResponse<List<VCMIMod>> response)
{ {
Log.i(this, "Initialized mods repo"); Log.i(this, "Initialized mods repo");
mModContainer.updateFromRepo(response.mContent); if (mModContainer == null)
mModsAdapter.updateModsList(mModContainer.submods()); {
mProgress.setVisibility(View.GONE); handleNoData();
}
else
{
mModContainer.updateFromRepo(response.mContent);
mModsAdapter.updateModsList(mModContainer.submods());
mProgress.setVisibility(View.GONE);
}
} }
@Override @Override

View File

@ -146,7 +146,7 @@ public class NativeMethods
public static void hapticFeedback() public static void hapticFeedback()
{ {
final Context ctx = SDL.getContext(); final Context ctx = SDL.getContext();
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 29) {
((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)); ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
} else { } else {
((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(30); ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(30);

View File

@ -133,6 +133,12 @@ public class ExportDataController extends LauncherSettingController<Void, Void>
} }
} }
if (exported == null)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
try( try(
final OutputStream targetStream = owner.getContentResolver() final OutputStream targetStream = owner.getContentResolver()
.openOutputStream(exported.getUri()); .openOutputStream(exported.getUri());

View File

@ -30,6 +30,7 @@ public class LanguageSettingDialog extends LauncherSettingDialog<String>
AVAILABLE_LANGUAGES.add("swedish"); AVAILABLE_LANGUAGES.add("swedish");
AVAILABLE_LANGUAGES.add("turkish"); AVAILABLE_LANGUAGES.add("turkish");
AVAILABLE_LANGUAGES.add("ukrainian"); AVAILABLE_LANGUAGES.add("ukrainian");
AVAILABLE_LANGUAGES.add("vietnamese");
AVAILABLE_LANGUAGES.add("other_cp1250"); AVAILABLE_LANGUAGES.add("other_cp1250");
AVAILABLE_LANGUAGES.add("other_cp1251"); AVAILABLE_LANGUAGES.add("other_cp1251");
AVAILABLE_LANGUAGES.add("other_cp1252"); AVAILABLE_LANGUAGES.add("other_cp1252");

View File

@ -81,7 +81,7 @@ public class FileUtil
{ {
if (file == null) if (file == null)
{ {
Log.e("Broken path given to fileutil"); Log.e("Broken path given to fileutil::ensureWriteable");
return false; return false;
} }
@ -99,6 +99,12 @@ public class FileUtil
public static boolean clearDirectory(final File dir) public static boolean clearDirectory(final File dir)
{ {
if (dir == null || dir.listFiles() == null)
{
Log.e("Broken path given to fileutil::clearDirectory");
return false;
}
for (final File f : dir.listFiles()) for (final File f : dir.listFiles())
{ {
if (f.isDirectory() && !clearDirectory(f)) if (f.isDirectory() && !clearDirectory(f))

View File

@ -38,6 +38,7 @@
#include <vstd/StringUtils.h> #include <vstd/StringUtils.h>
#include <SDL_main.h> #include <SDL_main.h>
#include <SDL.h>
#ifdef VCMI_ANDROID #ifdef VCMI_ANDROID
#include "../lib/CAndroidVMHelper.h" #include "../lib/CAndroidVMHelper.h"
@ -255,24 +256,17 @@ int main(int argc, char * argv[])
logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); logGlobal->debug("settings = %s", settings.toJsonNode().toJson());
// Some basic data validation to produce better error messages in cases of incorrect install // Some basic data validation to produce better error messages in cases of incorrect install
auto testFile = [](std::string filename, std::string message) -> bool auto testFile = [](std::string filename, std::string message)
{ {
if (CResourceHandler::get()->existsResource(ResourceID(filename))) if (!CResourceHandler::get()->existsResource(ResourceID(filename)))
return true; handleFatalError(message, false);
logGlobal->error("Error: %s was not found!", message);
return false;
}; };
if (!testFile("DATA/HELP.TXT", "Heroes III data") || testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!");
!testFile("MODS/VCMI/MOD.JSON", "VCMI data")) testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
{ testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them.");
exit(1); // These are unrecoverable errors testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them.");
} testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!");
// these two are optional + some installs have them on CD and not in data directory
testFile("VIDEO/GOOD1A.SMK", "campaign movies");
testFile("SOUNDS/G1A.WAV", "campaign music"); //technically not a music but voiced intro sounds
srand ( (unsigned int)time(nullptr) ); srand ( (unsigned int)time(nullptr) );
@ -510,3 +504,18 @@ void handleQuit(bool ask)
quitApplication(); quitApplication();
} }
} }
void handleFatalError(const std::string & message, bool terminate)
{
logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE");
logGlobal->error("Reason: %s", message);
std::string messageToShow = "Fatal error! " + message;
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr);
if (terminate)
throw std::runtime_error(message);
else
exit(1);
}

View File

@ -21,3 +21,7 @@ extern SDL_Surface *screen2; // and hlp surface (used to store not-active in
extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
void handleQuit(bool ask = true); void handleQuit(bool ask = true);
/// Notify user about encoutered fatal error and terminate the game
/// TODO: decide on better location for this method
[[noreturn]] void handleFatalError(const std::string & message, bool terminate);

View File

@ -14,6 +14,7 @@
#include "CMusicHandler.h" #include "CMusicHandler.h"
#include "CGameInfo.h" #include "CGameInfo.h"
#include "renderSDL/SDLRWwrapper.h" #include "renderSDL/SDLRWwrapper.h"
#include "gui/CGuiHandler.h"
#include "../lib/JsonNode.h" #include "../lib/JsonNode.h"
#include "../lib/GameConstants.h" #include "../lib/GameConstants.h"
@ -185,9 +186,9 @@ int CSoundHandler::playSound(std::string sound, int repeats, bool cache)
Mix_FreeChunk(chunk); Mix_FreeChunk(chunk);
} }
else if (cache) else if (cache)
callbacks[channel]; initCallback(channel);
else else
callbacks[channel] = [chunk](){ Mix_FreeChunk(chunk);}; initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
} }
else else
channel = -1; channel = -1;
@ -237,28 +238,50 @@ void CSoundHandler::setChannelVolume(int channel, ui32 percent)
void CSoundHandler::setCallback(int channel, std::function<void()> function) void CSoundHandler::setCallback(int channel, std::function<void()> function)
{ {
std::map<int, std::function<void()> >::iterator iter; boost::unique_lock lockGuard(mutexCallbacks);
iter = callbacks.find(channel);
auto iter = callbacks.find(channel);
//channel not found. It may have finished so fire callback now //channel not found. It may have finished so fire callback now
if(iter == callbacks.end()) if(iter == callbacks.end())
function(); function();
else else
iter->second = function; iter->second.push_back(function);
} }
void CSoundHandler::soundFinishedCallback(int channel) void CSoundHandler::soundFinishedCallback(int channel)
{ {
std::map<int, std::function<void()> >::iterator iter; boost::unique_lock lockGuard(mutexCallbacks);
iter = callbacks.find(channel);
if (iter == callbacks.end()) if (callbacks.count(channel) == 0)
return; return;
auto callback = std::move(iter->second); // store callbacks from container locally - SDL might reuse this channel for another sound
callbacks.erase(iter); // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
auto callback = callbacks.at(channel);
callbacks.erase(channel);
if (callback) if (!callback.empty())
callback(); {
GH.dispatchMainThread([callback](){
for (auto entry : callback)
entry();
});
}
}
void CSoundHandler::initCallback(int channel)
{
boost::unique_lock lockGuard(mutexCallbacks);
assert(callbacks.count(channel) == 0);
callbacks[channel] = {};
}
void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
{
boost::unique_lock lockGuard(mutexCallbacks);
assert(callbacks.count(channel) == 0);
callbacks[channel].push_back(function);
} }
int CSoundHandler::ambientGetRange() const int CSoundHandler::ambientGetRange() const
@ -471,44 +494,32 @@ void CMusicHandler::setVolume(ui32 percent)
void CMusicHandler::musicFinishedCallback() void CMusicHandler::musicFinishedCallback()
{ {
// boost::mutex::scoped_lock guard(mutex); // call music restart in separate thread to avoid deadlock in some cases
// FIXME: WORKAROUND FOR A POTENTIAL DEADLOCK
// It is possible for: // It is possible for:
// 1) SDL thread to call this method on end of playback // 1) SDL thread to call this method on end of playback
// 2) VCMI code to call queueNext() method to queue new file // 2) VCMI code to call queueNext() method to queue new file
// this leads to: // this leads to:
// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
// Because of that (and lack of clear way to fix that)
// We will try to acquire lock here and if failed - do nothing
// This may break music playback till next song is enqued but won't deadlock the game
if (!mutex.try_lock()) GH.dispatchMainThread([this]()
{ {
logGlobal->error("Failed to acquire mutex! Unable to restart music!"); boost::unique_lock lockGuard(mutex);
return; if (current.get() != nullptr)
}
if (current.get() != nullptr)
{
// if music is looped, play it again
if (current->play())
{ {
mutex.unlock(); // if music is looped, play it again
return; if (current->play())
return;
else
current.reset();
} }
else
{
current.reset();
}
}
if (current.get() == nullptr && next.get() != nullptr) if (current.get() == nullptr && next.get() != nullptr)
{ {
current.reset(next.release()); current.reset(next.release());
current->play(); current->play();
} }
mutex.unlock(); });
} }
MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart): MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart):

View File

@ -45,9 +45,13 @@ private:
Mix_Chunk *GetSoundChunk(std::string &sound, bool cache); Mix_Chunk *GetSoundChunk(std::string &sound, bool cache);
//have entry for every currently active channel /// have entry for every currently active channel
//std::function will be nullptr if callback was not set /// vector will be empty if callback was not set
std::map<int, std::function<void()> > callbacks; std::map<int, std::vector<std::function<void()>> > callbacks;
/// Protects access to callbacks member to avoid data races:
/// SDL calls sound finished callbacks from audio thread
boost::mutex mutexCallbacks;
int ambientDistToVolume(int distance) const; int ambientDistToVolume(int distance) const;
void ambientStopSound(std::string soundId); void ambientStopSound(std::string soundId);
@ -58,6 +62,9 @@ private:
std::map<std::string, int> ambientChannels; std::map<std::string, int> ambientChannels;
std::map<int, int> channelVolumes; std::map<int, int> channelVolumes;
void initCallback(int channel, const std::function<void()> & function);
void initCallback(int channel);
public: public:
CSoundHandler(); CSoundHandler();

View File

@ -476,6 +476,9 @@ void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
adventureInt->onHeroChanged(hero); adventureInt->onHeroChanged(hero);
if (makingTurn && hero->tempOwner == playerID) if (makingTurn && hero->tempOwner == playerID)
adventureInt->onHeroChanged(hero); adventureInt->onHeroChanged(hero);
for (auto window : GH.windows().findWindows<BattleWindow>())
window->heroManaPointsChanged(hero);
} }
void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
{ {
@ -649,26 +652,20 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat
waitForAllDialogs(); waitForAllDialogs();
} }
void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
{ {
EVENT_HANDLER_CALLED_BY_CLIENT; EVENT_HANDLER_CALLED_BY_CLIENT;
bool autoBattleResultRefused = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2);
lastBattleArmies.first = army1; bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
lastBattleArmies.second = army2; bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
//quick combat with neutral creatures only
auto * army2_object = dynamic_cast<const CGObjectInstance *>(army2); if ((replayAllowed && useQuickCombat) || forceQuickCombat)
if((!autoBattleResultRefused && !allowBattleReplay && army2_object
&& (army2_object->getOwner() == PlayerColor::UNFLAGGABLE || army2_object->getOwner() == PlayerColor::NEUTRAL)
&& settings["adventure"]["quickCombat"].Bool())
|| settings["adventure"]["alwaysSkipCombat"].Bool())
{ {
autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
autofightingAI->initBattleInterface(env, cb); autofightingAI->initBattleInterface(env, cb);
autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side); autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false);
isAutoFightOn = true; isAutoFightOn = true;
cb->registerBattleInterface(autofightingAI); cb->registerBattleInterface(autofightingAI);
// Player shouldn't be able to move on adventure map if quick combat is going
allowBattleReplay = true;
} }
//Don't wait for dialogs when we are non-active hot-seat player //Don't wait for dialogs when we are non-active hot-seat player
@ -840,13 +837,17 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
if(!battleInt) if(!battleInt)
{ {
bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); bool allowManualReplay = queryID != -1;
allowBattleReplay = false;
auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay); auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
wnd->resultCallback = [=](ui32 selection)
if (allowManualReplay)
{ {
cb->selectionMade(selection, queryID); wnd->resultCallback = [=](ui32 selection)
}; {
cb->selectionMade(selection, queryID);
};
}
GH.windows().pushWindow(wnd); GH.windows().pushWindow(wnd);
// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
// Otherwise NewTurn causes freeze. // Otherwise NewTurn causes freeze.
@ -1904,8 +1905,9 @@ bool CPlayerInterface::capturedAllEvents()
} }
bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations();
bool quickCombatOngoing = isAutoFightOn && !battleInt;
if (ignoreEvents || needToLockAdventureMap || isAutoFightOn) if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing )
{ {
GH.input().ignoreEventsUntilInput(); GH.input().ignoreEventsUntilInput();
return true; return true;

View File

@ -65,8 +65,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
int firstCall; int firstCall;
int autosaveCount; int autosaveCount;
std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
bool allowBattleReplay = false;
std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
@ -169,7 +167,7 @@ protected: // Call-ins from server, should not be called directly, but only via
void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleUnitsChanged(const std::vector<UnitChanges> & units) override; void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override; void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack

View File

@ -88,6 +88,8 @@ template<typename T> class CApplyOnLobby : public CBaseForLobbyApply
public: public:
bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override
{ {
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
T * ptr = static_cast<T *>(pack); T * ptr = static_cast<T *>(pack);
ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler);
@ -572,7 +574,16 @@ void CServerHandler::sendRestartGame() const
void CServerHandler::sendStartGame(bool allowOnlyAI) const void CServerHandler::sendStartGame(bool allowOnlyAI) const
{ {
verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); try
{
verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
}
catch (const std::exception & e)
{
showServerError( std::string("Unable to start map! Reason: ") + e.what());
return;
}
LobbyStartGame lsg; LobbyStartGame lsg;
if(client) if(client)
{ {
@ -696,7 +707,7 @@ void CServerHandler::startCampaignScenario(std::shared_ptr<CampaignState> cs)
}); });
} }
void CServerHandler::showServerError(std::string txt) void CServerHandler::showServerError(std::string txt) const
{ {
CInfoWindow::showInfoDialog(txt, {}); CInfoWindow::showInfoDialog(txt, {});
} }
@ -865,8 +876,11 @@ void CServerHandler::threadHandleConnection()
} }
else else
{ {
logNetwork->error("Lost connection to server, ending listening thread!"); if (e.code() == boost::asio::error::eof)
logNetwork->error(e.what()); logNetwork->error("Lost connection to server, ending listening thread! Connection has been closed");
else
logNetwork->error("Lost connection to server, ending listening thread! Reason: %s", e.what());
if(client) if(client)
{ {
state = EClientState::DISCONNECTING; state = EClientState::DISCONNECTING;

View File

@ -151,7 +151,7 @@ public:
void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
void endGameplay(bool closeConnection = true, bool restart = false); void endGameplay(bool closeConnection = true, bool restart = false);
void startCampaignScenario(std::shared_ptr<CampaignState> cs = {}); void startCampaignScenario(std::shared_ptr<CampaignState> cs = {});
void showServerError(std::string txt); void showServerError(std::string txt) const;
// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle
int howManyPlayerInterfaces(); int howManyPlayerInterfaces();

View File

@ -26,6 +26,7 @@
#include "../lib/gameState/CGameState.h" #include "../lib/gameState/CGameState.h"
#include "../lib/CThreadHelper.h" #include "../lib/CThreadHelper.h"
#include "../lib/VCMIDirs.h" #include "../lib/VCMIDirs.h"
#include "../lib/UnlockGuard.h"
#include "../lib/battle/BattleInfo.h" #include "../lib/battle/BattleInfo.h"
#include "../lib/serializer/BinaryDeserializer.h" #include "../lib/serializer/BinaryDeserializer.h"
#include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMapService.h"
@ -218,7 +219,7 @@ void CClient::loadGame(CGameState * initializedGameState)
// try to deserialize client data including sleepingHeroes // try to deserialize client data including sleepingHeroes
try try
{ {
boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME));
if(clientSaveName.empty()) if(clientSaveName.empty())
throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); throw std::runtime_error("Cannot open client part of " + CSH->si->mapname);
@ -579,7 +580,7 @@ void CClient::battleStarted(const BattleInfo * info)
auto callBattleStart = [&](PlayerColor color, ui8 side) auto callBattleStart = [&](PlayerColor color, ui8 side)
{ {
if(vstd::contains(battleints, color)) if(vstd::contains(battleints, color))
battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
}; };
callBattleStart(leftSide.color, 0); callBattleStart(leftSide.color, 0);
@ -624,6 +625,14 @@ void CClient::battleStarted(const BattleInfo * info)
CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
} }
} }
if(info->tacticDistance)
{
auto tacticianColor = info->sides[info->tacticsSide].color;
if (vstd::contains(battleints, tacticianColor))
battleints[tacticianColor]->yourTacticPhase(info->tacticDistance);
}
} }
void CClient::battleFinished() void CClient::battleFinished()
@ -645,6 +654,9 @@ void CClient::startPlayerBattleAction(PlayerColor color)
if(vstd::contains(battleints, color)) if(vstd::contains(battleints, color))
{ {
// we want to avoid locking gamestate and causing UI to freeze while AI is making turn
auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human);
assert(vstd::contains(battleints, color)); assert(vstd::contains(battleints, color));
battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false));
} }

View File

@ -95,6 +95,14 @@ void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
template<typename T, typename ... Args, typename ... Args2> template<typename T, typename ... Args, typename ... Args2>
void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
{ {
assert(cl.gameState()->curB);
if (!cl.gameState()->curB)
{
logGlobal->error("Attempt to call battle interface without ongoing battle!");
return;
}
callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward<Args2>(args)...); callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward<Args2>(args)...);
callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].color, ptr, std::forward<Args2>(args)...); callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].color, ptr, std::forward<Args2>(args)...);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)

View File

@ -192,7 +192,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
Point scrollDelta = scrollDirection * scrollDistance; Point scrollDelta = scrollDirection * scrollDistance;
bool cursorInScrollArea = scrollDelta != Point(0,0); bool cursorInScrollArea = scrollDelta != Point(0,0);
bool scrollingActive = cursorInScrollArea && isActive() && shortcuts->optionSidePanelActive() && !scrollingWasBlocked; bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked;
bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool();
if (!scrollingWasActive && scrollingBlocked) if (!scrollingWasActive && scrollingBlocked)
@ -323,7 +323,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma
mapAudio->onEnemyTurnStarted(); mapAudio->onEnemyTurnStarted();
widget->getMinimap()->setAIRadar(!isHuman); widget->getMinimap()->setAIRadar(!isHuman);
widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer()); widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer());
setState(EAdventureState::ENEMY_TURN); setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN);
} }
void AdventureMapInterface::setState(EAdventureState state) void AdventureMapInterface::setState(EAdventureState state)
@ -417,14 +417,14 @@ void AdventureMapInterface::hotkeyEndingTurn()
if(settings["session"]["spectate"].Bool()) if(settings["session"]["spectate"].Bool())
return; return;
LOCPLINT->makingTurn = false;
LOCPLINT->cb->endTurn();
if(!settings["general"]["startTurnAutosave"].Bool()) if(!settings["general"]["startTurnAutosave"].Bool())
{ {
LOCPLINT->performAutosave(); LOCPLINT->performAutosave();
} }
LOCPLINT->makingTurn = false;
LOCPLINT->cb->endTurn();
mapAudio->onPlayerTurnEnded(); mapAudio->onPlayerTurnEnded();
} }
@ -822,6 +822,15 @@ void AdventureMapInterface::hotkeyZoom(int delta)
void AdventureMapInterface::onScreenResize() void AdventureMapInterface::onScreenResize()
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
// remember our activation state and reactive after reconstruction
// since othervice activate() calls for created elements will bypass virtual dispatch
// and will call directly CIntObject::activate() instead of dispatching virtual function call
bool widgetActive = isActive();
if (widgetActive)
deactivate();
widget.reset(); widget.reset();
pos.x = pos.y = 0; pos.x = pos.y = 0;
pos.w = GH.screenDimensions().x; pos.w = GH.screenDimensions().x;
@ -838,4 +847,7 @@ void AdventureMapInterface::onScreenResize()
widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy());
adjustActiveness(); adjustActiveness();
if (widgetActive)
activate();
} }

View File

@ -461,7 +461,13 @@ bool AdventureMapShortcuts::optionSidePanelActive()
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW; return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
} }
bool AdventureMapShortcuts::optionMapScrollingActive()
{
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN);
}
bool AdventureMapShortcuts::optionMapViewActive() bool AdventureMapShortcuts::optionMapViewActive()
{ {
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL; return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL
|| (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN);
} }

View File

@ -81,6 +81,7 @@ public:
bool optionInMapView(); bool optionInMapView();
bool optionInWorldView(); bool optionInWorldView();
bool optionSidePanelActive(); bool optionSidePanelActive();
bool optionMapScrollingActive();
bool optionMapViewActive(); bool optionMapViewActive();
void setState(EAdventureState newState); void setState(EAdventureState newState);

View File

@ -14,7 +14,8 @@ enum class EAdventureState
NOT_INITIALIZED, NOT_INITIALIZED,
HOTSEAT_WAIT, HOTSEAT_WAIT,
MAKING_TURN, MAKING_TURN,
ENEMY_TURN, AI_PLAYER_TURN,
OTHER_HUMAN_PLAYER_TURN,
CASTING_SPELL, CASTING_SPELL,
WORLD_VIEW WORLD_VIEW
}; };

View File

@ -445,6 +445,8 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero));
}); });
} }
// animations will be executed by spell effects
} }
void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
@ -727,7 +729,7 @@ void BattleInterface::requestAutofightingAIToTakeAction()
// FIXME: unsafe // FIXME: unsafe
// Run task in separate thread to avoid UI lock while AI is making turn (which might take some time) // Run task in separate thread to avoid UI lock while AI is making turn (which might take some time)
// HOWEVER this thread won't atttempt to lock game state, potentially leading to races // HOWEVER this thread won't atttempt to lock game state, potentially leading to races
boost::thread aiThread([&]() boost::thread aiThread([this, activeStack]()
{ {
curInt->autofightingAI->activeStack(activeStack); curInt->autofightingAI->activeStack(activeStack);
}); });

View File

@ -389,6 +389,12 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit
background->colorize(hero.owner); background->colorize(hero.owner);
} }
initializeData(hero);
}
void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero)
{
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
auto attack = hero.details->primskills[0]; auto attack = hero.details->primskills[0];
auto defense = hero.details->primskills[1]; auto defense = hero.details->primskills[1];
auto power = hero.details->primskills[2]; auto power = hero.details->primskills[2];
@ -423,6 +429,14 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit
labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
} }
void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo)
{
icons.clear();
labels.clear();
initializeData(updatedInfo);
}
void HeroInfoBasicPanel::show(Canvas & to) void HeroInfoBasicPanel::show(Canvas & to)
{ {
showAll(to); showAll(to);
@ -622,7 +636,9 @@ void BattleResultWindow::show(Canvas & to)
void BattleResultWindow::buttonPressed(int button) void BattleResultWindow::buttonPressed(int button)
{ {
resultCallback(button); if (resultCallback)
resultCallback(button);
CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon
close(); close();

View File

@ -137,6 +137,9 @@ public:
HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true);
void show(Canvas & to) override; void show(Canvas & to) override;
void initializeData(const InfoAboutHero & hero);
void update(const InfoAboutHero & updatedInfo);
}; };
class HeroInfoWindow : public CWindowObject class HeroInfoWindow : public CWindowObject

View File

@ -127,13 +127,26 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
{ {
//Blit absolute obstacles //Blit absolute obstacles
for(auto & oi : owner.curInt->cb->battleGetAllObstacles()) for(auto & obstacle : owner.curInt->cb->battleGetAllObstacles())
{ {
if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
{ {
auto img = getObstacleImage(*oi); auto img = getObstacleImage(*obstacle);
if(img) if(img)
canvas.draw(img, Point(oi->getInfo().width, oi->getInfo().height)); canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height));
}
if (obstacle->obstacleType == CObstacleInstance::USUAL)
{
if (obstacle->getInfo().isForegroundObstacle)
continue;
auto img = getObstacleImage(*obstacle);
if(img)
{
Point p = getObstaclePosition(img, *obstacle);
canvas.draw(img, p);
}
} }
} }
} }
@ -148,11 +161,10 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere
if (obstacle->obstacleType == CObstacleInstance::MOAT) if (obstacle->obstacleType == CObstacleInstance::MOAT)
continue; continue;
bool isForeground = obstacle->obstacleType == CObstacleInstance::USUAL && obstacle->getInfo().isForegroundObstacle; if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle)
continue;
auto layer = isForeground ? EBattleFieldLayer::OBSTACLES_FG : EBattleFieldLayer::OBSTACLES_BG; renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
renderer.insert(layer, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
auto img = getObstacleImage(*obstacle); auto img = getObstacleImage(*obstacle);
if(img) if(img)
{ {

View File

@ -16,12 +16,11 @@ class BattleInterface;
enum class EBattleFieldLayer { enum class EBattleFieldLayer {
// confirmed ordering requirements: // confirmed ordering requirements:
OBSTACLES_BG = 0,
CORPSES = 0, CORPSES = 0,
WALLS = 1, WALLS = 1,
HEROES = 2, HEROES = 2,
STACKS = 2, // after corpses, obstacles, walls STACKS = 2, // after corpses, obstacles, walls
OBSTACLES_FG = 3, // after stacks OBSTACLES = 3, // after stacks
STACK_AMOUNTS = 3, // after stacks, obstacles, corpses STACK_AMOUNTS = 3, // after stacks, obstacles, corpses
EFFECTS = 4, // after obstacles, battlements EFFECTS = 4, // after obstacles, battlements
}; };

View File

@ -307,15 +307,13 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
}); });
renderer.insert( EBattleFieldLayer::OBSTACLES_FG, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
showWallPiece(canvas, wallPiece); showWallPiece(canvas, wallPiece);
}); });
} }
renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
showWallPiece(canvas, wallPiece); showWallPiece(canvas, wallPiece);
}); });
} }
} }
@ -330,8 +328,6 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
{ {
owner.checkForAnimations();
if (ca.attacker != -1) if (ca.attacker != -1)
{ {
const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker); const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker);

View File

@ -154,11 +154,6 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
void BattleStacksController::stackReset(const CStack * stack) void BattleStacksController::stackReset(const CStack * stack)
{ {
owner.checkForAnimations();
//reset orientation?
//stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER;
auto iter = stackAnimation.find(stack->unitId()); auto iter = stackAnimation.find(stack->unitId());
if(iter == stackAnimation.end()) if(iter == stackAnimation.end())
@ -240,6 +235,9 @@ void BattleStacksController::setActiveStack(const CStack *stack)
stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
owner.windowObject->blockUI(activeStack == nullptr); owner.windowObject->blockUI(activeStack == nullptr);
if (activeStack)
stackAmountBoxHidden.clear();
} }
bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
@ -501,7 +499,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance) void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
{ {
assert(destHex.size() > 0); assert(destHex.size() > 0);
owner.checkForAnimations(); //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed
owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) );

View File

@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
const JsonNode config(ResourceID("config/widgets/BattleWindow.json")); const JsonNode config(ResourceID("config/widgets/BattleWindow2.json"));
addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
@ -237,8 +237,8 @@ void BattleWindow::showStickyHeroWindows()
if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true)
return; return;
Settings showStickyHeroInfoWIndows = settings.write["battle"]["stickyHeroInfoWindows"]; Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"];
showStickyHeroInfoWIndows->Bool() = true; showStickyHeroInfoWindows->Bool() = true;
createStickyHeroInfoWindows(); createStickyHeroInfoWindows();
@ -250,6 +250,27 @@ void BattleWindow::updateQueue()
queue->update(); queue->update();
} }
void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero)
{
std::shared_ptr<HeroInfoBasicPanel> panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow;
panelToUpdate->update(hero);
}
void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
{
if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance)
{
InfoAboutHero heroInfo = InfoAboutHero();
heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE);
updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo);
}
else
{
logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window");
}
}
void BattleWindow::activate() void BattleWindow::activate()
{ {
GH.setStatusbar(console); GH.setStatusbar(console);
@ -480,7 +501,7 @@ void BattleWindow::bAutofightf()
auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
ai->initBattleInterface(owner.curInt->env, owner.curInt->cb); ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide()); ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false);
owner.curInt->autofightingAI = ai; owner.curInt->autofightingAI = ai;
owner.curInt->cb->registerBattleInterface(ai); owner.curInt->cb->registerBattleInterface(ai);

View File

@ -79,15 +79,22 @@ public:
void hideQueue(); void hideQueue();
void showQueue(); void showQueue();
/// Toggle permanent hero info windows visibility (HD mod feature)
void hideStickyHeroWindows(); void hideStickyHeroWindows();
void showStickyHeroWindows(); void showStickyHeroWindows();
/// Event handler for netpack changing hero mana points
void heroManaPointsChanged(const CGHeroInstance * hero);
/// block all UI elements when player is not allowed to act, e.g. during enemy turn /// block all UI elements when player is not allowed to act, e.g. during enemy turn
void blockUI(bool on); void blockUI(bool on);
/// Refresh queue after turn order changes /// Refresh queue after turn order changes
void updateQueue(); void updateQueue();
/// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side
void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero);
/// Get mouse-hovered battle queue unit ID if any found /// Get mouse-hovered battle queue unit ID if any found
std::optional<uint32_t> getQueueHoveredUnitId(); std::optional<uint32_t> getQueueHoveredUnitId();

View File

@ -70,13 +70,22 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current)
} }
} }
void InputHandler::processEvents() std::vector<SDL_Event> InputHandler::acquireEvents()
{ {
boost::unique_lock<boost::mutex> lock(eventsMutex); boost::unique_lock<boost::mutex> lock(eventsMutex);
for(const auto & currentEvent : eventsQueue)
std::vector<SDL_Event> result;
std::swap(result, eventsQueue);
return result;
}
void InputHandler::processEvents()
{
std::vector<SDL_Event> eventsToProcess = acquireEvents();
for(const auto & currentEvent : eventsToProcess)
handleCurrentEvent(currentEvent); handleCurrentEvent(currentEvent);
eventsQueue.clear();
fingerHandler->handleUpdate(); fingerHandler->handleUpdate();
} }
@ -239,6 +248,11 @@ void InputHandler::stopTextInput()
textHandler->stopTextInput(); textHandler->stopTextInput();
} }
void InputHandler::hapticFeedback()
{
fingerHandler->hapticFeedback();
}
bool InputHandler::hasTouchInputDevice() const bool InputHandler::hasTouchInputDevice() const
{ {
return fingerHandler->hasTouchInputDevice(); return fingerHandler->hasTouchInputDevice();

View File

@ -29,6 +29,8 @@ class InputHandler
Point cursorPosition; Point cursorPosition;
std::vector<SDL_Event> acquireEvents();
void preprocessEvent(const SDL_Event & event); void preprocessEvent(const SDL_Event & event);
void handleCurrentEvent(const SDL_Event & current); void handleCurrentEvent(const SDL_Event & current);
void handleUserEvent(const SDL_UserEvent & current); void handleUserEvent(const SDL_UserEvent & current);
@ -63,6 +65,9 @@ public:
/// Ends any existing text input state /// Ends any existing text input state
void stopTextInput(); void stopTextInput();
/// do a haptic feedback
void hapticFeedback();
/// returns true if system has active touchscreen /// returns true if system has active touchscreen
bool hasTouchInputDevice() const; bool hasTouchInputDevice() const;

View File

@ -17,6 +17,7 @@
#include "../gui/EventDispatcher.h" #include "../gui/EventDispatcher.h"
#include "../gui/ShortcutHandler.h" #include "../gui/ShortcutHandler.h"
#include <SDL_clipboard.h>
#include <SDL_events.h> #include <SDL_events.h>
#include <SDL_hints.h> #include <SDL_hints.h>
@ -30,13 +31,24 @@ InputSourceKeyboard::InputSourceKeyboard()
void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
{ {
if(key.repeat != 0)
return; // ignore periodic event resends
if (SDL_IsTextInputActive() == SDL_TRUE) if (SDL_IsTextInputActive() == SDL_TRUE)
{ {
if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown())
{
char * clipboardBuffer = SDL_GetClipboardText();
std::string clipboardContent = clipboardBuffer;
boost::erase_all(clipboardContent, "\r");
boost::erase_all(clipboardContent, "\n");
GH.events().dispatchTextInput(clipboardContent);
SDL_free(clipboardBuffer);
return;
}
if (key.keysym.sym >= ' ' && key.keysym.sym < 0x80) if (key.keysym.sym >= ' ' && key.keysym.sym < 0x80)
return; // printable character - will be handled as text input return; // printable character - will be handled as text input
} else {
if(key.repeat != 0)
return; // ignore periodic event resends
} }
assert(key.state == SDL_PRESSED); assert(key.state == SDL_PRESSED);

View File

@ -81,7 +81,10 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
{ {
Point distance = convertTouchToMouse(tfinger) - lastTapPosition; Point distance = convertTouchToMouse(tfinger) - lastTapPosition;
if ( std::abs(distance.x) > params.panningSensitivityThreshold || std::abs(distance.y) > params.panningSensitivityThreshold) if ( std::abs(distance.x) > params.panningSensitivityThreshold || std::abs(distance.y) > params.panningSensitivityThreshold)
{
state = TouchState::TAP_DOWN_PANNING; state = TouchState::TAP_DOWN_PANNING;
GH.events().dispatchGesturePanningStarted(lastTapPosition);
}
break; break;
} }
case TouchState::TAP_DOWN_PANNING: case TouchState::TAP_DOWN_PANNING:
@ -128,11 +131,16 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge
{ {
lastTapPosition = convertTouchToMouse(tfinger); lastTapPosition = convertTouchToMouse(tfinger);
GH.input().setCursorPosition(lastTapPosition); GH.input().setCursorPosition(lastTapPosition);
GH.events().dispatchGesturePanningStarted(lastTapPosition);
state = TouchState::TAP_DOWN_SHORT; state = TouchState::TAP_DOWN_SHORT;
break; break;
} }
case TouchState::TAP_DOWN_SHORT: case TouchState::TAP_DOWN_SHORT:
{
GH.input().setCursorPosition(convertTouchToMouse(tfinger));
GH.events().dispatchGesturePanningStarted(lastTapPosition);
state = TouchState::TAP_DOWN_DOUBLE;
break;
}
case TouchState::TAP_DOWN_PANNING: case TouchState::TAP_DOWN_PANNING:
{ {
GH.input().setCursorPosition(convertTouchToMouse(tfinger)); GH.input().setCursorPosition(convertTouchToMouse(tfinger));

View File

@ -96,8 +96,6 @@ class InputSourceTouch
void emitPanningEvent(const SDL_TouchFingerEvent & tfinger); void emitPanningEvent(const SDL_TouchFingerEvent & tfinger);
void emitPinchEvent(const SDL_TouchFingerEvent & tfinger); void emitPinchEvent(const SDL_TouchFingerEvent & tfinger);
void hapticFeedback();
public: public:
InputSourceTouch(); InputSourceTouch();
@ -106,6 +104,8 @@ public:
void handleEventFingerDown(const SDL_TouchFingerEvent & current); void handleEventFingerDown(const SDL_TouchFingerEvent & current);
void handleEventFingerUp(const SDL_TouchFingerEvent & current); void handleEventFingerUp(const SDL_TouchFingerEvent & current);
void hapticFeedback();
void handleUpdate(); void handleUpdate();
bool hasTouchInputDevice() const; bool hasTouchInputDevice() const;

View File

@ -88,6 +88,8 @@ void CGuiHandler::handleEvents()
void CGuiHandler::fakeMouseMove() void CGuiHandler::fakeMouseMove()
{ {
dispatchMainThread([](){ dispatchMainThread([](){
assert(CPlayerInterface::pim);
boost::unique_lock lock(*CPlayerInterface::pim);
GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition());
}); });
} }

View File

@ -31,8 +31,6 @@ namespace Cursor
}; };
enum class Combat { enum class Combat {
INVALID = -1,
BLOCKED = 0, BLOCKED = 0,
MOVE = 1, MOVE = 1,
FLY = 2, FLY = 2,
@ -157,12 +155,16 @@ public:
template<typename Index> template<typename Index>
Index get() Index get()
{ {
assert((std::is_same<Index, Cursor::Default>::value )|| type != Cursor::Type::DEFAULT ); bool typeValid = true;
assert((std::is_same<Index, Cursor::Map>::value )|| type != Cursor::Type::ADVENTURE );
assert((std::is_same<Index, Cursor::Combat>::value )|| type != Cursor::Type::COMBAT );
assert((std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK );
return static_cast<Index>(frame); typeValid &= (std::is_same<Index, Cursor::Default>::value )|| type != Cursor::Type::DEFAULT;
typeValid &= (std::is_same<Index, Cursor::Map>::value )|| type != Cursor::Type::ADVENTURE;
typeValid &= (std::is_same<Index, Cursor::Combat>::value )|| type != Cursor::Type::COMBAT;
typeValid &= (std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK;
if (typeValid)
return static_cast<Index>(frame);
return Index::POINTER;
} }
Point getPivotOffsetDefault(size_t index); Point getPivotOffsetDefault(size_t index);

View File

@ -76,7 +76,7 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
{SDLK_r, EShortcut::GAME_RESTART_GAME }, {SDLK_r, EShortcut::GAME_RESTART_GAME },
{SDLK_m, EShortcut::GAME_TO_MAIN_MENU }, {SDLK_m, EShortcut::GAME_TO_MAIN_MENU },
{SDLK_q, EShortcut::GAME_QUIT_GAME }, {SDLK_q, EShortcut::GAME_QUIT_GAME },
{SDLK_t, EShortcut::GAME_OPEN_MARKETPLACE }, {SDLK_b, EShortcut::GAME_OPEN_MARKETPLACE },
{SDLK_g, EShortcut::GAME_OPEN_THIEVES_GUILD }, {SDLK_g, EShortcut::GAME_OPEN_THIEVES_GUILD },
{SDLK_TAB, EShortcut::GAME_ACTIVATE_CONSOLE }, {SDLK_TAB, EShortcut::GAME_ACTIVATE_CONSOLE },
{SDLK_o, EShortcut::ADVENTURE_GAME_OPTIONS }, {SDLK_o, EShortcut::ADVENTURE_GAME_OPTIONS },

View File

@ -147,7 +147,6 @@ void RandomMapTab::updateMapInfoByHost()
mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels();
// Generate player information // Generate player information
mapInfo->mapHeader->players.clear();
int playersToGen = PlayerColor::PLAYER_LIMIT_I; int playersToGen = PlayerColor::PLAYER_LIMIT_I;
if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE)
{ {
@ -157,7 +156,6 @@ void RandomMapTab::updateMapInfoByHost()
playersToGen = mapGenOptions->getPlayerCount(); playersToGen = mapGenOptions->getPlayerCount();
} }
mapInfo->mapHeader->howManyTeams = playersToGen; mapInfo->mapHeader->howManyTeams = playersToGen;
//FIXME: Assign all human-controlled colors in first place //FIXME: Assign all human-controlled colors in first place
@ -165,6 +163,12 @@ void RandomMapTab::updateMapInfoByHost()
//TODO: Get human player count //TODO: Get human player count
std::set<TeamID> occupiedTeams; std::set<TeamID> occupiedTeams;
for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
{
mapInfo->mapHeader->players[i].canComputerPlay = false;
mapInfo->mapHeader->players[i].canHumanPlay = false;
}
for(int i = 0; i < playersToGen; ++i) for(int i = 0; i < playersToGen; ++i)
{ {
PlayerInfo player; PlayerInfo player;
@ -183,7 +187,7 @@ void RandomMapTab::updateMapInfoByHost()
occupiedTeams.insert(team); occupiedTeams.insert(team);
player.hasMainTown = true; player.hasMainTown = true;
player.generateHeroAtMainTown = true; player.generateHeroAtMainTown = true;
mapInfo->mapHeader->players.push_back(player); mapInfo->mapHeader->players[i] = player;
} }
for(auto & player : mapInfo->mapHeader->players) for(auto & player : mapInfo->mapHeader->players)
{ {
@ -317,7 +321,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
if(tmpl) if(tmpl)
w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL);
else else
w->addTextOverlay(readText(variables["defaultTemplate"]), EFonts::FONT_SMALL); w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL);
} }
for(auto r : VLC->roadTypeHandler->objects) for(auto r : VLC->roadTypeHandler->objects)
{ {
@ -337,7 +341,7 @@ void RandomMapTab::setTemplate(const CRmgTemplate * tmpl)
if(tmpl) if(tmpl)
w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL);
else else
w->addTextOverlay(readText(variables["defaultTemplate"]), EFonts::FONT_SMALL); w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL);
} }
updateMapInfoByHost(); updateMapInfoByHost();
} }
@ -398,7 +402,7 @@ void TemplatesDropBox::ListItem::updateItem(int idx, const CRmgTemplate * _item)
if(idx) if(idx)
w->setText(""); w->setText("");
else else
w->setText(readText(dropBox.variables["defaultTemplate"])); w->setText(readText(dropBox.variables["randomTemplate"]));
} }
} }
} }

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