1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-10 22:31:40 +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:
Ivan Savenko
2024-09-27 19:23:33 +03:00
committed by GitHub
15 changed files with 156 additions and 48 deletions

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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())

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

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

View File

@@ -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" },

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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)