From 5550edeb9a070a1750802df12b9da8d73651a8c6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 28 Apr 2025 19:34:36 +0300 Subject: [PATCH] Fix inability of unit to cast spell after receiving morale --- AI/BattleAI/BattleEvaluator.cpp | 2 +- AI/BattleAI/StackWithBonuses.cpp | 4 ++-- AI/BattleAI/StackWithBonuses.h | 2 +- client/NetPacksClient.cpp | 2 +- lib/CMakeLists.txt | 1 + lib/battle/BattleInfo.cpp | 4 ++-- lib/battle/BattleInfo.h | 2 +- lib/battle/BattleUnitTurnReason.h | 28 +++++++++++++++++++++++++ lib/battle/CUnitState.cpp | 8 ++++--- lib/battle/CUnitState.h | 3 ++- lib/battle/IBattleState.h | 3 ++- lib/networkPacks/NetPacksLib.cpp | 2 +- lib/networkPacks/PacksForClientBattle.h | 9 ++++---- server/battles/BattleFlowProcessor.cpp | 13 ++++++------ server/battles/BattleFlowProcessor.h | 3 ++- test/mock/mock_battle_IBattleState.h | 2 +- 16 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 lib/battle/BattleUnitTurnReason.h diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 62acf2193..d5c0a97ec 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -568,7 +568,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ourTurnSpan++; } - state->nextTurn(unit->unitId()); + state->nextTurn(unit->unitId(), BattleUnitTurnReason::TURN_QUEUE); PotentialTargets potentialTargets(unit, damageCache, state); diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index a6ee42e21..624a6e0aa 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -342,14 +342,14 @@ void HypotheticBattle::nextRound() } } -void HypotheticBattle::nextTurn(uint32_t unitId) +void HypotheticBattle::nextTurn(uint32_t unitId, BattleUnitTurnReason reason) { activeUnitId = unitId; auto unit = getForUpdate(unitId); unit->removeUnitBonus(Bonus::UntilGetsTurn); - unit->afterGetsTurn(); + unit->afterGetsTurn(reason); } void HypotheticBattle::addUnit(uint32_t id, const JsonNode & data) diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 737486bad..2f04fcb9c 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -137,7 +137,7 @@ public: battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; void nextRound() override; - void nextTurn(uint32_t unitId) override; + void nextTurn(uint32_t unitId, BattleUnitTurnReason reason) override; void addUnit(uint32_t id, const JsonNode & data) override; void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 7c3f7ebf2..ec74a867e 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -769,7 +769,7 @@ void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) { - if(!pack.askPlayerInterface) + if(pack.reason == BattleUnitTurnReason::AUTOMATIC_ACTION) return; const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 0a7fbd2a1..31a0eef72 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -418,6 +418,7 @@ set(lib_MAIN_HEADERS battle/BattleSide.h battle/BattleStateInfoForRetreat.h battle/BattleProxy.h + battle/BattleUnitTurnReason.h battle/CBattleInfoCallback.h battle/CBattleInfoEssentials.h battle/CObstacleInstance.h diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 6319078ba..894c4a536 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -666,7 +666,7 @@ void BattleInfo::nextRound() obst->battleTurnPassed(); } -void BattleInfo::nextTurn(uint32_t unitId) +void BattleInfo::nextTurn(uint32_t unitId, BattleUnitTurnReason reason) { activeStack = unitId; @@ -675,7 +675,7 @@ void BattleInfo::nextTurn(uint32_t unitId) //remove bonuses that last until when stack gets new turn st->removeBonusesRecursive(Bonus::UntilGetsTurn); - st->afterGetsTurn(); + st->afterGetsTurn(reason); } void BattleInfo::addUnit(uint32_t id, const JsonNode & data) diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 20a91984d..a5eddc4c3 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -128,7 +128,7 @@ public: // IBattleState void nextRound() override; - void nextTurn(uint32_t unitId) override; + void nextTurn(uint32_t unitId, BattleUnitTurnReason reason) override; void addUnit(uint32_t id, const JsonNode & data) override; void moveUnit(uint32_t id, const BattleHex & destination) override; diff --git a/lib/battle/BattleUnitTurnReason.h b/lib/battle/BattleUnitTurnReason.h new file mode 100644 index 000000000..2f34f106e --- /dev/null +++ b/lib/battle/BattleUnitTurnReason.h @@ -0,0 +1,28 @@ +/* + * BattleUnitTurnReason.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 + +VCMI_LIB_NAMESPACE_BEGIN + +enum class BattleUnitTurnReason : int8_t +{ + /// Unit gained turn due to becoming first unit in turn queue + TURN_QUEUE, + /// Unit gained turn due to morale triggering + MORALE, + /// Unit (re)gained turn due to hero casting a spell while this unit is active + HERO_SPELLCAST, + /// Unit gained turn due to casting a spell while having ability to cast spells without spending turn + UNIT_SPELLCAST, + /// Unit gained turn for automatic action, player can not select action for this unit + AUTOMATIC_ACTION +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 069d60c65..7ca727153 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -920,11 +920,13 @@ void CUnitState::afterNewRound() makeGhost(); } -void CUnitState::afterGetsTurn() +void CUnitState::afterGetsTurn(BattleUnitTurnReason reason) { - //if moving second time this round it must be high morale bonus - if(movedThisRound) + if(reason == BattleUnitTurnReason::MORALE) + { hadMorale = true; + castSpellThisTurn = false; + } } void CUnitState::makeGhost() diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 53842b761..03665abd8 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -10,6 +10,7 @@ #pragma once +#include "BattleUnitTurnReason.h" #include "Unit.h" #include "../bonuses/BonusCache.h" @@ -254,7 +255,7 @@ public: void afterNewRound(); - void afterGetsTurn(); + void afterGetsTurn(BattleUnitTurnReason reason); void makeGhost(); diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index cae3442d6..b9c2d5adb 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -10,6 +10,7 @@ #pragma once #include "CBattleInfoEssentials.h" +#include "BattleUnitTurnReason.h" VCMI_LIB_NAMESPACE_BEGIN @@ -80,7 +81,7 @@ class DLL_LINKAGE IBattleState : public IBattleInfo { public: virtual void nextRound() = 0; - virtual void nextTurn(uint32_t unitId) = 0; + virtual void nextTurn(uint32_t unitId, BattleUnitTurnReason reason) = 0; virtual void addUnit(uint32_t id, const JsonNode & data) = 0; virtual void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) = 0; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index a71c1cfbd..33e6c19a8 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2007,7 +2007,7 @@ void BattleNextRound::applyGs(CGameState *gs) void BattleSetActiveStack::applyGs(CGameState *gs) { - gs->getBattle(battleID)->nextTurn(stack); + gs->getBattle(battleID)->nextTurn(stack, reason); } void BattleTriggerEffect::applyGs(CGameState *gs) diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index 96dbfae22..e0e2eb1a1 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -12,9 +12,10 @@ #include "NetPacksBase.h" #include "BattleChanges.h" #include "PacksForClient.h" -#include "../battle/BattleHexArray.h" #include "../battle/BattleAction.h" #include "../battle/BattleInfo.h" +#include "../battle/BattleHexArray.h" +#include "../battle/BattleUnitTurnReason.h" #include "../texts/MetaString.h" class CClient; @@ -63,8 +64,8 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient void applyGs(CGameState * gs) override; BattleID battleID = BattleID::NONE; - ui32 stack = 0; - ui8 askPlayerInterface = true; + uint32_t stack = 0; + BattleUnitTurnReason reason; void visitTyped(ICPackVisitor & visitor) override; @@ -72,7 +73,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient { h & battleID; h & stack; - h & askPlayerInterface; + h & reason; assert(battleID != BattleID::NONE); } }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index ff44a5caf..e8f241499 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -334,7 +334,7 @@ void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle) if (!tryMakeAutomaticAction(battle, next)) { if(next->alive()) { - setActiveStack(battle, next); + setActiveStack(battle, next, BattleUnitTurnReason::TURN_QUEUE); break; } } @@ -576,7 +576,7 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const // NOTE: in case of random spellcaster, (e.g. Master Genie) spell has been selected by server and was not present in action received from player if(actedStack->castSpellThisTurn && ba.spell.hasValue() && ba.spell.toSpell()->canCastWithoutSkip()) { - setActiveStack(battle, actedStack); + setActiveStack(battle, actedStack, BattleUnitTurnReason::UNIT_SPELLCAST); return; } } @@ -589,7 +589,7 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const if (rollGoodMorale(battle, actedStack)) { // Good morale - same stack makes 2nd turn - setActiveStack(battle, actedStack); + setActiveStack(battle, actedStack, BattleUnitTurnReason::MORALE); return; } } @@ -599,7 +599,7 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const { // this is action made by hero AND unit is alive (e.g. not killed by casted spell) // keep current active stack for next action - setActiveStack(battle, activeStack); + setActiveStack(battle, activeStack, BattleUnitTurnReason::HERO_SPELLCAST); return; } } @@ -622,7 +622,7 @@ bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle BattleSetActiveStack bsa; bsa.battleID = battle.getBattle()->getBattleID(); bsa.stack = stack->unitId(); - bsa.askPlayerInterface = false; + bsa.reason = BattleUnitTurnReason::AUTOMATIC_ACTION; gameHandler->sendAndApply(bsa); bool ret = owner->makeAutomaticBattleAction(battle, ba); @@ -809,12 +809,13 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c } } -void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack) +void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack, BattleUnitTurnReason reason) { assert(stack); BattleSetActiveStack sas; sas.battleID = battle.getBattle()->getBattleID(); sas.stack = stack->unitId(); + sas.reason = reason; gameHandler->sendAndApply(sas); } diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index 6c50af985..08441e9cd 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -10,6 +10,7 @@ #pragma once #include "../lib/battle/BattleSide.h" +#include "../lib/battle/BattleUnitTurnReason.h" VCMI_LIB_NAMESPACE_BEGIN class CStack; @@ -48,7 +49,7 @@ class BattleFlowProcessor : boost::noncopyable void stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * stack); void removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle); void stackTurnTrigger(const CBattleInfoCallback & battle, const CStack * stack); - void setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack); + void setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack, BattleUnitTurnReason reason); void makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next); bool makeAutomaticAction(const CBattleInfoCallback & battle, const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index fe99f2638..0de7b3c0b 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -42,7 +42,7 @@ public: MOCK_CONST_METHOD1(getUsedSpells, std::vector(BattleSide)); MOCK_METHOD0(nextRound, void()); - MOCK_METHOD1(nextTurn, void(uint32_t)); + MOCK_METHOD2(nextTurn, void(uint32_t, BattleUnitTurnReason)); MOCK_METHOD2(addUnit, void(uint32_t, const JsonNode &)); MOCK_METHOD3(setUnitState, void(uint32_t, const JsonNode &, int64_t)); MOCK_METHOD2(moveUnit, void(uint32_t, const BattleHex &));