BattleActionsController.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. /*
  2. * BattleActionsController.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "BattleActionsController.h"
  12. #include "BattleFieldController.h"
  13. #include "BattleHero.h"
  14. #include "BattleInterface.h"
  15. #include "BattleSiegeController.h"
  16. #include "BattleStacksController.h"
  17. #include "BattleWindow.h"
  18. #include "../CPlayerInterface.h"
  19. #include "../GameEngine.h"
  20. #include "../GameInstance.h"
  21. #include "../gui/CIntObject.h"
  22. #include "../gui/CursorHandler.h"
  23. #include "../gui/WindowHandler.h"
  24. #include "../windows/CCreatureWindow.h"
  25. #include "../windows/InfoWindows.h"
  26. #include "../../lib/CConfigHandler.h"
  27. #include "../../lib/CRandomGenerator.h"
  28. #include "../../lib/CStack.h"
  29. #include "../../lib/battle/CUnitState.h"
  30. #include "../../lib/GameLibrary.h"
  31. #include "../../lib/battle/BattleAction.h"
  32. #include "../../lib/battle/CPlayerBattleCallback.h"
  33. #include "../../lib/callback/CCallback.h"
  34. #include "../../lib/spells/CSpellHandler.h"
  35. #include "../../lib/spells/ISpellMechanics.h"
  36. #include "../../lib/spells/effects/UnitEffect.h"
  37. #include "../../lib/spells/Problem.h"
  38. #include "../../lib/texts/CGeneralTextHandler.h"
  39. struct TextReplacement
  40. {
  41. std::string placeholder;
  42. std::string replacement;
  43. };
  44. using TextReplacementList = std::vector<TextReplacement>;
  45. static std::string replacePlaceholders(std::string input, const TextReplacementList & format )
  46. {
  47. for(const auto & entry : format)
  48. boost::replace_all(input, entry.placeholder, entry.replacement);
  49. return input;
  50. }
  51. static std::string translatePlural(int amount, const std::string& baseTextID)
  52. {
  53. if(amount == 1)
  54. return LIBRARY->generaltexth->translate(baseTextID + ".1");
  55. return LIBRARY->generaltexth->translate(baseTextID);
  56. }
  57. static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID)
  58. {
  59. std::string baseString = translatePlural(amount, baseTextID);
  60. TextReplacementList replacements {
  61. { "%d", amountString }
  62. };
  63. return replacePlaceholders(baseString, replacements);
  64. }
  65. static std::string formatPlural(int amount, const std::string & baseTextID)
  66. {
  67. return formatPluralImpl(amount, std::to_string(amount), baseTextID);
  68. }
  69. static std::string formatPlural(DamageRange range, const std::string & baseTextID)
  70. {
  71. if (range.min == range.max)
  72. return formatPlural(range.min, baseTextID);
  73. std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max);
  74. return formatPluralImpl(range.max, rangeString, baseTextID);
  75. }
  76. static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft)
  77. {
  78. TextReplacementList replacements = {
  79. { "%CREATURE", creatureName },
  80. { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") },
  81. { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") },
  82. { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") },
  83. };
  84. return replacePlaceholders(LIBRARY->generaltexth->translate(baseTextID), replacements);
  85. }
  86. static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName)
  87. {
  88. std::string baseTextID = estimation.kills.max == 0 ?
  89. "vcmi.battleWindow.damageEstimation.melee" :
  90. "vcmi.battleWindow.damageEstimation.meleeKills";
  91. return formatAttack(estimation, creatureName, baseTextID, 0);
  92. }
  93. static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft)
  94. {
  95. std::string baseTextID = estimation.kills.max == 0 ?
  96. "vcmi.battleWindow.damageEstimation.ranged" :
  97. "vcmi.battleWindow.damageEstimation.rangedKills";
  98. return formatAttack(estimation, creatureName, baseTextID, shotsLeft);
  99. }
  100. static std::string formatRetaliation(const DamageEstimation & estimation, bool mayBeKilled)
  101. {
  102. if (estimation.damage.max == 0)
  103. return LIBRARY->generaltexth->translate("vcmi.battleWindow.damageRetaliation.never");
  104. std::string baseTextID = estimation.kills.max == 0 ?
  105. "vcmi.battleWindow.damageRetaliation.damage" :
  106. "vcmi.battleWindow.damageRetaliation.damageKills";
  107. std::string prefixTextID = mayBeKilled ?
  108. "vcmi.battleWindow.damageRetaliation.may" :
  109. "vcmi.battleWindow.damageRetaliation.will";
  110. return LIBRARY->generaltexth->translate(prefixTextID) + formatAttack(estimation, "", baseTextID, 0);
  111. }
  112. static std::string prepareSpellEffectText(int gnrlTextID, const spells::effects::SpellEffectValue & value,
  113. std::string_view spellName, std::string_view targetName)
  114. {
  115. auto const & templateText = LIBRARY->generaltexth->allTexts[gnrlTextID];
  116. std::string baseText;
  117. if(!targetName.empty() && !spellName.empty())
  118. baseText = boost::str(boost::format(templateText) % spellName % targetName);
  119. else if(targetName.empty())
  120. baseText = boost::str(boost::format(templateText) % spellName);
  121. else
  122. baseText = boost::str(boost::format(templateText) % targetName);
  123. if(value.unitsDelta > 0)
  124. {
  125. auto unitTypeName = value.unitsDelta == 1 ? LIBRARY->creatures()->getById(value.unitType)->getNameSingularTranslated()
  126. : LIBRARY->creatures()->getById(value.unitType)->getNamePluralTranslated();
  127. return baseText +" (+ "+ std::to_string(value.unitsDelta) +" "+ unitTypeName +")";
  128. }
  129. if(value.hpDelta == 0)
  130. return baseText;
  131. std::string outputString;
  132. if(value.hpDelta > 0)
  133. {
  134. auto val = value.hpDelta;
  135. TextReplacementList replacements {{ "%d", std::to_string(val) }};
  136. int64_t correctPluralIndex = val > 3 ? 0 : std::clamp(val, int64_t(1), int64_t(2));
  137. std::string textTemplateKey;
  138. if(gnrlTextID == 549) //sacrifice spell
  139. textTemplateKey = "vcmi.battleWindow.sacrificeAcquiredHealth.";
  140. else
  141. textTemplateKey = "vcmi.battleWindow.healValuePreview.";
  142. outputString = LIBRARY->generaltexth->translate(textTemplateKey + std::to_string(correctPluralIndex));
  143. outputString = replacePlaceholders(outputString, replacements);
  144. }
  145. else
  146. {
  147. outputString = formatPlural(value.hpDelta * -1, "vcmi.battleWindow.damageEstimation.damage");
  148. if(value.unitsDelta < 0)
  149. outputString += ", "+ formatPlural(value.unitsDelta * -1, "vcmi.battleWindow.damageEstimation.kills");
  150. }
  151. return baseText +" ("+ outputString +")";
  152. }
  153. BattleActionsController::BattleActionsController(BattleInterface & owner):
  154. owner(owner),
  155. selectedStack(nullptr),
  156. heroSpellToCast(nullptr)
  157. {
  158. }
  159. void BattleActionsController::endCastingSpell()
  160. {
  161. if(heroSpellToCast)
  162. {
  163. heroSpellToCast.reset();
  164. owner.windowObject->blockUI(false);
  165. }
  166. if(owner.stacksController->getActiveStack())
  167. {
  168. possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
  169. owner.windowObject->setPossibleActions(possibleActions);
  170. }
  171. selectedStack = nullptr;
  172. ENGINE->fakeMouseMove();
  173. }
  174. bool BattleActionsController::isActiveStackSpellcaster() const
  175. {
  176. const CStack * casterStack = owner.stacksController->getActiveStack();
  177. if (!casterStack)
  178. return false;
  179. bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
  180. return (spellcaster && casterStack->canCast());
  181. }
  182. void BattleActionsController::enterCreatureCastingMode()
  183. {
  184. //silently check for possible errors
  185. if (owner.tacticsMode)
  186. return;
  187. //hero is casting a spell
  188. if (heroSpellToCast)
  189. return;
  190. if (!owner.stacksController->getActiveStack())
  191. return;
  192. if(owner.getBattle()->battleCanTargetEmptyHex(owner.stacksController->getActiveStack()))
  193. {
  194. auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
  195. {
  196. return x.get() != PossiblePlayerBattleAction::SHOOT;
  197. };
  198. vstd::erase_if(possibleActions, actionFilterPredicate);
  199. ENGINE->fakeMouseMove();
  200. return;
  201. }
  202. if (!isActiveStackSpellcaster())
  203. return;
  204. for(const auto & action : possibleActions)
  205. {
  206. if (action.get() != PossiblePlayerBattleAction::NO_LOCATION)
  207. continue;
  208. const spells::Caster * caster = owner.stacksController->getActiveStack();
  209. const CSpell * spell = action.spell().toSpell();
  210. spells::Target target;
  211. target.emplace_back();
  212. spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
  213. auto m = spell->battleMechanics(&cast);
  214. spells::detail::ProblemImpl ignored;
  215. const bool isCastingPossible = m->canBeCastAt(target, ignored);
  216. if (isCastingPossible)
  217. {
  218. owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId());
  219. selectedStack = nullptr;
  220. ENGINE->cursor().set(Cursor::Combat::POINTER);
  221. }
  222. return;
  223. }
  224. possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
  225. auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
  226. {
  227. return !x.spellcast();
  228. };
  229. vstd::erase_if(possibleActions, actionFilterPredicate);
  230. ENGINE->fakeMouseMove();
  231. }
  232. std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
  233. {
  234. BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
  235. for(const auto & spell : creatureSpells)
  236. data.creatureSpellsToCast.push_back(spell->id);
  237. data.tacticsMode = owner.tacticsMode;
  238. auto allActions = owner.getBattle()->getClientActionsForStack(stack, data);
  239. allActions.push_back(PossiblePlayerBattleAction::HERO_INFO);
  240. allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO);
  241. return std::vector<PossiblePlayerBattleAction>(allActions);
  242. }
  243. void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack)
  244. {
  245. if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
  246. auto assignPriority = [&](const PossiblePlayerBattleAction & item
  247. ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
  248. {
  249. switch(item.get())
  250. {
  251. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  252. case PossiblePlayerBattleAction::ANY_LOCATION:
  253. case PossiblePlayerBattleAction::NO_LOCATION:
  254. case PossiblePlayerBattleAction::FREE_LOCATION:
  255. case PossiblePlayerBattleAction::OBSTACLE:
  256. case PossiblePlayerBattleAction::SACRIFICE:
  257. if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr)
  258. {
  259. PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack);
  260. bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != owner.curInt->playerID;
  261. bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == owner.curInt->playerID;
  262. if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast)
  263. return 1;
  264. }
  265. return 100; //bottom priority
  266. break;
  267. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
  268. return 2;
  269. break;
  270. case PossiblePlayerBattleAction::SHOOT:
  271. if(targetStack == nullptr || targetStack->unitSide() == stack->unitSide() || !targetStack->alive())
  272. return 100; //bottom priority
  273. return 4;
  274. break;
  275. case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
  276. return 5;
  277. break;
  278. case PossiblePlayerBattleAction::ATTACK:
  279. return 6;
  280. break;
  281. case PossiblePlayerBattleAction::WALK_AND_ATTACK:
  282. return 7;
  283. break;
  284. case PossiblePlayerBattleAction::MOVE_STACK:
  285. return 8;
  286. break;
  287. case PossiblePlayerBattleAction::CATAPULT:
  288. return 9;
  289. break;
  290. case PossiblePlayerBattleAction::HEAL:
  291. return 10;
  292. break;
  293. case PossiblePlayerBattleAction::CREATURE_INFO:
  294. return 11;
  295. break;
  296. case PossiblePlayerBattleAction::HERO_INFO:
  297. return 12;
  298. break;
  299. case PossiblePlayerBattleAction::TELEPORT:
  300. return 13;
  301. break;
  302. default:
  303. assert(0);
  304. return 200;
  305. break;
  306. }
  307. };
  308. auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs)
  309. {
  310. return assignPriority(lhs) < assignPriority(rhs);
  311. };
  312. std::sort(possibleActions.begin(), possibleActions.end(), comparer);
  313. }
  314. void BattleActionsController::castThisSpell(SpellID spellID)
  315. {
  316. heroSpellToCast = std::make_shared<BattleAction>();
  317. heroSpellToCast->actionType = EActionType::HERO_SPELL;
  318. heroSpellToCast->spell = spellID;
  319. heroSpellToCast->stackNumber = -1;
  320. heroSpellToCast->side = owner.curInt->cb->getBattle(owner.getBattleID())->battleGetMySide();
  321. //choosing possible targets
  322. const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
  323. assert(castingHero); // code below assumes non-null hero
  324. PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
  325. if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
  326. {
  327. heroSpellToCast->aimToHex(BattleHex::INVALID);
  328. owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
  329. endCastingSpell();
  330. }
  331. else
  332. {
  333. possibleActions.clear();
  334. possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
  335. ENGINE->fakeMouseMove();//update cursor
  336. }
  337. owner.windowObject->blockUI(true);
  338. }
  339. const CSpell * BattleActionsController::getHeroSpellToCast( ) const
  340. {
  341. if (heroSpellToCast)
  342. return heroSpellToCast->spell.toSpell();
  343. return nullptr;
  344. }
  345. const CSpell * BattleActionsController::getStackSpellToCast(const BattleHex & hoveredHex)
  346. {
  347. if (heroSpellToCast)
  348. return nullptr;
  349. if (!owner.stacksController->getActiveStack())
  350. return nullptr;
  351. if (!hoveredHex.isValid())
  352. return nullptr;
  353. auto action = selectAction(hoveredHex);
  354. if(owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::SPELL_LIKE_ATTACK))
  355. {
  356. auto bonus = owner.stacksController->getActiveStack()->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
  357. return bonus->subtype.as<SpellID>().toSpell();
  358. }
  359. if (action.spell() == SpellID::NONE)
  360. return nullptr;
  361. return action.spell().toSpell();
  362. }
  363. const CSpell * BattleActionsController::getCurrentSpell(const BattleHex & hoveredHex)
  364. {
  365. if (getHeroSpellToCast())
  366. return getHeroSpellToCast();
  367. return getStackSpellToCast(hoveredHex);
  368. }
  369. const CStack * BattleActionsController::getStackForHex(const BattleHex & hoveredHex)
  370. {
  371. const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
  372. if(shere)
  373. return shere;
  374. return owner.getBattle()->battleGetStackByPos(hoveredHex, false);
  375. }
  376. void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  377. {
  378. switch (action.get())
  379. {
  380. case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
  381. ENGINE->cursor().set(Cursor::Combat::POINTER);
  382. return;
  383. case PossiblePlayerBattleAction::MOVE_TACTICS:
  384. case PossiblePlayerBattleAction::MOVE_STACK:
  385. if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
  386. ENGINE->cursor().set(Cursor::Combat::FLY);
  387. else
  388. ENGINE->cursor().set(Cursor::Combat::MOVE);
  389. return;
  390. case PossiblePlayerBattleAction::ATTACK:
  391. case PossiblePlayerBattleAction::WALK_AND_ATTACK:
  392. case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
  393. {
  394. static const std::map<BattleHex::EDir, Cursor::Combat> sectorCursor = {
  395. {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST},
  396. {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST},
  397. {BattleHex::RIGHT, Cursor::Combat::HIT_WEST },
  398. {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST},
  399. {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST},
  400. {BattleHex::LEFT, Cursor::Combat::HIT_EAST },
  401. {BattleHex::TOP, Cursor::Combat::HIT_SOUTH },
  402. {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH }
  403. };
  404. auto direction = owner.fieldController->selectAttackDirection(targetHex);
  405. assert(sectorCursor.count(direction) > 0);
  406. if (sectorCursor.count(direction))
  407. ENGINE->cursor().set(sectorCursor.at(direction));
  408. return;
  409. }
  410. case PossiblePlayerBattleAction::SHOOT:
  411. if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
  412. ENGINE->cursor().set(Cursor::Combat::SHOOT_PENALTY);
  413. else
  414. ENGINE->cursor().set(Cursor::Combat::SHOOT);
  415. return;
  416. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  417. case PossiblePlayerBattleAction::ANY_LOCATION:
  418. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
  419. case PossiblePlayerBattleAction::FREE_LOCATION:
  420. case PossiblePlayerBattleAction::OBSTACLE:
  421. ENGINE->cursor().set(Cursor::Spellcast::SPELL);
  422. return;
  423. case PossiblePlayerBattleAction::TELEPORT:
  424. ENGINE->cursor().set(Cursor::Combat::TELEPORT);
  425. return;
  426. case PossiblePlayerBattleAction::SACRIFICE:
  427. ENGINE->cursor().set(Cursor::Combat::SACRIFICE);
  428. return;
  429. case PossiblePlayerBattleAction::HEAL:
  430. ENGINE->cursor().set(Cursor::Combat::HEAL);
  431. return;
  432. case PossiblePlayerBattleAction::CATAPULT:
  433. ENGINE->cursor().set(Cursor::Combat::SHOOT_CATAPULT);
  434. return;
  435. case PossiblePlayerBattleAction::CREATURE_INFO:
  436. ENGINE->cursor().set(Cursor::Combat::QUERY);
  437. return;
  438. case PossiblePlayerBattleAction::HERO_INFO:
  439. ENGINE->cursor().set(Cursor::Combat::HERO);
  440. return;
  441. }
  442. assert(0);
  443. }
  444. void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  445. {
  446. switch (action.get())
  447. {
  448. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  449. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
  450. case PossiblePlayerBattleAction::TELEPORT:
  451. case PossiblePlayerBattleAction::SACRIFICE:
  452. case PossiblePlayerBattleAction::FREE_LOCATION:
  453. ENGINE->cursor().set(Cursor::Combat::BLOCKED);
  454. return;
  455. default:
  456. if (targetHex == -1)
  457. ENGINE->cursor().set(Cursor::Combat::POINTER);
  458. else
  459. ENGINE->cursor().set(Cursor::Combat::BLOCKED);
  460. return;
  461. }
  462. assert(0);
  463. }
  464. std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  465. {
  466. const CStack * targetStack = getStackForHex(targetHex);
  467. switch (action.get()) //display console message, realize selected action
  468. {
  469. case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
  470. return (boost::format(LIBRARY->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s
  471. case PossiblePlayerBattleAction::MOVE_TACTICS:
  472. case PossiblePlayerBattleAction::MOVE_STACK:
  473. if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
  474. return (boost::format(LIBRARY->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
  475. else
  476. return (boost::format(LIBRARY->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here
  477. case PossiblePlayerBattleAction::ATTACK:
  478. case PossiblePlayerBattleAction::WALK_AND_ATTACK:
  479. case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
  480. {
  481. const auto * attacker = owner.stacksController->getActiveStack();
  482. BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
  483. assert(attackFromHex.isValid());
  484. int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex.toInt()] : 0;
  485. DamageEstimation retaliation;
  486. BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
  487. attackInfo.attackerPos = attackFromHex;
  488. DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
  489. estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
  490. estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
  491. bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount();
  492. return formatMeleeAttack(estimation, targetStack->getName()) + "\n" + formatRetaliation(retaliation, enemyMayBeKilled);
  493. }
  494. case PossiblePlayerBattleAction::SHOOT:
  495. {
  496. const auto * shooter = owner.stacksController->getActiveStack();
  497. if(targetStack == nullptr) //should be true only for spell-like attack
  498. {
  499. auto spellLikeAttackBonus = shooter->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
  500. assert(spellLikeAttackBonus != nullptr);
  501. const CSpell * spell = spellLikeAttackBonus->subtype.as<SpellID>().toSpell();
  502. DamageEstimation est = owner.getBattle()->estimateSpellLikeAttackDamage(shooter, spell, targetHex);
  503. return formatRangedAttack(est, spell->getNameTranslated(), shooter->shots.available());
  504. }
  505. DamageEstimation retaliation;
  506. BattleAttackInfo attackInfo(shooter, targetStack, 0, true );
  507. DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
  508. estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
  509. estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
  510. return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());
  511. }
  512. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  513. {
  514. const CSpell * spell = action.spell().toSpell();
  515. auto spellEffectValue =
  516. owner.getBattle()->getSpellEffectValue(spell, getCurrentSpellcaster(), getCurrentCastMode(), targetHex);
  517. // "Cast %s on %s" plus dmg and kills info or how many units are risen/summoned
  518. return prepareSpellEffectText(27, *spellEffectValue, spell->getNameTranslated(), targetStack->getName());
  519. }
  520. case PossiblePlayerBattleAction::ANY_LOCATION:
  521. {
  522. const CSpell * spell = action.spell().toSpell();
  523. if(!spell)
  524. return {};
  525. auto spellEffectValue =
  526. owner.getBattle()->getSpellEffectValue(spell, getCurrentSpellcaster(), getCurrentCastMode(), targetHex);
  527. // "Cast %s" plus dmg and kills info
  528. return prepareSpellEffectText(26, *spellEffectValue, spell->getNameTranslated(), "");
  529. }
  530. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
  531. return boost::str(boost::format(LIBRARY->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on %
  532. case PossiblePlayerBattleAction::TELEPORT:
  533. return LIBRARY->generaltexth->allTexts[25]; //Teleport Here
  534. case PossiblePlayerBattleAction::OBSTACLE:
  535. return LIBRARY->generaltexth->allTexts[550];
  536. case PossiblePlayerBattleAction::SACRIFICE:
  537. {
  538. const CSpell * spell = action.spell().toSpell();
  539. if(!spell)
  540. return {};
  541. auto spellEffectValue =
  542. owner.getBattle()->getSpellEffectValue(spell, getCurrentSpellcaster(), getCurrentCastMode(), targetHex);
  543. //sacrifice the %s
  544. return prepareSpellEffectText(549, *spellEffectValue, "", targetStack->getName());
  545. }
  546. case PossiblePlayerBattleAction::FREE_LOCATION:
  547. return boost::str(boost::format(LIBRARY->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
  548. case PossiblePlayerBattleAction::HEAL:
  549. {
  550. spells::effects::SpellEffectValue value = {};
  551. value.hpDelta = owner.getBattle()->getFirstAidHealValue(owner.currentHero(), targetStack);
  552. //Apply first aid to the %s plus heal value
  553. return prepareSpellEffectText(419, value, "", targetStack->getName());
  554. }
  555. case PossiblePlayerBattleAction::CATAPULT:
  556. return ""; // TODO
  557. case PossiblePlayerBattleAction::CREATURE_INFO:
  558. return (boost::format(LIBRARY->generaltexth->allTexts[297]) % targetStack->getName()).str();
  559. case PossiblePlayerBattleAction::HERO_INFO:
  560. return LIBRARY->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats"
  561. }
  562. assert(0);
  563. return "";
  564. }
  565. std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  566. {
  567. switch (action.get())
  568. {
  569. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  570. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
  571. return LIBRARY->generaltexth->allTexts[23];
  572. break;
  573. case PossiblePlayerBattleAction::TELEPORT:
  574. return LIBRARY->generaltexth->allTexts[24]; //Invalid Teleport Destination
  575. break;
  576. case PossiblePlayerBattleAction::SACRIFICE:
  577. return LIBRARY->generaltexth->allTexts[543]; //choose army to sacrifice
  578. break;
  579. case PossiblePlayerBattleAction::FREE_LOCATION:
  580. return boost::str(boost::format(LIBRARY->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here
  581. break;
  582. default:
  583. return "";
  584. }
  585. }
  586. bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  587. {
  588. const CStack * targetStack = getStackForHex(targetHex);
  589. bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID;
  590. switch (action.get())
  591. {
  592. case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
  593. return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0);
  594. case PossiblePlayerBattleAction::CREATURE_INFO:
  595. return (targetStack && targetStack->alive());
  596. case PossiblePlayerBattleAction::HERO_INFO:
  597. if (targetHex == BattleHex::HERO_ATTACKER)
  598. return owner.attackingHero != nullptr;
  599. if (targetHex == BattleHex::HERO_DEFENDER)
  600. return owner.defendingHero != nullptr;
  601. return false;
  602. case PossiblePlayerBattleAction::MOVE_TACTICS:
  603. case PossiblePlayerBattleAction::MOVE_STACK:
  604. if (!(targetStack && targetStack->alive())) //we can walk on dead stacks
  605. {
  606. const CStack * currentStack = owner.stacksController->getActiveStack();
  607. return currentStack && owner.getBattle()->toWhichHexMove(currentStack, targetHex).isValid();
  608. }
  609. return false;
  610. case PossiblePlayerBattleAction::ATTACK:
  611. case PossiblePlayerBattleAction::WALK_AND_ATTACK:
  612. case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
  613. {
  614. const CStack * currentStack = owner.stacksController->getActiveStack();
  615. return currentStack &&
  616. owner.getBattle()->battleCanAttackUnit(currentStack, targetStack) &&
  617. owner.getBattle()->battleCanAttackHex(currentStack, targetHex);
  618. }
  619. case PossiblePlayerBattleAction::SHOOT:
  620. {
  621. auto currentStack = owner.stacksController->getActiveStack();
  622. if(!owner.getBattle()->battleCanShoot(currentStack, targetHex))
  623. return false;
  624. if(targetStack == nullptr && owner.getBattle()->battleCanTargetEmptyHex(currentStack))
  625. {
  626. auto spellLikeAttackBonus = currentStack->getBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
  627. const CSpell * spellDataToCheck = spellLikeAttackBonus->subtype.as<SpellID>().toSpell();
  628. return isCastingPossibleHere(spellDataToCheck, nullptr, targetHex);
  629. }
  630. return true;
  631. }
  632. case PossiblePlayerBattleAction::NO_LOCATION:
  633. return false;
  634. case PossiblePlayerBattleAction::ANY_LOCATION:
  635. return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
  636. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  637. return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
  638. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
  639. if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
  640. {
  641. SpellID spellID = owner.getBattle()->getRandomBeneficialSpell(CRandomGenerator::getDefault(), owner.stacksController->getActiveStack(), targetStack);
  642. return spellID != SpellID::NONE;
  643. }
  644. return false;
  645. case PossiblePlayerBattleAction::TELEPORT:
  646. return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex);
  647. case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
  648. {
  649. if(!targetStack)
  650. return false;
  651. auto unit = targetStack->acquire();
  652. return targetStack != selectedStack && targetStackOwned && targetStack->alive()
  653. && unit->isLiving() && !unit->hasBonusOfType(BonusType::MECHANICAL);
  654. }
  655. case PossiblePlayerBattleAction::OBSTACLE:
  656. case PossiblePlayerBattleAction::FREE_LOCATION:
  657. return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
  658. case PossiblePlayerBattleAction::CATAPULT:
  659. return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
  660. case PossiblePlayerBattleAction::HEAL:
  661. return targetStack && targetStackOwned && targetStack->canBeHealed();
  662. }
  663. assert(0);
  664. return false;
  665. }
  666. void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, const BattleHex & targetHex)
  667. {
  668. const CStack * targetStack = getStackForHex(targetHex);
  669. switch (action.get()) //display console message, realize selected action
  670. {
  671. case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
  672. {
  673. owner.stackActivated(targetStack);
  674. return;
  675. }
  676. case PossiblePlayerBattleAction::MOVE_TACTICS:
  677. case PossiblePlayerBattleAction::MOVE_STACK:
  678. {
  679. const auto * activeStack = owner.stacksController->getActiveStack();
  680. auto toHex = owner.getBattle()->toWhichHexMove(activeStack, targetHex);
  681. assert(toHex.isValid());
  682. owner.giveCommand(EActionType::WALK, toHex);
  683. return;
  684. }
  685. case PossiblePlayerBattleAction::ATTACK:
  686. case PossiblePlayerBattleAction::WALK_AND_ATTACK:
  687. case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
  688. {
  689. bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
  690. auto attacker = owner.stacksController->getActiveStack();
  691. BattleHex attackFromHex = owner.getBattle()->fromWhichHexAttack(attacker, targetHex, owner.fieldController->selectAttackDirection(targetHex));
  692. assert(attackFromHex.isValid());
  693. BattleAction command = BattleAction::makeMeleeAttack(attacker, targetHex, attackFromHex, returnAfterAttack);
  694. owner.sendCommand(command, attacker);
  695. return;
  696. }
  697. case PossiblePlayerBattleAction::SHOOT:
  698. {
  699. owner.giveCommand(EActionType::SHOOT, targetHex);
  700. return;
  701. }
  702. case PossiblePlayerBattleAction::HEAL:
  703. {
  704. owner.giveCommand(EActionType::STACK_HEAL, targetHex);
  705. return;
  706. };
  707. case PossiblePlayerBattleAction::CATAPULT:
  708. {
  709. owner.giveCommand(EActionType::CATAPULT, targetHex);
  710. return;
  711. }
  712. case PossiblePlayerBattleAction::CREATURE_INFO:
  713. {
  714. ENGINE->windows().createAndPushWindow<CStackWindow>(targetStack, false);
  715. return;
  716. }
  717. case PossiblePlayerBattleAction::HERO_INFO:
  718. {
  719. if (targetHex == BattleHex::HERO_ATTACKER)
  720. owner.attackingHero->heroLeftClicked();
  721. if (targetHex == BattleHex::HERO_DEFENDER)
  722. owner.defendingHero->heroLeftClicked();
  723. return;
  724. }
  725. case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
  726. case PossiblePlayerBattleAction::ANY_LOCATION:
  727. case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
  728. case PossiblePlayerBattleAction::TELEPORT:
  729. case PossiblePlayerBattleAction::OBSTACLE:
  730. case PossiblePlayerBattleAction::SACRIFICE:
  731. case PossiblePlayerBattleAction::FREE_LOCATION:
  732. {
  733. if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
  734. {
  735. if (action.spell() == SpellID::SACRIFICE)
  736. {
  737. heroSpellToCast->aimToHex(targetHex);
  738. possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()});
  739. selectedStack = targetStack;
  740. return;
  741. }
  742. if (action.spell() == SpellID::TELEPORT)
  743. {
  744. heroSpellToCast->aimToUnit(targetStack);
  745. possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()});
  746. selectedStack = targetStack;
  747. return;
  748. }
  749. }
  750. if (!heroSpellcastingModeActive())
  751. {
  752. if (action.spell().hasValue())
  753. {
  754. owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell());
  755. }
  756. else //unknown random spell
  757. {
  758. owner.giveCommand(EActionType::MONSTER_SPELL, targetHex);
  759. }
  760. }
  761. else
  762. {
  763. assert(getHeroSpellToCast());
  764. switch (getHeroSpellToCast()->id.toEnum())
  765. {
  766. case SpellID::SACRIFICE:
  767. heroSpellToCast->aimToUnit(targetStack);//victim
  768. break;
  769. default:
  770. heroSpellToCast->aimToHex(targetHex);
  771. break;
  772. }
  773. owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
  774. endCastingSpell();
  775. }
  776. selectedStack = nullptr;
  777. return;
  778. }
  779. }
  780. assert(0);
  781. return;
  782. }
  783. PossiblePlayerBattleAction BattleActionsController::selectAction(const BattleHex & targetHex)
  784. {
  785. assert(owner.stacksController->getActiveStack() != nullptr);
  786. assert(!possibleActions.empty());
  787. assert(targetHex.isValid());
  788. if (owner.stacksController->getActiveStack() == nullptr)
  789. return PossiblePlayerBattleAction::INVALID;
  790. if (possibleActions.empty())
  791. return PossiblePlayerBattleAction::INVALID;
  792. const CStack * targetStack = getStackForHex(targetHex);
  793. reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack);
  794. for (PossiblePlayerBattleAction action : possibleActions)
  795. {
  796. if (actionIsLegal(action, targetHex))
  797. return action;
  798. }
  799. return possibleActions.front();
  800. }
  801. void BattleActionsController::onHexHovered(const BattleHex & hoveredHex)
  802. {
  803. if (owner.openingPlaying())
  804. {
  805. currentConsoleMsg = LIBRARY->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro");
  806. ENGINE->statusbar()->write(currentConsoleMsg);
  807. return;
  808. }
  809. if (owner.stacksController->getActiveStack() == nullptr)
  810. return;
  811. if (hoveredHex == BattleHex::INVALID)
  812. {
  813. if (!currentConsoleMsg.empty())
  814. ENGINE->statusbar()->clearIfMatching(currentConsoleMsg);
  815. currentConsoleMsg.clear();
  816. ENGINE->cursor().set(Cursor::Combat::BLOCKED);
  817. return;
  818. }
  819. auto action = selectAction(hoveredHex);
  820. std::string newConsoleMsg;
  821. if (actionIsLegal(action, hoveredHex))
  822. {
  823. actionSetCursor(action, hoveredHex);
  824. newConsoleMsg = actionGetStatusMessage(action, hoveredHex);
  825. }
  826. else
  827. {
  828. actionSetCursorBlocked(action, hoveredHex);
  829. newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex);
  830. }
  831. if (!currentConsoleMsg.empty())
  832. ENGINE->statusbar()->clearIfMatching(currentConsoleMsg);
  833. if (!newConsoleMsg.empty())
  834. ENGINE->statusbar()->write(newConsoleMsg);
  835. currentConsoleMsg = newConsoleMsg;
  836. }
  837. void BattleActionsController::onHoverEnded()
  838. {
  839. ENGINE->cursor().set(Cursor::Combat::POINTER);
  840. if (!currentConsoleMsg.empty())
  841. ENGINE->statusbar()->clearIfMatching(currentConsoleMsg);
  842. currentConsoleMsg.clear();
  843. }
  844. void BattleActionsController::onHexLeftClicked(const BattleHex & clickedHex)
  845. {
  846. if (owner.stacksController->getActiveStack() == nullptr)
  847. return;
  848. auto action = selectAction(clickedHex);
  849. std::string newConsoleMsg;
  850. if (!actionIsLegal(action, clickedHex))
  851. return;
  852. actionRealize(action, clickedHex);
  853. ENGINE->statusbar()->clear();
  854. }
  855. void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack)
  856. {
  857. creatureSpells.clear();
  858. bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
  859. if(casterStack->canCast() && spellcaster)
  860. {
  861. // faerie dragon can cast only one, randomly selected spell until their next move
  862. //TODO: faerie dragon type spell should be selected by server
  863. const auto spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack);
  864. if (spellToCast.hasValue())
  865. creatureSpells.push_back(spellToCast.toSpell());
  866. }
  867. TConstBonusListPtr bl = casterStack->getBonusesOfType(BonusType::SPELLCASTER);
  868. for(const auto & bonus : *bl)
  869. {
  870. if (bonus->additionalInfo[0] <= 0 && bonus->subtype.as<SpellID>().hasValue())
  871. creatureSpells.push_back(bonus->subtype.as<SpellID>().toSpell());
  872. }
  873. }
  874. const spells::Caster * BattleActionsController::getCurrentSpellcaster() const
  875. {
  876. if (heroSpellToCast)
  877. return owner.currentHero();
  878. else
  879. return owner.stacksController->getActiveStack();
  880. }
  881. spells::Mode BattleActionsController::getCurrentCastMode() const
  882. {
  883. if (heroSpellToCast)
  884. return spells::Mode::HERO;
  885. else
  886. return spells::Mode::CREATURE_ACTIVE;
  887. }
  888. bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, const BattleHex & targetHex)
  889. {
  890. assert(currentSpell);
  891. if (!currentSpell)
  892. return false;
  893. auto caster = getCurrentSpellcaster();
  894. const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE;
  895. spells::Target target;
  896. if(targetStack)
  897. target.emplace_back(targetStack);
  898. target.emplace_back(targetHex);
  899. spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell);
  900. auto m = currentSpell->battleMechanics(&cast);
  901. spells::detail::ProblemImpl problem; //todo: display problem in status bar
  902. return m->canBeCastAt(target, problem);
  903. }
  904. void BattleActionsController::activateStack()
  905. {
  906. const CStack * s = owner.stacksController->getActiveStack();
  907. if(s)
  908. {
  909. tryActivateStackSpellcasting(s);
  910. possibleActions = getPossibleActionsForStack(s);
  911. owner.windowObject->setPossibleActions(possibleActions);
  912. }
  913. }
  914. void BattleActionsController::onHexRightClicked(const BattleHex & clickedHex)
  915. {
  916. bool isCurrentStackInSpellcastMode = creatureSpellcastingModeActive();
  917. if (heroSpellcastingModeActive() || isCurrentStackInSpellcastMode)
  918. {
  919. endCastingSpell();
  920. CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("core.genrltxt.731")); // spell cancelled
  921. return;
  922. }
  923. auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true);
  924. if (selectedStack != nullptr)
  925. ENGINE->windows().createAndPushWindow<CStackWindow>(selectedStack, true);
  926. if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero)
  927. owner.attackingHero->heroRightClicked();
  928. if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero)
  929. owner.defendingHero->heroRightClicked();
  930. }
  931. bool BattleActionsController::heroSpellcastingModeActive() const
  932. {
  933. return heroSpellToCast != nullptr;
  934. }
  935. bool BattleActionsController::creatureSpellcastingModeActive() const
  936. {
  937. auto spellcastModePredicate = [](const PossiblePlayerBattleAction & action)
  938. {
  939. return action.spellcast() || action.get() == PossiblePlayerBattleAction::SHOOT; //for hotkey-eligible SPELL_LIKE_ATTACK creature should have only SHOOT action
  940. };
  941. return !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastModePredicate);
  942. }
  943. bool BattleActionsController::currentActionSpellcasting(const BattleHex & hoveredHex)
  944. {
  945. if (heroSpellToCast)
  946. return true;
  947. if (!owner.stacksController->getActiveStack())
  948. return false;
  949. auto action = selectAction(hoveredHex);
  950. return action.spellcast();
  951. }
  952. const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const
  953. {
  954. return possibleActions;
  955. }
  956. void BattleActionsController::setPriorityActions(const std::vector<PossiblePlayerBattleAction> & actions)
  957. {
  958. possibleActions = actions;
  959. }
  960. void BattleActionsController::resetCurrentStackPossibleActions()
  961. {
  962. possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
  963. }