Browse Source

Initial unconditionally working version

Dydzio 1 year ago
parent
commit
1a2d349267

+ 23 - 1
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:
@@ -514,6 +529,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(bonus != nullptr);
+				return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % spellLikeAttackBonus->subtype.as<SpellID>().toSpell()->getNameTranslated());
+			}
+
 			const auto * shooter = owner.stacksController->getActiveStack();
 
 			DamageEstimation retaliation;
@@ -1020,7 +1042,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 {
 	auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action)
 	{
-		return action.spellcast();
+		return action.spellcast() || action.get() == PossiblePlayerBattleAction::SHOOT;
 	};
 
 	bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate);

+ 47 - 5
lib/battle/CBattleInfoCallback.cpp

@@ -725,18 +725,48 @@ 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(attacker->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK))
+	{
+		auto bonus = attacker->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+		const CSpell * spell = bonus->subtype.as<SpellID>().toSpell();
+		if(spell->isDamage())
+		{
+			spells::BattleCast cast(this, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell);
+
+			if(vstd::find(spell->battleMechanics(&cast)->getTargetTypes(), spells::AimType::LOCATION) != spell->battleMechanics(&cast)->getTargetTypes().end())
+			{
+				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 +777,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 +1627,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

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

@@ -166,8 +166,6 @@ EffectTarget UnitEffect::transformTargetByRange(const Mechanics * m, const Targe
 	{
 		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

@@ -347,20 +347,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())
@@ -381,11 +388,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);
 		}
@@ -907,7 +912,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;
@@ -962,7 +967,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
@@ -1044,7 +1049,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
@@ -1110,7 +1116,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)