1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

Merge pull request #609 from viciious/improve_battleai

Improve battle AI
This commit is contained in:
Alexander Shishkin 2020-05-25 05:50:40 +03:00 committed by GitHub
commit cc75b859d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 417 additions and 141 deletions

View File

@ -9,89 +9,182 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "AttackPossibility.h" #include "AttackPossibility.h"
#include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
AttackPossibility::AttackPossibility(BattleHex tile_, const BattleAttackInfo & attack_) AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: tile(tile_), : from(from), dest(dest), attack(attack)
attack(attack_)
{ {
} }
int64_t AttackPossibility::damageDiff() const int64_t AttackPossibility::damageDiff() const
{ {
//TODO: use target priority from HypotheticBattle return damageDealt - damageReceived - collateralDamage + shootersBlockedDmg;
const auto dealtDmgValue = damageDealt;
const auto receivedDmgValue = damageReceived;
int64_t diff = 0;
//friendly fire or not
if(attack.attacker->unitSide() == attack.defender->unitSide())
diff = -dealtDmgValue - receivedDmgValue;
else
diff = dealtDmgValue - receivedDmgValue;
//mind control
auto actualSide = getCbc()->playerToSide(getCbc()->battleGetOwner(attack.attacker));
if(actualSide && actualSide.get() != attack.attacker->unitSide())
diff = -diff;
return diff;
} }
int64_t AttackPossibility::attackValue() const int64_t AttackPossibility::attackValue() const
{ {
return damageDiff() + tacticImpact; return damageDiff();
} }
AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex) int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state)
{ {
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; int64_t res = 0;
static const auto selectorBlocksRetaliation = Selector::type(Bonus::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attackInfo.attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); if(attackInfo.shooting)
return 0;
AttackPossibility ap(hex, attackInfo); auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex);
ap.attackerState = attackInfo.attacker->acquireState(); for(BattleHex tile : hexes)
const int totalAttacks = ap.attackerState->getTotalAttacks(attackInfo.shooting);
if(!attackInfo.shooting)
ap.attackerState->setPosition(hex);
auto defenderState = attackInfo.defender->acquireState();
ap.affectedUnits.push_back(defenderState);
for(int i = 0; i < totalAttacks; i++)
{ {
TDmgRange retaliation(0,0); auto st = state->battleGetUnitByPos(tile, true);
auto attackDmg = getCbc()->battleEstimateDamage(ap.attack, &retaliation); if(!st || !state->battleMatchOwner(st, attacker))
continue;
if(!state->battleCanShoot(st))
continue;
vstd::amin(attackDmg.first, defenderState->getAvailableHealth()); BattleAttackInfo rangeAttackInfo(st, attacker, true);
vstd::amin(attackDmg.second, defenderState->getAvailableHealth()); rangeAttackInfo.defenderPos = hex;
vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth()); BattleAttackInfo meleeAttackInfo(st, attacker, false);
vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth()); meleeAttackInfo.defenderPos = hex;
ap.damageDealt += (attackDmg.first + attackDmg.second) / 2; auto rangeDmg = getCbc()->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = getCbc()->battleEstimateDamage(meleeAttackInfo);
ap.attackerState->afterAttack(attackInfo.shooting, false); int64_t gain = (rangeDmg.first + rangeDmg.second - meleeDmg.first - meleeDmg.second) / 2 + 1;
res += gain;
//FIXME: use ranged retaliation
if(!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{
ap.damageReceived += (retaliation.first + retaliation.second) / 2;
defenderState->afterAttack(attackInfo.shooting, true);
}
ap.attackerState->damage(ap.damageReceived);
defenderState->damage(ap.damageDealt);
if(!ap.attackerState->alive() || !defenderState->alive())
break;
} }
//TODO other damage related to attack (eg. fire shield and other abilities) return res;
}
return ap;
AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state)
{
auto attacker = attackInfo.attacker;
auto defender = attackInfo.defender;
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type(Bonus::BLOCKS_RETALIATION);
const auto attackerSide = getCbc()->playerToSide(getCbc()->battleGetOwner(attacker));
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo);
std::vector<BattleHex> defenderHex;
if(attackInfo.shooting)
defenderHex = defender->getHexes();
else
defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
for(BattleHex defHex : defenderHex)
{
if(defHex == hex) // should be impossible but check anyway
continue;
AttackPossibility ap(hex, defHex, attackInfo);
ap.attackerState = attacker->acquireState();
ap.shootersBlockedDmg = bestAp.shootersBlockedDmg;
const int totalAttacks = ap.attackerState->getTotalAttacks(attackInfo.shooting);
if (!attackInfo.shooting)
ap.attackerState->setPosition(hex);
std::vector<const battle::Unit*> units;
if (attackInfo.shooting)
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
else
units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
// ensure the defender is also affected
bool addDefender = true;
for(auto unit : units)
{
if (unit->unitId() == defender->unitId())
{
addDefender = false;
break;
}
}
if(addDefender)
units.push_back(defender);
for(auto u : units)
{
if(!ap.attackerState->alive())
break;
assert(u->unitId() != attacker->unitId());
auto defenderState = u->acquireState();
ap.affectedUnits.push_back(defenderState);
for(int i = 0; i < totalAttacks; i++)
{
si64 damageDealt, damageReceived;
TDmgRange retaliation(0, 0);
auto attackDmg = getCbc()->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.first, defenderState->getAvailableHealth());
vstd::amin(attackDmg.second, defenderState->getAvailableHealth());
vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth());
vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth());
damageDealt = (attackDmg.first + attackDmg.second) / 2;
ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation
damageReceived = 0;
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{
damageReceived = (retaliation.first + retaliation.second) / 2;
defenderState->afterAttack(attackInfo.shooting, true);
}
bool isEnemy = state->battleMatchOwner(attacker, u);
// this includes enemy units as well as attacker units under enemy's mind control
if(isEnemy)
ap.damageDealt += damageDealt;
// damaging attacker's units (even those under enemy's mind control) is considered friendly fire
if(attackerSide == u->unitSide())
ap.collateralDamage += damageDealt;
if(u->unitId() == defender->unitId() ||
(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
{
//FIXME: handle RANGED_RETALIATION ?
ap.damageReceived += damageReceived;
}
ap.attackerState->damage(damageReceived);
defenderState->damage(damageDealt);
if (!ap.attackerState->alive() || !defenderState->alive())
break;
}
}
if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
bestAp = ap;
}
// check how much damage we gain from blocking enemy shooters on this hex
bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, state);
logAi->debug("BattleAI best AP: %s -> %s at %d from %d, affects %d units: %lld %lld %lld %lld",
attackInfo.attacker->unitType()->identifier,
attackInfo.defender->unitType()->identifier,
(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
bestAp.damageDealt, bestAp.damageReceived, bestAp.collateralDamage, bestAp.shootersBlockedDmg);
//TODO other damage related to attack (eg. fire shield and other abilities)
return bestAp;
} }

View File

@ -16,7 +16,8 @@
class AttackPossibility class AttackPossibility
{ {
public: public:
BattleHex tile; //tile from which we attack BattleHex from; //tile from which we attack
BattleHex dest; //tile which we attack
BattleAttackInfo attack; BattleAttackInfo attack;
std::shared_ptr<battle::CUnitState> attackerState; std::shared_ptr<battle::CUnitState> attackerState;
@ -25,12 +26,16 @@ public:
int64_t damageDealt = 0; int64_t damageDealt = 0;
int64_t damageReceived = 0; //usually by counter-attack int64_t damageReceived = 0; //usually by counter-attack
int64_t tacticImpact = 0; int64_t collateralDamage = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0;
AttackPossibility(BattleHex tile_, const BattleAttackInfo & attack_); AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
int64_t damageDiff() const; int64_t damageDiff() const;
int64_t attackValue() const; int64_t attackValue() const;
static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex); static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state);
private:
static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state);
}; };

View File

@ -18,7 +18,9 @@
#include "../../lib/CThreadHelper.h" #include "../../lib/CThreadHelper.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/CStack.h"//todo: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#define LOGL(text) print(text) #define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
@ -154,7 +156,8 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
HypotheticBattle hb(getCbc()); HypotheticBattle hb(getCbc());
PotentialTargets targets(stack, &hb); PotentialTargets targets(stack, &hb);
if(targets.possibleAttacks.size())
if(!targets.possibleAttacks.empty())
{ {
AttackPossibility bestAttack = targets.bestAction(); AttackPossibility bestAttack = targets.bestAction();
@ -164,7 +167,18 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
else if(bestAttack.attack.shooting) else if(bestAttack.attack.shooting)
return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
else else
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.tile); {
auto &target = bestAttack;
logAi->debug("BattleAI: %s -> %s %d from, %d curpos %d dist %d speed %d: %lld %lld %lld",
target.attackerState->unitType()->identifier,
target.affectedUnits[0]->unitType()->identifier,
(int)target.affectedUnits.size(), (int)target.from, (int)bestAttack.attack.attacker->getPosition().hex,
bestAttack.attack.chargedFields, bestAttack.attack.attacker->Speed(0, true),
target.damageDealt, target.damageReceived, target.attackValue()
);
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
}
} }
else if(bestSpellcast.is_initialized()) else if(bestSpellcast.is_initialized())
{ {

View File

@ -53,7 +53,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
if(hex.isValid() && !shooting) if(hex.isValid() && !shooting)
bai.chargedFields = reachability.distances[hex]; bai.chargedFields = reachability.distances[hex];
return AttackPossibility::evaluate(bai, hex); return AttackPossibility::evaluate(bai, hex, state);
}; };
if(forceTarget) if(forceTarget)
@ -70,20 +70,45 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
else else
{ {
for(BattleHex hex : avHexes) for(BattleHex hex : avHexes)
if(CStack::isMeleeAttackPossible(attackerInfo, defender, hex)) {
possibleAttacks.push_back(GenerateAttackInfo(false, hex)); if(!CStack::isMeleeAttackPossible(attackerInfo, defender, hex))
continue;
auto bai = GenerateAttackInfo(false, hex);
if(!bai.affectedUnits.empty())
possibleAttacks.push_back(bai);
}
if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility & pa) { return pa.attack.defender->unitId() == defender->unitId(); })) if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility & pa) { return pa.attack.defender->unitId() == defender->unitId(); }))
unreachableEnemies.push_back(defender); unreachableEnemies.push_back(defender);
} }
} }
boost::sort(possibleAttacks, [](const AttackPossibility & lhs, const AttackPossibility & rhs) -> bool
{
if(lhs.collateralDamage > rhs.collateralDamage)
return false;
if(lhs.collateralDamage < rhs.collateralDamage)
return true;
return (lhs.damageDealt + lhs.shootersBlockedDmg - lhs.damageReceived > rhs.damageDealt + rhs.shootersBlockedDmg - rhs.damageReceived);
});
if (!possibleAttacks.empty())
{
auto &bestAp = possibleAttacks[0];
logGlobal->info("Battle AI best: %s -> %s at %d from %d, affects %d units: %lld %lld %lld %lld",
bestAp.attack.attacker->unitType()->identifier,
state->battleGetUnitByPos(bestAp.dest)->unitType()->identifier,
(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
bestAp.damageDealt, bestAp.damageReceived, bestAp.collateralDamage, bestAp.shootersBlockedDmg);
}
} }
int PotentialTargets::bestActionValue() const int64_t PotentialTargets::bestActionValue() const
{ {
if(possibleAttacks.empty()) if(possibleAttacks.empty())
return 0; return 0;
return bestAction().attackValue(); return bestAction().attackValue();
} }
@ -91,6 +116,6 @@ AttackPossibility PotentialTargets::bestAction() const
{ {
if(possibleAttacks.empty()) if(possibleAttacks.empty())
throw std::runtime_error("No best action, since we don't have any actions"); throw std::runtime_error("No best action, since we don't have any actions");
return possibleAttacks[0];
return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } ); //return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } );
} }

View File

@ -20,5 +20,5 @@ public:
PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state); PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state);
AttackPossibility bestAction() const; AttackPossibility bestAction() const;
int bestActionValue() const; int64_t bestActionValue() const;
}; };

View File

@ -108,6 +108,8 @@ static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayInde
void playIntro(); void playIntro();
static void mainLoop(); static void mainLoop();
static CBasicLogConfigurator *logConfig;
#ifndef VCMI_WINDOWS #ifndef VCMI_WINDOWS
#ifndef _GNU_SOURCE #ifndef _GNU_SOURCE
#define _GNU_SOURCE #define _GNU_SOURCE
@ -230,8 +232,8 @@ int main(int argc, char * argv[])
console->start(); console->start();
const bfs::path logPath = VCMIDirs::get().userCachePath() / "VCMI_Client_log.txt"; const bfs::path logPath = VCMIDirs::get().userCachePath() / "VCMI_Client_log.txt";
CBasicLogConfigurator logConfig(logPath, console); logConfig = new CBasicLogConfigurator(logPath, console);
logConfig.configureDefault(); logConfig->configureDefault();
logGlobal->info(NAME); logGlobal->info(NAME);
logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff());
logGlobal->info("The log file will be saved to %s", logPath); logGlobal->info("The log file will be saved to %s", logPath);
@ -292,7 +294,7 @@ int main(int argc, char * argv[])
setSettingInteger("general/saveFrequency", "savefrequency", 1); setSettingInteger("general/saveFrequency", "savefrequency", 1);
// Initialize logging based on settings // Initialize logging based on settings
logConfig.configure(); logConfig->configure();
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
@ -1434,6 +1436,13 @@ void handleQuit(bool ask)
SDL_Quit(); SDL_Quit();
} }
if(logConfig != nullptr)
{
logConfig->deconfigure();
delete logConfig;
logConfig = nullptr;
}
std::cout << "Ending...\n"; std::cout << "Ending...\n";
exit(0); exit(0);
}; };

View File

@ -13,6 +13,14 @@
#include "Geometries.h" #include "Geometries.h"
#include "../../lib/GameConstants.h" #include "../../lib/GameConstants.h"
#ifdef IN
#undef IN
#endif
#ifdef OUT
#undef OUT
#endif
struct SDL_Surface; struct SDL_Surface;
class JsonNode; class JsonNode;
class CDefFile; class CDefFile;

View File

@ -15,6 +15,14 @@
#include "gui/Geometries.h" #include "gui/Geometries.h"
#include "SDL.h" #include "SDL.h"
#ifdef IN
#undef IN
#endif
#ifdef OUT
#undef OUT
#endif
class CGObjectInstance; class CGObjectInstance;
class CGHeroInstance; class CGHeroInstance;
class CGBoat; class CGBoat;

View File

@ -252,22 +252,61 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, std::s
bsa.newState.operation = UnitChanges::EOperation::RESET_STATE; bsa.newState.operation = UnitChanges::EOperation::RESET_STATE;
} }
bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) std::vector<BattleHex> CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos)
{ {
if(!attackerPos.isValid()) int mask = 0;
std::vector<BattleHex> res;
if (!attackerPos.isValid())
attackerPos = attacker->getPosition(); attackerPos = attacker->getPosition();
if(!defenderPos.isValid()) if (!defenderPos.isValid())
defenderPos = defender->getPosition(); defenderPos = defender->getPosition();
return BattleHex otherAttackerPos = attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1);
(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0)//front <=> front BattleHex otherDefenderPos = defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1);
|| (attacker->doubleWide()//back <=> front
&& BattleHex::mutualPosition(attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1), defenderPos) >= 0)
|| (defender->doubleWide()//front <=> back
&& BattleHex::mutualPosition(attackerPos, defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1)) >= 0)
|| (defender->doubleWide() && attacker->doubleWide()//back <=> back
&& BattleHex::mutualPosition(attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1), defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1)) >= 0);
if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front
{
if((mask & 1) == 0)
{
mask |= 1;
res.push_back(defenderPos);
}
}
if (attacker->doubleWide() //back <=> front
&& BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0)
{
if((mask & 1) == 0)
{
mask |= 1;
res.push_back(defenderPos);
}
}
if (defender->doubleWide()//front <=> back
&& BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0)
{
if((mask & 2) == 0)
{
mask |= 2;
res.push_back(otherDefenderPos);
}
}
if (defender->doubleWide() && attacker->doubleWide()//back <=> back
&& BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0)
{
if((mask & 2) == 0)
{
mask |= 2;
res.push_back(otherDefenderPos);
}
}
return res;
}
bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos)
{
return !meleeAttackHexes(attacker, defender, attackerPos, defenderPos).empty();
} }
std::string CStack::getName() const std::string CStack::getName() const

View File

@ -55,6 +55,7 @@ public:
std::vector<si32> activeSpells() const; //returns vector of active spell IDs sorted by time of cast std::vector<si32> activeSpells() const; //returns vector of active spell IDs sorted by time of cast
const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise
static std::vector<BattleHex> meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
BattleHex::EDir destShiftDir() const; BattleHex::EDir destShiftDir() const;

View File

@ -19,6 +19,8 @@ BattleAttackInfo::BattleAttackInfo(const battle::Unit * Attacker, const battle::
chargedFields = 0; chargedFields = 0;
additiveBonus = 0.0; additiveBonus = 0.0;
multBonus = 1.0; multBonus = 1.0;
attackerPos = BattleHex::INVALID;
defenderPos = BattleHex::INVALID;
} }
BattleAttackInfo BattleAttackInfo::reverse() const BattleAttackInfo BattleAttackInfo::reverse() const
@ -26,6 +28,7 @@ BattleAttackInfo BattleAttackInfo::reverse() const
BattleAttackInfo ret = *this; BattleAttackInfo ret = *this;
std::swap(ret.attacker, ret.defender); std::swap(ret.attacker, ret.defender);
std::swap(ret.defenderPos, ret.attackerPos);
ret.shooting = false; ret.shooting = false;
ret.chargedFields = 0; ret.chargedFields = 0;

View File

@ -15,11 +15,16 @@ namespace battle
class CUnitState; class CUnitState;
} }
#include "BattleHex.h"
struct DLL_LINKAGE BattleAttackInfo struct DLL_LINKAGE BattleAttackInfo
{ {
const battle::Unit * attacker; const battle::Unit * attacker;
const battle::Unit * defender; const battle::Unit * defender;
BattleHex attackerPos;
BattleHex defenderPos;
bool shooting; bool shooting;
int chargedFields; int chargedFields;

View File

@ -667,37 +667,45 @@ bool CBattleInfoCallback::battleCanAttack(const CStack * stack, const CStack * t
return target->alive(); return target->alive();
} }
bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const
{ {
RETURN_IF_NOT_BATTLE(false); RETURN_IF_NOT_BATTLE(false);
if(battleTacticDist()) //no shooting during tactics if (battleTacticDist()) //no shooting during tactics
return false; return false;
const battle::Unit * defender = battleGetUnitByPos(dest); if (!attacker)
return false;
if(!attacker || !defender) if (attacker->creatureIndex() == CreatureID::CATAPULT) //catapult cannot attack creatures
return false; return false;
//forgetfulness //forgetfulness
TBonusListPtr forgetfulList = attacker->getBonuses(Selector::type(Bonus::FORGETFULL)); TBonusListPtr forgetfulList = attacker->getBonuses(Selector::type(Bonus::FORGETFULL));
if(!forgetfulList->empty()) if (!forgetfulList->empty())
{ {
int forgetful = forgetfulList->valOfBonuses(Selector::type(Bonus::FORGETFULL)); int forgetful = forgetfulList->valOfBonuses(Selector::type(Bonus::FORGETFULL));
//advanced+ level //advanced+ level
if(forgetful > 1) if (forgetful > 1)
return false; return false;
} }
if(attacker->creatureIndex() == CreatureID::CATAPULT && defender) //catapult cannot attack creatures return attacker->canShoot() && (!battleIsUnitBlocked(attacker)
|| attacker->hasBonusOfType(Bonus::FREE_SHOOTING));
}
bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const
{
RETURN_IF_NOT_BATTLE(false);
const battle::Unit * defender = battleGetUnitByPos(dest);
if(!attacker || !defender)
return false; return false;
return attacker->canShoot() if(battleMatchOwner(attacker, defender) && defender->alive())
&& battleMatchOwner(attacker, defender) return battleCanShoot(attacker);
&& defender->alive()
&& (!battleIsUnitBlocked(attacker) return false;
|| attacker->hasBonusOfType(Bonus::FREE_SHOOTING));
} }
TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const
@ -897,8 +905,11 @@ TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info)
if(info.shooting) if(info.shooting)
{ {
//wall / distance penalty + advanced air shield //wall / distance penalty + advanced air shield
const bool distPenalty = battleHasDistancePenalty(attackerBonuses, info.attacker->getPosition(), info.defender->getPosition()); BattleHex attackerPos = info.attackerPos.isValid() ? info.attackerPos : info.attacker->getPosition();
const bool obstaclePenalty = battleHasWallPenalty(attackerBonuses, info.attacker->getPosition(), info.defender->getPosition()); BattleHex defenderPos = info.defenderPos.isValid() ? info.defenderPos : info.defender->getPosition();
const bool distPenalty = battleHasDistancePenalty(attackerBonuses, attackerPos, defenderPos);
const bool obstaclePenalty = battleHasWallPenalty(attackerBonuses, attackerPos, defenderPos);
if(distPenalty || defenderBonuses->hasBonus(isAdvancedAirShield, cachingStrAdvAirShield)) if(distPenalty || defenderBonuses->hasBonus(isAdvancedAirShield, cachingStrAdvAirShield))
multBonus *= 0.5; multBonus *= 0.5;
@ -1340,11 +1351,11 @@ ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityIn
return ret; return ret;
} }
AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const
{ {
//does not return hex attacked directly //does not return hex attacked directly
//TODO: apply rotation to two-hex attackers //TODO: apply rotation to two-hex attackers
bool isAttacker = attacker->side == BattleSide::ATTACKER; bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
AttackableTiles at; AttackableTiles at;
RETURN_IF_NOT_BATTLE(at); RETURN_IF_NOT_BATTLE(at);
@ -1369,8 +1380,8 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const CStack
{ {
if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, hex) > -1)) //adjacent both to attacker's head and attacked tile if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, hex) > -1)) //adjacent both to attacker's head and attacked tile
{ {
const CStack * st = battleGetStackByPos(tile, true); auto st = battleGetUnitByPos(tile, true);
if(st && st->owner != attacker->owner) //only hostile stacks - does it work well with Berserk? if(st && battleMatchOwner(st, attacker)) //only hostile stacks - does it work well with Berserk?
{ {
at.hostileCreaturePositions.insert(tile); at.hostileCreaturePositions.insert(tile);
} }
@ -1391,45 +1402,50 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const CStack
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
{ {
//friendly stacks can also be damaged by Dragon Breath //friendly stacks can also be damaged by Dragon Breath
if(battleGetStackByPos(tile, true)) auto st = battleGetUnitByPos(tile, true);
if(st && st != attacker)
{ {
if(battleGetStackByPos(tile, true) != attacker) at.friendlyCreaturePositions.insert(tile);
at.friendlyCreaturePositions.insert(tile);
} }
} }
} }
else if(attacker->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH) && BattleHex::mutualPosition(destinationTile, hex) > -1) //only adjacent hexes are subject of dragon breath calculation else if(attacker->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH))
{ {
std::vector<BattleHex> hexes; //only one, in fact int pos = BattleHex::mutualPosition(destinationTile, hex);
int pseudoVector = destinationTile.hex - hex; if (pos > -1) //only adjacent hexes are subject of dragon breath calculation
switch(pseudoVector)
{ {
case 1: std::vector<BattleHex> hexes; //only one, in fact
case -1: int pseudoVector = destinationTile.hex - hex;
BattleHex::checkAndPush(destinationTile.hex + pseudoVector, hexes); switch (pseudoVector)
break; {
case WN: //17 //left-down or right-down case 1:
case -WN: //-17 //left-up or right-up case -1:
case WN + 1: //18 //right-down BattleHex::checkAndPush(destinationTile.hex + pseudoVector, hexes);
case -WN + 1: //-16 //right-up break;
BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : -1), hexes); case WN: //17 //left-down or right-down
break; case -WN: //-17 //left-up or right-up
case WN - 1: //16 //left-down case WN + 1: //18 //right-down
case -WN - 1: //-18 //left-up case -WN + 1: //-16 //right-up
BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : 0), hexes); BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : -1), hexes);
break; break;
} case WN - 1: //16 //left-down
for(BattleHex tile : hexes) case -WN - 1: //-18 //left-up
{ BattleHex::checkAndPush(destinationTile.hex + pseudoVector + (((hex / WN) % 2) ? 1 : 0), hexes);
//friendly stacks can also be damaged by Dragon Breath break;
if(battleGetStackByPos(tile, true)) }
at.friendlyCreaturePositions.insert(tile); for (BattleHex tile : hexes)
{
//friendly stacks can also be damaged by Dragon Breath
auto st = battleGetUnitByPos(tile, true);
if (st != nullptr)
at.friendlyCreaturePositions.insert(tile);
}
} }
} }
return at; return at;
} }
AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const CStack * attacker, BattleHex destinationTile, BattleHex attackerPos) const AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const
{ {
//does not return hex attacked directly //does not return hex attacked directly
AttackableTiles at; AttackableTiles at;
@ -1445,6 +1461,36 @@ AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const CStack *
return at; return at;
} }
std::vector<const battle::Unit*> CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const
{
std::vector<const battle::Unit*> units;
RETURN_IF_NOT_BATTLE(units);
AttackableTiles at;
if (rangedAttack)
at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos);
else
at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos);
units = battleGetUnitsIf([=](const battle::Unit * unit)
{
if (unit->isGhost() || !unit->alive())
return false;
for (BattleHex hex : battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()))
{
if (vstd::contains(at.hostileCreaturePositions, hex))
return true;
if (vstd::contains(at.friendlyCreaturePositions, hex))
return true;
}
return false;
});
return units;
}
std::set<const CStack*> CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const std::set<const CStack*> CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const
{ {
std::set<const CStack*> attackedCres; std::set<const CStack*> attackedCres;

View File

@ -89,6 +89,7 @@ public:
bool battleCanAttack(const CStack * stack, const CStack * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination bool battleCanAttack(const CStack * stack, const CStack * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination
bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination
bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle
bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack
std::set<const battle::Unit *> battleAdjacentUnits(const battle::Unit * unit) const; std::set<const battle::Unit *> battleAdjacentUnits(const battle::Unit * unit) const;
@ -123,8 +124,9 @@ public:
bool isInTacticRange(BattleHex dest) const; bool isInTacticRange(BattleHex dest) const;
si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side) si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side)
AttackableTiles getPotentiallyAttackableHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker AttackableTiles getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker
AttackableTiles getPotentiallyShootableHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const; AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const;
std::vector<const battle::Unit *> getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
std::set<const CStack*> getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks std::set<const CStack*> getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
bool isToReverse(BattleHex hexFrom, BattleHex hexTo, bool curDir /*if true, creature is in attacker's direction*/, bool toDoubleWide, bool toDir) const; //determines if creature should be reversed (it stands on hexFrom and should 'see' hexTo) bool isToReverse(BattleHex hexFrom, BattleHex hexTo, bool curDir /*if true, creature is in attacker's direction*/, bool toDoubleWide, bool toDir) const; //determines if creature should be reversed (it stands on hexFrom and should 'see' hexTo)
bool isToReverseHlp(BattleHex hexFrom, BattleHex hexTo, bool curDir) const; //helper for isToReverse bool isToReverseHlp(BattleHex hexFrom, BattleHex hexTo, bool curDir) const; //helper for isToReverse

View File

@ -140,3 +140,10 @@ EConsoleTextColor::EConsoleTextColor CBasicLogConfigurator::getConsoleColor(cons
else else
throw std::runtime_error("Color " + colorName + " unknown."); throw std::runtime_error("Color " + colorName + " unknown.");
} }
void CBasicLogConfigurator::deconfigure()
{
auto l = CLogger::getGlobalLogger();
if(l != nullptr)
l->clearTargets();
}

View File

@ -29,6 +29,10 @@ public:
/// Configures a default logging system by adding the console target and the file target to the global logger. /// Configures a default logging system by adding the console target and the file target to the global logger.
void configureDefault(); void configureDefault();
/// Removes all targets from the global logger.
void deconfigure();
private: private:
// Gets ELogLevel enum from string. (Should be moved to CLogger as a separate function?) // Gets ELogLevel enum from string. (Should be moved to CLogger as a separate function?)
// Throws: std::runtime_error // Throws: std::runtime_error

View File

@ -402,3 +402,8 @@ void CLogFileTarget::write(const LogRecord & record)
const CLogFormatter & CLogFileTarget::getFormatter() const { return formatter; } const CLogFormatter & CLogFileTarget::getFormatter() const { return formatter; }
void CLogFileTarget::setFormatter(const CLogFormatter & formatter) { this->formatter = formatter; } void CLogFileTarget::setFormatter(const CLogFormatter & formatter) { this->formatter = formatter; }
CLogFileTarget::~CLogFileTarget()
{
file.close();
}

View File

@ -213,6 +213,7 @@ public:
/// Constructs a CLogFileTarget and opens the file designated by filePath. If the append parameter is true, the file /// Constructs a CLogFileTarget and opens the file designated by filePath. If the append parameter is true, the file
/// will be appended to. Otherwise the file designated by filePath will be truncated before being opened. /// will be appended to. Otherwise the file designated by filePath will be truncated before being opened.
explicit CLogFileTarget(boost::filesystem::path filePath, bool append = true); explicit CLogFileTarget(boost::filesystem::path filePath, bool append = true);
~CLogFileTarget();
const CLogFormatter & getFormatter() const; const CLogFormatter & getFormatter() const;
void setFormatter(const CLogFormatter & formatter); void setFormatter(const CLogFormatter & formatter);

View File

@ -956,6 +956,7 @@ int main(int argc, char * argv[])
CAndroidVMHelper envHelper; CAndroidVMHelper envHelper;
envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer");
#endif #endif
logConfig.deconfigure();
vstd::clear_pointer(VLC); vstd::clear_pointer(VLC);
return 0; return 0;
} }