2023-08-14 18:46:42 +02:00
/*
* BattleActionProcessor . 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 "BattleActionProcessor.h"
# include "BattleProcessor.h"
# include "../CGameHandler.h"
2024-07-20 14:55:17 +02:00
# include "../../lib/texts/CGeneralTextHandler.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/CStack.h"
2024-08-31 13:00:36 +02:00
# include "../../lib/IGameSettings.h"
2023-08-28 16:43:57 +02:00
# include "../../lib/battle/CBattleInfoCallback.h"
2023-10-23 12:59:15 +02:00
# include "../../lib/battle/CObstacleInstance.h"
2023-08-28 16:43:57 +02:00
# include "../../lib/battle/IBattleState.h"
2023-08-17 21:15:44 +02:00
# include "../../lib/battle/BattleAction.h"
2024-08-28 21:33:56 +02:00
# include "../../lib/entities/building/TownFortifications.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/gameState/CGameState.h"
2023-10-23 12:59:15 +02:00
# include "../../lib/networkPacks/PacksForClientBattle.h"
2023-10-24 00:27:52 +02:00
# include "../../lib/networkPacks/SetStackEffect.h"
2023-08-14 18:46:42 +02:00
# include "../../lib/spells/AbilityCaster.h"
# include "../../lib/spells/CSpellHandler.h"
# include "../../lib/spells/ISpellMechanics.h"
# include "../../lib/spells/Problem.h"
2024-06-01 17:28:17 +02:00
# include <vstd/RNG.h>
2024-04-26 11:44:57 +02:00
BattleActionProcessor : : BattleActionProcessor ( 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
{
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doEmptyAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doEndTacticsAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doWaitAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-17 18:18:14 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-17 18:18:14 +02:00
return false ;
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doRetreatAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
if ( ! battle . battleCanFlee ( battle . sideToPlayer ( ba . side ) ) )
2023-08-16 23:51:50 +02:00
{
gameHandler - > complain ( " Cannot retreat! " ) ;
return false ;
}
2024-08-11 22:22:35 +02:00
owner - > setBattleResult ( battle , EBattleResult : : ESCAPE , battle . otherSide ( ba . side ) ) ;
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doSurrenderAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
PlayerColor player = battle . sideToPlayer ( ba . side ) ;
2023-08-25 17:23:15 +02:00
int cost = battle . battleGetSurrenderCost ( player ) ;
2023-08-16 23:51:50 +02:00
if ( cost < 0 )
{
gameHandler - > complain ( " Cannot surrender! " ) ;
return false ;
}
if ( gameHandler - > getResource ( player , EGameResID : : GOLD ) < cost )
{
gameHandler - > complain ( " Not enough gold to surrender! " ) ;
return false ;
}
gameHandler - > giveResource ( player , EGameResID : : GOLD , - cost ) ;
2024-08-11 22:22:35 +02:00
owner - > setBattleResult ( battle , EBattleResult : : SURRENDER , battle . otherSide ( ba . side ) ) ;
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doHeroSpellAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-25 17:23:15 +02:00
const CGHeroInstance * h = battle . battleGetFightingHero ( ba . side ) ;
2023-08-16 23:51:50 +02:00
if ( ! h )
{
logGlobal - > error ( " Wrong caster! " ) ;
return false ;
}
2024-01-20 16:41:10 +02:00
if ( ! ba . spell . hasValue ( ) )
2023-08-16 23:51:50 +02:00
{
2023-08-17 18:18:14 +02:00
logGlobal - > error ( " Wrong spell id (%d)! " , ba . spell . getNum ( ) ) ;
2023-08-16 23:51:50 +02:00
return false ;
}
2024-01-20 16:41:10 +02:00
const CSpell * s = ba . spell . toSpell ( ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , h , spells : : Mode : : HERO , s ) ;
2023-08-16 23:51:50 +02:00
spells : : detail : : ProblemImpl problem ;
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
auto m = s - > battleMechanics ( & parameters ) ;
if ( ! m - > canBeCast ( problem ) ) //todo: should we check aimed cast?
{
logGlobal - > warn ( " Spell cannot be cast! " ) ;
std : : vector < std : : string > texts ;
problem . getAll ( texts ) ;
for ( auto s : texts )
logGlobal - > warn ( s ) ;
return false ;
}
2023-08-25 17:23:15 +02:00
parameters . cast ( gameHandler - > spellEnv , ba . getTarget ( & battle ) ) ;
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doWalkAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-25 17:23:15 +02:00
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-14 18:46:42 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
if ( target . size ( ) < 1 )
{
gameHandler - > complain ( " Destination required for move action. " ) ;
return false ;
}
2023-08-14 18:46:42 +02:00
2023-08-25 17:23:15 +02:00
int walkedTiles = moveStack ( battle , ba . stackNumber , target . at ( 0 ) . hexValue ) ; //move
2023-08-16 23:51:50 +02:00
if ( ! walkedTiles )
{
gameHandler - > complain ( " Stack failed movement! " ) ;
return false ;
}
return true ;
}
2023-08-14 18:46:42 +02:00
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doDefendAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
//defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.)
SetStackEffect sse ;
2023-08-31 17:45:52 +02:00
sse . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-10-21 13:50:42 +02:00
Bonus defenseBonusToAdd ( BonusDuration : : STACK_GETS_TURN , BonusType : : PRIMARY_SKILL , BonusSource : : OTHER , 20 , BonusSourceID ( ) , BonusSubtypeID ( PrimarySkill : : DEFENSE ) , BonusValueType : : PERCENT_TO_ALL ) ;
Bonus bonus2 ( BonusDuration : : STACK_GETS_TURN , BonusType : : PRIMARY_SKILL , BonusSource : : OTHER , stack - > valOfBonuses ( BonusType : : DEFENSIVE_STANCE ) , BonusSourceID ( ) , BonusSubtypeID ( PrimarySkill : : DEFENSE ) , BonusValueType : : ADDITIVE_VALUE ) ;
Bonus alternativeWeakCreatureBonus ( BonusDuration : : STACK_GETS_TURN , BonusType : : PRIMARY_SKILL , BonusSource : : OTHER , 1 , BonusSourceID ( ) , BonusSubtypeID ( PrimarySkill : : DEFENSE ) , BonusValueType : : ADDITIVE_VALUE ) ;
2023-08-16 23:51:50 +02:00
2023-10-21 13:50:42 +02:00
BonusList defence = * stack - > getBonuses ( Selector : : typeSubtype ( BonusType : : PRIMARY_SKILL , BonusSubtypeID ( PrimarySkill : : DEFENSE ) ) ) ;
2023-08-16 23:51:50 +02:00
int oldDefenceValue = defence . totalValue ( ) ;
defence . push_back ( std : : make_shared < Bonus > ( defenseBonusToAdd ) ) ;
defence . push_back ( std : : make_shared < Bonus > ( bonus2 ) ) ;
int difference = defence . totalValue ( ) - oldDefenceValue ;
std : : vector < Bonus > buffer ;
if ( difference = = 0 ) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0)
{
difference = 1 ;
buffer . push_back ( alternativeWeakCreatureBonus ) ;
}
else
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
buffer . push_back ( defenseBonusToAdd ) ;
}
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
buffer . push_back ( bonus2 ) ;
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
sse . toUpdate . push_back ( std : : make_pair ( ba . stackNumber , buffer ) ) ;
gameHandler - > sendAndApply ( & sse ) ;
BattleLogMessage message ;
2023-08-31 17:45:52 +02:00
message . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-16 23:51:50 +02:00
MetaString text ;
stack - > addText ( text , EMetaText : : GENERAL_TXT , 120 ) ;
stack - > addNameReplacement ( text ) ;
text . replaceNumber ( difference ) ;
message . lines . push_back ( text ) ;
gameHandler - > sendAndApply ( & message ) ;
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doAttackAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-25 17:23:15 +02:00
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
if ( target . size ( ) < 2 )
{
gameHandler - > complain ( " Two destinations required for attack action. " ) ;
return false ;
2023-08-14 18:46:42 +02:00
}
2023-08-16 23:51:50 +02:00
BattleHex attackPos = target . at ( 0 ) . hexValue ;
BattleHex destinationTile = target . at ( 1 ) . hexValue ;
2023-08-25 17:23:15 +02:00
const CStack * destinationStack = battle . battleGetStackByPos ( destinationTile , true ) ;
2023-08-16 23:51:50 +02:00
if ( ! destinationStack )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
gameHandler - > complain ( " Invalid target to attack " ) ;
return false ;
}
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
BattleHex startingPos = stack - > getPosition ( ) ;
2023-08-25 17:23:15 +02:00
int distance = moveStack ( battle , ba . stackNumber , attackPos ) ;
2023-08-14 18:46:42 +02:00
2023-08-16 23:51:50 +02:00
logGlobal - > trace ( " %s will attack %s " , stack - > nodeName ( ) , destinationStack - > nodeName ( ) ) ;
if ( stack - > getPosition ( ) ! = attackPos & & ! ( stack - > doubleWide ( ) & & ( stack - > getPosition ( ) = = attackPos . cloneInDirection ( stack - > destShiftDir ( ) , false ) ) ) )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
// we were not able to reach destination tile, nor occupy specified hex
// abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine
return true ;
}
if ( destinationStack & & stack - > unitId ( ) = = destinationStack - > unitId ( ) ) //we should just move, it will be handled by following check
{
destinationStack = nullptr ;
}
if ( ! destinationStack )
{
gameHandler - > complain ( " Unit can not attack itself " ) ;
return false ;
}
if ( ! CStack : : isMeleeAttackPossible ( stack , destinationStack ) )
{
gameHandler - > complain ( " Attack cannot be performed! " ) ;
return false ;
}
//attack
int totalAttacks = stack - > totalAttacks . getMeleeValue ( ) ;
//TODO: move to CUnitState
2023-08-25 17:23:15 +02:00
const auto * attackingHero = battle . battleGetFightingHero ( ba . side ) ;
2023-08-16 23:51:50 +02:00
if ( attackingHero )
{
2023-10-21 13:50:42 +02:00
totalAttacks + = attackingHero - > valOfBonuses ( BonusType : : HERO_GRANTS_ATTACKS , BonusSubtypeID ( stack - > creatureId ( ) ) ) ;
2023-08-16 23:51:50 +02:00
}
2024-01-13 15:55:38 +02:00
static const auto firstStrikeSelector = Selector : : typeSubtype ( BonusType : : FIRST_STRIKE , BonusCustomSubtype : : damageTypeAll ) . Or ( Selector : : typeSubtype ( BonusType : : FIRST_STRIKE , BonusCustomSubtype : : damageTypeMelee ) ) ;
const bool firstStrike = destinationStack - > hasBonus ( firstStrikeSelector ) ;
2023-08-16 23:51:50 +02:00
const bool retaliation = destinationStack - > ableToRetaliate ( ) ;
2024-01-01 20:58:32 +02:00
bool ferocityApplied = false ;
2024-01-01 22:16:38 +02:00
int32_t defenderInitialQuantity = destinationStack - > getCount ( ) ;
2024-01-01 20:58:32 +02:00
2023-08-16 23:51:50 +02:00
for ( int i = 0 ; i < totalAttacks ; + + i )
{
//first strike
2024-01-13 15:55:38 +02:00
if ( i = = 0 & & firstStrike & & retaliation & & ! stack - > hasBonusOfType ( BonusType : : BLOCKS_RETALIATION ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
makeAttack ( battle , destinationStack , stack , 0 , stack - > getPosition ( ) , true , false , true ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-16 23:51:50 +02:00
//move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification
if ( stack - > alive ( ) & & ! stack - > hasBonusOfType ( BonusType : : NOT_ACTIVE ) & & destinationStack - > alive ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
makeAttack ( battle , stack , destinationStack , ( i ? 0 : distance ) , destinationTile , i = = 0 , false , false ) ; //no distance travelled on second attack
2024-01-01 20:58:32 +02:00
2024-01-01 22:16:38 +02:00
if ( ! ferocityApplied & & stack - > hasBonusOfType ( BonusType : : FEROCITY ) )
2024-01-01 20:58:32 +02:00
{
2024-01-01 22:16:38 +02:00
auto ferocityBonus = stack - > getBonus ( Selector : : type ( ) ( BonusType : : FEROCITY ) ) ;
int32_t requiredCreaturesToKill = ferocityBonus - > additionalInfo ! = CAddInfo : : NONE ? ferocityBonus - > additionalInfo [ 0 ] : 1 ;
if ( defenderInitialQuantity - destinationStack - > getCount ( ) > = requiredCreaturesToKill )
{
ferocityApplied = true ;
int additionalAttacksCount = stack - > valOfBonuses ( BonusType : : FEROCITY ) ;
totalAttacks + = additionalAttacksCount ;
}
2024-01-01 20:58:32 +02:00
}
2023-08-14 18:46:42 +02:00
}
2023-08-16 23:51:50 +02:00
//counterattack
//we check retaliation twice, so if it unblocked during attack it will work only on next attack
if ( stack - > alive ( )
& & ! stack - > hasBonusOfType ( BonusType : : BLOCKS_RETALIATION )
& & ( i = = 0 & & ! firstStrike )
& & retaliation & & destinationStack - > ableToRetaliate ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
makeAttack ( battle , destinationStack , stack , 0 , stack - > getPosition ( ) , true , false , true ) ;
2023-08-14 18:46:42 +02:00
}
2023-08-16 23:51:50 +02:00
}
//return
if ( stack - > hasBonusOfType ( BonusType : : RETURN_AFTER_STRIKE )
& & target . size ( ) = = 3
& & startingPos ! = stack - > getPosition ( )
& & startingPos = = target . at ( 2 ) . hexValue
& & stack - > alive ( ) )
{
2023-08-25 17:23:15 +02:00
moveStack ( battle , ba . stackNumber , startingPos ) ;
2023-08-16 23:51:50 +02:00
//NOTE: curStack->unitId() == ba.stackNumber (rev 1431)
}
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doShootAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-25 17:23:15 +02:00
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
if ( target . size ( ) < 1 )
{
gameHandler - > complain ( " Destination required for shot action. " ) ;
return false ;
}
auto destination = target . at ( 0 ) . hexValue ;
2023-08-25 17:23:15 +02:00
const CStack * destinationStack = battle . battleGetStackByPos ( destination ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! battle . battleCanShoot ( stack , destination ) )
2023-08-16 23:51:50 +02:00
{
gameHandler - > complain ( " Cannot shoot! " ) ;
return false ;
}
2024-09-22 15:07:44 +02:00
const bool emptyTileAreaAttack = battle . battleCanTargetEmptyHex ( stack ) ;
if ( ! destinationStack & & ! emptyTileAreaAttack )
2023-08-16 23:51:50 +02:00
{
gameHandler - > complain ( " No target to shoot! " ) ;
return false ;
}
2024-09-22 15:07:44 +02:00
bool firstStrike = false ;
if ( ! emptyTileAreaAttack )
{
static const auto firstStrikeSelector = Selector : : typeSubtype ( BonusType : : FIRST_STRIKE , BonusCustomSubtype : : damageTypeAll ) . Or ( Selector : : typeSubtype ( BonusType : : FIRST_STRIKE , BonusCustomSubtype : : damageTypeRanged ) ) ;
firstStrike = destinationStack - > hasBonus ( firstStrikeSelector ) ;
}
2024-01-13 15:55:38 +02:00
if ( ! firstStrike )
makeAttack ( battle , stack , destinationStack , 0 , destination , true , true , false ) ;
2023-08-16 23:51:50 +02:00
//ranged counterattack
2024-09-22 15:07:44 +02:00
if ( ! emptyTileAreaAttack
& & destinationStack - > hasBonusOfType ( BonusType : : RANGED_RETALIATION )
2023-08-16 23:51:50 +02:00
& & ! stack - > hasBonusOfType ( BonusType : : BLOCKS_RANGED_RETALIATION )
& & destinationStack - > ableToRetaliate ( )
2023-08-25 17:23:15 +02:00
& & battle . battleCanShoot ( destinationStack , stack - > getPosition ( ) )
2023-08-16 23:51:50 +02:00
& & stack - > alive ( ) ) //attacker may have died (fire shield)
{
2023-08-25 17:23:15 +02:00
makeAttack ( battle , destinationStack , stack , 0 , stack - > getPosition ( ) , true , true , true ) ;
2023-08-16 23:51:50 +02:00
}
//allow more than one additional attack
int totalRangedAttacks = stack - > totalAttacks . getRangedValue ( ) ;
//TODO: move to CUnitState
2023-08-25 17:23:15 +02:00
const auto * attackingHero = battle . battleGetFightingHero ( ba . side ) ;
2023-08-16 23:51:50 +02:00
if ( attackingHero )
{
2023-10-21 13:50:42 +02:00
totalRangedAttacks + = attackingHero - > valOfBonuses ( BonusType : : HERO_GRANTS_ATTACKS , BonusSubtypeID ( stack - > creatureId ( ) ) ) ;
2023-08-16 23:51:50 +02:00
}
2024-01-13 15:55:38 +02:00
for ( int i = firstStrike ? 0 : 1 ; i < totalRangedAttacks ; + + i )
2023-08-16 23:51:50 +02:00
{
2024-09-22 15:07:44 +02:00
if ( stack - > alive ( )
& & ( emptyTileAreaAttack | | destinationStack - > alive ( ) )
& & stack - > shots . canUse ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
makeAttack ( battle , stack , destinationStack , 0 , destination , false , true , false ) ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-16 23:51:50 +02:00
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doCatapultAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-25 17:23:15 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
2024-01-16 18:14:40 +02:00
std : : shared_ptr < const Bonus > catapultAbility = stack - > getFirstBonus ( Selector : : type ( ) ( BonusType : : CATAPULT ) ) ;
2023-10-21 13:50:42 +02:00
if ( ! catapultAbility | | catapultAbility - > subtype = = BonusSubtypeID ( ) )
2023-08-16 23:51:50 +02:00
{
gameHandler - > complain ( " We do not know how to shoot :P " ) ;
}
else
{
2023-10-05 17:18:14 +02:00
const CSpell * spell = catapultAbility - > subtype . as < SpellID > ( ) . toSpell ( ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , stack , spells : : Mode : : SPELL_LIKE_ATTACK , spell ) ; //We can shot infinitely by catapult
2023-08-16 23:51:50 +02:00
auto shotLevel = stack - > valOfBonuses ( Selector : : typeSubtype ( BonusType : : CATAPULT_EXTRA_SHOTS , catapultAbility - > subtype ) ) ;
parameters . setSpellLevel ( shotLevel ) ;
parameters . cast ( gameHandler - > spellEnv , target ) ;
}
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doUnitSpellAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
2023-08-25 17:23:15 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-17 18:18:14 +02:00
SpellID spellID = ba . spell ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
std : : shared_ptr < const Bonus > randSpellcaster = stack - > getBonus ( Selector : : type ( ) ( BonusType : : RANDOM_SPELLCASTER ) ) ;
2023-10-21 13:50:42 +02:00
std : : shared_ptr < const Bonus > spellcaster = stack - > getBonus ( Selector : : typeSubtype ( BonusType : : SPELLCASTER , BonusSubtypeID ( spellID ) ) ) ;
2023-08-14 18:46:42 +02:00
2023-12-24 01:10:05 +02:00
if ( ! spellcaster & & ! randSpellcaster )
{
2023-08-16 23:51:50 +02:00
gameHandler - > complain ( " That stack can't cast spells! " ) ;
2023-12-24 01:10:05 +02:00
return false ;
}
if ( randSpellcaster )
2023-08-16 23:51:50 +02:00
{
2023-12-24 01:10:05 +02:00
if ( target . size ( ) ! = 1 )
{
gameHandler - > complain ( " Invalid target for random spellcaster! " ) ;
return false ;
}
const battle : : Unit * subject = target [ 0 ] . unitValue ;
if ( target [ 0 ] . unitValue = = nullptr )
subject = battle . battleGetStackByPos ( target [ 0 ] . hexValue , true ) ;
if ( subject = = nullptr )
{
gameHandler - > complain ( " Invalid target for random spellcaster! " ) ;
return false ;
}
spellID = battle . getRandomBeneficialSpell ( gameHandler - > getRandomGenerator ( ) , stack , subject ) ;
if ( spellID = = SpellID : : NONE )
{
gameHandler - > complain ( " That stack can't cast spells! " ) ;
return false ;
}
2023-08-16 23:51:50 +02:00
}
2023-12-24 01:10:05 +02:00
const CSpell * spell = SpellID ( spellID ) . toSpell ( ) ;
spells : : BattleCast parameters ( & battle , stack , spells : : Mode : : CREATURE_ACTIVE , spell ) ;
int32_t spellLvl = 0 ;
if ( spellcaster )
vstd : : amax ( spellLvl , spellcaster - > val ) ;
if ( randSpellcaster )
vstd : : amax ( spellLvl , randSpellcaster - > val ) ;
parameters . setSpellLevel ( spellLvl ) ;
parameters . cast ( gameHandler - > spellEnv , target ) ;
2023-08-16 23:51:50 +02:00
return true ;
2023-08-14 18:46:42 +02:00
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : doHealAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
battle : : Target target = ba . getTarget ( & battle ) ;
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
if ( ! canStackAct ( battle , stack ) )
2023-08-16 23:51:50 +02:00
return false ;
if ( target . size ( ) < 1 )
{
gameHandler - > complain ( " Destination required for heal action. " ) ;
return false ;
}
const battle : : Unit * destStack = nullptr ;
2024-01-16 18:14:40 +02:00
std : : shared_ptr < const Bonus > healerAbility = stack - > getFirstBonus ( Selector : : type ( ) ( BonusType : : HEALER ) ) ;
2023-08-16 23:51:50 +02:00
if ( target . at ( 0 ) . unitValue )
destStack = target . at ( 0 ) . unitValue ;
else
2023-08-25 17:23:15 +02:00
destStack = battle . battleGetUnitByPos ( target . at ( 0 ) . hexValue ) ;
2023-08-16 23:51:50 +02:00
2024-07-29 17:58:07 +02:00
if ( stack = = nullptr | | destStack = = nullptr | | ! healerAbility | | ! healerAbility - > subtype . hasValue ( ) )
2023-08-16 23:51:50 +02:00
{
gameHandler - > complain ( " There is either no healer, no destination, or healer cannot heal :P " ) ;
}
else
{
2023-10-05 17:18:14 +02:00
const CSpell * spell = healerAbility - > subtype . as < SpellID > ( ) . toSpell ( ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , stack , spells : : Mode : : SPELL_LIKE_ATTACK , spell ) ; //We can heal infinitely by first aid tent
2023-08-16 23:51:50 +02:00
auto dest = battle : : Destination ( destStack , target . at ( 0 ) . hexValue ) ;
parameters . setSpellLevel ( 0 ) ;
parameters . cast ( gameHandler - > spellEnv , { dest } ) ;
}
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : canStackAct ( const CBattleInfoCallback & battle , const CStack * stack )
2023-08-16 23:51:50 +02:00
{
if ( ! stack )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
gameHandler - > complain ( " No such stack! " ) ;
return false ;
}
if ( ! stack - > alive ( ) )
{
gameHandler - > complain ( " This stack is dead: " + stack - > nodeName ( ) ) ;
return false ;
}
2023-08-28 16:43:57 +02:00
if ( battle . battleTacticDist ( ) )
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
if ( stack & & stack - > unitSide ( ) ! = battle . battleGetTacticsSide ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-16 23:51:50 +02:00
gameHandler - > complain ( " This is not a stack of side that has tactics! " ) ;
return false ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-17 18:18:14 +02:00
else
2023-08-16 23:51:50 +02:00
{
2023-08-28 16:43:57 +02:00
if ( stack ! = battle . battleActiveUnit ( ) )
2023-08-17 18:18:14 +02:00
{
gameHandler - > complain ( " Action has to be about active stack! " ) ;
return false ;
}
2023-08-16 23:51:50 +02:00
}
return true ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : dispatchBattleAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
switch ( ba . actionType )
{
2023-08-24 18:10:53 +02:00
case EActionType : : BAD_MORALE :
2023-08-16 23:51:50 +02:00
case EActionType : : NO_ACTION :
2023-08-25 17:23:15 +02:00
return doEmptyAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : END_TACTIC_PHASE :
2023-08-25 17:23:15 +02:00
return doEndTacticsAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : RETREAT :
2023-08-25 17:23:15 +02:00
return doRetreatAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : SURRENDER :
2023-08-25 17:23:15 +02:00
return doSurrenderAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : HERO_SPELL :
2023-08-25 17:23:15 +02:00
return doHeroSpellAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : WALK :
2023-08-25 17:23:15 +02:00
return doWalkAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : WAIT :
2023-08-25 17:23:15 +02:00
return doWaitAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : DEFEND :
2023-08-25 17:23:15 +02:00
return doDefendAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : WALK_AND_ATTACK :
2023-08-25 17:23:15 +02:00
return doAttackAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : SHOOT :
2023-08-25 17:23:15 +02:00
return doShootAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : CATAPULT :
2023-08-25 17:23:15 +02:00
return doCatapultAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : MONSTER_SPELL :
2023-08-25 17:23:15 +02:00
return doUnitSpellAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
case EActionType : : STACK_HEAL :
2023-08-25 17:23:15 +02:00
return doHealAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
}
gameHandler - > complain ( " Unrecognized action type received!! " ) ;
2023-08-14 18:46:42 +02:00
return false ;
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : makeBattleActionImpl ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-16 23:51:50 +02:00
{
logGlobal - > trace ( " Making action: %s " , ba . toString ( ) ) ;
2023-08-25 17:23:15 +02:00
const CStack * stack = battle . battleGetStackByID ( ba . stackNumber ) ;
2023-08-16 23:51:50 +02:00
2023-08-22 19:57:58 +02:00
// for these events client does not expects StartAction/EndAction wrapper
if ( ! ba . isBattleEndAction ( ) )
{
StartAction startAction ( ba ) ;
2023-08-31 17:45:52 +02:00
startAction . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-22 19:57:58 +02:00
gameHandler - > sendAndApply ( & startAction ) ;
}
2023-08-16 23:51:50 +02:00
2023-08-25 17:23:15 +02:00
bool result = dispatchBattleAction ( battle , ba ) ;
2023-08-16 23:51:50 +02:00
2023-08-22 19:57:58 +02:00
if ( ! ba . isBattleEndAction ( ) )
{
EndAction endAction ;
2023-08-31 17:45:52 +02:00
endAction . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-22 19:57:58 +02:00
gameHandler - > sendAndApply ( & endAction ) ;
}
2023-08-16 23:51:50 +02:00
if ( ba . actionType = = EActionType : : WAIT | | ba . actionType = = EActionType : : DEFEND | | ba . actionType = = EActionType : : SHOOT | | ba . actionType = = EActionType : : MONSTER_SPELL )
2023-08-28 16:43:57 +02:00
battle . handleObstacleTriggersForUnit ( * gameHandler - > spellEnv , * stack ) ;
2023-08-16 23:51:50 +02:00
return result ;
}
2023-08-28 16:43:57 +02:00
int BattleActionProcessor : : moveStack ( const CBattleInfoCallback & battle , int stack , BattleHex dest )
2023-08-14 18:46:42 +02:00
{
int ret = 0 ;
2023-08-28 16:43:57 +02:00
const CStack * curStack = battle . battleGetStackByID ( stack ) ;
2023-08-25 17:23:15 +02:00
const CStack * stackAtEnd = battle . battleGetStackByPos ( dest ) ;
2023-08-14 18:46:42 +02:00
assert ( curStack ) ;
assert ( dest < GameConstants : : BFIELD_SIZE ) ;
2023-08-28 16:43:57 +02:00
if ( battle . battleGetTacticDist ( ) )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
assert ( battle . isInTacticRange ( dest ) ) ;
2023-08-14 18:46:42 +02:00
}
auto start = curStack - > getPosition ( ) ;
if ( start = = dest )
return 0 ;
//initing necessary tables
2024-06-24 03:23:26 +02:00
auto accessibility = battle . getAccessibility ( curStack ) ;
2023-08-14 18:46:42 +02:00
std : : set < BattleHex > passed ;
//Ignore obstacles on starting position
passed . insert ( curStack - > getPosition ( ) ) ;
if ( curStack - > doubleWide ( ) )
passed . insert ( curStack - > occupiedHex ( ) ) ;
//shifting destination (if we have double wide stack and we can occupy dest but not be exactly there)
if ( ! stackAtEnd & & curStack - > doubleWide ( ) & & ! accessibility . accessible ( dest , curStack ) )
{
BattleHex shifted = dest . cloneInDirection ( curStack - > destShiftDir ( ) , false ) ;
if ( accessibility . accessible ( shifted , curStack ) )
dest = shifted ;
}
if ( ( stackAtEnd & & stackAtEnd ! = curStack & & stackAtEnd - > alive ( ) ) | | ! accessibility . accessible ( dest , curStack ) )
{
gameHandler - > complain ( " Given destination is not accessible! " ) ;
return 0 ;
}
bool canUseGate = false ;
2023-08-28 16:43:57 +02:00
auto dbState = battle . battleGetGateState ( ) ;
2024-08-28 21:33:56 +02:00
if ( battle . battleGetFortifications ( ) . wallsHealth > 0 & & curStack - > unitSide ( ) = = BattleSide : : DEFENDER & &
2023-08-14 18:46:42 +02:00
dbState ! = EGateState : : DESTROYED & &
dbState ! = EGateState : : BLOCKED )
{
canUseGate = true ;
}
2023-08-25 17:23:15 +02:00
std : : pair < std : : vector < BattleHex > , int > path = battle . getPath ( start , dest , curStack ) ;
2023-08-14 18:46:42 +02:00
ret = path . second ;
2024-01-14 17:14:36 +02:00
int creSpeed = curStack - > getMovementRange ( 0 ) ;
2023-08-14 18:46:42 +02:00
2023-08-28 16:43:57 +02:00
if ( battle . battleGetTacticDist ( ) > 0 & & creSpeed > 0 )
2023-08-14 18:46:42 +02:00
creSpeed = GameConstants : : BFIELD_SIZE ;
2023-08-28 16:43:57 +02:00
bool hasWideMoat = vstd : : contains_if ( battle . battleGetAllObstaclesOnPos ( BattleHex ( BattleHex : : GATE_BRIDGE ) , false ) , [ ] ( const std : : shared_ptr < const CObstacleInstance > & obst )
2023-08-14 18:46:42 +02:00
{
return obst - > obstacleType = = CObstacleInstance : : MOAT ;
} ) ;
auto isGateDrawbridgeHex = [ & ] ( BattleHex hex ) - > bool
{
2023-08-19 21:35:44 +02:00
if ( hasWideMoat & & hex = = BattleHex : : GATE_BRIDGE )
2023-08-14 18:46:42 +02:00
return true ;
2023-08-19 21:35:44 +02:00
if ( hex = = BattleHex : : GATE_OUTER )
2023-08-14 18:46:42 +02:00
return true ;
2023-08-19 21:35:44 +02:00
if ( hex = = BattleHex : : GATE_INNER )
2023-08-14 18:46:42 +02:00
return true ;
return false ;
} ;
auto occupyGateDrawbridgeHex = [ & ] ( BattleHex hex ) - > bool
{
if ( isGateDrawbridgeHex ( hex ) )
return true ;
if ( curStack - > doubleWide ( ) )
{
BattleHex otherHex = curStack - > occupiedHex ( hex ) ;
if ( otherHex . isValid ( ) & & isGateDrawbridgeHex ( otherHex ) )
return true ;
}
return false ;
} ;
if ( curStack - > hasBonusOfType ( BonusType : : FLYING ) )
{
if ( path . second < = creSpeed & & path . first . size ( ) > 0 )
{
if ( canUseGate & & dbState ! = EGateState : : OPENED & &
occupyGateDrawbridgeHex ( dest ) )
{
BattleUpdateGateState db ;
2023-08-31 17:45:52 +02:00
db . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
db . state = EGateState : : OPENED ;
gameHandler - > sendAndApply ( & db ) ;
}
//inform clients about move
BattleStackMoved sm ;
2023-08-31 17:45:52 +02:00
sm . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
sm . stack = curStack - > unitId ( ) ;
std : : vector < BattleHex > tiles ;
tiles . push_back ( path . first [ 0 ] ) ;
sm . tilesToMove = tiles ;
sm . distance = path . second ;
sm . teleporting = false ;
gameHandler - > sendAndApply ( & sm ) ;
}
}
else //for non-flying creatures
{
std : : vector < BattleHex > tiles ;
const int tilesToMove = std : : max ( ( int ) ( path . first . size ( ) - creSpeed ) , 0 ) ;
int v = ( int ) path . first . size ( ) - 1 ;
path . first . push_back ( start ) ;
// check if gate need to be open or closed at some point
BattleHex openGateAtHex , gateMayCloseAtHex ;
if ( canUseGate )
{
for ( int i = ( int ) path . first . size ( ) - 1 ; i > = 0 ; i - - )
{
auto needOpenGates = [ & ] ( BattleHex hex ) - > bool
{
2023-08-19 21:35:44 +02:00
if ( hasWideMoat & & hex = = BattleHex : : GATE_BRIDGE )
2023-08-14 18:46:42 +02:00
return true ;
2023-08-19 21:35:44 +02:00
if ( hex = = BattleHex : : GATE_BRIDGE & & i - 1 > = 0 & & path . first [ i - 1 ] = = BattleHex : : GATE_OUTER )
2023-08-14 18:46:42 +02:00
return true ;
2023-08-19 21:35:44 +02:00
else if ( hex = = BattleHex : : GATE_OUTER | | hex = = BattleHex : : GATE_INNER )
2023-08-14 18:46:42 +02:00
return true ;
return false ;
} ;
auto hex = path . first [ i ] ;
if ( ! openGateAtHex . isValid ( ) & & dbState ! = EGateState : : OPENED )
{
if ( needOpenGates ( hex ) )
openGateAtHex = path . first [ i + 1 ] ;
//TODO we need find batter way to handle double-wide stacks
//currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug.
2024-04-22 14:33:01 +02:00
if ( curStack - > doubleWide ( ) & & i + 2 < path . first . size ( ) )
2023-08-14 18:46:42 +02:00
{
BattleHex otherHex = curStack - > occupiedHex ( hex ) ;
if ( otherHex . isValid ( ) & & needOpenGates ( otherHex ) )
openGateAtHex = path . first [ i + 2 ] ;
}
//gate may be opened and then closed during stack movement, but not other way around
if ( openGateAtHex . isValid ( ) )
dbState = EGateState : : OPENED ;
}
if ( ! gateMayCloseAtHex . isValid ( ) & & dbState ! = EGateState : : CLOSED )
{
2023-08-19 21:35:44 +02:00
if ( hex = = BattleHex : : GATE_INNER & & i - 1 > = 0 & & path . first [ i - 1 ] ! = BattleHex : : GATE_OUTER )
2023-08-14 18:46:42 +02:00
{
gateMayCloseAtHex = path . first [ i - 1 ] ;
}
if ( hasWideMoat )
{
2023-08-19 21:35:44 +02:00
if ( hex = = BattleHex : : GATE_BRIDGE & & i - 1 > = 0 & & path . first [ i - 1 ] ! = BattleHex : : GATE_OUTER )
2023-08-14 18:46:42 +02:00
{
gateMayCloseAtHex = path . first [ i - 1 ] ;
}
2023-08-19 21:35:44 +02:00
else if ( hex = = BattleHex : : GATE_OUTER & & i - 1 > = 0 & &
path . first [ i - 1 ] ! = BattleHex : : GATE_INNER & &
path . first [ i - 1 ] ! = BattleHex : : GATE_BRIDGE )
2023-08-14 18:46:42 +02:00
{
gateMayCloseAtHex = path . first [ i - 1 ] ;
}
}
2023-08-19 21:35:44 +02:00
else if ( hex = = BattleHex : : GATE_OUTER & & i - 1 > = 0 & & path . first [ i - 1 ] ! = BattleHex : : GATE_INNER )
2023-08-14 18:46:42 +02:00
{
gateMayCloseAtHex = path . first [ i - 1 ] ;
}
}
}
}
bool stackIsMoving = true ;
while ( stackIsMoving )
{
if ( v < tilesToMove )
{
logGlobal - > error ( " Movement terminated abnormally " ) ;
break ;
}
bool gateStateChanging = false ;
//special handling for opening gate on from starting hex
if ( openGateAtHex . isValid ( ) & & openGateAtHex = = start )
gateStateChanging = true ;
else
{
for ( bool obstacleHit = false ; ( ! obstacleHit ) & & ( ! gateStateChanging ) & & ( v > = tilesToMove ) ; - - v )
{
BattleHex hex = path . first [ v ] ;
tiles . push_back ( hex ) ;
if ( ( openGateAtHex . isValid ( ) & & openGateAtHex = = hex ) | |
( gateMayCloseAtHex . isValid ( ) & & gateMayCloseAtHex = = hex ) )
{
gateStateChanging = true ;
}
//if we walked onto something, finalize this portion of stack movement check into obstacle
2023-08-28 16:43:57 +02:00
if ( ! battle . battleGetAllObstaclesOnPos ( hex , false ) . empty ( ) )
2023-08-14 18:46:42 +02:00
obstacleHit = true ;
if ( curStack - > doubleWide ( ) )
{
BattleHex otherHex = curStack - > occupiedHex ( hex ) ;
//two hex creature hit obstacle by backside
2023-08-28 16:43:57 +02:00
auto obstacle2 = battle . battleGetAllObstaclesOnPos ( otherHex , false ) ;
2023-08-14 18:46:42 +02:00
if ( otherHex . isValid ( ) & & ! obstacle2 . empty ( ) )
obstacleHit = true ;
}
if ( ! obstacleHit )
passed . insert ( hex ) ;
}
}
if ( ! tiles . empty ( ) )
{
//commit movement
BattleStackMoved sm ;
2023-08-31 17:45:52 +02:00
sm . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
sm . stack = curStack - > unitId ( ) ;
sm . distance = path . second ;
sm . teleporting = false ;
sm . tilesToMove = tiles ;
gameHandler - > sendAndApply ( & sm ) ;
tiles . clear ( ) ;
}
//we don't handle obstacle at the destination tile -> it's handled separately in the if at the end
if ( curStack - > getPosition ( ) ! = dest )
{
if ( stackIsMoving & & start ! = curStack - > getPosition ( ) )
{
2023-08-28 16:43:57 +02:00
stackIsMoving = battle . handleObstacleTriggersForUnit ( * gameHandler - > spellEnv , * curStack , passed ) ;
2023-08-14 18:46:42 +02:00
passed . insert ( curStack - > getPosition ( ) ) ;
if ( curStack - > doubleWide ( ) )
passed . insert ( curStack - > occupiedHex ( ) ) ;
}
if ( gateStateChanging )
{
if ( curStack - > getPosition ( ) = = openGateAtHex )
{
openGateAtHex = BattleHex ( ) ;
//only open gate if stack is still alive
if ( curStack - > alive ( ) )
{
BattleUpdateGateState db ;
2023-08-31 17:45:52 +02:00
db . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
db . state = EGateState : : OPENED ;
gameHandler - > sendAndApply ( & db ) ;
}
}
else if ( curStack - > getPosition ( ) = = gateMayCloseAtHex )
{
gateMayCloseAtHex = BattleHex ( ) ;
2023-08-25 17:23:15 +02:00
owner - > updateGateState ( battle ) ;
2023-08-14 18:46:42 +02:00
}
}
}
else
//movement finished normally: we reached destination
stackIsMoving = false ;
}
}
//handle last hex separately for deviation
2024-08-31 13:00:36 +02:00
if ( gameHandler - > getSettings ( ) . getBoolean ( EGameSettings : : COMBAT_ONE_HEX_TRIGGERS_OBSTACLES ) )
2023-08-14 18:46:42 +02:00
{
if ( dest = = battle : : Unit : : occupiedHex ( start , curStack - > doubleWide ( ) , curStack - > unitSide ( ) )
| | start = = battle : : Unit : : occupiedHex ( dest , curStack - > doubleWide ( ) , curStack - > unitSide ( ) ) )
passed . clear ( ) ; //Just empty passed, obstacles will handled automatically
}
2023-10-28 17:34:11 +02:00
if ( dest = = start ) //If dest is equal to start, then we should handle obstacles for it anyway
passed . clear ( ) ; //Just empty passed, obstacles will handled automatically
2023-08-14 18:46:42 +02:00
//handling obstacle on the final field (separate, because it affects both flying and walking stacks)
2023-08-28 16:43:57 +02:00
battle . handleObstacleTriggersForUnit ( * gameHandler - > spellEnv , * curStack , passed ) ;
2023-08-14 18:46:42 +02:00
return ret ;
}
2023-08-28 16:43:57 +02:00
void BattleActionProcessor : : makeAttack ( const CBattleInfoCallback & battle , const CStack * attacker , const CStack * defender , int distance , BattleHex targetHex , bool first , bool ranged , bool counter )
2023-08-14 18:46:42 +02:00
{
2024-09-22 15:07:44 +02:00
if ( defender & & first & & ! counter )
2023-08-25 17:23:15 +02:00
handleAttackBeforeCasting ( battle , ranged , attacker , defender ) ;
2023-08-14 18:46:42 +02:00
FireShieldInfo fireShield ;
BattleAttack bat ;
BattleLogMessage blm ;
2023-08-31 17:45:52 +02:00
blm . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
bat . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
bat . attackerChanges . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bat . stackAttacking = attacker - > unitId ( ) ;
bat . tile = targetHex ;
std : : shared_ptr < battle : : CUnitState > attackerState = attacker - > acquireState ( ) ;
if ( ranged )
bat . flags | = BattleAttack : : SHOT ;
if ( counter )
bat . flags | = BattleAttack : : COUNTER ;
const int attackerLuck = attacker - > luckVal ( ) ;
if ( attackerLuck > 0 )
{
2024-08-31 13:00:36 +02:00
auto diceSize = gameHandler - > getSettings ( ) . getVector ( EGameSettings : : COMBAT_GOOD_LUCK_DICE ) ;
2023-10-13 20:43:15 +02:00
size_t diceIndex = std : : min < size_t > ( diceSize . size ( ) , attackerLuck ) - 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 )
bat . flags | = BattleAttack : : LUCKY ;
}
if ( attackerLuck < 0 )
{
2024-08-31 13:00:36 +02:00
auto diceSize = gameHandler - > getSettings ( ) . getVector ( EGameSettings : : COMBAT_BAD_LUCK_DICE ) ;
2023-10-13 20:43:15 +02:00
size_t diceIndex = std : : min < size_t > ( diceSize . size ( ) , - attackerLuck ) - 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 )
bat . flags | = BattleAttack : : UNLUCKY ;
}
if ( gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) < attacker - > valOfBonuses ( BonusType : : DOUBLE_DAMAGE_CHANCE ) )
{
bat . flags | = BattleAttack : : DEATH_BLOW ;
}
2023-09-05 16:22:11 +02:00
const auto * owner = battle . battleGetFightingHero ( attacker - > unitSide ( ) ) ;
2023-08-14 18:46:42 +02:00
if ( owner )
{
2023-10-21 13:50:42 +02:00
int chance = owner - > valOfBonuses ( BonusType : : BONUS_DAMAGE_CHANCE , BonusSubtypeID ( attacker - > creatureId ( ) ) ) ;
2023-08-14 18:46:42 +02:00
if ( chance > gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) )
bat . flags | = BattleAttack : : BALLISTA_DOUBLE_DMG ;
}
2024-06-09 23:54:20 +02:00
battle : : HealInfo healInfo ;
2023-08-14 18:46:42 +02:00
// only primary target
2024-09-22 15:07:44 +02:00
if ( defender & & defender - > alive ( ) )
2024-06-11 16:47:23 +02:00
applyBattleEffects ( battle , bat , attackerState , fireShield , defender , healInfo , distance , false ) ;
2023-08-14 18:46:42 +02:00
//multiple-hex normal attack
2023-08-25 17:23:15 +02:00
std : : set < const CStack * > attackedCreatures = battle . getAttackedCreatures ( attacker , targetHex , bat . shot ( ) ) ; //creatures other than primary target
2023-08-14 18:46:42 +02:00
for ( const CStack * stack : attackedCreatures )
{
if ( stack ! = defender & & stack - > alive ( ) ) //do not hit same stack twice
2024-06-11 16:47:23 +02:00
applyBattleEffects ( battle , bat , attackerState , fireShield , stack , healInfo , distance , true ) ;
2023-08-14 18:46:42 +02:00
}
2024-01-16 18:14:40 +02:00
std : : shared_ptr < const Bonus > bonus = attacker - > getFirstBonus ( Selector : : type ( ) ( BonusType : : SPELL_LIKE_ATTACK ) ) ;
2024-07-29 17:58:07 +02:00
if ( bonus & & ranged & & bonus - > subtype . hasValue ( ) ) //TODO: make it work in melee?
2023-08-14 18:46:42 +02:00
{
//this is need for displaying hit animation
bat . flags | = BattleAttack : : SPELL_LIKE ;
2023-10-05 17:18:14 +02:00
bat . spellID = bonus - > subtype . as < SpellID > ( ) ;
2023-08-14 18:46:42 +02:00
//TODO: should spell override creature`s projectile?
auto spell = bat . spellID . toSpell ( ) ;
battle : : Target target ;
target . emplace_back ( defender , targetHex ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast event ( & battle , attacker , spells : : Mode : : SPELL_LIKE_ATTACK , spell ) ;
2023-08-14 18:46:42 +02:00
event . setSpellLevel ( bonus - > val ) ;
auto attackedCreatures = spell - > battleMechanics ( & event ) - > getAffectedStacks ( target ) ;
//TODO: get exact attacked hex for defender
for ( const CStack * stack : attackedCreatures )
{
if ( stack ! = defender & & stack - > alive ( ) ) //do not hit same stack twice
{
2024-06-11 16:47:23 +02:00
applyBattleEffects ( battle , bat , attackerState , fireShield , stack , healInfo , distance , true ) ;
2023-08-14 18:46:42 +02:00
}
}
//now add effect info for all attacked stacks
for ( BattleStackAttacked & bsa : bat . bsa )
{
if ( bsa . attackerID = = attacker - > unitId ( ) ) //this is our attack and not f.e. fire shield
{
//this is need for displaying affect animation
bsa . flags | = BattleStackAttacked : : SPELL_EFFECT ;
2023-10-05 17:18:14 +02:00
bsa . spellID = bonus - > subtype . as < SpellID > ( ) ;
2023-08-14 18:46:42 +02:00
}
}
}
attackerState - > afterAttack ( ranged , counter ) ;
{
UnitChanges info ( attackerState - > unitId ( ) , UnitChanges : : EOperation : : RESET_STATE ) ;
attackerState - > save ( info . data ) ;
bat . attackerChanges . changedStacks . push_back ( info ) ;
}
2024-06-09 23:54:20 +02:00
if ( healInfo . healedHealthPoints > 0 )
2023-08-14 18:46:42 +02:00
bat . flags | = BattleAttack : : LIFE_DRAIN ;
2023-08-31 17:45:52 +02:00
for ( BattleStackAttacked & bsa : bat . bsa )
bsa . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
gameHandler - > sendAndApply ( & bat ) ;
{
const bool multipleTargets = bat . bsa . size ( ) > 1 ;
int64_t totalDamage = 0 ;
int32_t totalKills = 0 ;
for ( const BattleStackAttacked & bsa : bat . bsa )
{
totalDamage + = bsa . damageAmount ;
totalKills + = bsa . killedAmount ;
}
2024-06-09 23:54:20 +02:00
addGenericDamageLog ( blm , attackerState , totalDamage ) ;
2023-08-14 18:46:42 +02:00
2024-09-22 15:07:44 +02:00
if ( defender )
addGenericKilledLog ( blm , defender , totalKills , multipleTargets ) ;
2023-08-14 18:46:42 +02:00
}
// drain life effect (as well as log entry) must be applied after the attack
2024-06-09 23:54:20 +02:00
if ( healInfo . healedHealthPoints > 0 )
2023-08-14 18:46:42 +02:00
{
2024-06-09 23:54:20 +02:00
addGenericDrainedLifeLog ( blm , attackerState , defender , healInfo . healedHealthPoints ) ;
addGenericResurrectedLog ( blm , attackerState , defender , healInfo . resurrectedCount ) ;
2023-08-14 18:46:42 +02:00
}
if ( ! fireShield . empty ( ) )
{
//todo: this should be "virtual" spell instead, we only need fire spell school bonus here
const CSpell * fireShieldSpell = SpellID ( SpellID : : FIRE_SHIELD ) . toSpell ( ) ;
int64_t totalDamage = 0 ;
for ( const auto & item : fireShield )
{
const CStack * actor = item . first ;
int64_t rawDamage = item . second ;
2023-12-09 18:48:53 +02:00
const CGHeroInstance * actorOwner = battle . battleGetFightingHero ( actor - > unitSide ( ) ) ;
2023-08-14 18:46:42 +02:00
if ( actorOwner )
{
rawDamage = fireShieldSpell - > adjustRawDamage ( actorOwner , attacker , rawDamage ) ;
}
else
{
rawDamage = fireShieldSpell - > adjustRawDamage ( actor , attacker , rawDamage ) ;
}
totalDamage + = rawDamage ;
//FIXME: add custom effect on actor
}
if ( totalDamage > 0 )
{
BattleStackAttacked bsa ;
2023-08-31 17:45:52 +02:00
bsa . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
bsa . flags | = BattleStackAttacked : : FIRE_SHIELD ;
bsa . stackAttacked = attacker - > unitId ( ) ; //invert
bsa . attackerID = defender - > unitId ( ) ;
bsa . damageAmount = totalDamage ;
attacker - > prepareAttacked ( bsa , gameHandler - > getRandomGenerator ( ) ) ;
StacksInjured pack ;
2023-08-31 17:45:52 +02:00
pack . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
pack . stacks . push_back ( bsa ) ;
gameHandler - > sendAndApply ( & pack ) ;
// TODO: this is already implemented in Damage::describeEffect()
{
MetaString text ;
text . appendLocalString ( EMetaText : : GENERAL_TXT , 376 ) ;
2023-11-02 22:01:49 +02:00
text . replaceName ( SpellID ( SpellID : : FIRE_SHIELD ) ) ;
2023-08-14 18:46:42 +02:00
text . replaceNumber ( totalDamage ) ;
blm . lines . push_back ( std : : move ( text ) ) ;
}
addGenericKilledLog ( blm , attacker , bsa . killedAmount , false ) ;
}
}
gameHandler - > sendAndApply ( & blm ) ;
2024-09-22 15:07:44 +02:00
if ( defender )
handleAfterAttackCasting ( battle , ranged , attacker , defender ) ;
2023-08-14 18:46:42 +02:00
}
2024-01-07 20:20:32 +02:00
void BattleActionProcessor : : attackCasting ( const CBattleInfoCallback & battle , bool ranged , BonusType attackMode , const battle : : Unit * attacker , const CStack * defender )
2023-08-14 18:46:42 +02:00
{
if ( attacker - > hasBonusOfType ( attackMode ) )
{
TConstBonusListPtr spells = attacker - > getBonuses ( Selector : : type ( ) ( attackMode ) ) ;
2024-01-07 22:05:55 +02:00
std : : set < SpellID > spellsToCast = getSpellsForAttackCasting ( spells , defender ) ;
2024-01-07 20:20:32 +02:00
2023-08-14 18:46:42 +02:00
for ( SpellID spellID : spellsToCast )
{
bool castMe = false ;
if ( ! defender - > alive ( ) )
{
logGlobal - > debug ( " attackCasting: all attacked creatures have been killed " ) ;
return ;
}
int32_t spellLevel = 0 ;
2023-10-21 13:50:42 +02:00
TConstBonusListPtr spellsByType = attacker - > getBonuses ( Selector : : typeSubtype ( attackMode , BonusSubtypeID ( spellID ) ) ) ;
2023-08-14 18:46:42 +02:00
for ( const auto & sf : * spellsByType )
{
int meleeRanged ;
2024-01-14 17:13:33 +02:00
vstd : : amax ( spellLevel , sf - > additionalInfo [ 0 ] ) ;
meleeRanged = sf - > additionalInfo [ 1 ] ;
if ( meleeRanged = = CAddInfo : : NONE | | meleeRanged = = 0 | | ( meleeRanged = = 1 & & ranged ) | | ( meleeRanged = = 2 & & ! ranged ) )
2023-08-14 18:46:42 +02:00
castMe = true ;
}
2023-10-21 13:50:42 +02:00
int chance = attacker - > valOfBonuses ( ( Selector : : typeSubtype ( attackMode , BonusSubtypeID ( spellID ) ) ) ) ;
2023-08-14 18:46:42 +02:00
vstd : : amin ( chance , 100 ) ;
const CSpell * spell = SpellID ( spellID ) . toSpell ( ) ;
spells : : AbilityCaster caster ( attacker , spellLevel ) ;
spells : : Target target ;
target . emplace_back ( defender ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , & caster , spells : : Mode : : PASSIVE , spell ) ;
2023-08-14 18:46:42 +02:00
auto m = spell - > battleMechanics ( & parameters ) ;
spells : : detail : : ProblemImpl ignored ;
if ( ! m - > canBeCastAt ( target , ignored ) )
continue ;
//check if spell should be cast (probability handling)
if ( gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) > = chance )
continue ;
//casting
if ( castMe )
{
parameters . cast ( gameHandler - > spellEnv , target ) ;
}
}
}
}
2024-01-07 22:05:55 +02:00
std : : set < SpellID > BattleActionProcessor : : getSpellsForAttackCasting ( TConstBonusListPtr spells , const CStack * defender )
{
std : : set < SpellID > spellsToCast ;
constexpr int unlayeredItemsInternalLayer = - 1 ;
std : : map < int , std : : vector < std : : shared_ptr < Bonus > > > spellsWithBackupLayers ;
for ( int i = 0 ; i < spells - > size ( ) ; i + + )
{
std : : shared_ptr < Bonus > bonus = spells - > operator [ ] ( i ) ;
int layer = bonus - > additionalInfo [ 2 ] ;
vstd : : amax ( layer , - 1 ) ;
spellsWithBackupLayers [ layer ] . push_back ( bonus ) ;
}
auto addSpellsFromLayer = [ & ] ( int layer ) - > void
{
assert ( spellsWithBackupLayers . find ( layer ) ! = spellsWithBackupLayers . end ( ) ) ;
for ( const auto & spell : spellsWithBackupLayers [ layer ] )
{
if ( spell - > subtype . as < SpellID > ( ) ! = SpellID ( ) )
spellsToCast . insert ( spell - > subtype . as < SpellID > ( ) ) ;
else
logGlobal - > error ( " Invalid spell to cast during attack! " ) ;
}
} ;
if ( spellsWithBackupLayers . find ( unlayeredItemsInternalLayer ) ! = spellsWithBackupLayers . end ( ) )
{
addSpellsFromLayer ( unlayeredItemsInternalLayer ) ;
spellsWithBackupLayers . erase ( unlayeredItemsInternalLayer ) ;
}
for ( auto item : spellsWithBackupLayers )
{
2024-01-11 00:56:32 +02:00
bool areCurrentLayerSpellsApplied = std : : all_of ( item . second . begin ( ) , item . second . end ( ) ,
[ & ] ( const std : : shared_ptr < Bonus > spell )
{
std : : vector < SpellID > activeSpells = defender - > activeSpells ( ) ;
return vstd : : find ( activeSpells , spell - > subtype . as < SpellID > ( ) ) ! = activeSpells . end ( ) ;
} ) ;
2024-01-07 22:05:55 +02:00
2024-01-11 00:56:32 +02:00
if ( ! areCurrentLayerSpellsApplied | | item . first = = spellsWithBackupLayers . rbegin ( ) - > first )
{
addSpellsFromLayer ( item . first ) ;
break ;
2024-01-07 22:05:55 +02:00
}
}
return spellsToCast ;
}
2023-08-28 16:43:57 +02:00
void BattleActionProcessor : : handleAttackBeforeCasting ( const CBattleInfoCallback & battle , bool ranged , const CStack * attacker , const CStack * defender )
2023-08-14 18:46:42 +02:00
{
2023-08-25 17:23:15 +02:00
attackCasting ( battle , ranged , BonusType : : SPELL_BEFORE_ATTACK , attacker , defender ) ; //no death stare / acid breath needed?
2023-08-14 18:46:42 +02:00
}
2024-01-13 15:55:07 +02:00
void BattleActionProcessor : : handleDeathStare ( const CBattleInfoCallback & battle , bool ranged , const CStack * attacker , const CStack * defender )
2023-08-14 18:46:42 +02:00
{
2024-01-11 00:23:10 +02:00
// mechanics of Death Stare as in H3:
// each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
//original formula x = min(x, (gorgons_count + 9)/10);
/* mechanics of Accurate Shot as in HotA:
* each creature in an attacking stack has a X % chance of killing a creature in the attacked squad ,
* but the total number of killed creatures cannot be more than ( number of creatures in an attacking squad ) * X / 100 ( rounded up ) .
* X = 3 multiplier for shooting without penalty and X = 2 if shooting with penalty . Ability doesn ' t work if shooting at creatures behind walls .
*/
2023-08-14 18:46:42 +02:00
2024-01-13 15:55:07 +02:00
auto subtype = BonusCustomSubtype : : deathStareGorgon ;
2023-08-14 18:46:42 +02:00
2024-01-13 15:55:07 +02:00
if ( ranged )
2024-01-11 22:10:22 +02:00
{
2024-01-13 15:55:07 +02:00
bool rangePenalty = battle . battleHasDistancePenalty ( attacker , attacker - > getPosition ( ) , defender - > getPosition ( ) ) ;
bool obstaclePenalty = battle . battleHasWallPenalty ( attacker , attacker - > getPosition ( ) , defender - > getPosition ( ) ) ;
2024-01-11 00:23:10 +02:00
2024-01-13 15:55:07 +02:00
if ( rangePenalty )
{
if ( obstaclePenalty )
subtype = BonusCustomSubtype : : deathStareRangeObstaclePenalty ;
else
subtype = BonusCustomSubtype : : deathStareRangePenalty ;
}
else
{
if ( obstaclePenalty )
subtype = BonusCustomSubtype : : deathStareObstaclePenalty ;
else
subtype = BonusCustomSubtype : : deathStareNoRangePenalty ;
}
2024-01-11 00:23:10 +02:00
}
2023-08-14 18:46:42 +02:00
2024-01-13 15:55:07 +02:00
int singleCreatureKillChancePercent = attacker - > valOfBonuses ( BonusType : : DEATH_STARE , subtype ) ;
2024-01-11 00:23:10 +02:00
double chanceToKill = singleCreatureKillChancePercent / 100.0 ;
vstd : : amin ( chanceToKill , 1 ) ; //cap at 100%
2024-06-01 17:28:17 +02:00
int killedCreatures = gameHandler - > getRandomGenerator ( ) . nextBinomialInt ( attacker - > getCount ( ) , chanceToKill ) ;
2024-01-04 23:27:51 +02:00
2024-08-01 22:48:55 +02:00
int maxToKill = vstd : : divideAndCeil ( attacker - > getCount ( ) * singleCreatureKillChancePercent , 100 ) ;
2024-01-11 22:10:22 +02:00
vstd : : amin ( killedCreatures , maxToKill ) ;
2024-01-04 23:27:51 +02:00
2024-01-13 15:55:07 +02:00
killedCreatures + = ( attacker - > level ( ) * attacker - > valOfBonuses ( BonusType : : DEATH_STARE , BonusCustomSubtype : : deathStareCommander ) ) / defender - > level ( ) ;
2024-01-04 23:27:51 +02:00
2024-01-11 00:23:10 +02:00
if ( killedCreatures )
{
//TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but...
2024-01-04 23:27:51 +02:00
2024-01-11 00:23:10 +02:00
SpellID spellID = SpellID ( SpellID : : DEATH_STARE ) ; //also used as fallback spell for ACCURATE_SHOT
2024-01-13 15:55:07 +02:00
auto bonus = attacker - > getBonus ( Selector : : typeSubtype ( BonusType : : DEATH_STARE , subtype ) ) ;
if ( bonus & & bonus - > additionalInfo [ 0 ] ! = SpellID : : NONE )
spellID = SpellID ( bonus - > additionalInfo [ 0 ] ) ;
2024-01-06 19:26:13 +02:00
2024-01-11 00:23:10 +02:00
const CSpell * spell = spellID . toSpell ( ) ;
spells : : AbilityCaster caster ( attacker , 0 ) ;
2024-01-06 19:26:13 +02:00
2024-01-11 00:23:10 +02:00
spells : : BattleCast parameters ( & battle , & caster , spells : : Mode : : PASSIVE , spell ) ;
spells : : Target target ;
target . emplace_back ( defender ) ;
parameters . setEffectValue ( killedCreatures ) ;
parameters . cast ( gameHandler - > spellEnv , target ) ;
}
}
2024-01-04 23:27:51 +02:00
2023-08-28 16:43:57 +02:00
void BattleActionProcessor : : handleAfterAttackCasting ( const CBattleInfoCallback & battle , bool ranged , const CStack * attacker , const CStack * defender )
2023-08-14 18:46:42 +02:00
{
if ( ! attacker - > alive ( ) | | ! defender - > alive ( ) ) // can be already dead
return ;
2023-08-25 17:23:15 +02:00
attackCasting ( battle , ranged , BonusType : : SPELL_AFTER_ATTACK , attacker , defender ) ;
2023-08-14 18:46:42 +02:00
if ( ! defender - > alive ( ) )
{
//don't try death stare or acid breath on dead stack (crash!)
return ;
}
2024-01-13 15:55:07 +02:00
if ( attacker - > hasBonusOfType ( BonusType : : DEATH_STARE ) )
handleDeathStare ( battle , ranged , attacker , defender ) ;
2023-08-14 18:46:42 +02:00
if ( ! defender - > alive ( ) )
return ;
int64_t acidDamage = 0 ;
TConstBonusListPtr acidBreath = attacker - > getBonuses ( Selector : : type ( ) ( BonusType : : ACID_BREATH ) ) ;
for ( const auto & b : * acidBreath )
{
if ( b - > additionalInfo [ 0 ] > gameHandler - > getRandomGenerator ( ) . nextInt ( 99 ) )
acidDamage + = b - > val ;
}
if ( acidDamage > 0 )
{
const CSpell * spell = SpellID ( SpellID : : ACID_BREATH_DAMAGE ) . toSpell ( ) ;
spells : : AbilityCaster caster ( attacker , 0 ) ;
2023-08-25 17:23:15 +02:00
spells : : BattleCast parameters ( & battle , & caster , spells : : Mode : : PASSIVE , spell ) ;
2023-08-14 18:46:42 +02:00
spells : : Target target ;
target . emplace_back ( defender ) ;
parameters . setEffectValue ( acidDamage * attacker - > getCount ( ) ) ;
parameters . cast ( gameHandler - > spellEnv , target ) ;
}
if ( ! defender - > alive ( ) )
return ;
if ( attacker - > hasBonusOfType ( BonusType : : TRANSMUTATION ) & & defender - > isLiving ( ) ) //transmutation mechanics, similar to WoG werewolf ability
{
double chanceToTrigger = attacker - > valOfBonuses ( BonusType : : TRANSMUTATION ) / 100.0f ;
vstd : : amin ( chanceToTrigger , 1 ) ; //cap at 100%
2024-06-01 15:36:38 +02:00
if ( gameHandler - > getRandomGenerator ( ) . nextDouble ( 0 , 1 ) > chanceToTrigger )
2023-08-14 18:46:42 +02:00
return ;
int bonusAdditionalInfo = attacker - > getBonus ( Selector : : type ( ) ( BonusType : : TRANSMUTATION ) ) - > additionalInfo [ 0 ] ;
2023-08-19 19:48:28 +02:00
if ( defender - > unitType ( ) - > getIndex ( ) = = bonusAdditionalInfo | |
2023-08-14 18:46:42 +02:00
( bonusAdditionalInfo = = CAddInfo : : NONE & & defender - > unitType ( ) - > getId ( ) = = attacker - > unitType ( ) - > getId ( ) ) )
return ;
battle : : UnitInfo resurrectInfo ;
2023-08-25 17:23:15 +02:00
resurrectInfo . id = battle . battleNextUnitId ( ) ;
2023-08-14 18:46:42 +02:00
resurrectInfo . summoned = false ;
resurrectInfo . position = defender - > getPosition ( ) ;
resurrectInfo . side = defender - > unitSide ( ) ;
if ( bonusAdditionalInfo ! = CAddInfo : : NONE )
resurrectInfo . type = CreatureID ( bonusAdditionalInfo ) ;
else
resurrectInfo . type = attacker - > creatureId ( ) ;
2023-10-21 13:50:42 +02:00
if ( attacker - > hasBonusOfType ( ( BonusType : : TRANSMUTATION ) , BonusCustomSubtype : : transmutationPerHealth ) )
2023-08-14 18:46:42 +02:00
resurrectInfo . count = std : : max ( ( defender - > getCount ( ) * defender - > getMaxHealth ( ) ) / resurrectInfo . type . toCreature ( ) - > getMaxHealth ( ) , 1u ) ;
2023-10-21 13:50:42 +02:00
else if ( attacker - > hasBonusOfType ( ( BonusType : : TRANSMUTATION ) , BonusCustomSubtype : : transmutationPerUnit ) )
2023-08-14 18:46:42 +02:00
resurrectInfo . count = defender - > getCount ( ) ;
else
return ; //wrong subtype
BattleUnitsChanged addUnits ;
2023-08-31 17:45:52 +02:00
addUnits . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
addUnits . changedStacks . emplace_back ( resurrectInfo . id , UnitChanges : : EOperation : : ADD ) ;
resurrectInfo . save ( addUnits . changedStacks . back ( ) . data ) ;
BattleUnitsChanged removeUnits ;
2023-08-31 17:45:52 +02:00
removeUnits . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
removeUnits . changedStacks . emplace_back ( defender - > unitId ( ) , UnitChanges : : EOperation : : REMOVE ) ;
gameHandler - > sendAndApply ( & removeUnits ) ;
gameHandler - > sendAndApply ( & addUnits ) ;
2023-09-15 12:59:02 +02:00
// send empty event to client
// temporary(?) workaround to force animations to trigger
StacksInjured fakeEvent ;
2024-01-05 19:39:38 +02:00
fakeEvent . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-09-15 12:59:02 +02:00
gameHandler - > sendAndApply ( & fakeEvent ) ;
2023-08-14 18:46:42 +02:00
}
2023-10-21 13:50:42 +02:00
if ( attacker - > hasBonusOfType ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillPercentage ) | | attacker - > hasBonusOfType ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillAmount ) )
2023-08-14 18:46:42 +02:00
{
double chanceToTrigger = 0 ;
int amountToDie = 0 ;
2023-10-21 13:50:42 +02:00
if ( attacker - > hasBonusOfType ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillPercentage ) ) //killing by percentage
2023-08-14 18:46:42 +02:00
{
2023-10-21 13:50:42 +02:00
chanceToTrigger = attacker - > valOfBonuses ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillPercentage ) / 100.0f ;
int percentageToDie = attacker - > getBonus ( Selector : : type ( ) ( BonusType : : DESTRUCTION ) . And ( Selector : : subtype ( ) ( BonusCustomSubtype : : destructionKillPercentage ) ) ) - > additionalInfo [ 0 ] ;
2023-08-14 18:46:42 +02:00
amountToDie = static_cast < int > ( defender - > getCount ( ) * percentageToDie * 0.01f ) ;
}
2023-10-21 13:50:42 +02:00
else if ( attacker - > hasBonusOfType ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillAmount ) ) //killing by count
2023-08-14 18:46:42 +02:00
{
2023-10-21 13:50:42 +02:00
chanceToTrigger = attacker - > valOfBonuses ( BonusType : : DESTRUCTION , BonusCustomSubtype : : destructionKillAmount ) / 100.0f ;
amountToDie = attacker - > getBonus ( Selector : : type ( ) ( BonusType : : DESTRUCTION ) . And ( Selector : : subtype ( ) ( BonusCustomSubtype : : destructionKillAmount ) ) ) - > additionalInfo [ 0 ] ;
2023-08-14 18:46:42 +02:00
}
vstd : : amin ( chanceToTrigger , 1 ) ; //cap trigger chance at 100%
2024-06-01 15:36:38 +02:00
if ( gameHandler - > getRandomGenerator ( ) . nextDouble ( 0 , 1 ) > chanceToTrigger )
2023-08-14 18:46:42 +02:00
return ;
BattleStackAttacked bsa ;
bsa . attackerID = - 1 ;
bsa . stackAttacked = defender - > unitId ( ) ;
bsa . damageAmount = amountToDie * defender - > getMaxHealth ( ) ;
bsa . flags = BattleStackAttacked : : SPELL_EFFECT ;
bsa . spellID = SpellID : : SLAYER ;
defender - > prepareAttacked ( bsa , gameHandler - > getRandomGenerator ( ) ) ;
StacksInjured si ;
2023-08-31 17:45:52 +02:00
si . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
si . stacks . push_back ( bsa ) ;
gameHandler - > sendAndApply ( & si ) ;
2023-08-31 17:45:52 +02:00
sendGenericKilledLog ( battle , defender , bsa . killedAmount , false ) ;
2023-08-14 18:46:42 +02:00
}
}
2024-06-11 16:47:23 +02:00
void BattleActionProcessor : : applyBattleEffects ( const CBattleInfoCallback & battle , BattleAttack & bat , std : : shared_ptr < battle : : CUnitState > attackerState , FireShieldInfo & fireShield , const CStack * def , battle : : HealInfo & healInfo , int distance , bool secondary ) const
2023-08-14 18:46:42 +02:00
{
BattleStackAttacked bsa ;
if ( secondary )
bsa . flags | = BattleStackAttacked : : SECONDARY ; //all other targets do not suffer from spells & spell-like abilities
bsa . attackerID = attackerState - > unitId ( ) ;
bsa . stackAttacked = def - > unitId ( ) ;
{
BattleAttackInfo bai ( attackerState . get ( ) , def , distance , bat . shot ( ) ) ;
bai . deathBlow = bat . deathBlow ( ) ;
bai . doubleDamage = bat . ballistaDoubleDmg ( ) ;
bai . luckyStrike = bat . lucky ( ) ;
bai . unluckyStrike = bat . unlucky ( ) ;
2023-08-25 17:23:15 +02:00
auto range = battle . calculateDmgRange ( bai ) ;
2023-08-28 16:43:57 +02:00
bsa . damageAmount = battle . getBattle ( ) - > getActualDamage ( range . damage , attackerState - > getCount ( ) , gameHandler - > getRandomGenerator ( ) ) ;
2023-08-14 18:46:42 +02:00
CStack : : prepareAttacked ( bsa , gameHandler - > getRandomGenerator ( ) , bai . defender - > acquireState ( ) ) ; //calculate casualties
}
//life drain handling
if ( attackerState - > hasBonusOfType ( BonusType : : LIFE_DRAIN ) & & def - > isLiving ( ) )
{
int64_t toHeal = bsa . damageAmount * attackerState - > valOfBonuses ( BonusType : : LIFE_DRAIN ) / 100 ;
2024-06-11 16:47:23 +02:00
healInfo + = attackerState - > heal ( toHeal , EHealLevel : : RESURRECT , EHealPower : : PERMANENT ) ;
2023-08-14 18:46:42 +02:00
}
//soul steal handling
if ( attackerState - > hasBonusOfType ( BonusType : : SOUL_STEAL ) & & def - > isLiving ( ) )
{
//we can have two bonuses - one with subtype 0 and another with subtype 1
//try to use permanent first, use only one of two
2023-10-21 13:50:42 +02:00
for ( const auto & subtype : { BonusCustomSubtype : : soulStealBattle , BonusCustomSubtype : : soulStealPermanent } )
2023-08-14 18:46:42 +02:00
{
if ( attackerState - > hasBonusOfType ( BonusType : : SOUL_STEAL , subtype ) )
{
int64_t toHeal = bsa . killedAmount * attackerState - > valOfBonuses ( BonusType : : SOUL_STEAL , subtype ) * attackerState - > getMaxHealth ( ) ;
2023-10-21 13:50:42 +02:00
bool permanent = subtype = = BonusCustomSubtype : : soulStealPermanent ;
2024-06-11 16:47:23 +02:00
healInfo + = attackerState - > heal ( toHeal , EHealLevel : : OVERHEAL , ( permanent ? EHealPower : : PERMANENT : EHealPower : : ONE_BATTLE ) ) ;
2023-08-14 18:46:42 +02:00
break ;
}
}
}
bat . bsa . push_back ( bsa ) ; //add this stack to the list of victims after drain life has been calculated
//fire shield handling
if ( ! bat . shot ( ) & &
! def - > isClone ( ) & &
def - > hasBonusOfType ( BonusType : : FIRE_SHIELD ) & &
2023-10-21 13:50:42 +02:00
! attackerState - > hasBonusOfType ( BonusType : : SPELL_SCHOOL_IMMUNITY , BonusSubtypeID ( SpellSchool : : FIRE ) ) & &
! attackerState - > hasBonusOfType ( BonusType : : NEGATIVE_EFFECTS_IMMUNITY , BonusSubtypeID ( SpellSchool : : FIRE ) ) & &
attackerState - > valOfBonuses ( BonusType : : SPELL_DAMAGE_REDUCTION , BonusSubtypeID ( SpellSchool : : FIRE ) ) < 100 & &
2023-08-14 18:46:42 +02:00
CStack : : isMeleeAttackPossible ( attackerState . get ( ) , def ) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack)
)
{
//TODO: use damage with bonus but without penalties
auto fireShieldDamage = ( std : : min < int64_t > ( def - > getAvailableHealth ( ) , bsa . damageAmount ) * def - > valOfBonuses ( BonusType : : FIRE_SHIELD ) ) / 100 ;
fireShield . push_back ( std : : make_pair ( def , fireShieldDamage ) ) ;
}
}
2023-08-31 17:45:52 +02:00
void BattleActionProcessor : : sendGenericKilledLog ( const CBattleInfoCallback & battle , const CStack * defender , int32_t killed , bool multiple )
2023-08-14 18:46:42 +02:00
{
if ( killed > 0 )
{
BattleLogMessage blm ;
2023-08-31 17:45:52 +02:00
blm . battleID = battle . getBattle ( ) - > getBattleID ( ) ;
2023-08-14 18:46:42 +02:00
addGenericKilledLog ( blm , defender , killed , multiple ) ;
gameHandler - > sendAndApply ( & blm ) ;
}
}
2024-06-09 23:54:20 +02:00
void BattleActionProcessor : : addGenericKilledLog ( BattleLogMessage & blm , const CStack * defender , int32_t killed , bool multiple ) const
2023-08-14 18:46:42 +02:00
{
if ( killed > 0 )
{
2024-04-08 17:21:40 +02:00
MetaString line ;
2023-08-14 18:46:42 +02:00
2024-04-08 17:21:40 +02:00
if ( killed > 1 )
2023-08-14 18:46:42 +02:00
{
2024-04-08 17:21:40 +02:00
line . appendTextID ( " core.genrltxt.379 " ) ; // %d %s perished
line . replaceNumber ( killed ) ;
2023-08-14 18:46:42 +02:00
}
2024-04-08 17:21:40 +02:00
else
line . appendTextID ( " core.genrltxt.378 " ) ; // One %s perishes
if ( multiple )
2023-08-14 18:46:42 +02:00
{
2024-04-08 17:21:40 +02:00
if ( killed > 1 )
line . replaceTextID ( " core.genrltxt.43 " ) ; // creatures
else
line . replaceTextID ( " core.genrltxt.42 " ) ; // creature
2023-08-14 18:46:42 +02:00
}
2024-04-08 17:21:40 +02:00
else
2024-04-09 12:16:44 +02:00
line . replaceName ( defender - > unitType ( ) - > getId ( ) , killed ) ;
2024-04-08 17:21:40 +02:00
blm . lines . push_back ( line ) ;
2023-08-14 18:46:42 +02:00
}
}
2023-08-17 18:18:14 +02:00
2024-06-09 23:54:20 +02:00
void BattleActionProcessor : : addGenericDamageLog ( BattleLogMessage & blm , const std : : shared_ptr < battle : : CUnitState > & attackerState , int64_t damageDealt ) const
{
MetaString text ;
attackerState - > addText ( text , EMetaText : : GENERAL_TXT , 376 ) ;
attackerState - > addNameReplacement ( text ) ;
text . replaceNumber ( damageDealt ) ;
blm . lines . push_back ( std : : move ( text ) ) ;
}
void BattleActionProcessor : : addGenericDrainedLifeLog ( BattleLogMessage & blm , const std : : shared_ptr < battle : : CUnitState > & attackerState , const CStack * defender , int64_t drainedLife ) const
{
MetaString text ;
attackerState - > addText ( text , EMetaText : : GENERAL_TXT , 361 ) ;
attackerState - > addNameReplacement ( text ) ;
text . replaceNumber ( drainedLife ) ;
defender - > addNameReplacement ( text ) ;
blm . lines . push_back ( std : : move ( text ) ) ;
}
void BattleActionProcessor : : addGenericResurrectedLog ( BattleLogMessage & blm , const std : : shared_ptr < battle : : CUnitState > & attackerState , const CStack * defender , int64_t resurrected ) const
{
if ( resurrected > 0 )
{
auto text = blm . lines . back ( ) . toString ( ) ;
text . pop_back ( ) ; // erase '.' at the end of line with life drain info
MetaString ms = MetaString : : createFromRawString ( text ) ;
if ( resurrected = = 1 )
{
ms . appendLocalString ( EMetaText : : GENERAL_TXT , 363 ) ; // "\n and one rises from the dead."
}
else
{
ms . appendLocalString ( EMetaText : : GENERAL_TXT , 364 ) ; // "\n and %d rise from the dead."
ms . replaceNumber ( resurrected ) ;
}
blm . lines [ blm . lines . size ( ) - 1 ] = std : : move ( ms ) ;
}
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : makeAutomaticBattleAction ( const CBattleInfoCallback & battle , const BattleAction & ba )
2023-08-17 18:18:14 +02:00
{
2023-08-25 17:23:15 +02:00
return makeBattleActionImpl ( battle , ba ) ;
2023-08-17 18:18:14 +02:00
}
2023-08-28 16:43:57 +02:00
bool BattleActionProcessor : : makePlayerBattleAction ( const CBattleInfoCallback & battle , PlayerColor player , const BattleAction & ba )
2023-08-17 18:18:14 +02:00
{
2024-08-11 22:22:35 +02:00
if ( ba . side ! = BattleSide : : ATTACKER & & ba . side ! = BattleSide : : DEFENDER & & gameHandler - > complain ( " Can not make action - invalid battle side! " ) )
2023-08-17 18:18:14 +02:00
return false ;
2023-08-28 16:43:57 +02:00
if ( battle . battleGetTacticDist ( ) ! = 0 )
2023-08-17 18:18:14 +02:00
{
if ( ! ba . isTacticsAction ( ) )
{
gameHandler - > complain ( " Can not make actions while in tactics mode! " ) ;
return false ;
}
2023-08-28 16:43:57 +02:00
if ( player ! = battle . sideToPlayer ( ba . side ) )
2023-08-17 18:18:14 +02:00
{
gameHandler - > complain ( " Can not make actions in battles you are not part of! " ) ;
return false ;
}
}
else
{
2023-08-28 16:43:57 +02:00
auto active = battle . battleActiveUnit ( ) ;
2024-02-12 12:59:22 +02:00
if ( ! active )
{
gameHandler - > complain ( " No active unit in battle! " ) ;
2023-08-28 16:43:57 +02:00
return false ;
2024-02-12 12:59:22 +02:00
}
2023-08-28 16:43:57 +02:00
if ( ba . isUnitAction ( ) & & ba . stackNumber ! = active - > unitId ( ) )
2023-08-17 18:18:14 +02:00
{
gameHandler - > complain ( " Can not make actions - stack is not active! " ) ;
return false ;
}
2023-08-25 17:23:15 +02:00
auto unitOwner = battle . battleGetOwner ( active ) ;
2023-08-17 18:18:14 +02:00
2024-02-12 12:59:22 +02:00
if ( player ! = unitOwner )
{
gameHandler - > complain ( " Can not make actions in battles you are not part of! " ) ;
2023-08-17 18:18:14 +02:00
return false ;
2024-02-12 12:59:22 +02:00
}
2023-08-17 18:18:14 +02:00
}
2023-08-25 17:23:15 +02:00
return makeBattleActionImpl ( battle , ba ) ;
2023-08-17 18:18:14 +02:00
}