2023-08-14 18:46:42 +02:00
/*
* BattleFlowProcessor . 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 "BattleFlowProcessor.h"
# include "BattleProcessor.h"
# include "../CGameHandler.h"
2024-04-26 12:26:33 +02:00
# include "../TurnTimerHandler.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/CStack.h"
# include "../../lib/GameSettings.h"
2023-08-28 16:43:57 +02:00
# include "../../lib/battle/CBattleInfoCallback.h"
# include "../../lib/battle/IBattleState.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/gameState/CGameState.h"
# include "../../lib/mapObjects/CGTownInstance.h"
2023-10-23 12:59:15 +02:00
# include "../../lib/networkPacks/PacksForClientBattle.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/spells/BonusCaster.h"
# include "../../lib/spells/ISpellMechanics.h"
# include "../../lib/spells/ObstacleCasterProxy.h"
2024-06-01 17:28:17 +02:00
# include <vstd/RNG.h>
2024-04-26 11:44:57 +02:00
BattleFlowProcessor : : BattleFlowProcessor ( BattleProcessor * owner , CGameHandler * newGameHandler )
2023-08-14 18:46:42 +02:00
: owner ( owner )
2024-04-26 11:44:57 +02:00
, gameHandler ( newGameHandler )
2023-08-14 18:46:42 +02:00
{
}
2024-08-11 22:22:35 +02:00
void BattleFlowProcessor : : summonGuardiansHelper ( const CBattleInfoCallback & battle , std : : vector < BattleHex > & output , const BattleHex & targetPosition , BattleSide side , bool targetIsTwoHex ) //return hexes for summoning two hex monsters in output, target = unit to guard
2023-08-14 18:46:42 +02:00
{
int x = targetPosition . getX ( ) ;
int y = targetPosition . getY ( ) ;
const bool targetIsAttacker = side = = BattleSide : : ATTACKER ;
if ( targetIsAttacker ) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3...
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) , output ) ;
else
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) , output ) ;
//guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's
if ( targetIsAttacker & & ( ( y % 2 = = 0 ) | | ( x > 1 ) ) )
{
if ( targetIsTwoHex & & ( y % 2 = = 1 ) & & ( x = = 2 ) ) //handle exceptional case
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : TOP_RIGHT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : BOTTOM_RIGHT , false ) , output ) ;
}
else
{ //add back-side guardians for two-hex target, side guardians for one-hex
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( targetIsTwoHex ? BattleHex : : EDir : : TOP_LEFT : BattleHex : : EDir : : TOP_RIGHT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( targetIsTwoHex ? BattleHex : : EDir : : BOTTOM_LEFT : BattleHex : : EDir : : BOTTOM_RIGHT , false ) , output ) ;
if ( ! targetIsTwoHex & & x > 2 ) //back guard for one-hex
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) , output ) ;
else if ( targetIsTwoHex ) //front-side guardians for two-hex target
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : TOP_RIGHT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : BOTTOM_RIGHT , false ) , output ) ;
if ( x > 3 ) //back guard for two-hex
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) , output ) ;
}
}
}
else if ( ! targetIsAttacker & & ( ( y % 2 = = 1 ) | | ( x < GameConstants : : BFIELD_WIDTH - 2 ) ) )
{
if ( targetIsTwoHex & & ( y % 2 = = 0 ) & & ( x = = GameConstants : : BFIELD_WIDTH - 3 ) ) //handle exceptional case... equivalent for above for defender side
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : TOP_LEFT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : BOTTOM_LEFT , false ) , output ) ;
}
else
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( targetIsTwoHex ? BattleHex : : EDir : : TOP_RIGHT : BattleHex : : EDir : : TOP_LEFT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( targetIsTwoHex ? BattleHex : : EDir : : BOTTOM_RIGHT : BattleHex : : EDir : : BOTTOM_LEFT , false ) , output ) ;
if ( ! targetIsTwoHex & & x < GameConstants : : BFIELD_WIDTH - 3 )
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) , output ) ;
else if ( targetIsTwoHex )
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : TOP_LEFT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : BOTTOM_LEFT , false ) , output ) ;
if ( x < GameConstants : : BFIELD_WIDTH - 4 )
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) , output ) ;
}
}
}
else if ( ! targetIsAttacker & & y % 2 = = 0 )
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : TOP_LEFT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : LEFT , false ) . cloneInDirection ( BattleHex : : EDir : : BOTTOM_LEFT , false ) , output ) ;
}
else if ( targetIsAttacker & & y % 2 = = 1 )
{
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : TOP_RIGHT , false ) , output ) ;
BattleHex : : checkAndPush ( targetPosition . cloneInDirection ( BattleHex : : EDir : : RIGHT , false ) . cloneInDirection ( BattleHex : : EDir : : BOTTOM_RIGHT , false ) , output ) ;
}
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : tryPlaceMoats ( const CBattleInfoCallback & battle )
2023-08-14 18:46:42 +02:00
{
2023-08-28 16:43:57 +02:00
const auto * town = battle . battleGetDefendedTown ( ) ;
2023-08-14 18:46:42 +02:00
//Moat should be initialized here, because only here we can use spellcasting
2023-08-28 16:43:57 +02:00
if ( town & & town - > fortLevel ( ) > = CGTownInstance : : CITADEL )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
const auto * h = battle . battleGetFightingHero ( BattleSide : : DEFENDER ) ;
2023-08-14 18:46:42 +02:00
const auto * actualCaster = h ? static_cast < const spells : : Caster * > ( h ) : nullptr ;
2023-08-28 16:43:57 +02:00
auto moatCaster = spells : : SilentCaster ( battle . sideToPlayer ( BattleSide : : DEFENDER ) , actualCaster ) ;
auto cast = spells : : BattleCast ( & battle , & moatCaster , spells : : Mode : : PASSIVE , town - > town - > moatAbility . toSpell ( ) ) ;
2023-08-14 18:46:42 +02:00
auto target = spells : : Target ( ) ;
cast . cast ( gameHandler - > spellEnv , target ) ;
}
2023-08-17 18:18:14 +02:00
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : onBattleStarted ( const CBattleInfoCallback & battle )
2023-08-17 18:18:14 +02:00
{
2023-08-25 17:23:15 +02:00
tryPlaceMoats ( battle ) ;
2023-08-22 12:14:50 +02:00
2024-04-26 12:15:39 +02:00
gameHandler - > turnTimerHandler - > onBattleStart ( battle . getBattle ( ) - > getBattleID ( ) ) ;
2023-08-14 18:46:42 +02:00
2023-08-28 16:43:57 +02:00
if ( battle . battleGetTacticDist ( ) = = 0 )
2023-08-25 17:23:15 +02:00
onTacticsEnded ( battle ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : trySummonGuardians ( const CBattleInfoCallback & battle , const CStack * stack )
2023-08-14 18:46:42 +02:00
{
2023-08-17 13:56:24 +02:00
if ( ! stack - > hasBonusOfType ( BonusType : : SUMMON_GUARDIANS ) )
return ;
2023-08-14 18:46:42 +02:00
2023-08-17 13:56:24 +02:00
std : : shared_ptr < const Bonus > summonInfo = stack - > getBonus ( Selector : : type ( ) ( BonusType : : SUMMON_GUARDIANS ) ) ;
2024-06-24 03:23:26 +02:00
auto accessibility = battle . getAccessibility ( ) ;
2023-10-05 17:18:14 +02:00
CreatureID creatureData = summonInfo - > subtype . as < CreatureID > ( ) ;
2023-08-17 13:56:24 +02:00
std : : vector < BattleHex > targetHexes ;
const bool targetIsBig = stack - > unitType ( ) - > isDoubleWide ( ) ; //target = creature to guard
const bool guardianIsBig = creatureData . toCreature ( ) - > isDoubleWide ( ) ;
/*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible.
For one - hex targets there are four guardians - front , back and one per side ( up + down ) .
Two - hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front
Additionally , there are special cases for starting positions etc . , where guardians would be outside of battlefield if spawned normally */
if ( ! guardianIsBig )
targetHexes = stack - > getSurroundingHexes ( ) ;
else
2023-08-25 17:23:15 +02:00
summonGuardiansHelper ( battle , targetHexes , stack - > getPosition ( ) , stack - > unitSide ( ) , targetIsBig ) ;
2023-08-17 13:56:24 +02:00
for ( auto hex : targetHexes )
2023-08-14 18:46:42 +02:00
{
2023-08-17 13:56:24 +02:00
if ( accessibility . accessible ( hex , guardianIsBig , stack - > unitSide ( ) ) ) //without this multiple creatures can occupy one hex
2023-08-14 18:46:42 +02:00
{
2023-08-17 13:56:24 +02:00
battle : : UnitInfo info ;
2023-08-25 17:23:15 +02:00
info . id = battle . battleNextUnitId ( ) ;
2023-08-17 13:56:24 +02:00
info . count = std : : max ( 1 , ( int ) ( stack - > getCount ( ) * 0.01 * summonInfo - > val ) ) ;
info . type = creatureData ;
info . side = stack - > unitSide ( ) ;
info . position = hex ;
info . summoned = true ;
BattleUnitsChanged pack ;
2023-08-31 17:45:52 +02:00
pack . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-17 13:56:24 +02:00
pack . changedStacks . emplace_back ( info . id , UnitChanges : : EOperation : : ADD ) ;
info . save ( pack . changedStacks . back ( ) . data ) ;
gameHandler - > sendAndApply ( & pack ) ;
2023-08-14 18:46:42 +02:00
}
}
2023-09-08 17:49:06 +02:00
// send empty event to client
// temporary(?) workaround to force animations to trigger
StacksInjured fakeEvent ;
2024-01-05 15:47:25 +02:00
fakeEvent . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-09-08 17:49:06 +02:00
gameHandler - > sendAndApply ( & fakeEvent ) ;
2023-08-17 13:56:24 +02:00
}
2023-08-14 18:46:42 +02:00
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : castOpeningSpells ( const CBattleInfoCallback & battle )
2023-08-17 13:56:24 +02:00
{
2024-08-11 22:22:35 +02:00
for ( auto i : { BattleSide : : ATTACKER , BattleSide : : DEFENDER } )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
auto h = battle . battleGetFightingHero ( i ) ;
2023-08-17 13:56:24 +02:00
if ( ! h )
continue ;
2023-08-14 18:46:42 +02:00
2023-08-17 13:56:24 +02:00
TConstBonusListPtr bl = h - > getBonuses ( Selector : : type ( ) ( BonusType : : OPENING_BATTLE_SPELL ) ) ;
for ( auto b : * bl )
{
spells : : BonusCaster caster ( h , b ) ;
2023-08-14 18:46:42 +02:00
2023-10-05 17:18:14 +02:00
const CSpell * spell = b - > subtype . as < SpellID > ( ) . toSpell ( ) ;
2023-08-14 18:46:42 +02:00
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , & caster , spells : : Mode : : PASSIVE , spell ) ;
2023-08-17 13:56:24 +02:00
parameters . setSpellLevel ( 3 ) ;
parameters . setEffectDuration ( b - > val ) ;
parameters . massive = true ;
parameters . castIfPossible ( gameHandler - > spellEnv , spells : : Target ( ) ) ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-17 13:56:24 +02:00
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : onTacticsEnded ( const CBattleInfoCallback & battle )
2023-08-17 13:56:24 +02:00
{
//initial stacks appearance triggers, e.g. built-in bonus spells
2023-08-28 16:43:57 +02:00
auto initialStacks = battle . battleGetAllStacks ( true ) ;
2023-08-17 13:56:24 +02:00
2023-08-28 16:43:57 +02:00
for ( const CStack * stack : initialStacks )
2023-08-17 13:56:24 +02:00
{
2023-08-25 17:23:15 +02:00
trySummonGuardians ( battle , stack ) ;
stackEnchantedTrigger ( battle , stack ) ;
2023-08-17 13:56:24 +02:00
}
2023-08-25 17:23:15 +02:00
castOpeningSpells ( battle ) ;
2023-08-17 13:56:24 +02:00
2023-08-14 18:46:42 +02:00
// it is possible that due to opening spells one side was eliminated -> check for end of battle
2023-08-25 17:23:15 +02:00
if ( owner - > checkBattleStateChanges ( battle ) )
2023-08-17 14:18:16 +02:00
return ;
2023-08-14 18:46:42 +02:00
2023-08-25 17:23:15 +02:00
startNextRound ( battle , true ) ;
activateNextStack ( battle ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : startNextRound ( const CBattleInfoCallback & battle , bool isFirstRound )
2023-08-14 18:46:42 +02:00
{
BattleNextRound bnr ;
2023-08-31 17:45:52 +02:00
bnr . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-28 16:43:57 +02:00
logGlobal - > debug ( " Next round starts " ) ;
2023-08-14 18:46:42 +02:00
gameHandler - > sendAndApply ( & bnr ) ;
2023-08-28 16:43:57 +02:00
// operate on copy - removing obstacles will invalidate iterator on 'battle' container
auto obstacles = battle . battleGetAllObstacles ( ) ;
2023-08-14 18:46:42 +02:00
for ( auto & obstPtr : obstacles )
{
if ( const SpellCreatedObstacle * sco = dynamic_cast < const SpellCreatedObstacle * > ( obstPtr . get ( ) ) )
if ( sco - > turnsRemaining = = 0 )
2023-08-25 17:23:15 +02:00
removeObstacle ( battle , * obstPtr ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
for ( auto stack : battle . battleGetAllStacks ( true ) )
2023-08-14 18:46:42 +02:00
{
if ( stack - > alive ( ) & & ! isFirstRound )
2023-08-25 17:23:15 +02:00
stackEnchantedTrigger ( battle , stack ) ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-28 16:43:57 +02:00
const CStack * BattleFlowProcessor : : getNextStack ( const CBattleInfoCallback & battle )
2023-08-14 18:46:42 +02:00
{
std : : vector < battle : : Units > q ;
2023-08-25 17:23:15 +02:00
battle . battleGetTurnOrder ( q , 1 , 0 , - 1 ) ; //todo: get rid of "turn -1"
2023-08-14 18:46:42 +02:00
if ( q . empty ( ) )
return nullptr ;
if ( q . front ( ) . empty ( ) )
return nullptr ;
auto next = q . front ( ) . front ( ) ;
const auto stack = dynamic_cast < const CStack * > ( next ) ;
// regeneration takes place before everything else but only during first turn attempt in each round
// also works under blind and similar effects
if ( stack & & stack - > alive ( ) & & ! stack - > waiting )
{
BattleTriggerEffect bte ;
2023-08-31 17:45:52 +02:00
bte . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bte . stackID = stack - > unitId ( ) ;
bte . effect = vstd : : to_underlying ( BonusType : : HP_REGENERATION ) ;
const int32_t lostHealth = stack - > getMaxHealth ( ) - stack - > getFirstHPleft ( ) ;
if ( stack - > hasBonusOfType ( BonusType : : HP_REGENERATION ) )
bte . val = std : : min ( lostHealth , stack - > valOfBonuses ( BonusType : : HP_REGENERATION ) ) ;
if ( bte . val ) // anything to heal
gameHandler - > sendAndApply ( & bte ) ;
}
2023-10-27 22:00:52 +02:00
if ( ! next | | ! next - > willMove ( ) )
2023-08-14 18:46:42 +02:00
return nullptr ;
return stack ;
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : activateNextStack ( const CBattleInfoCallback & battle )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
// Find next stack that requires manual control
for ( ; ; )
{
2023-08-17 14:18:16 +02:00
// battle has ended
2023-08-25 17:23:15 +02:00
if ( owner - > checkBattleStateChanges ( battle ) )
2023-08-17 14:18:16 +02:00
return ;
2023-08-25 17:23:15 +02:00
const CStack * next = getNextStack ( battle ) ;
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
if ( ! next )
2023-08-17 13:56:24 +02:00
{
// No stacks to move - start next round
2023-08-25 17:23:15 +02:00
startNextRound ( battle , false ) ;
next = getNextStack ( battle ) ;
2023-08-17 13:56:24 +02:00
if ( ! next )
throw std : : runtime_error ( " Failed to find valid stack to act! " ) ;
}
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
BattleUnitsChanged removeGhosts ;
2023-08-31 17:45:52 +02:00
removeGhosts . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
2023-08-28 16:43:57 +02:00
auto pendingGhosts = battle . battleGetStacksIf ( [ ] ( const CStack * stack ) {
return stack - > ghostPending ;
} ) ;
for ( auto stack : pendingGhosts )
removeGhosts . changedStacks . emplace_back ( stack - > unitId ( ) , UnitChanges : : EOperation : : REMOVE ) ;
2023-08-16 23:51:50 +02:00
if ( ! removeGhosts . changedStacks . empty ( ) )
gameHandler - > sendAndApply ( & removeGhosts ) ;
2023-08-21 23:49:50 +02:00
2024-04-26 12:15:39 +02:00
gameHandler - > turnTimerHandler - > onBattleNextStack ( battle . getBattle ( ) - > getBattleID ( ) , * next ) ;
2023-08-14 18:46:42 +02:00
2023-08-25 17:23:15 +02:00
if ( ! tryMakeAutomaticAction ( battle , next ) )
2023-08-16 23:51:50 +02:00
{
2023-08-25 17:23:15 +02:00
setActiveStack ( battle , next ) ;
2023-08-16 23:51:50 +02:00
break ;
}
}
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
bool BattleFlowProcessor : : tryMakeAutomaticAction ( const CBattleInfoCallback & battle , const CStack * next )
2023-08-14 18:46:42 +02:00
{
// check for bad morale => freeze
int nextStackMorale = next - > moraleVal ( ) ;
if ( ! next - > hadMorale & & ! next - > waited ( ) & & nextStackMorale < 0 )
{
auto diceSize = VLC - > settings ( ) - > getVector ( EGameSettings : : COMBAT_BAD_MORALE_DICE ) ;
2023-10-13 20:43:15 +02:00
size_t diceIndex = std : : min < size_t > ( diceSize . size ( ) , - nextStackMorale ) - 1 ; // array index, so 0-indexed
2023-08-14 18:46:42 +02:00
if ( diceSize . size ( ) > 0 & & gameHandler - > getRandomGenerator ( ) . nextInt ( 1 , diceSize [ diceIndex ] ) = = 1 )
{
//unit loses its turn - empty freeze action
BattleAction ba ;
ba . actionType = EActionType : : BAD_MORALE ;
ba . side = next - > unitSide ( ) ;
ba . stackNumber = next - > unitId ( ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , ba ) ;
2023-08-14 18:46:42 +02:00
return true ;
}
}
if ( next - > hasBonusOfType ( BonusType : : ATTACKS_NEAREST_CREATURE ) ) //while in berserk
{
logGlobal - > trace ( " Handle Berserk effect " ) ;
2023-08-25 17:23:15 +02:00
std : : pair < const battle : : Unit * , BattleHex > attackInfo = battle . getNearestStack ( next ) ;
2023-08-14 18:46:42 +02:00
if ( attackInfo . first ! = nullptr )
{
BattleAction attack ;
attack . actionType = EActionType : : WALK_AND_ATTACK ;
attack . side = next - > unitSide ( ) ;
attack . stackNumber = next - > unitId ( ) ;
attack . aimToHex ( attackInfo . second ) ;
attack . aimToUnit ( attackInfo . first ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , attack ) ;
2023-08-14 18:46:42 +02:00
logGlobal - > trace ( " Attacked nearest target %s " , attackInfo . first - > getDescription ( ) ) ;
}
else
{
2023-08-25 17:23:15 +02:00
makeStackDoNothing ( battle , next ) ;
2023-08-14 18:46:42 +02:00
logGlobal - > trace ( " No target found " ) ;
}
return true ;
}
2023-08-28 16:43:57 +02:00
const CGHeroInstance * curOwner = battle . battleGetOwnerHero ( next ) ;
2023-10-05 17:18:14 +02:00
const CreatureID stackCreatureId = next - > unitType ( ) - > getId ( ) ;
2023-08-14 18:46:42 +02:00
if ( ( stackCreatureId = = CreatureID : : ARROW_TOWERS | | stackCreatureId = = CreatureID : : BALLISTA )
2023-10-21 13:50:42 +02:00
& & ( ! curOwner | | gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) > = curOwner - > valOfBonuses ( BonusType : : MANUAL_CONTROL , BonusSubtypeID ( stackCreatureId ) ) ) )
2023-08-14 18:46:42 +02:00
{
BattleAction attack ;
attack . actionType = EActionType : : SHOOT ;
attack . side = next - > unitSide ( ) ;
attack . stackNumber = next - > unitId ( ) ;
2024-07-20 20:27:02 +02:00
// TODO: unify logic with AI?
// Find best target using logic similar to H3 AI
const auto & isBetterTarget = [ & battle ] ( const battle : : Unit * candidate , const battle : : Unit * current )
{
bool candidateInsideWalls = battle . battleIsInsideWalls ( candidate - > getPosition ( ) ) ;
bool currentInsideWalls = battle . battleIsInsideWalls ( current - > getPosition ( ) ) ;
if ( candidateInsideWalls ! = currentInsideWalls )
return candidateInsideWalls > currentInsideWalls ;
// also check for war machines - shooters are more dangerous than war machines, ballista or catapult
bool candidateCanShoot = candidate - > canShoot ( ) & & candidate - > unitType ( ) - > warMachine = = ArtifactID : : NONE ;
bool currentCanShoot = current - > canShoot ( ) & & current - > unitType ( ) - > warMachine = = ArtifactID : : NONE ;
if ( candidateCanShoot ! = currentCanShoot )
return candidateCanShoot > currentCanShoot ;
int64_t candidateTargetValue = static_cast < int64_t > ( candidate - > unitType ( ) - > getAIValue ( ) * candidate - > getCount ( ) ) ;
int64_t currentTargetValue = static_cast < int64_t > ( current - > unitType ( ) - > getAIValue ( ) * current - > getCount ( ) ) ;
return candidateTargetValue > currentTargetValue ;
} ;
2023-08-14 18:46:42 +02:00
const battle : : Unit * target = nullptr ;
2023-08-28 16:43:57 +02:00
for ( auto & elem : battle . battleGetAllStacks ( true ) )
2023-08-14 18:46:42 +02:00
{
2024-07-20 20:27:02 +02:00
if ( elem - > unitOwner ( ) = = next - > unitOwner ( ) )
continue ;
if ( ! elem - > isValidTarget ( ) )
continue ;
if ( ! battle . battleCanShoot ( next , elem - > getPosition ( ) ) )
continue ;
if ( target & & ! isBetterTarget ( elem , target ) )
continue ;
target = elem ;
2023-08-14 18:46:42 +02:00
}
if ( target = = nullptr )
{
2023-08-25 17:23:15 +02:00
makeStackDoNothing ( battle , next ) ;
2023-08-14 18:46:42 +02:00
}
else
{
attack . aimToUnit ( target ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , attack ) ;
2023-08-14 18:46:42 +02:00
}
return true ;
}
if ( next - > unitType ( ) - > getId ( ) = = CreatureID : : CATAPULT )
{
2023-08-25 17:23:15 +02:00
const auto & attackableBattleHexes = battle . getAttackableBattleHexes ( ) ;
2023-08-14 18:46:42 +02:00
if ( attackableBattleHexes . empty ( ) )
{
2023-08-25 17:23:15 +02:00
makeStackDoNothing ( battle , next ) ;
2023-08-14 18:46:42 +02:00
return true ;
}
2023-10-21 13:50:42 +02:00
if ( ! curOwner | | gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) > = curOwner - > valOfBonuses ( BonusType : : MANUAL_CONTROL , BonusSubtypeID ( CreatureID ( CreatureID : : CATAPULT ) ) ) )
2023-08-14 18:46:42 +02:00
{
BattleAction attack ;
attack . actionType = EActionType : : CATAPULT ;
attack . side = next - > unitSide ( ) ;
attack . stackNumber = next - > unitId ( ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , attack ) ;
2023-08-14 18:46:42 +02:00
return true ;
}
}
if ( next - > unitType ( ) - > getId ( ) = = CreatureID : : FIRST_AID_TENT )
{
2023-08-28 16:43:57 +02:00
TStacks possibleStacks = battle . battleGetStacksIf ( [ = ] ( const CStack * s )
2023-08-14 18:46:42 +02:00
{
return s - > unitOwner ( ) = = next - > unitOwner ( ) & & s - > canBeHealed ( ) ;
} ) ;
2023-08-16 23:51:50 +02:00
if ( possibleStacks . empty ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
makeStackDoNothing ( battle , next ) ;
2023-08-14 18:46:42 +02:00
return true ;
}
2023-10-21 13:50:42 +02:00
if ( ! curOwner | | gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) > = curOwner - > valOfBonuses ( BonusType : : MANUAL_CONTROL , BonusSubtypeID ( CreatureID ( CreatureID : : FIRST_AID_TENT ) ) ) )
2023-08-14 18:46:42 +02:00
{
RandomGeneratorUtil : : randomShuffle ( possibleStacks , gameHandler - > getRandomGenerator ( ) ) ;
const CStack * toBeHealed = possibleStacks . front ( ) ;
BattleAction heal ;
heal . actionType = EActionType : : STACK_HEAL ;
heal . aimToUnit ( toBeHealed ) ;
heal . side = next - > unitSide ( ) ;
heal . stackNumber = next - > unitId ( ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , heal ) ;
2023-08-14 18:46:42 +02:00
return true ;
}
}
2023-08-25 17:23:15 +02:00
stackTurnTrigger ( battle , next ) ; //various effects
2023-08-14 18:46:42 +02:00
if ( next - > fear )
{
2023-08-25 17:23:15 +02:00
makeStackDoNothing ( battle , next ) ; //end immediately if stack was affected by fear
2023-08-14 18:46:42 +02:00
return true ;
}
2023-08-16 23:51:50 +02:00
return false ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
bool BattleFlowProcessor : : rollGoodMorale ( const CBattleInfoCallback & battle , const CStack * next )
2023-08-14 18:46:42 +02:00
{
//check for good morale
auto nextStackMorale = next - > moraleVal ( ) ;
if ( ! next - > hadMorale
& & ! next - > defending
& & ! next - > waited ( )
& & ! next - > fear
& & next - > alive ( )
2023-09-06 11:40:05 +02:00
& & next - > canMove ( )
2023-08-14 18:46:42 +02:00
& & nextStackMorale > 0 )
{
auto diceSize = VLC - > settings ( ) - > getVector ( EGameSettings : : COMBAT_GOOD_MORALE_DICE ) ;
2023-10-13 20:43:15 +02:00
size_t diceIndex = std : : min < size_t > ( diceSize . size ( ) , nextStackMorale ) - 1 ; // array index, so 0-indexed
2023-08-14 18:46:42 +02:00
if ( diceSize . size ( ) > 0 & & gameHandler - > getRandomGenerator ( ) . nextInt ( 1 , diceSize [ diceIndex ] ) = = 1 )
{
BattleTriggerEffect bte ;
2023-08-31 17:45:52 +02:00
bte . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bte . stackID = next - > unitId ( ) ;
bte . effect = vstd : : to_underlying ( BonusType : : MORALE ) ;
bte . val = 1 ;
bte . additionalInfo = 0 ;
gameHandler - > sendAndApply ( & bte ) ; //play animation
2023-08-16 23:51:50 +02:00
return true ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-16 23:51:50 +02:00
return false ;
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : onActionMade ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const auto * actedStack = battle . battleGetStackByID ( ba . stackNumber , false ) ;
const auto * activeStack = battle . battleActiveUnit ( ) ;
2023-08-31 00:32:32 +02:00
if ( ba . actionType = = EActionType : : END_TACTIC_PHASE )
{
2023-08-25 17:23:15 +02:00
onTacticsEnded ( battle ) ;
2023-08-31 00:32:32 +02:00
return ;
}
2023-08-16 23:51:50 +02:00
//we're after action, all results applied
2023-08-17 14:18:16 +02:00
// check whether action has ended the battle
2023-08-25 17:23:15 +02:00
if ( owner - > checkBattleStateChanges ( battle ) )
2023-08-17 14:18:16 +02:00
return ;
2023-08-16 23:51:50 +02:00
2023-08-21 12:42:27 +02:00
// tactics - next stack will be selected by player
2023-08-28 16:43:57 +02:00
if ( battle . battleGetTacticDist ( ) ! = 0 )
2023-08-21 12:42:27 +02:00
return ;
2023-08-17 18:18:14 +02:00
if ( ba . isUnitAction ( ) )
2023-08-16 23:51:50 +02:00
{
2023-08-17 18:18:14 +02:00
assert ( activeStack ! = nullptr ) ;
assert ( actedStack ! = nullptr ) ;
2023-08-17 15:17:19 +02:00
2023-08-25 17:23:15 +02:00
if ( rollGoodMorale ( battle , actedStack ) )
2023-08-17 13:56:24 +02:00
{
2023-08-17 18:18:14 +02:00
// Good morale - same stack makes 2nd turn
2023-08-25 17:23:15 +02:00
setActiveStack ( battle , actedStack ) ;
2023-08-17 13:56:24 +02:00
return ;
}
2023-08-16 23:51:50 +02:00
}
2023-08-17 13:56:24 +02:00
else
2023-08-16 23:51:50 +02:00
{
2023-08-17 18:18:14 +02:00
if ( activeStack & & activeStack - > alive ( ) )
2023-08-17 13:56:24 +02:00
{
2023-08-17 18:18:14 +02:00
// this is action made by hero AND unit is alive (e.g. not killed by casted spell)
// keep current active stack for next action
2023-08-25 17:23:15 +02:00
setActiveStack ( battle , activeStack ) ;
2023-08-17 13:56:24 +02:00
return ;
}
2023-08-16 23:51:50 +02:00
}
2023-08-25 17:23:15 +02:00
activateNextStack ( battle ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : makeStackDoNothing ( const CBattleInfoCallback & battle , const CStack * next )
2023-08-14 18:46:42 +02:00
{
BattleAction doNothing ;
doNothing . actionType = EActionType : : NO_ACTION ;
doNothing . side = next - > unitSide ( ) ;
doNothing . stackNumber = next - > unitId ( ) ;
2023-08-25 17:23:15 +02:00
makeAutomaticAction ( battle , next , doNothing ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
bool BattleFlowProcessor : : makeAutomaticAction ( const CBattleInfoCallback & battle , const CStack * stack , BattleAction & ba )
2023-08-14 18:46:42 +02:00
{
BattleSetActiveStack bsa ;
2023-08-31 17:45:52 +02:00
bsa . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bsa . stack = stack - > unitId ( ) ;
bsa . askPlayerInterface = false ;
gameHandler - > sendAndApply ( & bsa ) ;
2023-08-25 17:23:15 +02:00
bool ret = owner - > makeAutomaticBattleAction ( battle , ba ) ;
2023-08-14 18:46:42 +02:00
return ret ;
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : stackEnchantedTrigger ( const CBattleInfoCallback & battle , const CStack * st )
2023-08-14 18:46:42 +02:00
{
auto bl = * ( st - > getBonuses ( Selector : : type ( ) ( BonusType : : ENCHANTED ) ) ) ;
for ( auto b : bl )
{
2024-02-05 21:07:01 +02:00
if ( ! b - > subtype . as < SpellID > ( ) . hasValue ( ) )
2023-08-14 18:46:42 +02:00
continue ;
2024-02-05 21:07:01 +02:00
const CSpell * sp = b - > subtype . as < SpellID > ( ) . toSpell ( ) ;
2023-08-14 18:46:42 +02:00
const int32_t val = bl . valOfBonuses ( Selector : : typeSubtype ( b - > type , b - > subtype ) ) ;
const int32_t level = ( ( val > 3 ) ? ( val - 3 ) : val ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast battleCast ( & battle , st , spells : : Mode : : PASSIVE , sp ) ;
2023-08-14 18:46:42 +02:00
//this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle
battleCast . setEffectDuration ( 50 ) ;
battleCast . setSpellLevel ( level ) ;
spells : : Target target ;
if ( val > 3 )
{
2023-08-25 17:23:15 +02:00
for ( auto s : battle . battleGetAllStacks ( ) )
2023-08-28 16:43:57 +02:00
if ( battle . battleMatchOwner ( st , s , true ) & & s - > isValidTarget ( ) ) //all allied
2023-08-14 18:46:42 +02:00
target . emplace_back ( s ) ;
}
else
{
target . emplace_back ( st ) ;
}
battleCast . applyEffects ( gameHandler - > spellEnv , target , false , true ) ;
}
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : removeObstacle ( const CBattleInfoCallback & battle , const CObstacleInstance & obstacle )
2023-08-14 18:46:42 +02:00
{
BattleObstaclesChanged obsRem ;
2023-08-31 17:45:52 +02:00
obsRem . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
obsRem . changes . emplace_back ( obstacle . uniqueID , ObstacleChanges : : EOperation : : REMOVE ) ;
gameHandler - > sendAndApply ( & obsRem ) ;
}
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : stackTurnTrigger ( const CBattleInfoCallback & battle , const CStack * st )
2023-08-14 18:46:42 +02:00
{
BattleTriggerEffect bte ;
2023-08-31 17:45:52 +02:00
bte . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bte . stackID = st - > unitId ( ) ;
bte . effect = - 1 ;
bte . val = 0 ;
bte . additionalInfo = 0 ;
if ( st - > alive ( ) )
{
//unbind
if ( st - > hasBonus ( Selector : : type ( ) ( BonusType : : BIND_EFFECT ) ) )
{
bool unbind = true ;
BonusList bl = * ( st - > getBonuses ( Selector : : type ( ) ( BonusType : : BIND_EFFECT ) ) ) ;
2023-08-25 17:23:15 +02:00
auto adjacent = battle . battleAdjacentUnits ( st ) ;
2023-08-14 18:46:42 +02:00
for ( auto b : bl )
{
if ( b - > additionalInfo ! = CAddInfo : : NONE )
{
2023-08-25 17:23:15 +02:00
const CStack * stack = battle . battleGetStackByID ( b - > additionalInfo [ 0 ] ) ; //binding stack must be alive and adjacent
2023-08-14 18:46:42 +02:00
if ( stack )
{
if ( vstd : : contains ( adjacent , stack ) ) //binding stack is still present
unbind = false ;
}
}
else
{
unbind = false ;
}
}
if ( unbind )
{
BattleSetStackProperty ssp ;
2023-08-31 17:45:52 +02:00
ssp . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
ssp . which = BattleSetStackProperty : : UNBIND ;
ssp . stackID = st - > unitId ( ) ;
gameHandler - > sendAndApply ( & ssp ) ;
}
}
if ( st - > hasBonusOfType ( BonusType : : POISON ) )
{
2024-01-16 18:14:40 +02:00
std : : shared_ptr < const Bonus > b = st - > getFirstBonus ( Selector : : source ( BonusSource : : SPELL_EFFECT , BonusSourceID ( SpellID ( SpellID : : POISON ) ) ) . And ( Selector : : type ( ) ( BonusType : : STACK_HEALTH ) ) ) ;
2023-08-14 18:46:42 +02:00
if ( b ) //TODO: what if not?...
{
bte . val = std : : max ( b - > val - 10 , - ( st - > valOfBonuses ( BonusType : : POISON ) ) ) ;
if ( bte . val < b - > val ) //(negative) poison effect increases - update it
{
bte . effect = vstd : : to_underlying ( BonusType : : POISON ) ;
gameHandler - > sendAndApply ( & bte ) ;
}
}
}
if ( st - > hasBonusOfType ( BonusType : : MANA_DRAIN ) & & ! st - > drainedMana )
{
2024-01-05 17:56:00 +02:00
const CGHeroInstance * opponentHero = battle . battleGetFightingHero ( battle . otherSide ( st - > unitSide ( ) ) ) ;
2023-08-14 18:46:42 +02:00
if ( opponentHero )
{
ui32 manaDrained = st - > valOfBonuses ( BonusType : : MANA_DRAIN ) ;
vstd : : amin ( manaDrained , opponentHero - > mana ) ;
if ( manaDrained )
{
bte . effect = vstd : : to_underlying ( BonusType : : MANA_DRAIN ) ;
bte . val = manaDrained ;
bte . additionalInfo = opponentHero - > id . getNum ( ) ; //for sanity
gameHandler - > sendAndApply ( & bte ) ;
}
}
}
if ( st - > isLiving ( ) & & ! st - > hasBonusOfType ( BonusType : : FEARLESS ) )
{
bool fearsomeCreature = false ;
2023-08-28 16:43:57 +02:00
for ( const CStack * stack : battle . battleGetAllStacks ( true ) )
2023-08-14 18:46:42 +02:00
{
2023-08-28 16:43:57 +02:00
if ( battle . battleMatchOwner ( st , stack ) & & stack - > alive ( ) & & stack - > hasBonusOfType ( BonusType : : FEAR ) )
2023-08-14 18:46:42 +02:00
{
fearsomeCreature = true ;
break ;
}
}
if ( fearsomeCreature )
{
if ( gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) < 10 ) //fixed 10%
{
bte . effect = vstd : : to_underlying ( BonusType : : FEAR ) ;
gameHandler - > sendAndApply ( & bte ) ;
}
}
}
BonusList bl = * ( st - > getBonuses ( Selector : : type ( ) ( BonusType : : ENCHANTER ) ) ) ;
2024-01-04 23:54:35 +02:00
bl . remove_if ( [ ] ( const Bonus * b )
{
return b - > subtype . as < SpellID > ( ) = = SpellID : : NONE ;
} ) ;
2024-08-11 22:22:35 +02:00
BattleSide side = battle . playerToSide ( st - > unitOwner ( ) ) ;
2023-08-25 17:23:15 +02:00
if ( st - > canCast ( ) & & battle . battleGetEnchanterCounter ( side ) = = 0 )
2023-08-14 18:46:42 +02:00
{
bool cast = false ;
while ( ! bl . empty ( ) & & ! cast )
{
auto bonus = * RandomGeneratorUtil : : nextItem ( bl , gameHandler - > getRandomGenerator ( ) ) ;
2023-10-05 17:18:14 +02:00
auto spellID = bonus - > subtype . as < SpellID > ( ) ;
2023-08-14 18:46:42 +02:00
const CSpell * spell = SpellID ( spellID ) . toSpell ( ) ;
bl . remove_if ( [ & bonus ] ( const Bonus * b )
{
return b = = bonus . get ( ) ;
} ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , st , spells : : Mode : : ENCHANTER , spell ) ;
2023-08-14 18:46:42 +02:00
parameters . setSpellLevel ( bonus - > val ) ;
parameters . massive = true ;
parameters . smart = true ;
//todo: recheck effect level
if ( parameters . castIfPossible ( gameHandler - > spellEnv , spells : : Target ( 1 , spells : : Destination ( ) ) ) )
{
cast = true ;
int cooldown = bonus - > additionalInfo [ 0 ] ;
BattleSetStackProperty ssp ;
2023-08-31 17:45:52 +02:00
ssp . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
ssp . which = BattleSetStackProperty : : ENCHANTER_COUNTER ;
ssp . absolute = false ;
ssp . val = cooldown ;
ssp . stackID = st - > unitId ( ) ;
gameHandler - > sendAndApply ( & ssp ) ;
}
}
}
}
}
2023-08-17 18:18:14 +02:00
2023-08-28 16:43:57 +02:00
void BattleFlowProcessor : : setActiveStack ( const CBattleInfoCallback & battle , const battle : : Unit * stack )
2023-08-17 18:18:14 +02:00
{
assert ( stack ) ;
BattleSetActiveStack sas ;
2023-08-31 17:45:52 +02:00
sas . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-17 18:18:14 +02:00
sas . stack = stack - > unitId ( ) ;
gameHandler - > sendAndApply ( & sas ) ;
}