mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Merge pull request #4654 from dydzio0614/any-hex-shooting
Allow targeting empty hex by shooters with multi-tile SPELL_LIKE_ABILITY
This commit is contained in:
		| @@ -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); | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -69,7 +69,8 @@ | ||||
| 				"defensePointDamageFactor" :    { "type" : "number" }, | ||||
| 				"defensePointDamageFactorCap" : { "type" : "number" }, | ||||
| 				"oneHexTriggersObstacles" :     { "type" : "boolean" }, | ||||
| 				"layouts" :                     { "type" : "object" } | ||||
| 				"layouts" :                     { "type" : "object" }, | ||||
| 				"areaShotCanTargetEmptyHex" :   { "type" : "boolean" } | ||||
| 			} | ||||
| 		}, | ||||
| 		"creatures": { | ||||
|   | ||||
| @@ -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"                      }, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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(battleMatchOwner(attacker, defender) && defender->alive()) | ||||
| 	if(!emptyHexAreaAttack) | ||||
| 	{ | ||||
| 		if(!defender) | ||||
| 			return false; | ||||
|  | ||||
| 		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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user