diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 0f7068402..94df8714d 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -18,6 +18,7 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/CStack.h" // TODO: remove @@ -283,20 +284,11 @@ void CBattleAI::activeStack( const CStack * stack ) return; } - attemptCastingSpell(); + if (attemptCastingSpell()) + return; logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); - if(cb->battleIsFinished() || !stack->alive()) - { - //spellcast may finish battle or kill active stack - //send special preudo-action - BattleAction cancel; - cancel.actionType = EActionType::CANCEL; - cb->battleMakeUnitAction(cancel); - return; - } - if(auto action = considerFleeingOrSurrendering()) { cb->battleMakeUnitAction(*action); @@ -476,14 +468,14 @@ BattleAction CBattleAI::useCatapult(const CStack * stack) return attack; } -void CBattleAI::attemptCastingSpell() +bool CBattleAI::attemptCastingSpell() { auto hero = cb->battleGetMyHero(); if(!hero) - return; + return false; if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK) - return; + return false; LOGL("Casting spells sounds like fun. Let's see..."); //Get all spells we can cast @@ -522,7 +514,7 @@ void CBattleAI::attemptCastingSpell() } LOGFL("Found %d spell-target combinations.", possibleCasts.size()); if(possibleCasts.empty()) - return; + return false; using ValueMap = PossibleSpellcast::ValueMap; @@ -657,7 +649,7 @@ void CBattleAI::attemptCastingSpell() if(battleIsFinishedOpt) { print("No need to cast a spell. Battle will finish soon."); - return; + return false; } } } @@ -780,16 +772,18 @@ void CBattleAI::attemptCastingSpell() LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; spellcast.actionType = EActionType::HERO_SPELL; - spellcast.actionSubtype = castToPerform.spell->id; + spellcast.spell = castToPerform.spell->getId(); spellcast.setTarget(castToPerform.dest); spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; cb->battleMakeSpellAction(spellcast); movesSkippedByDefense = 0; + return true; } else { LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); + return false; } } diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 37338d299..38551c13b 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -68,7 +68,7 @@ public: ~CBattleAI(); void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void attemptCastingSpell(); + bool attemptCastingSpell(); void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 60c13382c..f3b4dd9c5 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -12,6 +12,7 @@ #include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" void CEmptyAI::saveGame(BinarySerializer & h, const int version) { @@ -73,3 +74,8 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, { cb->selectionMade(0, askID); } + +std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 2598f5dbe..a71806e62 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -32,6 +32,7 @@ public: void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; }; #define NAME "EmptyAI 0.1" diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 85a9fe571..1ccc37f78 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -13,6 +13,7 @@ #include "../../lib/CStack.h" #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/battle/BattleAction.h" static std::shared_ptr cbc; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index f20ceda1f..8fd3ac923 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -2890,4 +2890,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) return true; } - +std::optional VCAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 7d57ad681..e1fd11c0f 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -203,6 +203,7 @@ public: void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleEnd(const BattleResult * br, QueryID queryID) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; void makeTurn(); void mainLoop(); diff --git a/CCallback.cpp b/CCallback.cpp index 8331a8ec2..93f7fb670 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -206,7 +206,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) void CBattleCallback::battleMakeSpellAction(const BattleAction & action) { assert(action.actionType == EActionType::HERO_SPELL); - MakeCustomAction mca(action); + MakeAction mca(action); sendRequest(&mca); } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 69fde8c7b..3a407a796 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -128,7 +128,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): destinationTeleportPos = int3(-1); GH.defActionsDef = 0; LOCPLINT = this; - curAction = nullptr; playerID=Player; human=true; battleInt = nullptr; @@ -769,8 +768,7 @@ void CPlayerInterface::actionStarted(const BattleAction &action) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - curAction = new BattleAction(action); - battleInt->startAction(curAction); + battleInt->startAction(action); } void CPlayerInterface::actionFinished(const BattleAction &action) @@ -778,9 +776,7 @@ void CPlayerInterface::actionFinished(const BattleAction &action) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->endAction(curAction); - delete curAction; - curAction = nullptr; + battleInt->endAction(action); } void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack @@ -935,8 +931,6 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - assert(curAction); - StackAttackInfo info; info.attacker = cb->battleGetStackByID(ba->stackAttacking); info.defender = nullptr; @@ -2110,3 +2104,8 @@ void CPlayerInterface::showWorldViewEx(const std::vector& objectP EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->openWorldView(objectPositions, showTerrain ); } + +std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index d4372ec5f..c85fce3be 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -66,7 +66,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable int autosaveCount; std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) - const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation int3 destinationTeleportPos; @@ -173,6 +172,7 @@ protected: // Call-ins from server, should not be called directly, but only via void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleGateStateChanged(const EGateState state) override; void yourTacticPhase(int distance) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; public: // public interface for use by client via LOCPLINT access diff --git a/client/Client.h b/client/Client.h index 64a3f5f04..013db8e28 100644 --- a/client/Client.h +++ b/client/Client.h @@ -13,7 +13,6 @@ #include #include "../lib/IGameCallback.h" -#include "../lib/battle/BattleAction.h" #include "../lib/battle/CBattleInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,6 +24,7 @@ class CBattleGameInterface; class CGameInterface; class BinaryDeserializer; class BinarySerializer; +class BattleAction; template class CApplier; @@ -118,7 +118,7 @@ public: std::map>> additionalBattleInts; - std::optional curbaction; + std::unique_ptr currentBattleAction; CClient(); ~CClient(); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index c2f89316f..764c954d0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -784,7 +784,7 @@ void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) { - cl.curbaction = std::make_optional(pack.ba); + cl.currentBattleAction = std::make_unique(pack.ba); callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba); } @@ -830,8 +830,8 @@ void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.curbaction); - cl.curbaction.reset(); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.currentBattleAction); + cl.currentBattleAction.reset(); } void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 1ccdbf10f..7f45e126e 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -286,7 +286,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) { heroSpellToCast = std::make_shared(); heroSpellToCast->actionType = EActionType::HERO_SPELL; - heroSpellToCast->actionSubtype = spellID; //spell number + heroSpellToCast->spell = spellID; heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; @@ -314,7 +314,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) const CSpell * BattleActionsController::getHeroSpellToCast( ) const { if (heroSpellToCast) - return SpellID(heroSpellToCast->actionSubtype).toSpell(); + return heroSpellToCast->spell.toSpell(); return nullptr; } diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index df394b7d8..bdaba92af 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -94,13 +94,13 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt owner.waitForAnimations(); } -void BattleEffectsController::startAction(const BattleAction* action) +void BattleEffectsController::startAction(const BattleAction & action) { owner.checkForAnimations(); - const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = owner.curInt->cb->battleGetStackByID(action.stackNumber); - switch(action->actionType) + switch(action.actionType) { case EActionType::WAIT: owner.appendBattleLog(stack->formatGeneralMessage(136)); diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 574b85212..3ea1452b4 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -60,7 +60,7 @@ public: BattleEffectsController(BattleInterface & owner); - void startAction(const BattleAction* action); + void startAction(const BattleAction & action); //displays custom effect on the battlefield void displayEffect(EBattleEffect effect, const BattleHex & destTile); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 6420cdc77..c21af7bf2 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -234,7 +234,7 @@ void BattleInterface::newRound(int number) console->addText(CGI->generaltexth->allTexts[412]); } -void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) +void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) { const CStack * actor = nullptr; if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) @@ -253,7 +253,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 addit ba.side = side.value(); ba.actionType = action; ba.aimToHex(tile); - ba.actionSubtype = additional; + ba.spell = spell; sendCommand(ba, actor); } @@ -567,12 +567,12 @@ bool BattleInterface::makingTurn() const return stacksController->getActiveStack() != nullptr; } -void BattleInterface::endAction(const BattleAction* action) +void BattleInterface::endAction(const BattleAction &action) { // it is possible that tactics mode ended while opening music is still playing waitForAnimations(); - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast activateStack(); @@ -585,7 +585,7 @@ void BattleInterface::endAction(const BattleAction* action) tacticNextStack(stack); //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed - if(action->actionType == EActionType::HERO_SPELL) + if(action.actionType == EActionType::HERO_SPELL) fieldController->redrawBackgroundWithHexes(); } @@ -594,15 +594,15 @@ void BattleInterface::appendBattleLog(const std::string & newEntry) console->addText(newEntry); } -void BattleInterface::startAction(const BattleAction* action) +void BattleInterface::startAction(const BattleAction & action) { - if(action->actionType == EActionType::END_TACTIC_PHASE) + if(action.actionType == EActionType::END_TACTIC_PHASE) { windowObject->tacticPhaseEnded(); return; } - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); if (stack) { @@ -610,17 +610,17 @@ void BattleInterface::startAction(const BattleAction* action) } else { - assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number + assert(action.actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number } stacksController->startAction(action); - if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell + if(action.actionType == EActionType::HERO_SPELL) //when hero casts spell return; if (!stack) { - logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber); + logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action.stackNumber); return; } diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index fabd68984..4124c5f47 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -156,7 +156,7 @@ public: void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all void requestAutofightingAIToTakeAction(); - void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1); + void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE); void sendCommand(BattleAction command, const CStack * actor = nullptr); const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell @@ -188,7 +188,7 @@ public: void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); //call-ins - void startAction(const BattleAction* action); + void startAction(const BattleAction & action); void stackReset(const CStack * stack); void stackAdded(const CStack * stack); //new stack appeared on battlefield void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled @@ -211,7 +211,7 @@ public: void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - void endAction(const BattleAction* action); + void endAction(const BattleAction & action); void obstaclePlaced(const std::vector> oi); void obstacleRemoved(const std::vector & obstacles); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 23226b84b..94ea31b38 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -567,12 +567,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface int text = 304; switch(br.result) { - case BattleResult::NORMAL: + case EBattleResult::NORMAL: break; - case BattleResult::ESCAPE: + case EBattleResult::ESCAPE: text = 303; break; - case BattleResult::SURRENDER: + case EBattleResult::SURRENDER: text = 302; break; default: @@ -601,14 +601,14 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface std::string videoName = "LBSTART.BIK"; switch(br.result) { - case BattleResult::NORMAL: + case EBattleResult::NORMAL: break; - case BattleResult::ESCAPE: + case EBattleResult::ESCAPE: musicName = "Music/Retreat Battle"; videoName = "RTSTART.BIK"; text = 310; break; - case BattleResult::SURRENDER: + case EBattleResult::SURRENDER: musicName = "Music/Surrender Battle"; videoName = "SURRENDER.BIK"; text = 309; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 4abc8b6ed..0a2145057 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -31,6 +31,7 @@ #include "../../CCallback.h" #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" @@ -398,15 +399,7 @@ void BattleStacksController::addNewAnim(BattleAnimation *anim) void BattleStacksController::stackRemoved(uint32_t stackID) { if (getActiveStack() && getActiveStack()->unitId() == stackID) - { - BattleAction action; - action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - action.actionType = EActionType::CANCEL; - action.stackNumber = getActiveStack()->unitId(); - - LOCPLINT->cb->battleMakeUnitAction(action); setActiveStack(nullptr); - } } void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) @@ -663,7 +656,7 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex return false; } -void BattleStacksController::endAction(const BattleAction* action) +void BattleStacksController::endAction(const BattleAction & action) { owner.checkForAnimations(); @@ -688,7 +681,7 @@ void BattleStacksController::endAction(const BattleAction* action) removeExpiredColorFilters(); } -void BattleStacksController::startAction(const BattleAction* action) +void BattleStacksController::startAction(const BattleAction & action) { removeExpiredColorFilters(); } diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 0be41d287..30352a3db 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -115,8 +115,8 @@ public: void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest - void startAction(const BattleAction* action); - void endAction(const BattleAction* action); + void startAction(const BattleAction & action); + void endAction(const BattleAction & action); void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 3ee27c21a..089dc2e5f 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -9,7 +9,6 @@ */ #pragma once -#include "battle/BattleAction.h" #include "IGameEventsReceiver.h" #include "spells/ViewSpellInt.h" @@ -36,6 +35,7 @@ class CCreatureSet; class CArmedInstance; class IShipyard; class IMarket; +class BattleAction; struct BattleResult; struct BattleAttack; struct BattleStackAttacked; @@ -107,10 +107,7 @@ public: virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) - { - return std::nullopt; - } + virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0; virtual void saveGame(BinarySerializer & h, const int version) = 0; virtual void loadGame(BinaryDeserializer & h, const int version) = 0; diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 5fcd99513..733596428 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -248,7 +248,6 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType) static const std::map actionTypeToString = { {EActionType::END_TACTIC_PHASE, "End tactic phase"}, - {EActionType::INVALID, "Invalid"}, {EActionType::NO_ACTION, "No action"}, {EActionType::HERO_SPELL, "Hero spell"}, {EActionType::WALK, "Walk"}, diff --git a/lib/GameConstants.h b/lib/GameConstants.h index ab2aaed89..23214e1a1 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -998,20 +998,21 @@ namespace Date }; } -enum class EActionType : int32_t +enum class EActionType : int8_t { - CANCEL = -3, - END_TACTIC_PHASE = -2, - INVALID = -1, - NO_ACTION = 0, - HERO_SPELL, - WALK, - DEFEND, + NO_ACTION, + + END_TACTIC_PHASE, RETREAT, SURRENDER, + + HERO_SPELL, + + WALK, + WAIT, + DEFEND, WALK_AND_ATTACK, SHOOT, - WAIT, CATAPULT, MONSTER_SPELL, BAD_MORALE, @@ -1379,6 +1380,13 @@ enum class EHealPower : ui8 PERMANENT }; +enum class EBattleResult : int8_t +{ + NORMAL = 0, + ESCAPE = 1, + SURRENDER = 2 +}; + // Typedef declarations using TExpType = si64; using TQuantity = si32; diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index da1c6e2e6..571695f3f 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -134,7 +134,6 @@ public: virtual void visitBuildBoat(BuildBoat & pack) {} virtual void visitQueryReply(QueryReply & pack) {} virtual void visitMakeAction(MakeAction & pack) {} - virtual void visitMakeCustomAction(MakeCustomAction & pack) {} virtual void visitDigWithHero(DigWithHero & pack) {} virtual void visitCastAdvSpell(CastAdvSpell & pack) {} virtual void visitSaveGame(SaveGame & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 735737da5..90921b1c4 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1524,11 +1524,9 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient struct DLL_LINKAGE BattleResult : public Query { - enum EResult { NORMAL = 0, ESCAPE = 1, SURRENDER = 2 }; - void applyFirstCl(CClient * cl); - EResult result = NORMAL; + EBattleResult result = EBattleResult::NORMAL; ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] std::map casualties[2]; //first => casualties of attackers - map crid => number TExpType exp[2] = {0, 0}; //exp for attacker and defender @@ -2515,24 +2513,6 @@ struct DLL_LINKAGE MakeAction : public CPackForServer } }; -struct DLL_LINKAGE MakeCustomAction : public CPackForServer -{ - MakeCustomAction() = default; - MakeCustomAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - } -}; - struct DLL_LINKAGE DigWithHero : public CPackForServer { ObjectInstanceID id; //digging hero id diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5dc6e5d56..eb6aa28e4 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -638,11 +638,6 @@ void MakeAction::visitTyped(ICPackVisitor & visitor) visitor.visitMakeAction(*this); } -void MakeCustomAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMakeCustomAction(*this); -} - void DigWithHero::visitTyped(ICPackVisitor & visitor) { visitor.visitDigWithHero(*this); @@ -2289,34 +2284,35 @@ void StartAction::applyGs(CGameState *gs) return; } - if(ba.actionType != EActionType::HERO_SPELL) //don't check for stack if it's custom action by hero + if (ba.isUnitAction()) { - assert(st); + assert(st); // stack must exists for all non-hero actions + + switch(ba.actionType) + { + case EActionType::DEFEND: + st->waiting = false; + st->defending = true; + st->defendingAnim = true; + break; + case EActionType::WAIT: + st->defendingAnim = false; + st->waiting = true; + st->waitedThisTurn = true; + break; + case EActionType::HERO_SPELL: //no change in current stack state + break; + default: //any active stack action - attack, catapult, heal, spell... + st->waiting = false; + st->defendingAnim = false; + st->movedThisRound = true; + break; + } } else { - gs->curB->sides[ba.side].usedSpellsHistory.emplace_back(ba.actionSubtype); - } - - switch(ba.actionType) - { - case EActionType::DEFEND: - st->waiting = false; - st->defending = true; - st->defendingAnim = true; - break; - case EActionType::WAIT: - st->defendingAnim = false; - st->waiting = true; - st->waitedThisTurn = true; - break; - case EActionType::HERO_SPELL: //no change in current stack state - break; - default: //any active stack action - attack, catapult, heal, spell... - st->waiting = false; - st->defendingAnim = false; - st->movedThisRound = true; - break; + if(ba.actionType == EActionType::HERO_SPELL) + gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell); } } diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index c0d1f8d9c..5a92ce08e 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -20,8 +20,7 @@ static const int32_t INVALID_UNIT_ID = -1000; BattleAction::BattleAction(): side(-1), stackNumber(-1), - actionType(EActionType::INVALID), - actionSubtype(-1) + actionType(EActionType::NO_ACTION) { } @@ -80,7 +79,7 @@ BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, con { BattleAction ba; ba.actionType = EActionType::MONSTER_SPELL; - ba.actionSubtype = spellID; + ba.spell = spellID; ba.setTarget(target); ba.side = stack->unitSide(); ba.stackNumber = stack->unitId(); @@ -144,7 +143,7 @@ std::string BattleAction::toString() const } boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); - fmt % static_cast(side) % stackNumber % actionTypeStream.str() % actionSubtype % targetStream.str(); + fmt % static_cast(side) % stackNumber % actionTypeStream.str() % spell.getNum() % targetStream.str(); return fmt.str(); } @@ -183,7 +182,7 @@ battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const void BattleAction::setTarget(const battle::Target & target_) { - target.clear(); + target.clear(); for(const auto & destination : target_) { if(destination.unitValue == nullptr) @@ -193,6 +192,44 @@ void BattleAction::setTarget(const battle::Target & target_) } } +bool BattleAction::isUnitAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::WAIT, + EActionType::DEFEND, + EActionType::WALK_AND_ATTACK, + EActionType::SHOOT, + EActionType::CATAPULT, + EActionType::MONSTER_SPELL, + EActionType::BAD_MORALE, + EActionType::STACK_HEAL + }; + + return vstd::contains(actions, actionType); +} + +bool BattleAction::isSpellAction() const +{ + static const std::array actions = { + EActionType::HERO_SPELL, + EActionType::MONSTER_SPELL + }; + + return vstd::contains(actions, actionType); +} + +bool BattleAction::isTacticsAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::END_TACTIC_PHASE, + EActionType::RETREAT, + EActionType::SURRENDER + }; + + return vstd::contains(actions, actionType); +} std::ostream & operator<<(std::ostream & os, const BattleAction & ba) { diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index db65ac37e..12eba04e4 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -28,7 +28,7 @@ public: ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, EActionType actionType; //use ActionType enum for values - si32 actionSubtype; + SpellID spell; BattleAction(); @@ -43,6 +43,9 @@ public: static BattleAction makeRetreat(ui8 side); static BattleAction makeSurrender(ui8 side); + bool isTacticsAction() const; + bool isUnitAction() const; + bool isSpellAction() const; std::string toString() const; void aimToHex(const BattleHex & destination); @@ -56,7 +59,7 @@ public: h & side; h & stackNumber; h & actionType; - h & actionSubtype; + h & spell; h & target; } private: diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 08db1d259..9f5fe19e9 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -352,7 +352,6 @@ void registerTypesServerPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 68a907cd9..4e47b8a3c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -10,59 +10,58 @@ #include "StdInc.h" #include "CGameHandler.h" -#include "HeroPoolProcessor.h" +#include "CVCMIServer.h" #include "ServerNetPackVisitors.h" #include "ServerSpellCastEnvironment.h" -#include "CVCMIServer.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" -#include "PlayerMessageProcessor.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileInfo.h" -#include "../lib/int3.h" #include "../lib/ArtifactUtils.h" -#include "../lib/StartInfo.h" #include "../lib/CArtHandler.h" #include "../lib/CBuildingHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/pathfinder/CPathfinder.h" -#include "../lib/pathfinder/PathfinderOptions.h" -#include "../lib/pathfinder/TurnInfo.h" -#include "../lib/spells/AbilityCaster.h" -#include "../lib/spells/BonusCaster.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/spells/ObstacleCasterProxy.h" -#include "../lib/spells/Problem.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CTownHandler.h" #include "../lib/CCreatureHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CStack.h" +#include "../lib/CCreatureSet.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CSoundBase.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CTownHandler.h" +#include "../lib/GameConstants.h" #include "../lib/UnlockGuard.h" #include "../lib/GameSettings.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/CondSh.h" +#include "../lib/ScriptHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/TerrainHandler.h" +#include "../lib/VCMIDirs.h" #include "../lib/VCMI_Lib.h" +#include "../lib/int3.h" + +#include "../lib/filesystem/FileInfo.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/gameState/CGameState.h" + #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" #include "../lib/modding/ModIncompatibility.h" -#include "../lib/rmg/CMapGenOptions.h" -#include "../lib/VCMIDirs.h" -#include "../lib/ScopeGuard.h" -#include "../lib/CSoundBase.h" -#include "../lib/TerrainHandler.h" -#include "../lib/CCreatureSet.h" -#include "../lib/CThreadHelper.h" -#include "../lib/GameConstants.h" +#include "../lib/pathfinder/CPathfinder.h" +#include "../lib/pathfinder/PathfinderOptions.h" +#include "../lib/pathfinder/TurnInfo.h" + #include "../lib/registerTypes/RegisterTypes.h" + +#include "../lib/rmg/CMapGenOptions.h" + #include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/Connection.h" #include "../lib/serializer/Cast.h" +#include "../lib/serializer/Connection.h" #include "../lib/serializer/JsonSerializer.h" -#include "../lib/ScriptHandler.h" + +#include "../lib/spells/CSpellHandler.h" + #include "vstd/CLoggerBase.h" -#include #include #include #include @@ -76,8 +75,6 @@ #define COMPLAIN_RET(txt) {complain(txt); return false;} #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} -CondSh battleMadeAction(false); -CondSh battleResult(nullptr); template class CApplyOnGH; class CBaseForGHApply @@ -129,95 +126,6 @@ static inline double distance(int3 a, int3 b) { return std::sqrt((double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); } -static void giveExp(BattleResult &r) -{ - if (r.winner > 1) - { - // draw - return; - } - r.exp[0] = 0; - r.exp[1] = 0; - for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) - { - r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; - } -} - -static void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard -{ - int x = targetPosition.getX(); - int y = targetPosition.getY(); - - const bool targetIsAttacker = side == BattleSide::ATTACKER; - - if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - - //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's - if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) - { - if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } - else - { //add back-side guardians for two-hex target, side guardians for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); - - if (!targetIsTwoHex && x > 2) //back guard for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); - else if (targetIsTwoHex)//front-side guardians for two-hex target - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - if (x > 3) //back guard for two-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - } - } - - } - - else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) - { - if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - else - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); - - if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else if (targetIsTwoHex) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - if (x < GameConstants::BFIELD_WIDTH - 4) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - } - } - } - - else if (!targetIsAttacker && y % 2 == 0) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - - else if (targetIsAttacker && y % 2 == 1) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } -} PlayerStatus PlayerStatuses::operator[](PlayerColor player) { @@ -349,7 +257,7 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) { auto levelUpQuery = std::make_shared(this, hlu, hero); hlu.queryID = levelUpQuery->queryID; - queries.addQuery(levelUpQuery); + queries->addQuery(levelUpQuery); sendAndApply(&hlu); //level up will be called on query reply } @@ -490,7 +398,7 @@ void CGameHandler::levelUpCommander(const CCommanderInstance * c) { auto commanderLevelUp = std::make_shared(this, clu, hero); clu.queryID = commanderLevelUp->queryID; - queries.addQuery(commanderLevelUp); + queries->addQuery(commanderLevelUp); sendAndApply(&clu); } } @@ -574,644 +482,6 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh giveSpells(hero->visitedTown, hero); } -void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) -{ - LOG_TRACE(logGlobal); - - //Fill BattleResult structure with exp info - giveExp(*battleResult.data); - - if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped - { - if(heroAttacker) - battleResult.data->exp[1] += 500; - if(heroDefender) - battleResult.data->exp[0] += 500; - } - - if(heroAttacker) - battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill - if(heroDefender) - battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - if (!battleQuery) - { - logGlobal->error("Cannot find battle query!"); - complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " has no battle query at the top!"); - return; - } - - battleQuery->result = std::make_optional(*battleResult.data); - - //Check how many battle queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; - finishingBattle = std::make_unique(battleQuery, queriedPlayers); - - // in battles against neutrals, 1st player can ask to replay battle manually - if (!gs->curB->sides[1].color.isValidPlayer()) - { - auto battleDialogQuery = std::make_shared(this, gs->curB); - battleResult.data->queryID = battleDialogQuery->queryID; - queries.addQuery(battleDialogQuery); - } - else - battleResult.data->queryID = -1; - - //set same battle result for all queries - for(auto q : queries.allQueries()) - { - auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery) - otherBattleQuery->result = battleQuery->result; - } - - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - - if (battleResult.data->queryID == -1) - endBattleConfirm(gs->curB); -} - -void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) -{ - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(battleInfo->sides.at(0).color)); - if(!battleQuery) - { - logGlobal->trace("No battle query, battle end was confirmed by another player"); - return; - } - - const BattleResult::EResult result = battleResult.get()->result; - - CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle - ChangeSpells cs; //for Eagle Eye - - if(!finishingBattle->isDraw() && finishingBattle->winnerHero) - { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) - { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) - { - auto spell = spellId.toSpell(VLC->spells()); - if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && getRandomGenerator().nextInt(99) < eagleEyeChance) - cs.spells.insert(spell->getId()); - } - } - } - std::vector arts; //display them in window - - if(result == BattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) - { - auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) - { - const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); - if(ArtifactUtils::isSlotBackpack(slot)) - ma->askAssemble = false; - sendAndApply(ma); - } - }; - - if (finishingBattle->loserHero) - { - //TODO: wrap it into a function, somehow (std::variant -_-) - auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig() && - art->artType->getId() != ArtifactID::SPELLBOOK) - // don't move war machines or locked arts (spellbook) - { - sendMoveArtifact(art, &ma); - } - } - for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - { - //we assume that no big artifacts can be found - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); - if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won - { - sendMoveArtifact(art, &ma); - } - } - if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? - { - artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - - if (arts.size()) //display loot - { - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - - iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact - - for (auto art : arts) //TODO; separate function to display loot for various ojects? - { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); - if (iw.components.size() >= 14) - { - sendAndApply(&iw); - iw.components.clear(); - } - } - if (iw.components.size()) - { - sendAndApply(&iw); - } - } - //Eagle Eye secondary skill handling - if (!cs.spells.empty()) - { - cs.learn = 1; - cs.hid = finishingBattle->winnerHero->id; - - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); - - std::ostringstream names; - for (int i = 0; i < cs.spells.size(); i++) - { - names << "%s"; - if (i < cs.spells.size() - 2) - names << ", "; - else if (i < cs.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.replaceRawString(names.str()); - - auto it = cs.spells.begin(); - for (int i = 0; i < cs.spells.size(); i++, it++) - { - iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); - if (i == cs.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); - } - sendAndApply(&iw); - sendAndApply(&cs); - } - cab1.updateArmy(this); - cab2.updateArmy(this); //take casualties after battle is deleted - - if(finishingBattle->loserHero) //remove beaten hero - { - RemoveObject ro(finishingBattle->loserHero->id); - sendAndApply(&ro); - } - if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - } - - if(battleResult.data->winner == BattleSide::DEFENDER - && finishingBattle->winnerHero - && finishingBattle->winnerHero->visitedTown - && !finishingBattle->winnerHero->inTownGarrison - && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) - { - swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place - } - //give exp - if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - - BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); - raccepted.heroResult[0].exp = battleResult.data->exp[0]; - raccepted.heroResult[1].exp = battleResult.data->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; - sendAndApply(&raccepted); - - queries.popIfTop(battleQuery); - //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query -} - -void CGameHandler::battleAfterLevelUp(const BattleResult &result) -{ - LOG_TRACE(logGlobal); - - if(!finishingBattle) - return; - - finishingBattle->remainingBattleQueriesCount--; - logGlobal->trace("Decremented queries count to %d", finishingBattle->remainingBattleQueriesCount); - - if (finishingBattle->remainingBattleQueriesCount > 0) - //Battle results will be handled when all battle queries are closed - return; - - //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible - // but the battle consequences are applied after final player is unblocked. Hard to abuse... - // Still, it looks like a hole. - - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); - // Give raised units to winner and show dialog, if any were raised, - // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); - - if (necroSlot != SlotID()) - { - finishingBattle->winnerHero->showNecromancyDialog(raisedStack, getRandomGenerator()); - addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); - } - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - sendAndApply(&resultsApplied); - - setBattle(nullptr); - - if (visitObjectAfterVictory && result.winner==0 && !finishingBattle->winnerHero->stacks.empty()) - { - logGlobal->trace("post-victory visit"); - visitObjectOnTile(*getTile(finishingBattle->winnerHero->visitablePos()), finishingBattle->winnerHero); - } - visitObjectAfterVictory = false; - - //handle victory/loss of engaged players - std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; - checkVictoryLossConditions(playerColors); - - if (result.result == BattleResult::SURRENDER) - heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); - - if (result.result == BattleResult::ESCAPE) - heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); - - if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() - && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - - if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) - heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); - } - - finishingBattle.reset(); -} - -void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) -{ - if(first && !counter) - handleAttackBeforeCasting(ranged, attacker, defender); - - FireShieldInfo fireShield; - BattleAttack bat; - BattleLogMessage blm; - bat.stackAttacking = attacker->unitId(); - bat.tile = targetHex; - - std::shared_ptr attackerState = attacker->acquireState(); - - if(ranged) - bat.flags |= BattleAttack::SHOT; - if(counter) - bat.flags |= BattleAttack::COUNTER; - - const int attackerLuck = attacker->luckVal(); - - if(attackerLuck > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::LUCKY; - } - - if(attackerLuck < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::UNLUCKY; - } - - if (getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) - { - bat.flags |= BattleAttack::DEATH_BLOW; - } - - const auto * owner = gs->curB->getHero(attacker->unitOwner()); - if(owner) - { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); - if (chance > getRandomGenerator().nextInt(99)) - bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; - } - - int64_t drainedLife = 0; - - // only primary target - if(defender->alive()) - drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); - - //multiple-hex normal attack - std::set attackedCreatures = gs->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - - std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? - { - //this is need for displaying hit animation - bat.flags |= BattleAttack::SPELL_LIKE; - bat.spellID = SpellID(bonus->subtype); - - //TODO: should spell override creature`s projectile? - - auto spell = bat.spellID.toSpell(); - - battle::Target target; - target.emplace_back(defender, targetHex); - - spells::BattleCast event(gs->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); - event.setSpellLevel(bonus->val); - - auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); - - //TODO: get exact attacked hex for defender - - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - { - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - } - - //now add effect info for all attacked stacks - for (BattleStackAttacked & bsa : bat.bsa) - { - if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield - { - //this is need for displaying affect animation - bsa.flags |= BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID(bonus->subtype); - } - } - } - - attackerState->afterAttack(ranged, counter); - - { - UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); - attackerState->save(info.data); - bat.attackerChanges.changedStacks.push_back(info); - } - - if (drainedLife > 0) - bat.flags |= BattleAttack::LIFE_DRAIN; - - sendAndApply(&bat); - - { - const bool multipleTargets = bat.bsa.size() > 1; - - int64_t totalDamage = 0; - int32_t totalKills = 0; - - for(const BattleStackAttacked & bsa : bat.bsa) - { - totalDamage += bsa.damageAmount; - totalKills += bsa.killedAmount; - } - - { - MetaString text; - attacker->addText(text, EMetaText::GENERAL_TXT, 376); - attacker->addNameReplacement(text); - text.replaceNumber(totalDamage); - blm.lines.push_back(text); - } - - addGenericKilledLog(blm, defender, totalKills, multipleTargets); - } - - // drain life effect (as well as log entry) must be applied after the attack - if(drainedLife > 0) - { - MetaString text; - attackerState->addText(text, EMetaText::GENERAL_TXT, 361); - attackerState->addNameReplacement(text, false); - text.replaceNumber(drainedLife); - defender->addNameReplacement(text, true); - blm.lines.push_back(std::move(text)); - } - - if(!fireShield.empty()) - { - //todo: this should be "virtual" spell instead, we only need fire spell school bonus here - const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); - int64_t totalDamage = 0; - - for(const auto & item : fireShield) - { - const CStack * actor = item.first; - int64_t rawDamage = item.second; - - const CGHeroInstance * actorOwner = gs->curB->getHero(actor->unitOwner()); - - if(actorOwner) - { - rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); - } - else - { - rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); - } - - totalDamage+=rawDamage; - //FIXME: add custom effect on actor - } - - if (totalDamage > 0) - { - BattleStackAttacked bsa; - - bsa.flags |= BattleStackAttacked::FIRE_SHIELD; - bsa.stackAttacked = attacker->unitId(); //invert - bsa.attackerID = defender->unitId(); - bsa.damageAmount = totalDamage; - attacker->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured pack; - pack.stacks.push_back(bsa); - sendAndApply(&pack); - - // TODO: this is already implemented in Damage::describeEffect() - { - MetaString text; - text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); - text.replaceNumber(totalDamage); - blm.lines.push_back(std::move(text)); - } - addGenericKilledLog(blm, attacker, bsa.killedAmount, false); - } - } - - sendAndApply(&blm); - - handleAfterAttackCasting(ranged, attacker, defender); -} - -int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) -{ - BattleStackAttacked bsa; - if(secondary) - bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities - - bsa.attackerID = attackerState->unitId(); - bsa.stackAttacked = def->unitId(); - { - BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); - - bai.deathBlow = bat.deathBlow(); - bai.doubleDamage = bat.ballistaDoubleDmg(); - bai.luckyStrike = bat.lucky(); - bai.unluckyStrike = bat.unlucky(); - - auto range = gs->curB->calculateDmgRange(bai); - bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator()); - CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties - } - - int64_t drainedLife = 0; - - //life drain handling - if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) - { - int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; - attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - drainedLife += toHeal; - } - - //soul steal handling - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) - { - //we can have two bonuses - one with subtype 0 and another with subtype 1 - //try to use permanent first, use only one of two - for(si32 subtype = 1; subtype >= 0; subtype--) - { - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) - { - int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); - drainedLife += toHeal; - break; - } - } - } - bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated - - //fire shield handling - if(!bat.shot() && - !def->isClone() && - def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && - CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) - ) - { - //TODO: use damage with bonus but without penalties - auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; - fireShield.push_back(std::make_pair(def, fireShieldDamage)); - } - - return drainedLife; -} - -void CGameHandler::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - BattleLogMessage blm; - addGenericKilledLog(blm, defender, killed, multiple); - sendAndApply(&blm); - } -} - -void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - const int32_t txtIndex = (killed > 1) ? 379 : 378; - std::string formatString = VLC->generaltexth->allTexts[txtIndex]; - - // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); - boost::algorithm::trim(formatString); - - boost::format txt(formatString); - if(killed > 1) - { - txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish - } - else //killed == 1 - { - txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes - } - MetaString line; - line.appendRawString(txt.str()); - blm.lines.push_back(std::move(line)); - } -} - void CGameHandler::handleClientDisconnection(std::shared_ptr c) { if(lobby->state == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) @@ -1271,300 +541,11 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) vstd::clear_pointer(pack); } -int CGameHandler::moveStack(int stack, BattleHex dest) -{ - int ret = 0; - - const CStack *curStack = gs->curB->battleGetStackByID(stack), - *stackAtEnd = gs->curB->battleGetStackByPos(dest); - - assert(curStack); - assert(dest < GameConstants::BFIELD_SIZE); - - if (gs->curB->tacticDistance) - { - assert(gs->curB->isInTacticRange(dest)); - } - - auto start = curStack->getPosition(); - if (start == dest) - return 0; - - //initing necessary tables - auto accessibility = getAccesibility(curStack); - std::set passed; - //Ignore obstacles on starting position - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - - //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) - if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) - { - BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); - - if(accessibility.accessible(shifted, curStack)) - dest = shifted; - } - - if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) - { - complain("Given destination is not accessible!"); - return 0; - } - - bool canUseGate = false; - auto dbState = gs->curB->si.gateState; - if(battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && - dbState != EGateState::DESTROYED && - dbState != EGateState::BLOCKED) - { - canUseGate = true; - } - - std::pair< std::vector, int > path = gs->curB->getPath(start, dest, curStack); - - ret = path.second; - - int creSpeed = curStack->speed(0, true); - - if (gs->curB->tacticDistance > 0 && creSpeed > 0) - creSpeed = GameConstants::BFIELD_SIZE; - - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_OUTER) - return true; - if (hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (isGateDrawbridgeHex(hex)) - return true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) - return true; - } - - return false; - }; - - if (curStack->hasBonusOfType(BonusType::FLYING)) - { - if (path.second <= creSpeed && path.first.size() > 0) - { - if (canUseGate && dbState != EGateState::OPENED && - occupyGateDrawbridgeHex(dest)) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - - //inform clients about move - BattleStackMoved sm; - sm.stack = curStack->unitId(); - std::vector tiles; - tiles.push_back(path.first[0]); - sm.tilesToMove = tiles; - sm.distance = path.second; - sm.teleporting = false; - sendAndApply(&sm); - } - } - else //for non-flying creatures - { - std::vector tiles; - const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); - int v = (int)path.first.size()-1; - path.first.push_back(start); - - // check if gate need to be open or closed at some point - BattleHex openGateAtHex, gateMayCloseAtHex; - if (canUseGate) - { - for (int i = (int)path.first.size()-1; i >= 0; i--) - { - auto needOpenGates = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) - return true; - else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto hex = path.first[i]; - if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) - { - if (needOpenGates(hex)) - openGateAtHex = path.first[i+1]; - - //TODO we need find batter way to handle double-wide stacks - //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && needOpenGates(otherHex)) - openGateAtHex = path.first[i+2]; - } - - //gate may be opened and then closed during stack movement, but not other way around - if (openGateAtHex.isValid()) - dbState = EGateState::OPENED; - } - - if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) - { - if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - if (hasWideMoat) - { - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && - path.first[i-1] != ESiegeHex::GATE_INNER && - path.first[i-1] != ESiegeHex::GATE_BRIDGE) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - } - } - - bool stackIsMoving = true; - - while(stackIsMoving) - { - if (verror("Movement terminated abnormally"); - break; - } - - bool gateStateChanging = false; - //special handling for opening gate on from starting hex - if (openGateAtHex.isValid() && openGateAtHex == start) - gateStateChanging = true; - else - { - for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) - { - BattleHex hex = path.first[v]; - tiles.push_back(hex); - - if ((openGateAtHex.isValid() && openGateAtHex == hex) || - (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) - { - gateStateChanging = true; - } - - //if we walked onto something, finalize this portion of stack movement check into obstacle - if(!battleGetAllObstaclesOnPos(hex, false).empty()) - obstacleHit = true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - //two hex creature hit obstacle by backside - auto obstacle2 = battleGetAllObstaclesOnPos(otherHex, false); - if(otherHex.isValid() && !obstacle2.empty()) - obstacleHit = true; - } - if(!obstacleHit) - passed.insert(hex); - } - } - - if (!tiles.empty()) - { - //commit movement - BattleStackMoved sm; - sm.stack = curStack->unitId(); - sm.distance = path.second; - sm.teleporting = false; - sm.tilesToMove = tiles; - sendAndApply(&sm); - tiles.clear(); - } - - //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end - if (curStack->getPosition() != dest) - { - if(stackIsMoving && start != curStack->getPosition()) - { - stackIsMoving = handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - } - if (gateStateChanging) - { - if (curStack->getPosition() == openGateAtHex) - { - openGateAtHex = BattleHex(); - //only open gate if stack is still alive - if (curStack->alive()) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - } - else if (curStack->getPosition() == gateMayCloseAtHex) - { - gateMayCloseAtHex = BattleHex(); - updateGateState(); - } - } - } - else - //movement finished normally: we reached destination - stackIsMoving = false; - } - } - //handle last hex separately for deviation - if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) - { - if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) - || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) - passed.clear(); //Just empty passed, obstacles will handled automatically - } - //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - - return ret; -} - CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) + , battles(std::make_unique(this)) + , queries(std::make_unique()) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") @@ -1574,19 +555,12 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) IObjectInterface::cb = this; applier = std::make_shared>(); registerTypesServerPacks(*applier); - visitObjectAfterVictory = false; spellEnv = new ServerSpellCastEnvironment(this); } CGameHandler::~CGameHandler() { - if (battleThread) - { - //Setting battleMadeAction is needed because battleThread waits for the action to continue the main loop - battleMadeAction.setn(true); - battleThread->join(); - } delete spellEnv; delete gs; } @@ -2112,45 +1086,6 @@ std::list CGameHandler::generatePlayerTurnOrder() const return playerTurnOrder; } -void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) -{ - battleResult.set(nullptr); - - const auto & t = *getTile(tile); - TerrainId terrain = t.terType->getId(); - if (gs->map->isCoastalTile(tile)) //coastal tile is always ground - terrain = ETerrainId::SAND; - - BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); - if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); - - //send info about battles - BattleStart bs; - bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); - - engageIntoBattle(bs.info->sides[0].color); - engageIntoBattle(bs.info->sides[1].color); - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(bs.info->sides[0].color)); - bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); - - sendAndApply(&bs); -} - -void CGameHandler::checkBattleStateChanges() -{ - //check if drawbridge state need to be changes - if (battleGetSiegeLevel() > 0) - updateGateState(); - - //check if battle ended - if (auto result = battleIsFinished()) - { - setBattleResult(BattleResult::NORMAL, *result); - } -} - void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) { if (!h->hasSpellbook()) @@ -2286,7 +1221,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo LOG_TRACE_PARAMS(logGlobal, "Hero %s starts movement from %s to %s", h->getNameTranslated() % tmh.start.toString() % tmh.end.toString()); auto moveQuery = std::make_shared(this, tmh, h); - queries.addQuery(moveQuery); + queries->addQuery(moveQuery); if (leavingTile == LEAVING_TILE) leaveTile(); @@ -2313,7 +1248,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - queries.popIfTop(moveQuery); + queries->popIfTop(moveQuery); logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; }; @@ -2475,7 +1410,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne void CGameHandler::showBlockingDialog(BlockingDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -2483,7 +1418,7 @@ void CGameHandler::showBlockingDialog(BlockingDialog *iw) void CGameHandler::showTeleportDialog(TeleportDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -2587,69 +1522,6 @@ void CGameHandler::removeArtifact(const ArtifactLocation &al) ea.al = al; sendAndApply(&ea); } -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) //use hero=nullptr for no hero -{ - if(gs->curB) - gs->curB.dellNull(); - - static const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - static const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; - - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - - //existing battle query for retying auto-combat - if(lastBattleQuery) - { - for(int i : {0, 1}) - { - if(heroes[i]) - { - SetMana restoreInitialMana; - restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; - restoreInitialMana.hid = heroes[i]->id; - sendAndApply(&restoreInitialMana); - } - } - - lastBattleQuery->bi = gs->curB; - lastBattleQuery->result = std::nullopt; - lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject; - lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject; - } - - auto nextBattleQuery = std::make_shared(this, gs->curB); - for(int i : {0, 1}) - { - if(heroes[i]) - { - nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; - } - } - queries.addQuery(nextBattleQuery); - - this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) -{ - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) { @@ -2815,7 +1687,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) sendAndApply(&hex); useScholarSkill(hero1,hero2); - queries.addQuery(exchange); + queries->addQuery(exchange); } } @@ -4316,13 +3188,13 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); logGlobal->trace(answer.toJson()); - auto topQuery = queries.topQuery(player); + auto topQuery = queries->topQuery(player); COMPLAIN_RET_FALSE_IF(!topQuery, "This player doesn't have any queries!"); if(topQuery->queryID != qid) { - auto currentQuery = queries.getQuery(qid); + auto currentQuery = queries->getQuery(qid); if(currentQuery != nullptr && currentQuery->endsByPlayerAnswer()) currentQuery->setReply(answer); @@ -4332,712 +3204,10 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor COMPLAIN_RET_FALSE_IF(!topQuery->endsByPlayerAnswer(), "This query cannot be ended by player's answer!"); topQuery->setReply(answer); - queries.popQuery(topQuery); + queries->popQuery(topQuery); return true; } -static EndAction end_action; - -void CGameHandler::updateGateState() -{ - // GATE_BRIDGE - leftmost tile, located over moat - // GATE_OUTER - central tile, mostly covered by gate image - // GATE_INNER - rightmost tile, inside the walls - - // GATE_OUTER or GATE_INNER: - // - if defender moves unit on these tiles, bridge will open - // - if there is a creature (dead or alive) on these tiles, bridge will always remain open - // - blocked to attacker if bridge is closed - - // GATE_BRIDGE - // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) - // - if Force Field is cast here, bridge can't open (but can close, in any town) - // - deals moat damage to attacker if bridge is closed (fortress only) - - bool hasForceFieldOnBridge = !battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); - bool hasStackAtGateInner = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; - bool hasStackAtGateOuter = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; - bool hasStackAtGateBridge = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - BattleUpdateGateState db; - db.state = gs->curB->si.gateState; - if (gs->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) - { - db.state = EGateState::DESTROYED; - } - else if (db.state == EGateState::OPENED) - { - bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; - bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; - - if (gateCanClose) - db.state = EGateState::CLOSED; - else - db.state = EGateState::OPENED; - } - else // CLOSED or BLOCKED - { - bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; - - if (gateBlocked) - db.state = EGateState::BLOCKED; - else - db.state = EGateState::CLOSED; - } - - if (db.state != gs->curB->si.gateState) - sendAndApply(&db); -} - -bool CGameHandler::makeBattleAction(BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - bool ok = true; - - battle::Target target = ba.getTarget(gs->curB); - - const CStack * stack = battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack - - const bool isAboutActiveStack = stack && (ba.stackNumber == gs->curB->getActiveStackID()); - - logGlobal->trace("Making action: %s", ba.toString()); - - switch(ba.actionType) - { - case EActionType::WALK: //walk - case EActionType::DEFEND: //defend - case EActionType::WAIT: //wait - case EActionType::WALK_AND_ATTACK: //walk or attack - case EActionType::SHOOT: //shoot - case EActionType::CATAPULT: //catapult - case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::MONSTER_SPELL: - - if (!stack) - { - complain("No such stack!"); - return false; - } - if (!stack->alive()) - { - complain("This stack is dead: " + stack->nodeName()); - return false; - } - - if (battleTacticDist()) - { - if (stack && stack->unitSide() != battleGetTacticsSide()) - { - complain("This is not a stack of side that has tactics!"); - return false; - } - } - else if (!isAboutActiveStack) - { - complain("Action has to be about active stack!"); - return false; - } - } - - auto wrapAction = [this](BattleAction &ba) - { - StartAction startAction(ba); - sendAndApply(&startAction); - - return vstd::makeScopeGuard([&]() - { - sendAndApply(&end_action); - }); - }; - - switch(ba.actionType) - { - case EActionType::END_TACTIC_PHASE: //wait - case EActionType::BAD_MORALE: - case EActionType::NO_ACTION: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::WALK: - { - auto wrapper = wrapAction(ba); - if(target.size() < 1) - { - complain("Destination required for move action."); - ok = false; - break; - } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move - if (!walkedTiles) - complain("Stack failed movement!"); - break; - } - case EActionType::DEFEND: - { - //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) - SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); - int oldDefenceValue = defence.totalValue(); - - defence.push_back(std::make_shared(defenseBonusToAdd)); - defence.push_back(std::make_shared(bonus2)); - - int difference = defence.totalValue() - oldDefenceValue; - std::vector buffer; - if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) - { - difference = 1; - buffer.push_back(alternativeWeakCreatureBonus); - } - else - { - buffer.push_back(defenseBonusToAdd); - } - - buffer.push_back(bonus2); - - sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - sendAndApply(&sse); - - BattleLogMessage message; - - MetaString text; - stack->addText(text, EMetaText::GENERAL_TXT, 120); - stack->addNameReplacement(text); - text.replaceNumber(difference); - - message.lines.push_back(text); - - sendAndApply(&message); - //don't break - we share code with next case - } - [[fallthrough]]; - case EActionType::WAIT: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::RETREAT: //retreat/flee - { - if (!gs->curB->battleCanFlee(gs->curB->sides.at(ba.side).color)) - complain("Cannot retreat!"); - else - setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case EActionType::SURRENDER: - { - PlayerColor player = gs->curB->sides.at(ba.side).color; - int cost = gs->curB->battleGetSurrenderCost(player); - if (cost < 0) - complain("Cannot surrender!"); - else if (getResource(player, EGameResID::GOLD) < cost) - complain("Not enough gold to surrender!"); - else - { - giveResource(player, EGameResID::GOLD, -cost); - setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case EActionType::WALK_AND_ATTACK: //walk or attack - { - auto wrapper = wrapAction(ba); - - if(!stack) - { - complain("No attacker"); - ok = false; - break; - } - - if(target.size() < 2) - { - complain("Two destinations required for attack action."); - ok = false; - break; - } - - BattleHex attackPos = target.at(0).hexValue; - BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gs->curB->battleGetStackByPos(destinationTile, true); - - if(!destinationStack) - { - complain("Invalid target to attack"); - ok = false; - break; - } - - BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); - - logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - - if(stack->getPosition() != attackPos - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) - ) - { - // we were not able to reach destination tile, nor occupy specified hex - // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine - break; - } - - if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - complain("Unit can not attack itself"); - ok = false; - break; - } - - if(!CStack::isMeleeAttackPossible(stack, destinationStack)) - { - complain("Attack cannot be performed!"); - ok = false; - break; - } - - //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); - const bool retaliation = destinationStack->ableToRetaliate(); - for (int i = 0; i < totalAttacks; ++i) - { - //first strike - if(i == 0 && firstStrike && retaliation) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - - //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification - if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) - { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - } - - //counterattack - //we check retaliation twice, so if it unblocked during attack it will work only on next attack - if(stack->alive() - && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) - && (i == 0 && !firstStrike) - && retaliation && destinationStack->ableToRetaliate()) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - } - - //return - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) - && target.size() == 3 - && startingPos != stack->getPosition() - && startingPos == target.at(2).hexValue - && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) - } - break; - } - case EActionType::SHOOT: - { - if(target.size() < 1) - { - complain("Destination required for shot action."); - ok = false; - break; - } - - auto destination = target.at(0).hexValue; - - const CStack * destinationStack = gs->curB->battleGetStackByPos(destination); - - if (!gs->curB->battleCanShoot(stack, destination)) - { - complain("Cannot shoot!"); - break; - } - if (!destinationStack) - { - complain("No target to shoot!"); - break; - } - - auto wrapper = wrapAction(ba); - - makeAttack(stack, destinationStack, 0, destination, true, true, false); - - //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) - && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) - && destinationStack->ableToRetaliate() - && gs->curB->battleCanShoot(destinationStack, stack->getPosition()) - && stack->alive()) //attacker may have died (fire shield) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); - } - //allow more than one additional attack - - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - for(int i = 1; i < totalRangedAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) - { - makeAttack(stack, destinationStack, 0, destination, false, true, false); - } - } - break; - } - case EActionType::CATAPULT: - { - auto wrapper = wrapAction(ba); - const CStack * shooter = gs->curB->battleGetStackByID(ba.stackNumber); - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) - { - complain("We do not know how to shoot :P"); - } - else - { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult - auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); - parameters.setSpellLevel(shotLevel); - parameters.cast(spellEnv, target); - } - //finish by scope guard - break; - } - case EActionType::STACK_HEAL: //healing with First Aid Tent - { - auto wrapper = wrapAction(ba); - const CStack * healer = gs->curB->battleGetStackByID(ba.stackNumber); - - if(target.size() < 1) - { - complain("Destination required for heal action."); - ok = false; - break; - } - - const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); - - if(target.at(0).unitValue) - destStack = target.at(0).unitValue; - else - destStack = gs->curB->battleGetUnitByPos(target.at(0).hexValue); - - if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) - { - complain("There is either no healer, no destination, or healer cannot heal :P"); - } - else - { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent - auto dest = battle::Destination(destStack, target.at(0).hexValue); - parameters.setSpellLevel(0); - parameters.cast(spellEnv, {dest}); - } - break; - } - case EActionType::MONSTER_SPELL: - { - auto wrapper = wrapAction(ba); - - const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.actionSubtype); - - std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if (randSpellcaster && battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID < 0) - complain("That stack can't cast spells!"); - else - { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gs->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(spellEnv, target); - } - break; - } - } - if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND - || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) - handleObstacleTriggersForUnit(*spellEnv, *stack); - if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished - battleMadeAction.setn(true); - return ok; -} - -bool CGameHandler::makeCustomAction(BattleAction & ba) -{ - boost::unique_lock lock(battleActionMutex); - - switch(ba.actionType) - { - case EActionType::HERO_SPELL: - { - COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); - - const CGHeroInstance *h = gs->curB->battleGetFightingHero(ba.side); - COMPLAIN_RET_FALSE_IF((!h), "Wrong caster!"); - - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); - if (!s) - { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); - return false; - } - - spells::BattleCast parameters(gs->curB, h, spells::Mode::HERO, s); - - spells::detail::ProblemImpl problem; - - auto m = s->battleMechanics(¶meters); - - if(!m->canBeCast(problem))//todo: should we check aimed cast? - { - logGlobal->warn("Spell cannot be cast!"); - std::vector texts; - problem.getAll(texts); - for(auto s : texts) - logGlobal->warn(s); - return false; - } - - StartAction start_action(ba); - sendAndApply(&start_action); //start spell casting - - parameters.cast(spellEnv, ba.getTarget(gs->curB)); - - sendAndApply(&end_action); - if (!gs->curB->battleGetStackByID(gs->curB->activeStack)) - { - battleMadeAction.setn(true); - } - checkBattleStateChanges(); - if (battleResult.get()) - { - battleMadeAction.setn(true); - //battle will be ended by startBattle function - //endBattle(gs->curB->tile, gs->curB->heroes[0], gs->curB->heroes[1]); - } - - return true; - - } - } - return false; -} - -void CGameHandler::stackEnchantedTrigger(const CStack * st) -{ - auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); - for(auto b : bl) - { - const CSpell * sp = SpellID(b->subtype).toSpell(); - if(!sp) - continue; - - const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); - const int32_t level = ((val > 3) ? (val - 3) : val); - - spells::BattleCast battleCast(gs->curB, st, spells::Mode::PASSIVE, sp); - //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle - battleCast.setEffectDuration(50); - battleCast.setSpellLevel(level); - spells::Target target; - - if(val > 3) - { - for(auto s : gs->curB->battleGetAllStacks()) - if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied - target.emplace_back(s); - } - else - { - target.emplace_back(st); - } - battleCast.applyEffects(spellEnv, target, false, true); - } -} - -void CGameHandler::stackTurnTrigger(const CStack *st) -{ - BattleTriggerEffect bte; - bte.stackID = st->unitId(); - bte.effect = -1; - bte.val = 0; - bte.additionalInfo = 0; - if (st->alive()) - { - //unbind - if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) - { - bool unbind = true; - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); - auto adjacent = gs->curB->battleAdjacentUnits(st); - - for (auto b : bl) - { - if(b->additionalInfo != CAddInfo::NONE) - { - const CStack * stack = gs->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent - if(stack) - { - if(vstd::contains(adjacent, stack)) //binding stack is still present - unbind = false; - } - } - else - { - unbind = false; - } - } - if (unbind) - { - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::UNBIND; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - - if (st->hasBonusOfType(BonusType::POISON)) - { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) //TODO: what if not?... - { - bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); - if (bte.val < b->val) //(negative) poison effect increases - update it - { - bte.effect = vstd::to_underlying(BonusType::POISON); - sendAndApply(&bte); - } - } - } - if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) - { - const PlayerColor opponent = gs->curB->otherPlayer(gs->curB->battleGetOwner(st)); - const CGHeroInstance * opponentHero = gs->curB->getHero(opponent); - if(opponentHero) - { - ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); - vstd::amin(manaDrained, opponentHero->mana); - if(manaDrained) - { - bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); - bte.val = manaDrained; - bte.additionalInfo = opponentHero->id.getNum(); //for sanity - sendAndApply(&bte); - } - } - } - if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) - { - bool fearsomeCreature = false; - for (CStack * stack : gs->curB->stacks) - { - if (battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) - { - fearsomeCreature = true; - break; - } - } - if (fearsomeCreature) - { - if (getRandomGenerator().nextInt(99) < 10) //fixed 10% - { - bte.effect = vstd::to_underlying(BonusType::FEAR); - sendAndApply(&bte); - } - } - } - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = gs->curB->whatSide(st->unitOwner()); - if(st->canCast() && gs->curB->battleGetEnchanterCounter(side) == 0) - { - bool cast = false; - while(!bl.empty() && !cast) - { - auto bonus = *RandomGeneratorUtil::nextItem(bl, getRandomGenerator()); - auto spellID = SpellID(bonus->subtype); - const CSpell * spell = SpellID(spellID).toSpell(); - bl.remove_if([&bonus](const Bonus * b) - { - return b == bonus.get(); - }); - spells::BattleCast parameters(gs->curB, st, spells::Mode::ENCHANTER, spell); - parameters.setSpellLevel(bonus->val); - parameters.massive = true; - parameters.smart = true; - //todo: recheck effect level - if(parameters.castIfPossible(spellEnv, spells::Target(1, spells::Destination()))) - { - cast = true; - - int cooldown = bonus->additionalInfo[0]; - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; - ssp.absolute = false; - ssp.val = cooldown; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - } - } -} - void CGameHandler::handleTimeEvents() { gs->map->events.sort(evntCmp); @@ -5197,7 +3367,7 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h assert(upperArmy); auto garrisonQuery = std::make_shared(this, upperArmy, lowerArmy); - queries.addQuery(garrisonQuery); + queries->addQuery(garrisonQuery); GarrisonDialog gd; gd.hid = hid; @@ -5251,10 +3421,10 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) } //Ongoing garrison exchange - usually picking from top garison (from o1 to o2), but who knows - auto dialog = std::dynamic_pointer_cast(queries.topQuery(o1->tempOwner)); + auto dialog = std::dynamic_pointer_cast(queries->topQuery(o1->tempOwner)); if (!dialog) { - dialog = std::dynamic_pointer_cast(queries.topQuery(o2->tempOwner)); + dialog = std::dynamic_pointer_cast(queries->topQuery(o2->tempOwner)); } if (dialog) { @@ -5295,7 +3465,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta } } visitQuery = std::make_shared(this, visitedObject, h, visitedObject->visitablePos()); - queries.addQuery(visitQuery); //TODO real visit pos + queries->addQuery(visitQuery); //TODO real visit pos HeroVisit hv; hv.objId = obj->id; @@ -5310,7 +3480,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta ObjectVisitStarted::defaultExecute(serverEventBus.get(), startVisit, h->tempOwner, h->id, obj->id); if(visitQuery) - queries.popIfTop(visitQuery); //visit ends here if no queries were created + queries->popIfTop(visitQuery); //visit ends here if no queries were created } void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) @@ -5365,16 +3535,6 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) return true; } -void CGameHandler::engageIntoBattle(PlayerColor player) -{ - //notify interfaces - PlayerBlocked pb; - pb.player = player; - pb.reason = PlayerBlocked::UPCOMING_BATTLE; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - sendAndApply(&pb); -} - void CGameHandler::checkVictoryLossConditions(const std::set & playerColors) { for (auto playerColor : playerColors) @@ -5537,234 +3697,6 @@ bool CGameHandler::dig(const CGHeroInstance *h) return true; } -void CGameHandler::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) -{ - if(attacker->hasBonusOfType(attackMode)) - { - std::set spellsToCast; - TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - spellsToCast.insert(SpellID(sf->subtype)); - } - for(SpellID spellID : spellsToCast) - { - bool castMe = false; - if(!defender->alive()) - { - logGlobal->debug("attackCasting: all attacked creatures have been killed"); - return; - } - int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); - for(const auto & sf : *spellsByType) - { - int meleeRanged; - if(sf->additionalInfo.size() < 2) - { - // legacy format - vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); - meleeRanged = sf->additionalInfo[0] / 1000; - } - else - { - vstd::amax(spellLevel, sf->additionalInfo[0]); - meleeRanged = sf->additionalInfo[1]; - } - if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) - castMe = true; - } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); - vstd::amin(chance, 100); - - const CSpell * spell = SpellID(spellID).toSpell(); - spells::AbilityCaster caster(attacker, spellLevel); - - spells::Target target; - target.emplace_back(defender); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - - auto m = spell->battleMechanics(¶meters); - - spells::detail::ProblemImpl ignored; - - if(!m->canBeCastAt(target, ignored)) - continue; - - //check if spell should be cast (probability handling) - if(getRandomGenerator().nextInt(99) >= chance) - continue; - - //casting - if(castMe) - { - parameters.cast(spellEnv, target); - } - } - } -} - -void CGameHandler::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? -} - -void CGameHandler::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - if(!attacker->alive() || !defender->alive()) // can be already dead - return; - - attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); - - if(!defender->alive()) - { - //don't try death stare or acid breath on dead stack (crash!) - return; - } - - if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - - int staredCreatures = distribution(getRandomGenerator().getStdGenerator()); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); - if(staredCreatures) - { - //TODO: death stare was not originally available for multiple-hex attacks, but... - const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - parameters.setEffectValue(staredCreatures); - parameters.cast(spellEnv, target); - } - } - - if(!defender->alive()) - return; - - int64_t acidDamage = 0; - TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); - for(const auto & b : *acidBreath) - { - if(b->additionalInfo[0] > getRandomGenerator().nextInt(99)) - acidDamage += b->val; - } - - if(acidDamage > 0) - { - const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - - parameters.setEffectValue(acidDamage * attacker->getCount()); - parameters.cast(spellEnv, target); - } - - - if(!defender->alive()) - return; - - if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability - { - double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; - vstd::amin(chanceToTrigger, 1); //cap at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; - - if(defender->unitType()->getId() == bonusAdditionalInfo || - (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) - return; - - battle::UnitInfo resurrectInfo; - resurrectInfo.id = gs->curB->battleNextUnitId(); - resurrectInfo.summoned = false; - resurrectInfo.position = defender->getPosition(); - resurrectInfo.side = defender->unitSide(); - - if(bonusAdditionalInfo != CAddInfo::NONE) - resurrectInfo.type = CreatureID(bonusAdditionalInfo); - else - resurrectInfo.type = attacker->creatureId(); - - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) - resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) - resurrectInfo.count = defender->getCount(); - else - return; //wrong subtype - - BattleUnitsChanged addUnits; - addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); - resurrectInfo.save(addUnits.changedStacks.back().data); - - BattleUnitsChanged removeUnits; - removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); - sendAndApply(&removeUnits); - sendAndApply(&addUnits); - } - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) - { - double chanceToTrigger = 0; - int amountToDie = 0; - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; - amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); - } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; - } - - vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - BattleStackAttacked bsa; - bsa.attackerID = -1; - bsa.stackAttacked = defender->unitId(); - bsa.damageAmount = amountToDie * defender->getMaxHealth(); - bsa.flags = BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID::SLAYER; - defender->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured si; - si.stacks.push_back(bsa); - - sendAndApply(&si); - sendGenericKilledLog(defender, bsa.killedAmount, false); - } -} - void CGameHandler::visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h) { if (!t.visitableObjects.empty()) @@ -5863,16 +3795,6 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h return true; } -void CGameHandler::makeStackDoNothing(const CStack * next) -{ - BattleAction doNothing; - doNothing.actionType = EActionType::NO_ACTION; - doNothing.side = next->unitSide(); - doNothing.stackNumber = next->unitId(); - - makeAutomaticAction(next, doNothing); -} - bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) { if (sl.army->hasStackAtSlot(sl.slot)) @@ -6051,430 +3973,6 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s } } -void CGameHandler::runBattle() -{ - boost::unique_lock lock(battleActionMutex); - - setBattle(gs->curB); - assert(gs->curB); - //TODO: pre-tactic stuff, call scripts etc. - - //Moat should be initialized here, because only here we can use spellcasting - if (gs->curB->town && gs->curB->town->fortLevel() >= CGTownInstance::CITADEL) - { - const auto * h = gs->curB->battleGetFightingHero(BattleSide::DEFENDER); - const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(gs->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(gs->curB, &moatCaster, spells::Mode::PASSIVE, gs->curB->town->town->moatAbility.toSpell()); - auto target = spells::Target(); - cast.cast(spellEnv, target); - } - - //tactic round - { - while ((lobby->state != EServerState::SHUTDOWN) && gs->curB->tacticDistance && !battleResult.get()) - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - } - - //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = gs->curB->stacks; //use temporary variable to outclude summoned stacks added to gs->curB->stacks from processing - - for (CStack * stack : initialStacks) - { - if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) - { - std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); - std::vector targetHexes; - const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard - const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); - - /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. - For one-hex targets there are four guardians - front, back and one per side (up + down). - Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front - Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ - if (!guardianIsBig) - targetHexes = stack->getSurroundingHexes(); - else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); - - for(auto hex : targetHexes) - { - if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex - { - battle::UnitInfo info; - info.id = gs->curB->battleNextUnitId(); - info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); - info.type = creatureData; - info.side = stack->unitSide(); - info.position = hex; - info.summoned = true; - - BattleUnitsChanged pack; - pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(pack.changedStacks.back().data); - sendAndApply(&pack); - } - } - } - - stackEnchantedTrigger(stack); - } - - //spells opening battle - for (int i = 0; i < 2; ++i) - { - auto h = gs->curB->battleGetFightingHero(i); - if (h) - { - TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); - - for (auto b : *bl) - { - spells::BonusCaster caster(h, b); - - const CSpell * spell = SpellID(b->subtype).toSpell(); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - parameters.setSpellLevel(3); - parameters.setEffectDuration(b->val); - parameters.massive = true; - parameters.castIfPossible(spellEnv, spells::Target()); - } - } - } - // it is possible that due to opening spells one side was eliminated -> check for end of battle - checkBattleStateChanges(); - - bool firstRound = true;//FIXME: why first round is -1? - - //main loop - while ((lobby->state != EServerState::SHUTDOWN) && !battleResult.get()) //till the end of the battle ;] - { - BattleNextRound bnr; - bnr.round = gs->curB->round + 1; - logGlobal->debug("Round %d", bnr.round); - sendAndApply(&bnr); - - auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it - for (auto &obstPtr : obstacles) - { - if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) - if (sco->turnsRemaining == 0) - removeObstacle(*obstPtr); - } - - const BattleInfo & curB = *gs->curB; - - for(auto stack : curB.stacks) - { - if(stack->alive() && !firstRound) - stackEnchantedTrigger(stack); - } - - //stack loop - - auto getNextStack = [this]() -> const CStack * - { - if(battleResult.get()) - return nullptr; - - std::vector q; - gs->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" - - if(!q.empty()) - { - if(!q.front().empty()) - { - auto next = q.front().front(); - const auto stack = dynamic_cast(next); - - // regeneration takes place before everything else but only during first turn attempt in each round - // also works under blind and similar effects - if(stack && stack->alive() && !stack->waiting) - { - BattleTriggerEffect bte; - bte.stackID = stack->unitId(); - bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); - - const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); - if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) - bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); - - if(bte.val) // anything to heal - sendAndApply(&bte); - } - - if(next->willMove()) - return stack; - } - } - - return nullptr; - }; - - const CStack * next = nullptr; - while((lobby->state != EServerState::SHUTDOWN) && (next = getNextStack())) - { - BattleUnitsChanged removeGhosts; - for(auto stack : curB.stacks) - { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } - - if(!removeGhosts.changedStacks.empty()) - sendAndApply(&removeGhosts); - - // check for bad morale => freeze - int nextStackMorale = next->moraleVal(); - if(!next->hadMorale && !next->waited() && nextStackMorale < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - //unit loses its turn - empty freeze action - BattleAction ba; - ba.actionType = EActionType::BAD_MORALE; - ba.side = next->unitSide(); - ba.stackNumber = next->unitId(); - - makeAutomaticAction(next, ba); - continue; - } - } - - if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk - { - logGlobal->trace("Handle Berserk effect"); - std::pair attackInfo = curB.getNearestStack(next); - if (attackInfo.first != nullptr) - { - BattleAction attack; - attack.actionType = EActionType::WALK_AND_ATTACK; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - attack.aimToHex(attackInfo.second); - attack.aimToUnit(attackInfo.first); - - makeAutomaticAction(next, attack); - logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); - } - else - { - makeStackDoNothing(next); - logGlobal->trace("No target found"); - } - continue; - } - - const CGHeroInstance * curOwner = battleGetOwnerHero(next); - const int stackCreatureId = next->unitType()->getId(); - - if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) - { - BattleAction attack; - attack.actionType = EActionType::SHOOT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - //TODO: select target by priority - - const battle::Unit * target = nullptr; - - for(auto & elem : gs->curB->stacks) - { - if(elem->unitType()->getId() != CreatureID::CATAPULT - && elem->unitOwner() != next->unitOwner() - && elem->isValidTarget() - && gs->curB->battleCanShoot(next, elem->getPosition())) - { - target = elem; - break; - } - } - - if(target == nullptr) - { - makeStackDoNothing(next); - } - else - { - attack.aimToUnit(target); - makeAutomaticAction(next, attack); - } - continue; - } - - if (next->unitType()->getId() == CreatureID::CATAPULT) - { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); - - if (attackableBattleHexes.empty()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) - { - BattleAction attack; - attack.actionType = EActionType::CATAPULT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - makeAutomaticAction(next, attack); - continue; - } - } - - if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) - { - TStacks possibleStacks = battleGetStacksIf([=](const CStack * s) - { - return s->unitOwner() == next->unitOwner() && s->canBeHealed(); - }); - - if (!possibleStacks.size()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) - { - RandomGeneratorUtil::randomShuffle(possibleStacks, getRandomGenerator()); - const CStack * toBeHealed = possibleStacks.front(); - - BattleAction heal; - heal.actionType = EActionType::STACK_HEAL; - heal.aimToUnit(toBeHealed); - heal.side = next->unitSide(); - heal.stackNumber = next->unitId(); - - makeAutomaticAction(next, heal); - continue; - } - } - - int numberOfAsks = 1; - bool breakOuter = false; - do - {//ask interface and wait for answer - if (!battleResult.get()) - { - stackTurnTrigger(next); //various effects - - if(next->fear) - { - makeStackDoNothing(next); //end immediately if stack was affected by fear - } - else - { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - sendAndApply(&sas); - - auto actionWasMade = [&]() -> bool - { - if (battleMadeAction.data)//active stack has made its action - return true; - if (battleResult.get())// battle is finished - return true; - if (next == nullptr)//active stack was been removed - return true; - return !next->alive();//active stack is dead - }; - - boost::unique_lock lock(battleMadeAction.mx); - battleMadeAction.data = false; - while ((lobby->state != EServerState::SHUTDOWN) && !actionWasMade()) - { - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - battleMadeAction.cond.wait(lock); - } - if (battleGetStackByID(nextId, false) != next) - next = nullptr; //it may be removed, while we wait - } - } - } - - if (battleResult.get()) //don't touch it, battle could be finished while waiting got action - { - breakOuter = true; - break; - } - //we're after action, all results applied - checkBattleStateChanges(); //check if this action ended the battle - - if(next != nullptr) - { - //check for good morale - nextStackMorale = next->moraleVal(); - if( !battleResult.get() - && !next->hadMorale - && !next->defending - && !next->waited() - && !next->fear - && next->alive() - && nextStackMorale > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - BattleTriggerEffect bte; - bte.stackID = next->unitId(); - bte.effect = vstd::to_underlying(BonusType::MORALE); - bte.val = 1; - bte.additionalInfo = 0; - sendAndApply(&bte); //play animation - - ++numberOfAsks; //move this stack once more - } - } - } - --numberOfAsks; - } while (numberOfAsks > 0); - - if (breakOuter) - { - break; - } - - } - firstRound = false; - } - - if (lobby->state != EServerState::SHUTDOWN) - endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1)); -} - -bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - BattleSetActiveStack bsa; - bsa.stack = stack->unitId(); - bsa.askPlayerInterface = false; - sendAndApply(&bsa); - - bool ret = makeBattleAction(ba); - checkBattleStateChanges(); - return ret; -} - bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) { assert(a->artType); @@ -6541,22 +4039,6 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact return false; } -void CGameHandler::setBattleResult(BattleResult::EResult resultType, int victoriusSide) -{ - boost::unique_lock guard(battleResult.mx); - if (battleResult.data) - { - complain((boost::format("The battle result has been already set (to %d, asked to %d)") - % battleResult.data->result % resultType).str()); - return; - } - auto br = new BattleResult(); - br->result = resultType; - br->winner = victoriusSide; //surrendering side loses - gs->curB->calculateCasualties(br->casualties); - battleResult.data = br; -} - void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { std::vector::iterator tile; @@ -6584,13 +4066,6 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) } } -void CGameHandler::removeObstacle(const CObstacleInstance & obstacle) -{ - BattleObstaclesChanged obsRem; - obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); - sendAndApply(&obsRem); -} - void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; @@ -6611,7 +4086,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) if (!strcmp(typeid(*pack).name(), typeid(PlayerMessage).name())) return false; - auto query = queries.topQuery(player); + auto query = queries->topQuery(player); if (query && query->blocksPack(pack)) { complain(boost::str(boost::format( @@ -6628,7 +4103,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) void CGameHandler::removeAfterVisit(const CGObjectInstance *object) { //If the object is being visited, there must be a matching query - for (const auto &query : queries.allQueries()) + for (const auto &query : queries->allQueries()) { if (auto someVistQuery = std::dynamic_pointer_cast(query)) { @@ -6677,7 +4152,7 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { - if (auto topQuery = queries.topQuery(hero->getOwner())) + if (auto topQuery = queries->topQuery(hero->getOwner())) if (auto visit = std::dynamic_pointer_cast(topQuery)) return !(visit->visitedObject == obj && visit->visitingHero == hero); @@ -6706,167 +4181,6 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): - army(battleSide.armyObject) -{ - heroWithDeadCommander = ObjectInstanceID(); - - PlayerColor color = battleSide.color; - - for(CStack * st : bat->stacks) - { - if(st->summoned) //don't take into account temporary summoned stacks - continue; - if(st->unitOwner() != color) //remove only our stacks - continue; - - logGlobal->debug("Calculating casualties for %s", st->nodeName()); - - st->health.takeResurrected(); - - if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) - { - logGlobal->debug("Ignored arrow towers stack."); - } - else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) - { - auto warMachine = st->unitType()->warMachine; - - if(warMachine == ArtifactID::NONE) - { - logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); - } - //catapult artifact remain even if "creature" killed in siege - else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) - { - logGlobal->debug("War machine has been destroyed"); - auto hero = dynamic_ptr_cast (army); - if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); - else - logGlobal->error("War machine in army without hero"); - } - } - else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) - { - if(st->alive() && st->getCount() > 0) - { - logGlobal->debug("Permanently summoned %d units.", st->getCount()); - const CreatureID summonedType = st->creatureId(); - summoned[summonedType] += st->getCount(); - } - } - else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - if (nullptr == st->base) - { - logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); - } - else - { - auto c = dynamic_cast (st->base); - if(c) - { - auto h = dynamic_cast (army); - if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) - { - logGlobal->debug("Commander is dead."); - heroWithDeadCommander = army->id; //TODO: unify commander handling - } - } - else - logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); - } - } - else if(st->base && !army->slotEmpty(st->unitSlot())) - { - logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); - if(st->getCount() == 0 || !st->alive()) - { - logGlobal->debug("Stack has been destroyed."); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); - } - else if(st->getCount() < army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - else if(st->getCount() > army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - } - else - { - logGlobal->warn("Unable to process stack: %s", st->nodeName()); - } - } -} - -void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) -{ - for (TStackAndItsNewCount &ncount : newStackCounts) - { - if (ncount.second > 0) - gh->changeStackCount(ncount.first, ncount.second, true); - else - gh->eraseStack(ncount.first, true); - } - for (auto summoned_iter : summoned) - { - SlotID slot = army->getSlotFor(summoned_iter.first); - if (slot.validSlot()) - { - StackLocation location(army, slot); - gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); - } - else - { - //even if it will be possible to summon anything permanently it should be checked for free slot - //necromancy is handled separately - gh->complain("No free slot to put summoned creature"); - } - } - for (auto al : removedWarMachines) - { - gh->removeArtifact(al); - } - if (heroWithDeadCommander != ObjectInstanceID()) - { - SetCommanderProperty scp; - scp.heroid = heroWithDeadCommander; - scp.which = SetCommanderProperty::ALIVE; - scp.amount = 0; - gh->sendAndApply(&scp); - } -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount) -{ - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; - winnerSide = result.winner; - remainingBattleQueriesCount = RemainingBattleQueriesCount; -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; - winnerSide = 0; - remainingBattleQueriesCount = 0; -} - CRandomGenerator & CGameHandler::getRandomGenerator() { return CRandomGenerator::getDefault(); @@ -6898,5 +4212,21 @@ void CGameHandler::deserializationFix() //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization // restore any places that requires such pointer manually heroPool->gameHandler = this; + battles->setGameHandler(this); playerMessages->gameHandler = this; } + +void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) +{ + battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +{ + battles->startBattleI(army1, army2, tile, creatureBank); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank ) +{ + battles->startBattleI(army1, army2, creatureBank); +} diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 49a6dfcd6..1f2cc3637 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -11,29 +11,25 @@ #include -#include "../lib/FunctionList.h" #include "../lib/IGameCallback.h" #include "../lib/battle/CBattleInfoCallback.h" -#include "../lib/battle/BattleAction.h" #include "../lib/ScriptHandler.h" -#include "CQuery.h" VCMI_LIB_NAMESPACE_BEGIN -class CGameState; -struct StartInfo; -struct BattleResult; struct SideInBattle; -struct BattleAttack; -struct BattleStackAttacked; +class IMarket; +class SpellCastEnvironment; +class CConnection; +class CCommanderInstance; +class EVictoryLossCheckResult; + struct CPack; -struct Query; +struct CPackForServer; +struct NewTurn; +struct CGarrisonOperationPack; struct SetResources; struct NewStructures; -class CGHeroInstance; -class IMarket; - -class SpellCastEnvironment; #if SCRIPTING_ENABLED namespace scripting @@ -42,16 +38,17 @@ namespace scripting } #endif - template class CApplier; VCMI_LIB_NAMESPACE_END class HeroPoolProcessor; -class CGameHandler; class CVCMIServer; class CBaseForGHApply; class PlayerMessageProcessor; +class BattleProcessor; +class QueriesProcessor; +class CObjectVisitQuery; struct PlayerStatus { @@ -80,33 +77,18 @@ public: } }; -struct CasualtiesAfterBattle -{ - using TStackAndItsNewCount = std::pair; - using TSummoned = std::map; - enum {ERASE = -1}; - const CArmedInstance * army; - std::vector newStackCounts; - std::vector removedWarMachines; - TSummoned summoned; - ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - - CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); - void updateArmy(CGameHandler *gh); -}; - class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment { CVCMIServer * lobby; std::shared_ptr> applier; - std::unique_ptr battleThread; public: - boost::recursive_mutex battleActionMutex; + using CCallbackBase::setBattle; std::unique_ptr heroPool; + std::unique_ptr battles; + std::unique_ptr queries; - using FireShieldInfo = std::vector>; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; @@ -120,7 +102,7 @@ public: //queries stuff boost::recursive_mutex gsm; ui32 QID; - Queries queries; + SpellCastEnvironment * spellEnv; @@ -135,26 +117,6 @@ public: bool isBlockedByQueries(const CPack *pack, PlayerColor player); bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); - int moveStack(int stack, BattleHex dest); //returned value - travelled distance - void runBattle(); - - ////used only in endBattle - don't touch elsewhere - bool visitObjectAfterVictory; - // - void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle - void endBattleConfirm(const BattleInfo * battleInfo); - - void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - - // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); - - void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); - void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); - - void checkBattleStateChanges(); - void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - void setBattleResult(BattleResult::EResult resultType, int victoriusSide); CGameHandler() = default; CGameHandler(CVCMIServer * lobby); @@ -242,14 +204,6 @@ public: PlayerColor getPlayerAt(std::shared_ptr c) const; bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; - void updateGateState(); - bool makeBattleAction(BattleAction &ba); - bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) - bool makeCustomAction(BattleAction &ba); - void stackEnchantedTrigger(const CStack * stack); - void stackTurnTrigger(const CStack *stack); - - void removeObstacle(const CObstacleInstance &obstacle); bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, ui8 formation ); @@ -284,7 +238,6 @@ public: bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ); void objectVisitEnded(const CObjectVisitQuery &query); - void engageIntoBattle( PlayerColor player ); bool dig(const CGHeroInstance *h); void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); @@ -292,7 +245,7 @@ public: { h & QID; h & states; - h & finishingBattle; + h & battles; h & heroPool; h & getRandomGenerator(); h & playerMessages; @@ -324,39 +277,8 @@ public: void throwAndComplain(CPackForServer * pack, std::string txt); bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - struct FinishingBattleHelper - { - FinishingBattleHelper(); - FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); - - inline bool isDraw() const {return winnerSide == 2;} - - const CGHeroInstance *winnerHero, *loserHero; - PlayerColor victor, loser; - ui8 winnerSide; - - int remainingBattleQueriesCount; - - template void serialize(Handler &h, const int version) - { - h & winnerHero; - h & loserHero; - h & victor; - h & loser; - h & winnerSide; - h & remainingBattleQueriesCount; - } - }; - - std::unique_ptr finishingBattle; - - void battleAfterLevelUp(const BattleResult &result); - void run(bool resume); void newTurn(); - void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); void spawnWanderingMonsters(CreatureID creatureID); @@ -384,8 +306,6 @@ private: void reinitScripting(); void deserializationFix(); - - void makeStackDoNothing(const CStack * next); void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; const std::string complainNoCreatures; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4239d07ba..49c40d60e 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,11 +1,21 @@ set(server_SRCS StdInc.cpp + battles/BattleActionProcessor.cpp + battles/BattleFlowProcessor.cpp + battles/BattleProcessor.cpp + battles/BattleResultProcessor.cpp + + queries/BattleQueries.cpp + queries/CQuery.cpp + queries/MapQueries.cpp + queries/QueriesProcessor.cpp + + processors/HeroPoolProcessor.cpp + processors/PlayerMessageProcessor.cpp + CGameHandler.cpp - HeroPoolProcessor.cpp - PlayerMessageProcessor.cpp ServerSpellCastEnvironment.cpp - CQuery.cpp CVCMIServer.cpp NetPacksServer.cpp NetPacksLobbyServer.cpp @@ -14,11 +24,21 @@ set(server_SRCS set(server_HEADERS StdInc.h + battles/BattleActionProcessor.h + battles/BattleFlowProcessor.h + battles/BattleProcessor.h + battles/BattleResultProcessor.h + + queries/BattleQueries.h + queries/CQuery.h + queries/MapQueries.h + queries/QueriesProcessor.h + + processors/HeroPoolProcessor.h + processors/PlayerMessageProcessor.h + CGameHandler.h - HeroPoolProcessor.h - PlayerMessageProcessor.h ServerSpellCastEnvironment.h - CQuery.h CVCMIServer.h LobbyNetPackVisitors.h ServerNetPackVisitors.h diff --git a/server/CQuery.cpp b/server/CQuery.cpp deleted file mode 100644 index 1d0a7d7b3..000000000 --- a/server/CQuery.cpp +++ /dev/null @@ -1,584 +0,0 @@ -/* - * CQuery.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 "CQuery.h" -#include "CGameHandler.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/mapObjects/MiscObjects.h" -#include "../lib/serializer/Cast.h" - -boost::mutex Queries::mx; - -template -std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") -{ - std::string ret = opener; - auto itr = std::begin(c); - if(itr != std::end(c)) - { - ret += std::to_string(*itr); - while(++itr != std::end(c)) - { - ret += delimeter; - ret += std::to_string(*itr); - } - } - ret += closer; - return ret; -} - -std::ostream & operator<<(std::ostream & out, const CQuery & query) -{ - return out << query.toString(); -} - -std::ostream & operator<<(std::ostream & out, QueryPtr query) -{ - return out << "[" << query.get() << "] " << query->toString(); -} - -CQuery::CQuery(Queries * Owner): - owner(Owner) -{ - boost::unique_lock l(Queries::mx); - - static QueryID QID = QueryID(0); - - queryID = ++QID; - logGlobal->trace("Created a new query with id %d", queryID); -} - - -CQuery::~CQuery() -{ - logGlobal->trace("Destructed the query with id %d", queryID); -} - -void CQuery::addPlayer(PlayerColor color) -{ - if(color.isValidPlayer()) - players.push_back(color); -} - -std::string CQuery::toString() const -{ - const auto size = players.size(); - const std::string plural = size > 1 ? "s" : ""; - std::string names; - - for(size_t i = 0; i < size; i++) - { - names += boost::to_upper_copy(players[i].getStr()); - - if(i < size - 2) - names += ", "; - else if(size > 1 && i == size - 2) - names += " and "; - } - std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") - % typeid(*this).name() - % queryID - % plural - % names - ); - return ret; -} - -bool CQuery::endsByPlayerAnswer() const -{ - return false; -} - -void CQuery::onRemoval(PlayerColor color) -{ - -} - -bool CQuery::blocksPack(const CPack * pack) const -{ - return false; -} - -void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - -} - -void CQuery::onExposure(QueryPtr topQuery) -{ - logGlobal->trace("Exposed query with id %d", queryID); - owner->popQuery(*this); -} - -void CQuery::onAdding(PlayerColor color) -{ - -} - -void CQuery::onAdded(PlayerColor color) -{ - -} - -void CQuery::setReply(const JsonNode & reply) -{ - -} - -bool CQuery::blockAllButReply(const CPack * pack) const -{ - //We accept only query replies from correct player - if(auto reply = dynamic_ptr_cast(pack)) - return !vstd::contains(players, reply->player); - - return true; -} - -CGhQuery::CGhQuery(CGameHandler * owner): - CQuery(&owner->queries), gh(owner) -{ - -} - -CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): - CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) -{ - addPlayer(Hero->tempOwner); -} - -bool CObjectVisitQuery::blocksPack(const CPack *pack) const -{ - //During the visit itself ALL actions are blocked. - //(However, the visit may trigger a query above that'll pass some.) - return true; -} - -void CObjectVisitQuery::onRemoval(PlayerColor color) -{ - gh->objectVisitEnded(*this); - - //TODO or should it be destructor? - //Can object visit affect 2 players and what would be desired behavior? - if(removeObjectAfterVisit) - gh->removeObject(visitedObject); -} - -void CObjectVisitQuery::onExposure(QueryPtr topQuery) -{ - //Object may have been removed and deleted. - if(gh->isValidObject(visitedObject)) - topQuery->notifyObjectAboutRemoval(*this); - - owner->popIfTop(*this); -} - -void Queries::popQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); - if(topQuery(player) != query) - { - logGlobal->trace("Cannot remove, not a top!"); - return; - } - - queries[player] -= query; - auto nextQuery = topQuery(player); - - query->onRemoval(player); - - //Exposure on query below happens only if removal didn't trigger any new query - if(nextQuery && nextQuery == topQuery(player)) - nextQuery->onExposure(query); -} - -void Queries::popQuery(const CQuery &query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); - - assert(query.players.size()); - for(auto player : query.players) - { - auto top = topQuery(player); - if(top.get() == &query) - popQuery(top); - else - { - logGlobal->trace("Cannot remove query %s", query.toString()); - logGlobal->trace("Queries found:"); - for(auto q : queries[player]) - { - logGlobal->trace(q->toString()); - } - } - } -} - -void Queries::popQuery(QueryPtr query) -{ - for(auto player : query->players) - popQuery(player, query); -} - -void Queries::addQuery(QueryPtr query) -{ - for(auto player : query->players) - addQuery(player, query); - - for(auto player : query->players) - query->onAdded(player); -} - -void Queries::addQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); - query->onAdding(player); - queries[player].push_back(query); -} - -QueryPtr Queries::topQuery(PlayerColor player) -{ - return vstd::backOrNull(queries[player]); -} - -void Queries::popIfTop(QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); - if(!query) - logGlobal->error("The query is nullptr! Ignoring."); - - popIfTop(*query); -} - -void Queries::popIfTop(const CQuery & query) -{ - for(PlayerColor color : query.players) - if(topQuery(color).get() == &query) - popQuery(color, topQuery(color)); -} - -std::vector> Queries::allQueries() const -{ - std::vector> ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -std::vector Queries::allQueries() -{ - //TODO code duplication with const function :( - std::vector ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -QueryPtr Queries::getQuery(QueryID queryID) -{ - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - if(query->queryID == queryID) - return query; - return nullptr; -} - -void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - if(result) - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): - CGhQuery(owner) -{ - belligerents[0] = Bi->sides[0].armyObject; - belligerents[1] = Bi->sides[1].armyObject; - - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner): - CGhQuery(owner), bi(nullptr) -{ - belligerents[0] = belligerents[1] = nullptr; -} - -bool CBattleQuery::blocksPack(const CPack * pack) const -{ - const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name()); -} - -void CBattleQuery::onRemoval(PlayerColor color) -{ - if(result) - gh->battleAfterLevelUp(*result); -} - -void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); -} - -CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): - CDialogQuery(owner) -{ - exchangingArmies[0] = up; - exchangingArmies[1] = down; - - addPlayer(up->tempOwner); - addPlayer(down->tempOwner); -} - -bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const -{ - std::set ourIds; - ourIds.insert(this->exchangingArmies[0]->id); - ourIds.insert(this->exchangingArmies[1]->id); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); - - if(auto arts = dynamic_ptr_cast(pack)) - { - if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) - if(!vstd::contains(ourIds, *id1)) - return true; - - if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) - if(!vstd::contains(ourIds, *id2)) - return true; - return false; - } - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->id); - - if(auto arts = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); - - if(auto art = dynamic_ptr_cast(pack)) - { - if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) - return !vstd::contains(ourIds, *id); - } - - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->heroID); - - if(auto upgrade = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, upgrade->id); - - if(auto formation = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, formation->hid); - - return CDialogQuery::blocksPack(pack); -} - -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): - CDialogQuery(owner) -{ - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -void CBattleDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - if(*answer == 1) - { - gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); - } - else - { - gh->endBattleConfirm(bi); - } -} - -void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - assert(answer); - objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); -} - -CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): - CDialogQuery(owner) -{ - this->bd = bd; - addPlayer(bd.player); -} - -void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - // do not change to dynamic_ptr_cast - SIGSEGV! - auto obj = dynamic_cast(objectVisit.visitedObject); - if(obj) - obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); - else - logGlobal->error("Invalid instance in teleport query"); -} - -CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): - CDialogQuery(owner) -{ - this->td = td; - addPlayer(td.player); -} - -CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - hlu = Hlu; - addPlayer(hero->tempOwner); -} - -void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); - gh->levelUpHero(hero, hlu.skills[*answer]); -} - -void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - -CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - clu = Clu; - addPlayer(hero->tempOwner); -} - -void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); - gh->levelUpCommander(hero->commander, clu.skills[*answer]); -} - -void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - -CDialogQuery::CDialogQuery(CGameHandler * owner): - CGhQuery(owner) -{ - -} - -bool CDialogQuery::endsByPlayerAnswer() const -{ - return true; -} - -bool CDialogQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -void CDialogQuery::setReply(const JsonNode & reply) -{ - if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) - answer = reply.Integer(); -} - -CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): - CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) -{ - players.push_back(hero->tempOwner); -} - -void CHeroMovementQuery::onExposure(QueryPtr topQuery) -{ - assert(players.size() == 1); - - if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard - //TODO what if there were H4-like escape? we should also check pos - { - logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); - //finish movement - visitDestAfterVictory = false; - gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); - } - - owner->popIfTop(*this); -} - -void CHeroMovementQuery::onRemoval(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; - gh->sendAndApply(&pb); -} - -void CHeroMovementQuery::onAdding(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - gh->sendAndApply(&pb); -} - -CGenericQuery::CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback): - CQuery(Owner), callback(Callback) -{ - addPlayer(color); -} - -bool CGenericQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -bool CGenericQuery::endsByPlayerAnswer() const -{ - return true; -} - -void CGenericQuery::onExposure(QueryPtr topQuery) -{ - //do nothing -} - -void CGenericQuery::setReply(const JsonNode & reply) -{ - this->reply = reply; -} - -void CGenericQuery::onRemoval(PlayerColor color) -{ - callback(reply); -} diff --git a/server/CQuery.h b/server/CQuery.h deleted file mode 100644 index 089e1c256..000000000 --- a/server/CQuery.h +++ /dev/null @@ -1,242 +0,0 @@ -/* - * CQuery.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/GameConstants.h" -#include "../lib/int3.h" -#include "../lib/NetPacks.h" -#include "JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CArmedInstance; - -VCMI_LIB_NAMESPACE_END - -class CGameHandler; -class CObjectVisitQuery; -class CQuery; -class Queries; - -using QueryPtr = std::shared_ptr; - -// This class represents any kind of prolonged interaction that may need to do something special after it is over. -// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. -// Examples: -// - all kinds of blocking dialog windows -// - battle -// - object visit -// - hero movement -// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. -class CQuery -{ -public: - std::vector players; //players that are affected (often "blocked") by query - QueryID queryID; - - CQuery(Queries * Owner); - - - virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. - - virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs) - virtual void onAdding(PlayerColor color); //called just before query is pushed on stack - virtual void onAdded(PlayerColor color); //called right after query is pushed on stack - virtual void onRemoval(PlayerColor color); //called after query is removed from stack - virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top) - virtual std::string toString() const; - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; - - virtual void setReply(const JsonNode & reply); - - virtual ~CQuery(); -protected: - Queries * owner; - void addPlayer(PlayerColor color); - bool blockAllButReply(const CPack * pack) const; -}; - -std::ostream &operator<<(std::ostream &out, const CQuery &query); -std::ostream &operator<<(std::ostream &out, QueryPtr query); - -class CGhQuery : public CQuery -{ -public: - CGhQuery(CGameHandler * owner); -protected: - CGameHandler * gh; -}; - -//Created when hero visits object. -//Removed when query above is resolved (or immediately after visit if no queries were created) -class CObjectVisitQuery : public CGhQuery -{ -public: - const CGObjectInstance *visitedObject; - const CGHeroInstance *visitingHero; - int3 tile; //may be different than hero pos -> eg. visit via teleport - bool removeObjectAfterVisit; - - CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); - - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; -}; - -class CBattleQuery : public CGhQuery -{ -public: - std::array belligerents; - std::array initialHeroMana; - - const BattleInfo *bi; - std::optional result; - - CBattleQuery(CGameHandler * owner); - CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; -}; - -//Created when hero attempts move and something happens -//(not necessarily position change, could be just an object interaction). -class CHeroMovementQuery : public CGhQuery -{ -public: - TryMoveHero tmh; - bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated - const CGHeroInstance *hero; - - virtual void onExposure(QueryPtr topQuery) override; - - CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); - virtual void onAdding(PlayerColor color) override; - virtual void onRemoval(PlayerColor color) override; -}; - -class CDialogQuery : public CGhQuery -{ -public: - CDialogQuery(CGameHandler * owner); - virtual bool endsByPlayerAnswer() const override; - virtual bool blocksPack(const CPack *pack) const override; - void setReply(const JsonNode & reply) override; -protected: - std::optional answer; -}; - -class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs -{ -public: - std::array exchangingArmies; - - CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; -}; - -class CBattleDialogQuery : public CDialogQuery -{ -public: - CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); - - const BattleInfo * bi; - - virtual void onRemoval(PlayerColor color) override; -}; - -//yes/no and component selection dialogs -class CBlockingDialogQuery : public CDialogQuery -{ -public: - BlockingDialog bd; //copy of pack... debug purposes - - CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CTeleportDialogQuery : public CDialogQuery -{ -public: - TeleportDialog td; //copy of pack... debug purposes - - CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CHeroLevelUpDialogQuery : public CDialogQuery -{ -public: - CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - HeroLevelUp hlu; - const CGHeroInstance * hero; -}; - -class CCommanderLevelUpDialogQuery : public CDialogQuery -{ -public: - CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - CommanderLevelUp clu; - const CGHeroInstance * hero; -}; - -class CGenericQuery : public CQuery -{ -public: - CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback); - - bool blocksPack(const CPack * pack) const override; - bool endsByPlayerAnswer() const override; - void onExposure(QueryPtr topQuery) override; - void setReply(const JsonNode & reply) override; - void onRemoval(PlayerColor color) override; -private: - std::function callback; - JsonNode reply; -}; - -class Queries -{ -private: - void addQuery(PlayerColor player, QueryPtr query); - void popQuery(PlayerColor player, QueryPtr query); - - std::map> queries; //player => stack of queries - -public: - static boost::mutex mx; - - void addQuery(QueryPtr query); - void popQuery(const CQuery &query); - void popQuery(QueryPtr query); - void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) - void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) - - QueryPtr topQuery(PlayerColor player); - - std::vector> allQueries() const; - std::vector allQueries(); - QueryPtr getQuery(QueryID queryID); - //void removeQuery -}; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index a7a75f039..83a711fe9 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -36,7 +36,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "CGameHandler.h" -#include "PlayerMessageProcessor.h" +#include "processors/PlayerMessageProcessor.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/GameConstants.h" #include "../lib/logging/CBasicLogConfigurator.h" diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 3027242c0..72499d783 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -11,8 +11,10 @@ #include "ServerNetPackVisitors.h" #include "CGameHandler.h" -#include "HeroPoolProcessor.h" -#include "PlayerMessageProcessor.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "queries/QueriesProcessor.h" #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" @@ -47,7 +49,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) } gh.throwOnWrongPlayer(&pack, pack.player); - if(gh.queries.topQuery(pack.player)) + if(gh.queries->topQuery(pack.player)) gh.throwAndComplain(&pack, "Cannot end turn before resolving queries!"); gh.states.setFlag(gs.currentPlayer, &PlayerStatus::makingTurn, false); @@ -280,52 +282,10 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { - boost::unique_lock lock(gh.battleActionMutex); + if (!gh.hasPlayerAt(pack.player, pack.c)) + gh.throwAndComplain(&pack, "No such pack.player!"); - const BattleInfo * b = gs.curB; - if(!b) - gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); - - if(b->tacticDistance) - { - if(pack.ba.actionType != EActionType::WALK && pack.ba.actionType != EActionType::END_TACTIC_PHASE - && pack.ba.actionType != EActionType::RETREAT && pack.ba.actionType != EActionType::SURRENDER) - gh.throwAndComplain(&pack, "Can not make actions while in tactics mode!"); - if(!vstd::contains(gh.connections[b->sides[b->tacticsSide].color], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - else - { - auto active = b->battleActiveUnit(); - if(!active) - gh.throwAndComplain(&pack, "No active unit in battle!"); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - - result = gh.makeBattleAction(pack.ba); -} - -void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) -{ - boost::unique_lock lock(gh.battleActionMutex); - - const BattleInfo * b = gs.curB; - if(!b) - gh.throwNotAllowedAction(&pack); - if(b->tacticDistance) - gh.throwNotAllowedAction(&pack); - auto active = b->battleActiveUnit(); - if(!active) - gh.throwNotAllowedAction(&pack); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwNotAllowedAction(&pack); - if(pack.ba.actionType != EActionType::HERO_SPELL) - gh.throwNotAllowedAction(&pack); - - result = gh.makeCustomAction(pack.ba); + result = gh.battles->makePlayerBattleAction(pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index a9ed4595b..821046418 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -55,8 +55,7 @@ public: virtual void visitBuildBoat(BuildBoat & pack) override; virtual void visitQueryReply(QueryReply & pack) override; virtual void visitMakeAction(MakeAction & pack) override; - virtual void visitMakeCustomAction(MakeCustomAction & pack) override; virtual void visitDigWithHero(DigWithHero & pack) override; virtual void visitCastAdvSpell(CastAdvSpell & pack) override; virtual void visitPlayerMessage(PlayerMessage & pack) override; -}; \ No newline at end of file +}; diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 7837ecba0..4f0fe8cba 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -8,10 +8,15 @@ * */ #include "StdInc.h" -#include "../lib/gameState/CGameState.h" -#include "CGameHandler.h" #include "ServerSpellCastEnvironment.h" +#include "CGameHandler.h" +#include "queries/QueriesProcessor.h" +#include "queries/CQuery.h" + +#include "../lib/gameState/CGameState.h" +#include "../lib/NetPacks.h" + ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) : gh(gh) @@ -90,8 +95,8 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function callback) { - auto query = std::make_shared(&gh->queries, color, callback); + auto query = std::make_shared(gh->queries.get(), color, callback); request->queryID = query->queryID; - gh->queries.addQuery(query); + gh->queries->addQuery(query); gh->sendAndApply(request); } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp new file mode 100644 index 000000000..e32d082f3 --- /dev/null +++ b/server/battles/BattleActionProcessor.cpp @@ -0,0 +1,1431 @@ +/* + * BattleActionProcessor.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 "BattleActionProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/NetPacks.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/Problem.h" + +BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +bool BattleActionProcessor::doEmptyAction(const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doWaitAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + + if (!canStackAct(stack)) + return false; + + return true; +} + +bool BattleActionProcessor::doRetreatAction(const BattleAction & ba) +{ + if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) + { + gameHandler->complain("Cannot retreat!"); + return false; + } + + owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); + return true; +} + +bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba) +{ + PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; + int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); + if (cost < 0) + { + gameHandler->complain("Cannot surrender!"); + return false; + } + + if (gameHandler->getResource(player, EGameResID::GOLD) < cost) + { + gameHandler->complain("Not enough gold to surrender!"); + return false; + } + + gameHandler->giveResource(player, EGameResID::GOLD, -cost); + owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); + return true; +} + +bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) +{ + const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if (!h) + { + logGlobal->error("Wrong caster!"); + return false; + } + + const CSpell * s = ba.spell.toSpell(); + if (!s) + { + logGlobal->error("Wrong spell id (%d)!", ba.spell.getNum()); + return false; + } + + spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); + + spells::detail::ProblemImpl problem; + + auto m = s->battleMechanics(¶meters); + + if(!m->canBeCast(problem))//todo: should we check aimed cast? + { + logGlobal->warn("Spell cannot be cast!"); + std::vector texts; + problem.getAll(texts); + for(auto s : texts) + logGlobal->warn(s); + return false; + } + + parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); + + return true; +} + +bool BattleActionProcessor::doWalkAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for move action."); + return false; + } + + int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move + if (!walkedTiles) + { + gameHandler->complain("Stack failed movement!"); + return false; + } + return true; +} + +bool BattleActionProcessor::doDefendAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + + if (!canStackAct(stack)) + return false; + + //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) + SetStackEffect sse; + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(defenseBonusToAdd)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + std::vector buffer; + if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) + { + difference = 1; + buffer.push_back(alternativeWeakCreatureBonus); + } + else + { + buffer.push_back(defenseBonusToAdd); + } + + buffer.push_back(bonus2); + + sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); + gameHandler->sendAndApply(&sse); + + BattleLogMessage message; + + MetaString text; + stack->addText(text, EMetaText::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.replaceNumber(difference); + + message.lines.push_back(text); + + gameHandler->sendAndApply(&message); + return true; +} + +bool BattleActionProcessor::doAttackAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 2) + { + gameHandler->complain("Two destinations required for attack action."); + return false; + } + + BattleHex attackPos = target.at(0).hexValue; + BattleHex destinationTile = target.at(1).hexValue; + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); + + if(!destinationStack) + { + gameHandler->complain("Invalid target to attack"); + return false; + } + + BattleHex startingPos = stack->getPosition(); + int distance = moveStack(ba.stackNumber, attackPos); + + logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); + + if(stack->getPosition() != attackPos && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) ) + { + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine + return true; + } + + if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + gameHandler->complain("Unit can not attack itself"); + return false; + } + + if(!CStack::isMeleeAttackPossible(stack, destinationStack)) + { + gameHandler->complain("Attack cannot be performed!"); + return false; + } + + //attack + int totalAttacks = stack->totalAttacks.getMeleeValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + const bool retaliation = destinationStack->ableToRetaliate(); + for (int i = 0; i < totalAttacks; ++i) + { + //first strike + if(i == 0 && firstStrike && retaliation) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) + { + makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + } + + //counterattack + //we check retaliation twice, so if it unblocked during attack it will work only on next attack + if(stack->alive() + && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) + && (i == 0 && !firstStrike) + && retaliation && destinationStack->ableToRetaliate()) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + } + + //return + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) + && target.size() == 3 + && startingPos != stack->getPosition() + && startingPos == target.at(2).hexValue + && stack->alive()) + { + moveStack(ba.stackNumber, startingPos); + //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) + } + return true; +} + +bool BattleActionProcessor::doShootAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for shot action."); + return false; + } + + auto destination = target.at(0).hexValue; + + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); + + if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) + { + gameHandler->complain("Cannot shoot!"); + return false; + } + + if (!destinationStack) + { + gameHandler->complain("No target to shoot!"); + return false; + } + + makeAttack(stack, destinationStack, 0, destination, true, true, false); + + //ranged counterattack + if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) + && destinationStack->ableToRetaliate() + && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) + && stack->alive()) //attacker may have died (fire shield) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); + } + //allow more than one additional attack + + int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + for(int i = 1; i < totalRangedAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots.canUse() + ) + { + makeAttack(stack, destinationStack, 0, destination, false, true, false); + } + } + + return true; +} + +bool BattleActionProcessor::doCatapultAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + if(!catapultAbility || catapultAbility->subtype < 0) + { + gameHandler->complain("We do not know how to shoot :P"); + } + else + { + const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); + parameters.setSpellLevel(shotLevel); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + SpellID spellID = ba.spell; + + if (!canStackAct(stack)) + return false; + + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); + + //TODO special bonus for genies ability + if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + + if (spellID < 0) + gameHandler->complain("That stack can't cast spells!"); + else + { + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doHealAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for heal action."); + return false; + } + + const battle::Unit * destStack = nullptr; + std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + + if(target.at(0).unitValue) + destStack = target.at(0).unitValue; + else + destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); + + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) + { + gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); + } + else + { + const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + auto dest = battle::Destination(destStack, target.at(0).hexValue); + parameters.setSpellLevel(0); + parameters.cast(gameHandler->spellEnv, {dest}); + } + return true; +} + +bool BattleActionProcessor::canStackAct(const CStack * stack) +{ + if (!stack) + { + gameHandler->complain("No such stack!"); + return false; + } + if (!stack->alive()) + { + gameHandler->complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if (gameHandler->battleTacticDist()) + { + if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) + { + gameHandler->complain("This is not a stack of side that has tactics!"); + return false; + } + } + else + { + if (stack->unitId() != gameHandler->gameState()->curB->getActiveStackID()) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } + } + return true; +} + +bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) +{ + switch(ba.actionType) + { + case EActionType::NO_ACTION: + return doEmptyAction(ba); + case EActionType::END_TACTIC_PHASE: + return doEndTacticsAction(ba); + case EActionType::RETREAT: + return doRetreatAction(ba); + case EActionType::SURRENDER: + return doSurrenderAction(ba); + case EActionType::HERO_SPELL: + return doHeroSpellAction(ba); + case EActionType::WALK: + return doWalkAction(ba); + case EActionType::WAIT: + return doWaitAction(ba); + case EActionType::DEFEND: + return doDefendAction(ba); + case EActionType::WALK_AND_ATTACK: + return doAttackAction(ba); + case EActionType::SHOOT: + return doShootAction(ba); + case EActionType::CATAPULT: + return doCatapultAction(ba); + case EActionType::MONSTER_SPELL: + return doUnitSpellAction(ba); + case EActionType::STACK_HEAL: + return doHealAction(ba); + } + gameHandler->complain("Unrecognized action type received!!"); + return false; +} + +bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) +{ + logGlobal->trace("Making action: %s", ba.toString()); + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + + StartAction startAction(ba); + gameHandler->sendAndApply(&startAction); + + bool result = dispatchBattleAction(ba); + + EndAction endAction; + gameHandler->sendAndApply(&endAction); + + if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); + + return result; +} + +int BattleActionProcessor::moveStack(int stack, BattleHex dest) +{ + int ret = 0; + + const CStack *curStack = gameHandler->battleGetStackByID(stack); + const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); + + assert(curStack); + assert(dest < GameConstants::BFIELD_SIZE); + + if (gameHandler->gameState()->curB->tacticDistance) + { + assert(gameHandler->gameState()->curB->isInTacticRange(dest)); + } + + auto start = curStack->getPosition(); + if (start == dest) + return 0; + + //initing necessary tables + auto accessibility = gameHandler->getAccesibility(curStack); + std::set passed; + //Ignore obstacles on starting position + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + + //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) + if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) + { + BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); + + if(accessibility.accessible(shifted, curStack)) + dest = shifted; + } + + if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) + { + gameHandler->complain("Given destination is not accessible!"); + return 0; + } + + bool canUseGate = false; + auto dbState = gameHandler->gameState()->curB->si.gateState; + if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + dbState != EGateState::DESTROYED && + dbState != EGateState::BLOCKED) + { + canUseGate = true; + } + + std::pair< std::vector, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack); + + ret = path.second; + + int creSpeed = curStack->speed(0, true); + + if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) + creSpeed = GameConstants::BFIELD_SIZE; + + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_OUTER) + return true; + if (hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (isGateDrawbridgeHex(hex)) + return true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) + return true; + } + + return false; + }; + + if (curStack->hasBonusOfType(BonusType::FLYING)) + { + if (path.second <= creSpeed && path.first.size() > 0) + { + if (canUseGate && dbState != EGateState::OPENED && + occupyGateDrawbridgeHex(dest)) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + + //inform clients about move + BattleStackMoved sm; + sm.stack = curStack->unitId(); + std::vector tiles; + tiles.push_back(path.first[0]); + sm.tilesToMove = tiles; + sm.distance = path.second; + sm.teleporting = false; + gameHandler->sendAndApply(&sm); + } + } + else //for non-flying creatures + { + std::vector tiles; + const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); + int v = (int)path.first.size()-1; + path.first.push_back(start); + + // check if gate need to be open or closed at some point + BattleHex openGateAtHex, gateMayCloseAtHex; + if (canUseGate) + { + for (int i = (int)path.first.size()-1; i >= 0; i--) + { + auto needOpenGates = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) + return true; + else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto hex = path.first[i]; + if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) + { + if (needOpenGates(hex)) + openGateAtHex = path.first[i+1]; + + //TODO we need find batter way to handle double-wide stacks + //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && needOpenGates(otherHex)) + openGateAtHex = path.first[i+2]; + } + + //gate may be opened and then closed during stack movement, but not other way around + if (openGateAtHex.isValid()) + dbState = EGateState::OPENED; + } + + if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) + { + if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + if (hasWideMoat) + { + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && + path.first[i-1] != ESiegeHex::GATE_INNER && + path.first[i-1] != ESiegeHex::GATE_BRIDGE) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + } + } + + bool stackIsMoving = true; + + while(stackIsMoving) + { + if (verror("Movement terminated abnormally"); + break; + } + + bool gateStateChanging = false; + //special handling for opening gate on from starting hex + if (openGateAtHex.isValid() && openGateAtHex == start) + gateStateChanging = true; + else + { + for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) + { + BattleHex hex = path.first[v]; + tiles.push_back(hex); + + if ((openGateAtHex.isValid() && openGateAtHex == hex) || + (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) + { + gateStateChanging = true; + } + + //if we walked onto something, finalize this portion of stack movement check into obstacle + if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty()) + obstacleHit = true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + //two hex creature hit obstacle by backside + auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false); + if(otherHex.isValid() && !obstacle2.empty()) + obstacleHit = true; + } + if(!obstacleHit) + passed.insert(hex); + } + } + + if (!tiles.empty()) + { + //commit movement + BattleStackMoved sm; + sm.stack = curStack->unitId(); + sm.distance = path.second; + sm.teleporting = false; + sm.tilesToMove = tiles; + gameHandler->sendAndApply(&sm); + tiles.clear(); + } + + //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end + if (curStack->getPosition() != dest) + { + if(stackIsMoving && start != curStack->getPosition()) + { + stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + } + if (gateStateChanging) + { + if (curStack->getPosition() == openGateAtHex) + { + openGateAtHex = BattleHex(); + //only open gate if stack is still alive + if (curStack->alive()) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + } + else if (curStack->getPosition() == gateMayCloseAtHex) + { + gateMayCloseAtHex = BattleHex(); + owner->updateGateState(); + } + } + } + else + //movement finished normally: we reached destination + stackIsMoving = false; + } + } + //handle last hex separately for deviation + if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) + { + if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) + || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) + passed.clear(); //Just empty passed, obstacles will handled automatically + } + //handling obstacle on the final field (separate, because it affects both flying and walking stacks) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + + return ret; +} + +void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) +{ + if(first && !counter) + handleAttackBeforeCasting(ranged, attacker, defender); + + FireShieldInfo fireShield; + BattleAttack bat; + BattleLogMessage blm; + bat.stackAttacking = attacker->unitId(); + bat.tile = targetHex; + + std::shared_ptr attackerState = attacker->acquireState(); + + if(ranged) + bat.flags |= BattleAttack::SHOT; + if(counter) + bat.flags |= BattleAttack::COUNTER; + + const int attackerLuck = attacker->luckVal(); + + if(attackerLuck > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::LUCKY; + } + + if(attackerLuck < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::UNLUCKY; + } + + if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) + { + bat.flags |= BattleAttack::DEATH_BLOW; + } + + const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner()); + if(owner) + { + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); + if (chance > gameHandler->getRandomGenerator().nextInt(99)) + bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; + } + + int64_t drainedLife = 0; + + // only primary target + if(defender->alive()) + drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); + + //multiple-hex normal attack + std::set attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + + std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + if(bonus && ranged) //TODO: make it work in melee? + { + //this is need for displaying hit animation + bat.flags |= BattleAttack::SPELL_LIKE; + bat.spellID = SpellID(bonus->subtype); + + //TODO: should spell override creature`s projectile? + + auto spell = bat.spellID.toSpell(); + + battle::Target target; + target.emplace_back(defender, targetHex); + + spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + event.setSpellLevel(bonus->val); + + auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); + + //TODO: get exact attacked hex for defender + + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + { + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + } + + //now add effect info for all attacked stacks + for (BattleStackAttacked & bsa : bat.bsa) + { + if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield + { + //this is need for displaying affect animation + bsa.flags |= BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID(bonus->subtype); + } + } + } + + attackerState->afterAttack(ranged, counter); + + { + UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); + attackerState->save(info.data); + bat.attackerChanges.changedStacks.push_back(info); + } + + if (drainedLife > 0) + bat.flags |= BattleAttack::LIFE_DRAIN; + + gameHandler->sendAndApply(&bat); + + { + const bool multipleTargets = bat.bsa.size() > 1; + + int64_t totalDamage = 0; + int32_t totalKills = 0; + + for(const BattleStackAttacked & bsa : bat.bsa) + { + totalDamage += bsa.damageAmount; + totalKills += bsa.killedAmount; + } + + { + MetaString text; + attacker->addText(text, EMetaText::GENERAL_TXT, 376); + attacker->addNameReplacement(text); + text.replaceNumber(totalDamage); + blm.lines.push_back(text); + } + + addGenericKilledLog(blm, defender, totalKills, multipleTargets); + } + + // drain life effect (as well as log entry) must be applied after the attack + if(drainedLife > 0) + { + MetaString text; + attackerState->addText(text, EMetaText::GENERAL_TXT, 361); + attackerState->addNameReplacement(text, false); + text.replaceNumber(drainedLife); + defender->addNameReplacement(text, true); + blm.lines.push_back(std::move(text)); + } + + if(!fireShield.empty()) + { + //todo: this should be "virtual" spell instead, we only need fire spell school bonus here + const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); + int64_t totalDamage = 0; + + for(const auto & item : fireShield) + { + const CStack * actor = item.first; + int64_t rawDamage = item.second; + + const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner()); + + if(actorOwner) + { + rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); + } + else + { + rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); + } + + totalDamage+=rawDamage; + //FIXME: add custom effect on actor + } + + if (totalDamage > 0) + { + BattleStackAttacked bsa; + + bsa.flags |= BattleStackAttacked::FIRE_SHIELD; + bsa.stackAttacked = attacker->unitId(); //invert + bsa.attackerID = defender->unitId(); + bsa.damageAmount = totalDamage; + attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured pack; + pack.stacks.push_back(bsa); + gameHandler->sendAndApply(&pack); + + // TODO: this is already implemented in Damage::describeEffect() + { + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 376); + text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); + text.replaceNumber(totalDamage); + blm.lines.push_back(std::move(text)); + } + addGenericKilledLog(blm, attacker, bsa.killedAmount, false); + } + } + + gameHandler->sendAndApply(&blm); + + handleAfterAttackCasting(ranged, attacker, defender); +} + +void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +{ + if(attacker->hasBonusOfType(attackMode)) + { + std::set spellsToCast; + TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); + for(const auto & sf : *spells) + { + spellsToCast.insert(SpellID(sf->subtype)); + } + for(SpellID spellID : spellsToCast) + { + bool castMe = false; + if(!defender->alive()) + { + logGlobal->debug("attackCasting: all attacked creatures have been killed"); + return; + } + int32_t spellLevel = 0; + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); + for(const auto & sf : *spellsByType) + { + int meleeRanged; + if(sf->additionalInfo.size() < 2) + { + // legacy format + vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); + meleeRanged = sf->additionalInfo[0] / 1000; + } + else + { + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + } + if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) + castMe = true; + } + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); + vstd::amin(chance, 100); + + const CSpell * spell = SpellID(spellID).toSpell(); + spells::AbilityCaster caster(attacker, spellLevel); + + spells::Target target; + target.emplace_back(defender); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + + auto m = spell->battleMechanics(¶meters); + + spells::detail::ProblemImpl ignored; + + if(!m->canBeCastAt(target, ignored)) + continue; + + //check if spell should be cast (probability handling) + if(gameHandler->getRandomGenerator().nextInt(99) >= chance) + continue; + + //casting + if(castMe) + { + parameters.cast(gameHandler->spellEnv, target); + } + } + } +} + +void BattleActionProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? +} + +void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + if(!attacker->alive() || !defender->alive()) // can be already dead + return; + + attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); + + if(!defender->alive()) + { + //don't try death stare or acid breath on dead stack (crash!) + return; + } + + if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) + { + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; + vstd::amin(chanceToKill, 1); //cap at 100% + + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + + int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(staredCreatures, maxToKill); + + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); + if(staredCreatures) + { + //TODO: death stare was not originally available for multiple-hex attacks, but... + const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(staredCreatures); + parameters.cast(gameHandler->spellEnv, target); + } + } + + if(!defender->alive()) + return; + + int64_t acidDamage = 0; + TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); + for(const auto & b : *acidBreath) + { + if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99)) + acidDamage += b->val; + } + + if(acidDamage > 0) + { + const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + + parameters.setEffectValue(acidDamage * attacker->getCount()); + parameters.cast(gameHandler->spellEnv, target); + } + + + if(!defender->alive()) + return; + + if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability + { + double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; + vstd::amin(chanceToTrigger, 1); //cap at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; + + if(defender->unitType()->getId() == bonusAdditionalInfo || + (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) + return; + + battle::UnitInfo resurrectInfo; + resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId(); + resurrectInfo.summoned = false; + resurrectInfo.position = defender->getPosition(); + resurrectInfo.side = defender->unitSide(); + + if(bonusAdditionalInfo != CAddInfo::NONE) + resurrectInfo.type = CreatureID(bonusAdditionalInfo); + else + resurrectInfo.type = attacker->creatureId(); + + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) + resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) + resurrectInfo.count = defender->getCount(); + else + return; //wrong subtype + + BattleUnitsChanged addUnits; + addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); + resurrectInfo.save(addUnits.changedStacks.back().data); + + BattleUnitsChanged removeUnits; + removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&removeUnits); + gameHandler->sendAndApply(&addUnits); + } + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) + { + double chanceToTrigger = 0; + int amountToDie = 0; + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; + amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); + } + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; + } + + vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + BattleStackAttacked bsa; + bsa.attackerID = -1; + bsa.stackAttacked = defender->unitId(); + bsa.damageAmount = amountToDie * defender->getMaxHealth(); + bsa.flags = BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID::SLAYER; + defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured si; + si.stacks.push_back(bsa); + + gameHandler->sendAndApply(&si); + sendGenericKilledLog(defender, bsa.killedAmount, false); + } +} + +int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +{ + BattleStackAttacked bsa; + if(secondary) + bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities + + bsa.attackerID = attackerState->unitId(); + bsa.stackAttacked = def->unitId(); + { + BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); + + bai.deathBlow = bat.deathBlow(); + bai.doubleDamage = bat.ballistaDoubleDmg(); + bai.luckyStrike = bat.lucky(); + bai.unluckyStrike = bat.unlucky(); + + auto range = gameHandler->gameState()->curB->calculateDmgRange(bai); + bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties + } + + int64_t drainedLife = 0; + + //life drain handling + if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) + { + int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; + attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + drainedLife += toHeal; + } + + //soul steal handling + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) + { + //we can have two bonuses - one with subtype 0 and another with subtype 1 + //try to use permanent first, use only one of two + for(si32 subtype = 1; subtype >= 0; subtype--) + { + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) + { + int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); + attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); + drainedLife += toHeal; + break; + } + } + } + bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated + + //fire shield handling + if(!bat.shot() && + !def->isClone() && + def->hasBonusOfType(BonusType::FIRE_SHIELD) && + !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && + CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) + ) + { + //TODO: use damage with bonus but without penalties + auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; + fireShield.push_back(std::make_pair(def, fireShieldDamage)); + } + + return drainedLife; +} + +void BattleActionProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + BattleLogMessage blm; + addGenericKilledLog(blm, defender, killed, multiple); + gameHandler->sendAndApply(&blm); + } +} + +void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + const int32_t txtIndex = (killed > 1) ? 379 : 378; + std::string formatString = VLC->generaltexth->allTexts[txtIndex]; + + // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); + boost::algorithm::trim(formatString); + + boost::format txt(formatString); + if(killed > 1) + { + txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish + } + else //killed == 1 + { + txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes + } + MetaString line; + line.appendRawString(txt.str()); + blm.lines.push_back(std::move(line)); + } +} + +bool BattleActionProcessor::makeAutomaticBattleAction(const BattleAction & ba) +{ + return makeBattleActionImpl(ba); +} + +bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) +{ + const BattleInfo * battle = gameHandler->gameState()->curB; + + if(!battle && gameHandler->complain("Can not make action - there is no battle ongoing!")) + return false; + + if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) + return false; + + if(battle->tacticDistance != 0) + { + if(!ba.isTacticsAction()) + { + gameHandler->complain("Can not make actions while in tactics mode!"); + return false; + } + + if(player != battle->sides[ba.side].color) + { + gameHandler->complain("Can not make actions in battles you are not part of!"); + return false; + } + } + else + { + if (ba.isUnitAction() && ba.stackNumber != battle->getActiveStackID()) + { + gameHandler->complain("Can not make actions - stack is not active!"); + return false; + } + + auto active = battle->battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + auto unitOwner = battle->battleGetOwner(active); + + if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + return false; + } + + return makeBattleActionImpl(ba); +} diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h new file mode 100644 index 000000000..10a08165b --- /dev/null +++ b/server/battles/BattleActionProcessor.h @@ -0,0 +1,79 @@ +/* + * BattleActionProcessor.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 + +struct BattleLogMessage; +struct BattleAttack; +class BattleAction; +struct BattleHex; +class CStack; +class PlayerColor; +enum class BonusType; + +namespace battle +{ +class Unit; +class CUnitState; +} + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class BattleProcessor; + +/// Processes incoming battle action queries and applies requested action(s) +class BattleActionProcessor : boost::noncopyable +{ + using FireShieldInfo = std::vector>; + + BattleProcessor * owner; + CGameHandler * gameHandler; + + int moveStack(int stack, BattleHex dest); //returned value - travelled distance + void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + + void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + + // damage, drain life & fire shield; returns amount of drained life + int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + + void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); + void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + + bool canStackAct(const CStack * stack); + + bool doEmptyAction(const BattleAction & ba); + bool doEndTacticsAction(const BattleAction & ba); + bool doRetreatAction(const BattleAction & ba); + bool doSurrenderAction(const BattleAction & ba); + bool doHeroSpellAction(const BattleAction & ba); + bool doWalkAction(const BattleAction & ba); + bool doWaitAction(const BattleAction & ba); + bool doDefendAction(const BattleAction & ba); + bool doAttackAction(const BattleAction & ba); + bool doShootAction(const BattleAction & ba); + bool doCatapultAction(const BattleAction & ba); + bool doUnitSpellAction(const BattleAction & ba); + bool doHealAction(const BattleAction & ba); + + bool dispatchBattleAction(const BattleAction & ba); + bool makeBattleActionImpl(const BattleAction & ba); + +public: + explicit BattleActionProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + bool makeAutomaticBattleAction(const BattleAction & ba); + bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); +}; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp new file mode 100644 index 000000000..acbd881d0 --- /dev/null +++ b/server/battles/BattleFlowProcessor.cpp @@ -0,0 +1,741 @@ +/* + * BattleFlowProcessor.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 "BattleFlowProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/NetPacks.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" + +BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +void BattleFlowProcessor::summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +{ + int x = targetPosition.getX(); + int y = targetPosition.getY(); + + const bool targetIsAttacker = side == BattleSide::ATTACKER; + + if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + + //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's + if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) + { + if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } + else + { //add back-side guardians for two-hex target, side guardians for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); + + if (!targetIsTwoHex && x > 2) //back guard for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); + else if (targetIsTwoHex)//front-side guardians for two-hex target + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + if (x > 3) //back guard for two-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + } + } + + } + + else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) + { + if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + else + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); + + if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else if (targetIsTwoHex) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + if (x < GameConstants::BFIELD_WIDTH - 4) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + } + } + } + + else if (!targetIsAttacker && y % 2 == 0) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + + else if (targetIsAttacker && y % 2 == 1) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } +} + +void BattleFlowProcessor::tryPlaceMoats() +{ + //Moat should be initialized here, because only here we can use spellcasting + if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) + { + const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER); + const auto * actualCaster = h ? static_cast(h) : nullptr; + auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); + auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell()); + auto target = spells::Target(); + cast.cast(gameHandler->spellEnv, target); + } +} + +void BattleFlowProcessor::onBattleStarted() +{ + gameHandler->setBattle(gameHandler->gameState()->curB); + assert(gameHandler->gameState()->curB); + + tryPlaceMoats(); + + if (gameHandler->gameState()->curB->tacticDistance == 0) + onTacticsEnded(); +} + +void BattleFlowProcessor::trySummonGuardians(const CStack * stack) +{ + if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) + return; + + std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); + auto accessibility = gameHandler->getAccesibility(); + CreatureID creatureData = CreatureID(summonInfo->subtype); + std::vector targetHexes; + const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard + const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); + + /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. + For one-hex targets there are four guardians - front, back and one per side (up + down). + Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front + Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ + if (!guardianIsBig) + targetHexes = stack->getSurroundingHexes(); + else + summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + + for(auto hex : targetHexes) + { + if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex + { + battle::UnitInfo info; + info.id = gameHandler->gameState()->curB->battleNextUnitId(); + info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); + info.type = creatureData; + info.side = stack->unitSide(); + info.position = hex; + info.summoned = true; + + BattleUnitsChanged pack; + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + gameHandler->sendAndApply(&pack); + } + } +} + +void BattleFlowProcessor::castOpeningSpells() +{ + for (int i = 0; i < 2; ++i) + { + auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); + if (!h) + continue; + + TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + + for (auto b : *bl) + { + spells::BonusCaster caster(h, b); + + const CSpell * spell = SpellID(b->subtype).toSpell(); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + parameters.setSpellLevel(3); + parameters.setEffectDuration(b->val); + parameters.massive = true; + parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); + } + } +} + +void BattleFlowProcessor::onTacticsEnded() +{ + //initial stacks appearance triggers, e.g. built-in bonus spells + auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing + + for (CStack * stack : initialStacks) + { + trySummonGuardians(stack); + stackEnchantedTrigger(stack); + } + + castOpeningSpells(); + + // it is possible that due to opening spells one side was eliminated -> check for end of battle + if (owner->checkBattleStateChanges()) + return; + + startNextRound(true); + activateNextStack(); +} + +void BattleFlowProcessor::startNextRound(bool isFirstRound) +{ + BattleNextRound bnr; + bnr.round = gameHandler->gameState()->curB->round + 1; + logGlobal->debug("Round %d", bnr.round); + gameHandler->sendAndApply(&bnr); + + auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it + for (auto &obstPtr : obstacles) + { + if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) + if (sco->turnsRemaining == 0) + removeObstacle(*obstPtr); + } + + const BattleInfo & curB = *gameHandler->gameState()->curB; + + for(auto stack : curB.stacks) + { + if(stack->alive() && !isFirstRound) + stackEnchantedTrigger(stack); + } +} + +const CStack * BattleFlowProcessor::getNextStack() +{ + std::vector q; + gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" + + if(q.empty()) + return nullptr; + + if(q.front().empty()) + return nullptr; + + auto next = q.front().front(); + const auto stack = dynamic_cast(next); + + // regeneration takes place before everything else but only during first turn attempt in each round + // also works under blind and similar effects + if(stack && stack->alive() && !stack->waiting) + { + BattleTriggerEffect bte; + bte.stackID = stack->unitId(); + bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); + + const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); + if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) + bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); + + if(bte.val) // anything to heal + gameHandler->sendAndApply(&bte); + } + + if(!next->willMove()) + return nullptr; + + return stack; +} + +void BattleFlowProcessor::activateNextStack() +{ + //TODO: activate next round if next == nullptr + const auto & curB = *gameHandler->gameState()->curB; + + // Find next stack that requires manual control + for (;;) + { + // battle has ended + if (owner->checkBattleStateChanges()) + return; + + const CStack * next = getNextStack(); + + if (!next) + { + // No stacks to move - start next round + startNextRound(false); + next = getNextStack(); + if (!next) + throw std::runtime_error("Failed to find valid stack to act!"); + } + + BattleUnitsChanged removeGhosts; + + for(auto stack : curB.stacks) + { + if(stack->ghostPending) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); + } + + if(!removeGhosts.changedStacks.empty()) + gameHandler->sendAndApply(&removeGhosts); + + if (!tryMakeAutomaticAction(next)) + { + setActiveStack(next); + break; + } + } +} + +bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) +{ + const auto & curB = *gameHandler->gameState()->curB; + + // check for bad morale => freeze + int nextStackMorale = next->moraleVal(); + if(!next->hadMorale && !next->waited() && nextStackMorale < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + //unit loses its turn - empty freeze action + BattleAction ba; + ba.actionType = EActionType::BAD_MORALE; + ba.side = next->unitSide(); + ba.stackNumber = next->unitId(); + + makeAutomaticAction(next, ba); + return true; + } + } + + if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk + { + logGlobal->trace("Handle Berserk effect"); + std::pair attackInfo = curB.getNearestStack(next); + if (attackInfo.first != nullptr) + { + BattleAction attack; + attack.actionType = EActionType::WALK_AND_ATTACK; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + attack.aimToHex(attackInfo.second); + attack.aimToUnit(attackInfo.first); + + makeAutomaticAction(next, attack); + logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); + } + else + { + makeStackDoNothing(next); + logGlobal->trace("No target found"); + } + return true; + } + + const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next); + const int stackCreatureId = next->unitType()->getId(); + + if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) + { + BattleAction attack; + attack.actionType = EActionType::SHOOT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + //TODO: select target by priority + + const battle::Unit * target = nullptr; + + for(auto & elem : gameHandler->gameState()->curB->stacks) + { + if(elem->unitType()->getId() != CreatureID::CATAPULT + && elem->unitOwner() != next->unitOwner() + && elem->isValidTarget() + && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition())) + { + target = elem; + break; + } + } + + if(target == nullptr) + { + makeStackDoNothing(next); + } + else + { + attack.aimToUnit(target); + makeAutomaticAction(next, attack); + } + return true; + } + + if (next->unitType()->getId() == CreatureID::CATAPULT) + { + const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); + + if (attackableBattleHexes.empty()) + { + makeStackDoNothing(next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) + { + BattleAction attack; + attack.actionType = EActionType::CATAPULT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + makeAutomaticAction(next, attack); + return true; + } + } + + if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) + { + TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s) + { + return s->unitOwner() == next->unitOwner() && s->canBeHealed(); + }); + + if (possibleStacks.empty()) + { + makeStackDoNothing(next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) + { + RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); + const CStack * toBeHealed = possibleStacks.front(); + + BattleAction heal; + heal.actionType = EActionType::STACK_HEAL; + heal.aimToUnit(toBeHealed); + heal.side = next->unitSide(); + heal.stackNumber = next->unitId(); + + makeAutomaticAction(next, heal); + return true; + } + } + + stackTurnTrigger(next); //various effects + + if(next->fear) + { + makeStackDoNothing(next); //end immediately if stack was affected by fear + return true; + } + return false; +} + +bool BattleFlowProcessor::rollGoodMorale(const CStack * next) +{ + //check for good morale + auto nextStackMorale = next->moraleVal(); + if( !next->hadMorale + && !next->defending + && !next->waited() + && !next->fear + && next->alive() + && nextStackMorale > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + BattleTriggerEffect bte; + bte.stackID = next->unitId(); + bte.effect = vstd::to_underlying(BonusType::MORALE); + bte.val = 1; + bte.additionalInfo = 0; + gameHandler->sendAndApply(&bte); //play animation + return true; + } + } + return false; +} + +void BattleFlowProcessor::onActionMade(const BattleAction &ba) +{ + const auto & battle = gameHandler->gameState()->curB; + + const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber, false); + const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID(), false); + + //we're after action, all results applied + + // check whether action has ended the battle + if(owner->checkBattleStateChanges()) + return; + + // tactics - next stack will be selected by player + if(battle->tacticDistance != 0) + return; + + if (ba.isUnitAction()) + { + assert(activeStack != nullptr); + assert(actedStack != nullptr); + + if (rollGoodMorale(actedStack)) + { + // Good morale - same stack makes 2nd turn + setActiveStack(actedStack); + return; + } + } + else + { + if (activeStack && activeStack->alive()) + { + // 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(activeStack); + return; + } + } + + activateNextStack(); +} + +void BattleFlowProcessor::makeStackDoNothing(const CStack * next) +{ + BattleAction doNothing; + doNothing.actionType = EActionType::NO_ACTION; + doNothing.side = next->unitSide(); + doNothing.stackNumber = next->unitId(); + + makeAutomaticAction(next, doNothing); +} + +bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) +{ + BattleSetActiveStack bsa; + bsa.stack = stack->unitId(); + bsa.askPlayerInterface = false; + gameHandler->sendAndApply(&bsa); + + bool ret = owner->makeAutomaticBattleAction(ba); + return ret; +} + +void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) +{ + auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); + for(auto b : bl) + { + const CSpell * sp = SpellID(b->subtype).toSpell(); + if(!sp) + continue; + + const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); + const int32_t level = ((val > 3) ? (val - 3) : val); + + spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp); + //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle + battleCast.setEffectDuration(50); + battleCast.setSpellLevel(level); + spells::Target target; + + if(val > 3) + { + for(auto s : gameHandler->gameState()->curB->battleGetAllStacks()) + if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + target.emplace_back(s); + } + else + { + target.emplace_back(st); + } + battleCast.applyEffects(gameHandler->spellEnv, target, false, true); + } +} + +void BattleFlowProcessor::removeObstacle(const CObstacleInstance & obstacle) +{ + BattleObstaclesChanged obsRem; + obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&obsRem); +} + +void BattleFlowProcessor::stackTurnTrigger(const CStack *st) +{ + BattleTriggerEffect bte; + bte.stackID = st->unitId(); + bte.effect = -1; + bte.val = 0; + bte.additionalInfo = 0; + if (st->alive()) + { + //unbind + if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) + { + bool unbind = true; + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); + auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st); + + for (auto b : bl) + { + if(b->additionalInfo != CAddInfo::NONE) + { + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent + if(stack) + { + if(vstd::contains(adjacent, stack)) //binding stack is still present + unbind = false; + } + } + else + { + unbind = false; + } + } + if (unbind) + { + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::UNBIND; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + + if (st->hasBonusOfType(BonusType::POISON)) + { + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) //TODO: what if not?... + { + bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); + if (bte.val < b->val) //(negative) poison effect increases - update it + { + bte.effect = vstd::to_underlying(BonusType::POISON); + gameHandler->sendAndApply(&bte); + } + } + } + if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) + { + const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st)); + const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent); + if(opponentHero) + { + ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); + vstd::amin(manaDrained, opponentHero->mana); + if(manaDrained) + { + bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); + bte.val = manaDrained; + bte.additionalInfo = opponentHero->id.getNum(); //for sanity + gameHandler->sendAndApply(&bte); + } + } + } + if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) + { + bool fearsomeCreature = false; + for (CStack * stack : gameHandler->gameState()->curB->stacks) + { + if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) + { + fearsomeCreature = true; + break; + } + } + if (fearsomeCreature) + { + if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% + { + bte.effect = vstd::to_underlying(BonusType::FEAR); + gameHandler->sendAndApply(&bte); + } + } + } + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + int side = gameHandler->gameState()->curB->whatSide(st->unitOwner()); + if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0) + { + bool cast = false; + while(!bl.empty() && !cast) + { + auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); + auto spellID = SpellID(bonus->subtype); + const CSpell * spell = SpellID(spellID).toSpell(); + bl.remove_if([&bonus](const Bonus * b) + { + return b == bonus.get(); + }); + spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell); + parameters.setSpellLevel(bonus->val); + parameters.massive = true; + parameters.smart = true; + //todo: recheck effect level + if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) + { + cast = true; + + int cooldown = bonus->additionalInfo[0]; + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; + ssp.absolute = false; + ssp.val = cooldown; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + } + } +} + +void BattleFlowProcessor::setActiveStack(const CStack * stack) +{ + assert(stack); + + logGlobal->trace("Activating %s", stack->nodeName()); + BattleSetActiveStack sas; + sas.stack = stack->unitId(); + gameHandler->sendAndApply(&sas); +} diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h new file mode 100644 index 000000000..20d7a9a23 --- /dev/null +++ b/server/battles/BattleFlowProcessor.h @@ -0,0 +1,55 @@ +/* + * BattleFlowProcessor.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 +class CStack; +struct BattleHex; +class BattleAction; +struct CObstacleInstance; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class BattleProcessor; + +/// Controls flow of battles - battle startup actions and switching to next stack or next round after actions +class BattleFlowProcessor : boost::noncopyable +{ + BattleProcessor * owner; + CGameHandler * gameHandler; + + const CStack * getNextStack(); + + bool rollGoodMorale(const CStack * stack); + bool tryMakeAutomaticAction(const CStack * stack); + + void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void trySummonGuardians(const CStack * stack); + void tryPlaceMoats(); + void castOpeningSpells(); + void activateNextStack(); + void startNextRound(bool isFirstRound); + + void stackEnchantedTrigger(const CStack * stack); + void removeObstacle(const CObstacleInstance & obstacle); + void stackTurnTrigger(const CStack * stack); + void setActiveStack(const CStack * stack); + + void makeStackDoNothing(const CStack * next); + bool makeAutomaticAction(const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + +public: + explicit BattleFlowProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + void onBattleStarted(); + void onTacticsEnded(); + void onActionMade(const BattleAction & ba); +}; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp new file mode 100644 index 000000000..62a0be3a1 --- /dev/null +++ b/server/battles/BattleProcessor.cpp @@ -0,0 +1,250 @@ +/* + * BattleProcessor.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 "BattleProcessor.h" + +#include "BattleActionProcessor.h" +#include "BattleFlowProcessor.h" +#include "BattleResultProcessor.h" + +#include "../CGameHandler.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/TerrainHandler.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/modding/IdentifierStorage.h" + +BattleProcessor::BattleProcessor(CGameHandler * gameHandler) + : gameHandler(gameHandler) + , flowProcessor(std::make_unique(this)) + , actionsProcessor(std::make_unique(this)) + , resultProcessor(std::make_unique(this)) +{ + setGameHandler(gameHandler); +} + +BattleProcessor::BattleProcessor(): + BattleProcessor(nullptr) +{ +} + +BattleProcessor::~BattleProcessor() = default; + +void BattleProcessor::engageIntoBattle(PlayerColor player) +{ + //notify interfaces + PlayerBlocked pb; + pb.player = player; + pb.reason = PlayerBlocked::UPCOMING_BATTLE; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + gameHandler->sendAndApply(&pb); +} + +void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, + const CGTownInstance *town) //use hero=nullptr for no hero +{ + if(gameHandler->gameState()->curB) + gameHandler->gameState()->curB.dellNull(); + + engageIntoBattle(army1->tempOwner); + engageIntoBattle(army2->tempOwner); + + static const CArmedInstance *armies[2]; + armies[0] = army1; + armies[1] = army2; + static const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + + setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); + + //existing battle query for retying auto-combat + if(lastBattleQuery) + { + for(int i : {0, 1}) + { + if(heroes[i]) + { + SetMana restoreInitialMana; + restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; + restoreInitialMana.hid = heroes[i]->id; + gameHandler->sendAndApply(&restoreInitialMana); + } + } + + lastBattleQuery->bi = gameHandler->gameState()->curB; + lastBattleQuery->result = std::nullopt; + lastBattleQuery->belligerents[0] = gameHandler->gameState()->curB->sides[0].armyObject; + lastBattleQuery->belligerents[1] = gameHandler->gameState()->curB->sides[1].armyObject; + } + + auto nextBattleQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + for(int i : {0, 1}) + { + if(heroes[i]) + { + nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; + } + } + gameHandler->queries->addQuery(nextBattleQuery); + + flowProcessor->onBattleStarted(); +} + +void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) +{ + startBattlePrimary(army1, army2, tile, + army1->ID == Obj::HERO ? static_cast(army1) : nullptr, + army2->ID == Obj::HERO ? static_cast(army2) : nullptr, + creatureBank); +} + +void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) +{ + startBattleI(army1, army2, army2->visitablePos(), creatureBank); +} + +void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) +{ + const auto & t = *gameHandler->getTile(tile); + TerrainId terrain = t.terType->getId(); + if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground + terrain = ETerrainId::SAND; + + BattleField terType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator()); + if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) + terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); + + //send info about battles + BattleStart bs; + bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + + engageIntoBattle(bs.info->sides[0].color); + engageIntoBattle(bs.info->sides[1].color); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[0].color)); + bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); + + gameHandler->sendAndApply(&bs); +} + +bool BattleProcessor::checkBattleStateChanges() +{ + //check if drawbridge state need to be changes + if (gameHandler->battleGetSiegeLevel() > 0) + updateGateState(); + + //check if battle ended + if (auto result = gameHandler->battleIsFinished()) + { + setBattleResult(EBattleResult::NORMAL, *result); + return true; + } + + return false; +} + +void BattleProcessor::updateGateState() +{ + // GATE_BRIDGE - leftmost tile, located over moat + // GATE_OUTER - central tile, mostly covered by gate image + // GATE_INNER - rightmost tile, inside the walls + + // GATE_OUTER or GATE_INNER: + // - if defender moves unit on these tiles, bridge will open + // - if there is a creature (dead or alive) on these tiles, bridge will always remain open + // - blocked to attacker if bridge is closed + + // GATE_BRIDGE + // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) + // - if Force Field is cast here, bridge can't open (but can close, in any town) + // - deals moat damage to attacker if bridge is closed (fortress only) + + bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); + bool hasStackAtGateInner = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; + bool hasStackAtGateOuter = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; + bool hasStackAtGateBridge = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + BattleUpdateGateState db; + db.state = gameHandler->gameState()->curB->si.gateState; + if (gameHandler->gameState()->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) + { + db.state = EGateState::DESTROYED; + } + else if (db.state == EGateState::OPENED) + { + bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; + bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; + + if (gateCanClose) + db.state = EGateState::CLOSED; + else + db.state = EGateState::OPENED; + } + else // CLOSED or BLOCKED + { + bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; + + if (gateBlocked) + db.state = EGateState::BLOCKED; + else + db.state = EGateState::CLOSED; + } + + if (db.state != gameHandler->gameState()->curB->si.gateState) + gameHandler->sendAndApply(&db); +} + +bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) +{ + bool result = actionsProcessor->makePlayerBattleAction(player, ba); + flowProcessor->onActionMade(ba); + return result; +} + +void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +{ + resultProcessor->setBattleResult(resultType, victoriusSide); + resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); +} + +bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba) +{ + return actionsProcessor->makeAutomaticBattleAction(ba); +} + +void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) +{ + resultProcessor->endBattleConfirm(battleInfo); +} + +void BattleProcessor::battleAfterLevelUp(const BattleResult &result) +{ + resultProcessor->battleAfterLevelUp(result); +} + +void BattleProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; + + actionsProcessor->setGameHandler(newGameHandler); + flowProcessor->setGameHandler(newGameHandler); + resultProcessor->setGameHandler(newGameHandler); +} diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h new file mode 100644 index 000000000..07df198dd --- /dev/null +++ b/server/battles/BattleProcessor.h @@ -0,0 +1,80 @@ +/* + * BattleProcessor.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/GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +class BattleAction; +class int3; +class BattleInfo; +struct BattleResult; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class CBattleQuery; +class BattleActionProcessor; +class BattleFlowProcessor; +class BattleResultProcessor; + +/// Main class for battle handling. Contains all public interface for battles that is accessible from outside, e.g. for CGameHandler +class BattleProcessor : boost::noncopyable +{ + friend class BattleActionProcessor; + friend class BattleFlowProcessor; + friend class BattleResultProcessor; + + CGameHandler * gameHandler; + std::unique_ptr actionsProcessor; + std::unique_ptr flowProcessor; + std::unique_ptr resultProcessor; + + void updateGateState(); + void engageIntoBattle(PlayerColor player); + + bool checkBattleStateChanges(); + void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); + + bool makeAutomaticBattleAction(const BattleAction & ba); + + void setBattleResult(EBattleResult resultType, int victoriusSide); + +public: + explicit BattleProcessor(CGameHandler * gameHandler); + BattleProcessor(); + ~BattleProcessor(); + + void setGameHandler(CGameHandler * gameHandler); + + /// Starts battle with specified parameters + void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); + /// Starts battle between two armies (which can also be heroes) at specified tile + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); + /// Starts battle between two armies (which can also be heroes) at position of 2nd object + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); + + /// Processing of incoming battle action netpack + bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); + + /// Applies results of a battle once player agrees to them + void endBattleConfirm(const BattleInfo * battleInfo); + /// Applies results of a battle after potential levelup + void battleAfterLevelUp(const BattleResult & result); + + template void serialize(Handler &h, const int version) + { + + } + +}; + diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp new file mode 100644 index 000000000..912de4e65 --- /dev/null +++ b/server/battles/BattleResultProcessor.cpp @@ -0,0 +1,539 @@ +/* + * BattleResultProcessor.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 "BattleResultProcessor.h" + +#include "../CGameHandler.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/serializer/Cast.h" +#include "../../lib/spells/CSpellHandler.h" + +BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner) +// : owner(owner) + : gameHandler(nullptr) +{ +} + +void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): + army(battleSide.armyObject) +{ + heroWithDeadCommander = ObjectInstanceID(); + + PlayerColor color = battleSide.color; + + for(CStack * st : bat->stacks) + { + if(st->summoned) //don't take into account temporary summoned stacks + continue; + if(st->unitOwner() != color) //remove only our stacks + continue; + + logGlobal->debug("Calculating casualties for %s", st->nodeName()); + + st->health.takeResurrected(); + + if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) + { + logGlobal->debug("Ignored arrow towers stack."); + } + else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) + { + auto warMachine = st->unitType()->warMachine; + + if(warMachine == ArtifactID::NONE) + { + logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); + } + //catapult artifact remain even if "creature" killed in siege + else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) + { + logGlobal->debug("War machine has been destroyed"); + auto hero = dynamic_ptr_cast (army); + if (hero) + removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + else + logGlobal->error("War machine in army without hero"); + } + } + else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) + { + if(st->alive() && st->getCount() > 0) + { + logGlobal->debug("Permanently summoned %d units.", st->getCount()); + const CreatureID summonedType = st->creatureId(); + summoned[summonedType] += st->getCount(); + } + } + else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + if (nullptr == st->base) + { + logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); + } + else + { + auto c = dynamic_cast (st->base); + if(c) + { + auto h = dynamic_cast (army); + if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) + { + logGlobal->debug("Commander is dead."); + heroWithDeadCommander = army->id; //TODO: unify commander handling + } + } + else + logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); + } + } + else if(st->base && !army->slotEmpty(st->unitSlot())) + { + logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); + if(st->getCount() == 0 || !st->alive()) + { + logGlobal->debug("Stack has been destroyed."); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); + } + else if(st->getCount() < army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + else if(st->getCount() > army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + } + else + { + logGlobal->warn("Unable to process stack: %s", st->nodeName()); + } + } +} + +void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) +{ + for (TStackAndItsNewCount &ncount : newStackCounts) + { + if (ncount.second > 0) + gh->changeStackCount(ncount.first, ncount.second, true); + else + gh->eraseStack(ncount.first, true); + } + for (auto summoned_iter : summoned) + { + SlotID slot = army->getSlotFor(summoned_iter.first); + if (slot.validSlot()) + { + StackLocation location(army, slot); + gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); + } + else + { + //even if it will be possible to summon anything permanently it should be checked for free slot + //necromancy is handled separately + gh->complain("No free slot to put summoned creature"); + } + } + for (auto al : removedWarMachines) + { + gh->removeArtifact(al); + } + if (heroWithDeadCommander != ObjectInstanceID()) + { + SetCommanderProperty scp; + scp.heroid = heroWithDeadCommander; + scp.which = SetCommanderProperty::ALIVE; + scp.amount = 0; + gh->sendAndApply(&scp); + } +} + +FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int remainingBattleQueriesCount) +{ + assert(Query->result); + assert(Query->bi); + auto &result = *Query->result; + auto &info = *Query->bi; + + winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; + loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; + victor = info.sides[result.winner].color; + loser = info.sides[!result.winner].color; + winnerSide = result.winner; + this->remainingBattleQueriesCount = remainingBattleQueriesCount; +} + +FinishingBattleHelper::FinishingBattleHelper() +{ + winnerHero = loserHero = nullptr; + winnerSide = 0; + remainingBattleQueriesCount = 0; +} + +void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) +{ + auto const & giveExp = [](BattleResult &r) + { + if (r.winner > 1) + { + // draw + return; + } + r.exp[0] = 0; + r.exp[1] = 0; + for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) + { + r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; + } + }; + + LOG_TRACE(logGlobal); + + //Fill BattleResult structure with exp info + giveExp(*battleResult); + + if (battleResult->result == EBattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped + { + if(heroAttacker) + battleResult->exp[1] += 500; + if(heroDefender) + battleResult->exp[0] += 500; + } + + if(heroAttacker) + battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill + if(heroDefender) + battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); + + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); + if (!battleQuery) + { + logGlobal->error("Cannot find battle query!"); + gameHandler->complain("Player " + boost::lexical_cast(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!"); + return; + } + + battleQuery->result = std::make_optional(*battleResult); + + //Check how many battle gameHandler->queries were created (number of players blocked by battle) + const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; + finishingBattle = std::make_unique(battleQuery, queriedPlayers); + + // in battles against neutrals, 1st player can ask to replay battle manually + if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer()) + { + auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + battleResult->queryID = battleDialogQuery->queryID; + gameHandler->queries->addQuery(battleDialogQuery); + } + else + battleResult->queryID = -1; + + //set same battle result for all gameHandler->queries + for(auto q : gameHandler->queries->allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery) + otherBattleQuery->result = battleQuery->result; + } + + gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed + + if (battleResult->queryID == -1) + endBattleConfirm(gameHandler->gameState()->curB); +} + +void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) +{ + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battleInfo->sides.at(0).color)); + if(!battleQuery) + { + logGlobal->trace("No battle query, battle end was confirmed by another player"); + return; + } + + const EBattleResult result = battleResult.get()->result; + + CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle + ChangeSpells cs; //for Eagle Eye + + if(!finishingBattle->isDraw() && finishingBattle->winnerHero) + { + if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) + { + double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); + for(auto & spellId : battleInfo->sides.at(!battleResult->winner).usedSpellsHistory) + { + auto spell = spellId.toSpell(VLC->spells()); + if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) + cs.spells.insert(spell->getId()); + } + } + } + std::vector arts; //display them in window + + if(result == EBattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) + { + auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) + { + const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); + if(slot != ArtifactPosition::PRE_FIRST) + { + arts.push_back(art); + ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); + if(ArtifactUtils::isSlotBackpack(slot)) + ma->askAssemble = false; + gameHandler->sendAndApply(ma); + } + }; + + if (finishingBattle->loserHero) + { + //TODO: wrap it into a function, somehow (std::variant -_-) + auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig() && + art->artType->getId() != ArtifactID::SPELLBOOK) + // don't move war machines or locked arts (spellbook) + { + sendMoveArtifact(art, &ma); + } + } + for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) + { + //we assume that no big artifacts can be found + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, + ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning + const CArtifactInstance * art = ma.src.getArt(); + if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won + { + sendMoveArtifact(art, &ma); + } + } + if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? + { + artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + for (auto armySlot : battleInfo->sides.at(!battleResult->winner).armyObject->stacks) + { + auto artifactsWorn = armySlot.second->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(armySlot.second, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + + if (arts.size()) //display loot + { + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + + iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact + + for (auto art : arts) //TODO; separate function to display loot for various ojects? + { + iw.components.emplace_back( + Component::EComponentType::ARTIFACT, art->artType->getId(), + art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); + if (iw.components.size() >= 14) + { + gameHandler->sendAndApply(&iw); + iw.components.clear(); + } + } + if (iw.components.size()) + { + gameHandler->sendAndApply(&iw); + } + } + //Eagle Eye secondary skill handling + if (!cs.spells.empty()) + { + cs.learn = 1; + cs.hid = finishingBattle->winnerHero->id; + + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s + iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); + + std::ostringstream names; + for (int i = 0; i < cs.spells.size(); i++) + { + names << "%s"; + if (i < cs.spells.size() - 2) + names << ", "; + else if (i < cs.spells.size() - 1) + names << "%s"; + } + names << "."; + + iw.text.replaceRawString(names.str()); + + auto it = cs.spells.begin(); + for (int i = 0; i < cs.spells.size(); i++, it++) + { + iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); + if (i == cs.spells.size() - 2) //we just added pre-last name + iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " + iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); + } + gameHandler->sendAndApply(&iw); + gameHandler->sendAndApply(&cs); + } + cab1.updateArmy(gameHandler); + cab2.updateArmy(gameHandler); //take casualties after battle is deleted + + if(finishingBattle->loserHero) //remove beaten hero + { + RemoveObject ro(finishingBattle->loserHero->id); + gameHandler->sendAndApply(&ro); + } + if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + } + + if(battleResult->winner == BattleSide::DEFENDER + && finishingBattle->winnerHero + && finishingBattle->winnerHero->visitedTown + && !finishingBattle->winnerHero->inTownGarrison + && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) + { + gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place + } + //give exp + if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); + + BattleResultAccepted raccepted; + raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); + raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); + raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); + raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); + raccepted.heroResult[0].exp = battleResult->exp[0]; + raccepted.heroResult[1].exp = battleResult->exp[1]; + raccepted.winnerSide = finishingBattle->winnerSide; + gameHandler->sendAndApply(&raccepted); + + gameHandler->queries->popIfTop(battleQuery); + //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query +} + +void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) +{ + LOG_TRACE(logGlobal); + + if(!finishingBattle) + return; + + finishingBattle->remainingBattleQueriesCount--; + logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); + + if (finishingBattle->remainingBattleQueriesCount > 0) + //Battle results will be handled when all battle gameHandler->queries are closed + return; + + //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible + // but the battle consequences are applied after final player is unblocked. Hard to abuse... + // Still, it looks like a hole. + + // Necromancy if applicable. + const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult) : CStackBasicDescriptor(); + // Give raised units to winner and show dialog, if any were raised, + // units will be given after casualties are taken + const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); + + if (necroSlot != SlotID()) + { + finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); + gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + } + + BattleResultsApplied resultsApplied; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + gameHandler->sendAndApply(&resultsApplied); + + gameHandler->setBattle(nullptr); + + //handle victory/loss of engaged players + std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; + gameHandler->checkVictoryLossConditions(playerColors); + + if (result.result == EBattleResult::SURRENDER) + gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + + if (result.result == EBattleResult::ESCAPE) + gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + + if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() + && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + + if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) + gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); + } + + finishingBattle.reset(); +} + +void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +{ + battleResult = std::make_unique(); + battleResult->result = resultType; + battleResult->winner = victoriusSide; //surrendering side loses + gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); +} diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h new file mode 100644 index 000000000..ec4b13ab6 --- /dev/null +++ b/server/battles/BattleResultProcessor.h @@ -0,0 +1,78 @@ +/* + * BattleProcessor.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/GameConstants.h" +#include "../../lib/NetPacks.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct SideInBattle; +VCMI_LIB_NAMESPACE_END + +class CBattleQuery; +class BattleProcessor; +class CGameHandler; + +struct CasualtiesAfterBattle +{ + using TStackAndItsNewCount = std::pair; + using TSummoned = std::map; + // enum {ERASE = -1}; + const CArmedInstance * army; + std::vector newStackCounts; + std::vector removedWarMachines; + TSummoned summoned; + ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations + + CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); + void updateArmy(CGameHandler * gh); +}; + +struct FinishingBattleHelper +{ + FinishingBattleHelper(); + FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} + + const CGHeroInstance *winnerHero, *loserHero; + PlayerColor victor, loser; + ui8 winnerSide; + + int remainingBattleQueriesCount; + + template void serialize(Handler &h, const int version) + { + h & winnerHero; + h & loserHero; + h & victor; + h & loser; + h & winnerSide; + h & remainingBattleQueriesCount; + } +}; + +class BattleResultProcessor : boost::noncopyable +{ + // BattleProcessor * owner; + CGameHandler * gameHandler; + + std::unique_ptr battleResult; + std::unique_ptr finishingBattle; + +public: + explicit BattleResultProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + void setBattleResult(EBattleResult resultType, int victoriusSide); + void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle + void endBattleConfirm(const BattleInfo * battleInfo); + void battleAfterLevelUp(const BattleResult & result); +}; diff --git a/server/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp similarity index 96% rename from server/HeroPoolProcessor.cpp rename to server/processors/HeroPoolProcessor.cpp index f32a88be0..180234388 100644 --- a/server/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -10,17 +10,17 @@ #include "StdInc.h" #include "HeroPoolProcessor.h" -#include "CGameHandler.h" +#include "../CGameHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameSettings.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/gameState/TavernHeroesPool.h" -#include "../lib/gameState/TavernSlot.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameSettings.h" +#include "../../lib/NetPacks.h" +#include "../../lib/StartInfo.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/gameState/TavernHeroesPool.h" +#include "../../lib/gameState/TavernSlot.h" HeroPoolProcessor::HeroPoolProcessor() : gameHandler(nullptr) diff --git a/server/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h similarity index 100% rename from server/HeroPoolProcessor.h rename to server/processors/HeroPoolProcessor.h diff --git a/server/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp similarity index 93% rename from server/PlayerMessageProcessor.cpp rename to server/processors/PlayerMessageProcessor.cpp index 2736f68c9..75e4217fc 100644 --- a/server/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -10,18 +10,19 @@ #include "StdInc.h" #include "PlayerMessageProcessor.h" -#include "CGameHandler.h" -#include "CVCMIServer.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameConstants.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/mapObjects/CGTownInstance.h" +#include "../../lib/serializer/Connection.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameConstants.h" +#include "../../lib/NetPacks.h" +#include "../../lib/StartInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" #include "../lib/modding/IdentifierStorage.h" #include "../lib/modding/ModScope.h" @@ -212,15 +213,27 @@ void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroI gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3); } -void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero) +void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words) { if (!hero) return; - for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods + if (!words.empty()) { - if(VLC->arth->objects[g]->canBePutAt(hero)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + for (auto const & word : words) + { + auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false); + if(artID && VLC->arth->objects[*artID]) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[*artID], ArtifactPosition::FIRST_AVAILABLE); + } + } + else + { + for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods + { + if(VLC->arth->objects[g]->canBePutAt(hero)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + } } } @@ -433,7 +446,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla const auto & doCheatGiveArmyCustom = [&]() { cheatGiveArmy(player, hero, words); }; const auto & doCheatGiveArmyFixed = [&](std::vector customWords) { cheatGiveArmy(player, hero, customWords); }; const auto & doCheatGiveMachines = [&]() { cheatGiveMachines(player, hero); }; - const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero); }; + const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero, words); }; const auto & doCheatLevelup = [&]() { cheatLevelup(player, hero, words); }; const auto & doCheatExperience = [&]() { cheatExperience(player, hero, words); }; const auto & doCheatMovement = [&]() { cheatMovement(player, hero, words); }; diff --git a/server/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h similarity index 96% rename from server/PlayerMessageProcessor.h rename to server/processors/PlayerMessageProcessor.h index 2351140ca..d8f9e9878 100644 --- a/server/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/GameConstants.h" +#include "../../lib/GameConstants.h" VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; @@ -31,7 +31,7 @@ class PlayerMessageProcessor void cheatBuildTown(PlayerColor player, const CGTownInstance * town); void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero); - void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero); + void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words); diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp new file mode 100644 index 000000000..e3deeef80 --- /dev/null +++ b/server/queries/BattleQueries.cpp @@ -0,0 +1,75 @@ +/* + * BattleQueries.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 "BattleQueries.h" +#include "MapQueries.h" + +#include "../CGameHandler.h" +#include "../battles/BattleProcessor.h" + +#include "../../lib/battle/BattleInfo.h" + +void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + if(result) + objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): + CGhQuery(owner) +{ + belligerents[0] = Bi->sides[0].armyObject; + belligerents[1] = Bi->sides[1].armyObject; + + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner): + CGhQuery(owner), bi(nullptr) +{ + belligerents[0] = belligerents[1] = nullptr; +} + +bool CBattleQuery::blocksPack(const CPack * pack) const +{ + const char * name = typeid(*pack).name(); + return strcmp(name, typeid(MakeAction).name()) != 0; +} + +void CBattleQuery::onRemoval(PlayerColor color) +{ + if(result) + gh->battles->battleAfterLevelUp(*result); +} + +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): + CDialogQuery(owner) +{ + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} + +void CBattleDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + if(*answer == 1) + { + gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); + } + else + { + gh->battles->endBattleConfirm(bi); + } +} diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h new file mode 100644 index 000000000..03fce57a7 --- /dev/null +++ b/server/queries/BattleQueries.h @@ -0,0 +1,40 @@ +/* + * BattleQueries.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 "CQuery.h" + +#include "../../lib/NetPacks.h" + +class CBattleQuery : public CGhQuery +{ +public: + std::array belligerents; + std::array initialHeroMana; + + const BattleInfo *bi; + std::optional result; + + CBattleQuery(CGameHandler * owner); + CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; +}; + +class CBattleDialogQuery : public CDialogQuery +{ +public: + CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); + + const BattleInfo * bi; + + virtual void onRemoval(PlayerColor color) override; +}; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp new file mode 100644 index 000000000..b8c1e072c --- /dev/null +++ b/server/queries/CQuery.cpp @@ -0,0 +1,201 @@ +/* + * CQuery.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 "CQuery.h" + +#include "QueriesProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/serializer/Cast.h" +#include "../../lib/NetPacks.h" + +template +std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") +{ + std::string ret = opener; + auto itr = std::begin(c); + if(itr != std::end(c)) + { + ret += std::to_string(*itr); + while(++itr != std::end(c)) + { + ret += delimeter; + ret += std::to_string(*itr); + } + } + ret += closer; + return ret; +} + +std::ostream & operator<<(std::ostream & out, const CQuery & query) +{ + return out << query.toString(); +} + +std::ostream & operator<<(std::ostream & out, QueryPtr query) +{ + return out << "[" << query.get() << "] " << query->toString(); +} + +CQuery::CQuery(QueriesProcessor * Owner): + owner(Owner) +{ + boost::unique_lock l(QueriesProcessor::mx); + + static QueryID QID = QueryID(0); + + queryID = ++QID; + logGlobal->trace("Created a new query with id %d", queryID); +} + +CQuery::~CQuery() +{ + logGlobal->trace("Destructed the query with id %d", queryID); +} + +void CQuery::addPlayer(PlayerColor color) +{ + if(color.isValidPlayer()) + players.push_back(color); +} + +std::string CQuery::toString() const +{ + const auto size = players.size(); + const std::string plural = size > 1 ? "s" : ""; + std::string names; + + for(size_t i = 0; i < size; i++) + { + names += boost::to_upper_copy(players[i].getStr()); + + if(i < size - 2) + names += ", "; + else if(size > 1 && i == size - 2) + names += " and "; + } + std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") + % typeid(*this).name() + % queryID + % plural + % names + ); + return ret; +} + +bool CQuery::endsByPlayerAnswer() const +{ + return false; +} + +void CQuery::onRemoval(PlayerColor color) +{ + +} + +bool CQuery::blocksPack(const CPack * pack) const +{ + return false; +} + +void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + +} + +void CQuery::onExposure(QueryPtr topQuery) +{ + logGlobal->trace("Exposed query with id %d", queryID); + owner->popQuery(*this); +} + +void CQuery::onAdding(PlayerColor color) +{ + +} + +void CQuery::onAdded(PlayerColor color) +{ + +} + +void CQuery::setReply(const JsonNode & reply) +{ + +} + +bool CQuery::blockAllButReply(const CPack * pack) const +{ + //We accept only query replies from correct player + if(auto reply = dynamic_ptr_cast(pack)) + return !vstd::contains(players, reply->player); + + return true; +} + +CGhQuery::CGhQuery(CGameHandler * owner): + CQuery(owner->queries.get()), gh(owner) +{ + +} + +CDialogQuery::CDialogQuery(CGameHandler * owner): + CGhQuery(owner) +{ + +} + +bool CDialogQuery::endsByPlayerAnswer() const +{ + return true; +} + +bool CDialogQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +void CDialogQuery::setReply(const JsonNode & reply) +{ + if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) + answer = reply.Integer(); +} + +CGenericQuery::CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback): + CQuery(Owner), callback(Callback) +{ + addPlayer(color); +} + +bool CGenericQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +bool CGenericQuery::endsByPlayerAnswer() const +{ + return true; +} + +void CGenericQuery::onExposure(QueryPtr topQuery) +{ + //do nothing +} + +void CGenericQuery::setReply(const JsonNode & reply) +{ + this->reply = reply; +} + +void CGenericQuery::onRemoval(PlayerColor color) +{ + callback(reply); +} diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h new file mode 100644 index 000000000..0d105ca37 --- /dev/null +++ b/server/queries/CQuery.h @@ -0,0 +1,99 @@ +/* + * CQuery.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/GameConstants.h" +#include "../../lib/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CPack; + +VCMI_LIB_NAMESPACE_END + +class CObjectVisitQuery; +class QueriesProcessor; +class CQuery; + +using QueryPtr = std::shared_ptr; + +// This class represents any kind of prolonged interaction that may need to do something special after it is over. +// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. +// Examples: +// - all kinds of blocking dialog windows +// - battle +// - object visit +// - hero movement +// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. +class CQuery +{ +public: + std::vector players; //players that are affected (often "blocked") by query + QueryID queryID; + + CQuery(QueriesProcessor * Owner); + + + virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. + + virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs) + virtual void onAdding(PlayerColor color); //called just before query is pushed on stack + virtual void onAdded(PlayerColor color); //called right after query is pushed on stack + virtual void onRemoval(PlayerColor color); //called after query is removed from stack + virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top) + virtual std::string toString() const; + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; + + virtual void setReply(const JsonNode & reply); + + virtual ~CQuery(); +protected: + QueriesProcessor * owner; + void addPlayer(PlayerColor color); + bool blockAllButReply(const CPack * pack) const; +}; + +std::ostream &operator<<(std::ostream &out, const CQuery &query); +std::ostream &operator<<(std::ostream &out, QueryPtr query); + +class CGhQuery : public CQuery +{ +public: + CGhQuery(CGameHandler * owner); +protected: + CGameHandler * gh; +}; + +class CDialogQuery : public CGhQuery +{ +public: + CDialogQuery(CGameHandler * owner); + virtual bool endsByPlayerAnswer() const override; + virtual bool blocksPack(const CPack *pack) const override; + void setReply(const JsonNode & reply) override; +protected: + std::optional answer; +}; + +class CGenericQuery : public CQuery +{ +public: + CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback); + + bool blocksPack(const CPack * pack) const override; + bool endsByPlayerAnswer() const override; + void onExposure(QueryPtr topQuery) override; + void setReply(const JsonNode & reply) override; + void onRemoval(PlayerColor color) override; +private: + std::function callback; + JsonNode reply; +}; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp new file mode 100644 index 000000000..054f3526c --- /dev/null +++ b/server/queries/MapQueries.cpp @@ -0,0 +1,227 @@ +/* + * MapQueries.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 "MapQueries.h" + +#include "QueriesProcessor.h" +#include "../CGameHandler.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/serializer/Cast.h" + +CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): + CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) +{ + addPlayer(Hero->tempOwner); +} + +bool CObjectVisitQuery::blocksPack(const CPack *pack) const +{ + //During the visit itself ALL actions are blocked. + //(However, the visit may trigger a query above that'll pass some.) + return true; +} + +void CObjectVisitQuery::onRemoval(PlayerColor color) +{ + gh->objectVisitEnded(*this); + + //TODO or should it be destructor? + //Can object visit affect 2 players and what would be desired behavior? + if(removeObjectAfterVisit) + gh->removeObject(visitedObject); +} + +void CObjectVisitQuery::onExposure(QueryPtr topQuery) +{ + //Object may have been removed and deleted. + if(gh->isValidObject(visitedObject)) + topQuery->notifyObjectAboutRemoval(*this); + + owner->popIfTop(*this); +} + +void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); +} + +CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): + CDialogQuery(owner) +{ + exchangingArmies[0] = up; + exchangingArmies[1] = down; + + addPlayer(up->tempOwner); + addPlayer(down->tempOwner); +} + +bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const +{ + std::set ourIds; + ourIds.insert(this->exchangingArmies[0]->id); + ourIds.insert(this->exchangingArmies[1]->id); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); + + if(auto arts = dynamic_ptr_cast(pack)) + { + if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) + if(!vstd::contains(ourIds, *id1)) + return true; + + if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) + if(!vstd::contains(ourIds, *id2)) + return true; + return false; + } + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->id); + + if(auto arts = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); + + if(auto art = dynamic_ptr_cast(pack)) + { + if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) + return !vstd::contains(ourIds, *id); + } + + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->heroID); + + if(auto upgrade = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, upgrade->id); + + if(auto formation = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, formation->hid); + + return CDialogQuery::blocksPack(pack); +} + +void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + assert(answer); + objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); +} + +CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): + CDialogQuery(owner) +{ + this->bd = bd; + addPlayer(bd.player); +} + +void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + // do not change to dynamic_ptr_cast - SIGSEGV! + auto obj = dynamic_cast(objectVisit.visitedObject); + if(obj) + obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); + else + logGlobal->error("Invalid instance in teleport query"); +} + +CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): + CDialogQuery(owner) +{ + this->td = td; + addPlayer(td.player); +} + +CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + hlu = Hlu; + addPlayer(hero->tempOwner); +} + +void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); + gh->levelUpHero(hero, hlu.skills[*answer]); +} + +void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + clu = Clu; + addPlayer(hero->tempOwner); +} + +void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); + gh->levelUpCommander(hero->commander, clu.skills[*answer]); +} + +void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): + CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) +{ + players.push_back(hero->tempOwner); +} + +void CHeroMovementQuery::onExposure(QueryPtr topQuery) +{ + assert(players.size() == 1); + + if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard + //TODO what if there were H4-like escape? we should also check pos + { + logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); + //finish movement + visitDestAfterVictory = false; + gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); + } + + owner->popIfTop(*this); +} + +void CHeroMovementQuery::onRemoval(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; + gh->sendAndApply(&pb); +} + +void CHeroMovementQuery::onAdding(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + gh->sendAndApply(&pb); +} diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h new file mode 100644 index 000000000..c89be802d --- /dev/null +++ b/server/queries/MapQueries.h @@ -0,0 +1,103 @@ +/* + * MapQueries.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 "CQuery.h" + +#include "../../lib/NetPacks.h" + +//Created when hero visits object. +//Removed when query above is resolved (or immediately after visit if no queries were created) +class CObjectVisitQuery : public CGhQuery +{ +public: + const CGObjectInstance *visitedObject; + const CGHeroInstance *visitingHero; + int3 tile; //may be different than hero pos -> eg. visit via teleport + bool removeObjectAfterVisit; + + CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); + + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; + virtual void onExposure(QueryPtr topQuery) override; +}; + +//Created when hero attempts move and something happens +//(not necessarily position change, could be just an object interaction). +class CHeroMovementQuery : public CGhQuery +{ +public: + TryMoveHero tmh; + bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated + const CGHeroInstance *hero; + + virtual void onExposure(QueryPtr topQuery) override; + + CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); + virtual void onAdding(PlayerColor color) override; + virtual void onRemoval(PlayerColor color) override; +}; + + +class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs +{ +public: + std::array exchangingArmies; + + CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; +}; + +//yes/no and component selection dialogs +class CBlockingDialogQuery : public CDialogQuery +{ +public: + BlockingDialog bd; //copy of pack... debug purposes + + CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class CTeleportDialogQuery : public CDialogQuery +{ +public: + TeleportDialog td; //copy of pack... debug purposes + + CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class CHeroLevelUpDialogQuery : public CDialogQuery +{ +public: + CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + HeroLevelUp hlu; + const CGHeroInstance * hero; +}; + +class CCommanderLevelUpDialogQuery : public CDialogQuery +{ +public: + CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + CommanderLevelUp clu; + const CGHeroInstance * hero; +}; diff --git a/server/queries/QueriesProcessor.cpp b/server/queries/QueriesProcessor.cpp new file mode 100644 index 000000000..259e33a3e --- /dev/null +++ b/server/queries/QueriesProcessor.cpp @@ -0,0 +1,129 @@ +/* + * CQuery.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 "QueriesProcessor.h" + +#include "CQuery.h" + +boost::mutex QueriesProcessor::mx; + +void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); + if(topQuery(player) != query) + { + logGlobal->trace("Cannot remove, not a top!"); + return; + } + + queries[player] -= query; + auto nextQuery = topQuery(player); + + query->onRemoval(player); + + //Exposure on query below happens only if removal didn't trigger any new query + if(nextQuery && nextQuery == topQuery(player)) + nextQuery->onExposure(query); +} + +void QueriesProcessor::popQuery(const CQuery &query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); + + assert(query.players.size()); + for(auto player : query.players) + { + auto top = topQuery(player); + if(top.get() == &query) + popQuery(top); + else + { + logGlobal->trace("Cannot remove query %s", query.toString()); + logGlobal->trace("Queries found:"); + for(auto q : queries[player]) + { + logGlobal->trace(q->toString()); + } + } + } +} + +void QueriesProcessor::popQuery(QueryPtr query) +{ + for(auto player : query->players) + popQuery(player, query); +} + +void QueriesProcessor::addQuery(QueryPtr query) +{ + for(auto player : query->players) + addQuery(player, query); + + for(auto player : query->players) + query->onAdded(player); +} + +void QueriesProcessor::addQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); + query->onAdding(player); + queries[player].push_back(query); +} + +QueryPtr QueriesProcessor::topQuery(PlayerColor player) +{ + return vstd::backOrNull(queries[player]); +} + +void QueriesProcessor::popIfTop(QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); + if(!query) + logGlobal->error("The query is nullptr! Ignoring."); + + popIfTop(*query); +} + +void QueriesProcessor::popIfTop(const CQuery & query) +{ + for(PlayerColor color : query.players) + if(topQuery(color).get() == &query) + popQuery(color, topQuery(color)); +} + +std::vector> QueriesProcessor::allQueries() const +{ + std::vector> ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +std::vector QueriesProcessor::allQueries() +{ + //TODO code duplication with const function :( + std::vector ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +QueryPtr QueriesProcessor::getQuery(QueryID queryID) +{ + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + if(query->queryID == queryID) + return query; + return nullptr; +} diff --git a/server/queries/QueriesProcessor.h b/server/queries/QueriesProcessor.h new file mode 100644 index 000000000..d0fd6df35 --- /dev/null +++ b/server/queries/QueriesProcessor.h @@ -0,0 +1,40 @@ +/* + * QueriesProcessor.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/GameConstants.h" + +class CQuery; +using QueryPtr = std::shared_ptr; + +class QueriesProcessor +{ +private: + void addQuery(PlayerColor player, QueryPtr query); + void popQuery(PlayerColor player, QueryPtr query); + + std::map> queries; //player => stack of queries + +public: + static boost::mutex mx; + + void addQuery(QueryPtr query); + void popQuery(const CQuery &query); + void popQuery(QueryPtr query); + void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) + void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) + + QueryPtr topQuery(PlayerColor player); + + std::vector> allQueries() const; + std::vector allQueries(); + QueryPtr getQuery(QueryID queryID); + //void removeQuery +};