From 02fe0425c734ca97fb8d62d6232ced39f90e6164 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Nov 2022 22:56:42 +0200 Subject: [PATCH] Moved battle actions handling into a separate class --- client/CMakeLists.txt | 2 + client/battle/CBattleActionsController.cpp | 768 +++++++++++++++++++++ client/battle/CBattleActionsController.h | 57 ++ client/battle/CBattleControlPanel.cpp | 22 +- client/battle/CBattleFieldController.cpp | 7 +- client/battle/CBattleInterface.cpp | 727 +------------------ client/battle/CBattleInterface.h | 30 +- client/battle/CBattleInterfaceClasses.cpp | 3 +- 8 files changed, 862 insertions(+), 754 deletions(-) create mode 100644 client/battle/CBattleActionsController.cpp create mode 100644 client/battle/CBattleActionsController.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index c6b7b088d..3a630b0a3 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -6,6 +6,7 @@ set(client_SRCS battle/CBattleControlPanel.cpp battle/CBattleInterfaceClasses.cpp battle/CBattleInterface.cpp + battle/CBattleActionsController.cpp battle/CBattleFieldController.cpp battle/CBattleObstacleController.cpp battle/CBattleProjectileController.cpp @@ -85,6 +86,7 @@ set(client_HEADERS battle/CBattleControlPanel.h battle/CBattleInterfaceClasses.h battle/CBattleInterface.h + battle/CBattleActionsController.h battle/CBattleFieldController.h battle/CBattleObstacleController.h battle/CBattleProjectileController.h diff --git a/client/battle/CBattleActionsController.cpp b/client/battle/CBattleActionsController.cpp new file mode 100644 index 000000000..4b0c5fd9d --- /dev/null +++ b/client/battle/CBattleActionsController.cpp @@ -0,0 +1,768 @@ +/* + * CBattleActionsController.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 "CBattleActionsController.h" + +#include "CBattleControlPanel.h" +#include "CBattleStacksController.h" +#include "CBattleInterface.h" +#include "CBattleFieldController.h" +#include "CBattleSiegeController.h" +#include "CBattleInterfaceClasses.h" + +#include "../gui/CCursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CIntObject.h" +#include "../windows/CCreatureWindow.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../../CCallback.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" +#include "../../lib/CGeneralTextHandler.h" + +static std::string formatDmgRange(std::pair dmgRange) +{ + if (dmgRange.first != dmgRange.second) + return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str(); + else + return (boost::format("%d") % dmgRange.first).str(); +} + + +CBattleActionsController::CBattleActionsController(CBattleInterface * owner): + owner(owner), + creatureCasting(false), + spellDestSelectMode(false), + spellToCast(nullptr), + sp(nullptr) +{ + currentAction = PossiblePlayerBattleAction::INVALID; + selectedAction = PossiblePlayerBattleAction::INVALID; +} + +void CBattleActionsController::endCastingSpell() +{ + if(spellDestSelectMode) + { + spellToCast.reset(); + + sp = nullptr; + spellDestSelectMode = false; + CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); + + if(owner->stacksController->getActiveStack()) + { + possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack()); //restore actions after they were cleared + owner->myTurn = true; + } + } + else + { + if(owner->stacksController->getActiveStack()) + { + possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack()); + GH.fakeMouseMove(); + } + } +} + +void CBattleActionsController::enterCreatureCastingMode() +{ + //silently check for possible errors + if (!owner->myTurn) + return; + + if (owner->tacticsMode) + return; + + //hero is casting a spell + if (spellDestSelectMode) + return; + + if (!owner->stacksController->getActiveStack()) + return; + + if (!owner->stacksController->activeStackSpellcaster()) + return; + + //random spellcaster + if (owner->stacksController->activeStackSpellToCast() == SpellID::NONE) + return; + + if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION)) + { + const spells::Caster * caster = owner->stacksController->getActiveStack(); + const CSpell * spell = owner->stacksController->activeStackSpellToCast().toSpell(); + + spells::Target target; + target.emplace_back(); + + spells::BattleCast cast(owner->curInt->cb.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->myTurn = false; + owner->giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, owner->stacksController->activeStackSpellToCast()); + owner->stacksController->setSelectedStack(nullptr); + + CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); + } + } + else + { + possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack()); + + auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) + { + return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) && + (x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) && + (x != PossiblePlayerBattleAction::OBSTACLE); + }; + + vstd::erase_if(possibleActions, actionFilterPredicate); + GH.fakeMouseMove(); + } +} + +std::vector CBattleActionsController::getPossibleActionsForStack(const CStack *stack) +{ + BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass + data.creatureSpellToCast = owner->stacksController->activeStackSpellToCast(); + data.tacticsMode = owner->tacticsMode; + auto allActions = owner->curInt->cb->getClientActionsForStack(stack, data); + + return std::vector(allActions); +} + +void CBattleActionsController::reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context) +{ + if(owner->tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack + + auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it + { + switch(item) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::NO_LOCATION: + case PossiblePlayerBattleAction::FREE_LOCATION: + case PossiblePlayerBattleAction::OBSTACLE: + if(!stack->hasBonusOfType(Bonus::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX) + return 1; + else + return 100;//bottom priority + break; + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + return 2; break; + case PossiblePlayerBattleAction::RISE_DEMONS: + return 3; 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; + default: + return 200; break; + } + }; + + auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs) + { + return assignPriority(lhs) > assignPriority(rhs); + }; + + std::make_heap(possibleActions.begin(), possibleActions.end(), comparer); +} + +void CBattleActionsController::castThisSpell(SpellID spellID) +{ + spellToCast = std::make_shared(); + spellToCast->actionType = EActionType::HERO_SPELL; + spellToCast->actionSubtype = spellID; //spell number + spellToCast->stackNumber = (owner->attackingHeroInstance->tempOwner == owner->curInt->playerID) ? -1 : -2; + spellToCast->side = owner->defendingHeroInstance ? (owner->curInt->playerID == owner->defendingHeroInstance->tempOwner) : false; + spellDestSelectMode = true; + creatureCasting = 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 + sp = spellID.toSpell(); + PossiblePlayerBattleAction spellSelMode = owner->curInt->cb->getCasterAction(sp, castingHero, spells::Mode::HERO); + + if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location + { + spellToCast->aimToHex(BattleHex::INVALID); + owner->curInt->cb->battleMakeAction(spellToCast.get()); + endCastingSpell(); + } + else + { + possibleActions.clear(); + possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment + GH.fakeMouseMove();//update cursor + } +} + + +void CBattleActionsController::handleHex(BattleHex myNumber, int eventType) +{ + if (!owner->myTurn || !owner->battleActionsStarted) //we are not permit to do anything + return; + + // This function handles mouse move over hexes and l-clicking on them. + // First we decide what happens if player clicks on this hex and set appropriately + // consoleMsg, cursorFrame/Type and prepare lambda realizeAction. + // + // Then, depending whether it was hover/click we either call the action or set tooltip/cursor. + + //used when hovering -> tooltip message and cursor to be set + std::string consoleMsg; + bool setCursor = true; //if we want to suppress setting cursor + ECursor::ECursorTypes cursorType = ECursor::COMBAT; + int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used? + + //used when l-clicking -> action to be called upon the click + std::function realizeAction; + + //Get stack on the hex - first try to grab the alive one, if not found -> allow dead stacks. + const CStack * shere = owner->curInt->cb->battleGetStackByPos(myNumber, true); + if(!shere) + shere = owner->curInt->cb->battleGetStackByPos(myNumber, false); + + if(!owner->stacksController->getActiveStack()) + return; + + bool ourStack = false; + if (shere) + ourStack = shere->owner == owner->curInt->playerID; + + //stack may have changed, update selection border + owner->stacksController->setHoveredStack(shere); + + localActions.clear(); + illegalActions.clear(); + + reorderPossibleActionsPriority(owner->stacksController->getActiveStack(), shere ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX); + const bool forcedAction = possibleActions.size() == 1; + + for (PossiblePlayerBattleAction action : possibleActions) + { + bool legalAction = false; //this action is legal and can be performed + bool notLegal = false; //this action is not legal and should display message + + switch (action) + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + if (shere && ourStack) + legalAction = true; + break; + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + { + if (!(shere && shere->alive())) //we can walk on dead stacks + { + if(canStackMoveHere(owner->stacksController->getActiveStack(), myNumber)) + legalAction = true; + } + break; + } + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + { + if(owner->curInt->cb->battleCanAttack(owner->stacksController->getActiveStack(), shere, myNumber)) + { + if (owner->fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack? + { + owner->fieldController->setBattleCursor(myNumber); // temporary - needed for following function :( + BattleHex attackFromHex = owner->fieldController->fromWhichHexAttack(myNumber); + + if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) + legalAction = true; + } + } + } + break; + case PossiblePlayerBattleAction::SHOOT: + if(owner->curInt->cb->battleCanShoot(owner->stacksController->getActiveStack(), myNumber)) + legalAction = true; + break; + case PossiblePlayerBattleAction::ANY_LOCATION: + if (myNumber > -1) //TODO: this should be checked for all actions + { + if(isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber)) + legalAction = true; + } + break; + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + if(shere && isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber)) + legalAction = true; + break; + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + { + if(shere && ourStack && shere != owner->stacksController->getActiveStack() && shere->alive()) //only positive spells for other allied creatures + { + int spellID = owner->curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), shere, CBattleInfoCallback::RANDOM_GENIE); + if(spellID > -1) + { + legalAction = true; + } + } + } + break; + case PossiblePlayerBattleAction::OBSTACLE: + if(isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber)) + legalAction = true; + break; + case PossiblePlayerBattleAction::TELEPORT: + { + //todo: move to mechanics + ui8 skill = 0; + if (creatureCasting) + skill = owner->stacksController->getActiveStack()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); + else + skill = owner->getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); + //TODO: explicitely save power, skill + if (owner->curInt->cb->battleCanTeleportTo(owner->stacksController->getSelectedStack(), myNumber, skill)) + legalAction = true; + else + notLegal = true; + } + break; + case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice + if (shere && shere != owner->stacksController->getSelectedStack() && ourStack && shere->alive()) + legalAction = true; + else + notLegal = true; + break; + case PossiblePlayerBattleAction::FREE_LOCATION: + legalAction = true; + if(!isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber)) + { + legalAction = false; + notLegal = true; + } + break; + case PossiblePlayerBattleAction::CATAPULT: + if (owner->siegeController && owner->siegeController->isCatapultAttackable(myNumber)) + legalAction = true; + break; + case PossiblePlayerBattleAction::HEAL: + if (shere && ourStack && shere->canBeHealed()) + legalAction = true; + break; + case PossiblePlayerBattleAction::RISE_DEMONS: + if (shere && ourStack && !shere->alive()) + { + if (!(shere->hasBonusOfType(Bonus::UNDEAD) + || shere->hasBonusOfType(Bonus::NON_LIVING) + || shere->hasBonusOfType(Bonus::GARGOYLE) + || shere->summoned + || shere->isClone() + || shere->hasBonusOfType(Bonus::SIEGE_WEAPON) + )) + legalAction = true; + } + break; + } + if (legalAction) + localActions.push_back (action); + else if (notLegal || forcedAction) + illegalActions.push_back (action); + } + illegalAction = PossiblePlayerBattleAction::INVALID; //clear it in first place + + if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default + currentAction = selectedAction; + else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority) + currentAction = localActions.front(); + else //no legal action possible + { + currentAction = PossiblePlayerBattleAction::INVALID; //don't allow to do anything + + if (vstd::contains(illegalActions, selectedAction)) + illegalAction = selectedAction; + else if (illegalActions.size()) + illegalAction = illegalActions.front(); + else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature + { + currentAction = PossiblePlayerBattleAction::CREATURE_INFO; + } + else + illegalAction = PossiblePlayerBattleAction::INVALID; //we should never be here + } + + bool isCastingPossible = false; + bool secondaryTarget = false; + + if (currentAction > PossiblePlayerBattleAction::INVALID) + { + switch (currentAction) //display console message, realize selected action + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s + realizeAction = [=](){ owner->stackActivated(shere); }; + break; + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (owner->stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING)) + { + cursorFrame = ECursor::COMBAT_FLY; + consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % owner->stacksController->getActiveStack()->getName()).str(); //Fly %s here + } + else + { + cursorFrame = ECursor::COMBAT_MOVE; + consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % owner->stacksController->getActiveStack()->getName()).str(); //Move %s here + } + + realizeAction = [=]() + { + if(owner->stacksController->getActiveStack()->doubleWide()) + { + std::vector acc = owner->curInt->cb->battleGetAvailableHexes(owner->stacksController->getActiveStack()); + BattleHex shiftedDest = myNumber.cloneInDirection(owner->stacksController->getActiveStack()->destShiftDir(), false); + if(vstd::contains(acc, myNumber)) + owner->giveCommand(EActionType::WALK, myNumber); + else if(vstd::contains(acc, shiftedDest)) + owner->giveCommand(EActionType::WALK, shiftedDest); + } + else + { + owner->giveCommand(EActionType::WALK, myNumber); + } + }; + break; + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return + { + owner->fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile + setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean? + + bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN; + + realizeAction = [=]() + { + BattleHex attackFromHex = owner->fieldController->fromWhichHexAttack(myNumber); + if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) + { + auto command = new BattleAction(BattleAction::makeMeleeAttack(owner->stacksController->getActiveStack(), myNumber, attackFromHex, returnAfterAttack)); + owner->sendCommand(command, owner->stacksController->getActiveStack()); + } + }; + + TDmgRange damage = owner->curInt->cb->battleEstimateDamage(owner->stacksController->getActiveStack(), shere); + std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg + consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage) + } + break; + case PossiblePlayerBattleAction::SHOOT: + { + if (owner->curInt->cb->battleHasShootingPenalty(owner->stacksController->getActiveStack(), myNumber)) + cursorFrame = ECursor::COMBAT_SHOOT_PENALTY; + else + cursorFrame = ECursor::COMBAT_SHOOT; + + realizeAction = [=](){owner->giveCommand(EActionType::SHOOT, myNumber);}; + TDmgRange damage = owner->curInt->cb->battleEstimateDamage(owner->stacksController->getActiveStack(), shere); + std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg + //printing - Shoot %s (%d shots left, %s damage) + consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % owner->stacksController->getActiveStack()->shots.available() % estDmgText).str(); + } + break; + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + sp = CGI->spellh->objects[creatureCasting ? owner->stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s + switch (sp->id) + { + case SpellID::SACRIFICE: + case SpellID::TELEPORT: + owner->stacksController->setSelectedStack(shere); //remember first target + secondaryTarget = true; + break; + } + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::ANY_LOCATION: + sp = CGI->spellh->objects[creatureCasting ? owner->stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell + sp = nullptr; + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on % + creatureCasting = true; + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::TELEPORT: + consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here + cursorFrame = ECursor::COMBAT_TELEPORT; + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::OBSTACLE: + consoleMsg = CGI->generaltexth->allTexts[550]; + //TODO: remove obstacle cursor + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::SACRIFICE: + consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s + cursorFrame = ECursor::COMBAT_SACRIFICE; + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::FREE_LOCATION: + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s + isCastingPossible = true; + break; + case PossiblePlayerBattleAction::HEAL: + cursorFrame = ECursor::COMBAT_HEAL; + consoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s + realizeAction = [=](){ owner->giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing + break; + case PossiblePlayerBattleAction::RISE_DEMONS: + cursorType = ECursor::SPELLBOOK; + realizeAction = [=]() + { + owner->giveCommand(EActionType::DAEMON_SUMMONING, myNumber); + }; + break; + case PossiblePlayerBattleAction::CATAPULT: + cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT; + realizeAction = [=](){ owner->giveCommand(EActionType::CATAPULT, myNumber); }; + break; + case PossiblePlayerBattleAction::CREATURE_INFO: + { + cursorFrame = ECursor::COMBAT_QUERY; + consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str(); + realizeAction = [=](){ GH.pushIntT(shere, false); }; + break; + } + } + } + else //no possible valid action, display message + { + switch (illegalAction) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + cursorFrame = ECursor::COMBAT_BLOCKED; + consoleMsg = CGI->generaltexth->allTexts[23]; + break; + case PossiblePlayerBattleAction::TELEPORT: + cursorFrame = ECursor::COMBAT_BLOCKED; + consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination + break; + case PossiblePlayerBattleAction::SACRIFICE: + consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice + break; + case PossiblePlayerBattleAction::FREE_LOCATION: + cursorFrame = ECursor::COMBAT_BLOCKED; + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % sp->name); //No room to place %s here + break; + default: + if (myNumber == -1) + CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc. + else + cursorFrame = ECursor::COMBAT_BLOCKED; + break; + } + } + + if (isCastingPossible) //common part + { + switch (currentAction) //don't use that with teleport / sacrifice + { + case PossiblePlayerBattleAction::TELEPORT: //FIXME: more generic solution? + case PossiblePlayerBattleAction::SACRIFICE: + break; + default: + cursorType = ECursor::SPELLBOOK; + cursorFrame = 0; + if (consoleMsg.empty() && sp) + consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s + break; + } + + realizeAction = [=]() + { + if(secondaryTarget) //select that target now + { + + possibleActions.clear(); + switch (sp->id.toEnum()) + { + case SpellID::TELEPORT: //don't cast spell yet, only select target + spellToCast->aimToUnit(shere); + possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT); + break; + case SpellID::SACRIFICE: + spellToCast->aimToHex(myNumber); + possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE); + break; + } + } + else + { + if (creatureCasting) + { + if (sp) + { + owner->giveCommand(EActionType::MONSTER_SPELL, myNumber, owner->stacksController->activeStackSpellToCast()); + } + else //unknown random spell + { + owner->giveCommand(EActionType::MONSTER_SPELL, myNumber); + } + } + else + { + assert(sp); + switch (sp->id.toEnum()) + { + case SpellID::SACRIFICE: + spellToCast->aimToUnit(shere);//victim + break; + default: + spellToCast->aimToHex(myNumber); + break; + } + owner->curInt->cb->battleMakeAction(spellToCast.get()); + endCastingSpell(); + } + owner->stacksController->setSelectedStack(nullptr); + } + }; + } + + { + if (eventType == CIntObject::MOVE) + { + if (setCursor) + CCS->curh->changeGraphic(cursorType, cursorFrame); + owner->controlPanel->console->write(consoleMsg); + } + if (eventType == CIntObject::LCLICK && realizeAction) + { + //opening creature window shouldn't affect myTurn... + if ((currentAction != PossiblePlayerBattleAction::CREATURE_INFO) && !secondaryTarget) + { + owner->myTurn = false; //tends to crash with empty calls + } + realizeAction(); + if (!secondaryTarget) //do not replace teleport or sacrifice cursor + CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); + owner->controlPanel->console->clear(); + } + } +} + + +bool CBattleActionsController::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber) +{ + creatureCasting = owner->stacksController->activeStackSpellcaster() && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells + + bool isCastingPossible = true; + + int spellID = -1; + if (creatureCasting) + { + if (owner->stacksController->activeStackSpellToCast() != SpellID::NONE && (shere != sactive)) //can't cast on itself + spellID = owner->stacksController->activeStackSpellToCast(); //TODO: merge with SpellTocast? + } + else //hero casting + { + spellID = spellToCast->actionSubtype; + } + + + sp = nullptr; + if (spellID >= 0) + sp = CGI->spellh->objects[spellID]; + + if (sp) + { + const spells::Caster *caster = creatureCasting ? static_cast(sactive) : static_cast(owner->curInt->cb->battleGetMyHero()); + if (caster == nullptr) + { + isCastingPossible = false;//just in case + } + else + { + const spells::Mode mode = creatureCasting ? spells::Mode::CREATURE_ACTIVE : spells::Mode::HERO; + + spells::Target target; + target.emplace_back(myNumber); + + spells::BattleCast cast(owner->curInt->cb.get(), caster, mode, sp); + + auto m = sp->battleMechanics(&cast); + spells::detail::ProblemImpl problem; //todo: display problem in status bar + + isCastingPossible = m->canBeCastAt(target, problem); + } + } + else + isCastingPossible = false; + if (!myNumber.isAvailable() && !shere) //empty tile outside battlefield (or in the unavailable border column) + isCastingPossible = false; + + return isCastingPossible; +} + +bool CBattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) +{ + std::vector acc = owner->curInt->cb->battleGetAvailableHexes(stackToMove); + 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 CBattleActionsController::activateStack() +{ + const CStack * s = owner->stacksController->getActiveStack(); + if(s) + possibleActions = getPossibleActionsForStack(s); +} + +bool CBattleActionsController::spellcastingModeActive() +{ + return spellDestSelectMode; +} + +SpellID CBattleActionsController::selectedSpell() +{ + if (!spellToCast) + return SpellID::NONE; + return SpellID(spellToCast->actionSubtype); +} diff --git a/client/battle/CBattleActionsController.h b/client/battle/CBattleActionsController.h new file mode 100644 index 000000000..f5f496db7 --- /dev/null +++ b/client/battle/CBattleActionsController.h @@ -0,0 +1,57 @@ +/* + * CBattleActionsController.h, 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 + * + */ +#pragma once + +#include "../../lib/battle/CBattleInfoCallback.h" + +class BattleAction; +class CBattleInterface; + +enum class MouseHoveredHexContext +{ + UNOCCUPIED_HEX, + OCCUPIED_HEX +}; + +class CBattleActionsController +{ + CBattleInterface * owner; + + std::vector possibleActions; //all actions possible to call at the moment by player + std::vector localActions; //actions possible to take on hovered hex + std::vector illegalActions; //these actions display message in case of illegal target + PossiblePlayerBattleAction currentAction; //action that will be performed on l-click + PossiblePlayerBattleAction selectedAction; //last action chosen (and saved) by player + PossiblePlayerBattleAction illegalAction; //most likely action that can't be performed here + + bool creatureCasting; //if true, stack currently aims to cats a spell + bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console + std::shared_ptr spellToCast; //spell for which player is choosing destination + const CSpell *sp; //spell pointer for convenience + + bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber); + bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber); //TODO: move to BattleState / callback + + std::vector getPossibleActionsForStack (const CStack *stack); //called when stack gets its turn + void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context); + +public: + CBattleActionsController(CBattleInterface * owner); + + void activateStack(); + void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled) + void enterCreatureCastingMode(); + + SpellID selectedSpell(); + bool spellcastingModeActive(); + void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook + void handleHex(BattleHex myNumber, int eventType); + +}; diff --git a/client/battle/CBattleControlPanel.cpp b/client/battle/CBattleControlPanel.cpp index cc5692e0a..433ffd044 100644 --- a/client/battle/CBattleControlPanel.cpp +++ b/client/battle/CBattleControlPanel.cpp @@ -12,6 +12,7 @@ #include "CBattleInterface.h" #include "CBattleInterfaceClasses.h" #include "CBattleStacksController.h" +#include "CBattleActionsController.h" #include "../widgets/Buttons.h" #include "../CGameInfo.h" #include "../CBitmapHandler.h" @@ -67,13 +68,16 @@ void CBattleControlPanel::tacticPhaseEnded() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + btactNext.reset(); + btactEnd.reset(); + menu = std::make_shared("CBAR.BMP", 0, 0); menu->colorize(owner->curInt->playerID); } void CBattleControlPanel::bOptionsf() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; CCS->curh->changeGraphic(ECursor::ADVENTURE,0); @@ -85,7 +89,7 @@ void CBattleControlPanel::bOptionsf() void CBattleControlPanel::bSurrenderf() { - if(owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; int cost = owner->curInt->cb->battleGetSurrenderCost(); @@ -105,7 +109,7 @@ void CBattleControlPanel::bSurrenderf() void CBattleControlPanel::bFleef() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; if ( owner->curInt->cb->battleCanFlee() ) @@ -153,7 +157,7 @@ void CBattleControlPanel::reallySurrender() void CBattleControlPanel::bAutofightf() { - if(owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; //Stop auto-fight mode @@ -180,7 +184,7 @@ void CBattleControlPanel::bAutofightf() void CBattleControlPanel::bSpellf() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; if (!owner->myTurn) @@ -222,7 +226,7 @@ void CBattleControlPanel::bSpellf() void CBattleControlPanel::bWaitf() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; if (owner->stacksController->getActiveStack() != nullptr) @@ -231,7 +235,7 @@ void CBattleControlPanel::bWaitf() void CBattleControlPanel::bDefencef() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; if (owner->stacksController->getActiveStack() != nullptr) @@ -240,7 +244,7 @@ void CBattleControlPanel::bDefencef() void CBattleControlPanel::bConsoleUpf() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; console->scrollUp(); @@ -248,7 +252,7 @@ void CBattleControlPanel::bConsoleUpf() void CBattleControlPanel::bConsoleDownf() { - if (owner->spellDestSelectMode) //we are casting a spell + if (owner->actionsController->spellcastingModeActive()) return; console->scrollDown(); diff --git a/client/battle/CBattleFieldController.cpp b/client/battle/CBattleFieldController.cpp index d09286400..f2320f75d 100644 --- a/client/battle/CBattleFieldController.cpp +++ b/client/battle/CBattleFieldController.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CBattleFieldController.h" #include "CBattleInterface.h" +#include "CBattleActionsController.h" #include "CBattleInterfaceClasses.h" #include "CBattleSiegeController.h" #include "CBattleStacksController.h" @@ -220,12 +221,12 @@ void CBattleFieldController::showHighlightedHexes(SDL_Surface *to) spells::Mode mode = spells::Mode::HERO; - if(owner->spellToCast)//hero casts spell + if(owner->actionsController->spellcastingModeActive())//hero casts spell { - spell = SpellID(owner->spellToCast->actionSubtype).toSpell(); + spell = owner->actionsController->selectedSpell().toSpell(); caster = owner->getActiveHero(); } - else if(owner->stacksController->activeStackSpellToCast() != SpellID::NONE && owner->creatureCasting)//stack casts spell + else if(owner->stacksController->activeStackSpellToCast() != SpellID::NONE)//stack casts spell { spell = SpellID(owner->stacksController->activeStackSpellToCast()).toSpell(); caster = owner->stacksController->getActiveStack(); diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index bff5a03fa..318e9710e 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -11,6 +11,7 @@ #include "CBattleInterface.h" #include "CBattleAnimations.h" +#include "CBattleActionsController.h" #include "CBattleInterfaceClasses.h" #include "CCreatureAnimation.h" #include "CBattleProjectileController.h" @@ -62,7 +63,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet const SDL_Rect & myRect, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt) : attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), - creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr), attackerInt(att), defenderInt(defen), curInt(att), myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false) { @@ -129,6 +129,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet //preparing menu background and terrain fieldController.reset( new CBattleFieldController(this)); stacksController.reset( new CBattleStacksController(this)); + actionsController.reset( new CBattleActionsController(this)); //loading hero animations if(hero1) // attacking hero @@ -197,8 +198,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); - currentAction = PossiblePlayerBattleAction::INVALID; - selectedAction = PossiblePlayerBattleAction::INVALID; addUsedEvents(RCLICK | MOVE | KEYBOARD); controlPanel->blockUI(true); } @@ -300,28 +299,28 @@ void CBattleInterface::keyPressed(const SDL_KeyboardEvent & key) } else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED) { - enterCreatureCastingMode(); + actionsController->enterCreatureCastingMode(); } else if(key.keysym.sym == SDLK_ESCAPE) { if(!battleActionsStarted) CCS->soundh->stopSound(battleIntroSoundChannel); else - endCastingSpell(); + actionsController->endCastingSpell(); } } void CBattleInterface::mouseMoved(const SDL_MouseMotionEvent &sEvent) { BattleHex selectedHex = fieldController->getHoveredHex(); - handleHex(selectedHex, MOVE); + actionsController->handleHex(selectedHex, MOVE); } void CBattleInterface::clickRight(tribool down, bool previousState) { if (!down) { - endCastingSpell(); + actionsController->endCastingSpell(); } } @@ -453,7 +452,7 @@ const CGHeroInstance * CBattleInterface::getActiveHero() void CBattleInterface::hexLclicked(int whichOne) { - handleHex(whichOne, LCLICK); + actionsController->handleHex(whichOne, LCLICK); } void CBattleInterface::stackIsCatapulting(const CatapultAttack & ca) @@ -618,36 +617,6 @@ void CBattleInterface::setHeroAnimation(ui8 side, int phase) } } -void CBattleInterface::castThisSpell(SpellID spellID) -{ - spellToCast = std::make_shared(); - spellToCast->actionType = EActionType::HERO_SPELL; - spellToCast->actionSubtype = spellID; //spell number - spellToCast->stackNumber = (attackingHeroInstance->tempOwner == curInt->playerID) ? -1 : -2; - spellToCast->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false; - spellDestSelectMode = true; - creatureCasting = false; - - //choosing possible targets - const CGHeroInstance *castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance; - assert(castingHero); // code below assumes non-null hero - sp = spellID.toSpell(); - PossiblePlayerBattleAction spellSelMode = curInt->cb->getCasterAction(sp, castingHero, spells::Mode::HERO); - - if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location - { - spellToCast->aimToHex(BattleHex::INVALID); - curInt->cb->battleMakeAction(spellToCast.get()); - endCastingSpell(); - } - else - { - possibleActions.clear(); - possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment - GH.fakeMouseMove();//update cursor - } -} - void CBattleInterface::displayBattleLog(const std::vector & battleLog) { for(const auto & line : battleLog) @@ -798,158 +767,10 @@ void CBattleInterface::activateStack() myTurn = true; queue->update(); fieldController->redrawBackgroundWithHexes(); - possibleActions = getPossibleActionsForStack(s); + actionsController->activateStack(); GH.fakeMouseMove(); } -void CBattleInterface::endCastingSpell() -{ - if(spellDestSelectMode) - { - spellToCast.reset(); - - sp = nullptr; - spellDestSelectMode = false; - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); - - if(stacksController->getActiveStack()) - { - possibleActions = getPossibleActionsForStack(stacksController->getActiveStack()); //restore actions after they were cleared - myTurn = true; - } - } - else - { - if(stacksController->getActiveStack()) - { - possibleActions = getPossibleActionsForStack(stacksController->getActiveStack()); - GH.fakeMouseMove(); - } - } -} - -void CBattleInterface::enterCreatureCastingMode() -{ - //silently check for possible errors - if (!myTurn) - return; - - if (tacticsMode) - return; - - //hero is casting a spell - if (spellDestSelectMode) - return; - - if (!stacksController->getActiveStack()) - return; - - if (!stacksController->activeStackSpellcaster()) - return; - - //random spellcaster - if (stacksController->activeStackSpellToCast() == SpellID::NONE) - return; - - if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION)) - { - const spells::Caster * caster = stacksController->getActiveStack(); - const CSpell * spell = stacksController->activeStackSpellToCast().toSpell(); - - spells::Target target; - target.emplace_back(); - - spells::BattleCast cast(curInt->cb.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) - { - myTurn = false; - giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, stacksController->activeStackSpellToCast()); - stacksController->setSelectedStack(nullptr); - - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); - } - } - else - { - possibleActions = getPossibleActionsForStack(stacksController->getActiveStack()); - - auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) - { - return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) && - (x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) && - (x != PossiblePlayerBattleAction::OBSTACLE); - }; - - vstd::erase_if(possibleActions, actionFilterPredicate); - GH.fakeMouseMove(); - } -} - -std::vector CBattleInterface::getPossibleActionsForStack(const CStack *stack) -{ - BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass - data.creatureSpellToCast = stacksController->activeStackSpellToCast(); - data.tacticsMode = tacticsMode; - auto allActions = curInt->cb->getClientActionsForStack(stack, data); - - return std::vector(allActions); -} - -void CBattleInterface::reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context) -{ - if(tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack - - auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it - { - switch(item) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::NO_LOCATION: - case PossiblePlayerBattleAction::FREE_LOCATION: - case PossiblePlayerBattleAction::OBSTACLE: - if(!stack->hasBonusOfType(Bonus::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX) - return 1; - else - return 100;//bottom priority - break; - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - return 2; break; - case PossiblePlayerBattleAction::RISE_DEMONS: - return 3; 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; - default: - return 200; break; - } - }; - - auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs) - { - return assignPriority(lhs) > assignPriority(rhs); - }; - - std::make_heap(possibleActions.begin(), possibleActions.end(), comparer); -} - void CBattleInterface::endAction(const BattleAction* action) { const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); @@ -1114,531 +935,6 @@ void CBattleInterface::tacticNextStack(const CStack * current) } -std::string formatDmgRange(std::pair dmgRange) -{ - if (dmgRange.first != dmgRange.second) - return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str(); - else - return (boost::format("%d") % dmgRange.first).str(); -} - -bool CBattleInterface::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) -{ - std::vector acc = curInt->cb->battleGetAvailableHexes(stackToMove); - 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 CBattleInterface::handleHex(BattleHex myNumber, int eventType) -{ - if (!myTurn || !battleActionsStarted) //we are not permit to do anything - return; - - // This function handles mouse move over hexes and l-clicking on them. - // First we decide what happens if player clicks on this hex and set appropriately - // consoleMsg, cursorFrame/Type and prepare lambda realizeAction. - // - // Then, depending whether it was hover/click we either call the action or set tooltip/cursor. - - //used when hovering -> tooltip message and cursor to be set - std::string consoleMsg; - bool setCursor = true; //if we want to suppress setting cursor - ECursor::ECursorTypes cursorType = ECursor::COMBAT; - int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used? - - //used when l-clicking -> action to be called upon the click - std::function realizeAction; - - //Get stack on the hex - first try to grab the alive one, if not found -> allow dead stacks. - const CStack * shere = curInt->cb->battleGetStackByPos(myNumber, true); - if(!shere) - shere = curInt->cb->battleGetStackByPos(myNumber, false); - - if(!stacksController->getActiveStack()) - return; - - bool ourStack = false; - if (shere) - ourStack = shere->owner == curInt->playerID; - - //stack may have changed, update selection border - stacksController->setHoveredStack(shere); - - localActions.clear(); - illegalActions.clear(); - - reorderPossibleActionsPriority(stacksController->getActiveStack(), shere ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX); - const bool forcedAction = possibleActions.size() == 1; - - for (PossiblePlayerBattleAction action : possibleActions) - { - bool legalAction = false; //this action is legal and can be performed - bool notLegal = false; //this action is not legal and should display message - - switch (action) - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - if (shere && ourStack) - legalAction = true; - break; - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - { - if (!(shere && shere->alive())) //we can walk on dead stacks - { - if(canStackMoveHere(stacksController->getActiveStack(), myNumber)) - legalAction = true; - } - break; - } - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - { - if(curInt->cb->battleCanAttack(stacksController->getActiveStack(), shere, myNumber)) - { - if (fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack? - { - fieldController->setBattleCursor(myNumber); // temporary - needed for following function :( - BattleHex attackFromHex = fieldController->fromWhichHexAttack(myNumber); - - if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) - legalAction = true; - } - } - } - break; - case PossiblePlayerBattleAction::SHOOT: - if(curInt->cb->battleCanShoot(stacksController->getActiveStack(), myNumber)) - legalAction = true; - break; - case PossiblePlayerBattleAction::ANY_LOCATION: - if (myNumber > -1) //TODO: this should be checked for all actions - { - if(isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber)) - legalAction = true; - } - break; - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - if(shere && isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber)) - legalAction = true; - break; - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - { - if(shere && ourStack && shere != stacksController->getActiveStack() && shere->alive()) //only positive spells for other allied creatures - { - int spellID = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), shere, CBattleInfoCallback::RANDOM_GENIE); - if(spellID > -1) - { - legalAction = true; - } - } - } - break; - case PossiblePlayerBattleAction::OBSTACLE: - if(isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber)) - legalAction = true; - break; - case PossiblePlayerBattleAction::TELEPORT: - { - //todo: move to mechanics - ui8 skill = 0; - if (creatureCasting) - skill = stacksController->getActiveStack()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); - else - skill = getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell()); - //TODO: explicitely save power, skill - if (curInt->cb->battleCanTeleportTo(stacksController->getSelectedStack(), myNumber, skill)) - legalAction = true; - else - notLegal = true; - } - break; - case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice - if (shere && shere != stacksController->getSelectedStack() && ourStack && shere->alive()) - legalAction = true; - else - notLegal = true; - break; - case PossiblePlayerBattleAction::FREE_LOCATION: - legalAction = true; - if(!isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber)) - { - legalAction = false; - notLegal = true; - } - break; - case PossiblePlayerBattleAction::CATAPULT: - if (siegeController && siegeController->isCatapultAttackable(myNumber)) - legalAction = true; - break; - case PossiblePlayerBattleAction::HEAL: - if (shere && ourStack && shere->canBeHealed()) - legalAction = true; - break; - case PossiblePlayerBattleAction::RISE_DEMONS: - if (shere && ourStack && !shere->alive()) - { - if (!(shere->hasBonusOfType(Bonus::UNDEAD) - || shere->hasBonusOfType(Bonus::NON_LIVING) - || shere->hasBonusOfType(Bonus::GARGOYLE) - || shere->summoned - || shere->isClone() - || shere->hasBonusOfType(Bonus::SIEGE_WEAPON) - )) - legalAction = true; - } - break; - } - if (legalAction) - localActions.push_back (action); - else if (notLegal || forcedAction) - illegalActions.push_back (action); - } - illegalAction = PossiblePlayerBattleAction::INVALID; //clear it in first place - - if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default - currentAction = selectedAction; - else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority) - currentAction = localActions.front(); - else //no legal action possible - { - currentAction = PossiblePlayerBattleAction::INVALID; //don't allow to do anything - - if (vstd::contains(illegalActions, selectedAction)) - illegalAction = selectedAction; - else if (illegalActions.size()) - illegalAction = illegalActions.front(); - else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature - { - currentAction = PossiblePlayerBattleAction::CREATURE_INFO; - } - else - illegalAction = PossiblePlayerBattleAction::INVALID; //we should never be here - } - - bool isCastingPossible = false; - bool secondaryTarget = false; - - if (currentAction > PossiblePlayerBattleAction::INVALID) - { - switch (currentAction) //display console message, realize selected action - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s - realizeAction = [=](){ stackActivated(shere); }; - break; - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING)) - { - cursorFrame = ECursor::COMBAT_FLY; - consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % stacksController->getActiveStack()->getName()).str(); //Fly %s here - } - else - { - cursorFrame = ECursor::COMBAT_MOVE; - consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % stacksController->getActiveStack()->getName()).str(); //Move %s here - } - - realizeAction = [=]() - { - if(stacksController->getActiveStack()->doubleWide()) - { - std::vector acc = curInt->cb->battleGetAvailableHexes(stacksController->getActiveStack()); - BattleHex shiftedDest = myNumber.cloneInDirection(stacksController->getActiveStack()->destShiftDir(), false); - if(vstd::contains(acc, myNumber)) - giveCommand(EActionType::WALK, myNumber); - else if(vstd::contains(acc, shiftedDest)) - giveCommand(EActionType::WALK, shiftedDest); - } - else - { - giveCommand(EActionType::WALK, myNumber); - } - }; - break; - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return - { - fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile - setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean? - - bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN; - - realizeAction = [=]() - { - BattleHex attackFromHex = fieldController->fromWhichHexAttack(myNumber); - if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) - { - auto command = new BattleAction(BattleAction::makeMeleeAttack(stacksController->getActiveStack(), myNumber, attackFromHex, returnAfterAttack)); - sendCommand(command, stacksController->getActiveStack()); - } - }; - - TDmgRange damage = curInt->cb->battleEstimateDamage(stacksController->getActiveStack(), shere); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage) - } - break; - case PossiblePlayerBattleAction::SHOOT: - { - if (curInt->cb->battleHasShootingPenalty(stacksController->getActiveStack(), myNumber)) - cursorFrame = ECursor::COMBAT_SHOOT_PENALTY; - else - cursorFrame = ECursor::COMBAT_SHOOT; - - realizeAction = [=](){giveCommand(EActionType::SHOOT, myNumber);}; - TDmgRange damage = curInt->cb->battleEstimateDamage(stacksController->getActiveStack(), shere); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - //printing - Shoot %s (%d shots left, %s damage) - consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % stacksController->getActiveStack()->shots.available() % estDmgText).str(); - } - break; - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - sp = CGI->spellh->objects[creatureCasting ? stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s - switch (sp->id) - { - case SpellID::SACRIFICE: - case SpellID::TELEPORT: - stacksController->setSelectedStack(shere); //remember first target - secondaryTarget = true; - break; - } - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::ANY_LOCATION: - sp = CGI->spellh->objects[creatureCasting ? stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell - sp = nullptr; - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on % - creatureCasting = true; - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::TELEPORT: - consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here - cursorFrame = ECursor::COMBAT_TELEPORT; - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::OBSTACLE: - consoleMsg = CGI->generaltexth->allTexts[550]; - //TODO: remove obstacle cursor - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::SACRIFICE: - consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s - cursorFrame = ECursor::COMBAT_SACRIFICE; - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::FREE_LOCATION: - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s - isCastingPossible = true; - break; - case PossiblePlayerBattleAction::HEAL: - cursorFrame = ECursor::COMBAT_HEAL; - consoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s - realizeAction = [=](){ giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing - break; - case PossiblePlayerBattleAction::RISE_DEMONS: - cursorType = ECursor::SPELLBOOK; - realizeAction = [=]() - { - giveCommand(EActionType::DAEMON_SUMMONING, myNumber); - }; - break; - case PossiblePlayerBattleAction::CATAPULT: - cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT; - realizeAction = [=](){ giveCommand(EActionType::CATAPULT, myNumber); }; - break; - case PossiblePlayerBattleAction::CREATURE_INFO: - { - cursorFrame = ECursor::COMBAT_QUERY; - consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str(); - realizeAction = [=](){ GH.pushIntT(shere, false); }; - break; - } - } - } - else //no possible valid action, display message - { - switch (illegalAction) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - cursorFrame = ECursor::COMBAT_BLOCKED; - consoleMsg = CGI->generaltexth->allTexts[23]; - break; - case PossiblePlayerBattleAction::TELEPORT: - cursorFrame = ECursor::COMBAT_BLOCKED; - consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination - break; - case PossiblePlayerBattleAction::SACRIFICE: - consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice - break; - case PossiblePlayerBattleAction::FREE_LOCATION: - cursorFrame = ECursor::COMBAT_BLOCKED; - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % sp->name); //No room to place %s here - break; - default: - if (myNumber == -1) - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc. - else - cursorFrame = ECursor::COMBAT_BLOCKED; - break; - } - } - - if (isCastingPossible) //common part - { - switch (currentAction) //don't use that with teleport / sacrifice - { - case PossiblePlayerBattleAction::TELEPORT: //FIXME: more generic solution? - case PossiblePlayerBattleAction::SACRIFICE: - break; - default: - cursorType = ECursor::SPELLBOOK; - cursorFrame = 0; - if (consoleMsg.empty() && sp) - consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s - break; - } - - realizeAction = [=]() - { - if(secondaryTarget) //select that target now - { - - possibleActions.clear(); - switch (sp->id.toEnum()) - { - case SpellID::TELEPORT: //don't cast spell yet, only select target - spellToCast->aimToUnit(shere); - possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT); - break; - case SpellID::SACRIFICE: - spellToCast->aimToHex(myNumber); - possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE); - break; - } - } - else - { - if (creatureCasting) - { - if (sp) - { - giveCommand(EActionType::MONSTER_SPELL, myNumber, stacksController->activeStackSpellToCast()); - } - else //unknown random spell - { - giveCommand(EActionType::MONSTER_SPELL, myNumber); - } - } - else - { - assert(sp); - switch (sp->id.toEnum()) - { - case SpellID::SACRIFICE: - spellToCast->aimToUnit(shere);//victim - break; - default: - spellToCast->aimToHex(myNumber); - break; - } - curInt->cb->battleMakeAction(spellToCast.get()); - endCastingSpell(); - } - stacksController->setSelectedStack(nullptr); - } - }; - } - - { - if (eventType == MOVE) - { - if (setCursor) - CCS->curh->changeGraphic(cursorType, cursorFrame); - controlPanel->console->write(consoleMsg); - } - if (eventType == LCLICK && realizeAction) - { - //opening creature window shouldn't affect myTurn... - if ((currentAction != PossiblePlayerBattleAction::CREATURE_INFO) && !secondaryTarget) - { - myTurn = false; //tends to crash with empty calls - } - realizeAction(); - if (!secondaryTarget) //do not replace teleport or sacrifice cursor - CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); - controlPanel->console->clear(); - } - } -} - -bool CBattleInterface::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber) -{ - creatureCasting = stacksController->activeStackSpellcaster() && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells - - bool isCastingPossible = true; - - int spellID = -1; - if (creatureCasting) - { - if (stacksController->activeStackSpellToCast() != SpellID::NONE && (shere != sactive)) //can't cast on itself - spellID = stacksController->activeStackSpellToCast(); //TODO: merge with SpellTocast? - } - else //hero casting - { - spellID = spellToCast->actionSubtype; - } - - - sp = nullptr; - if (spellID >= 0) - sp = CGI->spellh->objects[spellID]; - - if (sp) - { - const spells::Caster *caster = creatureCasting ? static_cast(sactive) : static_cast(curInt->cb->battleGetMyHero()); - if (caster == nullptr) - { - isCastingPossible = false;//just in case - } - else - { - const spells::Mode mode = creatureCasting ? spells::Mode::CREATURE_ACTIVE : spells::Mode::HERO; - - spells::Target target; - target.emplace_back(myNumber); - - spells::BattleCast cast(curInt->cb.get(), caster, mode, sp); - - auto m = sp->battleMechanics(&cast); - spells::detail::ProblemImpl problem; //todo: display problem in status bar - - isCastingPossible = m->canBeCastAt(target, problem); - } - } - else - isCastingPossible = false; - if (!myNumber.isAvailable() && !shere) //empty tile outside battlefield (or in the unavailable border column) - isCastingPossible = false; - - return isCastingPossible; -} - void CBattleInterface::obstaclePlaced(const CObstacleInstance & oi) { obstacleController->obstaclePlaced(oi); @@ -1784,8 +1080,6 @@ void CBattleInterface::showBattlefieldObjects(SDL_Surface *to) showHexEntry(objects.afterAll); } - - void CBattleInterface::showBattleEffects(SDL_Surface *to, const std::vector &battleEffects) { for (auto & elem : battleEffects) @@ -1852,3 +1146,8 @@ BattleObjectsByHex CBattleInterface::sortObjectsByHex() return sorted; } + +void CBattleInterface::castThisSpell(SpellID spellID) +{ + actionsController->castThisSpell(spellID); +} diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index 3fb8c0e8e..f3fe2e8fb 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -64,6 +64,7 @@ class CBattleObstacleController; class CBattleFieldController; class CBattleControlPanel; class CBattleStacksController; +class CBattleActionsController; /// Small struct which contains information about the id of the attacked stack, the damage dealt,... struct StackAttackedInfo @@ -109,12 +110,6 @@ struct BattleObjectsByHex std::array hex; }; -enum class MouseHoveredHexContext -{ - UNOCCUPIED_HEX, - OCCUPIED_HEX -}; - /// Big class which handles the overall battle interface actions and it is also responsible for /// drawing everything correctly. class CBattleInterface : public WindowBase @@ -135,16 +130,6 @@ private: ui8 animCount; bool tacticsMode; - bool creatureCasting; //if true, stack currently aims to cats a spell - bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console - std::shared_ptr spellToCast; //spell for which player is choosing destination - const CSpell *sp; //spell pointer for convenience - std::vector possibleActions; //all actions possible to call at the moment by player - std::vector localActions; //actions possible to take on hovered hex - std::vector illegalActions; //these actions display message in case of illegal target - PossiblePlayerBattleAction currentAction; //action that will be performed on l-click - PossiblePlayerBattleAction selectedAction; //last action chosen (and saved) by player - PossiblePlayerBattleAction illegalAction; //most likely action that can't be performed here bool battleActionsStarted; //used for delaying battle actions until intro sound stops int battleIntroSoundChannel; //required as variable for disabling it via ESC key @@ -154,12 +139,6 @@ private: void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all void requestAutofightingAIToTakeAction(); - std::vector getPossibleActionsForStack (const CStack *stack); //called when stack gets its turn - void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled) - void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context); - - //force active stack to cast a spell if possible - void enterCreatureCastingMode(); void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1); void sendCommand(BattleAction *& command, const CStack * actor = nullptr); @@ -181,12 +160,12 @@ public: std::unique_ptr obstacleController; std::unique_ptr fieldController; std::unique_ptr stacksController; + std::unique_ptr actionsController; static CondSh animsAreDisplayed; //for waiting with the end of battle for end of anims static CondSh givenCommand; //data != nullptr if we have i.e. moved current unit bool myTurn; //if true, interface is active (commands can be ordered) - bool moveStarted; //if true, the creature that is already moving is going to make its first step int moveSoundHander; // sound handler used when moving a unit @@ -251,10 +230,6 @@ public: void hideQueue(); void showQueue(); - void handleHex(BattleHex myNumber, int eventType); - bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber); - bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber); //TODO: move to BattleState / callback - void obstaclePlaced(const CObstacleInstance & oi); void gateStateChanged(const EGateState state); @@ -286,4 +261,5 @@ public: friend class CBattleFieldController; friend class CBattleControlPanel; friend class CBattleStacksController; + friend class CBattleActionsController; }; diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index 6f2207f0a..b7b66fd68 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -11,6 +11,7 @@ #include "CBattleInterfaceClasses.h" #include "CBattleInterface.h" +#include "CBattleActionsController.h" #include "CBattleSiegeController.h" #include "CBattleFieldController.h" #include "CBattleStacksController.h" @@ -191,7 +192,7 @@ void CBattleHero::hover(bool on) void CBattleHero::clickLeft(tribool down, bool previousState) { - if(myOwner->spellDestSelectMode) //we are casting a spell + if(myOwner->actionsController->spellcastingModeActive()) //we are casting a spell return; if(boost::logic::indeterminate(down))