/* * BattleActionsController.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "BattleActionsController.h" #include "BattleWindow.h" #include "BattleStacksController.h" #include "BattleInterface.h" #include "BattleFieldController.h" #include "BattleSiegeController.h" #include "BattleInterfaceClasses.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CIntObject.h" #include "../gui/WindowHandler.h" #include "../windows/CCreatureWindow.h" #include "../windows/InfoWindows.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" struct TextReplacement { std::string placeholder; std::string replacement; }; using TextReplacementList = std::vector; static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) { for(const auto & entry : format) boost::replace_all(input, entry.placeholder, entry.replacement); return input; } static std::string translatePlural(int amount, const std::string& baseTextID) { if(amount == 1) return CGI->generaltexth->translate(baseTextID + ".1"); return CGI->generaltexth->translate(baseTextID); } static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) { std::string baseString = translatePlural(amount, baseTextID); TextReplacementList replacements { { "%d", amountString } }; return replacePlaceholders(baseString, replacements); } static std::string formatPlural(int amount, const std::string & baseTextID) { return formatPluralImpl(amount, std::to_string(amount), baseTextID); } static std::string formatPlural(DamageRange range, const std::string & baseTextID) { if (range.min == range.max) return formatPlural(range.min, baseTextID); std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); return formatPluralImpl(range.max, rangeString, baseTextID); } static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) { TextReplacementList replacements = { { "%CREATURE", creatureName }, { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, }; return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); } static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) { std::string baseTextID = estimation.kills.max == 0 ? "vcmi.battleWindow.damageEstimation.melee" : "vcmi.battleWindow.damageEstimation.meleeKills"; return formatAttack(estimation, creatureName, baseTextID, 0); } static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) { std::string baseTextID = estimation.kills.max == 0 ? "vcmi.battleWindow.damageEstimation.ranged" : "vcmi.battleWindow.damageEstimation.rangedKills"; return formatAttack(estimation, creatureName, baseTextID, shotsLeft); } static std::string formatRetaliation(const DamageEstimation & estimation, bool mayBeKilled) { if (estimation.damage.max == 0) return CGI->generaltexth->translate("vcmi.battleWindow.damageRetaliation.never"); std::string baseTextID = estimation.kills.max == 0 ? "vcmi.battleWindow.damageRetaliation.damage" : "vcmi.battleWindow.damageRetaliation.damageKills"; std::string prefixTextID = mayBeKilled ? "vcmi.battleWindow.damageRetaliation.may" : "vcmi.battleWindow.damageRetaliation.will"; return CGI->generaltexth->translate(prefixTextID) + formatAttack(estimation, "", baseTextID, 0); } BattleActionsController::BattleActionsController(BattleInterface & owner): owner(owner), selectedStack(nullptr), heroSpellToCast(nullptr) { } void BattleActionsController::endCastingSpell() { if(heroSpellToCast) { heroSpellToCast.reset(); owner.windowObject->blockUI(false); } if(owner.stacksController->getActiveStack()) possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared selectedStack = nullptr; GH.fakeMouseMove(); } bool BattleActionsController::isActiveStackSpellcaster() const { const CStack * casterStack = owner.stacksController->getActiveStack(); if (!casterStack) return false; bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); return (spellcaster && casterStack->canCast()); } void BattleActionsController::enterCreatureCastingMode() { //silently check for possible errors if (owner.tacticsMode) return; //hero is casting a spell if (heroSpellToCast) return; if (!owner.stacksController->getActiveStack()) return; if (!isActiveStackSpellcaster()) return; for(const auto & action : possibleActions) { if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) continue; const spells::Caster * caster = owner.stacksController->getActiveStack(); const CSpell * spell = action.spell().toSpell(); spells::Target target; target.emplace_back(); spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell); auto m = spell->battleMechanics(&cast); spells::detail::ProblemImpl ignored; const bool isCastingPossible = m->canBeCastAt(target, ignored); if (isCastingPossible) { owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId()); selectedStack = nullptr; CCS->curh->set(Cursor::Combat::POINTER); } return; } possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) { return !x.spellcast(); }; vstd::erase_if(possibleActions, actionFilterPredicate); GH.fakeMouseMove(); } std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const { BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass for(const auto & spell : creatureSpells) data.creatureSpellsToCast.push_back(spell->id); data.tacticsMode = owner.tacticsMode; auto allActions = owner.getBattle()->getClientActionsForStack(stack, data); allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); return std::vector(allActions); } void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack) { if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack auto assignPriority = [&](const PossiblePlayerBattleAction & item ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it { switch(item.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::ANY_LOCATION: case PossiblePlayerBattleAction::NO_LOCATION: case PossiblePlayerBattleAction::FREE_LOCATION: case PossiblePlayerBattleAction::OBSTACLE: if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) { PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack); bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast) return 1; } return 100; //bottom priority break; case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: return 2; break; case PossiblePlayerBattleAction::SHOOT: return 4; break; case PossiblePlayerBattleAction::ATTACK_AND_RETURN: return 5; break; case PossiblePlayerBattleAction::ATTACK: return 6; break; case PossiblePlayerBattleAction::WALK_AND_ATTACK: return 7; break; case PossiblePlayerBattleAction::MOVE_STACK: return 8; break; case PossiblePlayerBattleAction::CATAPULT: return 9; break; case PossiblePlayerBattleAction::HEAL: return 10; break; case PossiblePlayerBattleAction::CREATURE_INFO: return 11; break; case PossiblePlayerBattleAction::HERO_INFO: return 12; break; case PossiblePlayerBattleAction::TELEPORT: return 13; break; default: assert(0); return 200; break; } }; auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs) { return assignPriority(lhs) < assignPriority(rhs); }; std::sort(possibleActions.begin(), possibleActions.end(), comparer); } void BattleActionsController::castThisSpell(SpellID spellID) { heroSpellToCast = std::make_shared(); heroSpellToCast->actionType = EActionType::HERO_SPELL; heroSpellToCast->spell = spellID; heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; //choosing possible targets const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; assert(castingHero); // code below assumes non-null hero PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location { heroSpellToCast->aimToHex(BattleHex::INVALID); owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); endCastingSpell(); } else { possibleActions.clear(); possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment GH.fakeMouseMove();//update cursor } owner.windowObject->blockUI(true); } const CSpell * BattleActionsController::getHeroSpellToCast( ) const { if (heroSpellToCast) return heroSpellToCast->spell.toSpell(); return nullptr; } const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) { if (heroSpellToCast) return nullptr; if (!owner.stacksController->getActiveStack()) return nullptr; if (!hoveredHex.isValid()) return nullptr; auto action = selectAction(hoveredHex); if (action.spell() == SpellID::NONE) return nullptr; return action.spell().toSpell(); } const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) { if (getHeroSpellToCast()) return getHeroSpellToCast(); return getStackSpellToCast(hoveredHex); } const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) { const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true); if(shere) return shere; return owner.getBattle()->battleGetStackByPos(hoveredHex, false); } void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) { switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: CCS->curh->set(Cursor::Combat::POINTER); return; case PossiblePlayerBattleAction::MOVE_TACTICS: case PossiblePlayerBattleAction::MOVE_STACK: if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) CCS->curh->set(Cursor::Combat::FLY); else CCS->curh->set(Cursor::Combat::MOVE); return; case PossiblePlayerBattleAction::ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: { static const std::map sectorCursor = { {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST}, {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST}, {BattleHex::RIGHT, Cursor::Combat::HIT_WEST }, {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST}, {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST}, {BattleHex::LEFT, Cursor::Combat::HIT_EAST }, {BattleHex::TOP, Cursor::Combat::HIT_SOUTH }, {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH } }; auto direction = owner.fieldController->selectAttackDirection(targetHex); assert(sectorCursor.count(direction) > 0); if (sectorCursor.count(direction)) CCS->curh->set(sectorCursor.at(direction)); return; } case PossiblePlayerBattleAction::SHOOT: if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); else CCS->curh->set(Cursor::Combat::SHOOT); return; case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::ANY_LOCATION: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: case PossiblePlayerBattleAction::FREE_LOCATION: case PossiblePlayerBattleAction::OBSTACLE: CCS->curh->set(Cursor::Spellcast::SPELL); return; case PossiblePlayerBattleAction::TELEPORT: CCS->curh->set(Cursor::Combat::TELEPORT); return; case PossiblePlayerBattleAction::SACRIFICE: CCS->curh->set(Cursor::Combat::SACRIFICE); return; case PossiblePlayerBattleAction::HEAL: CCS->curh->set(Cursor::Combat::HEAL); return; case PossiblePlayerBattleAction::CATAPULT: CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT); return; case PossiblePlayerBattleAction::CREATURE_INFO: CCS->curh->set(Cursor::Combat::QUERY); return; case PossiblePlayerBattleAction::HERO_INFO: CCS->curh->set(Cursor::Combat::HERO); return; } assert(0); } void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) { switch (action.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: case PossiblePlayerBattleAction::TELEPORT: case PossiblePlayerBattleAction::SACRIFICE: case PossiblePlayerBattleAction::FREE_LOCATION: CCS->curh->set(Cursor::Combat::BLOCKED); return; default: if (targetHex == -1) CCS->curh->set(Cursor::Combat::POINTER); else CCS->curh->set(Cursor::Combat::BLOCKED); return; } assert(0); } std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex) { const CStack * targetStack = getStackForHex(targetHex); switch (action.get()) //display console message, realize selected action { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s case PossiblePlayerBattleAction::MOVE_TACTICS: case PossiblePlayerBattleAction::MOVE_STACK: if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here else return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here case PossiblePlayerBattleAction::ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); DamageEstimation retaliation; DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount(); return formatMeleeAttack(estimation, targetStack->getName()) + "\n" + formatRetaliation(retaliation, enemyMayBeKilled); } case PossiblePlayerBattleAction::SHOOT: { const auto * shooter = owner.stacksController->getActiveStack(); DamageEstimation retaliation; DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); } case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s case PossiblePlayerBattleAction::ANY_LOCATION: return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % case PossiblePlayerBattleAction::TELEPORT: return CGI->generaltexth->allTexts[25]; //Teleport Here case PossiblePlayerBattleAction::OBSTACLE: return CGI->generaltexth->allTexts[550]; case PossiblePlayerBattleAction::SACRIFICE: return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s case PossiblePlayerBattleAction::FREE_LOCATION: return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s case PossiblePlayerBattleAction::HEAL: return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s case PossiblePlayerBattleAction::CATAPULT: return ""; // TODO case PossiblePlayerBattleAction::CREATURE_INFO: return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str(); case PossiblePlayerBattleAction::HERO_INFO: return CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats" } assert(0); return ""; } std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) { switch (action.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: return CGI->generaltexth->allTexts[23]; break; case PossiblePlayerBattleAction::TELEPORT: return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination break; case PossiblePlayerBattleAction::SACRIFICE: return CGI->generaltexth->allTexts[543]; //choose army to sacrifice break; case PossiblePlayerBattleAction::FREE_LOCATION: return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here break; default: return ""; } } bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex) { const CStack * targetStack = getStackForHex(targetHex); bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID; switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0); case PossiblePlayerBattleAction::CREATURE_INFO: return (targetStack && targetStackOwned && targetStack->alive()); case PossiblePlayerBattleAction::HERO_INFO: if (targetHex == BattleHex::HERO_ATTACKER) return owner.attackingHero != nullptr; if (targetHex == BattleHex::HERO_DEFENDER) return owner.defendingHero != nullptr; return false; case PossiblePlayerBattleAction::MOVE_TACTICS: case PossiblePlayerBattleAction::MOVE_STACK: if (!(targetStack && targetStack->alive())) //we can walk on dead stacks { if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex)) return true; } return false; case PossiblePlayerBattleAction::ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) { if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? return true; } return false; case PossiblePlayerBattleAction::SHOOT: return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); case PossiblePlayerBattleAction::NO_LOCATION: return false; case PossiblePlayerBattleAction::ANY_LOCATION: return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures { SpellID spellID = owner.getBattle()->getRandomBeneficialSpell(CRandomGenerator::getDefault(), owner.stacksController->getActiveStack(), targetStack); return spellID != SpellID::NONE; } return false; case PossiblePlayerBattleAction::TELEPORT: return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex); case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive(); case PossiblePlayerBattleAction::OBSTACLE: case PossiblePlayerBattleAction::FREE_LOCATION: return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); case PossiblePlayerBattleAction::CATAPULT: return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); case PossiblePlayerBattleAction::HEAL: return targetStack && targetStackOwned && targetStack->canBeHealed(); } assert(0); return false; } void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex) { const CStack * targetStack = getStackForHex(targetHex); switch (action.get()) //display console message, realize selected action { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: { owner.stackActivated(targetStack); return; } case PossiblePlayerBattleAction::MOVE_TACTICS: case PossiblePlayerBattleAction::MOVE_STACK: { if(owner.stacksController->getActiveStack()->doubleWide()) { std::vector acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); if(vstd::contains(acc, targetHex)) owner.giveCommand(EActionType::WALK, targetHex); else if(vstd::contains(acc, shiftedDest)) owner.giveCommand(EActionType::WALK, shiftedDest); } else { owner.giveCommand(EActionType::WALK, targetHex); } return; } case PossiblePlayerBattleAction::ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) { BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack); owner.sendCommand(command, owner.stacksController->getActiveStack()); } return; } case PossiblePlayerBattleAction::SHOOT: { owner.giveCommand(EActionType::SHOOT, targetHex); return; } case PossiblePlayerBattleAction::HEAL: { owner.giveCommand(EActionType::STACK_HEAL, targetHex); return; }; case PossiblePlayerBattleAction::CATAPULT: { owner.giveCommand(EActionType::CATAPULT, targetHex); return; } case PossiblePlayerBattleAction::CREATURE_INFO: { GH.windows().createAndPushWindow(targetStack, false); return; } case PossiblePlayerBattleAction::HERO_INFO: { if (targetHex == BattleHex::HERO_ATTACKER) owner.attackingHero->heroLeftClicked(); if (targetHex == BattleHex::HERO_DEFENDER) owner.defendingHero->heroLeftClicked(); return; } case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::ANY_LOCATION: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell case PossiblePlayerBattleAction::TELEPORT: case PossiblePlayerBattleAction::OBSTACLE: case PossiblePlayerBattleAction::SACRIFICE: case PossiblePlayerBattleAction::FREE_LOCATION: { if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) { if (action.spell() == SpellID::SACRIFICE) { heroSpellToCast->aimToHex(targetHex); possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()}); selectedStack = targetStack; return; } if (action.spell() == SpellID::TELEPORT) { heroSpellToCast->aimToUnit(targetStack); possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()}); selectedStack = targetStack; return; } } if (!spellcastingModeActive()) { if (action.spell().hasValue()) { owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); } else //unknown random spell { owner.giveCommand(EActionType::MONSTER_SPELL, targetHex); } } else { assert(getHeroSpellToCast()); switch (getHeroSpellToCast()->id.toEnum()) { case SpellID::SACRIFICE: heroSpellToCast->aimToUnit(targetStack);//victim break; default: heroSpellToCast->aimToHex(targetHex); break; } owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); endCastingSpell(); } selectedStack = nullptr; return; } } assert(0); return; } PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex) { assert(owner.stacksController->getActiveStack() != nullptr); assert(!possibleActions.empty()); assert(targetHex.isValid()); if (owner.stacksController->getActiveStack() == nullptr) return PossiblePlayerBattleAction::INVALID; if (possibleActions.empty()) return PossiblePlayerBattleAction::INVALID; const CStack * targetStack = getStackForHex(targetHex); reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack); for (PossiblePlayerBattleAction action : possibleActions) { if (actionIsLegal(action, targetHex)) return action; } return possibleActions.front(); } void BattleActionsController::onHexHovered(BattleHex hoveredHex) { if (owner.openingPlaying()) { currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro"); GH.statusbar()->write(currentConsoleMsg); return; } if (owner.stacksController->getActiveStack() == nullptr) return; if (hoveredHex == BattleHex::INVALID) { if (!currentConsoleMsg.empty()) GH.statusbar()->clearIfMatching(currentConsoleMsg); currentConsoleMsg.clear(); CCS->curh->set(Cursor::Combat::BLOCKED); return; } auto action = selectAction(hoveredHex); std::string newConsoleMsg; if (actionIsLegal(action, hoveredHex)) { actionSetCursor(action, hoveredHex); newConsoleMsg = actionGetStatusMessage(action, hoveredHex); } else { actionSetCursorBlocked(action, hoveredHex); newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex); } if (!currentConsoleMsg.empty()) GH.statusbar()->clearIfMatching(currentConsoleMsg); if (!newConsoleMsg.empty()) GH.statusbar()->write(newConsoleMsg); currentConsoleMsg = newConsoleMsg; } void BattleActionsController::onHoverEnded() { CCS->curh->set(Cursor::Combat::POINTER); if (!currentConsoleMsg.empty()) GH.statusbar()->clearIfMatching(currentConsoleMsg); currentConsoleMsg.clear(); } void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) { if (owner.stacksController->getActiveStack() == nullptr) return; auto action = selectAction(clickedHex); std::string newConsoleMsg; if (!actionIsLegal(action, clickedHex)) return; actionRealize(action, clickedHex); GH.statusbar()->clear(); } void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) { creatureSpells.clear(); bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); if(casterStack->canCast() && spellcaster) { // faerie dragon can cast only one, randomly selected spell until their next move //TODO: faerie dragon type spell should be selected by server const auto spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack); if (spellToCast.hasValue()) creatureSpells.push_back(spellToCast.toSpell()); } TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); for(const auto & bonus : *bl) { if (bonus->additionalInfo[0] <= 0 && bonus->subtype.as().hasValue()) creatureSpells.push_back(bonus->subtype.as().toSpell()); } } const spells::Caster * BattleActionsController::getCurrentSpellcaster() const { if (heroSpellToCast) return owner.currentHero(); else return owner.stacksController->getActiveStack(); } spells::Mode BattleActionsController::getCurrentCastMode() const { if (heroSpellToCast) return spells::Mode::HERO; else return spells::Mode::CREATURE_ACTIVE; } bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex) { assert(currentSpell); if (!currentSpell) return false; auto caster = getCurrentSpellcaster(); const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE; spells::Target target; if(targetStack) target.emplace_back(targetStack); target.emplace_back(targetHex); spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell); auto m = currentSpell->battleMechanics(&cast); spells::detail::ProblemImpl problem; //todo: display problem in status bar return m->canBeCastAt(target, problem); } bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const { std::vector acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false); BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); if (vstd::contains(acc, myNumber)) return true; else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest)) return true; else return false; } void BattleActionsController::activateStack() { const CStack * s = owner.stacksController->getActiveStack(); if(s) { tryActivateStackSpellcasting(s); possibleActions = getPossibleActionsForStack(s); std::list actionsToSelect; if(!possibleActions.empty()) { switch(possibleActions.front().get()) { case PossiblePlayerBattleAction::SHOOT: actionsToSelect.push_back(possibleActions.front()); actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); break; case PossiblePlayerBattleAction::ATTACK_AND_RETURN: actionsToSelect.push_back(possibleActions.front()); actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); break; case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: actionsToSelect.push_back(possibleActions.front()); actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); break; case PossiblePlayerBattleAction::ANY_LOCATION: actionsToSelect.push_back(possibleActions.front()); actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); break; } } owner.windowObject->setAlternativeActions(actionsToSelect); } } void BattleActionsController::onHexRightClicked(BattleHex clickedHex) { auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) { return action.spellcast(); }; bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); if (spellcastingModeActive() || isCurrentStackInSpellcastMode) { endCastingSpell(); CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled return; } auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true); if (selectedStack != nullptr) GH.windows().createAndPushWindow(selectedStack, true); if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero) owner.attackingHero->heroRightClicked(); if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero) owner.defendingHero->heroRightClicked(); } bool BattleActionsController::spellcastingModeActive() const { return heroSpellToCast != nullptr; } bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) { if (heroSpellToCast) return true; if (!owner.stacksController->getActiveStack()) return false; auto action = selectAction(hoveredHex); return action.spellcast(); } const std::vector & BattleActionsController::getPossibleActions() const { return possibleActions; } void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action) { vstd::erase(possibleActions, action); } void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action) { possibleActions.insert(possibleActions.begin(), action); }