mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-24 22:14:36 +02:00
891 lines
30 KiB
C++
891 lines
30 KiB
C++
/*
|
|
* 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 "../windows/CCreatureWindow.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<ui32, ui32> dmgRange)
|
|
{
|
|
if (dmgRange.first != dmgRange.second)
|
|
return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str();
|
|
else
|
|
return (boost::format("%d") % dmgRange.first).str();
|
|
}
|
|
|
|
BattleActionsController::BattleActionsController(BattleInterface & owner):
|
|
owner(owner),
|
|
heroSpellToCast(nullptr),
|
|
creatureSpellToCast(nullptr)
|
|
{}
|
|
|
|
void BattleActionsController::endCastingSpell()
|
|
{
|
|
if(heroSpellToCast)
|
|
heroSpellToCast.reset();
|
|
|
|
if(owner.stacksController->getActiveStack())
|
|
possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
|
|
|
|
GH.fakeMouseMove();
|
|
}
|
|
|
|
bool BattleActionsController::isActiveStackSpellcaster() const
|
|
{
|
|
const CStack * casterStack = owner.stacksController->getActiveStack();
|
|
if (!casterStack)
|
|
return false;
|
|
|
|
const auto randomSpellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER));
|
|
return (randomSpellcaster && 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;
|
|
|
|
if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
|
|
{
|
|
const spells::Caster * caster = owner.stacksController->getActiveStack();
|
|
const CSpell * spell = getStackSpellToCast();
|
|
|
|
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.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId());
|
|
owner.stacksController->setSelectedStack(nullptr);
|
|
|
|
CCS->curh->set(Cursor::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<PossiblePlayerBattleAction> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
|
|
{
|
|
BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
|
|
|
|
if (getStackSpellToCast())
|
|
data.creatureSpellToCast = getStackSpellToCast()->getId();
|
|
else
|
|
data.creatureSpellToCast = SpellID::NONE;
|
|
|
|
data.tacticsMode = owner.tacticsMode;
|
|
auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data);
|
|
|
|
allActions.push_back(PossiblePlayerBattleAction::HERO_INFO);
|
|
allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO);
|
|
|
|
return std::vector<PossiblePlayerBattleAction>(allActions);
|
|
}
|
|
|
|
void BattleActionsController::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::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 = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs)
|
|
{
|
|
return assignPriority(lhs) < assignPriority(rhs);
|
|
};
|
|
|
|
std::sort(possibleActions.begin(), possibleActions.end(), comparer);
|
|
}
|
|
|
|
void BattleActionsController::castThisSpell(SpellID spellID)
|
|
{
|
|
heroSpellToCast = std::make_shared<BattleAction>();
|
|
heroSpellToCast->actionType = EActionType::HERO_SPELL;
|
|
heroSpellToCast->actionSubtype = spellID; //spell number
|
|
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.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
|
|
|
|
if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
|
|
{
|
|
heroSpellToCast->aimToHex(BattleHex::INVALID);
|
|
owner.curInt->cb->battleMakeAction(heroSpellToCast.get());
|
|
endCastingSpell();
|
|
}
|
|
else
|
|
{
|
|
possibleActions.clear();
|
|
possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
|
|
GH.fakeMouseMove();//update cursor
|
|
}
|
|
}
|
|
|
|
const CSpell * BattleActionsController::getHeroSpellToCast( ) const
|
|
{
|
|
if (heroSpellToCast)
|
|
return SpellID(heroSpellToCast->actionSubtype).toSpell();
|
|
return nullptr;
|
|
}
|
|
|
|
const CSpell * BattleActionsController::getStackSpellToCast( ) const
|
|
{
|
|
if (isActiveStackSpellcaster())
|
|
return creatureSpellToCast;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const CSpell * BattleActionsController::getCurrentSpell( ) const
|
|
{
|
|
if (getHeroSpellToCast())
|
|
return getHeroSpellToCast();
|
|
return getStackSpellToCast();
|
|
}
|
|
|
|
const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
|
|
{
|
|
const CStack * shere = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
|
|
if(shere)
|
|
return shere;
|
|
return owner.curInt->cb->battleGetStackByPos(hoveredHex, false);
|
|
}
|
|
|
|
void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
|
|
{
|
|
switch (action)
|
|
{
|
|
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(Bonus::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:
|
|
owner.fieldController->setBattleCursor(targetHex);
|
|
return;
|
|
|
|
case PossiblePlayerBattleAction::SHOOT:
|
|
if (owner.curInt->cb->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)
|
|
{
|
|
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) //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(Bonus::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);
|
|
TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
|
|
std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
|
|
return (boost::format(CGI->generaltexth->allTexts[36]) % targetStack->getName() % estDmgText).str(); //Attack %s (%s damage)
|
|
}
|
|
|
|
case PossiblePlayerBattleAction::SHOOT:
|
|
{
|
|
auto const * shooter = owner.stacksController->getActiveStack();
|
|
|
|
TDmgRange damage = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
|
|
std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
|
|
//printing - Shoot %s (%d shots left, %s damage)
|
|
return (boost::format(CGI->generaltexth->allTexts[296]) % targetStack->getName() % shooter->shots.available() % estDmgText).str();
|
|
}
|
|
|
|
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
|
|
return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % getCurrentSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s
|
|
|
|
case PossiblePlayerBattleAction::ANY_LOCATION:
|
|
return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % getCurrentSpell()->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]) % getCurrentSpell()->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)
|
|
{
|
|
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]) % getCurrentSpell()->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->owner == owner.curInt->playerID;
|
|
|
|
switch (action)
|
|
{
|
|
case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
|
|
case PossiblePlayerBattleAction::CREATURE_INFO:
|
|
return (targetStack && targetStackOwned);
|
|
|
|
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.curInt->cb->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.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
|
|
|
|
case PossiblePlayerBattleAction::ANY_LOCATION:
|
|
return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
|
|
|
|
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
|
|
return targetStack && isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
|
|
|
|
case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
|
|
if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
|
|
{
|
|
int spellID = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
|
|
return spellID > -1;
|
|
}
|
|
return false;
|
|
|
|
case PossiblePlayerBattleAction::TELEPORT:
|
|
{
|
|
ui8 skill = getCurrentSpellcaster()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
|
|
return owner.curInt->cb->battleCanTeleportTo(owner.stacksController->getSelectedStack(), targetHex, skill);
|
|
}
|
|
|
|
case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
|
|
return targetStack && targetStack != owner.stacksController->getSelectedStack() && targetStackOwned && targetStack->alive();
|
|
|
|
case PossiblePlayerBattleAction::OBSTACLE:
|
|
case PossiblePlayerBattleAction::FREE_LOCATION:
|
|
return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex);
|
|
return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, 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) //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<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack());
|
|
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 == 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)
|
|
{
|
|
auto command = new BattleAction(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.pushIntT<CStackWindow>(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 == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
|
|
{
|
|
if (getCurrentSpell()->id == SpellID::SACRIFICE)
|
|
{
|
|
heroSpellToCast->aimToHex(targetHex);
|
|
possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE);
|
|
owner.stacksController->setSelectedStack(targetStack);
|
|
return;
|
|
}
|
|
if (getCurrentSpell()->id == SpellID::TELEPORT)
|
|
{
|
|
heroSpellToCast->aimToUnit(targetStack);
|
|
possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT);
|
|
owner.stacksController->setSelectedStack(targetStack);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!spellcastingModeActive())
|
|
{
|
|
if (getStackSpellToCast())
|
|
{
|
|
owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, getStackSpellToCast()->getId());
|
|
}
|
|
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->battleMakeAction(heroSpellToCast.get());
|
|
endCastingSpell();
|
|
}
|
|
owner.stacksController->setSelectedStack(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 ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX);
|
|
|
|
for (PossiblePlayerBattleAction action : possibleActions)
|
|
{
|
|
if (actionIsLegal(action, targetHex))
|
|
return action;
|
|
}
|
|
return possibleActions.front();
|
|
}
|
|
|
|
void BattleActionsController::onHexHovered(BattleHex hoveredHex)
|
|
{
|
|
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)
|
|
{
|
|
const auto spellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::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
|
|
creatureSpellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
|
|
}
|
|
}
|
|
|
|
const spells::Caster * BattleActionsController::getCurrentSpellcaster() const
|
|
{
|
|
if (heroSpellToCast)
|
|
return owner.getActiveHero();
|
|
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 CStack *casterStack, const CStack *targetStack, BattleHex targetHex)
|
|
{
|
|
auto currentSpell = getCurrentSpell();
|
|
assert(currentSpell);
|
|
|
|
if (!currentSpell)
|
|
return false;
|
|
|
|
auto caster = getCurrentSpellcaster();
|
|
|
|
const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE;
|
|
|
|
spells::Target target;
|
|
target.emplace_back(targetHex);
|
|
|
|
spells::BattleCast cast(owner.curInt->cb.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<BattleHex> 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 BattleActionsController::activateStack()
|
|
{
|
|
const CStack * s = owner.stacksController->getActiveStack();
|
|
if(s)
|
|
{
|
|
tryActivateStackSpellcasting(s);
|
|
|
|
possibleActions = getPossibleActionsForStack(s);
|
|
std::list<PossiblePlayerBattleAction> actionsToSelect;
|
|
if(!possibleActions.empty())
|
|
{
|
|
switch(possibleActions.front())
|
|
{
|
|
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());
|
|
break;
|
|
}
|
|
}
|
|
owner.windowObject->setAlternativeActions(actionsToSelect);
|
|
}
|
|
}
|
|
|
|
void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
|
|
{
|
|
auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true);
|
|
|
|
if (selectedStack != nullptr)
|
|
GH.pushIntT<CStackWindow>(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 == PossiblePlayerBattleAction::ANY_LOCATION ||
|
|
action == PossiblePlayerBattleAction::NO_LOCATION ||
|
|
action == PossiblePlayerBattleAction::FREE_LOCATION ||
|
|
action == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ||
|
|
action == PossiblePlayerBattleAction::OBSTACLE;
|
|
}
|
|
|
|
const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const
|
|
{
|
|
return possibleActions;
|
|
}
|
|
|
|
void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
|
|
{
|
|
vstd::erase(possibleActions, action);
|
|
}
|
|
|
|
void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
|
|
{
|
|
possibleActions.insert(possibleActions.begin(), action);
|
|
}
|