#include "StdInc.h" #include "CBattleCallback.h" #include "BattleState.h" #include "CGameState.h" #include "NetPacks.h" #include "spells/CSpellHandler.h" #include "VCMI_Lib.h" #include "CTownHandler.h" /* * CBattleCallback.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 * */ #define RETURN_IF_NOT_BATTLE(X) if(!duringBattle()) {logGlobal->errorStream() << __FUNCTION__ << " called when no battle!"; return X; } namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO { static void retreiveTurretDamageRange(const CGTownInstance * town, const CStack *turret, double &outMinDmg, double &outMaxDmg) { assert(turret->getCreature()->idNumber == CreatureID::ARROW_TOWERS); assert(town); assert(turret->position >= -4 && turret->position <= -2); float multiplier = (turret->position == -2) ? 1 : 0.5; int baseMin = 6; int baseMax = 10; outMinDmg = multiplier * (baseMin + town->getTownLevel() * 2); outMaxDmg = multiplier * (baseMax + town->getTownLevel() * 3); } static BattleHex lineToWallHex(int line) //returns hex with wall in given line (y coordinate) { static const BattleHex lineToHex[] = {12, 29, 45, 62, 78, 95, 112, 130, 147, 165, 182}; return lineToHex[line]; } static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) { const int wallInStackLine = lineToWallHex(pos1.getY()); const int wallInDestLine = lineToWallHex(pos2.getY()); const bool stackLeft = pos1 < wallInStackLine; const bool destLeft = pos2 < wallInDestLine; return stackLeft != destLeft; } // parts of wall static const std::pair wallParts[] = { std::make_pair(50, EWallPart::KEEP), std::make_pair(183, EWallPart::BOTTOM_TOWER), std::make_pair(182, EWallPart::BOTTOM_WALL), std::make_pair(130, EWallPart::BELOW_GATE), std::make_pair(62, EWallPart::OVER_GATE), std::make_pair(29, EWallPart::UPPER_WALL), std::make_pair(12, EWallPart::UPPER_TOWER), std::make_pair(95, EWallPart::INDESTRUCTIBLE_PART_OF_GATE), std::make_pair(96, EWallPart::GATE), std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), std::make_pair(78, EWallPart::INDESTRUCTIBLE_PART), std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART) }; static EWallPart::EWallPart hexToWallPart(BattleHex hex) { for(auto & elem : wallParts) { if(elem.first == hex) return elem.second; } return EWallPart::INVALID; //not found! } static BattleHex WallPartToHex(EWallPart::EWallPart part) { for(auto & elem : wallParts) { if(elem.second == part) return elem.first; } return BattleHex::INVALID; //not found! } } using namespace SiegeStuffThatShouldBeMovedToHandlers; boost::shared_mutex& CCallbackBase::getGsMutex() { return *gs->mx; } bool CCallbackBase::duringBattle() const { return getBattle() != nullptr; } void CCallbackBase::setBattle(const BattleInfo *B) { battle = B; } boost::optional CCallbackBase::getPlayerID() const { return player; } ETerrainType CBattleInfoEssentials::battleTerrainType() const { RETURN_IF_NOT_BATTLE(ETerrainType::WRONG); return getBattle()->terrainType; } BFieldType CBattleInfoEssentials::battleGetBattlefieldType() const { RETURN_IF_NOT_BATTLE(BFieldType::NONE); return getBattle()->battlefieldType; } std::vector > CBattleInfoEssentials::battleGetAllObstacles(boost::optional perspective /*= boost::none*/) const { std::vector > ret; RETURN_IF_NOT_BATTLE(ret); if(!perspective) { //if no particular perspective request, use default one perspective = battleGetMySide(); } else { if(!!player && *perspective != battleGetMySide()) { logGlobal->errorStream() << "Unauthorized access attempt!"; assert(0); //I want to notice if that happens //perspective = battleGetMySide(); } } for(auto oi : getBattle()->obstacles) { if(getBattle()->battleIsObstacleVisibleForSide(*oi, *perspective)) ret.push_back(oi); } return ret; } bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const { RETURN_IF_NOT_BATTLE(false); return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); } bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const { RETURN_IF_NOT_BATTLE(false); for(const CStack *s : battleGetAllStacks()) { if(s->attackerOwned == !side && s->getCreature()->isItNativeTerrain(getBattle()->terrainType)) return true; } return false; } TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets /*= false*/) const { return battleGetStacksIf([](const CStack * s){return true;},includeTurrets); } TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate, bool includeTurrets /*= false*/) const { TStacks ret; RETURN_IF_NOT_BATTLE(ret); vstd::copy_if(getBattle()->stacks, std::back_inserter(ret), [=](const CStack * s){ return predicate(s) && (includeTurrets || !(s->type->idNumber == CreatureID::ARROW_TOWERS)); }); return ret; } TStacks CBattleInfoEssentials::battleAliveStacks() const { return battleGetStacksIf([](const CStack * s){ return s->alive(); }); } TStacks CBattleInfoEssentials::battleAliveStacks(ui8 side) const { return battleGetStacksIf([=](const CStack * s){ return s->alive() && s->attackerOwned == !side; }); } int CBattleInfoEssentials::battleGetMoatDmg() const { RETURN_IF_NOT_BATTLE(0); auto town = getBattle()->town; if(!town) return 0; return town->town->moatDamage; } const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const { RETURN_IF_NOT_BATTLE(nullptr); if(!getBattle() || getBattle()->town == nullptr) return nullptr; return getBattle()->town; } BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const { RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); if(!player) return BattlePerspective::ALL_KNOWING; if(*player == getBattle()->sides[0].color) return BattlePerspective::LEFT_SIDE; if(*player == getBattle()->sides[1].color) return BattlePerspective::RIGHT_SIDE; logGlobal->errorStream() << "Cannot find player " << *player << " in battle!"; return BattlePerspective::INVALID; } const CStack * CBattleInfoEssentials::battleActiveStack() const { RETURN_IF_NOT_BATTLE(nullptr); return battleGetStackByID(getBattle()->activeStack); } const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const { RETURN_IF_NOT_BATTLE(nullptr); for(auto s : battleGetAllStacks(true)) if(s->ID == ID && (!onlyAlive || s->alive())) return s; return nullptr; } bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const { RETURN_IF_NOT_BATTLE(false); auto p = battleGetMySide(); return p == BattlePerspective::ALL_KNOWING || p == side; } si8 CBattleInfoEssentials::battleTacticDist() const { RETURN_IF_NOT_BATTLE(0); return getBattle()->tacticDistance; } si8 CBattleInfoEssentials::battleGetTacticsSide() const { RETURN_IF_NOT_BATTLE(-1); return getBattle()->tacticsSide; } const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const { RETURN_IF_NOT_BATTLE(nullptr); if(side > 1) { logGlobal->errorStream() << "FIXME: " << __FUNCTION__ << " wrong argument!"; return nullptr; } if(!battleDoWeKnowAbout(side)) { logGlobal->errorStream() << "FIXME: " << __FUNCTION__ << " access check "; return nullptr; } return getBattle()->sides[side].hero; } const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const { RETURN_IF_NOT_BATTLE(nullptr); if(side > 1) { logGlobal->errorStream() << "FIXME: " << __FUNCTION__ << " wrong argument!"; return nullptr; } if(!battleDoWeKnowAbout(side)) { logGlobal->errorStream() << "FIXME: " << __FUNCTION__ << " access check "; return nullptr; } return getBattle()->sides[side].armyObject; } InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo( ui8 side ) const { auto hero = getBattle()->sides[side].hero; if(!hero) { logGlobal->warnStream() << __FUNCTION__ << ": side " << (int)side << " does not have hero!"; return InfoAboutHero(); } return InfoAboutHero(hero, battleDoWeKnowAbout(side)); } int CBattleInfoEssentials::battleCastSpells(ui8 side) const { RETURN_IF_NOT_BATTLE(-1); return getBattle()->sides[side].castSpellsCount; } ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(PlayerColor player, ECastingMode::ECastingMode mode) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); const ui8 side = playerToSide(player); if(!battleDoWeKnowAbout(side)) { logGlobal->warnStream() << "You can't check if enemy can cast given spell!"; return ESpellCastProblem::INVALID; } switch (mode) { case ECastingMode::HERO_CASTING: { if(battleTacticDist()) return ESpellCastProblem::ONGOING_TACTIC_PHASE; if(battleCastSpells(side) > 0) return ESpellCastProblem::ALREADY_CASTED_THIS_TURN; auto hero = battleGetFightingHero(side); if(!hero) return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; if(!hero->getArt(ArtifactPosition::SPELLBOOK)) return ESpellCastProblem::NO_SPELLBOOK; if(hero->hasBonusOfType(Bonus::BLOCK_ALL_MAGIC)) return ESpellCastProblem::MAGIC_IS_BLOCKED; } break; default: break; } return ESpellCastProblem::OK; } bool CBattleInfoEssentials::battleCanFlee(PlayerColor player) const { RETURN_IF_NOT_BATTLE(false); ui8 mySide = playerToSide(player); const CGHeroInstance *myHero = battleGetFightingHero(mySide); //current player have no hero if(!myHero) return false; //eg. one of heroes is wearing shakles of war if(myHero->hasBonusOfType(Bonus::BATTLE_NO_FLEEING)) return false; //we are besieged defender if(mySide == BattleSide::DEFENDER && battleGetSiegeLevel()) { auto town = battleGetDefendedTown(); if(!town->hasBuilt(BuildingID::ESCAPE_TUNNEL, ETownType::STRONGHOLD)) return false; } return true; } ui8 CBattleInfoEssentials::playerToSide(PlayerColor player) const { RETURN_IF_NOT_BATTLE(-1); int ret = vstd::find_pos_if(getBattle()->sides, [=](const SideInBattle &side){ return side.color == player; }); if(ret < 0) logGlobal->warnStream() << "Cannot find side for player " << player; return ret; } ui8 CBattleInfoEssentials::battleGetSiegeLevel() const { RETURN_IF_NOT_BATTLE(0); return getBattle()->town ? getBattle()->town->fortLevel() : CGTownInstance::NONE; } bool CBattleInfoEssentials::battleCanSurrender(PlayerColor player) const { RETURN_IF_NOT_BATTLE(false); //conditions like for fleeing + enemy must have a hero return battleCanFlee(player) && battleHasHero(!playerToSide(player)); } bool CBattleInfoEssentials::battleHasHero(ui8 side) const { RETURN_IF_NOT_BATTLE(false); assert(side < 2); return getBattle()->sides[side].hero; } si8 CBattleInfoEssentials::battleGetWallState(int partOfWall) const { RETURN_IF_NOT_BATTLE(0); if(getBattle()->town == nullptr || getBattle()->town->fortLevel() == CGTownInstance::NONE) return EWallState::NONE; assert(partOfWall >= 0 && partOfWall < EWallPart::PARTS_COUNT); return getBattle()->si.wallState[partOfWall]; } si8 CBattleInfoCallback::battleHasWallPenalty( const CStack * stack, BattleHex destHex ) const { return battleHasWallPenalty(stack, stack->position, destHex); } si8 CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer *bonusBearer, BattleHex shooterPosition, BattleHex destHex) const { RETURN_IF_NOT_BATTLE(false); if (!battleGetSiegeLevel() || bonusBearer->hasBonusOfType(Bonus::NO_WALL_PENALTY)) return false; const int wallInStackLine = lineToWallHex(shooterPosition.getY()); const int wallInDestLine = lineToWallHex(destHex.getY()); const bool stackLeft = shooterPosition < wallInStackLine; const bool destRight = destHex > wallInDestLine; if (stackLeft && destRight) //shooting from outside to inside { int row = (shooterPosition + destHex) / (2 * GameConstants::BFIELD_WIDTH); if (shooterPosition > destHex && ((destHex % GameConstants::BFIELD_WIDTH - shooterPosition % GameConstants::BFIELD_WIDTH) < 2)) //shooting up high row -= 2; const int wallPos = lineToWallHex(row); if (!isWallPartPotentiallyAttackable(battleHexToWallPart(wallPos))) return true; } return false; } si8 CBattleInfoCallback::battleCanTeleportTo(const CStack * stack, BattleHex destHex, int telportLevel) const { RETURN_IF_NOT_BATTLE(false); if (!getAccesibility(stack).accessible(destHex, stack)) return false; if (battleGetSiegeLevel() && telportLevel < 2) //check for wall return sameSideOfWall(stack->position, destHex); return true; } std::set CBattleInfoCallback::battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos /*= BattleHex::INVALID*/) const { std::set attackedHexes; RETURN_IF_NOT_BATTLE(attackedHexes); AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); for (BattleHex tile : at.hostileCreaturePositions) { const CStack * st = battleGetStackByPos(tile, true); if(st && st->owner != attacker->owner) //only hostile stacks - does it work well with Berserk? { attackedHexes.insert(tile); } } for (BattleHex tile : at.friendlyCreaturePositions) { if(battleGetStackByPos(tile, true)) //friendly stacks can also be damaged by Dragon Breath { attackedHexes.insert(tile); } } return attackedHexes; } SpellID CBattleInfoCallback::battleGetRandomStackSpell(const CStack * stack, ERandomSpell mode) const { switch (mode) { case RANDOM_GENIE: return getRandomBeneficialSpell(stack); //target break; case RANDOM_AIMED: return getRandomCastedSpell(stack); //caster break; default: logGlobal->errorStream() << "Incorrect mode of battleGetRandomSpell (" << mode <<")"; return SpellID::NONE; } } const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const { RETURN_IF_NOT_BATTLE(nullptr); for(auto s : battleGetAllStacks(true)) if(vstd::contains(s->getHexes(), pos) && (!onlyAlive || s->alive())) return s; return nullptr; } void CBattleInfoCallback::battleGetStackQueue(std::vector &out, const int howMany, const int turn /*= 0*/, int lastMoved /*= -1*/) const { RETURN_IF_NOT_BATTLE(); //let's define a huge lambda auto takeStack = [&](std::vector &st) -> const CStack* { const CStack *ret = nullptr; unsigned i, //fastest stack j=0; //fastest stack of the other side for(i = 0; i < st.size(); i++) if(st[i]) break; //no stacks left if(i == st.size()) return nullptr; const CStack *fastest = st[i], *other = nullptr; int bestSpeed = fastest->Speed(turn); //FIXME: comparison between bool and integer. Logic does not makes sense either if(fastest->attackerOwned != lastMoved) { ret = fastest; } else { for(j = i + 1; j < st.size(); j++) { if(!st[j]) continue; if(st[j]->attackerOwned != lastMoved || st[j]->Speed(turn) != bestSpeed) break; } if(j >= st.size()) { ret = fastest; } else { other = st[j]; if(other->Speed(turn) != bestSpeed) ret = fastest; else ret = other; } } assert(ret); if(ret == fastest) st[i] = nullptr; else st[j] = nullptr; lastMoved = ret->attackerOwned; return ret; }; //We'll split creatures with remaining movement to 4 buckets // [0] - turrets/catapult, // [1] - normal (unmoved) creatures, other war machines, // [2] - waited cres that had morale, // [3] - rest of waited cres std::vector phase[4]; int toMove = 0; //how many stacks still has move const CStack *active = battleActiveStack(); //active stack hasn't taken any action yet - must be placed at the beginning of queue, no matter what if(!turn && active && active->willMove() && !active->waited()) { out.push_back(active); if(out.size() == howMany) return; } auto allStacks = battleGetAllStacks(true); if(!vstd::contains_if(allStacks, [](const CStack *stack) { return stack->willMove(100000); })) //little evil, but 100000 should be enough for all effects to disappear { //No stack will be able to move, battle is over. out.clear(); return; } for(auto s : battleGetAllStacks(true)) { if((turn <= 0 && !s->willMove()) //we are considering current round and stack won't move || (turn > 0 && !s->canMove(turn)) //stack won't be able to move in later rounds || (turn <= 0 && s == active && out.size() && s == out.front())) //it's active stack already added at the beginning of queue { continue; } int p = -1; //in which phase this tack will move? if(turn <= 0 && s->waited()) //consider waiting state only for ongoing round { if(vstd::contains(s->state, EBattleStackState::HAD_MORALE)) p = 2; else p = 3; } else if(s->getCreature()->idNumber == CreatureID::CATAPULT || s->getCreature()->idNumber == CreatureID::ARROW_TOWERS) //catapult and turrets are first { p = 0; } else { p = 1; } phase[p].push_back(s); toMove++; } for(int i = 0; i < 4; i++) boost::sort(phase[i], CMP_stack(i, turn > 0 ? turn : 0)); for(size_t i = 0; i < phase[0].size() && i < howMany; i++) out.push_back(phase[0][i]); if(out.size() == howMany) return; if(lastMoved == -1) { if(active) { //FIXME: both branches contain same code!!! if(out.size() && out.front() == active) lastMoved = active->attackerOwned; else lastMoved = active->attackerOwned; } else { lastMoved = 0; } } int pi = 1; while(out.size() < howMany) { const CStack *hlp = takeStack(phase[pi]); if(!hlp) { pi++; if(pi > 3) { //if(turn != 2) battleGetStackQueue(out, howMany, turn + 1, lastMoved); return; } } else { out.push_back(hlp); } } } void CBattleInfoCallback::battleGetStackCountOutsideHexes(bool *ac) const { RETURN_IF_NOT_BATTLE(); auto accessibility = getAccesibility(); for(int i = 0; i < accessibility.size(); i++) ac[i] = (accessibility[i] == EAccessibility::ACCESSIBLE); } std::vector CBattleInfoCallback::battleGetAvailableHexes(const CStack * stack, bool addOccupiable, std::vector * attackable) const { std::vector ret; RETURN_IF_NOT_BATTLE(ret); if(!stack->position.isValid()) //turrets return ret; auto reachability = getReachability(stack); for (int i = 0; i < GameConstants::BFIELD_SIZE; ++i) { // If obstacles or other stacks makes movement impossible, it can't be helped. if(!reachability.isReachable(i)) continue; if(battleTacticDist() && battleGetTacticsSide() == !stack->attackerOwned) { //Stack has to perform tactic-phase movement -> can enter any reachable tile within given range if(!isInTacticRange(i)) continue; } else { //Not tactics phase -> destination must be reachable and within stack range. if(reachability.distances[i] > stack->Speed(0, true)) continue; } ret.push_back(i); if(addOccupiable && stack->doubleWide()) { //If two-hex stack can stand on hex i then obviously it can occupy its second hex from that position ret.push_back(stack->occupiedHex(i)); } } if(attackable) { auto meleeAttackable = [&](BattleHex hex) -> bool { // Return true if given hex has at least one available neighbour. // Available hexes are already present in ret vector. auto availableNeighbor = boost::find_if(ret, [=] (BattleHex availableHex) { return BattleHex::mutualPosition(hex, availableHex) >= 0; }); return availableNeighbor != ret.end(); }; for(const CStack * otherSt : battleAliveStacks(stack->attackerOwned)) { if(!otherSt->isValidTarget(false)) continue; std::vector occupied = otherSt->getHexes(); if(battleCanShoot(stack, otherSt->position)) { attackable->insert(attackable->end(), occupied.begin(), occupied.end()); continue; } for(BattleHex he : occupied) { if(meleeAttackable(he)) attackable->push_back(he); } } } //adding occupiable likely adds duplicates to ret -> clean it up boost::sort(ret); ret.erase(boost::unique(ret).end(), ret.end()); return ret; } bool CBattleInfoCallback::battleCanAttack(const CStack * stack, const CStack * target, BattleHex dest) const { RETURN_IF_NOT_BATTLE(false); if(battleTacticDist()) return false; if (!stack || !target) return false; if (stack->owner == target->owner) return false; auto &id = stack->getCreature()->idNumber; if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) return false; if (!target->alive()) return false; return true; } bool CBattleInfoCallback::battleCanShoot(const CStack * stack, BattleHex dest) const { RETURN_IF_NOT_BATTLE(false); if(battleTacticDist()) //no shooting during tactics return false; const CStack *dst = battleGetStackByPos(dest); if(!stack || !dst) return false; if(stack->hasBonusOfType(Bonus::FORGETFULL)) //forgetfulness return false; if(stack->getCreature()->idNumber == CreatureID::CATAPULT && dst) //catapult cannot attack creatures return false; //const CGHeroInstance * stackHero = battleGetOwner(stack); if(stack->hasBonusOfType(Bonus::SHOOTER)//it's shooter && stack->owner != dst->owner && dst->alive() && (!battleIsStackBlocked(stack) || stack->hasBonusOfType(Bonus::FREE_SHOOTING)) && stack->shots ) return true; return false; } TDmgRange CBattleInfoCallback::calculateDmgRange(const CStack* attacker, const CStack* defender, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const { return calculateDmgRange(attacker, defender, attacker->count, shooting, charge, lucky, unlucky, deathBlow, ballistaDoubleDmg); } TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo &info) const { auto battleBonusValue = [&](const IBonusBearer * bearer, CSelector selector) -> int { auto noLimit = Selector::effectRange(Bonus::NO_LIMIT); auto limitMatches = info.shooting ? Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT) : Selector::effectRange(Bonus::ONLY_MELEE_FIGHT); //any regular bonuses or just ones for melee/ranged return bearer->getBonuses(selector, noLimit.Or(limitMatches))->totalValue(); }; double additiveBonus = 1.0, multBonus = 1.0, minDmg = info.attackerBonuses->getMinDamage() * info.attackerCount,//TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT maxDmg = info.attackerBonuses->getMaxDamage() * info.attackerCount; const CCreature *attackerType = info.attacker->getCreature(), *defenderType = info.defender->getCreature(); if(attackerType->idNumber == CreatureID::ARROW_TOWERS) { SiegeStuffThatShouldBeMovedToHandlers::retreiveTurretDamageRange(battleGetDefendedTown(), info.attacker, minDmg, maxDmg); } if(info.attackerBonuses->hasBonusOfType(Bonus::SIEGE_WEAPON) && attackerType->idNumber != CreatureID::ARROW_TOWERS) //any siege weapon, but only ballista can attack (second condition - not arrow turret) { //minDmg and maxDmg are multiplied by hero attack + 1 auto retreiveHeroPrimSkill = [&](int skill) -> int { const Bonus *b = info.attackerBonuses->getBonus(Selector::sourceTypeSel(Bonus::HERO_BASE_SKILL).And(Selector::typeSubtype(Bonus::PRIMARY_SKILL, skill))); return b ? b->val : 0; //if there is no hero or no info on his primary skill, return 0 }; minDmg *= retreiveHeroPrimSkill(PrimarySkill::ATTACK) + 1; maxDmg *= retreiveHeroPrimSkill(PrimarySkill::ATTACK) + 1; } int attackDefenceDifference = 0; double multAttackReduction = (100 - battleBonusValue (info.attackerBonuses, Selector::type(Bonus::GENERAL_ATTACK_REDUCTION))) / 100.0; attackDefenceDifference += battleBonusValue (info.attackerBonuses, Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK)) * multAttackReduction; double multDefenceReduction = (100 - battleBonusValue (info.attackerBonuses, Selector::type(Bonus::ENEMY_DEFENCE_REDUCTION))) / 100.0; attackDefenceDifference -= info.defenderBonuses->Defense() * multDefenceReduction; if(const Bonus *slayerEffect = info.attackerBonuses->getEffect(SpellID::SLAYER)) //slayer handling //TODO: apply only ONLY_MELEE_FIGHT / DISTANCE_FIGHT? { std::vector affectedIds; int spLevel = slayerEffect->val; for(int g = 0; g < VLC->creh->creatures.size(); ++g) { for(const Bonus *b : VLC->creh->creatures[g]->getBonusList()) { if ( (b->type == Bonus::KING3 && spLevel >= 3) || //expert (b->type == Bonus::KING2 && spLevel >= 2) || //adv + (b->type == Bonus::KING1 && spLevel >= 0) ) //none or basic + { affectedIds.push_back(g); break; } } } for(auto & affectedId : affectedIds) { if(defenderType->idNumber == affectedId) { attackDefenceDifference += SpellID(SpellID::SLAYER).toSpell()->getPower(spLevel); break; } } } //bonus from attack/defense skills if(attackDefenceDifference < 0) //decreasing dmg { const double dec = std::min(0.025 * (-attackDefenceDifference), 0.7); multBonus *= 1.0 - dec; } else //increasing dmg { const double inc = std::min(0.05 * attackDefenceDifference, 4.0); additiveBonus += inc; } //applying jousting bonus if( info.attackerBonuses->hasBonusOfType(Bonus::JOUSTING) && !info.defenderBonuses->hasBonusOfType(Bonus::CHARGE_IMMUNITY) ) additiveBonus += info.chargedFields * 0.05; //handling secondary abilities and artifacts giving premies to them if(info.shooting) additiveBonus += info.attackerBonuses->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ARCHERY) / 100.0; else additiveBonus += info.attackerBonuses->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::OFFENCE) / 100.0; if(info.defenderBonuses) multBonus *= (std::max(0, 100 - info.defenderBonuses->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ARMORER))) / 100.0; //handling hate effect additiveBonus += info.attackerBonuses->valOfBonuses(Bonus::HATE, defenderType->idNumber.toEnum()) / 100.; //luck bonus if (info.luckyHit) { additiveBonus += 1.0; } //unlucky hit, used only if negative luck is enabled if (info.unluckyHit) { additiveBonus -= 0.5; // FIXME: how bad (and luck in general) should work with following bonuses? } //ballista double dmg if(info.ballistaDoubleDamage) { additiveBonus += 1.0; } if (info.deathBlow) //Dread Knight and many WoGified creatures { additiveBonus += 1.0; } //handling spell effects if(!info.shooting) //eg. shield { multBonus *= (100 - info.defenderBonuses->valOfBonuses(Bonus::GENERAL_DAMAGE_REDUCTION, 0)) / 100.0; } else if(info.shooting) //eg. air shield { multBonus *= (100 - info.defenderBonuses->valOfBonuses(Bonus::GENERAL_DAMAGE_REDUCTION, 1)) / 100.0; } TBonusListPtr curseEffects = info.attackerBonuses->getBonuses(Selector::type(Bonus::ALWAYS_MINIMUM_DAMAGE)); TBonusListPtr blessEffects = info.attackerBonuses->getBonuses(Selector::type(Bonus::ALWAYS_MAXIMUM_DAMAGE)); int curseBlessAdditiveModifier = blessEffects->totalValue() - curseEffects->totalValue(); double curseMultiplicativePenalty = curseEffects->size() ? (*std::max_element(curseEffects->begin(), curseEffects->end(), &Bonus::compareByAdditionalInfo))->additionalInfo : 0; if(curseMultiplicativePenalty) //curse handling (partial, the rest is below) { multBonus *= 1.0 - curseMultiplicativePenalty/100; } auto isAdvancedAirShield = [](const Bonus *bonus) { return bonus->source == Bonus::SPELL_EFFECT && bonus->sid == SpellID::AIR_SHIELD && bonus->val >= SecSkillLevel::ADVANCED; }; //wall / distance penalty + advanced air shield const bool distPenalty = !info.attackerBonuses->hasBonusOfType(Bonus::NO_DISTANCE_PENALTY) && battleHasDistancePenalty(info.attackerBonuses, info.attackerPosition, info.defenderPosition); const bool obstaclePenalty = battleHasWallPenalty(info.attackerBonuses, info.attackerPosition, info.defenderPosition); if (info.shooting) { if (distPenalty || info.defenderBonuses->hasBonus(isAdvancedAirShield)) { multBonus *= 0.5; } if (obstaclePenalty) { multBonus *= 0.5; //cumulative } } if(!info.shooting && info.attackerBonuses->hasBonusOfType(Bonus::SHOOTER) && !info.attackerBonuses->hasBonusOfType(Bonus::NO_MELEE_PENALTY)) { multBonus *= 0.5; } // TODO attack on petrified unit 50% // psychic elementals versus mind immune units 50% // blinded unit retaliates minDmg *= additiveBonus * multBonus; maxDmg *= additiveBonus * multBonus; TDmgRange returnedVal; if(curseEffects->size()) //curse handling (rest) { minDmg += curseBlessAdditiveModifier; returnedVal = std::make_pair(int(minDmg), int(minDmg)); } else if(blessEffects->size()) //bless handling { maxDmg += curseBlessAdditiveModifier; returnedVal = std::make_pair(int(maxDmg), int(maxDmg)); } else { returnedVal = std::make_pair(int(minDmg), int(maxDmg)); } //damage cannot be less than 1 vstd::amax(returnedVal.first, 1); vstd::amax(returnedVal.second, 1); return returnedVal; } TDmgRange CBattleInfoCallback::calculateDmgRange( const CStack* attacker, const CStack* defender, TQuantity attackerCount, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg ) const { BattleAttackInfo bai(attacker, defender, shooting); bai.attackerCount = attackerCount; bai.chargedFields = charge; bai.luckyHit = lucky; bai.unluckyHit = unlucky; bai.deathBlow = deathBlow; bai.ballistaDoubleDamage = ballistaDoubleDmg; return calculateDmgRange(bai); } TDmgRange CBattleInfoCallback::battleEstimateDamage(const CStack * attacker, const CStack * defender, TDmgRange * retaliationDmg) const { RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); const bool shooting = battleCanShoot(attacker, defender->position); const BattleAttackInfo bai(attacker, defender, shooting); return battleEstimateDamage(bai, retaliationDmg); } std::pair CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo &bai, std::pair * retaliationDmg /*= nullptr*/) const { RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); //const bool shooting = battleCanShoot(bai.attacker, bai.defenderPosition); //TODO handle bonus bearer //const ui8 mySide = !attacker->attackerOwned; TDmgRange ret = calculateDmgRange(bai); if(retaliationDmg) { if(bai.shooting) { retaliationDmg->first = retaliationDmg->second = 0; } else { ui32 TDmgRange::* pairElems[] = {&TDmgRange::first, &TDmgRange::second}; for (int i=0; i<2; ++i) { BattleStackAttacked bsa; bsa.damageAmount = ret.*pairElems[i]; bai.defender->prepareAttacked(bsa, gs->getRandomGenerator(), bai.defenderCount); auto retaliationAttack = bai.reverse(); retaliationAttack.attackerCount = bsa.newAmount; retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i]; } } } return ret; } shared_ptr CBattleInfoCallback::battleGetObstacleOnPos(BattleHex tile, bool onlyBlocking /*= true*/) const { RETURN_IF_NOT_BATTLE(shared_ptr()); for(auto &obs : battleGetAllObstacles()) { if(vstd::contains(obs->getBlockedTiles(), tile) || (!onlyBlocking && vstd::contains(obs->getAffectedTiles(), tile))) { return obs; } } return shared_ptr(); } AccessibilityInfo CBattleInfoCallback::getAccesibility() const { AccessibilityInfo ret; ret.fill(EAccessibility::ACCESSIBLE); //removing accessibility for side columns of hexes for(int y = 0; y < GameConstants::BFIELD_HEIGHT; y++) { ret[BattleHex(GameConstants::BFIELD_WIDTH - 1, y)] = EAccessibility::SIDE_COLUMN; ret[BattleHex(0, y)] = EAccessibility::SIDE_COLUMN; } //gate -> should be before stacks if(battleGetSiegeLevel() > 0 && battleGetWallState(EWallPart::GATE) != EWallState::DESTROYED) { ret[95] = ret[96] = EAccessibility::GATE; //block gate's hexes } //tiles occupied by standing stacks for(auto stack : battleAliveStacks()) { for(auto hex : stack->getHexes()) if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns ret[hex] = EAccessibility::ALIVE_STACK; } //obstacles for(const auto &obst : battleGetAllObstacles()) { for(auto hex : obst->getBlockedTiles()) ret[hex] = EAccessibility::OBSTACLE; } //walls if(battleGetSiegeLevel() > 0) { static const int permanentlyLocked[] = {12, 45, 78, 112, 147, 165}; for(auto hex : permanentlyLocked) ret[hex] = EAccessibility::UNAVAILABLE; //TODO likely duplicated logic static const std::pair lockedIfNotDestroyed[] = //(which part of wall, which hex is blocked if this part of wall is not destroyed {std::make_pair(2, BattleHex(182)), std::make_pair(3, BattleHex(130)), std::make_pair(4, BattleHex(62)), std::make_pair(5, BattleHex(29))}; for(auto & elem : lockedIfNotDestroyed) { if(battleGetWallState(elem.first) != EWallState::DESTROYED) ret[elem.second] = EAccessibility::DESTRUCTIBLE_WALL; } } return ret; } AccessibilityInfo CBattleInfoCallback::getAccesibility(const CStack *stack) const { return getAccesibility(stack->getHexes()); } AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector &accessibleHexes) const { auto ret = getAccesibility(); for(auto hex : accessibleHexes) if(hex.isValid()) ret[hex] = EAccessibility::ACCESSIBLE; return ret; } ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters ¶ms) const { ReachabilityInfo ret; ret.accessibility = accessibility; ret.params = params; ret.predecessors.fill(BattleHex::INVALID); ret.distances.fill(ReachabilityInfo::INFINITE_DIST); if(!params.startPosition.isValid()) //if got call for arrow turrets return ret; const std::set quicksands = getStoppers(params.perspective); //const bool twoHexCreature = params.doubleWide; std::queue hexq; //bfs queue //first element hexq.push(params.startPosition); ret.distances[params.startPosition] = 0; while(!hexq.empty()) //bfs loop { const BattleHex curHex = hexq.front(); hexq.pop(); //walking stack can't step past the quicksands //TODO what if second hex of two-hex creature enters quicksand if(curHex != params.startPosition && vstd::contains(quicksands, curHex)) continue; const int costToNeighbour = ret.distances[curHex] + 1; for(BattleHex neighbour : curHex.neighbouringTiles()) { const bool accessible = accessibility.accessible(neighbour, params.doubleWide, params.attackerOwned); const int costFoundSoFar = ret.distances[neighbour]; if(accessible && costToNeighbour < costFoundSoFar) { hexq.push(neighbour); ret.distances[neighbour] = costToNeighbour; ret.predecessors[neighbour] = curHex; } } } return ret; } ReachabilityInfo CBattleInfoCallback::makeBFS(const CStack *stack) const { return makeBFS(getAccesibility(stack), ReachabilityInfo::Parameters(stack)); } std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const { std::set ret; RETURN_IF_NOT_BATTLE(ret); for(auto &oi : battleGetAllObstacles(whichSidePerspective)) { if(battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) { range::copy(oi->getStoppingTile(), vstd::set_inserter(ret)); } } return ret; } std::pair CBattleInfoCallback::getNearestStack(const CStack * closest, boost::logic::tribool attackerOwned) const { auto reachability = getReachability(closest); auto avHexes = battleGetAvailableHexes(closest, false); // I hate std::pairs with their undescriptive member names first / second struct DistStack { int distanceToPred; BattleHex destination; const CStack *stack; }; std::vector stackPairs; std::vector possibleStacks = battleGetStacksIf([=](const CStack * s) { return s != closest && s->alive() && (boost::logic::indeterminate(attackerOwned) || s->attackerOwned == attackerOwned); }, false); for(const CStack * st : possibleStacks) for(BattleHex hex : avHexes) if(CStack::isMeleeAttackPossible(closest, st, hex)) { DistStack hlp = {reachability.distances[st->position], hex, st}; stackPairs.push_back(hlp); } if (stackPairs.size()) { auto comparator = [](DistStack lhs, DistStack rhs) { return lhs.distanceToPred < rhs.distanceToPred; }; auto minimal = boost::min_element(stackPairs, comparator); return std::make_pair(minimal->stack, minimal->destination); } else return std::make_pair(nullptr, BattleHex::INVALID); } si8 CBattleInfoCallback::battleGetTacticDist() const { RETURN_IF_NOT_BATTLE(0); //TODO get rid of this method if(battleDoWeKnowAbout(battleGetTacticsSide())) return battleTacticDist(); return 0; } bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const { RETURN_IF_NOT_BATTLE(false); auto side = battleGetTacticsSide(); auto dist = battleGetTacticDist(); return ((!side && dest.getX() > 0 && dest.getX() <= dist) || (side && dest.getX() < GameConstants::BFIELD_WIDTH - 1 && dest.getX() >= GameConstants::BFIELD_WIDTH - dist - 1)); } ReachabilityInfo CBattleInfoCallback::getReachability(const CStack *stack) const { ReachabilityInfo::Parameters params(stack); if(!battleDoWeKnowAbout(!stack->attackerOwned)) { //Stack is held by enemy, we can't use his perspective to check for reachability. // Happens ie. when hovering enemy stack for its range. The arg could be set properly, but it's easier to fix it here. params.perspective = battleGetMySide(); } return getReachability(params); } ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Parameters ¶ms) const { if(params.flying) return getFlyingReachability(params); else return makeBFS(getAccesibility(params.knownAccessible), params); } ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const { ReachabilityInfo ret; ret.accessibility = getAccesibility(params.knownAccessible); for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) { if(ret.accessibility.accessible(i, params.doubleWide, params.attackerOwned)) { ret.predecessors[i] = params.startPosition; ret.distances[i] = BattleHex::getDistance(params.startPosition, i); } } return ret; } AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes (const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const { //does not return hex attacked directly //TODO: apply rotation to two-hex attackers bool isAttacker = attacker->attackerOwned; AttackableTiles at; RETURN_IF_NOT_BATTLE(at); const int WN = GameConstants::BFIELD_WIDTH; ui16 hex = (attackerPos != BattleHex::INVALID) ? attackerPos.hex : attacker->position.hex; //real or hypothetical (cursor) position //FIXME: dragons or cerbers can rotate before attack, making their base hex different (#1124) bool reverse = isToReverse (hex, destinationTile, isAttacker, attacker->doubleWide(), isAttacker); if (reverse) { hex = attacker->occupiedHex(hex); //the other hex stack stands on } if (attacker->hasBonusOfType(Bonus::ATTACKS_ALL_ADJACENT)) { boost::copy (attacker->getSurroundingHexes (attackerPos), vstd::set_inserter (at.hostileCreaturePositions)); } if (attacker->hasBonusOfType(Bonus::THREE_HEADED_ATTACK)) { std::vector hexes = attacker->getSurroundingHexes(attackerPos); for (BattleHex tile : hexes) { 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); if(st && st->owner != attacker->owner) //only hostile stacks - does it work well with Berserk? { at.hostileCreaturePositions.insert(tile); } } } } if (attacker->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH) && BattleHex::mutualPosition (destinationTile.hex, hex) > -1) //only adjacent hexes are subject of dragon breath calculation { std::vector hexes; //only one, in fact int pseudoVector = destinationTile.hex - hex; switch (pseudoVector) { case 1: case -1: BattleHex::checkAndPush (destinationTile.hex + pseudoVector, hexes); break; case WN: //17 //left-down or right-down case -WN: //-17 //left-up or right-up case WN + 1: //18 //right-down case -WN + 1: //-16 //right-up BattleHex::checkAndPush (destinationTile.hex + pseudoVector + (((hex/WN)%2) ? 1 : -1 ), hexes); break; case WN-1: //16 //left-down case -WN-1: //-18 //left-up BattleHex::checkAndPush (destinationTile.hex + pseudoVector + (((hex/WN)%2) ? 1 : 0), hexes); break; } for (BattleHex tile : hexes) { //friendly stacks can also be damaged by Dragon Breath if (battleGetStackByPos (tile, true)) at.friendlyCreaturePositions.insert (tile); } } return at; } std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos /*= BattleHex::INVALID*/) const { std::set attackedCres; RETURN_IF_NOT_BATTLE(attackedCres); AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); for (BattleHex tile : at.hostileCreaturePositions) //all around & three-headed attack { const CStack * st = battleGetStackByPos(tile, true); if(st && st->owner != attacker->owner) //only hostile stacks - does it work well with Berserk? { attackedCres.insert(st); } } for (BattleHex tile : at.friendlyCreaturePositions) { const CStack * st = battleGetStackByPos(tile, true); if(st) //friendly stacks can also be damaged by Dragon Breath { attackedCres.insert(st); } } return attackedCres; } //TODO: this should apply also to mechanics and cursor interface bool CBattleInfoCallback::isToReverseHlp (BattleHex hexFrom, BattleHex hexTo, bool curDir) const { int fromX = hexFrom.getX(); int fromY = hexFrom.getY(); int toX = hexTo.getX(); int toY = hexTo.getY(); if (curDir) // attacker, facing right { if (fromX < toX) return false; if (fromX > toX) return true; if (fromY % 2 == 0 && toY % 2 == 1) return true; return false; } else // defender, facing left { if(fromX < toX) return true; if(fromX > toX) return false; if (fromY % 2 == 1 && toY % 2 == 0) return true; return false; } } //TODO: this should apply also to mechanics and cursor interface bool CBattleInfoCallback::isToReverse (BattleHex hexFrom, BattleHex hexTo, bool curDir, bool toDoubleWide, bool toDir) const { if (hexTo < 0 || hexFrom < 0) //turret return false; if (toDoubleWide) { if (isToReverseHlp (hexFrom, hexTo, curDir)) { if (toDir) return isToReverseHlp (hexFrom, hexTo-1, curDir); else return isToReverseHlp (hexFrom, hexTo+1, curDir); } return false; } else { return isToReverseHlp(hexFrom, hexTo, curDir); } } ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const CStack * stack, BattleHex hex /*= BattleHex::INVALID*/, BattleHex * predecessors /*= nullptr*/) const { ReachabilityInfo::TDistances ret; ret.fill(-1); RETURN_IF_NOT_BATTLE(ret); ReachabilityInfo::Parameters params(stack); params.perspective = battleGetMySide(); params.startPosition = hex.isValid() ? hex : stack->position; auto reachability = getReachability(params); boost::copy(reachability.distances, ret.begin()); if(predecessors) for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) predecessors[i] = reachability.predecessors[i]; return ret; } si8 CBattleInfoCallback::battleHasDistancePenalty(const CStack * stack, BattleHex destHex) const { return battleHasDistancePenalty(stack, stack->position, destHex); } si8 CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer *bonusBearer, BattleHex shooterPosition, BattleHex destHex) const { RETURN_IF_NOT_BATTLE(false); if(bonusBearer->hasBonusOfType(Bonus::NO_DISTANCE_PENALTY)) return false; if(const CStack * dstStack = battleGetStackByPos(destHex, false)) { //If any hex of target creature is within range, there is no penalty for(auto hex : dstStack->getHexes()) if(BattleHex::getDistance(shooterPosition, hex) <= GameConstants::BATTLE_PENALTY_DISTANCE) return false; //TODO what about two-hex shooters? } else { if (BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_PENALTY_DISTANCE) return false; } return true; } BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart::EWallPart part) const { RETURN_IF_NOT_BATTLE(BattleHex::INVALID); return WallPartToHex(part); } EWallPart::EWallPart CBattleInfoCallback::battleHexToWallPart(BattleHex hex) const { RETURN_IF_NOT_BATTLE(EWallPart::INVALID); return hexToWallPart(hex); } bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart::EWallPart wallPart) const { RETURN_IF_NOT_BATTLE(false); return wallPart != EWallPart::INDESTRUCTIBLE_PART && wallPart != EWallPart::INDESTRUCTIBLE_PART_OF_GATE && wallPart != EWallPart::INVALID; } std::vector CBattleInfoCallback::getAttackableBattleHexes() const { std::vector attackableBattleHexes; RETURN_IF_NOT_BATTLE(attackableBattleHexes); for(auto & wallPartPair : wallParts) { if(isWallPartPotentiallyAttackable(wallPartPair.second)) { auto wallState = static_cast(battleGetWallState(static_cast(wallPartPair.second))); if(wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) { attackableBattleHexes.push_back(BattleHex(wallPartPair.first)); } } } return attackableBattleHexes; } ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell( PlayerColor player, const CSpell * spell, ECastingMode::ECastingMode mode ) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); const ui8 side = playerToSide(player); if(!battleDoWeKnowAbout(side)) return ESpellCastProblem::INVALID; ESpellCastProblem::ESpellCastProblem genProblem = battleCanCastSpell(player, mode); if(genProblem != ESpellCastProblem::OK) return genProblem; //Casting hero, set only if he is an actual caster. const CGHeroInstance *castingHero = mode == ECastingMode::HERO_CASTING ? battleGetFightingHero(side) : nullptr; switch(mode) { case ECastingMode::HERO_CASTING: { assert(castingHero); if(!castingHero->canCastThisSpell(spell)) return ESpellCastProblem::HERO_DOESNT_KNOW_SPELL; if(castingHero->mana < battleGetSpellCost(spell, castingHero)) //not enough mana return ESpellCastProblem::NOT_ENOUGH_MANA; } break; } if(!spell->combatSpell) return ESpellCastProblem::ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL; const ESpellCastProblem::ESpellCastProblem specificProblem = spell->canBeCasted(this, player); if(specificProblem != ESpellCastProblem::OK) return specificProblem; if(spell->isNegative() || spell->hasEffects()) { bool allStacksImmune = true; //we are interested only in enemy stacks when casting offensive spells //TODO: should hero be able to cast non-smart negative spell if all enemy stacks are immune? auto stacks = spell->isNegative() ? battleAliveStacks(!side) : battleAliveStacks(); for(auto stack : stacks) { if(ESpellCastProblem::OK == spell->isImmuneByStack(castingHero, stack)) { allStacksImmune = false; break; } } if(allStacksImmune) return ESpellCastProblem::NO_APPROPRIATE_TARGET; } if(battleMaxSpellLevel() < spell->level) //effect like Recanter's Cloak or Orb of Inhibition return ESpellCastProblem::SPELL_LEVEL_LIMIT_EXCEEDED; //checking if there exists an appropriate target switch(spell->getTargetType()) { case CSpell::CREATURE: if(mode == ECastingMode::HERO_CASTING) { const CGHeroInstance * caster = battleGetFightingHero(side); const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell)); bool targetExists = false; for(const CStack * stack : battleGetAllStacks()) //dead stacks will be immune anyway { bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); bool casterStack = stack->owner == caster->getOwner(); if(!immune) { switch (spell->positiveness) { case CSpell::POSITIVE: if(casterStack || !ti.smart) { targetExists = true; break; } break; case CSpell::NEUTRAL: targetExists = true; break; case CSpell::NEGATIVE: if(!casterStack || !ti.smart) { targetExists = true; break; } break; } } } if(!targetExists) { return ESpellCastProblem::NO_APPROPRIATE_TARGET; } } break; case CSpell::OBSTACLE: break; } return ESpellCastProblem::OK; } std::vector CBattleInfoCallback::battleGetPossibleTargets(PlayerColor player, const CSpell *spell) const { std::vector ret; RETURN_IF_NOT_BATTLE(ret); switch(spell->getTargetType()) { case CSpell::CREATURE: { const CGHeroInstance * caster = battleGetFightingHero(playerToSide(player)); //TODO const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell)); for(const CStack * stack : battleAliveStacks()) { bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack); bool casterStack = stack->owner == caster->getOwner(); if(!immune) switch (spell->positiveness) { case CSpell::POSITIVE: if(casterStack || ti.smart) ret.push_back(stack->position); break; case CSpell::NEUTRAL: ret.push_back(stack->position); break; case CSpell::NEGATIVE: if(!casterStack || ti.smart) ret.push_back(stack->position); break; } } } break; default: logGlobal->errorStream() << "FIXME " << __FUNCTION__ << " doesn't work with target type " << spell->getTargetType(); } return ret; } ui32 CBattleInfoCallback::battleGetSpellCost(const CSpell * sp, const CGHeroInstance * caster) const { RETURN_IF_NOT_BATTLE(-1); //TODO should be replaced using bonus system facilities (propagation onto battle node) ui32 ret = caster->getSpellCost(sp); //checking for friendly stacks reducing cost of the spell and //enemy stacks increasing it si32 manaReduction = 0; si32 manaIncrease = 0; for(auto stack : battleAliveStacks()) { if(stack->owner == caster->tempOwner && stack->hasBonusOfType(Bonus::CHANGES_SPELL_COST_FOR_ALLY) ) { vstd::amax(manaReduction, stack->valOfBonuses(Bonus::CHANGES_SPELL_COST_FOR_ALLY)); } if( stack->owner != caster->tempOwner && stack->hasBonusOfType(Bonus::CHANGES_SPELL_COST_FOR_ENEMY) ) { vstd::amax(manaIncrease, stack->valOfBonuses(Bonus::CHANGES_SPELL_COST_FOR_ENEMY)); } } return ret - manaReduction + manaIncrease; } ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpellHere( PlayerColor player, const CSpell * spell, ECastingMode::ECastingMode mode, BattleHex dest ) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); ESpellCastProblem::ESpellCastProblem moreGeneralProblem = battleCanCastThisSpell(player, spell, mode); if(moreGeneralProblem != ESpellCastProblem::OK) return moreGeneralProblem; if(spell->getTargetType() == CSpell::OBSTACLE) { if(spell->id == SpellID::REMOVE_OBSTACLE) { if(auto obstacle = battleGetObstacleOnPos(dest, false)) { switch (obstacle->obstacleType) { case CObstacleInstance::ABSOLUTE_OBSTACLE: //cliff-like obstacles can't be removed case CObstacleInstance::MOAT: return ESpellCastProblem::NO_APPROPRIATE_TARGET; case CObstacleInstance::USUAL: return ESpellCastProblem::OK; // //TODO FIRE_WALL only for ADVANCED level casters // case CObstacleInstance::FIRE_WALL: // return // //TODO other magic obstacles for EXPERT // case CObstacleInstance::QUICKSAND: // case CObstacleInstance::LAND_MINE: // case CObstacleInstance::FORCE_FIELD: // return default: // assert(0); return ESpellCastProblem::OK; } } } //isObstacleOnTile(dest) // // //TODO //assert that it's remove obstacle //rules whether we can remove spell-created obstacle return ESpellCastProblem::NO_APPROPRIATE_TARGET; } //get dead stack if we cast resurrection or animate dead const CStack *deadStack = getStackIf([dest](const CStack *s) { return !s->alive() && s->coversPos(dest); }); const CStack *aliveStack = getStackIf([dest](const CStack *s) { return s->alive() && s->coversPos(dest);}); if(spell->isRisingSpell()) { if(!deadStack && !aliveStack) return ESpellCastProblem::NO_APPROPRIATE_TARGET; if(spell->id == SpellID::ANIMATE_DEAD && deadStack && !deadStack->hasBonusOfType(Bonus::UNDEAD)) return ESpellCastProblem::NO_APPROPRIATE_TARGET; if(deadStack && deadStack->owner != player) //you can resurrect only your own stacks //FIXME: it includes alive stacks as well return ESpellCastProblem::NO_APPROPRIATE_TARGET; } else if(spell->getTargetType() == CSpell::CREATURE) { if(!aliveStack) return ESpellCastProblem::NO_APPROPRIATE_TARGET; if(spell->isNegative() && aliveStack->owner == player) return ESpellCastProblem::NO_APPROPRIATE_TARGET; if(spell->isPositive() && aliveStack->owner != player) return ESpellCastProblem::NO_APPROPRIATE_TARGET; } const CGHeroInstance * caster = nullptr; if (mode == ECastingMode::HERO_CASTING) caster = battleGetFightingHero(playerToSide(player)); return spell->isImmuneAt(this, caster, mode, dest); } const CStack * CBattleInfoCallback::getStackIf(std::function pred) const { RETURN_IF_NOT_BATTLE(nullptr); auto stacks = battleGetAllStacks(); auto stackItr = range::find_if(stacks, pred); return stackItr == stacks.end() ? nullptr : *stackItr; } bool CBattleInfoCallback::battleIsStackBlocked(const CStack * stack) const { RETURN_IF_NOT_BATTLE(false); if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON)) //siege weapons cannot be blocked return false; for(const CStack * s : batteAdjacentCreatures(stack)) { if (s->owner != stack->owner) //blocked by enemy stack return true; } return false; } std::set CBattleInfoCallback:: batteAdjacentCreatures(const CStack * stack) const { std::set stacks; RETURN_IF_NOT_BATTLE(stacks); for (BattleHex hex : stack->getSurroundingHexes()) if(const CStack *neighbour = battleGetStackByPos(hex, true)) stacks.insert(neighbour); return stacks; } SpellID CBattleInfoCallback::getRandomBeneficialSpell(const CStack * subject) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); //This is complete list. No spells from mods. //todo: this should be Spellbook of caster Stack static const std::set allPossibleSpells = { SpellID::AIR_SHIELD, SpellID::ANTI_MAGIC, SpellID::BLESS, SpellID::BLOODLUST, SpellID::COUNTERSTRIKE, SpellID::CURE, SpellID::FIRE_SHIELD, SpellID::FORTUNE, SpellID::HASTE, SpellID::MAGIC_MIRROR, SpellID::MIRTH, SpellID::PRAYER, SpellID::PRECISION, SpellID::PROTECTION_FROM_AIR, SpellID::PROTECTION_FROM_EARTH, SpellID::PROTECTION_FROM_FIRE, SpellID::PROTECTION_FROM_WATER, SpellID::SHIELD, SpellID::SLAYER, SpellID::STONE_SKIN }; std::vector beneficialSpells; auto getAliveEnemy = [=](const std::function & pred) { return getStackIf([=](const CStack * stack) { return pred(stack) && stack->owner != subject->owner && stack->alive(); }); }; for(const SpellID spellID : allPossibleSpells) { if (subject->hasBonusFrom(Bonus::SPELL_EFFECT, spellID) //TODO: this ability has special limitations || battleCanCastThisSpellHere(subject->owner, spellID.toSpell(), ECastingMode::CREATURE_ACTIVE_CASTING, subject->position) != ESpellCastProblem::OK) continue; switch (spellID) { case SpellID::SHIELD: case SpellID::FIRE_SHIELD: // not if all enemy units are shooters { auto walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack { return !stack->shots; }); if (!walker) continue; } break; case SpellID::AIR_SHIELD: //only against active shooters { auto shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack { return stack->hasBonusOfType(Bonus::SHOOTER) && stack->shots; }); if (!shooter) continue; } break; case SpellID::ANTI_MAGIC: case SpellID::MAGIC_MIRROR: case SpellID::PROTECTION_FROM_AIR: case SpellID::PROTECTION_FROM_EARTH: case SpellID::PROTECTION_FROM_FIRE: case SpellID::PROTECTION_FROM_WATER: { const ui8 enemySide = (ui8)subject->attackerOwned; //todo: only if enemy has spellbook if (!battleHasHero(enemySide)) //only if there is enemy hero continue; } break; case SpellID::CURE: //only damaged units { //do not cast on affected by debuffs if (subject->firstHPleft >= subject->MaxHealth()) continue; } break; case SpellID::BLOODLUST: { if (subject->shots) //if can shoot - only if enemy uits are adjacent continue; } break; case SpellID::PRECISION: { if (!(subject->hasBonusOfType(Bonus::SHOOTER) && subject->shots)) continue; } break; case SpellID::SLAYER://only if monsters are present { auto kingMonster = getAliveEnemy([&](const CStack *stack) -> bool //look for enemy, non-shooting stack { const auto isKing = Selector::type(Bonus::KING1) .Or(Selector::type(Bonus::KING2)) .Or(Selector::type(Bonus::KING3)); return stack->hasBonus(isKing); }); if (!kingMonster) continue; } break; } beneficialSpells.push_back(spellID); } if(!beneficialSpells.empty()) { return *RandomGeneratorUtil::nextItem(beneficialSpells, gs->getRandomGenerator()); } else { return SpellID::NONE; } } SpellID CBattleInfoCallback::getRandomCastedSpell(const CStack * caster) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); TBonusListPtr bl = caster->getBonuses(Selector::type(Bonus::SPELLCASTER)); if (!bl->size()) return SpellID::NONE; int totalWeight = 0; for(Bonus * b : *bl) { totalWeight += std::max(b->additionalInfo, 1); //minimal chance to cast is 1 } int randomPos = gs->getRandomGenerator().nextInt(totalWeight - 1); for(Bonus * b : *bl) { randomPos -= std::max(b->additionalInfo, 1); if(randomPos < 0) { return SpellID(b->subtype); } } return SpellID::NONE; } int CBattleInfoCallback::battleGetSurrenderCost(PlayerColor Player) const { RETURN_IF_NOT_BATTLE(-3); if(!battleCanSurrender(Player)) return -1; int ret = 0; double discount = 0; for(const CStack *s : battleAliveStacks(playerToSide(Player))) if(s->base) //we pay for our stack that comes from our army slots - condition eliminates summoned cres and war machines ret += s->getCreature()->cost[Res::GOLD] * s->count; if(const CGHeroInstance *h = battleGetFightingHero(playerToSide(Player))) discount += h->valOfBonuses(Bonus::SURRENDER_DISCOUNT); ret *= (100.0 - discount) / 100.0; vstd::amax(ret, 0); //no negative costs for >100% discounts (impossible in original H3 mechanics, but some day...) return ret; } si8 CBattleInfoCallback::battleMaxSpellLevel() const { const CBonusSystemNode *node = nullptr; if(const CGHeroInstance *h = battleGetFightingHero(battleGetMySide())) node = h; //TODO else use battle node if(!node) return GameConstants::SPELL_LEVELS; //We can't "just get value" - it'd be 0 if there are bonuses (and all would be blocked) auto b = node->getBonuses(Selector::type(Bonus::BLOCK_MAGIC_ABOVE)); if(b->size()) return b->totalValue(); return GameConstants::SPELL_LEVELS; } boost::optional CBattleInfoCallback::battleIsFinished() const { auto stacks = battleGetAllStacks(); //checking winning condition bool hasStack[2]; //hasStack[0] - true if attacker has a living stack; defender similarly hasStack[0] = hasStack[1] = false; for(auto & stack : stacks) { if(stack->alive() && !stack->hasBonusOfType(Bonus::SIEGE_WEAPON)) { hasStack[1-stack->attackerOwned] = true; } } if(!hasStack[0] && !hasStack[1]) return 2; if(!hasStack[1]) return 0; if(!hasStack[0]) return 1; return boost::none; } bool AccessibilityInfo::accessible(BattleHex tile, const CStack *stack) const { return accessible(tile, stack->doubleWide(), stack->attackerOwned); } bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, bool attackerOwned) const { // All hexes that stack would cover if standing on tile have to be accessible. for(auto hex : CStack::getHexes(tile, doubleWide, attackerOwned)) { // If the hex is out of range then the tile isn't accessible if(!hex.isValid()) return false; // If we're no defender which step on gate and the hex isn't accessible, then the tile // isn't accessible else if(at(hex) != EAccessibility::ACCESSIBLE && !(at(hex) == EAccessibility::GATE && !attackerOwned)) return false; } return true; } bool AccessibilityInfo::occupiable(const CStack *stack, BattleHex tile) const { //obviously, we can occupy tile by standing on it if(accessible(tile, stack)) return true; if(stack->doubleWide()) { //Check the tile next to -> if stack stands there, it'll also occupy considered hex const BattleHex anotherTile = tile + (stack->attackerOwned ? BattleHex::RIGHT : BattleHex::LEFT); if(accessible(anotherTile, stack)) return true; } return false; } ReachabilityInfo::Parameters::Parameters() { stack = nullptr; perspective = BattlePerspective::ALL_KNOWING; attackerOwned = doubleWide = flying = false; } ReachabilityInfo::Parameters::Parameters(const CStack *Stack) { stack = Stack; perspective = (BattlePerspective::BattlePerspective)(!Stack->attackerOwned); startPosition = Stack->position; doubleWide = stack->doubleWide(); attackerOwned = stack->attackerOwned; flying = stack->hasBonusOfType(Bonus::FLYING); knownAccessible = stack->getHexes(); } ESpellCastProblem::ESpellCastProblem CPlayerBattleCallback::battleCanCastThisSpell(const CSpell * spell) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); ASSERT_IF_CALLED_WITH_PLAYER return CBattleInfoCallback::battleCanCastThisSpell(*player, spell, ECastingMode::HERO_CASTING); } ESpellCastProblem::ESpellCastProblem CPlayerBattleCallback::battleCanCastThisSpell(const CSpell * spell, BattleHex destination) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); ASSERT_IF_CALLED_WITH_PLAYER return battleCanCastThisSpellHere(*player, spell, ECastingMode::HERO_CASTING, destination); } ESpellCastProblem::ESpellCastProblem CPlayerBattleCallback::battleCanCreatureCastThisSpell(const CSpell * spell, BattleHex destination) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); ASSERT_IF_CALLED_WITH_PLAYER return battleCanCastThisSpellHere(*player, spell, ECastingMode::CREATURE_ACTIVE_CASTING, destination); } bool CPlayerBattleCallback::battleCanFlee() const { RETURN_IF_NOT_BATTLE(false); ASSERT_IF_CALLED_WITH_PLAYER return CBattleInfoEssentials::battleCanFlee(*player); } TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose /*= MINE_AND_ENEMY*/, bool onlyAlive /*= true*/) const { if(whose != MINE_AND_ENEMY) { ASSERT_IF_CALLED_WITH_PLAYER } return battleGetStacksIf([=](const CStack * s){ const bool ownerMatches = (whose == MINE_AND_ENEMY) || (whose == ONLY_MINE && s->owner == player) || (whose == ONLY_ENEMY && s->owner != player); const bool alivenessMatches = s->alive() || !onlyAlive; return ownerMatches && alivenessMatches; }); } int CPlayerBattleCallback::battleGetSurrenderCost() const { RETURN_IF_NOT_BATTLE(-3) ASSERT_IF_CALLED_WITH_PLAYER return CBattleInfoCallback::battleGetSurrenderCost(*player); } bool CPlayerBattleCallback::battleCanCastSpell(ESpellCastProblem::ESpellCastProblem *outProblem /*= nullptr*/) const { RETURN_IF_NOT_BATTLE(false); ASSERT_IF_CALLED_WITH_PLAYER auto problem = CBattleInfoCallback::battleCanCastSpell(*player, ECastingMode::HERO_CASTING); if(outProblem) *outProblem = problem; return problem == ESpellCastProblem::OK; } const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const { return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide()); } InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const { return battleGetHeroInfo(!battleGetMySide()); } BattleAttackInfo::BattleAttackInfo(const CStack *Attacker, const CStack *Defender, bool Shooting) { attacker = Attacker; defender = Defender; attackerBonuses = Attacker; defenderBonuses = Defender; attackerPosition = Attacker->position; defenderPosition = Defender->position; attackerCount = Attacker->count; defenderCount = Defender->count; shooting = Shooting; chargedFields = 0; luckyHit = false; deathBlow = false; ballistaDoubleDamage = false; } BattleAttackInfo BattleAttackInfo::reverse() const { BattleAttackInfo ret = *this; std::swap(ret.attacker, ret.defender); std::swap(ret.attackerBonuses, ret.defenderBonuses); std::swap(ret.attackerPosition, ret.defenderPosition); std::swap(ret.attackerCount, ret.defenderCount); ret.shooting = false; ret.chargedFields = 0; ret.luckyHit = ret.ballistaDoubleDamage = ret.deathBlow = false; return ret; }