mirror of
https://github.com/vcmi/vcmi.git
synced 2024-11-30 08:57:00 +02:00
f264c541fb
Tactics is split to 2 bonuses, and it is now possible to block tactics without having tactics itself. But tactics for two sides is not implemented, because it is huge rework and not high priority for me now, I want to do basic secondary skill rework first.
1047 lines
28 KiB
C++
1047 lines
28 KiB
C++
/*
|
|
* BattleInfo.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 "BattleInfo.h"
|
|
#include "../CStack.h"
|
|
#include "../CHeroHandler.h"
|
|
#include "../NetPacks.h"
|
|
#include "../filesystem/Filesystem.h"
|
|
#include "../mapObjects/CGTownInstance.h"
|
|
#include "../CGeneralTextHandler.h"
|
|
#include "../BattleFieldHandler.h"
|
|
#include "../ObstacleHandler.h"
|
|
|
|
//TODO: remove
|
|
#include "../IGameCallback.h"
|
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
///BattleInfo
|
|
std::pair< std::vector<BattleHex>, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const CStack * stack)
|
|
{
|
|
auto reachability = getReachability(stack);
|
|
|
|
if(reachability.predecessors[dest] == -1) //cannot reach destination
|
|
{
|
|
return std::make_pair(std::vector<BattleHex>(), 0);
|
|
}
|
|
|
|
//making the Path
|
|
std::vector<BattleHex> path;
|
|
BattleHex curElem = dest;
|
|
while(curElem != start)
|
|
{
|
|
path.push_back(curElem);
|
|
curElem = reachability.predecessors[curElem];
|
|
}
|
|
|
|
return std::make_pair(path, reachability.distances[dest]);
|
|
}
|
|
|
|
void BattleInfo::calculateCasualties(std::map<ui32,si32> * casualties) const
|
|
{
|
|
for(const auto & elem : stacks) //setting casualties
|
|
{
|
|
const CStack * const st = elem;
|
|
si32 killed = st->getKilled();
|
|
if(killed > 0)
|
|
casualties[st->side][st->getCreature()->getId()] += killed;
|
|
}
|
|
}
|
|
|
|
CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position)
|
|
{
|
|
PlayerColor owner = sides[side].color;
|
|
assert((owner >= PlayerColor::PLAYER_LIMIT) ||
|
|
(base.armyObj && base.armyObj->tempOwner == owner));
|
|
|
|
auto * ret = new CStack(&base, owner, id, side, slot);
|
|
ret->initialPosition = getAvaliableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found?
|
|
stacks.push_back(ret);
|
|
return ret;
|
|
}
|
|
|
|
CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position)
|
|
{
|
|
PlayerColor owner = sides[side].color;
|
|
auto * ret = new CStack(&base, owner, id, side, slot);
|
|
ret->initialPosition = position;
|
|
stacks.push_back(ret);
|
|
return ret;
|
|
}
|
|
|
|
void BattleInfo::localInit()
|
|
{
|
|
for(int i = 0; i < 2; i++)
|
|
{
|
|
auto * armyObj = battleGetArmyObject(i);
|
|
armyObj->battle = this;
|
|
armyObj->attachTo(*this);
|
|
}
|
|
|
|
for(CStack * s : stacks)
|
|
s->localInit(this);
|
|
|
|
exportBonuses();
|
|
}
|
|
|
|
namespace CGH
|
|
{
|
|
static void readBattlePositions(const JsonNode &node, std::vector< std::vector<int> > & dest)
|
|
{
|
|
for(const JsonNode &level : node.Vector())
|
|
{
|
|
std::vector<int> pom;
|
|
for(const JsonNode &value : level.Vector())
|
|
{
|
|
pom.push_back(static_cast<int>(value.Float()));
|
|
}
|
|
|
|
dest.push_back(pom);
|
|
}
|
|
}
|
|
}
|
|
|
|
//RNG that works like H3 one
|
|
struct RandGen
|
|
{
|
|
ui32 seed;
|
|
|
|
void srand(ui32 s)
|
|
{
|
|
seed = s;
|
|
}
|
|
void srand(const int3 & pos)
|
|
{
|
|
srand(110291 * static_cast<ui32>(pos.x) + 167801 * static_cast<ui32>(pos.y) + 81569);
|
|
}
|
|
int rand()
|
|
{
|
|
seed = 214013 * seed + 2531011;
|
|
return (seed >> 16) & 0x7FFF;
|
|
}
|
|
int rand(int min, int max)
|
|
{
|
|
if(min == max)
|
|
return min;
|
|
if(min > max)
|
|
return min;
|
|
return min + rand() % (max - min + 1);
|
|
}
|
|
};
|
|
|
|
struct RangeGenerator
|
|
{
|
|
class ExhaustedPossibilities : public std::exception
|
|
{
|
|
};
|
|
|
|
RangeGenerator(int _min, int _max, std::function<int()> _myRand):
|
|
min(_min),
|
|
remainingCount(_max - _min + 1),
|
|
remaining(remainingCount, true),
|
|
myRand(std::move(_myRand))
|
|
{
|
|
}
|
|
|
|
int generateNumber() const
|
|
{
|
|
if(!remainingCount)
|
|
throw ExhaustedPossibilities();
|
|
if(remainingCount == 1)
|
|
return 0;
|
|
return myRand() % remainingCount;
|
|
}
|
|
|
|
//get number fulfilling predicate. Never gives the same number twice.
|
|
int getSuchNumber(const std::function<bool(int)> & goodNumberPred = nullptr)
|
|
{
|
|
int ret = -1;
|
|
do
|
|
{
|
|
int n = generateNumber();
|
|
int i = 0;
|
|
for(;;i++)
|
|
{
|
|
assert(i < (int)remaining.size());
|
|
if(!remaining[i])
|
|
continue;
|
|
if(!n)
|
|
break;
|
|
n--;
|
|
}
|
|
|
|
remainingCount--;
|
|
remaining[i] = false;
|
|
ret = i + min;
|
|
} while(goodNumberPred && !goodNumberPred(ret));
|
|
return ret;
|
|
}
|
|
|
|
int min, remainingCount;
|
|
std::vector<bool> remaining;
|
|
std::function<int()> myRand;
|
|
};
|
|
|
|
BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town)
|
|
{
|
|
CMP_stack cmpst;
|
|
auto * curB = new BattleInfo();
|
|
|
|
for(auto i = 0u; i < curB->sides.size(); i++)
|
|
curB->sides[i].init(heroes[i], armies[i]);
|
|
|
|
|
|
std::vector<CStack*> & stacks = (curB->stacks);
|
|
|
|
curB->tile = tile;
|
|
curB->battlefieldType = battlefieldType;
|
|
curB->round = -2;
|
|
curB->activeStack = -1;
|
|
|
|
if(town)
|
|
{
|
|
curB->town = town;
|
|
curB->terrainType = (*VLC->townh)[town->subID]->nativeTerrain;
|
|
}
|
|
else
|
|
{
|
|
curB->town = nullptr;
|
|
curB->terrainType = terrain;
|
|
}
|
|
|
|
//setting up siege obstacles
|
|
if (town && town->hasFort())
|
|
{
|
|
curB->si.gateState = EGateState::CLOSED;
|
|
|
|
curB->si.wallState[EWallPart::GATE] = EWallState::INTACT;
|
|
|
|
for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL})
|
|
{
|
|
if (town->hasBuilt(BuildingID::CASTLE))
|
|
curB->si.wallState[wall] = EWallState::REINFORCED;
|
|
else
|
|
curB->si.wallState[wall] = EWallState::INTACT;
|
|
}
|
|
|
|
if (town->hasBuilt(BuildingID::CITADEL))
|
|
curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT;
|
|
|
|
if (town->hasBuilt(BuildingID::CASTLE))
|
|
{
|
|
curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT;
|
|
curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT;
|
|
}
|
|
}
|
|
|
|
//randomize obstacles
|
|
if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank
|
|
{
|
|
RandGen r{};
|
|
auto ourRand = [&](){ return r.rand(); };
|
|
r.srand(tile);
|
|
r.rand(1,8); //battle sound ID to play... can't do anything with it here
|
|
int tilesToBlock = r.rand(5,12);
|
|
|
|
std::vector<BattleHex> blockedTiles;
|
|
|
|
auto appropriateAbsoluteObstacle = [&](int id)
|
|
{
|
|
const auto * info = Obstacle(id).getInfo();
|
|
return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType);
|
|
};
|
|
auto appropriateUsualObstacle = [&](int id)
|
|
{
|
|
const auto * info = Obstacle(id).getInfo();
|
|
return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType);
|
|
};
|
|
|
|
if(r.rand(1,100) <= 40) //put cliff-like obstacle
|
|
{
|
|
try
|
|
{
|
|
RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand);
|
|
auto obstPtr = std::make_shared<CObstacleInstance>();
|
|
obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE;
|
|
obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle);
|
|
obstPtr->uniqueID = static_cast<si32>(curB->obstacles.size());
|
|
curB->obstacles.push_back(obstPtr);
|
|
|
|
for(BattleHex blocked : obstPtr->getBlockedTiles())
|
|
blockedTiles.push_back(blocked);
|
|
tilesToBlock -= Obstacle(obstPtr->ID).getInfo()->blockedTiles.size() / 2;
|
|
}
|
|
catch(RangeGenerator::ExhaustedPossibilities &)
|
|
{
|
|
//silently ignore, if we can't place absolute obstacle, we'll go with the usual ones
|
|
logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place absolute obstacle");
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
while(tilesToBlock > 0)
|
|
{
|
|
RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand);
|
|
auto tileAccessibility = curB->getAccesibility();
|
|
const int obid = obidgen.getSuchNumber(appropriateUsualObstacle);
|
|
const ObstacleInfo &obi = *Obstacle(obid).getInfo();
|
|
|
|
auto validPosition = [&](BattleHex pos) -> bool
|
|
{
|
|
if(obi.height >= pos.getY())
|
|
return false;
|
|
if(pos.getX() == 0)
|
|
return false;
|
|
if(pos.getX() + obi.width > 15)
|
|
return false;
|
|
if(vstd::contains(blockedTiles, pos))
|
|
return false;
|
|
|
|
for(BattleHex blocked : obi.getBlocked(pos))
|
|
{
|
|
if(tileAccessibility[blocked] == EAccessibility::UNAVAILABLE) //for ship-to-ship battlefield - exclude hardcoded unavailable tiles
|
|
return false;
|
|
if(vstd::contains(blockedTiles, blocked))
|
|
return false;
|
|
int x = blocked.getX();
|
|
if(x <= 2 || x >= 14)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
RangeGenerator posgenerator(18, 168, ourRand);
|
|
|
|
auto obstPtr = std::make_shared<CObstacleInstance>();
|
|
obstPtr->ID = obid;
|
|
obstPtr->pos = posgenerator.getSuchNumber(validPosition);
|
|
obstPtr->uniqueID = static_cast<si32>(curB->obstacles.size());
|
|
curB->obstacles.push_back(obstPtr);
|
|
|
|
for(BattleHex blocked : obstPtr->getBlockedTiles())
|
|
blockedTiles.push_back(blocked);
|
|
tilesToBlock -= static_cast<int>(obi.blockedTiles.size());
|
|
}
|
|
}
|
|
catch(RangeGenerator::ExhaustedPossibilities &)
|
|
{
|
|
logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place usual obstacle");
|
|
}
|
|
}
|
|
|
|
//reading battleStartpos - add creatures AFTER random obstacles are generated
|
|
//TODO: parse once to some structure
|
|
std::vector<std::vector<int>> looseFormations[2];
|
|
std::vector<std::vector<int>> tightFormations[2];
|
|
std::vector<std::vector<int>> creBankFormations[2];
|
|
std::vector<int> commanderField;
|
|
std::vector<int> commanderBank;
|
|
const JsonNode config(ResourceID("config/battleStartpos.json"));
|
|
const JsonVector &positions = config["battle_positions"].Vector();
|
|
|
|
CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]);
|
|
CGH::readBattlePositions(positions[1]["levels"], looseFormations[1]);
|
|
CGH::readBattlePositions(positions[2]["levels"], tightFormations[0]);
|
|
CGH::readBattlePositions(positions[3]["levels"], tightFormations[1]);
|
|
CGH::readBattlePositions(positions[4]["levels"], creBankFormations[0]);
|
|
CGH::readBattlePositions(positions[5]["levels"], creBankFormations[1]);
|
|
|
|
for (auto position : config["commanderPositions"]["field"].Vector())
|
|
{
|
|
commanderField.push_back(static_cast<int>(position.Float()));
|
|
}
|
|
for (auto position : config["commanderPositions"]["creBank"].Vector())
|
|
{
|
|
commanderBank.push_back(static_cast<int>(position.Float()));
|
|
}
|
|
|
|
|
|
//adding war machines
|
|
if(!creatureBank)
|
|
{
|
|
//Checks if hero has artifact and create appropriate stack
|
|
auto handleWarMachine = [&](int side, const ArtifactPosition & artslot, BattleHex hex)
|
|
{
|
|
const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot);
|
|
|
|
if(nullptr != warMachineArt)
|
|
{
|
|
CreatureID cre = warMachineArt->artType->warMachine;
|
|
|
|
if(cre != CreatureID::NONE)
|
|
curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex);
|
|
}
|
|
};
|
|
|
|
if(heroes[0])
|
|
{
|
|
|
|
handleWarMachine(0, ArtifactPosition::MACH1, 52);
|
|
handleWarMachine(0, ArtifactPosition::MACH2, 18);
|
|
handleWarMachine(0, ArtifactPosition::MACH3, 154);
|
|
if(town && town->hasFort())
|
|
handleWarMachine(0, ArtifactPosition::MACH4, 120);
|
|
}
|
|
|
|
if(heroes[1])
|
|
{
|
|
if(!town) //defending hero shouldn't receive ballista (bug #551)
|
|
handleWarMachine(1, ArtifactPosition::MACH1, 66);
|
|
handleWarMachine(1, ArtifactPosition::MACH2, 32);
|
|
handleWarMachine(1, ArtifactPosition::MACH3, 168);
|
|
}
|
|
}
|
|
//war machines added
|
|
|
|
//battleStartpos read
|
|
for(int side = 0; side < 2; side++)
|
|
{
|
|
int formationNo = armies[side]->stacksCount() - 1;
|
|
vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1);
|
|
|
|
int k = 0; //stack serial
|
|
for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++)
|
|
{
|
|
std::vector<int> *formationVector = nullptr;
|
|
if(creatureBank)
|
|
formationVector = &creBankFormations[side][formationNo];
|
|
else if(armies[side]->formation)
|
|
formationVector = &tightFormations[side][formationNo];
|
|
else
|
|
formationVector = &looseFormations[side][formationNo];
|
|
|
|
BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0);
|
|
if(creatureBank && i->second->type->isDoubleWide())
|
|
pos += side ? BattleHex::LEFT : BattleHex::RIGHT;
|
|
|
|
curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos);
|
|
}
|
|
}
|
|
|
|
//adding commanders
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive)
|
|
{
|
|
curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]);
|
|
}
|
|
|
|
}
|
|
|
|
if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL)
|
|
{
|
|
// keep tower
|
|
curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER);
|
|
|
|
if (curB->town->fortLevel() >= CGTownInstance::CASTLE)
|
|
{
|
|
// lower tower + upper tower
|
|
curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER);
|
|
|
|
curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER);
|
|
}
|
|
|
|
//moat
|
|
auto moat = std::make_shared<MoatObstacle>();
|
|
moat->ID = curB->town->subID;
|
|
moat->obstacleType = CObstacleInstance::MOAT;
|
|
moat->uniqueID = static_cast<si32>(curB->obstacles.size());
|
|
curB->obstacles.push_back(moat);
|
|
}
|
|
|
|
std::stable_sort(stacks.begin(),stacks.end(),cmpst);
|
|
|
|
auto neutral = std::make_shared<CreatureAlignmentLimiter>(EAlignment::NEUTRAL);
|
|
auto good = std::make_shared<CreatureAlignmentLimiter>(EAlignment::GOOD);
|
|
auto evil = std::make_shared<CreatureAlignmentLimiter>(EAlignment::EVIL);
|
|
|
|
const auto * bgInfo = VLC->battlefields()->getById(battlefieldType);
|
|
|
|
for(const std::shared_ptr<Bonus> & bonus : bgInfo->bonuses)
|
|
{
|
|
curB->addNewBonus(bonus);
|
|
}
|
|
|
|
//native terrain bonuses
|
|
static auto nativeTerrain = std::make_shared<CreatureTerrainLimiter>();
|
|
|
|
curB->addNewBonus(std::make_shared<Bonus>(Bonus::ONE_BATTLE, Bonus::STACKS_SPEED, Bonus::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain));
|
|
curB->addNewBonus(std::make_shared<Bonus>(Bonus::ONE_BATTLE, Bonus::PRIMARY_SKILL, Bonus::TERRAIN_NATIVE, 1, 0, PrimarySkill::ATTACK)->addLimiter(nativeTerrain));
|
|
curB->addNewBonus(std::make_shared<Bonus>(Bonus::ONE_BATTLE, Bonus::PRIMARY_SKILL, Bonus::TERRAIN_NATIVE, 1, 0, PrimarySkill::DEFENSE)->addLimiter(nativeTerrain));
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
//tactics
|
|
bool isTacticsAllowed = !creatureBank; //no tactics in creature banks
|
|
|
|
constexpr int sideSize = 2;
|
|
|
|
std::array<int, sideSize> battleRepositionHex = {};
|
|
std::array<int, sideSize> battleRepositionHexBlock = {};
|
|
for(int i = 0; i < sideSize; i++)
|
|
{
|
|
if(heroes[i])
|
|
{
|
|
battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(Bonus::BEFORE_BATTLE_REPOSITION));
|
|
battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(Bonus::BEFORE_BATTLE_REPOSITION_BLOCK));
|
|
}
|
|
}
|
|
int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER];
|
|
int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER];
|
|
|
|
/* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides
|
|
when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this
|
|
will not happen because tactics block opposite tactics on same value.
|
|
TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if
|
|
double tactics will be implemented.
|
|
*/
|
|
|
|
if(isTacticsAllowed)
|
|
{
|
|
if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0)
|
|
logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
|
|
if(tacticsSkillDiffAttacker > 0)
|
|
{
|
|
curB->tacticsSide = BattleSide::ATTACKER;
|
|
//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
|
|
curB->tacticDistance = 1 + tacticsSkillDiffAttacker;
|
|
}
|
|
else if(tacticsSkillDiffDefender > 0)
|
|
{
|
|
curB->tacticsSide = BattleSide::DEFENDER;
|
|
//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
|
|
curB->tacticDistance = 1 + tacticsSkillDiffDefender;
|
|
}
|
|
else
|
|
curB->tacticDistance = 0;
|
|
}
|
|
|
|
return curB;
|
|
}
|
|
|
|
const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const
|
|
{
|
|
for(const auto & side : sides)
|
|
if(side.color == player)
|
|
return side.hero;
|
|
|
|
logGlobal->error("Player %s is not in battle!", player.getStr());
|
|
return nullptr;
|
|
}
|
|
|
|
ui8 BattleInfo::whatSide(const PlayerColor & player) const
|
|
{
|
|
for(int i = 0; i < sides.size(); i++)
|
|
if(sides[i].color == player)
|
|
return i;
|
|
|
|
logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.getStr());
|
|
return -1;
|
|
}
|
|
|
|
CStack * BattleInfo::getStack(int stackID, bool onlyAlive)
|
|
{
|
|
return const_cast<CStack *>(battleGetStackByID(stackID, onlyAlive));
|
|
}
|
|
|
|
BattleInfo::BattleInfo():
|
|
round(-1),
|
|
activeStack(-1),
|
|
town(nullptr),
|
|
tile(-1,-1,-1),
|
|
battlefieldType(BattleField::NONE),
|
|
tacticsSide(0),
|
|
tacticDistance(0)
|
|
{
|
|
setBattle(this);
|
|
setNodeType(BATTLE);
|
|
}
|
|
|
|
BattleInfo::~BattleInfo() = default;
|
|
|
|
int32_t BattleInfo::getActiveStackID() const
|
|
{
|
|
return activeStack;
|
|
}
|
|
|
|
TStacks BattleInfo::getStacksIf(TStackFilter predicate) const
|
|
{
|
|
TStacks ret;
|
|
vstd::copy_if(stacks, std::back_inserter(ret), predicate);
|
|
return ret;
|
|
}
|
|
|
|
battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const
|
|
{
|
|
battle::Units ret;
|
|
vstd::copy_if(stacks, std::back_inserter(ret), predicate);
|
|
return ret;
|
|
}
|
|
|
|
|
|
BattleField BattleInfo::getBattlefieldType() const
|
|
{
|
|
return battlefieldType;
|
|
}
|
|
|
|
TerrainId BattleInfo::getTerrainType() const
|
|
{
|
|
return terrainType;
|
|
}
|
|
|
|
IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const
|
|
{
|
|
ObstacleCList ret;
|
|
|
|
for(const auto & obstacle : obstacles)
|
|
ret.push_back(obstacle);
|
|
|
|
return ret;
|
|
}
|
|
|
|
PlayerColor BattleInfo::getSidePlayer(ui8 side) const
|
|
{
|
|
return sides.at(side).color;
|
|
}
|
|
|
|
const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const
|
|
{
|
|
return sides.at(side).armyObject;
|
|
}
|
|
|
|
const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const
|
|
{
|
|
return sides.at(side).hero;
|
|
}
|
|
|
|
ui8 BattleInfo::getTacticDist() const
|
|
{
|
|
return tacticDistance;
|
|
}
|
|
|
|
ui8 BattleInfo::getTacticsSide() const
|
|
{
|
|
return tacticsSide;
|
|
}
|
|
|
|
const CGTownInstance * BattleInfo::getDefendedTown() const
|
|
{
|
|
return town;
|
|
}
|
|
|
|
EWallState BattleInfo::getWallState(EWallPart partOfWall) const
|
|
{
|
|
return si.wallState.at(partOfWall);
|
|
}
|
|
|
|
EGateState BattleInfo::getGateState() const
|
|
{
|
|
return si.gateState;
|
|
}
|
|
|
|
uint32_t BattleInfo::getCastSpells(ui8 side) const
|
|
{
|
|
return sides.at(side).castSpellsCount;
|
|
}
|
|
|
|
int32_t BattleInfo::getEnchanterCounter(ui8 side) const
|
|
{
|
|
return sides.at(side).enchanterCounter;
|
|
}
|
|
|
|
const IBonusBearer * BattleInfo::asBearer() const
|
|
{
|
|
return this;
|
|
}
|
|
|
|
int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const
|
|
{
|
|
if(damage.first != damage.second)
|
|
{
|
|
int64_t sum = 0;
|
|
|
|
auto howManyToAv = std::min<int32_t>(10, attackerCount);
|
|
auto rangeGen = rng.getInt64Range(damage.first, damage.second);
|
|
|
|
for(int32_t g = 0; g < howManyToAv; ++g)
|
|
sum += rangeGen();
|
|
|
|
return sum / howManyToAv;
|
|
}
|
|
else
|
|
{
|
|
return damage.first;
|
|
}
|
|
}
|
|
|
|
void BattleInfo::nextRound(int32_t roundNr)
|
|
{
|
|
for(int i = 0; i < 2; ++i)
|
|
{
|
|
sides.at(i).castSpellsCount = 0;
|
|
vstd::amax(--sides.at(i).enchanterCounter, 0);
|
|
}
|
|
round = roundNr;
|
|
|
|
for(CStack * s : stacks)
|
|
{
|
|
// new turn effects
|
|
s->reduceBonusDurations(Bonus::NTurns);
|
|
|
|
s->afterNewRound();
|
|
}
|
|
|
|
for(auto & obst : obstacles)
|
|
obst->battleTurnPassed();
|
|
}
|
|
|
|
void BattleInfo::nextTurn(uint32_t unitId)
|
|
{
|
|
activeStack = unitId;
|
|
|
|
CStack * st = getStack(activeStack);
|
|
|
|
//remove bonuses that last until when stack gets new turn
|
|
st->removeBonusesRecursive(Bonus::UntilGetsTurn);
|
|
|
|
st->afterGetsTurn();
|
|
}
|
|
|
|
void BattleInfo::addUnit(uint32_t id, const JsonNode & data)
|
|
{
|
|
battle::UnitInfo info;
|
|
info.load(id, data);
|
|
CStackBasicDescriptor base(info.type, info.count);
|
|
|
|
PlayerColor owner = getSidePlayer(info.side);
|
|
|
|
auto * ret = new CStack(&base, owner, info.id, info.side, SlotID::SUMMONED_SLOT_PLACEHOLDER);
|
|
ret->initialPosition = info.position;
|
|
stacks.push_back(ret);
|
|
ret->localInit(this);
|
|
ret->summoned = info.summoned;
|
|
}
|
|
|
|
void BattleInfo::moveUnit(uint32_t id, BattleHex destination)
|
|
{
|
|
auto * sta = getStack(id);
|
|
if(!sta)
|
|
{
|
|
logGlobal->error("Cannot find stack %d", id);
|
|
return;
|
|
}
|
|
sta->position = destination;
|
|
}
|
|
|
|
void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta)
|
|
{
|
|
CStack * changedStack = getStack(id, false);
|
|
if(!changedStack)
|
|
throw std::runtime_error("Invalid unit id in BattleInfo update");
|
|
|
|
if(!changedStack->alive() && healthDelta > 0)
|
|
{
|
|
//checking if we resurrect a stack that is under a living stack
|
|
auto accessibility = getAccesibility();
|
|
|
|
if(!accessibility.accessible(changedStack->getPosition(), changedStack))
|
|
{
|
|
logNetwork->error("Cannot resurrect %s because hex %d is occupied!", changedStack->nodeName(), changedStack->getPosition().hex);
|
|
return; //position is already occupied
|
|
}
|
|
}
|
|
|
|
bool killed = (-healthDelta) >= changedStack->getAvailableHealth();//todo: check using alive state once rebirth will be handled separately
|
|
|
|
bool resurrected = !changedStack->alive() && healthDelta > 0;
|
|
|
|
//applying changes
|
|
changedStack->load(data);
|
|
|
|
|
|
if(healthDelta < 0)
|
|
{
|
|
changedStack->removeBonusesRecursive(Bonus::UntilBeingAttacked);
|
|
}
|
|
|
|
resurrected = resurrected || (killed && changedStack->alive());
|
|
|
|
if(killed)
|
|
{
|
|
if(changedStack->cloneID >= 0)
|
|
{
|
|
//remove clone as well
|
|
CStack * clone = getStack(changedStack->cloneID);
|
|
if(clone)
|
|
clone->makeGhost();
|
|
|
|
changedStack->cloneID = -1;
|
|
}
|
|
}
|
|
|
|
if(resurrected || killed)
|
|
{
|
|
//removing all spells effects
|
|
auto selector = [](const Bonus * b)
|
|
{
|
|
//Special case: DISRUPTING_RAY is absolutely permanent
|
|
return b->source == Bonus::SPELL_EFFECT && b->sid != SpellID::DISRUPTING_RAY;
|
|
};
|
|
changedStack->removeBonusesRecursive(selector);
|
|
}
|
|
|
|
if(!changedStack->alive() && changedStack->isClone())
|
|
{
|
|
for(CStack * s : stacks)
|
|
{
|
|
if(s->cloneID == changedStack->unitId())
|
|
s->cloneID = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void BattleInfo::removeUnit(uint32_t id)
|
|
{
|
|
std::set<uint32_t> ids;
|
|
ids.insert(id);
|
|
|
|
while(!ids.empty())
|
|
{
|
|
auto toRemoveId = *ids.begin();
|
|
auto * toRemove = getStack(toRemoveId, false);
|
|
|
|
if(!toRemove)
|
|
{
|
|
logGlobal->error("Cannot find stack %d", toRemoveId);
|
|
return;
|
|
}
|
|
|
|
if(!toRemove->ghost)
|
|
{
|
|
toRemove->onRemoved();
|
|
toRemove->detachFromAll();
|
|
|
|
//stack may be removed instantly (not being killed first)
|
|
//handle clone remove also here
|
|
if(toRemove->cloneID >= 0)
|
|
{
|
|
ids.insert(toRemove->cloneID);
|
|
toRemove->cloneID = -1;
|
|
}
|
|
|
|
//cleanup remaining clone links if any
|
|
for(auto * s : stacks)
|
|
{
|
|
if(s->cloneID == toRemoveId)
|
|
s->cloneID = -1;
|
|
}
|
|
}
|
|
|
|
ids.erase(toRemoveId);
|
|
}
|
|
}
|
|
|
|
void BattleInfo::updateUnit(uint32_t id, const JsonNode & data)
|
|
{
|
|
//TODO
|
|
}
|
|
|
|
void BattleInfo::addUnitBonus(uint32_t id, const std::vector<Bonus> & bonus)
|
|
{
|
|
CStack * sta = getStack(id, false);
|
|
|
|
if(!sta)
|
|
{
|
|
logGlobal->error("Cannot find stack %d", id);
|
|
return;
|
|
}
|
|
|
|
for(const Bonus & b : bonus)
|
|
addOrUpdateUnitBonus(sta, b, true);
|
|
}
|
|
|
|
void BattleInfo::updateUnitBonus(uint32_t id, const std::vector<Bonus> & bonus)
|
|
{
|
|
CStack * sta = getStack(id, false);
|
|
|
|
if(!sta)
|
|
{
|
|
logGlobal->error("Cannot find stack %d", id);
|
|
return;
|
|
}
|
|
|
|
for(const Bonus & b : bonus)
|
|
addOrUpdateUnitBonus(sta, b, false);
|
|
}
|
|
|
|
void BattleInfo::removeUnitBonus(uint32_t id, const std::vector<Bonus> & bonus)
|
|
{
|
|
CStack * sta = getStack(id, false);
|
|
|
|
if(!sta)
|
|
{
|
|
logGlobal->error("Cannot find stack %d", id);
|
|
return;
|
|
}
|
|
|
|
for(const Bonus & one : bonus)
|
|
{
|
|
auto selector = [one](const Bonus * b)
|
|
{
|
|
//compare everything but turnsRemain, limiter and propagator
|
|
return one.duration == b->duration
|
|
&& one.type == b->type
|
|
&& one.subtype == b->subtype
|
|
&& one.source == b->source
|
|
&& one.val == b->val
|
|
&& one.sid == b->sid
|
|
&& one.valType == b->valType
|
|
&& one.additionalInfo == b->additionalInfo
|
|
&& one.effectRange == b->effectRange
|
|
&& one.description == b->description;
|
|
};
|
|
sta->removeBonusesRecursive(selector);
|
|
}
|
|
}
|
|
|
|
uint32_t BattleInfo::nextUnitId() const
|
|
{
|
|
return static_cast<uint32_t>(stacks.size());
|
|
}
|
|
|
|
void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd)
|
|
{
|
|
if(forceAdd || !sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, value.sid).And(Selector::typeSubtype(value.type, value.subtype))))
|
|
{
|
|
//no such effect or cumulative - add new
|
|
logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description());
|
|
sta->addNewBonus(std::make_shared<Bonus>(value));
|
|
}
|
|
else
|
|
{
|
|
logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description());
|
|
|
|
for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize
|
|
{
|
|
if(stackBonus->source == value.source && stackBonus->sid == value.sid && stackBonus->type == value.type && stackBonus->subtype == value.subtype)
|
|
{
|
|
stackBonus->turnsRemain = std::max(stackBonus->turnsRemain, value.turnsRemain);
|
|
}
|
|
}
|
|
CBonusSystemNode::treeHasChanged();
|
|
}
|
|
}
|
|
|
|
void BattleInfo::setWallState(EWallPart partOfWall, EWallState state)
|
|
{
|
|
si.wallState[partOfWall] = state;
|
|
}
|
|
|
|
void BattleInfo::addObstacle(const ObstacleChanges & changes)
|
|
{
|
|
std::shared_ptr<SpellCreatedObstacle> obstacle = std::make_shared<SpellCreatedObstacle>();
|
|
obstacle->fromInfo(changes);
|
|
obstacles.push_back(obstacle);
|
|
}
|
|
|
|
void BattleInfo::updateObstacle(const ObstacleChanges& changes)
|
|
{
|
|
std::shared_ptr<SpellCreatedObstacle> changedObstacle = std::make_shared<SpellCreatedObstacle>();
|
|
changedObstacle->fromInfo(changes);
|
|
|
|
for(auto & obstacle : obstacles)
|
|
{
|
|
if(obstacle->uniqueID == changes.id) // update this obstacle
|
|
{
|
|
auto * spellObstacle = dynamic_cast<SpellCreatedObstacle *>(obstacle.get());
|
|
assert(spellObstacle);
|
|
|
|
// Currently we only support to update the "revealed" property
|
|
spellObstacle->revealed = changedObstacle->revealed;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void BattleInfo::removeObstacle(uint32_t id)
|
|
{
|
|
for(int i=0; i < obstacles.size(); ++i)
|
|
{
|
|
if(obstacles[i]->uniqueID == id) //remove this obstacle
|
|
{
|
|
obstacles.erase(obstacles.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const
|
|
{
|
|
return const_cast<CArmedInstance*>(CBattleInfoEssentials::battleGetArmyObject(side));
|
|
}
|
|
|
|
CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const
|
|
{
|
|
return const_cast<CGHeroInstance*>(CBattleInfoEssentials::battleGetFightingHero(side));
|
|
}
|
|
|
|
#if SCRIPTING_ENABLED
|
|
scripting::Pool * BattleInfo::getContextPool() const
|
|
{
|
|
//this is real battle, use global scripting context pool
|
|
//TODO: make this line not ugly
|
|
return IObjectInterface::cb->getGlobalContextPool();
|
|
}
|
|
#endif
|
|
|
|
bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const
|
|
{
|
|
switch(phase)
|
|
{
|
|
case 0: //catapult moves after turrets
|
|
return a->creatureIndex() > b->creatureIndex(); //catapult is 145 and turrets are 149
|
|
case 1:
|
|
case 2:
|
|
case 3:
|
|
{
|
|
int as = a->getInitiative(turn);
|
|
int bs = b->getInitiative(turn);
|
|
|
|
if(as != bs)
|
|
return as > bs;
|
|
|
|
if(a->unitSide() == b->unitSide())
|
|
return a->unitSlot() < b->unitSlot();
|
|
|
|
return (a->unitSide() == side || b->unitSide() == side)
|
|
? a->unitSide() != side
|
|
: a->unitSide() < b->unitSide();
|
|
}
|
|
default:
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side):
|
|
phase(Phase),
|
|
turn(Turn),
|
|
side(Side)
|
|
{
|
|
}
|
|
|
|
VCMI_LIB_NAMESPACE_END
|