Przeglądaj źródła

Start of stabilization - battles now start correctly

Ivan Savenko 2 lat temu
rodzic
commit
6297140bf5

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -292,7 +292,7 @@ void CBattleAI::activeStack( const CStack * stack )
 			//spellcast may finish battle or kill active stack
 			//send special preudo-action
 			BattleAction cancel;
-			cancel.actionType = EActionType::CANCEL;
+			cancel.actionType = EActionType::NO_ACTION;
 			cb->battleMakeUnitAction(cancel);
 			return;
 		}

+ 1 - 1
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);
 }
 

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -401,7 +401,7 @@ void BattleStacksController::stackRemoved(uint32_t stackID)
 	{
 		BattleAction action;
 		action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
-		action.actionType = EActionType::CANCEL;
+		action.actionType = EActionType::NO_ACTION;
 		action.stackNumber = getActiveStack()->unitId();
 
 		LOCPLINT->cb->battleMakeUnitAction(action);

+ 0 - 1
lib/GameConstants.cpp

@@ -248,7 +248,6 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType)
 	static const std::map<EActionType, std::string> actionTypeToString =
 	{
 		{EActionType::END_TACTIC_PHASE, "End tactic phase"},
-		{EActionType::INVALID, "Invalid"},
 		{EActionType::NO_ACTION, "No action"},
 		{EActionType::HERO_SPELL, "Hero spell"},
 		{EActionType::WALK, "Walk"},

+ 8 - 7
lib/GameConstants.h

@@ -1000,18 +1000,19 @@ namespace Date
 
 enum class EActionType : int32_t
 {
-	CANCEL = -3,
-	END_TACTIC_PHASE = -2,
-	INVALID = -1,
-	NO_ACTION = 0,
+	NO_ACTION,
+
+	END_TACTIC_PHASE,
+	RETREAT,
+	SURRENDER,
+
 	HERO_SPELL,
+
 	WALK,
+	WAIT,
 	DEFEND,
-	RETREAT,
-	SURRENDER,
 	WALK_AND_ATTACK,
 	SHOOT,
-	WAIT,
 	CATAPULT,
 	MONSTER_SPELL,
 	BAD_MORALE,

+ 0 - 1
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) {}

+ 0 - 18
lib/NetPacks.h

@@ -2513,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 <typename Handler> void serialize(Handler & h, const int version)
-	{
-		h & static_cast<CPackForServer &>(*this);
-		h & ba;
-	}
-};
-
 struct DLL_LINKAGE DigWithHero : public CPackForServer
 {
 	ObjectInstanceID id; //digging hero id

+ 0 - 5
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);

+ 1 - 1
lib/battle/BattleAction.cpp

@@ -20,7 +20,7 @@ static const int32_t INVALID_UNIT_ID = -1000;
 BattleAction::BattleAction():
 	side(-1),
 	stackNumber(-1),
-	actionType(EActionType::INVALID),
+	actionType(EActionType::NO_ACTION),
 	actionSubtype(-1)
 {
 }

+ 0 - 1
lib/registerTypes/RegisterTypes.h

@@ -352,7 +352,6 @@ void registerTypesServerPacks(Serializer &s)
 	s.template registerType<CPackForServer, BuildBoat>();
 	s.template registerType<CPackForServer, QueryReply>();
 	s.template registerType<CPackForServer, MakeAction>();
-	s.template registerType<CPackForServer, MakeCustomAction>();
 	s.template registerType<CPackForServer, DigWithHero>();
 	s.template registerType<CPackForServer, CastAdvSpell>();
 	s.template registerType<CPackForServer, CastleTeleportHero>();

+ 0 - 8
server/NetPacksServer.cpp

@@ -288,14 +288,6 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 	result = gh.battles->makeBattleAction(pack.player, pack.ba);
 }
 
-void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack)
-{
-	if (!gh.hasPlayerAt(pack.player, pack.c))
-		gh.throwAndComplain(&pack, "No such pack.player!");
-
-	result = gh.battles->makeCustomAction(pack.player, pack.ba);
-}
-
 void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack)
 {
 	gh.throwOnWrongOwner(&pack, pack.id);

+ 1 - 2
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;
-};
+};

+ 439 - 393
server/battles/BattleActionProcessor.cpp

@@ -51,484 +51,530 @@ void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler)
 	gameHandler = newGameHandler;
 }
 
-bool BattleActionProcessor::makeBattleAction(BattleAction &ba)
+bool BattleActionProcessor::doEmptyAction(const BattleAction & ba)
 {
-	bool ok = true;
+	return true;
+}
 
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba)
+{
+	return true;
+}
 
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack
+bool BattleActionProcessor::doWaitAction(const BattleAction & ba)
+{
+	return true;
+}
 
-	const bool isAboutActiveStack = stack && (ba.stackNumber == gameHandler->gameState()->curB->getActiveStackID());
+bool BattleActionProcessor::doBadMoraleAction(const BattleAction & ba)
+{
+	return true;
+}
 
-	logGlobal->trace("Making action: %s", ba.toString());
+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;
+	}
 
-	switch(ba.actionType)
+	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)
 	{
-	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:
+		gameHandler->complain("Cannot surrender!");
+		return false;
+	}
 
-		if (!stack)
-		{
-			gameHandler->complain("No such stack!");
-			return false;
-		}
-		if (!stack->alive())
-		{
-			gameHandler->complain("This stack is dead: " + stack->nodeName());
-			return false;
-		}
+	if (gameHandler->getResource(player, EGameResID::GOLD) < cost)
+	{
+		gameHandler->complain("Not enough gold to surrender!");
+		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 (!isAboutActiveStack)
-		{
-			gameHandler->complain("Action has to be about active stack!");
-			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;
 	}
 
-	static EndAction end_action;
-	auto wrapAction = [this](BattleAction &ba)
+	const CSpell * s = SpellID(ba.actionSubtype).toSpell();
+	if (!s)
 	{
-		StartAction startAction(ba);
-		gameHandler->sendAndApply(&startAction);
+		logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype);
+		return false;
+	}
 
-		return vstd::makeScopeGuard([&]()
-		{
-			gameHandler->sendAndApply(&end_action);
-		});
-	};
+	spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s);
 
-	switch(ba.actionType)
+	spells::detail::ProblemImpl problem;
+
+	auto m = s->battleMechanics(&parameters);
+
+	if(!m->canBeCast(problem))//todo: should we check aimed cast?
 	{
-	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)
-			{
-				gameHandler->complain("Destination required for move action.");
-				ok = false;
-				break;
-			}
-			int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move
-			if (!walkedTiles)
-				gameHandler->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<Bonus>(defenseBonusToAdd));
-			defence.push_back(std::make_shared<Bonus>(bonus2));
-
-			int difference = defence.totalValue() - oldDefenceValue;
-			std::vector<Bonus> 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);
-			}
+		logGlobal->warn("Spell cannot be cast!");
+		std::vector<std::string> texts;
+		problem.getAll(texts);
+		for(auto s : texts)
+			logGlobal->warn(s);
+		return false;
+	}
 
-			buffer.push_back(bonus2);
+	StartAction start_action(ba);
+	gameHandler->sendAndApply(&start_action); //start spell casting
 
-			sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer));
-			gameHandler->sendAndApply(&sse);
+	parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB));
 
-			BattleLogMessage message;
+	EndAction end_action;
+	gameHandler->sendAndApply(&end_action);
+	return true;
+}
 
-			MetaString text;
-			stack->addText(text, EMetaText::GENERAL_TXT, 120);
-			stack->addNameReplacement(text);
-			text.replaceNumber(difference);
+bool BattleActionProcessor::doWalkAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
 
-			message.lines.push_back(text);
+	if (!canStackAct(stack))
+		return false;
 
-			gameHandler->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 (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color))
-				gameHandler->complain("Cannot retreat!");
-			else
-				owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); //surrendering side loses
-			break;
-		}
-	case EActionType::SURRENDER:
-		{
-			PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color;
-			int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player);
-			if (cost < 0)
-				gameHandler->complain("Cannot surrender!");
-			else if (gameHandler->getResource(player, EGameResID::GOLD) < cost)
-				gameHandler->complain("Not enough gold to surrender!");
-			else
-			{
-				gameHandler->giveResource(player, EGameResID::GOLD, -cost);
-				owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); //surrendering side loses
-			}
-			break;
-		}
-	case EActionType::WALK_AND_ATTACK: //walk or attack
-		{
-			auto wrapper = wrapAction(ba);
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for move action.");
+		return false;
+	}
 
-			if(!stack)
-			{
-				gameHandler->complain("No attacker");
-				ok = false;
-				break;
-			}
+	int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move
+	if (!walkedTiles)
+	{
+		gameHandler->complain("Stack failed movement!");
+		return false;
+	}
+	return true;
+}
 
-			if(target.size() < 2)
-			{
-				gameHandler->complain("Two destinations required for attack action.");
-				ok = false;
-				break;
-			}
+bool BattleActionProcessor::doDefendAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
 
-			BattleHex attackPos = target.at(0).hexValue;
-			BattleHex destinationTile = target.at(1).hexValue;
-			const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true);
+	if (!canStackAct(stack))
+		return false;
 
-			if(!destinationStack)
-			{
-				gameHandler->complain("Invalid target to attack");
-				ok = false;
-				break;
-			}
+	//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);
 
-			BattleHex startingPos = stack->getPosition();
-			int distance = moveStack(ba.stackNumber, attackPos);
+	BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE));
+	int oldDefenceValue = defence.totalValue();
 
-			logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName());
+	defence.push_back(std::make_shared<Bonus>(defenseBonusToAdd));
+	defence.push_back(std::make_shared<Bonus>(bonus2));
 
-			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;
-			}
+	int difference = defence.totalValue() - oldDefenceValue;
+	std::vector<Bonus> 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);
+	}
 
-			if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check
-			{
-				destinationStack = nullptr;
-			}
+	buffer.push_back(bonus2);
 
-			if(!destinationStack)
-			{
-				gameHandler->complain("Unit can not attack itself");
-				ok = false;
-				break;
-			}
+	sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer));
+	gameHandler->sendAndApply(&sse);
 
-			if(!CStack::isMeleeAttackPossible(stack, destinationStack))
-			{
-				gameHandler->complain("Attack cannot be performed!");
-				ok = false;
-				break;
-			}
+	BattleLogMessage message;
 
-			//attack
-			int totalAttacks = stack->totalAttacks.getMeleeValue();
+	MetaString text;
+	stack->addText(text, EMetaText::GENERAL_TXT, 120);
+	stack->addNameReplacement(text);
+	text.replaceNumber(difference);
 
-			//TODO: move to CUnitState
-			const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
-			if(attackingHero)
-			{
-				totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
-			}
+	message.lines.push_back(text);
 
+	gameHandler->sendAndApply(&message);
+	return true;
+}
 
-			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);
-				}
+bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
 
-				//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
-				}
+	if (!canStackAct(stack))
+		return false;
 
-				//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);
-				}
-			}
+	if(target.size() < 2)
+	{
+		gameHandler->complain("Two destinations required for attack action.");
+		return false;
+	}
 
-			//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)
-			{
-				gameHandler->complain("Destination required for shot action.");
-				ok = false;
-				break;
-			}
+	BattleHex attackPos = target.at(0).hexValue;
+	BattleHex destinationTile = target.at(1).hexValue;
+	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true);
 
-			auto destination = target.at(0).hexValue;
+	if(!destinationStack)
+	{
+		gameHandler->complain("Invalid target to attack");
+		return false;
+	}
 
-			const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination);
+	BattleHex startingPos = stack->getPosition();
+	int distance = moveStack(ba.stackNumber, attackPos);
 
-			if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination))
-			{
-				gameHandler->complain("Cannot shoot!");
-				break;
-			}
-			if (!destinationStack)
-			{
-				gameHandler->complain("No target to shoot!");
-				break;
-			}
+	logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName());
 
-			auto wrapper = wrapAction(ba);
+	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;
+	}
 
-			makeAttack(stack, destinationStack, 0, destination, true, true, false);
+	if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check
+	{
+		destinationStack = nullptr;
+	}
 
-			//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
+	if(!destinationStack)
+	{
+		gameHandler->complain("Unit can not attack itself");
+		return false;
+	}
 
-			int totalRangedAttacks = stack->totalAttacks.getRangedValue();
+	if(!CStack::isMeleeAttackPossible(stack, destinationStack))
+	{
+		gameHandler->complain("Attack cannot be performed!");
+		return false;
+	}
 
-			//TODO: move to CUnitState
-			const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
-			if(attackingHero)
-			{
-				totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
-			}
+	//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());
+	}
 
-			for(int i = 1; i < totalRangedAttacks; ++i)
-			{
-				if(
-					stack->alive()
-					&& destinationStack->alive()
-					&& stack->shots.canUse()
-					)
-				{
-					makeAttack(stack, destinationStack, 0, destination, false, true, false);
-				}
-			}
-			break;
+	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);
 		}
-	case EActionType::CATAPULT:
+
+		//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())
 		{
-			auto wrapper = wrapAction(ba);
-			const CStack * shooter = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
-			std::shared_ptr<const Bonus> 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, 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(gameHandler->spellEnv, target);
-			}
-			//finish by scope guard
-			break;
+			makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack
 		}
-		case EActionType::STACK_HEAL: //healing with First Aid Tent
+
+		//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())
 		{
-			auto wrapper = wrapAction(ba);
-			const CStack * healer = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+			makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true);
+		}
+	}
 
-			if(target.size() < 1)
-			{
-				gameHandler->complain("Destination required for heal action.");
-				ok = false;
-				break;
-			}
+	//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);
 
-			const battle::Unit * destStack = nullptr;
-			std::shared_ptr<const Bonus> healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER));
+	if (!canStackAct(stack))
+		return false;
 
-			if(target.at(0).unitValue)
-				destStack = target.at(0).unitValue;
-			else
-				destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue);
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for shot action.");
+		return false;
+	}
 
-			if(healer == 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, 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(gameHandler->spellEnv, {dest});
-			}
-			break;
-		}
-		case EActionType::MONSTER_SPELL:
-		{
-			auto wrapper = wrapAction(ba);
+	auto destination = target.at(0).hexValue;
 
-			const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
-			SpellID spellID = SpellID(ba.actionSubtype);
+	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination);
 
-			std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER));
-			std::shared_ptr<const Bonus> spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID));
+	if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination))
+	{
+		gameHandler->complain("Cannot shoot!");
+		return false;
+	}
 
-			//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 (!destinationStack)
+	{
+		gameHandler->complain("No target to shoot!");
+		return false;
+	}
 
-			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);
-			}
-			break;
+	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);
 		}
 	}
 
-	if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL)
-		gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack);
+	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);
 
-	return ok;
+	if (!canStackAct(stack))
+		return false;
+
+	std::shared_ptr<const Bonus> 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::makeCustomAction(BattleAction & ba)
+bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba)
 {
-	switch(ba.actionType)
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	SpellID spellID = SpellID(ba.actionSubtype);
+
+	if (!canStackAct(stack))
+		return false;
+
+	std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER));
+	std::shared_ptr<const Bonus> 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
 	{
-	case EActionType::HERO_SPELL:
-		{
-			const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
-			if (!h)
-			{
-				logGlobal->error("Wrong caster!");
-				return false;
-			}
+		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;
+}
 
-			const CSpell * s = SpellID(ba.actionSubtype).toSpell();
-			if (!s)
-			{
-				logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype);
-				return false;
-			}
+bool BattleActionProcessor::doHealAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
 
-			spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s);
+	if (!canStackAct(stack))
+		return false;
 
-			spells::detail::ProblemImpl problem;
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for heal action.");
+		return false;
+	}
 
-			auto m = s->battleMechanics(&parameters);
+	const battle::Unit * destStack = nullptr;
+	std::shared_ptr<const Bonus> healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER));
 
-			if(!m->canBeCast(problem))//todo: should we check aimed cast?
-			{
-				logGlobal->warn("Spell cannot be cast!");
-				std::vector<std::string> texts;
-				problem.getAll(texts);
-				for(auto s : texts)
-					logGlobal->warn(s);
-				return false;
-			}
+	if(target.at(0).unitValue)
+		destStack = target.at(0).unitValue;
+	else
+		destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue);
 
-			StartAction start_action(ba);
-			gameHandler->sendAndApply(&start_action); //start spell casting
+	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;
+}
 
-			parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB));
+bool BattleActionProcessor::canStackAct(const CStack * stack)
+{
+	const bool isAboutActiveStack = stack->unitId() == gameHandler->gameState()->curB->getActiveStackID();
 
-			EndAction end_action;
-			gameHandler->sendAndApply(&end_action);
-			return true;
+	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 (!isAboutActiveStack)
+	{
+		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::BAD_MORALE:
+			return doBadMoraleAction(ba);
+		case EActionType::STACK_HEAL:
+			return doHealAction(ba);
+	}
+	gameHandler->complain("Unrecognized action type received!!");
 	return false;
 }
 
+bool BattleActionProcessor::makeBattleAction(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),
-		*stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest);
+	const CStack *curStack = gameHandler->battleGetStackByID(stack);
+	const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest);
 
 	assert(curStack);
 	assert(dest < GameConstants::BFIELD_SIZE);

+ 23 - 4
server/battles/BattleActionProcessor.h

@@ -13,7 +13,6 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 struct BattleLogMessage;
 struct BattleAttack;
-class BattleProcessor;
 class BattleAction;
 struct BattleHex;
 class CStack;
@@ -27,6 +26,7 @@ class CUnitState;
 VCMI_LIB_NAMESPACE_END
 
 class CGameHandler;
+class BattleProcessor;
 
 class BattleActionProcessor : boost::noncopyable
 {
@@ -48,11 +48,30 @@ class BattleActionProcessor : boost::noncopyable
 	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 doBadMoraleAction(const BattleAction & ba);
+	bool doHealAction(const BattleAction & ba);
+
+	bool dispatchBattleAction(const BattleAction & ba);
+
 public:
-	BattleActionProcessor(BattleProcessor * owner);
+	explicit BattleActionProcessor(BattleProcessor * owner);
 	void setGameHandler(CGameHandler * newGameHandler);
 
-	bool makeBattleAction(BattleAction &ba);
-	bool makeCustomAction(BattleAction &ba);
+	bool makeBattleAction(const BattleAction &ba);
+
 };
 

+ 57 - 30
server/battles/BattleFlowProcessor.cpp

@@ -290,21 +290,35 @@ void BattleFlowProcessor::activateNextStack()
 	//TODO: activate next round if next == nullptr
 	const auto & curB = *gameHandler->gameState()->curB;
 
-	const CStack * next = getNextStack();
+	// Find next stack that requires manual control
+	for (;;)
+	{
+		const CStack * next = getNextStack();
 
-	if (!next)
-		return;
+		if (!next)
+			return;
 
-	BattleUnitsChanged removeGhosts;
+		BattleUnitsChanged removeGhosts;
 
-	for(auto stack : curB.stacks)
-	{
-		if(stack->ghostPending)
-			removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE);
-	}
+		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(!removeGhosts.changedStacks.empty())
-		gameHandler->sendAndApply(&removeGhosts);
+		if (!tryMakeAutomaticAction(next))
+		{
+			logGlobal->trace("Activating %s", next->nodeName());
+			auto nextId = next->unitId();
+			BattleSetActiveStack sas;
+			sas.stack = nextId;
+			gameHandler->sendAndApply(&sas);
+			break;
+		}
+	}
 }
 
 bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
@@ -423,7 +437,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 			return s->unitOwner() == next->unitOwner() && s->canBeHealed();
 		});
 
-		if (!possibleStacks.size())
+		if (possibleStacks.empty())
 		{
 			makeStackDoNothing(next);
 			return true;
@@ -452,25 +466,11 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 		makeStackDoNothing(next); //end immediately if stack was affected by fear
 		return true;
 	}
-	else
-	{
-		logGlobal->trace("Activating %s", next->nodeName());
-		auto nextId = next->unitId();
-		BattleSetActiveStack sas;
-		sas.stack = nextId;
-		gameHandler->sendAndApply(&sas);
-		return false;
-	}
+	return false;
 }
 
-void BattleFlowProcessor::onActionMade(const CStack *next)
+bool BattleFlowProcessor::rollGoodMorale(const CStack * next)
 {
-	//we're after action, all results applied
-	owner->checkBattleStateChanges(); //check if this action ended the battle
-
-	if(next == nullptr)
-		return;
-
 	//check for good morale
 	auto nextStackMorale = next->moraleVal();
 	if(    !next->hadMorale
@@ -491,11 +491,38 @@ void BattleFlowProcessor::onActionMade(const CStack *next)
 			bte.val = 1;
 			bte.additionalInfo = 0;
 			gameHandler->sendAndApply(&bte); //play animation
+			return true;
 		}
 	}
+	return false;
+}
+
+void BattleFlowProcessor::onActionMade(const BattleAction &ba)
+{
+	const CStack * next = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+
+	//we're after action, all results applied
+	owner->checkBattleStateChanges(); //check if this action ended the battle
+
+	if(next == nullptr)
+		return;
+
+	bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER|| ba.actionType ==EActionType::RETREAT;
+
+	if (heroAction && next->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
+		return;
+	}
 
-	if (gameHandler->gameLobby()->state != EServerState::SHUTDOWN)
-		owner->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1));
+	if (rollGoodMorale(next))
+	{
+		// Good morale - same stack makes 2nd turn
+		return;
+	}
+
+	activateNextStack();
 }
 
 void BattleFlowProcessor::makeStackDoNothing(const CStack * next)

+ 3 - 2
server/battles/BattleFlowProcessor.h

@@ -26,6 +26,7 @@ class BattleFlowProcessor : boost::noncopyable
 
 	const CStack * getNextStack();
 
+	bool rollGoodMorale(const CStack * stack);
 	bool tryMakeAutomaticAction(const CStack * stack);
 
 	void summonGuardiansHelper(std::vector<BattleHex> & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex);
@@ -40,10 +41,10 @@ class BattleFlowProcessor : boost::noncopyable
 	bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
 
 public:
-	BattleFlowProcessor(BattleProcessor * owner);
+	explicit BattleFlowProcessor(BattleProcessor * owner);
 	void setGameHandler(CGameHandler * newGameHandler);
 
 	void onBattleStarted();
 	void onTacticsEnded();
-	void onActionMade(const CStack *stack);
+	void onActionMade(const BattleAction &ba);
 };

+ 6 - 44
server/battles/BattleProcessor.cpp

@@ -270,58 +270,20 @@ bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba)
 			return false;
 	}
 
-	return actionsProcessor->makeBattleAction(ba);
-}
-
-bool BattleProcessor::makeCustomAction(PlayerColor player, BattleAction &ba)
-{
-	const BattleInfo * b = gameHandler->gameState()->curB;
-
-	if(!b && 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(b->tacticDistance)
-	{
-		gameHandler->complain("Can not cast spell during tactics mode!");
-		return false;
-	}
-
-	auto active = b->battleActiveUnit();
-	if(!active && gameHandler->complain("No active unit in battle!"))
-		return false;
-
-	auto unitOwner = b->battleGetOwner(active);
-
-	if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!"))
-		return false;
-
-	if(ba.actionType != EActionType::HERO_SPELL && gameHandler->complain("Invalid custom action type!"))
-		return false;
-
-	return actionsProcessor->makeCustomAction(ba);
+	return makeBattleAction(ba);
 }
 
 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::makeBattleAction(BattleAction &ba)
-{
-	return actionsProcessor->makeBattleAction(ba);
-}
-
-bool BattleProcessor::makeCustomAction(BattleAction &ba)
-{
-	return actionsProcessor->makeCustomAction(ba);
-}
-
-void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2)
+bool BattleProcessor::makeBattleAction(const BattleAction &ba)
 {
-	resultProcessor->endBattle(tile, hero1, hero2);
+	bool result = actionsProcessor->makeBattleAction(ba);
+	flowProcessor->onActionMade(ba);
+	return result;
 }
 
 void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo)

+ 1 - 4
server/battles/BattleProcessor.h

@@ -44,8 +44,7 @@ class BattleProcessor : boost::noncopyable
 	void checkBattleStateChanges();
 	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
 
-	bool makeBattleAction(BattleAction &ba);
-	bool makeCustomAction(BattleAction &ba);
+	bool makeBattleAction(const BattleAction &ba);
 
 	void setBattleResult(EBattleResult resultType, int victoriusSide);
 public:
@@ -60,9 +59,7 @@ public:
 	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
 
 	bool makeBattleAction(PlayerColor player, BattleAction &ba);
-	bool makeCustomAction(PlayerColor player, BattleAction &ba);
 
-	void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle
 	void endBattleConfirm(const BattleInfo * battleInfo);
 	void battleAfterLevelUp(const BattleResult &result);
 

+ 2 - 3
server/battles/BattleResultProcessor.cpp

@@ -39,8 +39,8 @@
 #include "../../lib/spells/Problem.h"
 
 BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner)
-	: owner(owner)
-	, gameHandler(nullptr)
+//	: owner(owner)
+	: gameHandler(nullptr)
 {
 }
 
@@ -550,5 +550,4 @@ void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victor
 	battleResult->result = resultType;
 	battleResult->winner = victoriusSide; //surrendering side loses
 	gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties);
-
 }

+ 2 - 2
server/battles/BattleResultProcessor.h

@@ -61,14 +61,14 @@ struct FinishingBattleHelper
 
 class BattleResultProcessor : boost::noncopyable
 {
-	BattleProcessor * owner;
+//	BattleProcessor * owner;
 	CGameHandler * gameHandler;
 
 	std::unique_ptr<BattleResult> battleResult;
 	std::unique_ptr<FinishingBattleHelper> finishingBattle;
 
 public:
-	BattleResultProcessor(BattleProcessor * owner);
+	explicit BattleResultProcessor(BattleProcessor * owner);
 	void setGameHandler(CGameHandler * newGameHandler);
 
 	void setBattleResult(EBattleResult resultType, int victoriusSide);

+ 1 - 1
server/queries/BattleQueries.cpp

@@ -43,7 +43,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner):
 bool CBattleQuery::blocksPack(const CPack * pack) const
 {
 	const char * name = typeid(*pack).name();
-	return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name());
+	return strcmp(name, typeid(MakeAction).name()) != 0;
 }
 
 void CBattleQuery::onRemoval(PlayerColor color)