1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00

Creature spellcast refactor (#569)

* Move some logic to lib
* Mouse action priority queue enhancement
* Get rid of siegehandler dependency
* Improve AI offensive spellcasting
* CBattleInterface cleanup
This commit is contained in:
Dydzio 2019-05-04 05:42:55 +02:00 committed by Alexander Shishkin
parent 14e3bb29f1
commit e50efdc279
9 changed files with 330 additions and 153 deletions

View File

@ -14,7 +14,6 @@
#include "StackWithBonuses.h"
#include "EnemyInfo.h"
#include "PossibleSpellcast.h"
#include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h"
#include "../../lib/spells/CSpellHandler.h"
@ -122,16 +121,54 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
return *action;
//best action is from effective owner point if view, we are effective owner as we received "activeStack"
//evaluate casting spell for spellcasting stack
boost::optional<PossibleSpellcast> bestSpellcast(boost::none);
//TODO: faerie dragon type spell should be selected by server
SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
if(stack->hasBonusOfType(Bonus::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
{
const CSpell * spell = creatureSpellToCast.toSpell();
if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack))
{
std::vector<PossibleSpellcast> possibleCasts;
spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
for(auto & target : temp.findPotentialTargets())
{
PossibleSpellcast ps;
ps.dest = target;
ps.spell = spell;
evaluateCreatureSpellcast(stack, ps);
possibleCasts.push_back(ps);
}
std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; });
if(!possibleCasts.empty() && possibleCasts.front().value > 0)
{
bestSpellcast = boost::optional<PossibleSpellcast>(possibleCasts.front());
}
}
}
HypotheticBattle hb(getCbc());
PotentialTargets targets(stack, &hb);
if(targets.possibleAttacks.size())
{
auto hlp = targets.bestAction();
if(hlp.attack.shooting)
return BattleAction::makeShotAttack(stack, hlp.attack.defender);
AttackPossibility bestAttack = targets.bestAction();
//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
if(bestSpellcast.is_initialized() && bestSpellcast->value > bestAttack.damageDiff())
return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
else if(bestAttack.attack.shooting)
return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
else
return BattleAction::makeMeleeAttack(stack, hlp.attack.defender->getPosition(), hlp.tile);
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.tile);
}
else if(bestSpellcast.is_initialized())
{
return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
}
else
{
@ -521,6 +558,58 @@ void CBattleAI::attemptCastingSpell()
}
}
//Below method works only for offensive spells
void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps)
{
using ValueMap = PossibleSpellcast::ValueMap;
RNGStub rngStub;
HypotheticBattle state(getCbc());
TStacks all = getCbc()->battleGetAllStacks(false);
ValueMap healthOfStack;
ValueMap newHealthOfStack;
for(auto unit : all)
{
healthOfStack[unit->unitId()] = unit->getAvailableHealth();
}
spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell);
cast.target = ps.dest;
cast.cast(&state, rngStub);
for(auto unit : all)
{
auto unitId = unit->unitId();
auto localUnit = state.battleGetUnitByID(unitId);
newHealthOfStack[unitId] = localUnit->getAvailableHealth();
}
int64_t totalGain = 0;
for(auto unit : all)
{
auto unitId = unit->unitId();
auto localUnit = state.battleGetUnitByID(unitId);
auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId];
if(localUnit->unitOwner() != getCbc()->getPlayerID())
healthDiff = -healthDiff;
if(healthDiff < 0)
{
ps.value = -1;
return; //do not damage own units at all
}
totalGain += healthDiff;
}
ps.value = totalGain;
};
int CBattleAI::distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances &dists, BattleHex *chosenHex)
{
int ret = 1000000;

View File

@ -9,6 +9,7 @@
*/
#pragma once
#include "../../lib/AI_Base.h"
#include "PossibleSpellcast.h"
#include "PotentialTargets.h"
class CSpell;
@ -60,6 +61,8 @@ public:
void init(std::shared_ptr<CBattleCallback> CB) override;
void attemptCastingSpell();
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
BattleAction goTowards(const CStack * stack, BattleHex hex );

View File

@ -415,8 +415,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
memset(stackCountOutsideHexes, 1, GameConstants::BFIELD_SIZE *sizeof(bool)); //initialize array with trues
currentAction = INVALID;
selectedAction = INVALID;
currentAction = PossiblePlayerBattleAction::INVALID;
selectedAction = PossiblePlayerBattleAction::INVALID;
addUsedEvents(RCLICK | MOVE | KEYBOARD);
blockUI(true);
}
@ -1395,24 +1395,6 @@ void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
redrawBackgroundWithHexes(activeStack);
}
CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const
{
PossibleActions spellSelMode = ANY_LOCATION;
const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode);
if(ti.massive || ti.type == spells::AimType::NO_TARGET)
spellSelMode = NO_LOCATION;
else if(ti.type == spells::AimType::LOCATION && ti.clearAffected)
spellSelMode = FREE_LOCATION;
else if(ti.type == spells::AimType::CREATURE)
spellSelMode = AIMED_SPELL_CREATURE;
else if(ti.type == spells::AimType::OBSTACLE)
spellSelMode = OBSTACLE;
return spellSelMode;
}
void CBattleInterface::setHeroAnimation(ui8 side, int phase)
{
if(side == BattleSide::ATTACKER)
@ -1441,9 +1423,9 @@ void CBattleInterface::castThisSpell(SpellID spellID)
const CGHeroInstance *castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance;
assert(castingHero); // code below assumes non-null hero
sp = spellID.toSpell();
PossibleActions spellSelMode = getCasterAction(sp, castingHero, spells::Mode::HERO);
PossiblePlayerBattleAction spellSelMode = curInt->cb->getCasterAction(sp, castingHero, spells::Mode::HERO);
if (spellSelMode == NO_LOCATION) //user does not have to select location
if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
{
spellToCast->aimToHex(BattleHex::INVALID);
curInt->cb->battleMakeAction(spellToCast.get());
@ -1669,7 +1651,7 @@ void CBattleInterface::activateStack()
creatureSpellToCast = -1;
}
getPossibleActionsForStack(s, false);
possibleActions = getPossibleActionsForStack(s);
GH.fakeMouseMove();
}
@ -1686,7 +1668,7 @@ void CBattleInterface::endCastingSpell()
if(activeStack)
{
getPossibleActionsForStack(activeStack, false); //restore actions after they were cleared
possibleActions = getPossibleActionsForStack(activeStack); //restore actions after they were cleared
myTurn = true;
}
}
@ -1694,7 +1676,7 @@ void CBattleInterface::endCastingSpell()
{
if(activeStack)
{
getPossibleActionsForStack(activeStack, false);
possibleActions = getPossibleActionsForStack(activeStack);
GH.fakeMouseMove();
}
}
@ -1723,7 +1705,7 @@ void CBattleInterface::enterCreatureCastingMode()
if (creatureSpellToCast == -1)
return;
if (vstd::contains(possibleActions, NO_LOCATION))
if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
{
const spells::Caster *caster = activeStack;
const CSpell *spell = SpellID(creatureSpellToCast).toSpell();
@ -1740,67 +1722,77 @@ void CBattleInterface::enterCreatureCastingMode()
}
else
{
getPossibleActionsForStack(activeStack, true);
possibleActions = getPossibleActionsForStack(activeStack);
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();
}
}
void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const bool forceCast)
std::vector<PossiblePlayerBattleAction> CBattleInterface::getPossibleActionsForStack(const CStack *stack)
{
possibleActions.clear();
if (tacticsMode)
BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
data.creatureSpellToCast = creatureSpellToCast;
data.tacticsMode = tacticsMode;
auto allActions = curInt->cb->getClientActionsForStack(stack, data);
return std::vector<PossiblePlayerBattleAction>(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
{
possibleActions.push_back(MOVE_TACTICS);
possibleActions.push_back(CHOOSE_TACTICS_STACK);
}
else
{
PossibleActions notPriority = INVALID;
//first action will be prioritized over later ones
if(stack->canCast()) //TODO: check for battlefield effects that prevent casting?
switch(item)
{
if(stack->hasBonusOfType (Bonus::SPELLCASTER))
{
if(creatureSpellToCast != -1)
{
const CSpell *spell = SpellID(creatureSpellToCast).toSpell();
PossibleActions act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE);
if(forceCast)
{
//forced action to be only one possible
possibleActions.push_back(act);
return;
}
else
//if cast is not forced, cast action will have lowest priority
notPriority = act;
}
}
if (stack->hasBonusOfType (Bonus::RANDOM_SPELLCASTER))
possibleActions.push_back (RANDOM_GENIE_SPELL);
if (stack->hasBonusOfType (Bonus::DAEMON_SUMMONING))
possibleActions.push_back (RISE_DEMONS);
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;
}
if(stack->canShoot())
possibleActions.push_back(SHOOT);
if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE))
possibleActions.push_back(ATTACK_AND_RETURN);
};
possibleActions.push_back(ATTACK); //all active stacks can attack
possibleActions.push_back(WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere
auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs)
{
return assignPriority(lhs) > assignPriority(rhs);
};
if (stack->canMove() && stack->Speed(0, true)) //probably no reason to try move war machines or bound stacks
possibleActions.push_back (MOVE_STACK); //all active stacks can attack
if (siegeH && stack->hasBonusOfType (Bonus::CATAPULT)) //TODO: check shots
possibleActions.push_back (CATAPULT);
if (stack->hasBonusOfType (Bonus::HEALER))
possibleActions.push_back (HEAL);
if (notPriority != INVALID)
possibleActions.push_back(notPriority);
}
std::make_heap(possibleActions.begin(), possibleActions.end(), comparer);
}
void CBattleInterface::printConsoleAttacked(const CStack * defender, int dmg, int killed, const CStack * attacker, bool multiple)
@ -2127,21 +2119,22 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
localActions.clear();
illegalActions.clear();
reorderPossibleActionsPriority(activeStack, shere ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX);
const bool forcedAction = possibleActions.size() == 1;
for (PossibleActions action : possibleActions)
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 CHOOSE_TACTICS_STACK:
case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
if (shere && ourStack)
legalAction = true;
break;
case MOVE_TACTICS:
case MOVE_STACK:
case PossiblePlayerBattleAction::MOVE_TACTICS:
case PossiblePlayerBattleAction::MOVE_STACK:
{
if (!(shere && shere->alive())) //we can walk on dead stacks
{
@ -2150,9 +2143,9 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
}
break;
}
case ATTACK:
case WALK_AND_ATTACK:
case ATTACK_AND_RETURN:
case PossiblePlayerBattleAction::ATTACK:
case PossiblePlayerBattleAction::WALK_AND_ATTACK:
case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
{
if(curInt->cb->battleCanAttack(activeStack, shere, myNumber))
{
@ -2167,22 +2160,22 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
}
}
break;
case SHOOT:
case PossiblePlayerBattleAction::SHOOT:
if(curInt->cb->battleCanShoot(activeStack, myNumber))
legalAction = true;
break;
case ANY_LOCATION:
case PossiblePlayerBattleAction::ANY_LOCATION:
if (myNumber > -1) //TODO: this should be checked for all actions
{
if(isCastingPossibleHere(activeStack, shere, myNumber))
legalAction = true;
}
break;
case AIMED_SPELL_CREATURE:
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
if(shere && isCastingPossibleHere(activeStack, shere, myNumber))
legalAction = true;
break;
case RANDOM_GENIE_SPELL:
case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
{
if(shere && ourStack && shere != activeStack && shere->alive()) //only positive spells for other allied creatures
{
@ -2194,11 +2187,11 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
}
}
break;
case OBSTACLE:
case PossiblePlayerBattleAction::OBSTACLE:
if(isCastingPossibleHere(activeStack, shere, myNumber))
legalAction = true;
break;
case TELEPORT:
case PossiblePlayerBattleAction::TELEPORT:
{
//todo: move to mechanics
ui8 skill = 0;
@ -2213,13 +2206,13 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
notLegal = true;
}
break;
case SACRIFICE: //choose our living stack to sacrifice
case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
if (shere && shere != selectedStack && ourStack && shere->alive())
legalAction = true;
else
notLegal = true;
break;
case FREE_LOCATION:
case PossiblePlayerBattleAction::FREE_LOCATION:
legalAction = true;
if(!isCastingPossibleHere(activeStack, shere, myNumber))
{
@ -2227,15 +2220,15 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
notLegal = true;
}
break;
case CATAPULT:
case PossiblePlayerBattleAction::CATAPULT:
if (isCatapultAttackable(myNumber))
legalAction = true;
break;
case HEAL:
case PossiblePlayerBattleAction::HEAL:
if (shere && ourStack && shere->canBeHealed())
legalAction = true;
break;
case RISE_DEMONS:
case PossiblePlayerBattleAction::RISE_DEMONS:
if (shere && ourStack && !shere->alive())
{
if (!(shere->hasBonusOfType(Bonus::UNDEAD)
@ -2253,7 +2246,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
else if (notLegal || forcedAction)
illegalActions.push_back (action);
}
illegalAction = INVALID; //clear it in first place
illegalAction = PossiblePlayerBattleAction::INVALID; //clear it in first place
if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default
currentAction = selectedAction;
@ -2261,7 +2254,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
currentAction = localActions.front();
else //no legal action possible
{
currentAction = INVALID; //don't allow to do anything
currentAction = PossiblePlayerBattleAction::INVALID; //don't allow to do anything
if (vstd::contains(illegalActions, selectedAction))
illegalAction = selectedAction;
@ -2269,25 +2262,25 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
illegalAction = illegalActions.front();
else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature
{
currentAction = CREATURE_INFO;
currentAction = PossiblePlayerBattleAction::CREATURE_INFO;
}
else
illegalAction = INVALID; //we should never be here
illegalAction = PossiblePlayerBattleAction::INVALID; //we should never be here
}
bool isCastingPossible = false;
bool secondaryTarget = false;
if (currentAction > INVALID)
if (currentAction > PossiblePlayerBattleAction::INVALID)
{
switch (currentAction) //display console message, realize selected action
{
case CHOOSE_TACTICS_STACK:
case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s
realizeAction = [=](){ stackActivated(shere); };
break;
case MOVE_TACTICS:
case MOVE_STACK:
case PossiblePlayerBattleAction::MOVE_TACTICS:
case PossiblePlayerBattleAction::MOVE_STACK:
if (activeStack->hasBonusOfType(Bonus::FLYING))
{
cursorFrame = ECursor::COMBAT_FLY;
@ -2316,14 +2309,14 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
}
};
break;
case ATTACK:
case WALK_AND_ATTACK:
case ATTACK_AND_RETURN: //TODO: allow to disable return
case PossiblePlayerBattleAction::ATTACK:
case PossiblePlayerBattleAction::WALK_AND_ATTACK:
case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
{
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 == ATTACK_AND_RETURN;
bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
realizeAction = [=]()
{
@ -2339,7 +2332,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage)
}
break;
case SHOOT:
case PossiblePlayerBattleAction::SHOOT:
{
if (curInt->cb->battleHasShootingPenalty(activeStack, myNumber))
cursorFrame = ECursor::COMBAT_SHOOT_PENALTY;
@ -2352,7 +2345,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % activeStack->shots.available() % estDmgText).str();
}
break;
case AIMED_SPELL_CREATURE:
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : 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)
@ -2365,53 +2358,53 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
}
isCastingPossible = true;
break;
case ANY_LOCATION:
case PossiblePlayerBattleAction::ANY_LOCATION:
sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : 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 RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
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 TELEPORT:
case PossiblePlayerBattleAction::TELEPORT:
consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here
cursorFrame = ECursor::COMBAT_TELEPORT;
isCastingPossible = true;
break;
case OBSTACLE:
case PossiblePlayerBattleAction::OBSTACLE:
consoleMsg = CGI->generaltexth->allTexts[550];
//TODO: remove obstacle cursor
isCastingPossible = true;
break;
case SACRIFICE:
case PossiblePlayerBattleAction::SACRIFICE:
consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s
cursorFrame = ECursor::COMBAT_SACRIFICE;
isCastingPossible = true;
break;
case FREE_LOCATION:
case PossiblePlayerBattleAction::FREE_LOCATION:
consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
isCastingPossible = true;
break;
case HEAL:
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 RISE_DEMONS:
case PossiblePlayerBattleAction::RISE_DEMONS:
cursorType = ECursor::SPELLBOOK;
realizeAction = [=]()
{
giveCommand(EActionType::DAEMON_SUMMONING, myNumber);
};
break;
case CATAPULT:
case PossiblePlayerBattleAction::CATAPULT:
cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT;
realizeAction = [=](){ giveCommand(EActionType::CATAPULT, myNumber); };
break;
case CREATURE_INFO:
case PossiblePlayerBattleAction::CREATURE_INFO:
{
cursorFrame = ECursor::COMBAT_QUERY;
consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str();
@ -2424,19 +2417,19 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
{
switch (illegalAction)
{
case AIMED_SPELL_CREATURE:
case RANDOM_GENIE_SPELL:
case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
cursorFrame = ECursor::COMBAT_BLOCKED;
consoleMsg = CGI->generaltexth->allTexts[23];
break;
case TELEPORT:
case PossiblePlayerBattleAction::TELEPORT:
cursorFrame = ECursor::COMBAT_BLOCKED;
consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
break;
case SACRIFICE:
case PossiblePlayerBattleAction::SACRIFICE:
consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice
break;
case FREE_LOCATION:
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;
@ -2453,8 +2446,8 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
{
switch (currentAction) //don't use that with teleport / sacrifice
{
case TELEPORT: //FIXME: more generic solution?
case SACRIFICE:
case PossiblePlayerBattleAction::TELEPORT: //FIXME: more generic solution?
case PossiblePlayerBattleAction::SACRIFICE:
break;
default:
cursorType = ECursor::SPELLBOOK;
@ -2474,11 +2467,11 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
{
case SpellID::TELEPORT: //don't cast spell yet, only select target
spellToCast->aimToUnit(shere);
possibleActions.push_back(TELEPORT);
possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT);
break;
case SpellID::SACRIFICE:
spellToCast->aimToHex(myNumber);
possibleActions.push_back(SACRIFICE);
possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE);
break;
}
}
@ -2526,7 +2519,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
if (eventType == LCLICK && realizeAction)
{
//opening creature window shouldn't affect myTurn...
if ((currentAction != CREATURE_INFO) && !secondaryTarget)
if ((currentAction != PossiblePlayerBattleAction::CREATURE_INFO) && !secondaryTarget)
{
myTurn = false; //tends to crash with empty calls
}

View File

@ -15,6 +15,7 @@
#include "CBattleAnimations.h"
#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
#include "../../lib/battle/CBattleInfoCallback.h"
class CLabel;
class CCreatureSet;
@ -104,20 +105,16 @@ struct CatapultProjectileInfo
double calculateY(double x);
};
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
{
enum PossibleActions // actions performed at l-click
{
INVALID = -1, CREATURE_INFO,
MOVE_TACTICS, CHOOSE_TACTICS_STACK,
MOVE_STACK, ATTACK, WALK_AND_ATTACK, ATTACK_AND_RETURN, SHOOT, //OPEN_GATE, //we can open castle gate during siege
NO_LOCATION, ANY_LOCATION, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL,
FREE_LOCATION, //used with Force Field and Fire Wall - all tiles affected by spell must be free
CATAPULT, HEAL, RISE_DEMONS,
AIMED_SPELL_CREATURE
};
private:
SDL_Surface *background, *menu, *amountNormal, *amountNegative, *amountPositive, *amountEffNeutral, *cellBorders, *backgroundWithHexes;
@ -169,12 +166,12 @@ private:
std::shared_ptr<BattleAction> spellToCast; //spell for which player is choosing destination
const CSpell *sp; //spell pointer for convenience
si32 creatureSpellToCast;
std::vector<PossibleActions> possibleActions; //all actions possible to call at the moment by player
std::vector<PossibleActions> localActions; //actions possible to take on hovered hex
std::vector<PossibleActions> illegalActions; //these actions display message in case of illegal target
PossibleActions currentAction; //action that will be performed on l-click
PossibleActions selectedAction; //last action chosen (and saved) by player
PossibleActions illegalAction; //most likely action that can't be performed here
std::vector<PossiblePlayerBattleAction> possibleActions; //all actions possible to call at the moment by player
std::vector<PossiblePlayerBattleAction> localActions; //actions possible to take on hovered hex
std::vector<PossiblePlayerBattleAction> 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
@ -183,8 +180,9 @@ private:
void requestAutofightingAIToTakeAction();
void getPossibleActionsForStack (const CStack *stack, const bool forceCast); //called when stack gets its turn
std::vector<PossiblePlayerBattleAction> 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();
@ -275,8 +273,6 @@ private:
void redrawBackgroundWithHexes(const CStack *activeStack);
/** End of battle screen blitting methods */
PossibleActions getCasterAction(const CSpell *spell, const spells::Caster *caster, spells::Mode mode) const;
void setHeroAnimation(ui8 side, int phase);
public:
static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims

View File

@ -270,6 +270,7 @@ public:
BONUS_NAME(BLOCK_MAGIC_BELOW) /*blocks casting spells of the level < value */ \
BONUS_NAME(DESTRUCTION) /*kills extra units after hit, subtype = 0 - kill percentage of units, 1 - kill amount, val = chance in percent to trigger, additional info - amount/percentage to kill*/ \
BONUS_NAME(SPECIAL_CRYSTAL_GENERATION) /*crystal dragon crystal generation*/ \
BONUS_NAME(NO_SPELLCAST_BY_DEFAULT) /*spellcast will not be default attack option for this creature*/ \
/* end of list */

View File

@ -74,6 +74,17 @@ BattleAction BattleAction::makeShotAttack(const battle::Unit * shooter, const ba
return ba;
}
BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, SpellID spellID)
{
BattleAction ba;
ba.actionType = EActionType::MONSTER_SPELL;
ba.actionSubtype = spellID;
ba.setTarget(target);
ba.side = stack->unitSide();
ba.stackNumber = stack->unitId();
return ba;
}
BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest)
{
BattleAction ba;

View File

@ -35,6 +35,7 @@ public:
static BattleAction makeWait(const battle::Unit * stack);
static BattleAction makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack = true);
static BattleAction makeShotAttack(const battle::Unit * shooter, const battle::Unit * target);
static BattleAction makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, SpellID spellID);
static BattleAction makeMove(const battle::Unit * stack, BattleHex dest);
static BattleAction makeEndOFTacticPhase(ui8 side);

View File

@ -203,6 +203,70 @@ si8 CBattleInfoCallback::battleCanTeleportTo(const battle::Unit * stack, BattleH
return true;
}
std::vector<PossiblePlayerBattleAction> CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data)
{
RETURN_IF_NOT_BATTLE(std::vector<PossiblePlayerBattleAction>());
std::vector<PossiblePlayerBattleAction> allowedActionList;
if(data.tacticsMode) //would "if(battleGetTacticDist() > 0)" work?
{
allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_TACTICS);
allowedActionList.push_back(PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK);
}
else
{
if(stack->canCast()) //TODO: check for battlefield effects that prevent casting?
{
if(stack->hasBonusOfType(Bonus::SPELLCASTER) && data.creatureSpellToCast != -1)
{
const CSpell *spell = SpellID(data.creatureSpellToCast).toSpell();
PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE);
allowedActionList.push_back(act);
}
if(stack->hasBonusOfType(Bonus::RANDOM_SPELLCASTER))
allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL);
if(stack->hasBonusOfType(Bonus::DAEMON_SUMMONING))
allowedActionList.push_back(PossiblePlayerBattleAction::RISE_DEMONS);
}
if(stack->canShoot())
allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT);
if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE))
allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN);
allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack
allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere
if(stack->canMove() && stack->Speed(0, true)) //probably no reason to try move war machines or bound stacks
allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK);
auto siegedTown = battleGetDefendedTown();
if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(Bonus::CATAPULT)) //TODO: check shots
allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT);
if(stack->hasBonusOfType(Bonus::HEALER))
allowedActionList.push_back(PossiblePlayerBattleAction::HEAL);
}
return allowedActionList;
}
PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const
{
RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID);
PossiblePlayerBattleAction spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION;
const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode);
if(ti.massive || ti.type == spells::AimType::NO_TARGET)
spellSelMode = PossiblePlayerBattleAction::NO_LOCATION;
else if(ti.type == spells::AimType::LOCATION && ti.clearAffected)
spellSelMode = PossiblePlayerBattleAction::FREE_LOCATION;
else if(ti.type == spells::AimType::CREATURE)
spellSelMode = PossiblePlayerBattleAction::AIMED_SPELL_CREATURE;
else if(ti.type == spells::AimType::OBSTACLE)
spellSelMode = PossiblePlayerBattleAction::OBSTACLE;
return spellSelMode;
}
std::set<BattleHex> CBattleInfoCallback::battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const
{
std::set<BattleHex> attackedHexes;

View File

@ -32,6 +32,23 @@ struct DLL_LINKAGE AttackableTiles
}
};
enum class PossiblePlayerBattleAction // actions performed at l-click
{
INVALID = -1, CREATURE_INFO,
MOVE_TACTICS, CHOOSE_TACTICS_STACK,
MOVE_STACK, ATTACK, WALK_AND_ATTACK, ATTACK_AND_RETURN, SHOOT, //OPEN_GATE, //we can open castle gate during siege
NO_LOCATION, ANY_LOCATION, OBSTACLE, TELEPORT, SACRIFICE, RANDOM_GENIE_SPELL,
FREE_LOCATION, //used with Force Field and Fire Wall - all tiles affected by spell must be free
CATAPULT, HEAL, RISE_DEMONS,
AIMED_SPELL_CREATURE
};
struct DLL_LINKAGE BattleClientInterfaceData
{
si32 creatureSpellToCast;
ui8 tacticsMode;
};
class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials
{
public:
@ -99,6 +116,8 @@ public:
SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon
si8 battleCanTeleportTo(const battle::Unit * stack, BattleHex destHex, int telportLevel) const; //checks if teleportation of given stack to given position can take place
std::vector<PossiblePlayerBattleAction> getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data);
PossiblePlayerBattleAction getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const;
//convenience methods using the ones above
bool isInTacticRange(BattleHex dest) const;