Ver código fonte

Merge pull request #4654 from dydzio0614/any-hex-shooting

Allow targeting empty hex by shooters with multi-tile SPELL_LIKE_ABILITY
Ivan Savenko 1 ano atrás
pai
commit
19db016473

+ 1 - 1
client/CPlayerInterface.cpp

@@ -930,7 +930,7 @@ void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttac
 			info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked));
 		}
 	}
-	assert(info.defender != nullptr);
+	assert(info.defender != nullptr || (info.spellEffect != SpellID::NONE && info.indirectAttack));
 	assert(info.attacker != nullptr);
 
 	battleInt->stackAttacking(info);

+ 56 - 10
client/battle/BattleActionsController.cpp

@@ -175,6 +175,18 @@ void BattleActionsController::enterCreatureCastingMode()
 	if (!owner.stacksController->getActiveStack())
 		return;
 
+	if(owner.getBattle()->battleCanTargetEmptyHex(owner.stacksController->getActiveStack()))
+	{
+		auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
+		{
+			return x.get() != PossiblePlayerBattleAction::SHOOT;
+		};
+
+		vstd::erase_if(possibleActions, actionFilterPredicate);
+		GH.fakeMouseMove();
+		return;
+	}
+
 	if (!isActiveStackSpellcaster())
 		return;
 
@@ -263,6 +275,9 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 				return 2;
 				break;
 			case PossiblePlayerBattleAction::SHOOT:
+				if(targetStack == nullptr || targetStack->unitSide() == stack->unitSide())
+					return 100; //bottom priority
+
 				return 4;
 				break;
 			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
@@ -356,6 +371,12 @@ const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex
 
 	auto action = selectAction(hoveredHex);
 
+	if(owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK))
+	{
+		auto bonus = owner.stacksController->getActiveStack()->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+		return bonus->subtype.as<SpellID>().toSpell();
+	}
+
 	if (action.spell() == SpellID::NONE)
 		return nullptr;
 
@@ -514,6 +535,13 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 
 		case PossiblePlayerBattleAction::SHOOT:
 		{
+			if(targetStack == nullptr) //should be true only for spell-like attack
+			{
+				auto spellLikeAttackBonus = owner.stacksController->getActiveStack()->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+				assert(spellLikeAttackBonus != nullptr);
+				return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % spellLikeAttackBonus->subtype.as<SpellID>().toSpell()->getNameTranslated());
+			}
+
 			const auto * shooter = owner.stacksController->getActiveStack();
 
 			DamageEstimation retaliation;
@@ -625,7 +653,20 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 			return false;
 
 		case PossiblePlayerBattleAction::SHOOT:
-			return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
+			{
+				auto currentStack = owner.stacksController->getActiveStack();
+				if(!owner.getBattle()->battleCanShoot(currentStack, targetHex))
+					return false;
+
+				if(targetStack == nullptr && owner.getBattle()->battleCanTargetEmptyHex(currentStack))
+				{
+					auto spellLikeAttackBonus = currentStack->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+					const CSpell * spellDataToCheck = spellLikeAttackBonus->subtype.as<SpellID>().toSpell();
+					return isCastingPossibleHere(spellDataToCheck, nullptr, targetHex);
+				}
+
+				return true;
+			}
 
 		case PossiblePlayerBattleAction::NO_LOCATION:
 			return false;
@@ -771,7 +812,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 				}
 			}
 
-			if (!spellcastingModeActive())
+			if (!heroSpellcastingModeActive())
 			{
 				if (action.spell().hasValue())
 				{
@@ -1018,14 +1059,9 @@ void BattleActionsController::activateStack()
 
 void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 {
-	auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action)
-	{
-		return action.spellcast();
-	};
+	bool isCurrentStackInSpellcastMode = creatureSpellcastingModeActive();
 
-	bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate);
-
-	if (spellcastingModeActive() || isCurrentStackInSpellcastMode)
+	if (heroSpellcastingModeActive() || isCurrentStackInSpellcastMode)
 	{
 		endCastingSpell();
 		CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled
@@ -1044,11 +1080,21 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 		owner.defendingHero->heroRightClicked();
 }
 
-bool BattleActionsController::spellcastingModeActive() const
+bool BattleActionsController::heroSpellcastingModeActive() const
 {
 	return heroSpellToCast != nullptr;
 }
 
+bool BattleActionsController::creatureSpellcastingModeActive() const
+{
+	auto spellcastModePredicate = [](const PossiblePlayerBattleAction & action)
+	{
+		return action.spellcast() || action.get() == PossiblePlayerBattleAction::SHOOT; //for hotkey-eligible SPELL_LIKE_ATTACK creature should have only SHOOT action
+	};
+
+	return !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastModePredicate);
+}
+
 bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex)
 {
 	if (heroSpellToCast)

+ 4 - 2
client/battle/BattleActionsController.h

@@ -82,8 +82,10 @@ public:
 	/// initialize list of potential actions for new active stack
 	void activateStack();
 
-	/// returns true if UI is currently in target selection mode
-	bool spellcastingModeActive() const;
+	/// returns true if UI is currently in hero spell target selection mode
+	bool heroSpellcastingModeActive() const;
+	/// returns true if UI is currently in "F" hotkey creature spell target selection mode
+	bool creatureSpellcastingModeActive() const;
 
 	/// returns true if one of the following is true:
 	/// - we are casting spell by hero

+ 3 - 1
client/battle/BattleFieldController.cpp

@@ -566,7 +566,9 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 		calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighlights);
 	}
 
-	bool useSpellRangeForMouse = hoveredHex != BattleHex::INVALID && owner.actionsController->currentActionSpellcasting(getHoveredHex());
+	bool useSpellRangeForMouse = hoveredHex != BattleHex::INVALID
+		&& (owner.actionsController->currentActionSpellcasting(getHoveredHex())
+			|| owner.actionsController->creatureSpellcastingModeActive()); //at least shooting with SPELL_LIKE_ATTACK can operate in spellcasting mode without being actual spellcast
 	bool useMoveRangeForMouse = !hoveredMoveHexes.empty() || !settings["battle"]["mouseShadow"].Bool();
 
 	const auto & hoveredMouseHexes = useSpellRangeForMouse ? hoveredSpellHexes : ( useMoveRangeForMouse ? hoveredMoveHexes : hoveredMouseHex);

+ 1 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -328,7 +328,7 @@ void BattleHero::setPhase(EHeroAnimType newPhase)
 
 void BattleHero::heroLeftClicked()
 {
-	if(owner.actionsController->spellcastingModeActive()) //we are casting a spell
+	if(owner.actionsController->heroSpellcastingModeActive()) //we are casting a spell
 		return;
 
 	if(!hero || !owner.makingTurn())

+ 2 - 1
client/battle/BattleStacksController.cpp

@@ -862,7 +862,8 @@ std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
 	spell = owner.actionsController->getCurrentSpell(hoveredHex);
 	caster = owner.actionsController->getCurrentSpellcaster();
 
-	if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell
+	//casting spell or in explicit spellcasting mode that also handles SPELL_LIKE_ATTACK
+	if(caster && spell && (owner.actionsController->currentActionSpellcasting(hoveredHex) || owner.actionsController->creatureSpellcastingModeActive()))
 	{
 		spells::Target target;
 		target.emplace_back(hoveredHex);

+ 11 - 11
client/battle/BattleWindow.cpp

@@ -538,7 +538,7 @@ void BattleWindow::tacticPhaseEnded()
 
 void BattleWindow::bOptionsf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	CCS->curh->set(Cursor::Map::POINTER);
@@ -548,7 +548,7 @@ void BattleWindow::bOptionsf()
 
 void BattleWindow::bSurrenderf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	int cost = owner.getBattle()->battleGetSurrenderCost();
@@ -568,7 +568,7 @@ void BattleWindow::bSurrenderf()
 
 void BattleWindow::bFleef()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	if ( owner.getBattle()->battleCanFlee() )
@@ -675,7 +675,7 @@ void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAct
 
 void BattleWindow::bAutofightf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	if(settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman)
@@ -712,7 +712,7 @@ void BattleWindow::bAutofightf()
 
 void BattleWindow::bSpellf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	if (!owner.makingTurn())
@@ -785,7 +785,7 @@ void BattleWindow::bSwitchActionf()
 
 void BattleWindow::bWaitf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	if (owner.stacksController->getActiveStack() != nullptr)
@@ -794,7 +794,7 @@ void BattleWindow::bWaitf()
 
 void BattleWindow::bDefencef()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	if (owner.stacksController->getActiveStack() != nullptr)
@@ -803,7 +803,7 @@ void BattleWindow::bDefencef()
 
 void BattleWindow::bConsoleUpf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	console->scrollUp();
@@ -811,7 +811,7 @@ void BattleWindow::bConsoleUpf()
 
 void BattleWindow::bConsoleDownf()
 {
-	if (owner.actionsController->spellcastingModeActive())
+	if (owner.actionsController->heroSpellcastingModeActive())
 		return;
 
 	console->scrollDown();
@@ -851,8 +851,8 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
 	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->spellcastingModeActive() : owner.actionsController->spellcastingModeActive());
-	setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->heroSpellcastingModeActive() : owner.actionsController->heroSpellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->heroSpellcastingModeActive());
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on || !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on || !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);

+ 2 - 0
config/gameConfig.json

@@ -335,6 +335,8 @@
 			"defensePointDamageFactorCap": 0.7,
 			// If set to true, double-wide creatures will trigger obstacle effect when moving one tile forward or backwards
 			"oneHexTriggersObstacles": false,
+			// Allow area shooters with SPELL_LIKE_ATTACK bonus such as liches or magogs to target empty hexes
+			"areaShotCanTargetEmptyHex" : false,
 			
 			// Positions of units on start of the combat
 			// If battle does not defines specific configuration, 'default' configuration will be used

+ 2 - 1
config/schemas/gameSettings.json

@@ -69,7 +69,8 @@
 				"defensePointDamageFactor" :    { "type" : "number" },
 				"defensePointDamageFactorCap" : { "type" : "number" },
 				"oneHexTriggersObstacles" :     { "type" : "boolean" },
-				"layouts" :                     { "type" : "object" }
+				"layouts" :                     { "type" : "object" },
+				"areaShotCanTargetEmptyHex" :   { "type" : "boolean" }
 			}
 		},
 		"creatures": {

+ 1 - 0
lib/GameSettings.cpp

@@ -40,6 +40,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"            },
 		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                           },
 		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                          },
+		{EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX,  "combat",    "areaShotCanTargetEmptyHex"        },
 		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"          },
 		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,  "combat",    "attackPointDamageFactorCap"       },
 		{EGameSettings::COMBAT_BAD_LUCK_DICE,                   "combat",    "badLuckDice"                      },

+ 1 - 0
lib/IGameSettings.h

@@ -18,6 +18,7 @@ enum class EGameSettings
 	BANKS_SHOW_GUARDS_COMPOSITION,
 	BONUSES_GLOBAL,
 	BONUSES_PER_HERO,
+	COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX,
 	COMBAT_ATTACK_POINT_DAMAGE_FACTOR,
 	COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,
 	COMBAT_BAD_LUCK_DICE,

+ 49 - 5
lib/battle/CBattleInfoCallback.cpp

@@ -17,6 +17,7 @@
 #include "BattleInfo.h"
 #include "CObstacleInstance.h"
 #include "DamageCalculator.h"
+#include "IGameSettings.h"
 #include "PossiblePlayerBattleAction.h"
 #include "../entities/building/TownFortifications.h"
 #include "../spells/ObstacleCasterProxy.h"
@@ -725,18 +726,49 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const
 			|| attacker->hasBonusOfType(BonusType::FREE_SHOOTING));
 }
 
+bool CBattleInfoCallback::battleCanTargetEmptyHex(const battle::Unit * attacker) const
+{
+	RETURN_IF_NOT_BATTLE(false);
+
+	if(!VLC->engineSettings()->getBoolean(EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX))
+		return false;
+
+	if(attacker->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK))
+	{
+		auto bonus = attacker->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+		const CSpell * spell = bonus->subtype.as<SpellID>().toSpell();
+		spells::BattleCast cast(this, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell);
+		BattleHex dummySpellTarget = BattleHex(50); //check arbitrary hex for general spell range since currently there is no general way to access amount of hexes
+
+		if(spell->battleMechanics(&cast)->rangeInHexes(dummySpellTarget).size() > 1)
+		{
+			return true;
+		}
+	}
+
+	return false;
+}
+
 bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const
 {
 	RETURN_IF_NOT_BATTLE(false);
 
 	const battle::Unit * defender = battleGetUnitByPos(dest);
-	if(!attacker || !defender)
+	if(!attacker)
 		return false;
 
-	if(defender->hasBonusOfType(BonusType::INVINCIBLE))
-		return false;
+	bool emptyHexAreaAttack = battleCanTargetEmptyHex(attacker);
+
+	if(!emptyHexAreaAttack)
+	{
+		if(!defender)
+			return false;
 
-	if(battleMatchOwner(attacker, defender) && defender->alive())
+		if(defender->hasBonusOfType(BonusType::INVINCIBLE))
+			return false;
+	}
+
+	if(emptyHexAreaAttack || (battleMatchOwner(attacker, defender) && defender->alive()))
 	{
 		if(battleCanShoot(attacker))
 		{
@@ -747,7 +779,11 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHe
 			}
 
 			int shootingRange = limitedRangeBonus->val;
-			return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange);
+
+			if(defender)
+				return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange);
+			else
+				return isHexWithinSpecifiedRange(attacker->getPosition(), dest, shootingRange);
 		}
 	}
 
@@ -1593,6 +1629,14 @@ bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosi
 	return false;
 }
 
+bool CBattleInfoCallback::isHexWithinSpecifiedRange(BattleHex attackerPosition, BattleHex targetPosition, unsigned int range) const
+{
+	if(BattleHex::getDistance(attackerPosition, targetPosition) <= range)
+		return true;
+
+	return false;
+}
+
 BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const
 {
 	RETURN_IF_NOT_BATTLE(BattleHex::INVALID);

+ 2 - 0
lib/battle/CBattleInfoCallback.h

@@ -86,9 +86,11 @@ public:
 	ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const;
 	std::set<BattleHex> battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const;
 	bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const;
+	bool isHexWithinSpecifiedRange(BattleHex attackerPosition, BattleHex targetPosition, unsigned int range) const;
 
 	std::pair< std::vector<BattleHex>, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const;
 
+	bool battleCanTargetEmptyHex(const battle::Unit * attacker) const; //determines of stack with given ID can target empty hex to attack - currently used only for SPELL_LIKE_ATTACK shooting
 	bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle

+ 1 - 2
lib/spells/effects/UnitEffect.cpp

@@ -164,10 +164,9 @@ EffectTarget UnitEffect::transformTargetByRange(const Mechanics * m, const Targe
 
 	if(m->alwaysHitFirstTarget())
 	{
+		//TODO: examine if adjustments needed related to INVINCIBLE bonus
 		if(!aimPoint.empty() && aimPoint.front().unitValue)
 			targets.insert(aimPoint.front().unitValue);
-		else
-			logGlobal->error("Spell-like attack with no primary target.");
 	}
 
 	EffectTarget effectTarget;

+ 20 - 13
server/battles/BattleActionProcessor.cpp

@@ -348,20 +348,27 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co
 		return false;
 	}
 
-	if (!destinationStack)
+	const bool emptyTileAreaAttack = battle.battleCanTargetEmptyHex(stack);
+
+	if (!destinationStack && !emptyTileAreaAttack)
 	{
 		gameHandler->complain("No target to shoot!");
 		return false;
 	}
 
-	static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged));
-	const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector);
+	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);
+	}
 
 	if (!firstStrike)
 		makeAttack(battle, stack, destinationStack, 0, destination, true, true, false);
 
 	//ranged counterattack
-	if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION)
+	if (!emptyTileAreaAttack
+		&& destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION)
 		&& !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION)
 		&& destinationStack->ableToRetaliate()
 		&& battle.battleCanShoot(destinationStack, stack->getPosition())
@@ -382,11 +389,9 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co
 
 	for(int i = firstStrike ? 0:1; i < totalRangedAttacks; ++i)
 	{
-		if(
-			stack->alive()
-			&& destinationStack->alive()
-			&& stack->shots.canUse()
-			)
+		if(stack->alive()
+			&& (emptyTileAreaAttack || destinationStack->alive())
+			&& stack->shots.canUse())
 		{
 			makeAttack(battle, stack, destinationStack, 0, destination, false, true, false);
 		}
@@ -908,7 +913,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta
 
 void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter)
 {
-	if(first && !counter)
+	if(defender && first && !counter)
 		handleAttackBeforeCasting(battle, ranged, attacker, defender);
 
 	FireShieldInfo fireShield;
@@ -963,7 +968,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
 	battle::HealInfo healInfo;
 
 	// only primary target
-	if(defender->alive())
+	if(defender && defender->alive())
 		applyBattleEffects(battle, bat, attackerState, fireShield, defender, healInfo, distance, false);
 
 	//multiple-hex normal attack
@@ -1045,7 +1050,8 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
 
 		addGenericDamageLog(blm, attackerState, totalDamage);
 
-		addGenericKilledLog(blm, defender, totalKills, multipleTargets);
+		if(defender)
+			addGenericKilledLog(blm, defender, totalKills, multipleTargets);
 	}
 
 	// drain life effect (as well as log entry) must be applied after the attack
@@ -1111,7 +1117,8 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
 
 	gameHandler->sendAndApply(&blm);
 
-	handleAfterAttackCasting(battle, ranged, attacker, defender);
+	if(defender)
+		handleAfterAttackCasting(battle, ranged, attacker, defender);
 }
 
 void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender)