2
0
Эх сурвалжийг харах

Merge pull request #323 from vcmi/CStackTweaks

CStack tweaks
ArseniyShestakov 8 жил өмнө
parent
commit
ea0ceb1805
39 өөрчлөгдсөн 1499 нэмэгдсэн , 763 устгасан
  1. 4 8
      AI/BattleAI/AttackPossibility.cpp
  2. 1 1
      AI/BattleAI/BattleAI.cpp
  3. 59 49
      client/CPlayerInterface.cpp
  4. 2 2
      client/NetPacksClient.cpp
  5. 33 58
      client/battle/CBattleInterface.cpp
  6. 1 1
      client/battle/CBattleInterface.h
  7. 6 5
      client/battle/CBattleInterfaceClasses.cpp
  8. 23 18
      client/windows/CCreatureWindow.cpp
  9. 1 1
      client/windows/CSpellWindow.cpp
  10. 12 0
      lib/CGeneralTextHandler.cpp
  11. 4 2
      lib/CGeneralTextHandler.h
  12. 508 160
      lib/CStack.cpp
  13. 182 36
      lib/CStack.h
  14. 13 0
      lib/GameConstants.h
  15. 23 0
      lib/HeroBonus.cpp
  16. 16 0
      lib/HeroBonus.h
  17. 11 18
      lib/NetPacks.h
  18. 19 0
      lib/NetPacksBase.h
  19. 85 96
      lib/NetPacksLib.cpp
  20. 3 6
      lib/battle/BattleAttackInfo.cpp
  21. 3 2
      lib/battle/BattleAttackInfo.h
  22. 14 25
      lib/battle/BattleInfo.cpp
  23. 0 1
      lib/battle/BattleInfo.h
  24. 14 35
      lib/battle/CBattleInfoCallback.cpp
  25. 0 2
      lib/battle/CBattleInfoCallback.h
  26. 10 1
      lib/battle/CBattleInfoEssentials.cpp
  27. 1 0
      lib/battle/CBattleInfoEssentials.h
  28. 2 3
      lib/mapObjects/CGHeroInstance.cpp
  29. 1 1
      lib/mapObjects/MiscObjects.cpp
  30. 1 1
      lib/serializer/CSerializer.h
  31. 57 39
      lib/spells/BattleSpellMechanics.cpp
  32. 4 8
      lib/spells/BattleSpellMechanics.h
  33. 24 54
      lib/spells/CDefaultSpellMechanics.cpp
  34. 3 20
      lib/spells/CSpellHandler.cpp
  35. 2 2
      lib/spells/CreatureSpellMechanics.cpp
  36. 132 108
      server/CGameHandler.cpp
  37. 1 0
      test/CMakeLists.txt
  38. 1 0
      test/Test.cbp
  39. 223 0
      test/battle/CHealthTest.cpp

+ 4 - 8
AI/BattleAI/AttackPossibility.cpp

@@ -29,7 +29,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo
 	auto attacker = AttackInfo.attacker;
 	auto enemy = AttackInfo.defender;
 
-	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacksRemaining());
+	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacks.available());
 	const bool counterAttacksBlocked = attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || enemy->hasBonusOfType(Bonus::NO_RETALIATION);
 	const int totalAttacks = 1 + AttackInfo.attackerBonuses->getBonuses(Selector::type(Bonus::ADDITIONAL_ATTACK), (Selector::effectRange (Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue();
 
@@ -46,19 +46,15 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo
 		if(remainingCounterAttacks <= i || counterAttacksBlocked)
 			ap.damageReceived = 0;
 
-		curBai.attackerCount = attacker->count - attacker->countKilledByAttack(ap.damageReceived).first;
-		curBai.defenderCount = enemy->count - enemy->countKilledByAttack(ap.damageDealt).first;
-		if(!curBai.attackerCount)
+		curBai.attackerHealth = attacker->healthAfterAttacked(ap.damageReceived);
+		curBai.defenderHealth = enemy->healthAfterAttacked(ap.damageDealt);
+		if(curBai.attackerHealth.getCount() <= 0)
 			break;
 		//TODO what about defender? should we break? but in pessimistic scenario defender might be alive
 	}
 
 	//TODO other damage related to attack (eg. fire shield and other abilities)
 
-	//Limit damages by total stack health
-	vstd::amin(ap.damageDealt, enemy->totalHealth());
-	vstd::amin(ap.damageReceived, attacker->totalHealth());
-
 	return ap;
 }
 

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -55,7 +55,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 			auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
 			std::map<int, const CStack*> woundHpToStack;
 			for(auto stack : healingTargets)
-				if(auto woundHp = stack->MaxHealth() - stack->firstHPleft)
+				if(auto woundHp = stack->MaxHealth() - stack->getFirstHPleft())
 					woundHpToStack[woundHp] = stack;
 			if(woundHpToStack.empty())
 				return BattleAction::makeDefend(stack);

+ 59 - 49
client/CPlayerInterface.cpp

@@ -317,7 +317,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
 		return;
 	}
 
-	ui32 speed;
+	ui32 speed = 0;
 	if(settings["session"]["spectate"].Bool())
 	{
 		if(!settings["session"]["spectate-hero-speed"].isNull())
@@ -699,33 +699,46 @@ void CPlayerInterface::battleStacksHealedRes(const std::vector<std::pair<ui32, u
 		}
 	}
 
-	if (lifeDrain)
+	if(lifeDrain)
 	{
-		const CStack *attacker = cb->battleGetStackByID(healedStacks[0].first, false);
-		const CStack *defender = cb->battleGetStackByID(lifeDrainFrom, false);
-		int textOff = 0;
+		const CStack * attacker = cb->battleGetStackByID(healedStacks[0].first, false);
+		const CStack * defender = cb->battleGetStackByID(lifeDrainFrom, false);
 
-		if (attacker)
+		if(attacker && defender)
 		{
 			battleInt->displayEffect(52, attacker->position); //TODO: transparency
-			if (attacker->count > 1)
-			{
-				textOff += 1;
-			}
 			CCS->soundh->playSound(soundBase::DRAINLIF);
 
-			//print info about life drain
-			auto txt =  boost::format (CGI->generaltexth->allTexts[361 + textOff]) %  attacker->getCreature()->nameSing % healedStacks[0].second % defender->getCreature()->namePl;
-			battleInt->console->addText(boost::to_string(txt));
+			MetaString text;
+			attacker->addText(text, MetaString::GENERAL_TXT, 361);
+			attacker->addNameReplacement(text, false);
+			text.addReplacement(healedStacks[0].second);
+			defender->addNameReplacement(text, true);
+			battleInt->console->addText(text.toString());
+		}
+		else
+		{
+			logGlobal->error("Unable to display life drain info");
 		}
 	}
-	if (tentHeal)
+	if(tentHeal)
 	{
-		std::string text = CGI->generaltexth->allTexts[414];
-		boost::algorithm::replace_first(text, "%s", cb->battleGetStackByID(lifeDrainFrom, false)->getCreature()->nameSing);
-		boost::algorithm::replace_first(text, "%s",	cb->battleGetStackByID(healedStacks[0].first, false)->getCreature()->nameSing);
-		boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(healedStacks[0].second));
-		battleInt->console->addText(text);
+		const CStack * healer = cb->battleGetStackByID(lifeDrainFrom, false);
+		const CStack * target = cb->battleGetStackByID(healedStacks[0].first, false);
+
+		if(healer && target)
+		{
+			MetaString text;
+			text.addTxt(MetaString::GENERAL_TXT, 414);
+			healer->addNameReplacement(text, false);
+			target->addNameReplacement(text, false);
+			text.addReplacement(healedStacks[0].second);
+			battleInt->console->addText(text.toString());
+		}
+		else
+		{
+			logGlobal->error("Unable to display tent heal info");
+		}
 	}
 }
 
@@ -939,53 +952,50 @@ void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacke
 
 	battleInt->stacksAreAttacked(arg);
 }
-void CPlayerInterface::battleAttack(const BattleAttack *ba)
+void CPlayerInterface::battleAttack(const BattleAttack * ba)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
 	assert(curAction);
-	if (ba->lucky()) //lucky hit
+
+	const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking);
+
+	if(!attacker)
+	{
+		logGlobal->error("Attacking stack not found");
+		return;
+	}
+
+	if(ba->lucky()) //lucky hit
 	{
-		const CStack *stack = cb->battleGetStackByID(ba->stackAttacking);
-		std::string hlp = CGI->generaltexth->allTexts[45];
-		boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str());
-		battleInt->console->addText(hlp);
-		battleInt->displayEffect(18, stack->position);
+		battleInt->console->addText(attacker->formatGeneralMessage(-45));
+		battleInt->displayEffect(18, attacker->position);
 		CCS->soundh->playSound(soundBase::GOODLUCK);
 	}
-	if (ba->unlucky()) //unlucky hit
+	if(ba->unlucky()) //unlucky hit
 	{
-		const CStack *stack = cb->battleGetStackByID(ba->stackAttacking);
-		std::string hlp = CGI->generaltexth->allTexts[44];
-		boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str());
-		battleInt->console->addText(hlp);
-		battleInt->displayEffect(48, stack->position);
+		battleInt->console->addText(attacker->formatGeneralMessage(-44));
+		battleInt->displayEffect(48, attacker->position);
 		CCS->soundh->playSound(soundBase::BADLUCK);
 	}
-	if (ba->deathBlow())
+	if(ba->deathBlow())
 	{
-		const CStack *stack = cb->battleGetStackByID(ba->stackAttacking);
-		std::string hlp = CGI->generaltexth->allTexts[(stack->count != 1) ? 366 : 365];
-		boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str());
-		battleInt->console->addText(hlp);
-		for (auto & elem : ba->bsa)
+		battleInt->console->addText(attacker->formatGeneralMessage(365));
+		for(auto & elem : ba->bsa)
 		{
 			const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
 			battleInt->displayEffect(73, attacked->position);
 		}
 		CCS->soundh->playSound(soundBase::deathBlow);
-
 	}
 	battleInt->waitForAnims();
 
-	const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking);
-
-	if (ba->shot())
+	if(ba->shot())
 	{
-		for (auto & elem : ba->bsa)
+		for(auto & elem : ba->bsa)
 		{
-			if (!elem.isSecondary()) //display projectile only for primary target
+			if(!elem.isSecondary()) //display projectile only for primary target
 			{
 				const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
 				battleInt->stackAttacking(attacker, attacked->position, attacked, true);
@@ -995,27 +1005,27 @@ void CPlayerInterface::battleAttack(const BattleAttack *ba)
 	else
 	{
 		int shift = 0;
-		if (ba->counter() && BattleHex::mutualPosition(curAction->destinationTile, attacker->position) < 0)
+		if(ba->counter() && BattleHex::mutualPosition(curAction->destinationTile, attacker->position) < 0)
 		{
 			int distp = BattleHex::getDistance(curAction->destinationTile + 1, attacker->position);
 			int distm = BattleHex::getDistance(curAction->destinationTile - 1, attacker->position);
 
-			if ( distp < distm )
+			if(distp < distm)
 				shift = 1;
 			else
 				shift = -1;
 		}
 		const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked);
-		battleInt->stackAttacking( attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false);
+		battleInt->stackAttacking(attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false);
 	}
 
 	//battleInt->waitForAnims(); //FIXME: freeze
 
-	if (ba->spellLike())
+	if(ba->spellLike())
 	{
 		//display hit animation
 		SpellID spellID = ba->spellID;
-		battleInt->displaySpellHit(spellID,curAction->destinationTile);
+		battleInt->displaySpellHit(spellID, curAction->destinationTile);
 	}
 }
 void CPlayerInterface::battleObstaclePlaced(const CObstacleInstance &obstacle)

+ 2 - 2
client/NetPacksClient.cpp

@@ -728,12 +728,12 @@ void BattleResultsApplied::applyCl(CClient *cl)
 	INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied);
 }
 
-void StacksHealedOrResurrected::applyCl(CClient *cl)
+void StacksHealedOrResurrected::applyCl(CClient * cl)
 {
 	std::vector<std::pair<ui32, ui32> > shiftedHealed;
 	for(auto & elem : healedStacks)
 	{
-		shiftedHealed.push_back(std::make_pair(elem.stackID, elem.healedHP));
+		shiftedHealed.push_back(std::make_pair(elem.stackId, (ui32)elem.delta));
 	}
 	BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksHealedRes, shiftedHealed, lifeDrain, tentHealing, drainedFrom);
 }

+ 33 - 58
client/battle/CBattleInterface.cpp

@@ -1356,25 +1356,8 @@ void CBattleInterface::spellCast(const BattleSpellCast *sc)
 
 void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
 {
-	if(sse.stacks.size() == 1 && sse.effect.size() == 2 && sse.effect.back().sid == -1)
-	{
-		const Bonus & bns = sse.effect.front();
-		if(bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL)
-		{
-			//defensive stance
-			const CStack *stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin());
-			int txtid = 120;
-
-			if(stack->count != 1)
-				txtid++; //move to plural text
-
-			BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE)));
-			defenseBonuses.remove_if(Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance
-			int val = stack->Defense() - defenseBonuses.totalValue();
-			auto txt = boost::format(CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val;
-			console->addText(boost::to_string(txt));
-		}
-	}
+	for(const MetaString & line : sse.battleLog)
+		console->addText(line.toString());
 
 	if(activeStack != nullptr)
 		redrawBackgroundWithHexes(activeStack);
@@ -1592,10 +1575,10 @@ void CBattleInterface::activateStack()
 	//set casting flag to true if creature can use it to not check it every time
 	const auto spellcaster = s->getBonusLocalFirst(Selector::type(Bonus::SPELLCASTER)),
 		randomSpellcaster = s->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER));
-	if (s->casts &&  (spellcaster || randomSpellcaster))
+	if(s->canCast() && (spellcaster || randomSpellcaster))
 	{
 		stackCanCastSpell = true;
-		if (randomSpellcaster)
+		if(randomSpellcaster)
 			creatureSpellToCast = -1; //spell will be set later on cast
 		else
 			creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move
@@ -1696,16 +1679,16 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo
 	{
 		PossibleActions notPriority = INVALID;
 		//first action will be prioritized over later ones
-		if (stack->casts) //TODO: check for battlefield effects that prevent casting?
+		if(stack->canCast()) //TODO: check for battlefield effects that prevent casting?
 		{
-			if (stack->hasBonusOfType (Bonus::SPELLCASTER))
+			if(stack->hasBonusOfType (Bonus::SPELLCASTER))
 			{
-				if (creatureSpellToCast != -1)
+				if(creatureSpellToCast != -1)
 				{
 					const CSpell *spell = SpellID(creatureSpellToCast).toSpell();
 					PossibleActions act = getCasterAction(spell, stack, ECastingMode::CREATURE_ACTIVE_CASTING);
 
-					if (forceCast)
+					if(forceCast)
 					{
 						//forced action to be only one possible
 						possibleActions.push_back(act);
@@ -1721,10 +1704,10 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo
 			if (stack->hasBonusOfType (Bonus::DAEMON_SUMMONING))
 				possibleActions.push_back (RISE_DEMONS);
 		}
-		if (stack->shots && stack->hasBonusOfType (Bonus::SHOOTER))
-			possibleActions.push_back (SHOOT);
-		if (stack->hasBonusOfType (Bonus::RETURN_AFTER_STRIKE))
-			possibleActions.push_back (ATTACK_AND_RETURN);
+		if(stack->canShoot())
+			possibleActions.push_back(SHOOT);
+		if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE))
+			possibleActions.push_back(ATTACK_AND_RETURN);
 
 		possibleActions.push_back(ATTACK); //all active stacks can attack
 		possibleActions.push_back(WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere
@@ -1742,39 +1725,39 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo
 	}
 }
 
-void CBattleInterface::printConsoleAttacked( const CStack *defender, int dmg, int killed, const CStack *attacker, bool multiple )
+void CBattleInterface::printConsoleAttacked(const CStack * defender, int dmg, int killed, const CStack * attacker, bool multiple)
 {
 	std::string formattedText;
-	if (attacker) //ignore if stacks were killed by spell
+	if(attacker) //ignore if stacks were killed by spell
 	{
-		boost::format txt = boost::format (CGI->generaltexth->allTexts[attacker->count > 1 ? 377 : 376]) %
-			(attacker->count > 1 ? attacker->getCreature()->namePl : attacker->getCreature()->nameSing) % dmg;
-		formattedText.append(boost::to_string(txt));
+		MetaString text;
+		attacker->addText(text, MetaString::GENERAL_TXT, 376);
+		attacker->addNameReplacement(text);
+		text.addReplacement(dmg);
+		formattedText = text.toString();
 	}
-	if (killed > 0)
+
+	if(killed > 0)
 	{
-		if (attacker)
+		if(attacker)
 			formattedText.append(" ");
 
 		boost::format txt;
-		if (killed > 1)
+		if(killed > 1)
 		{
-			txt = boost::format (CGI->generaltexth->allTexts[379]) % killed % (multiple ? CGI->generaltexth->allTexts[43] : defender->getCreature()->namePl); // creatures perish
+			txt = boost::format(CGI->generaltexth->allTexts[379]) % killed % (multiple ? CGI->generaltexth->allTexts[43] : defender->getCreature()->namePl); // creatures perish
 		}
 		else //killed == 1
 		{
-			txt = boost::format (CGI->generaltexth->allTexts[378]) % (multiple ? CGI->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes
+			txt = boost::format(CGI->generaltexth->allTexts[378]) % (multiple ? CGI->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes
 		}
 		std::string trimmed = boost::to_string(txt);
 		boost::algorithm::trim(trimmed); // these default h3 texts have unnecessary new lines, so get rid of them before displaying
 		formattedText.append(trimmed);
 	}
 	console->addText(formattedText);
-
 }
 
-
-
 void CBattleInterface::endAction(const BattleAction* action)
 {
 	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
@@ -1960,19 +1943,11 @@ void CBattleInterface::startAction(const BattleAction* action)
 		break;
 	}
 
-	if (txtid > 0  &&  stack->count != 1)
-		txtid++; //move to plural text
-	else if (txtid < 0)
-		txtid = -txtid;
-
-	if (txtid)
-	{
-		std::string name = (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str();
-		console->addText((boost::format(CGI->generaltexth->allTexts[txtid].c_str()) % name).str());
-	}
+	if(txtid != 0)
+		console->addText(stack->formatGeneralMessage(txtid));
 
 	//displaying special abilities
-	switch (action->actionType)
+	switch(action->actionType)
 	{
 		case Battle::STACK_HEAL:
 			displayEffect(74, action->destinationTile);
@@ -2200,7 +2175,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
 					if (!(shere->hasBonusOfType(Bonus::UNDEAD)
 						|| shere->hasBonusOfType(Bonus::NON_LIVING)
 						|| vstd::contains(shere->state, EBattleStackState::SUMMONED)
-						|| vstd::contains(shere->state, EBattleStackState::CLONED)
+						|| shere->isClone()
 						|| shere->hasBonusOfType(Bonus::SIEGE_WEAPON)
 						))
 						legalAction = true;
@@ -2304,7 +2279,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
 				realizeAction = [=] {giveCommand(Battle::SHOOT, myNumber, activeStack->ID);};
 				std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(CRandomGenerator::getDefault(), sactive, shere)); //calculating estimated dmg
 				//printing - Shoot %s (%d shots left, %s damage)
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots % estDmgText).str();
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots.available() % estDmgText).str();
 			}
 				break;
 			case AIMED_SPELL_CREATURE:
@@ -3273,10 +3248,10 @@ void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vector<const CStack
 {
 	auto isAmountBoxVisible = [&](const CStack *stack) -> bool
 	{
-		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->count == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
+		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
 			return false;
 
-		if(stack->count == 0) //hide box when target is going to die anyway - do not display "0 creatures"
+		if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures"
 			return false;
 
 		for(auto anim : pendingAnims) //no matter what other conditions below are, hide box when creature is playing hit animation
@@ -3353,7 +3328,7 @@ void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vector<const CStack
 			//blitting amount
 			Point textPos(creAnims[stack->ID]->pos.x + xAdd + amountNormal->w/2,
 			              creAnims[stack->ID]->pos.y + yAdd + amountNormal->h/2);
-			graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->count), Colors::WHITE, textPos);
+			graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->getCount()), Colors::WHITE, textPos);
 		}
 	}
 }

+ 1 - 1
client/battle/CBattleInterface.h

@@ -53,7 +53,7 @@ class CBattleGameInterface;
 struct StackAttackedInfo
 {
 	const CStack *defender; //attacked stack
-	unsigned int dmg; //damage dealt
+	int32_t dmg; //damage dealt
 	unsigned int amountKilled; //how many creatures in stack has been killed
 	const CStack *attacker; //attacking stack
 	bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack

+ 6 - 5
client/battle/CBattleInterfaceClasses.cpp

@@ -595,9 +595,10 @@ void CClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent)
 			attackedStack->owner != myInterface->getCurrentPlayerInterface()->playerID &&
 			attackedStack->alive())
 		{
-			const std::string & attackedName = attackedStack->count == 1 ? attackedStack->getCreature()->nameSing : attackedStack->getCreature()->namePl;
-			auto txt = boost::format (CGI->generaltexth->allTexts[220]) % attackedName;
-			myInterface->console->alterTxt = boost::to_string(txt);
+			MetaString text;
+			text.addTxt(MetaString::GENERAL_TXT, 220);
+			attackedStack->addNameReplacement(text);
+			myInterface->console->alterTxt = text.toString();
 			setAlterText = true;
 		}
 	}
@@ -744,9 +745,9 @@ void CStackQueue::StackBox::showAll(SDL_Surface * to)
 	CIntObject::showAll(to);
 
 	if(small)
-		printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 7, FONT_SMALL, Colors::WHITE, to);
+		printAtMiddleLoc(makeNumberShort(stack->getCount()), pos.w/2, pos.h - 7, FONT_SMALL, Colors::WHITE, to);
 	else
-		printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 8, FONT_MEDIUM, Colors::WHITE, to);
+		printAtMiddleLoc(makeNumberShort(stack->getCount()), pos.w/2, pos.h - 8, FONT_MEDIUM, Colors::WHITE, to);
 }
 
 void CStackQueue::StackBox::setStack( const CStack *stack )

+ 23 - 18
client/windows/CCreatureWindow.cpp

@@ -224,41 +224,46 @@ void CStackWindow::CWindowSection::createStackInfo(bool showExp, bool showArt)
 	new CPicture("stackWindow/icons", 117, 32);
 
 	const CStack * battleStack = parent->info->stack;
-	bool shooter = parent->info->stackNode->hasBonusOfType(Bonus::SHOOTER) && parent->info->stackNode->valOfBonuses(Bonus::SHOTS);
-	bool caster  = parent->info->stackNode->valOfBonuses(Bonus::CASTS);
 
-	if (battleStack != nullptr) // in battle
+	auto morale = new MoraleLuckBox(true, genRect(42, 42, 321, 110));
+	auto luck = new MoraleLuckBox(false, genRect(42, 42, 375, 110));
+
+	if(battleStack != nullptr) // in battle
 	{
 		printStatBase(EStat::ATTACK, CGI->generaltexth->primarySkillNames[0], parent->info->creature->Attack(), battleStack->Attack());
 		printStatBase(EStat::DEFENCE, CGI->generaltexth->primarySkillNames[1], parent->info->creature->Defense(false), battleStack->Defense());
 		printStatRange(EStat::DAMAGE, CGI->generaltexth->allTexts[199], parent->info->stackNode->getMinDamage() * dmgMultiply, battleStack->getMaxDamage() * dmgMultiply);
-		printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->valOfBonuses(Bonus::STACK_HEALTH), battleStack->valOfBonuses(Bonus::STACK_HEALTH));
+		printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->MaxHealth(), battleStack->MaxHealth());
 		printStatBase(EStat::SPEED, CGI->generaltexth->zelp[441].first, parent->info->creature->Speed(), battleStack->Speed());
 
-		if (shooter)
-			printStatBase(EStat::SHOTS, CGI->generaltexth->allTexts[198], battleStack->valOfBonuses(Bonus::SHOTS), battleStack->shots);
-		if (caster)
-			printStatBase(EStat::MANA, CGI->generaltexth->allTexts[399], battleStack->valOfBonuses(Bonus::CASTS), battleStack->casts);
-		printStat(EStat::HEALTH_LEFT, CGI->generaltexth->allTexts[200], battleStack->firstHPleft);
+		if(battleStack->isShooter())
+			printStatBase(EStat::SHOTS, CGI->generaltexth->allTexts[198], battleStack->shots.total(), battleStack->shots.available());
+		if(battleStack->isCaster())
+			printStatBase(EStat::MANA, CGI->generaltexth->allTexts[399], battleStack->casts.total(), battleStack->casts.available());
+		printStat(EStat::HEALTH_LEFT, CGI->generaltexth->allTexts[200], battleStack->getFirstHPleft());
+
+		morale->set(battleStack);
+		luck->set(battleStack);
 	}
 	else
 	{
+		const bool shooter = parent->info->stackNode->hasBonusOfType(Bonus::SHOOTER) && parent->info->stackNode->valOfBonuses(Bonus::SHOTS);
+		const bool caster  = parent->info->stackNode->valOfBonuses(Bonus::CASTS);
+
 		printStatBase(EStat::ATTACK, CGI->generaltexth->primarySkillNames[0], parent->info->creature->Attack(), parent->info->stackNode->Attack());
 		printStatBase(EStat::DEFENCE, CGI->generaltexth->primarySkillNames[1], parent->info->creature->Defense(false), parent->info->stackNode->Defense());
 		printStatRange(EStat::DAMAGE, CGI->generaltexth->allTexts[199], parent->info->stackNode->getMinDamage() * dmgMultiply, parent->info->stackNode->getMaxDamage() * dmgMultiply);
-		printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->valOfBonuses(Bonus::STACK_HEALTH), parent->info->stackNode->valOfBonuses(Bonus::STACK_HEALTH));
+		printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->MaxHealth(), parent->info->stackNode->MaxHealth());
 		printStatBase(EStat::SPEED, CGI->generaltexth->zelp[441].first, parent->info->creature->Speed(), parent->info->stackNode->Speed());
 
-		if (shooter)
+		if(shooter)
 			printStat(EStat::SHOTS, CGI->generaltexth->allTexts[198], parent->info->stackNode->valOfBonuses(Bonus::SHOTS));
-		if (caster)
+		if(caster)
 			printStat(EStat::MANA, CGI->generaltexth->allTexts[399], parent->info->stackNode->valOfBonuses(Bonus::CASTS));
-	}
 
-	auto morale = new MoraleLuckBox(true, genRect(42, 42, 321, 110));
-	morale->set(parent->info->stackNode);
-	auto luck = new MoraleLuckBox(false, genRect(42, 42, 375, 110));
-	luck->set(parent->info->stackNode);
+		morale->set(parent->info->stackNode);
+		luck->set(parent->info->stackNode);
+	}
 
 	if (showExp)
 	{
@@ -881,7 +886,7 @@ CStackWindow::CStackWindow(const CStack * stack, bool popup):
 	info->stack = stack;
 	info->stackNode = stack->base;
 	info->creature = stack->type;
-	info->creatureCount = stack->count;
+	info->creatureCount = stack->getCount();
 	info->popupWindow = popup;
 	init();
 }

+ 1 - 1
client/windows/CSpellWindow.cpp

@@ -567,7 +567,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 					{
 						return s->owner == player
 							&& vstd::contains(s->state, EBattleStackState::SUMMONED)
-							&& !vstd::contains(s->state, EBattleStackState::CLONED);
+							&& !s->isClone();
 					});
 					for(const CStack * s : stacks)
 					{

+ 12 - 0
lib/CGeneralTextHandler.cpp

@@ -507,3 +507,15 @@ CGeneralTextHandler::CGeneralTextHandler()
 		}
 	}
 }
+
+int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const
+{
+	if(textIndex == 0)
+		return 0;
+	else if(textIndex < 0)
+		return -textIndex;
+	else if(count == 1)
+		return textIndex;
+	else
+		return textIndex + 1;
+}

+ 4 - 2
lib/CGeneralTextHandler.h

@@ -37,7 +37,7 @@ namespace Unicode
 	/// NOTE: usage of these functions should be avoided if possible
 	std::string DLL_LINKAGE fromUnicode(const std::string & text);
 	std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding);
-	
+
 	///delete (amount) UTF characters from right
 	DLL_LINKAGE void trimRight(std::string & text, const size_t amount = 1);
 };
@@ -103,7 +103,7 @@ public:
 	std::vector<std::string> overview;//text for Kingdom Overview window
 	std::vector<std::string> colors; //names of player colors ("red",...)
 	std::vector<std::string> capColors; //names of player colors with first letter capitalized ("Red",...)
-	std::vector<std::string> turnDurations; //turn durations for pregame (1 Minute ... Unlimited) 
+	std::vector<std::string> turnDurations; //turn durations for pregame (1 Minute ... Unlimited)
 
 	//towns
 	std::vector<std::string> tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen
@@ -143,6 +143,8 @@ public:
 
 	static void readToVector(std::string sourceName, std::vector<std::string> &dest);
 
+	int32_t pluralText(const int32_t textIndex, const int32_t count) const;
+
 	CGeneralTextHandler();
 	CGeneralTextHandler(const CGeneralTextHandler&) = delete;
 	CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete;

+ 508 - 160
lib/CStack.cpp

@@ -9,74 +9,365 @@
  */
 #include "StdInc.h"
 #include "CStack.h"
+#include "CGeneralTextHandler.h"
 #include "battle/BattleInfo.h"
 #include "spells/CSpellHandler.h"
 #include "CRandomGenerator.h"
 #include "NetPacks.h"
 
 
-CStack::CStack(const CStackInstance *Base, PlayerColor O, int I, ui8 Side, SlotID S)
-	: base(Base), ID(I), owner(O), slot(S), side(Side),
-	counterAttacksPerformed(0),counterAttacksTotalCache(0), cloneID(-1),
-	firstHPleft(-1), position(), shots(0), casts(0), resurrected(0)
+///CAmmo
+CAmmo::CAmmo(const CStack * Owner, CSelector totalSelector):
+	CStackResource(Owner), totalProxy(Owner, totalSelector)
+{
+
+}
+
+int32_t CAmmo::available() const
+{
+	return total() - used;
+}
+
+bool CAmmo::canUse(int32_t amount) const
+{
+	return available() - amount >= 0;
+}
+
+void CAmmo::reset()
+{
+	used = 0;
+}
+
+int32_t CAmmo::total() const
+{
+	return totalProxy->totalValue();
+}
+
+void CAmmo::use(int32_t amount)
+{
+	if(available() - amount < 0)
+	{
+		logGlobal->error("Stack ammo overuse");
+		used += available();
+	}
+	else
+		used += amount;
+}
+
+///CShots
+CShots::CShots(const CStack * Owner):
+	CAmmo(Owner, Selector::type(Bonus::SHOTS))
+{
+
+}
+
+void CShots::use(int32_t amount)
+{
+	//don't remove ammo if we control a working ammo cart
+	bool hasAmmoCart = false;
+
+	for(const CStack * st : owner->battle->stacks)
+	{
+		if(owner->battle->battleMatchOwner(st, owner, true) && st->getCreature()->idNumber == CreatureID::AMMO_CART && st->alive())
+		{
+			hasAmmoCart = true;
+			break;
+		}
+	}
+
+	if(!hasAmmoCart)
+		CAmmo::use(amount);
+}
+
+///CCasts
+CCasts::CCasts(const CStack * Owner):
+	CAmmo(Owner, Selector::type(Bonus::CASTS))
+{
+
+}
+
+///CRetaliations
+CRetaliations::CRetaliations(const CStack * Owner):
+	CAmmo(Owner, Selector::type(Bonus::ADDITIONAL_RETALIATION)), totalCache(0)
+{
+
+}
+
+int32_t CRetaliations::total() const
+{
+	//after dispell bonus should remain during current round
+	int32_t val = 1 + totalProxy->totalValue();
+	vstd::amax(totalCache, val);
+	return totalCache;
+}
+
+void CRetaliations::reset()
+{
+	CAmmo::reset();
+	totalCache = 0;
+}
+
+///CHealth
+CHealth::CHealth(const IUnitHealthInfo * Owner):
+	owner(Owner)
+{
+	reset();
+}
+
+CHealth::CHealth(const CHealth & other):
+	owner(other.owner),
+	firstHPleft(other.firstHPleft),
+	fullUnits(other.fullUnits),
+	resurrected(other.resurrected)
+{
+
+}
+
+void CHealth::init()
+{
+	reset();
+	fullUnits = owner->unitBaseAmount() > 1 ? owner->unitBaseAmount() - 1 : 0;
+	firstHPleft = owner->unitBaseAmount() > 0 ? owner->unitMaxHealth() : 0;
+}
+
+void CHealth::addResurrected(int32_t amount)
+{
+	resurrected += amount;
+	vstd::amax(resurrected, 0);
+}
+
+int64_t CHealth::available() const
+{
+	return static_cast<int64_t>(firstHPleft) + owner->unitMaxHealth() * fullUnits;
+}
+
+int64_t CHealth::total() const
+{
+	return static_cast<int64_t>(owner->unitMaxHealth()) * owner->unitBaseAmount();
+}
+
+void CHealth::damage(int32_t & amount)
+{
+	const int32_t oldCount = getCount();
+
+	const bool withKills = amount >= firstHPleft;
+
+	if(withKills)
+	{
+		int64_t totalHealth = available();
+		if(amount > totalHealth)
+			amount = totalHealth;
+		totalHealth -= amount;
+		if(totalHealth <= 0)
+		{
+			fullUnits = 0;
+			firstHPleft = 0;
+		}
+		else
+		{
+			setFromTotal(totalHealth);
+		}
+	}
+	else
+	{
+		firstHPleft -= amount;
+	}
+
+	addResurrected(getCount() - oldCount);
+}
+
+void CHealth::heal(int32_t & amount, EHealLevel level, EHealPower power)
+{
+	const int32_t unitHealth = owner->unitMaxHealth();
+	const int32_t oldCount = getCount();
+
+	int32_t maxHeal = std::numeric_limits<int32_t>::max();
+
+	switch(level)
+	{
+	case EHealLevel::HEAL:
+		maxHeal = std::max(0, unitHealth - firstHPleft);
+		break;
+	case EHealLevel::RESURRECT:
+		maxHeal = total() - available();
+		break;
+	default:
+		assert(level == EHealLevel::OVERHEAL);
+		break;
+	}
+
+	vstd::amax(maxHeal, 0);
+	vstd::abetween(amount, 0, maxHeal);
+
+	if(amount == 0)
+		return;
+
+	int64_t availableHealth = available();
+
+	availableHealth	+= amount;
+	setFromTotal(availableHealth);
+
+	if(power == EHealPower::ONE_BATTLE)
+		addResurrected(getCount() - oldCount);
+	else
+		assert(power == EHealPower::PERMANENT);
+}
+
+void CHealth::setFromTotal(const int64_t totalHealth)
+{
+	const int32_t unitHealth = owner->unitMaxHealth();
+	firstHPleft = totalHealth % unitHealth;
+	fullUnits = totalHealth / unitHealth;
+
+	if(firstHPleft == 0 && fullUnits > 1)
+	{
+		firstHPleft = unitHealth;
+		fullUnits -= 1;
+	}
+}
+
+void CHealth::reset()
+{
+	fullUnits = 0;
+	firstHPleft = 0;
+	resurrected = 0;
+}
+
+int32_t CHealth::getCount() const
+{
+	return fullUnits + (firstHPleft > 0 ? 1 : 0);
+}
+
+int32_t CHealth::getFirstHPleft() const
+{
+	return firstHPleft;
+}
+
+int32_t CHealth::getResurrected() const
+{
+	return resurrected;
+}
+
+void CHealth::fromInfo(const CHealthInfo & info)
+{
+	firstHPleft = info.firstHPleft;
+	fullUnits = info.fullUnits;
+	resurrected = info.resurrected;
+}
+
+void CHealth::toInfo(CHealthInfo & info) const
+{
+	info.firstHPleft = firstHPleft;
+	info.fullUnits = fullUnits;
+	info.resurrected = resurrected;
+}
+
+void CHealth::takeResurrected()
+{
+	if(resurrected != 0)
+	{
+		int64_t totalHealth = available();
+
+		totalHealth -= resurrected * owner->unitMaxHealth();
+		vstd::amax(totalHealth, 0);
+		setFromTotal(totalHealth);
+		resurrected = 0;
+	}
+}
+
+///CStack
+CStack::CStack(const CStackInstance * Base, PlayerColor O, int I, ui8 Side, SlotID S):
+	base(Base), ID(I), owner(O), slot(S), side(Side),
+	counterAttacks(this), shots(this), casts(this), health(this), cloneID(-1),
+	position()
 {
 	assert(base);
 	type = base->type;
-	count = baseAmount = base->count;
+	baseAmount = base->count;
+	health.init(); //???
 	setNodeType(STACK_BATTLE);
 }
-CStack::CStack()
+
+CStack::CStack():
+	counterAttacks(this), shots(this), casts(this), health(this)
 {
 	init();
 	setNodeType(STACK_BATTLE);
 }
-CStack::CStack(const CStackBasicDescriptor *stack, PlayerColor O, int I, ui8 Side, SlotID S)
-	: base(nullptr), ID(I), owner(O), slot(S), side(Side),
-	counterAttacksPerformed(0), counterAttacksTotalCache(0), cloneID(-1),
-	firstHPleft(-1), position(), shots(0), casts(0), resurrected(0)
+
+CStack::CStack(const CStackBasicDescriptor * stack, PlayerColor O, int I, ui8 Side, SlotID S):
+	base(nullptr), ID(I), owner(O), slot(S), side(Side),
+	counterAttacks(this), shots(this), casts(this), health(this), cloneID(-1),
+	position()
 {
 	type = stack->type;
-	count = baseAmount = stack->count;
+	baseAmount = stack->count;
+	health.init(); //???
 	setNodeType(STACK_BATTLE);
 }
 
+int32_t CStack::getKilled() const
+{
+	int32_t res = baseAmount - health.getCount() + health.getResurrected();
+	vstd::amax(res, 0);
+	return res;
+}
+
+int32_t CStack::getCount() const
+{
+	return health.getCount();
+}
+
+int32_t CStack::getFirstHPleft() const
+{
+	return health.getFirstHPleft();
+}
+
+const CCreature * CStack::getCreature() const
+{
+	return type;
+}
+
 void CStack::init()
 {
 	base = nullptr;
 	type = nullptr;
 	ID = -1;
-	count = baseAmount = -1;
-	firstHPleft = -1;
+	baseAmount = -1;
 	owner = PlayerColor::NEUTRAL;
 	slot = SlotID(255);
 	side = 1;
 	position = BattleHex();
-	counterAttacksPerformed = 0;
-	counterAttacksTotalCache = 0;
 	cloneID = -1;
-
-	shots = 0;
-	casts = 0;
-	resurrected = 0;
 }
 
-void CStack::postInit()
+void CStack::localInit(BattleInfo * battleInfo)
 {
+	battle = battleInfo;
+	cloneID = -1;
 	assert(type);
-	assert(getParentNodes().size());
 
-	firstHPleft = MaxHealth();
-	shots = getCreature()->valOfBonuses(Bonus::SHOTS);
-	counterAttacksPerformed = 0;
-	counterAttacksTotalCache = 0;
-	casts = valOfBonuses(Bonus::CASTS);
-	resurrected = 0;
-	cloneID = -1;
+	exportBonuses();
+	if(base) //stack originating from "real" stack in garrison -> attach to it
+	{
+		attachTo(const_cast<CStackInstance *>(base));
+	}
+	else //attach directly to obj to which stack belongs and creature type
+	{
+		CArmedInstance * army = battle->battleGetArmyObject(side);
+		attachTo(army);
+		attachTo(const_cast<CCreature *>(type));
+	}
+
+	shots.reset();
+	counterAttacks.reset();
+	casts.reset();
+	health.init();
 }
 
 ui32 CStack::level() const
 {
-	if (base)
+	if(base)
 		return base->getLevel(); //creatture or commander
 	else
 		return std::max(1, (int)getCreature()->level); //war machine, clone etc
@@ -85,19 +376,19 @@ ui32 CStack::level() const
 si32 CStack::magicResistance() const
 {
 	si32 magicResistance;
-	if (base) //TODO: make war machines receive aura of magic resistance
+	if(base)  //TODO: make war machines receive aura of magic resistance
 	{
 		magicResistance = base->magicResistance();
 		int auraBonus = 0;
-		for (const CStack * stack : base->armyObj->battle-> batteAdjacentCreatures(this))
-	{
-		if (stack->owner == owner)
+		for(const CStack * stack : base->armyObj->battle-> batteAdjacentCreatures(this))
 		{
-			vstd::amax(auraBonus, stack->valOfBonuses(Bonus::SPELL_RESISTANCE_AURA)); //max value
+			if(stack->owner == owner)
+			{
+				vstd::amax(auraBonus, stack->valOfBonuses(Bonus::SPELL_RESISTANCE_AURA)); //max value
+			}
 		}
-	}
 		magicResistance += auraBonus;
-		vstd::amin (magicResistance, 100);
+		vstd::amin(magicResistance, 100);
 	}
 	else
 		magicResistance = type->magicResistance();
@@ -106,18 +397,38 @@ si32 CStack::magicResistance() const
 
 bool CStack::willMove(int turn /*= 0*/) const
 {
-	return ( turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING) )
-		&& !moved(turn)
-		&& canMove(turn);
+	return (turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING))
+		   && !moved(turn)
+		   && canMove(turn);
 }
 
-bool CStack::canMove( int turn /*= 0*/ ) const
+bool CStack::canMove(int turn /*= 0*/) const
 {
 	return alive()
-		&& !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature
+		   && !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature
+}
+
+bool CStack::canCast() const
+{
+	return casts.canUse(1);//do not check specific cast abilities here
+}
+
+bool CStack::isCaster() const
+{
+	return casts.total() > 0;//do not check specific cast abilities here
+}
+
+bool CStack::canShoot() const
+{
+	return shots.canUse(1) && hasBonusOfType(Bonus::SHOOTER);
+}
+
+bool CStack::isShooter() const
+{
+	return shots.total() > 0 && hasBonusOfType(Bonus::SHOOTER);
 }
 
-bool CStack::moved( int turn /*= 0*/ ) const
+bool CStack::moved(int turn /*= 0*/) const
 {
 	if(!turn)
 		return vstd::contains(state, EBattleStackState::MOVED);
@@ -197,26 +508,27 @@ std::vector<BattleHex> CStack::getSurroundingHexes(BattleHex attackerPos) const
 	{
 		const int WN = GameConstants::BFIELD_WIDTH;
 		if(side == BattleSide::ATTACKER)
-		{ //position is equal to front hex
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+2 : WN+1 ), hexes);
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+1 : WN ), hexes);
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN : WN-1 ), hexes);
+		{
+			//position is equal to front hex
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 2 : WN + 1), hexes);
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 1 : WN), hexes);
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN : WN - 1), hexes);
 			BattleHex::checkAndPush(hex - 2, hexes);
 			BattleHex::checkAndPush(hex + 1, hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-2 : WN-1 ), hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-1 : WN ), hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN : WN+1 ), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 2 : WN - 1), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 1 : WN), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN : WN + 1), hexes);
 		}
 		else
 		{
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+1 : WN ), hexes);
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN : WN-1 ), hexes);
-			BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN-1 : WN-2 ), hexes);
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 1 : WN), hexes);
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN : WN - 1), hexes);
+			BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN - 1 : WN - 2), hexes);
 			BattleHex::checkAndPush(hex + 2, hexes);
 			BattleHex::checkAndPush(hex - 1, hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-1 : WN ), hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN : WN+1 ), hexes);
-			BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN+1 : WN+2 ), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 1 : WN), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN : WN + 1), hexes);
+			BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN + 1 : WN + 2), hexes);
 		}
 		return hexes;
 	}
@@ -248,15 +560,15 @@ std::vector<si32> CStack::activeSpells() const
 	std::stringstream cachingStr;
 	cachingStr << "!type_" << Bonus::NONE << "source_" << Bonus::SPELL_EFFECT;
 	CSelector selector = Selector::sourceType(Bonus::SPELL_EFFECT)
-		.And(CSelector([](const Bonus *b)->bool
-		{
-			return b->type != Bonus::NONE;
-		}));
+						 .And(CSelector([](const Bonus * b)->bool
+	{
+		return b->type != Bonus::NONE;
+	}));
 
 	TBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str());
 	for(const std::shared_ptr<Bonus> it : *spellEffects)
 	{
-		if (!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects
+		if(!vstd::contains(ret, it->sid))  //do not duplicate spells with multiple effects
 			ret.push_back(it->sid);
 	}
 
@@ -273,22 +585,17 @@ const CGHeroInstance * CStack::getMyHero() const
 	if(base)
 		return dynamic_cast<const CGHeroInstance *>(base->armyObj);
 	else //we are attached directly?
-		for(const CBonusSystemNode *n : getParentNodes())
+		for(const CBonusSystemNode * n : getParentNodes())
 			if(n->getNodeType() == HERO)
 				return dynamic_cast<const CGHeroInstance *>(n);
 
 	return nullptr;
 }
 
-ui32 CStack::totalHealth() const
-{
-	return ((count > 0) ? MaxHealth() * (count-1) : 0) + firstHPleft;//do not hide possible invalid firstHPleft for dead stack
-}
-
 std::string CStack::nodeName() const
 {
 	std::ostringstream oss;
-	oss << "Battle stack [" << ID << "]: " << count << " creatures of ";
+	oss << "Battle stack [" << ID << "]: " << health.getCount() << " creatures of ";
 	if(type)
 		oss << type->namePl;
 	else
@@ -300,65 +607,79 @@ std::string CStack::nodeName() const
 	return oss.str();
 }
 
-std::pair<int,int> CStack::countKilledByAttack(int damageReceived) const
+CHealth CStack::healthAfterAttacked(int32_t & damage) const
 {
-	int newRemainingHP = 0;
-	int killedCount = damageReceived / MaxHealth();
-	unsigned damageFirst = damageReceived % MaxHealth();
+	return healthAfterAttacked(damage, health);
+}
 
-	if (damageReceived && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
+CHealth CStack::healthAfterAttacked(int32_t & damage, const CHealth & customHealth) const
+{
+	CHealth res = customHealth;
+
+	if(isClone())
 	{
-		killedCount = count;
+		// block ability should not kill clone (0 damage)
+		if(damage > 0)
+		{
+			damage = 1;//??? what should be actual damage against clone?
+			res.reset();
+		}
 	}
 	else
 	{
-		if( firstHPleft <= damageFirst )
-		{
-			killedCount++;
-			newRemainingHP = firstHPleft + MaxHealth() - damageFirst;
-		}
-		else
-		{
-			newRemainingHP = firstHPleft - damageFirst;
-		}
+		res.damage(damage);
 	}
 
-	if(killedCount == count)
-		newRemainingHP = 0;
+	return res;
+}
+
+CHealth CStack::healthAfterHealed(int32_t & toHeal, EHealLevel level, EHealPower power) const
+{
+	CHealth res = health;
+
+	if(level == EHealLevel::HEAL && power == EHealPower::ONE_BATTLE)
+		logGlobal->error("Heal for one battle does not make sense", nodeName(), toHeal);
+	else if(isClone())
+		logGlobal->error("Attempt to heal clone: %s for %d HP", nodeName(), toHeal);
+	else
+		res.heal(toHeal, level, power);
 
-	return std::make_pair(killedCount, newRemainingHP);
+	return res;
 }
 
-void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional<int> customCount /*= boost::none*/) const
+void CStack::prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand) const
 {
-	auto afterAttack = countKilledByAttack(bsa.damageAmount);
+	prepareAttacked(bsa, rand, health);
+}
 
-	bsa.killedAmount = afterAttack.first;
-	bsa.newHP = afterAttack.second;
+void CStack::prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand, const CHealth & customHealth) const
+{
+	CHealth afterAttack = healthAfterAttacked(bsa.damageAmount, customHealth);
 
+	bsa.killedAmount = customHealth.getCount() - afterAttack.getCount();
+	afterAttack.toInfo(bsa.newHealth);
+	bsa.newHealth.stackId = ID;
+	bsa.newHealth.delta = -bsa.damageAmount;
 
-	if(bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
+	if(afterAttack.available() <= 0 && isClone())
 	{
 		bsa.flags |= BattleStackAttacked::CLONE_KILLED;
 		return; // no rebirth I believe
 	}
 
-	const int countToUse = customCount ? *customCount : count;
-
-	if(countToUse <= bsa.killedAmount) //stack killed
+	if(afterAttack.available() <= 0) //stack killed
 	{
-		bsa.newAmount = 0;
 		bsa.flags |= BattleStackAttacked::KILLED;
-		bsa.killedAmount = countToUse; //we cannot kill more creatures than we have
 
 		int resurrectFactor = valOfBonuses(Bonus::REBIRTH);
-		if(resurrectFactor > 0 && casts) //there must be casts left
+		if(resurrectFactor > 0 && canCast()) //there must be casts left
 		{
-			int resurrectedStackCount = base->count * resurrectFactor / 100;
+			int resurrectedStackCount = baseAmount * resurrectFactor / 100;
 
 			// last stack has proportional chance to rebirth
-			auto diff = base->count * resurrectFactor / 100.0 - resurrectedStackCount;
-			if (diff > rand.nextDouble(0, 0.99))
+			//FIXME: diff is always 0
+			auto diff = baseAmount * resurrectFactor / 100.0 - resurrectedStackCount;
+			if(diff > rand.nextDouble(0, 0.99))
 			{
 				resurrectedStackCount += 1;
 			}
@@ -372,64 +693,45 @@ void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand,
 			if(resurrectedStackCount > 0)
 			{
 				bsa.flags |= BattleStackAttacked::REBIRTH;
-				bsa.newAmount = resurrectedStackCount; //risky?
-				bsa.newHP = MaxHealth(); //resore full health
+				//TODO: use StackHealedOrResurrected
+				bsa.newHealth.firstHPleft = MaxHealth();
+				bsa.newHealth.fullUnits = resurrectedStackCount - 1;
+				bsa.newHealth.resurrected = 0; //TODO: add one-battle rebirth?
 			}
 		}
 	}
-	else
-	{
-		bsa.newAmount = countToUse - bsa.killedAmount;
-	}
 }
 
 bool CStack::isMeleeAttackPossible(const CStack * attacker, const CStack * defender, BattleHex attackerPos /*= BattleHex::INVALID*/, BattleHex defenderPos /*= BattleHex::INVALID*/)
 {
-	if (!attackerPos.isValid())
-	{
+	if(!attackerPos.isValid())
 		attackerPos = attacker->position;
-	}
-	if (!defenderPos.isValid())
-	{
+	if(!defenderPos.isValid())
 		defenderPos = defender->position;
-	}
 
 	return
-		(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0)						//front <=> front
-		|| (attacker->doubleWide()									//back <=> front
-		&& BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos) >= 0)
-		|| (defender->doubleWide()									//front <=> back
-		&& BattleHex::mutualPosition(attackerPos, defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0)
+		(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0)//front <=> front
+		|| (attacker->doubleWide()//back <=> front
+			&& BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos) >= 0)
+		|| (defender->doubleWide()//front <=> back
+			&& BattleHex::mutualPosition(attackerPos, defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0)
 		|| (defender->doubleWide() && attacker->doubleWide()//back <=> back
-		&& BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0);
+			&& BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0);
 
 }
 
-bool CStack::ableToRetaliate() const //FIXME: crash after clone is killed
+bool CStack::ableToRetaliate() const
 {
 	return alive()
-		&& (counterAttacksPerformed < counterAttacksTotal() || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS))
-		&& !hasBonusOfType(Bonus::SIEGE_WEAPON)
-		&& !hasBonusOfType(Bonus::HYPNOTIZED)
-		&& !hasBonusOfType(Bonus::NO_RETALIATION);
-}
-
-ui8 CStack::counterAttacksTotal() const
-{
-	//after dispell bonus should remain during current round
-	ui8 val = 1 + valOfBonuses(Bonus::ADDITIONAL_RETALIATION);
-	vstd::amax(counterAttacksTotalCache, val);
-	return counterAttacksTotalCache;
-}
-
-si8 CStack::counterAttacksRemaining() const
-{
-	return counterAttacksTotal() - counterAttacksPerformed;
+		   && (counterAttacks.canUse() || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS))
+		   && !hasBonusOfType(Bonus::SIEGE_WEAPON)
+		   && !hasBonusOfType(Bonus::HYPNOTIZED)
+		   && !hasBonusOfType(Bonus::NO_RETALIATION);
 }
 
 std::string CStack::getName() const
 {
-	return (count > 1) ? type->namePl : type->nameSing; //War machines can't use base
+	return (health.getCount() == 1) ? type->nameSing : type->namePl; //War machines can't use base
 }
 
 bool CStack::isValidTarget(bool allowDead/* = false*/) const
@@ -442,9 +744,14 @@ bool CStack::isDead() const
 	return !alive() && !isGhost();
 }
 
+bool CStack::isClone() const
+{
+	return vstd::contains(state, EBattleStackState::CLONED);
+}
+
 bool CStack::isGhost() const
 {
-	return vstd::contains(state,EBattleStackState::GHOST);
+	return vstd::contains(state, EBattleStackState::GHOST);
 }
 
 bool CStack::isTurret() const
@@ -454,9 +761,9 @@ bool CStack::isTurret() const
 
 bool CStack::canBeHealed() const
 {
-	return firstHPleft < MaxHealth()
-		&& isValidTarget()
-		&& !hasBonusOfType(Bonus::SIEGE_WEAPON);
+	return getFirstHPleft() < MaxHealth()
+		   && isValidTarget()
+		   && !hasBonusOfType(Bonus::SIEGE_WEAPON);
 }
 
 void CStack::makeGhost()
@@ -467,26 +774,13 @@ void CStack::makeGhost()
 
 bool CStack::alive() const //determines if stack is alive
 {
-	return vstd::contains(state,EBattleStackState::ALIVE);
-}
-
-ui32 CStack::calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const
-{
-	if(!resurrect && !alive())
-	{
-		logGlobal->warnStream() <<"Attempt to heal corpse detected.";
-		return 0;
-	}
-
-	return std::min<ui32>(toHeal, MaxHealth() - firstHPleft + (resurrect ? (baseAmount - count) * MaxHealth() : 0));
+	return vstd::contains(state, EBattleStackState::ALIVE);
 }
 
 ui8 CStack::getSpellSchoolLevel(const CSpell * spell, int * outSelectedSchool) const
 {
 	int skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->id));
-
 	vstd::abetween(skill, 0, 3);
-
 	return skill;
 }
 
@@ -503,37 +797,91 @@ int CStack::getEffectLevel(const CSpell * spell) const
 
 int CStack::getEffectPower(const CSpell * spell) const
 {
-	return valOfBonuses(Bonus::CREATURE_SPELL_POWER) * count / 100;
+	return valOfBonuses(Bonus::CREATURE_SPELL_POWER) * health.getCount() / 100;
 }
 
 int CStack::getEnchantPower(const CSpell * spell) const
 {
 	int res = valOfBonuses(Bonus::CREATURE_ENCHANT_POWER);
-	if(res<=0)
+	if(res <= 0)
 		res = 3;//default for creatures
 	return res;
 }
 
 int CStack::getEffectValue(const CSpell * spell) const
 {
-	return valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spell->id.toEnum()) * count;
+	return valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spell->id.toEnum()) * health.getCount();
 }
 
 const PlayerColor CStack::getOwner() const
 {
-	return owner;
+	return battle->battleGetOwner(this);
 }
 
 void CStack::getCasterName(MetaString & text) const
 {
 	//always plural name in case of spell cast.
-	text.addReplacement(MetaString::CRE_PL_NAMES, type->idNumber.num);
+	addNameReplacement(text, true);
 }
 
-void CStack::getCastDescription(const CSpell * spell, const std::vector<const CStack*> & attacked, MetaString & text) const
+void CStack::getCastDescription(const CSpell * spell, const std::vector<const CStack *> & attacked, MetaString & text) const
 {
 	text.addTxt(MetaString::GENERAL_TXT, 565);//The %s casts %s
 	//todo: use text 566 for single creature
 	getCasterName(text);
 	text.addReplacement(MetaString::SPELL_NAME, spell->id.toEnum());
 }
+
+int32_t CStack::unitMaxHealth() const
+{
+	return MaxHealth();
+}
+
+int32_t CStack::unitBaseAmount() const
+{
+	return baseAmount;
+}
+
+void CStack::addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural) const
+{
+	if(boost::logic::indeterminate(plural))
+		serial = VLC->generaltexth->pluralText(serial, health.getCount());
+	else if(plural)
+		serial = VLC->generaltexth->pluralText(serial, 2);
+	else
+		serial = VLC->generaltexth->pluralText(serial, 1);
+
+	text.addTxt(type, serial);
+}
+
+void CStack::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const
+{
+	if(boost::logic::indeterminate(plural))
+		text.addCreReplacement(type->idNumber, health.getCount());
+	else if(plural)
+		text.addReplacement(MetaString::CRE_PL_NAMES, type->idNumber.num);
+	else
+		text.addReplacement(MetaString::CRE_SING_NAMES, type->idNumber.num);
+}
+
+std::string CStack::formatGeneralMessage(const int32_t baseTextId) const
+{
+	const int32_t textId = VLC->generaltexth->pluralText(baseTextId, health.getCount());
+
+	MetaString text;
+	text.addTxt(MetaString::GENERAL_TXT, textId);
+	text.addCreReplacement(type->idNumber, health.getCount());
+
+	return text.toString();
+}
+
+void CStack::setHealth(const CHealthInfo & value)
+{
+	health.reset();
+	health.fromInfo(value);
+}
+
+void CStack::setHealth(const CHealth & value)
+{
+	health = value;
+}

+ 182 - 36
lib/CStack.h

@@ -7,64 +7,186 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+
 #pragma once
 #include "battle/BattleHex.h"
 #include "CCreatureHandler.h"
 #include "mapObjects/CGHeroInstance.h" // for commander serialization
 
 struct BattleStackAttacked;
+struct BattleInfo;
+class CStack;
+class CHealthInfo;
+
+template <typename Quantity>
+class DLL_LINKAGE CStackResource
+{
+public:
+	CStackResource(const CStack * Owner):
+		owner(Owner)
+	{
+		reset();
+	}
+
+	virtual void reset()
+	{
+		used = 0;
+	};
+
+protected:
+	const CStack * owner;
+	Quantity used;
+};
+
+class DLL_LINKAGE CAmmo : public CStackResource<int32_t>
+{
+public:
+	CAmmo(const CStack * Owner, CSelector totalSelector);
+
+	int32_t available() const;
+	bool canUse(int32_t amount = 1) const;
+	virtual void reset() override;
+	virtual int32_t total() const;
+	virtual void use(int32_t amount = 1);
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		if(!h.saving)
+			reset();
+		h & used;
+	}
+protected:
+	CBonusProxy totalProxy;
+};
+
+class DLL_LINKAGE CShots : public CAmmo
+{
+public:
+	CShots(const CStack * Owner);
+	void use(int32_t amount = 1) override;
+};
+
+class DLL_LINKAGE CCasts : public CAmmo
+{
+public:
+	CCasts(const CStack * Owner);
+};
+
+class DLL_LINKAGE CRetaliations : public CAmmo
+{
+public:
+	CRetaliations(const CStack * Owner);
+	int32_t total() const override;
+	void reset() override;
+private:
+	mutable int32_t totalCache;
+};
+
+class DLL_LINKAGE IUnitHealthInfo
+{
+public:
+	virtual int32_t unitMaxHealth() const = 0;
+	virtual int32_t unitBaseAmount() const = 0;
+};
+
+class DLL_LINKAGE CHealth
+{
+public:
+	CHealth(const IUnitHealthInfo * Owner);
+	CHealth(const CHealth & other);
+
+	void init();
+	void reset();
+
+	void damage(int32_t & amount);
+	void heal(int32_t & amount, EHealLevel level, EHealPower power);
+
+	int32_t getCount() const;
+	int32_t getFirstHPleft() const;
+	int32_t getResurrected() const;
+
+	int64_t available() const;
+	int64_t total() const;
+
+	void toInfo(CHealthInfo & info) const;
+	void fromInfo(const CHealthInfo & info);
+
+	void takeResurrected();
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		if(!h.saving)
+			reset();
+		h & firstHPleft & fullUnits & resurrected;
+	}
+private:
+	void addResurrected(int32_t amount);
+	void setFromTotal(const int64_t totalHealth);
+	const IUnitHealthInfo * owner;
 
-class DLL_LINKAGE CStack : public CBonusSystemNode, public CStackBasicDescriptor, public ISpellCaster
+	int32_t firstHPleft;
+	int32_t fullUnits;
+	int32_t resurrected;
+};
+
+class DLL_LINKAGE CStack : public CBonusSystemNode, public ISpellCaster, public IUnitHealthInfo
 {
 public:
-	const CStackInstance *base; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc)
+	const CStackInstance * base; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc)
 
 	ui32 ID; //unique ID of stack
 	ui32 baseAmount;
-	ui32 firstHPleft; //HP of first creature in stack
-	PlayerColor owner; //owner - player colour (255 for neutrals)
+	const CCreature * type;
+
+	PlayerColor owner; //owner - player color (255 for neutrals)
 	SlotID slot;  //slot - position in garrison (may be 255 for neutrals/called creatures)
 	ui8 side;
 	BattleHex position; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower
-	///how many times this stack has been counterattacked this round
-	ui8 counterAttacksPerformed;
-	///cached total count of counterattacks; should be cleared each round;do not serialize
-	mutable ui8 counterAttacksTotalCache;
-	si16 shots; //how many shots left
-	ui8 casts; //how many casts left
-	TQuantity resurrected; // these units will be taken back after battle is over
+
+	CRetaliations counterAttacks;
+	CShots shots;
+	CCasts casts;
+	CHealth health;
+
 	///id of alive clone of this stack clone if any
 	si32 cloneID;
 	std::set<EBattleStackState::EBattleStackState> state;
-	//overrides
-	const CCreature* getCreature() const {return type;}
 
-	CStack(const CStackInstance *base, PlayerColor O, int I, ui8 Side, SlotID S); //c-tor
-	CStack(const CStackBasicDescriptor *stack, PlayerColor O, int I, ui8 Side, SlotID S = SlotID(255)); //c-tor
+	CStack(const CStackInstance * base, PlayerColor O, int I, ui8 Side, SlotID S); //c-tor
+	CStack(const CStackBasicDescriptor * stack, PlayerColor O, int I, ui8 Side, SlotID S = SlotID(255)); //c-tor
 	CStack(); //c-tor
 	~CStack();
+
+	int32_t getKilled() const;
+	int32_t getCount() const;
+	int32_t getFirstHPleft() const;
+	const CCreature * getCreature() const;
+
 	std::string nodeName() const override;
 
 	void init(); //set initial (invalid) values
-	void postInit(); //used to finish initialization when inheriting creature parameters is working
+	void localInit(BattleInfo * battleInfo);
 	std::string getName() const; //plural or singular
 	bool willMove(int turn = 0) const; //if stack has remaining move this turn
 	bool ableToRetaliate() const; //if stack can retaliate after attacked
-	///how many times this stack can counterattack in one round
-	ui8 counterAttacksTotal() const;
-	///how many times this stack can counterattack in one round more
-	si8 counterAttacksRemaining() const;
+
 	bool moved(int turn = 0) const; //if stack was already moved this turn
 	bool waited(int turn = 0) const;
+
+	bool canCast() const;
+	bool isCaster() const;
+
 	bool canMove(int turn = 0) const; //if stack can move
+
+	bool canShoot() const;
+	bool isShooter() const;
+
 	bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines
-	///returns actual heal value based on internal state
-	ui32 calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const;
+
 	ui32 level() const;
 	si32 magicResistance() const override; //include aura of resistance
 	std::vector<si32> activeSpells() const; //returns vector of active spell IDs sorted by time of cast
-	const CGHeroInstance *getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise
-	ui32 totalHealth() const; // total health for all creatures in stack;
+	const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise
 
 	static bool isMeleeAttackPossible(const CStack * attacker, const CStack * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID);
 
@@ -79,11 +201,17 @@ public:
 
 	BattleHex::EDir destShiftDir() const;
 
-	std::pair<int,int> countKilledByAttack(int damageReceived) const; //returns pair<killed count, new left HP>
-	void prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional<int> customCount = boost::none) const; //requires bsa.damageAmout filled
+	CHealth healthAfterAttacked(int32_t & damage) const;
+	CHealth healthAfterAttacked(int32_t & damage, const CHealth & customHealth) const;
+
+	CHealth healthAfterHealed(int32_t & toHeal, EHealLevel level, EHealPower power) const;
+
+	void prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand) const; //requires bsa.damageAmout filled
+	void prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand, const CHealth & customHealth) const; //requires bsa.damageAmout filled
 
 	///ISpellCaster
-	ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const override;
+
+	ui8 getSpellSchoolLevel(const CSpell * spell, int * outSelectedSchool = nullptr) const override;
 	ui32 getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const override;
 
 	///default spell school level for effect calculation
@@ -99,23 +227,36 @@ public:
 	int getEffectValue(const CSpell * spell) const override;
 
 	const PlayerColor getOwner() const override;
-
 	void getCasterName(MetaString & text) const override;
-
 	void getCastDescription(const CSpell * spell, const std::vector<const CStack *> & attacked, MetaString & text) const override;
 
+	///IUnitHealthInfo
+
+	int32_t unitMaxHealth() const override;
+	int32_t unitBaseAmount() const override;
+
+	///MetaStrings
+
+	void addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural = boost::logic::indeterminate) const;
+	void addNameReplacement(MetaString & text, const boost::logic::tribool & plural = boost::logic::indeterminate) const;
+	std::string formatGeneralMessage(const int32_t baseTextId) const;
+
+	///Non const API for NetPacks
+
 	///stack will be ghost in next battle state update
 	void makeGhost();
+	void setHealth(const CHealthInfo & value);
+	void setHealth(const CHealth & value);
 
-	template <typename Handler> void serialize(Handler &h, const int version)
+	template <typename Handler> void serialize(Handler & h, const int version)
 	{
 		assert(isIndependentNode());
-		h & static_cast<CBonusSystemNode&>(*this);
-		h & static_cast<CStackBasicDescriptor&>(*this);
-		h & ID & baseAmount & firstHPleft & owner & slot & side & position & state & counterAttacksPerformed
-			& shots & casts & count & resurrected;
+		h & static_cast<CBonusSystemNode &>(*this);
+		h & type;
+		h & ID & baseAmount & owner & slot & side & position & state;
+		h & shots & casts & counterAttacks & health;
 
-		const CArmedInstance *army = (base ? base->armyObj : nullptr);
+		const CArmedInstance * army = (base ? base->armyObj : nullptr);
 		SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID());
 
 		if(h.saving)
@@ -128,7 +269,7 @@ public:
 			if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER)
 			{
 				auto hero = dynamic_cast<const CGHeroInstance *>(army);
-				assert (hero);
+				assert(hero);
 				base = hero->commander;
 			}
 			else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT)
@@ -150,8 +291,13 @@ public:
 	}
 	bool alive() const;
 
+	bool isClone() const;
 	bool isDead() const;
 	bool isGhost() const; //determines if stack was removed
 	bool isValidTarget(bool allowDead = false) const; //non-turret non-ghost stacks (can be attacked or be object of magic effect)
 	bool isTurret() const;
+
+	friend class CShots; //for BattleInfo access
+private:
+	const BattleInfo * battle; //do not serialize
 };

+ 13 - 0
lib/GameConstants.h

@@ -1089,6 +1089,19 @@ enum class EMetaclass: ui8
 	RESOURCE
 };
 
+enum class EHealLevel: ui8
+{
+	HEAL,
+	RESURRECT,
+	OVERHEAL
+};
+
+enum class EHealPower : ui8
+{
+	ONE_BATTLE,
+	PERMANENT
+};
+
 // Typedef declarations
 typedef ui8 TFaction;
 typedef si64 TExpType;

+ 23 - 0
lib/HeroBonus.cpp

@@ -79,6 +79,29 @@ const std::map<std::string, TPropagatorPtr> bonusPropagatorMap =
 	{"GLOBAL_EFFECT", std::make_shared<CPropagatorNodeType>(CBonusSystemNode::GLOBAL_EFFECTS)}
 }; //untested
 
+///CBonusProxy
+CBonusProxy::CBonusProxy(const IBonusBearer * Target, CSelector Selector):
+	cachedLast(0), target(Target), selector(Selector), data()
+{
+
+}
+
+TBonusListPtr CBonusProxy::get() const
+{
+	if(CBonusSystemNode::treeChanged != cachedLast || !data)
+	{
+		//TODO: support limiters
+		data = target->getAllBonuses(selector, nullptr);
+		data->eliminateDuplicates();
+		cachedLast = CBonusSystemNode::treeChanged;
+	}
+	return data;
+}
+
+const BonusList * CBonusProxy::operator->() const
+{
+	return get().get();
+}
 
 #define BONUS_LOG_LINE(x) logBonus->traceStream() << x
 

+ 16 - 0
lib/HeroBonus.h

@@ -13,6 +13,7 @@
 
 class CCreature;
 struct Bonus;
+class IBonusBearer;
 class CBonusSystemNode;
 class ILimiter;
 class IPropagator;
@@ -63,7 +64,20 @@ public:
 	}
 };
 
+class DLL_LINKAGE CBonusProxy : public boost::noncopyable
+{
+public:
+	CBonusProxy(const IBonusBearer * Target, CSelector Selector);
 
+	TBonusListPtr get() const;
+
+	const BonusList * operator->() const;
+private:
+	mutable int cachedLast;
+	const IBonusBearer * target;
+	CSelector selector;
+	mutable TBonusListPtr data;
+};
 
 #define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix();
 
@@ -684,6 +698,8 @@ public:
 		BONUS_TREE_DESERIALIZATION_FIX
 		//h & parents & children;
 	}
+
+	friend class CBonusProxy;
 };
 
 namespace NBonus

+ 11 - 18
lib/NetPacks.h

@@ -1315,34 +1315,22 @@ struct BattleStackMoved : public CPackForClient
 struct StacksHealedOrResurrected : public CPackForClient
 {
 	StacksHealedOrResurrected()
-		:lifeDrain(false), tentHealing(false), drainedFrom(0), cure(false), canOverheal(false)
+		:lifeDrain(false), tentHealing(false), drainedFrom(0), cure(false)
 	{}
 
 	DLL_LINKAGE void applyGs(CGameState *gs);
 	void applyCl(CClient *cl);
 
-	struct HealInfo
-	{
-		ui32 stackID;
-		ui32 healedHP;
-		bool lowLevelResurrection; //in case this stack is resurrected by this heal, it will be marked as SUMMONED //TODO: replace with separate counter
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & stackID & healedHP & lowLevelResurrection;
-		}
-	};
-
-	std::vector<HealInfo> healedStacks;
+	std::vector<CHealthInfo> healedStacks;
 	bool lifeDrain; //if true, this heal is an effect of life drain or soul steal
 	bool tentHealing; //if true, than it's healing via First Aid Tent
 	si32 drainedFrom; //if life drain or soul steal - then stack life was drain from, if tentHealing - stack that is a healer
 	bool cure; //archangel cast also remove negative effects
-	bool canOverheal; //to allow healing over initial stack amount
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & healedStacks & lifeDrain & tentHealing & drainedFrom;
-		h & cure & canOverheal;
+		h & cure;
 	}
 };
 
@@ -1350,7 +1338,8 @@ struct BattleStackAttacked : public CPackForClient
 {
 	BattleStackAttacked():
 		stackAttacked(0), attackerID(0),
-		newAmount(0), newHP(0), killedAmount(0), damageAmount(0),
+		killedAmount(0), damageAmount(0),
+		newHealth(),
 		flags(0), effect(0), spellID(SpellID::NONE)
 	{};
 	void applyFirstCl(CClient * cl);
@@ -1358,7 +1347,9 @@ struct BattleStackAttacked : public CPackForClient
 	DLL_LINKAGE void applyGs(CGameState *gs);
 
 	ui32 stackAttacked, attackerID;
-	ui32 newAmount, newHP, killedAmount, damageAmount;
+	ui32 killedAmount;
+	si32 damageAmount;
+	CHealthInfo newHealth;
 	enum EFlags {KILLED = 1, EFFECT = 2/*deprecated */, SECONDARY = 4, REBIRTH = 8, CLONE_KILLED = 16, SPELL_EFFECT = 32 /*, BONUS_EFFECT = 64 */};
 	ui32 flags; //uses EFlags (above)
 	ui32 effect; //set only if flag EFFECT is set
@@ -1396,7 +1387,7 @@ struct BattleStackAttacked : public CPackForClient
 	}
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & stackAttacked & attackerID & newAmount & newHP & flags & killedAmount & damageAmount & effect
+		h & stackAttacked & attackerID & newHealth & flags & killedAmount & damageAmount & effect
 			& healedStacks;
 		h & spellID;
 	}
@@ -1540,10 +1531,12 @@ struct SetStackEffect : public CPackForClient
 	std::vector<Bonus> cumulativeEffects; //bonuses to apply
 	std::vector<std::pair<ui32, Bonus> > cumulativeUniqueBonuses; //bonuses per single stack
 
+	std::vector<MetaString> battleLog;
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & stacks & effect & uniqueBonuses;
 		h & cumulativeEffects & cumulativeUniqueBonuses;
+		h & battleLog;
 	}
 };
 

+ 19 - 0
lib/NetPacksBase.h

@@ -188,3 +188,22 @@ struct ArtifactLocation
 		h & artHolder & slot;
 	}
 };
+
+class CHealthInfo
+{
+public:
+	CHealthInfo():
+		stackId(0), delta(0), firstHPleft(0), fullUnits(0), resurrected(0)
+	{
+	}
+	uint32_t stackId;
+	int32_t delta;
+	int32_t firstHPleft;
+	int32_t fullUnits;
+	int32_t resurrected;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & stackId & delta & firstHPleft & fullUnits & resurrected;
+	}
+};

+ 85 - 96
lib/NetPacksLib.cpp

@@ -1247,12 +1247,11 @@ DLL_LINKAGE void BattleNextRound::applyGs(CGameState *gs)
 		s->state -= EBattleStackState::HAD_MORALE;
 		s->state -= EBattleStackState::FEAR;
 		s->state -= EBattleStackState::DRAINED_MANA;
-		s->counterAttacksPerformed = 0;
-		s->counterAttacksTotalCache = 0;
+		s->counterAttacks.reset();
 		// new turn effects
 		s->updateBonuses(Bonus::NTurns);
 
-		if(s->alive() && vstd::contains(s->state, EBattleStackState::CLONED))
+		if(s->alive() && s->isClone())
 		{
 			//cloned stack has special lifetime marker
 			//check it after bonuses updated in battleTurnPassed()
@@ -1280,36 +1279,40 @@ DLL_LINKAGE void BattleSetActiveStack::applyGs(CGameState *gs)
 
 DLL_LINKAGE void BattleTriggerEffect::applyGs(CGameState *gs)
 {
-	CStack *st = gs->curB->getStack(stackID);
-	switch (effect)
+	CStack * st = gs->curB->getStack(stackID);
+	assert(st);
+	switch(effect)
 	{
-		case Bonus::HP_REGENERATION:
-			st->firstHPleft += val;
-			vstd::amin (st->firstHPleft, (ui32)st->MaxHealth());
-			break;
-		case Bonus::MANA_DRAIN:
-		{
-			CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo));
-			st->state.insert (EBattleStackState::DRAINED_MANA);
-			h->mana -= val;
-			vstd::amax(h->mana, 0);
-			break;
-		}
-		case Bonus::POISON:
-		{
-			auto b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON)
-					.And(Selector::type(Bonus::STACK_HEALTH)));
-			if (b)
-				b->val = val;
-			break;
-		}
-		case Bonus::ENCHANTER:
-			break;
-		case Bonus::FEAR:
-			st->state.insert(EBattleStackState::FEAR);
-			break;
-		default:
-			logNetwork->warnStream() << "Unrecognized trigger effect type "<< effect;
+	case Bonus::HP_REGENERATION:
+	{
+		int32_t toHeal = val;
+		CHealth health = st->healthAfterHealed(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT);
+		st->setHealth(health);
+		break;
+	}
+	case Bonus::MANA_DRAIN:
+	{
+		CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo));
+		st->state.insert (EBattleStackState::DRAINED_MANA);
+		h->mana -= val;
+		vstd::amax(h->mana, 0);
+		break;
+	}
+	case Bonus::POISON:
+	{
+		auto b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON)
+				.And(Selector::type(Bonus::STACK_HEALTH)));
+		if (b)
+			b->val = val;
+		break;
+	}
+	case Bonus::ENCHANTER:
+		break;
+	case Bonus::FEAR:
+		st->state.insert(EBattleStackState::FEAR);
+		break;
+	default:
+		logNetwork->warnStream() << "Unrecognized trigger effect type "<< effect;
 	}
 }
 
@@ -1392,11 +1395,14 @@ void BattleStackMoved::applyGs(CGameState *gs)
 
 DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs)
 {
-	CStack *at = gs->curB->getStack(stackAttacked);
+	CStack * at = gs->curB->getStack(stackAttacked);
 	assert(at);
 	at->popBonuses(Bonus::UntilBeingAttacked);
-	at->count = newAmount;
-	at->firstHPleft = newHP;
+
+	if(willRebirth())
+		at->health.reset();//kill stack first
+	else
+		at->setHealth(newHealth);
 
 	if(killed())
 	{
@@ -1413,16 +1419,29 @@ DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs)
 		}
 	}
 	//life drain handling
-	for (auto & elem : healedStacks)
-	{
+	for(auto & elem : healedStacks)
 		elem.applyGs(gs);
-	}
-	if (willRebirth())
+
+	if(willRebirth())
 	{
-		at->casts--;
-		at->state.insert(EBattleStackState::ALIVE); //hmm?
+		//TODO: handle rebirth with StacksHealedOrResurrected
+		at->casts.use();
+		at->state.insert(EBattleStackState::ALIVE);
+		at->setHealth(newHealth);
+
+		//removing all spells effects
+		auto selector = [](const Bonus * b)
+		{
+			//Special case: DISRUPTING_RAY is "immune" to dispell
+			//Other even PERMANENT effects can be removed
+			if(b->source == Bonus::SPELL_EFFECT)
+				return b->sid != SpellID::DISRUPTING_RAY;
+			else
+				return false;
+		};
+		at->popBonuses(selector);
 	}
-	if (cloneKilled())
+	if(cloneKilled())
 	{
 		//"hide" killed creatures instead so we keep info about it
 		at->makeGhost();
@@ -1436,35 +1455,20 @@ DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs)
 
 	//killed summoned creature should be removed like clone
 	if(killed() && vstd::contains(at->state, EBattleStackState::SUMMONED))
-	{
 		at->makeGhost();
-	}
 }
 
-DLL_LINKAGE void BattleAttack::applyGs(CGameState *gs)
+DLL_LINKAGE void BattleAttack::applyGs(CGameState * gs)
 {
-	CStack *attacker = gs->curB->getStack(stackAttacking);
+	CStack * attacker = gs->curB->getStack(stackAttacking);
+	assert(attacker);
+
 	if(counter())
-		attacker->counterAttacksPerformed++;
+		attacker->counterAttacks.use();
 
 	if(shot())
-	{
-		//don't remove ammo if we have a working ammo cart
-		bool hasAmmoCart = false;
-		for(const CStack * st : gs->curB->stacks)
-		{
-			if(st->owner == attacker->owner && st->getCreature()->idNumber == CreatureID::AMMO_CART && st->alive())
-			{
-				hasAmmoCart = true;
-				break;
-			}
-		}
+		attacker->shots.use();
 
-		if (!hasAmmoCart)
-		{
-			attacker->shots--;
-		}
-	}
 	for(BattleStackAttacked & stackAttacked : bsa)
 		stackAttacked.applyGs(gs);
 
@@ -1619,7 +1623,8 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs)
 {
 	for(auto & elem : healedStacks)
 	{
-		CStack * changedStack = gs->curB->getStack(elem.stackID, false);
+		CStack * changedStack = gs->curB->getStack(elem.stackId, false);
+		assert(changedStack);
 
 		//checking if we resurrect a stack that is under a living stack
 		auto accessibility = gs->curB->getAccesibility();
@@ -1634,29 +1639,14 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs)
 		bool resurrected = !changedStack->alive(); //indicates if stack is resurrected or just healed
 		if(resurrected)
 		{
-			if(changedStack->count > 0 || changedStack->firstHPleft > 0)
-				logGlobal->warn("Dead stack %s with positive total HP %d", changedStack->nodeName(), changedStack->totalHealth());
+			if(auto totalHealth = changedStack->health.available())
+				logGlobal->warn("Dead stack %s with positive total HP %d", changedStack->nodeName(), totalHealth);
 
 			changedStack->state.insert(EBattleStackState::ALIVE);
 		}
-		int res;
-		if(canOverheal) //for example WoG ghost soul steal ability allows getting more units than before battle
-			res = elem.healedHP / changedStack->MaxHealth();
-		else
-			res = std::min(elem.healedHP / changedStack->MaxHealth() , changedStack->baseAmount - changedStack->count);
-		changedStack->count += res;
-		if(elem.lowLevelResurrection)
-			changedStack->resurrected += res;
-		changedStack->firstHPleft += elem.healedHP - res * changedStack->MaxHealth();
-		if(changedStack->firstHPleft > changedStack->MaxHealth())
-		{
-			changedStack->firstHPleft -= changedStack->MaxHealth();
-			if(changedStack->baseAmount > changedStack->count)
-			{
-				changedStack->count += 1;
-			}
-		}
-		vstd::amin(changedStack->firstHPleft, changedStack->MaxHealth());
+
+		changedStack->setHealth(elem);
+
 		if(resurrected)
 		{
 			//removing all spells effects
@@ -1674,7 +1664,7 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs)
 		else if(cure)
 		{
 			//removing all effects from negative spells
-			auto selector = [](const Bonus* b)
+			auto selector = [](const Bonus * b)
 			{
 				//Special case: DISRUPTING_RAY is "immune" to dispell
 				//Other even PERMANENT effects can be removed
@@ -1795,7 +1785,7 @@ DLL_LINKAGE void BattleStacksRemoved::applyGs(CGameState *gs)
 DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs)
 {
 	newStackID = 0;
-	if (!BattleHex(pos).isValid())
+	if(!BattleHex(pos).isValid())
 	{
 		logNetwork->warnStream() << "No place found for new stack!";
 		return;
@@ -1803,33 +1793,32 @@ DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs)
 
 	CStackBasicDescriptor csbd(creID, amount);
 	CStack * addedStack = gs->curB->generateNewStack(csbd, side, SlotID::SUMMONED_SLOT_PLACEHOLDER, pos); //TODO: netpacks?
-	if (summoned)
+	if(summoned)
 		addedStack->state.insert(EBattleStackState::SUMMONED);
 
-	gs->curB->localInitStack(addedStack);
-	gs->curB->stacks.push_back(addedStack); //the stack is not "SUMMONED", it is permanent
+	addedStack->localInit(gs->curB.get());
+	gs->curB->stacks.push_back(addedStack);
 
 	newStackID = addedStack->ID;
 }
 
-DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs)
+DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState * gs)
 {
 	CStack * stack = gs->curB->getStack(stackID);
-	switch (which)
+	switch(which)
 	{
 		case CASTS:
 		{
-			if (absolute)
-				stack->casts = val;
+			if(absolute)
+				logNetwork->error("Can not change casts in absolute mode");
 			else
-				stack->casts += val;
-			vstd::amax(stack->casts, 0);
+				stack->casts.use(-val);
 			break;
 		}
 		case ENCHANTER_COUNTER:
 		{
 			auto & counter = gs->curB->sides[gs->curB->whatSide(stack->owner)].enchanterCounter;
-			if (absolute)
+			if(absolute)
 				counter = val;
 			else
 				counter += val;

+ 3 - 6
lib/battle/BattleAttackInfo.cpp

@@ -9,10 +9,10 @@
  */
 #include "StdInc.h"
 #include "BattleAttackInfo.h"
-#include "../CStack.h"
 
 
-BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defender, bool Shooting)
+BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defender, bool Shooting):
+	attackerHealth(Attacker->health), defenderHealth(Defender->health)
 {
 	attacker = Attacker;
 	defender = Defender;
@@ -23,9 +23,6 @@ BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defen
 	attackerPosition = Attacker->position;
 	defenderPosition = Defender->position;
 
-	attackerCount = Attacker->count;
-	defenderCount = Defender->count;
-
 	shooting = Shooting;
 	chargedFields = 0;
 
@@ -41,7 +38,7 @@ BattleAttackInfo BattleAttackInfo::reverse() const
 	std::swap(ret.attacker, ret.defender);
 	std::swap(ret.attackerBonuses, ret.defenderBonuses);
 	std::swap(ret.attackerPosition, ret.defenderPosition);
-	std::swap(ret.attackerCount, ret.defenderCount);
+	std::swap(ret.attackerHealth, ret.defenderHealth);
 
 	ret.shooting = false;
 	ret.chargedFields = 0;

+ 3 - 2
lib/battle/BattleAttackInfo.h

@@ -9,8 +9,8 @@
  */
 #pragma once
 #include "BattleHex.h"
+#include "../CStack.h"
 
-class CStack;
 class IBonusBearer;
 
 struct DLL_LINKAGE BattleAttackInfo
@@ -19,7 +19,8 @@ struct DLL_LINKAGE BattleAttackInfo
 	const CStack *attacker, *defender;
 	BattleHex attackerPosition, defenderPosition;
 
-	int attackerCount, defenderCount;
+	CHealth attackerHealth, defenderHealth;
+
 	bool shooting;
 	int chargedFields;
 

+ 14 - 25
lib/battle/BattleInfo.cpp

@@ -78,15 +78,22 @@ std::pair< std::vector<BattleHex>, int > BattleInfo::getPath(BattleHex start, Ba
 	return std::make_pair(path, reachability.distances[dest]);
 }
 
-ui32 BattleInfo::calculateDmg(const CStack* attacker, const CStack* defender,
+ui32 BattleInfo::calculateDmg(const CStack * attacker, const CStack * defender,
 	bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg, CRandomGenerator & rand)
 {
-	TDmgRange range = calculateDmgRange(attacker, defender, shooting, charge, lucky, unlucky, deathBlow, ballistaDoubleDmg);
+	BattleAttackInfo bai(attacker, defender, shooting);
+	bai.chargedFields = charge;
+	bai.luckyHit = lucky;
+	bai.unluckyHit = unlucky;
+	bai.deathBlow = deathBlow;
+	bai.ballistaDoubleDamage = ballistaDoubleDmg;
+
+	TDmgRange range = calculateDmgRange(bai);
 
 	if(range.first != range.second)
 	{
 		ui32 sum = 0;
-		ui32 howManyToAv = std::min<ui32>(10, attacker->count);
+		ui32 howManyToAv = std::min<ui32>(10, attacker->getCount());
 		for(int g=0; g<howManyToAv; ++g)
 			sum += (ui32)rand.nextInt(range.first, range.second);
 
@@ -101,9 +108,8 @@ void BattleInfo::calculateCasualties(std::map<ui32,si32> * casualties) const
 	for(auto & elem : stacks)//setting casualties
 	{
 		const CStack * const st = elem;
-		si32 killed = (st->alive() ? (st->baseAmount - st->count + st->resurrected) : st->baseAmount);
-		vstd::amax(killed, 0);
-		if(killed)
+		si32 killed = st->getKilled();
+		if(killed > 0)
 			casualties[st->side][st->getCreature()->idNumber] += killed;
 	}
 }
@@ -140,29 +146,12 @@ void BattleInfo::localInit()
 		armyObj->attachTo(this);
 	}
 
-	for(CStack *s : stacks)
-		localInitStack(s);
+	for(CStack * s : stacks)
+		s->localInit(this);
 
 	exportBonuses();
 }
 
-void BattleInfo::localInitStack(CStack * s)
-{
-	s->exportBonuses();
-	if(s->base) //stack originating from "real" stack in garrison -> attach to it
-	{
-		s->attachTo(const_cast<CStackInstance*>(s->base));
-	}
-	else //attach directly to obj to which stack belongs and creature type
-	{
-		CArmedInstance *army = battleGetArmyObject(s->side);
-		s->attachTo(army);
-		assert(s->type);
-		s->attachTo(const_cast<CCreature*>(s->type));
-	}
-	s->postInit();
-}
-
 namespace CGH
 {
 	static void readBattlePositions(const JsonNode &node, std::vector< std::vector<int> > & dest)

+ 0 - 1
lib/battle/BattleInfo.h

@@ -72,7 +72,6 @@ struct DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallb
 
 	void localInit();
 
-	void localInitStack(CStack * s);
 	static BattleInfo * setupBattle(int3 tile, ETerrainType terrain, BFieldType battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town);
 	//bool hasNativeStack(ui8 side) const;
 

+ 14 - 35
lib/battle/CBattleInfoCallback.cpp

@@ -529,22 +529,14 @@ bool CBattleInfoCallback::battleCanShoot(const CStack * stack, BattleHex dest) c
 	if(stack->getCreature()->idNumber == CreatureID::CATAPULT && dst) //catapult cannot attack creatures
 		return false;
 
-	if(stack->hasBonusOfType(Bonus::SHOOTER)//it's shooter
-	&& battleMatchOwner(stack, dst)
-	&& dst->alive()
-	&& (!battleIsStackBlocked(stack) || stack->hasBonusOfType(Bonus::FREE_SHOOTING))
-	&& stack->shots
-	)
+	if(stack->canShoot()
+		&& battleMatchOwner(stack, dst)
+		&& dst->alive()
+		&& (!battleIsStackBlocked(stack) || stack->hasBonusOfType(Bonus::FREE_SHOOTING)))
 		return true;
 	return false;
 }
 
-TDmgRange CBattleInfoCallback::calculateDmgRange(const CStack* attacker, const CStack* defender, bool shooting,
-												 ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const
-{
-	return calculateDmgRange(attacker, defender, attacker->count, shooting, charge, lucky, unlucky, deathBlow, ballistaDoubleDmg);
-}
-
 TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const
 {
 	auto battleBonusValue = [&](const IBonusBearer * bearer, CSelector selector) -> int
@@ -559,8 +551,8 @@ TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info)
 	};
 
 	double additiveBonus = 1.0, multBonus = 1.0,
-			minDmg = info.attackerBonuses->getMinDamage() * info.attackerCount,//TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT
-			maxDmg = info.attackerBonuses->getMaxDamage() * info.attackerCount;
+			minDmg = info.attackerBonuses->getMinDamage() * info.attackerHealth.getCount(),//TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT
+			maxDmg = info.attackerBonuses->getMaxDamage() * info.attackerHealth.getCount();
 
 	const CCreature *attackerType = info.attacker->getCreature(),
 			*defenderType = info.defender->getCreature();
@@ -774,19 +766,6 @@ TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info)
 	return returnedVal;
 }
 
-TDmgRange CBattleInfoCallback::calculateDmgRange(const CStack* attacker, const CStack* defender, TQuantity attackerCount,
-												bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const
-{
-	BattleAttackInfo bai(attacker, defender, shooting);
-	bai.attackerCount = attackerCount;
-	bai.chargedFields = charge;
-	bai.luckyHit = lucky;
-	bai.unluckyHit = unlucky;
-	bai.deathBlow = deathBlow;
-	bai.ballistaDoubleDamage = ballistaDoubleDmg;
-	return calculateDmgRange(bai);
-}
-
 TDmgRange CBattleInfoCallback::battleEstimateDamage(CRandomGenerator & rand, const CStack * attacker, const CStack * defender, TDmgRange * retaliationDmg) const
 {
 	RETURN_IF_NOT_BATTLE(std::make_pair(0, 0));
@@ -816,10 +795,10 @@ std::pair<ui32, ui32> CBattleInfoCallback::battleEstimateDamage(CRandomGenerator
 			{
 				BattleStackAttacked bsa;
 				bsa.damageAmount = ret.*pairElems[i];
-				bai.defender->prepareAttacked(bsa, rand, bai.defenderCount);
+				bai.defender->prepareAttacked(bsa, rand, bai.defenderHealth);
 
 				auto retaliationAttack = bai.reverse();
-				retaliationAttack.attackerCount = bsa.newAmount;
+				retaliationAttack.attackerHealth = retaliationAttack.attacker->healthAfterAttacked(bsa.damageAmount);
 				retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i];
 			}
 		}
@@ -1494,7 +1473,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		{
 			auto walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack
 			{
-				return !stack->shots;
+				return !stack->canShoot();
 			});
 
 			if (!walker)
@@ -1505,7 +1484,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		{
 			auto shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack
 			{
-				return stack->hasBonusOfType(Bonus::SHOOTER) && stack->shots;
+				return stack->canShoot();
 			});
 			if (!shooter)
 				continue;
@@ -1527,19 +1506,19 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		case SpellID::CURE: //only damaged units
 		{
 			//do not cast on affected by debuffs
-			if (subject->firstHPleft >= subject->MaxHealth())
+			if(!subject->canBeHealed())
 				continue;
 		}
 			break;
 		case SpellID::BLOODLUST:
 		{
-			if (subject->shots) //if can shoot - only if enemy uits are adjacent
+			if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent
 				continue;
 		}
 			break;
 		case SpellID::PRECISION:
 		{
-			if (!(subject->hasBonusOfType(Bonus::SHOOTER) && subject->shots))
+			if(!subject->canShoot())
 				continue;
 		}
 			break;
@@ -1611,7 +1590,7 @@ int CBattleInfoCallback::battleGetSurrenderCost(PlayerColor Player) const
 	double discount = 0;
 	for(const CStack * s : battleAliveStacks(side.get()))
 		if(s->base) //we pay for our stack that comes from our army slots - condition eliminates summoned cres and war machines
-			ret += s->getCreature()->cost[Res::GOLD] * s->count;
+			ret += s->getCreature()->cost[Res::GOLD] * s->getCount(); //todo: extract CStack method
 
 	if(const CGHeroInstance * h = battleGetFightingHero(side.get()))
 		discount += h->valOfBonuses(Bonus::SURRENDER_DISCOUNT);

+ 0 - 2
lib/battle/CBattleInfoCallback.h

@@ -59,8 +59,6 @@ public:
 	std::set<const CStack*> batteAdjacentCreatures (const CStack * stack) const;
 
 	TDmgRange calculateDmgRange(const BattleAttackInfo & info) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair <min dmg, max dmg>
-	TDmgRange calculateDmgRange(const CStack* attacker, const CStack* defender, TQuantity attackerCount, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair <min dmg, max dmg>
-	TDmgRange calculateDmgRange(const CStack* attacker, const CStack* defender, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair <min dmg, max dmg>
 
 	//hextowallpart //int battleGetWallUnderHex(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found
 	std::pair<ui32, ui32> battleEstimateDamage(CRandomGenerator & rand, const BattleAttackInfo & bai, std::pair<ui32, ui32> * retaliationDmg = nullptr) const; //estimates damage dealt by attacker to defender; it may be not precise especially when stack has randomly working bonuses; returns pair <min dmg, max dmg>

+ 10 - 1
lib/battle/CBattleInfoEssentials.cpp

@@ -360,8 +360,17 @@ bool CBattleInfoEssentials::battleMatchOwner(const CStack * attacker, const CSta
 		return true;
 	else if(attacker == defender)
 		return positivness;
+	else
+		return battleMatchOwner(battleGetOwner(attacker), defender, positivness);
+}
+
+bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const CStack * defender, const boost::logic::tribool positivness /* = false*/) const
+{
+	RETURN_IF_NOT_BATTLE(false);
+	if(boost::logic::indeterminate(positivness))
+		return true;
 	else if(defender->owner != battleGetOwner(defender))
 		return true; //mind controlled unit is attackable for both sides
 	else
-		return (battleGetOwner(attacker) == battleGetOwner(defender)) == positivness;
+		return (attacker == battleGetOwner(defender)) == positivness;
 }

+ 1 - 0
lib/battle/CBattleInfoEssentials.h

@@ -101,4 +101,5 @@ public:
 	///check that stacks are controlled by same|other player(s) depending on positiveness
 	///mind control included
 	bool battleMatchOwner(const CStack * attacker, const CStack * defender, const boost::logic::tribool positivness = false) const;
+	bool battleMatchOwner(const PlayerColor & attacker, const CStack * defender, const boost::logic::tribool positivness = false) const;
 };

+ 2 - 3
lib/mapObjects/CGHeroInstance.cpp

@@ -366,7 +366,6 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor *dst /*=
 			if(dst != this)
 				continue;
 
-			int slot = -1;
 			ArtifactID aid = creature->warMachine;
 			const CArtifact * art = aid.toArtifact();
 
@@ -1080,7 +1079,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 		const CreatureID creatureTypes[] = {CreatureID::SKELETON, CreatureID::WALKING_DEAD, CreatureID::WIGHTS, CreatureID::LICHES};
 		const bool improvedNecromancy = hasBonusOfType(Bonus::IMPROVED_NECROMANCY);
 		const CCreature *raisedUnitType = VLC->creh->creatures[creatureTypes[improvedNecromancy ? necromancyLevel : 0]];
-		const ui32 raisedUnitHP = raisedUnitType->valOfBonuses(Bonus::STACK_HEALTH);
+		const ui32 raisedUnitHP = raisedUnitType->MaxHealth();
 
 		//calculate creatures raised from each defeated stack
 		for (auto & casualtie : casualties)
@@ -1088,7 +1087,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 			// Get lost enemy hit points convertible to units.
 			CCreature * c = VLC->creh->creatures[casualtie.first];
 
-			const ui32 raisedHP = c->valOfBonuses(Bonus::STACK_HEALTH) * casualtie.second * necromancySkill;
+			const ui32 raisedHP = c->MaxHealth() * casualtie.second * necromancySkill;
 			raisedUnits += std::min<ui32>(raisedHP / raisedUnitHP, casualtie.second * necromancySkill); //limit to % of HP and % of original stack count
 		}
 

+ 1 - 1
lib/mapObjects/MiscObjects.cpp

@@ -1941,7 +1941,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 			if(drown)
 			{
 				cb->changeStackCount(StackLocation(h, i->first), -drown);
-				xp += drown * i->second->type->valOfBonuses(Bonus::STACK_HEALTH);
+				xp += drown * i->second->type->MaxHealth();
 			}
 		}
 

+ 1 - 1
lib/serializer/CSerializer.h

@@ -12,7 +12,7 @@
 #include "../ConstTransitivePtr.h"
 #include "../GameConstants.h"
 
-const ui32 SERIALIZATION_VERSION = 774;
+const ui32 SERIALIZATION_VERSION = 775;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 753;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 57 - 39
lib/spells/BattleSpellMechanics.cpp

@@ -26,21 +26,25 @@ HealingSpellMechanics::HealingSpellMechanics(const CSpell * s):
 void HealingSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
 {
 	EHealLevel healLevel = getHealLevel(parameters.effectLevel);
+	EHealPower healPower = getHealPower(parameters.effectLevel);
+
 	int hpGained = calculateHealedHP(env, parameters, ctx);
 	StacksHealedOrResurrected shr;
 	shr.lifeDrain = false;
 	shr.tentHealing = false;
+
 	//special case for Archangel
 	shr.cure = parameters.mode == ECastingMode::CREATURE_ACTIVE_CASTING && owner->id == SpellID::RESURRECTION;
 
-	const bool resurrect = (healLevel != EHealLevel::HEAL);
 	for(auto & attackedCre : ctx.attackedCres)
 	{
-		StacksHealedOrResurrected::HealInfo hi;
-		hi.stackID = (attackedCre)->ID;
-		int stackHPgained = parameters.caster->getSpellBonus(owner, hpGained, attackedCre);
-		hi.healedHP = attackedCre->calculateHealedHealthPoints(stackHPgained, resurrect);
-		hi.lowLevelResurrection = (healLevel == EHealLevel::RESURRECT);
+		int32_t stackHPgained = parameters.caster->getSpellBonus(owner, hpGained, attackedCre);
+		CHealth health = attackedCre->healthAfterHealed(stackHPgained, healLevel, healPower);
+
+		CHealthInfo hi;
+		health.toInfo(hi);
+		hi.stackId = attackedCre->ID;
+		hi.delta = stackHPgained;
 		shr.healedStacks.push_back(hi);
 	}
 	if(!shr.healedStacks.empty())
@@ -147,7 +151,7 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, const
 	bsa.side = parameters.casterSide;
 	bsa.summoned = true;
 	bsa.pos = parameters.cb->getAvaliableHex(bsa.creID, parameters.casterSide);
-	bsa.amount = clonedStack->count;
+	bsa.amount = clonedStack->getCount();
 	env->sendAndApply(&bsa);
 
 	BattleSetStackProperty ssp;
@@ -174,19 +178,16 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, const
 ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const
 {
 	//can't clone already cloned creature
-	if(vstd::contains(obj->state, EBattleStackState::CLONED))
+	if(obj->isClone())
 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
+	//can`t clone if old clone still alive
 	if(obj->cloneID != -1)
 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 	ui8 schoolLevel;
 	if(caster)
-	{
 		schoolLevel = caster->getEffectLevel(owner);
-	}
 	else
-	{
 		schoolLevel = 3;//todo: remove
-	}
 
 	if(schoolLevel < 3)
 	{
@@ -211,11 +212,16 @@ void CureMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * pac
 	doDispell(battle, packet, dispellSelector);
 }
 
-HealingSpellMechanics::EHealLevel CureMechanics::getHealLevel(int effectLevel) const
+EHealLevel CureMechanics::getHealLevel(int effectLevel) const
 {
 	return EHealLevel::HEAL;
 }
 
+EHealPower CureMechanics::getHealPower(int effectLevel) const
+{
+	return EHealPower::PERMANENT;
+}
+
 bool CureMechanics::dispellSelector(const Bonus * b)
 {
 	if(b->source == Bonus::SPELL_EFFECT)
@@ -436,10 +442,10 @@ ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const I
 	if(nullptr != caster)
 	{
 		//TODO: what with other creatures casting hypnotize, Faerie Dragons style?
-		ui32 subjectHealth = obj->totalHealth();
+		int64_t subjectHealth = obj->health.available();
 		//apply 'damage' bonus for hypnotize, including hero specialty
-		ui32 maxHealth = caster->getSpellBonus(owner, owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)), obj);
-		if (subjectHealth > maxHealth)
+		int64_t maxHealth = caster->getSpellBonus(owner, owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)), obj);
+		if(subjectHealth > maxHealth)
 			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 	}
 	return DefaultSpellMechanics::isImmuneByStack(caster, obj);
@@ -798,13 +804,18 @@ RisingSpellMechanics::RisingSpellMechanics(const CSpell * s):
 {
 }
 
-HealingSpellMechanics::EHealLevel RisingSpellMechanics::getHealLevel(int effectLevel) const
+EHealLevel RisingSpellMechanics::getHealLevel(int effectLevel) const
+{
+	return EHealLevel::RESURRECT;
+}
+
+EHealPower RisingSpellMechanics::getHealPower(int effectLevel) const
 {
 	//this may be even distinct class
 	if((effectLevel <= 1) && (owner->id == SpellID::RESURRECTION))
-		return EHealLevel::RESURRECT;
-
-	return EHealLevel::TRUE_RESURRECT;
+		return EHealPower::ONE_BATTLE;
+	else
+		return EHealPower::PERMANENT;
 }
 
 ///SacrificeMechanics
@@ -889,7 +900,7 @@ int SacrificeMechanics::calculateHealedHP(const SpellCastEnvironment* env, const
 		return 0;
 	}
 
-	return (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->count;
+	return (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->getCount();
 }
 
 bool SacrificeMechanics::requiresCreatureTarget() const
@@ -905,22 +916,23 @@ SpecialRisingSpellMechanics::SpecialRisingSpellMechanics(const CSpell * s):
 
 ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const
 {
+	auto mainFilter = [cb, ctx, this](const CStack * s) -> bool
+	{
+		const bool ownerMatches = !ctx.ti.smart || cb->battleMatchOwner(ctx.caster->getOwner(), s, owner->getPositiveness());
+		return ownerMatches && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s);
+	};
 	//find alive possible target
-	const CStack * stackToHeal = cb->getStackIf([ctx, this](const CStack * s)
+	const CStack * stackToHeal = cb->getStackIf([mainFilter](const CStack * s)
 	{
-		const bool ownerMatches = !ctx.ti.smart || s->owner == ctx.caster->getOwner();
-
-		return ownerMatches && s->isValidTarget(false) && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s);
+		return s->isValidTarget(false) && mainFilter(s);
 	});
 
 	if(nullptr == stackToHeal)
 	{
 		//find dead possible target if there is no alive target
-		stackToHeal = cb->getStackIf([ctx, this](const CStack * s)
+		stackToHeal = cb->getStackIf([mainFilter](const CStack * s)
 		{
-			const bool ownerMatches = !ctx.ti.smart || s->owner == ctx.caster->getOwner();
-
-			return ownerMatches && s->isValidTarget(true) && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s);
+			return s->isValidTarget(true) && mainFilter(s);
 		});
 
 		//we have found dead target
@@ -930,7 +942,7 @@ ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::canBeCast(cons
 			{
 				const CStack * other = cb->getStackIf([hex, stackToHeal](const CStack * s)
 				{
-					return s->isValidTarget(false) && s->coversPos(hex) && s != stackToHeal;
+					return s->isValidTarget(true) && s->coversPos(hex) && s != stackToHeal;
 				});
 				if(nullptr != other)
 					return ESpellCastProblem::NO_APPROPRIATE_TARGET;//alive stack blocks resurrection
@@ -949,16 +961,22 @@ ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::isImmuneByStac
 	// following does apply to resurrect and animate dead(?) only
 	// for sacrifice health calculation and health limit check don't matter
 
-	if(obj->count >= obj->baseAmount)
+	if(obj->getCount() >= obj->baseAmount)
 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 
-	//FIXME: Archangels can cast immune stack and this should be applied for them and not hero
-//	if(caster)
-//	{
-//		auto maxHealth = calculateHealedHP(caster, obj, nullptr);
-//		if (maxHealth < obj->MaxHealth()) //must be able to rise at least one full creature
-//			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
-//	}
+	//FIXME: code duplication with BattleSpellCastParameters
+	auto getEffectValue = [&]() -> si32
+	{
+		si32 effectValue = caster->getEffectValue(owner);
+		return (effectValue == 0) ? owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)) : effectValue;
+	};
+
+	if(caster)
+	{
+		auto maxHealth = getEffectValue();
+		if (maxHealth < obj->MaxHealth()) //must be able to rise at least one full creature
+			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
+	}
 
 	return DefaultSpellMechanics::isImmuneByStack(caster,obj);
 }
@@ -983,7 +1001,7 @@ ESpellCastProblem::ESpellCastProblem SummonMechanics::canBeCast(const CBattleInf
 	{
 		return (st->owner == caster->getOwner())
 			&& (vstd::contains(st->state, EBattleStackState::SUMMONED))
-			&& (!vstd::contains(st->state, EBattleStackState::CLONED))
+			&& (!st->isClone())
 			&& (st->getCreature()->idNumber != creatureToSummon);
 	});
 

+ 4 - 8
lib/spells/BattleSpellMechanics.h

@@ -18,18 +18,12 @@ class SpellCreatedObstacle;
 class DLL_LINKAGE HealingSpellMechanics : public DefaultSpellMechanics
 {
 public:
-	enum class EHealLevel
-	{
-		HEAL,
-		RESURRECT,
-		TRUE_RESURRECT
-	};
-
 	HealingSpellMechanics(const CSpell * s);
 protected:
 	void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override;
 	virtual int calculateHealedHP(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
 	virtual EHealLevel getHealLevel(int effectLevel) const = 0;
+	virtual EHealPower getHealPower(int effectLevel) const = 0;
 };
 
 class DLL_LINKAGE AntimagicMechanics : public DefaultSpellMechanics
@@ -63,6 +57,7 @@ public:
 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override final;
 	ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override;
 	EHealLevel getHealLevel(int effectLevel) const override final;
+	EHealPower getHealPower(int effectLevel) const override final;
 private:
     static bool dispellSelector(const Bonus * b);
 };
@@ -177,7 +172,8 @@ class DLL_LINKAGE RisingSpellMechanics : public HealingSpellMechanics
 {
 public:
 	RisingSpellMechanics(const CSpell * s);
-	EHealLevel getHealLevel(int effectLevel) const override;
+	EHealLevel getHealLevel(int effectLevel) const override final;
+	EHealPower getHealPower(int effectLevel) const override final;
 };
 
 class DLL_LINKAGE SacrificeMechanics : public RisingSpellMechanics

+ 24 - 54
lib/spells/CDefaultSpellMechanics.cpp

@@ -346,24 +346,11 @@ void DefaultSpellMechanics::battleLog(std::vector<MetaString> & logLines, const
 
 	auto attackedStack = attacked.at(0);
 
-	auto getPluralFormat = [attackedStack](const int baseTextID) -> si32
-	{
-		return attackedStack->count > 1 ? baseTextID + 1 : baseTextID;
-	};
-
-	auto logSimple = [attackedStack, &logLines, getPluralFormat](const int baseTextID)
-	{
-		MetaString line;
-		line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(baseTextID));
-		line.addReplacement(*attackedStack);
-		logLines.push_back(line);
-	};
-
-	auto logPlural = [attackedStack, &logLines, getPluralFormat](const int baseTextID)
+	auto addLogLine = [attackedStack, &logLines](const int baseTextID, const boost::logic::tribool & plural)
 	{
 		MetaString line;
-		line.addTxt(MetaString::GENERAL_TXT, baseTextID);
-		line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num);
+		attackedStack->addText(line, MetaString::GENERAL_TXT, baseTextID, plural);
+		attackedStack->addNameReplacement(line, plural);
 		logLines.push_back(line);
 	};
 
@@ -372,26 +359,26 @@ void DefaultSpellMechanics::battleLog(std::vector<MetaString> & logLines, const
 	switch(owner->id)
 	{
 	case SpellID::STONE_GAZE:
-		logSimple(558);
+		addLogLine(558, boost::logic::indeterminate);
 		break;
 	case SpellID::POISON:
-		logSimple(561);
+		addLogLine(561, boost::logic::indeterminate);
 		break;
 	case SpellID::BIND:
-		logPlural(560);//Roots and vines bind the %s to the ground!
+		addLogLine(-560, true);//"Roots and vines bind the %s to the ground!"
 		break;
 	case SpellID::DISEASE:
-		logSimple(553);
+		addLogLine(553, boost::logic::indeterminate);
 		break;
 	case SpellID::PARALYZE:
-		logSimple(563);
+		addLogLine(563, boost::logic::indeterminate);
 		break;
 	case SpellID::AGE:
 		{
-			//The %s shrivel with age, and lose %d hit points."
+			//"The %s shrivel with age, and lose %d hit points."
 			MetaString line;
-			line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(551));
-			line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num);
+			attackedStack->addText(line, MetaString::GENERAL_TXT, 551);
+			attackedStack->addNameReplacement(line);
 
 			//todo: display effect from only this cast
 			TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH));
@@ -403,7 +390,7 @@ void DefaultSpellMechanics::battleLog(std::vector<MetaString> & logLines, const
 		break;
 	case SpellID::THUNDERBOLT:
 		{
-			logPlural(367);
+			addLogLine(-367, true);
 			MetaString line;
 			//todo: handle newlines in metastring
 			std::string text = VLC->generaltexth->allTexts[343].substr(1, VLC->generaltexth->allTexts[343].size() - 1); //Does %d points of damage.
@@ -413,22 +400,22 @@ void DefaultSpellMechanics::battleLog(std::vector<MetaString> & logLines, const
 		}
 		break;
 	case SpellID::DISPEL_HELPFUL_SPELLS:
-		logPlural(555);
+		addLogLine(-555, true);
 		break;
 	case SpellID::DEATH_STARE:
-		if (damageToDisplay > 0)
+		if(damageToDisplay > 0)
 		{
 			MetaString line;
-			if (damageToDisplay > 1)
+			if(damageToDisplay > 1)
 			{
 				line.addTxt(MetaString::GENERAL_TXT, 119); //%d %s die under the terrible gaze of the %s.
 				line.addReplacement(damageToDisplay);
-				line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num);
+				attackedStack->addNameReplacement(line, true);
 			}
 			else
 			{
 				line.addTxt(MetaString::GENERAL_TXT, 118); //One %s dies under the terrible gaze of the %s.
-				line.addReplacement(MetaString::CRE_SING_NAMES, attackedStack->getCreature()->idNumber.num);
+				attackedStack->addNameReplacement(line, false);
 			}
 			parameters.caster->getCasterName(line);
 			logLines.push_back(line);
@@ -650,9 +637,9 @@ std::vector<const CStack *> DefaultSpellMechanics::getAffectedStacks(const CBatt
 	return attackedCres;
 }
 
-std::vector<const CStack *> DefaultSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback* cb, const SpellTargetingContext& ctx) const
+std::vector<const CStack *> DefaultSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const
 {
-	std::set<const CStack* > attackedCres;//std::set to exclude multiple occurrences of two hex creatures
+	std::set<const CStack *> attackedCres;//std::set to exclude multiple occurrences of two hex creatures
 
 	const auto side = cb->playerToSide(ctx.caster->getOwner());
 	if(!side)
@@ -665,10 +652,9 @@ std::vector<const CStack *> DefaultSpellMechanics::calculateAffectedStacks(const
 
 	auto mainFilter = [=](const CStack * s)
 	{
-		const bool positiveToAlly = owner->isPositive() && s->owner == ctx.caster->getOwner();
-		const bool negativeToEnemy = owner->isNegative() && s->owner != ctx.caster->getOwner();
+		const bool ownerMatches = cb->battleMatchOwner(ctx.caster->getOwner(), s, owner->getPositiveness());
 		const bool validTarget = s->isValidTarget(!ctx.ti.onlyAlive); //todo: this should be handled by spell class
-		const bool positivenessFlag = !ctx.ti.smart || owner->isNeutral() || positiveToAlly || negativeToEnemy;
+		const bool positivenessFlag = !ctx.ti.smart || ownerMatches;
 
 		return positivenessFlag && validTarget;
 	};
@@ -701,7 +687,7 @@ std::vector<const CStack *> DefaultSpellMechanics::calculateAffectedStacks(const
 	else if(ctx.ti.massive)
 	{
 		TStacks stacks = cb->battleGetStacksIf(mainFilter);
-		for (auto stack : stacks)
+		for(auto stack : stacks)
 			attackedCres.insert(stack);
 	}
 	else //custom range from attackedHexes
@@ -732,28 +718,12 @@ ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::canBeCast(const CBat
 	{
 		std::vector<const CStack *> affected = getAffectedStacks(cb, ctx);
 
-		//allow to cast spell if affects is at least one smart target
+		//allow to cast spell if it affects at least one smart target
 		bool targetExists = false;
 
 		for(const CStack * stack : affected)
 		{
-			bool casterStack = stack->owner == ctx.caster->getOwner();
-
-			switch (owner->positiveness)
-			{
-			case CSpell::POSITIVE:
-				if(casterStack)
-					targetExists = true;
-				break;
-			case CSpell::NEUTRAL:
-				targetExists = true;
-				break;
-			case CSpell::NEGATIVE:
-				if(!casterStack)
-					targetExists = true;
-				break;
-			}
-
+			targetExists = cb->battleMatchOwner(ctx.caster->getOwner(), stack, owner->getPositiveness());
 			if(targetExists)
 				break;
 		}

+ 3 - 20
lib/spells/CSpellHandler.cpp

@@ -219,26 +219,9 @@ ESpellCastProblem::ESpellCastProblem CSpell::canBeCast(const CBattleInfoCallback
 
 				for(const CStack * stack : cb->battleGetAllStacks())
 				{
-					bool immune = !(stack->isValidTarget(!tinfo.onlyAlive) && ESpellCastProblem::OK == isImmuneByStack(caster, stack));
-					bool casterStack = stack->owner == caster->getOwner();
-
-					if(!immune)
-					{
-						switch (positiveness)
-						{
-						case CSpell::POSITIVE:
-							if(casterStack)
-								targetExists = true;
-							break;
-						case CSpell::NEUTRAL:
-								targetExists = true;
-								break;
-						case CSpell::NEGATIVE:
-							if(!casterStack)
-								targetExists = true;
-							break;
-						}
-					}
+					const bool immune = !(stack->isValidTarget(!tinfo.onlyAlive) && ESpellCastProblem::OK == isImmuneByStack(caster, stack));
+					const bool ownerMatches = cb->battleMatchOwner(caster->getOwner(), stack, getPositiveness());
+					targetExists = !immune && ownerMatches;
 					if(targetExists)
 						break;
 				}

+ 2 - 2
lib/spells/CreatureSpellMechanics.cpp

@@ -72,7 +72,7 @@ void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, c
 	si32 damageToDisplay = parameters.effectPower;
 
 	if(!ctx.attackedCres.empty())
-		vstd::amin(damageToDisplay, (*ctx.attackedCres.begin())->count); //stack is already reduced after attack
+		vstd::amin(damageToDisplay, (*ctx.attackedCres.begin())->getCount()); //stack is already reduced after attack
 
 	ctx.setDamageToDisplay(damageToDisplay);
 
@@ -81,7 +81,7 @@ void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, c
 		BattleStackAttacked bsa;
 		bsa.flags |= BattleStackAttacked::SPELL_EFFECT;
 		bsa.spellID = owner->id;
-		bsa.damageAmount = parameters.effectPower * (attackedCre)->valOfBonuses(Bonus::STACK_HEALTH);//todo: move here all DeathStare calculation
+		bsa.damageAmount = parameters.effectPower * (attackedCre)->MaxHealth();//todo: move here all DeathStare calculation
 		bsa.stackAttacked = (attackedCre)->ID;
 		bsa.attackerID = -1;
 		(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator());

+ 132 - 108
server/CGameHandler.cpp

@@ -950,7 +950,7 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons
 	def->prepareAttacked(bsa, getRandomGenerator()); //calculate casualties
 
 	//life drain handling
-	if (att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving())
+	if(att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving())
 	{
 		StacksHealedOrResurrected shi;
 		shi.lifeDrain = true;
@@ -958,48 +958,49 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons
 		shi.cure = false;
 		shi.drainedFrom = def->ID;
 
-		StacksHealedOrResurrected::HealInfo hi;
-		hi.stackID = att->ID;
-		hi.healedHP = att->calculateHealedHealthPoints(bsa.damageAmount * att->valOfBonuses (Bonus::LIFE_DRAIN) / 100, true);
-		hi.lowLevelResurrection = false;
+		int32_t toHeal = bsa.damageAmount * att->valOfBonuses(Bonus::LIFE_DRAIN) / 100;
+		CHealth health = att->healthAfterHealed(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT);
+
+		CHealthInfo hi;
+		health.toInfo(hi);
+		hi.stackId = att->ID;
+		hi.delta = toHeal;
 		shi.healedStacks.push_back(hi);
 
-		if (hi.healedHP > 0)
-		{
+		if(hi.delta > 0)
 			bsa.healedStacks.push_back(shi);
-		}
 	}
 
 	//soul steal handling
-	if (att->hasBonusOfType(Bonus::SOUL_STEAL) && def->isLiving())
+	if(att->hasBonusOfType(Bonus::SOUL_STEAL) && def->isLiving())
 	{
 		StacksHealedOrResurrected shi;
 		shi.lifeDrain = true;
 		shi.tentHealing = false;
 		shi.cure = false;
-		shi.canOverheal = true;
 		shi.drainedFrom = def->ID;
 
-		for (int i = 0; i < 2; i++) //we can have two bonuses - one with subtype 0 and another with subtype 1
+		for(int i = 0; i < 2; i++) //we can have two bonuses - one with subtype 0 and another with subtype 1
 		{
-			if (att->hasBonusOfType(Bonus::SOUL_STEAL, i))
+			if(att->hasBonusOfType(Bonus::SOUL_STEAL, i))
 			{
-				StacksHealedOrResurrected::HealInfo hi;
-				hi.stackID = att->ID;
-				hi.healedHP = bsa.killedAmount * att->valOfBonuses(Bonus::SOUL_STEAL, i) * att->MaxHealth();
-				hi.lowLevelResurrection = (bool)i;
-				shi.healedStacks.push_back(hi);
+				int32_t toHeal = bsa.killedAmount * att->valOfBonuses(Bonus::SOUL_STEAL, i) * att->MaxHealth();
+				CHealth health = att->healthAfterHealed(toHeal, EHealLevel::OVERHEAL, ((i == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT));
+				CHealthInfo hi;
+				health.toInfo(hi);
+				hi.stackId = att->ID;
+				hi.delta = toHeal;
+				if(hi.delta > 0)
+					shi.healedStacks.push_back(hi);
 			}
 		}
-		if (std::any_of(shi.healedStacks.begin(), shi.healedStacks.end(), [](StacksHealedOrResurrected::HealInfo healInfo) { return healInfo.healedHP > 0; }))
-		{
+		if(!shi.healedStacks.empty())
 			bsa.healedStacks.push_back(shi);
-		}
 	}
 	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() && !vstd::contains(def->state, EBattleStackState::CLONED) &&
+	if(!bat.shot() && !def->isClone() &&
 		def->hasBonusOfType(Bonus::FIRE_SHIELD) && !att->hasBonusOfType(Bonus::FIRE_IMMUNITY))
 	{
 		// TODO: Fire shield damage should be calculated separately after BattleAttack applied.
@@ -1011,7 +1012,7 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons
 		bsa2.flags |= BattleStackAttacked::EFFECT; //FIXME: play animation upon efreet and not attacker
 		bsa2.effect = 11;
 
-		bsa2.damageAmount = (std::min(def->totalHealth(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense
+		bsa2.damageAmount = (std::min<int64_t>(def->health.available(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense
 		att->prepareAttacked(bsa2, getRandomGenerator());
 		bat.bsa.push_back(bsa2);
 	}
@@ -3830,11 +3831,29 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 		}
 	case Battle::DEFEND:
 		{
-			//defensive stance //TODO: remove this bonus when stack becomes active
+			//defensive stance
 			SetStackEffect sse;
-			sse.effect.push_back(Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL));
-			sse.effect.push_back(Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, gs->curB->battleGetStackByID(ba.stackNumber)->valOfBonuses(Bonus::DEFENSIVE_STANCE),
-				 -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE));
+			Bonus bonus1(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL);
+			Bonus bonus2(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, stack->valOfBonuses(Bonus::DEFENSIVE_STANCE),
+				 -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE);
+			BonusList defence = *stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE));
+			int oldDefenceValue = defence.totalValue();
+
+			defence.push_back(std::make_shared<Bonus>(bonus1));
+			defence.push_back(std::make_shared<Bonus>(bonus2));
+
+			int difference = defence.totalValue() - oldDefenceValue;
+
+			MetaString text;
+			stack->addText(text, MetaString::GENERAL_TXT, 120);
+			stack->addNameReplacement(text);
+			text.addReplacement(difference);
+
+			sse.battleLog.push_back(text);
+
+			sse.effect.push_back(bonus1);
+			sse.effect.push_back(bonus2);
+
 			sse.stacks.push_back(ba.stackNumber);
 			sendAndApply(&sse);
 
@@ -4005,12 +4024,12 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 
 			int additionalAttacks = stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK),
 				(Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT))))->totalValue();
-			for (int i = 0; i < additionalAttacks; ++i)
+			for(int i = 0; i < additionalAttacks; ++i)
 			{
 				if (
 					stack->alive()
 					&& destinationStack->alive()
-					&& stack->shots
+					&& stack->shots.canUse()
 					)
 				{
 					BattleAttack bat;
@@ -4177,37 +4196,37 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			const CStack *healer = gs->curB->battleGetStackByID(ba.stackNumber),
 				*destStack = gs->curB->battleGetStackByPos(ba.destinationTile);
 
-			ui32 healed = 0;
 
-			if (healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER))
+			if(healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER))
 			{
 				complain("There is either no healer, no destination, or healer cannot heal :P");
 			}
 			else
 			{
-				ui32 maxiumHeal = healer->count * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID));
-				healed = destStack->calculateHealedHealthPoints(maxiumHeal, false);
-			}
-
-			if (healed == 0)
-			{
-				//nothing to heal.. should we complain?
-			}
-			else
-			{
-				StacksHealedOrResurrected shr;
-				shr.lifeDrain = false;
-				shr.tentHealing = true;
-				shr.cure = false;
-				shr.drainedFrom = ba.stackNumber;
+				int32_t toHeal = healer->getCount() * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID));
 
-				StacksHealedOrResurrected::HealInfo hi;
-				hi.healedHP = healed;
-				hi.lowLevelResurrection = false;
-				hi.stackID = destStack->ID;
+				//TODO: allow resurrection for mods
+				CHealth health = destStack->healthAfterHealed(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT);
 
-				shr.healedStacks.push_back(hi);
-				sendAndApply(&shr);
+				if(toHeal == 0)
+				{
+					logGlobal->warn("Nothing to heal");
+				}
+				else
+				{
+					StacksHealedOrResurrected shr;
+					shr.lifeDrain = false;
+					shr.tentHealing = true;
+					shr.cure = false;
+					shr.drainedFrom = ba.stackNumber;
+
+					CHealthInfo hi;
+					health.toInfo(hi);
+					hi.stackId = destStack->ID;
+					hi.delta = toHeal;
+					shr.healedStacks.push_back(hi);
+					sendAndApply(&shr);
+				}
 			}
 			break;
 		}
@@ -4223,7 +4242,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			bsa.side = summoner->side;
 
 			bsa.creID = summonedType;
-			ui64 risedHp = summoner->count * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum());
+			ui64 risedHp = summoner->getCount() * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum());
 			ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount;
 
 			ui64 canRiseHp = std::min(targetHealth, risedHp);
@@ -4489,12 +4508,12 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 		if (st->hasBonusOfType(Bonus::HP_REGENERATION))
 		{
 			bte.effect = Bonus::HP_REGENERATION;
-			bte.val = std::min((int)(st->MaxHealth() - st->firstHPleft), st->valOfBonuses(Bonus::HP_REGENERATION));
+			bte.val = std::min((int)(st->MaxHealth() - st->getFirstHPleft()), st->valOfBonuses(Bonus::HP_REGENERATION));
 		}
 		if (st->hasBonusOfType(Bonus::FULL_HP_REGENERATION))
 		{
 			bte.effect = Bonus::HP_REGENERATION;
-			bte.val = st->MaxHealth() - st->firstHPleft;
+			bte.val = st->MaxHealth() - st->getFirstHPleft();
 		}
 		if (bte.val) //anything to heal
 			sendAndApply(&bte);
@@ -4551,7 +4570,7 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 		}
 		BonusList bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTER)));
 		int side = gs->curB->whatSide(st->owner);
-		if (st->casts && !gs->curB->sides.at(side).enchanterCounter)
+		if(st->canCast() && !gs->curB->sides.at(side).enchanterCounter)
 		{
 			bool cast = false;
 			while (!bl.empty() && !cast)
@@ -5216,7 +5235,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta
 			const CStack * oneOfAttacked = nullptr;
 			for (auto & elem : bat.bsa)
 			{
-				if (elem.newAmount > 0 && !elem.isSecondary()) //apply effects only to first target stack if it's alive
+				if ((elem.newHealth.fullUnits > 0 || elem.newHealth.firstHPleft > 0) && !elem.isSecondary()) //apply effects only to first target stack if it's alive
 				{
 					oneOfAttacked = gs->curB->battleGetStackByID(elem.stackAttacked);
 					break;
@@ -5282,7 +5301,10 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat)
 	if (!attacker || bat.bsa.empty()) // can be already dead
 		return;
 
-	const CStack *defender = gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked);
+	const CStack * defender = gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked);
+
+	if(!defender)
+		return;//already dead
 
 	auto cast = [=](SpellID spellID, int power)
 	{
@@ -5291,7 +5313,7 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat)
 		BattleSpellCastParameters parameters(gs->curB, attacker, spell);
 		parameters.spellLvl = 0;
 		parameters.effectLevel = 0;
-		parameters.aimToStack(gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked));
+		parameters.aimToStack(defender);
 		parameters.effectPower = power;
 		parameters.mode = ECastingMode::AFTER_ATTACK_CASTING;
 		parameters.cast(spellEnv);
@@ -5299,13 +5321,13 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat)
 
 	attackCasting(bat, Bonus::SPELL_AFTER_ATTACK, attacker);
 
-	if (bat.bsa.at(0).newAmount <= 0)
+	if(!defender->alive())
 	{
 		//don't try death stare or acid breath on dead stack (crash!)
 		return;
 	}
 
-	if (attacker->hasBonusOfType(Bonus::DEATH_STARE) && bat.bsa.size())
+	if(attacker->hasBonusOfType(Bonus::DEATH_STARE))
 	{
 		// mechanics of Death Stare as in H3:
 		// each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
@@ -5314,45 +5336,50 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat)
 		double chanceToKill = attacker->valOfBonuses(Bonus::DEATH_STARE, 0) / 100.0f;
 		vstd::amin(chanceToKill, 1); //cap at 100%
 
-		std::binomial_distribution<> distribution(attacker->count, chanceToKill);
+		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 = (attacker->count + cap - 1) / cap; //not much more than chance * count
+		int maxToKill = (attacker->getCount() + cap - 1) / cap; //not much more than chance * count
 		vstd::amin(staredCreatures, maxToKill);
 
-		staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->level();
-		if (staredCreatures)
+		staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / defender->level();
+		if(staredCreatures)
 		{
-			if (bat.bsa.at(0).newAmount > 0) //TODO: death stare was not originally available for multiple-hex attacks, but...
-				cast(SpellID::DEATH_STARE, staredCreatures);
+			//TODO: death stare was not originally available for multiple-hex attacks, but...
+			cast(SpellID::DEATH_STARE, staredCreatures);
 		}
 	}
 
+	if(!defender->alive())
+		return;
+
 	int acidDamage = 0;
 	TBonusListPtr acidBreath = attacker->getBonuses(Selector::type(Bonus::ACID_BREATH));
-	for (const std::shared_ptr<Bonus> b : *acidBreath)
+	for(const std::shared_ptr<Bonus> b : *acidBreath)
 	{
-		if (b->additionalInfo > getRandomGenerator().nextInt(99))
+		if(b->additionalInfo > getRandomGenerator().nextInt(99))
 			acidDamage += b->val;
 	}
-	if (acidDamage)
-	{
-		cast(SpellID::ACID_BREATH_DAMAGE, acidDamage * attacker->count);
-	}
 
-	if (attacker->hasBonusOfType(Bonus::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability
+	if(acidDamage)
+		cast(SpellID::ACID_BREATH_DAMAGE, acidDamage * attacker->getCount());
+
+	if(!defender->alive())
+		return;
+
+	if(attacker->hasBonusOfType(Bonus::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability
 	{
 		double chanceToTrigger = attacker->valOfBonuses(Bonus::TRANSMUTATION) / 100.0f;
 		vstd::amin(chanceToTrigger, 1); //cap at 100%
 
-		if (getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger)
+		if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger)
 			return;
 
 		int bonusAdditionalInfo = attacker->getBonus(Selector::type(Bonus::TRANSMUTATION))->additionalInfo;
 
-		if (defender->getCreature()->idNumber == bonusAdditionalInfo ||
+		if(defender->getCreature()->idNumber == bonusAdditionalInfo ||
 			(bonusAdditionalInfo == -1 && defender->getCreature()->idNumber == attacker->getCreature()->idNumber))
 			return;
 
@@ -5360,17 +5387,15 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat)
 		resurrectInfo.pos = defender->position;
 		resurrectInfo.side = defender->side;
 
-		if (bonusAdditionalInfo != -1)
+		if(bonusAdditionalInfo != -1)
 			resurrectInfo.creID = (CreatureID)bonusAdditionalInfo;
 		else
 			resurrectInfo.creID = attacker->getCreature()->idNumber;
 
-		if (attacker->hasBonusOfType((Bonus::TRANSMUTATION), 0))
-		{
-			resurrectInfo.amount = std::max((defender->count * defender->MaxHealth()) / resurrectInfo.creID.toCreature()->MaxHealth(), 1u);
-		}
+		if(attacker->hasBonusOfType((Bonus::TRANSMUTATION), 0))
+			resurrectInfo.amount = std::max((defender->getCount() * defender->MaxHealth()) / resurrectInfo.creID.toCreature()->MaxHealth(), 1u);
 		else if (attacker->hasBonusOfType((Bonus::TRANSMUTATION), 1))
-			resurrectInfo.amount = defender->count;
+			resurrectInfo.amount = defender->getCount();
 		else
 			return; //wrong subtype
 
@@ -5647,7 +5672,7 @@ void CGameHandler::runBattle()
 				if (accessibility.accessible(hex, guardianIsBig, stack->side)) //without this multiple creatures can occupy one hex
 				{
 					BattleStackAdded newStack;
-					newStack.amount = std::max(1, (int)(stack->count * 0.01 * summonInfo->val));
+					newStack.amount = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val));
 					newStack.creID = creatureData.num;
 					newStack.side = stack->side;
 					newStack.summoned = true;
@@ -6308,36 +6333,34 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl
 	heroWithDeadCommander = ObjectInstanceID();
 
 	PlayerColor color = army->tempOwner;
-	if (color == PlayerColor::UNFLAGGABLE)
+	if(color == PlayerColor::UNFLAGGABLE)
 		color = PlayerColor::NEUTRAL;
 
-	for (CStack *st : bat->stacks)
+	for(CStack * st : bat->stacks)
 	{
-		if (vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account temporary summoned stacks
+		if(vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account temporary summoned stacks
 			continue;
-		if (st->owner != color) //remove only our stacks
+		if(st->owner != color) //remove only our stacks
 			continue;
 
 		logGlobal->debug("Calculating casualties for %s", st->nodeName());
 
-		//FIXME: this info is also used in BattleInfo::calculateCasualties, refactor
-		st->count = std::max (0, st->count - st->resurrected);
+		st->health.takeResurrected();
 
-		if (st->slot == SlotID::ARROW_TOWERS_SLOT)
+		if(st->slot == SlotID::ARROW_TOWERS_SLOT)
 		{
-			//do nothing
 			logGlobal->debug("Ignored arrow towers stack.");
 		}
-		else if (st->slot == SlotID::WAR_MACHINES_SLOT)
+		else if(st->slot == SlotID::WAR_MACHINES_SLOT)
 		{
 			auto warMachine = st->type->warMachine;
 
-			if (warMachine == ArtifactID::NONE)
+			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->count)
+			else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0)
 			{
 				logGlobal->debug("War machine has been destroyed");
 				auto hero = dynamic_ptr_cast<CGHeroInstance> (army);
@@ -6347,16 +6370,16 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl
 					logGlobal->error("War machine in army without hero");
 			}
 		}
-		else if (st->slot == SlotID::SUMMONED_SLOT_PLACEHOLDER)
+		else if(st->slot == SlotID::SUMMONED_SLOT_PLACEHOLDER)
 		{
-			if (st->alive() && st->count > 0)
+			if(st->alive() && st->getCount() > 0)
 			{
-				logGlobal->debug("Permanently summoned %d units.", st->count);
+				logGlobal->debug("Permanently summoned %d units.", st->getCount());
 				const CreatureID summonedType = st->type->idNumber;
-				summoned[summonedType] += st->count;
+				summoned[summonedType] += st->getCount();
 			}
 		}
-		else if (st->slot == SlotID::COMMANDER_SLOT_PLACEHOLDER)
+		else if(st->slot == SlotID::COMMANDER_SLOT_PLACEHOLDER)
 		{
 			if (nullptr == st->base)
 			{
@@ -6365,10 +6388,10 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl
 			else
 			{
 				auto c = dynamic_cast <const CCommanderInstance *>(st->base);
-				if (c)
+				if(c)
 				{
 					auto h = dynamic_cast <const CGHeroInstance *>(army);
-					if (h && h->commander == c && (st->count == 0 || !st->alive()))
+					if(h && h->commander == c && (st->getCount() == 0 || !st->alive()))
 					{
 						logGlobal->debug("Commander is dead.");
 						heroWithDeadCommander = army->id; //TODO: unify commander handling
@@ -6378,25 +6401,26 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl
 					logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName());
 			}
 		}
-		else if (st->base && !army->slotEmpty(st->slot))
+		else if(st->base && !army->slotEmpty(st->slot))
 		{
-			if (st->count == 0 || !st->alive())
+			logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->slot));
+			if(st->getCount() == 0 || !st->alive())
 			{
 				logGlobal->debug("Stack has been destroyed.");
 				StackLocation sl(army, st->slot);
 				newStackCounts.push_back(TStackAndItsNewCount(sl, 0));
 			}
-			else if (st->count < army->getStackCount(st->slot))
+			else if(st->getCount() < army->getStackCount(st->slot))
 			{
-				logGlobal->debug("Stack lost %d units.", army->getStackCount(st->slot) - st->count);
+				logGlobal->debug("Stack lost %d units.", army->getStackCount(st->slot) - st->getCount());
 				StackLocation sl(army, st->slot);
-				newStackCounts.push_back(TStackAndItsNewCount(sl, st->count));
+				newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount()));
 			}
-			else if (st->count > army->getStackCount(st->slot))
+			else if(st->getCount() > army->getStackCount(st->slot))
 			{
-				logGlobal->debug("Stack gained %d units.", st->count - army->getStackCount(st->slot));
+				logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->slot));
 				StackLocation sl(army, st->slot);
-				newStackCounts.push_back(TStackAndItsNewCount(sl, st->count));
+				newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount()));
 			}
 		}
 		else

+ 1 - 0
test/CMakeLists.txt

@@ -13,6 +13,7 @@ set(test_SRCS
 		CMemoryBufferTest.cpp
 		CVcmiTestConfig.cpp
 		MapComparer.cpp
+                battle/CHealthTest.cpp
 )
 
 set(test_HEADERS

+ 1 - 0
test/Test.cbp

@@ -76,6 +76,7 @@
 			<Option compile="1" />
 			<Option weight="0" />
 		</Unit>
+		<Unit filename="battle/CHealthTest.cpp" />
 		<Extensions>
 			<code_completion />
 			<envvars />

+ 223 - 0
test/battle/CHealthTest.cpp

@@ -0,0 +1,223 @@
+/*
+ * CHealthTest.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 <boost/test/unit_test.hpp>
+#include "../../lib/CStack.h"
+
+static const int32_t UNIT_HEALTH = 123;
+static const int32_t UNIT_AMOUNT = 300;
+
+class CUnitHealthInfoMock : public IUnitHealthInfo
+{
+public:
+	CUnitHealthInfoMock():
+		maxHealth(UNIT_HEALTH),
+		baseAmount(UNIT_AMOUNT),
+		health(this)
+	{
+		health.init();
+	}
+
+	int32_t maxHealth;
+	int32_t baseAmount;
+
+	CHealth health;
+
+	int32_t unitMaxHealth() const override
+	{
+		return maxHealth;
+	};
+
+	int32_t unitBaseAmount() const override
+	{
+		return baseAmount;
+	};
+};
+
+static void checkTotal(const CHealth & health, const CUnitHealthInfoMock & mock)
+{
+	BOOST_CHECK_EQUAL(health.total(), mock.maxHealth * mock.baseAmount);
+}
+
+static void checkEmptyHealth(const CHealth & health, const CUnitHealthInfoMock & mock)
+{
+	checkTotal(health, mock);
+	BOOST_CHECK_EQUAL(health.getCount(), 0);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), 0);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+	BOOST_CHECK_EQUAL(health.available(), 0);
+}
+
+static void checkFullHealth(const CHealth & health, const CUnitHealthInfoMock & mock)
+{
+	checkTotal(health, mock);
+	BOOST_CHECK_EQUAL(health.getCount(), mock.baseAmount);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), mock.maxHealth);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+	BOOST_CHECK_EQUAL(health.available(), mock.maxHealth * mock.baseAmount);
+}
+
+static void checkDamage(CHealth & health, const int32_t initialDamage, const int32_t expectedDamage)
+{
+	int32_t damage = initialDamage;
+	health.damage(damage);
+	BOOST_CHECK_EQUAL(damage, expectedDamage);
+}
+
+static void checkNormalDamage(CHealth & health, const int32_t initialDamage)
+{
+	checkDamage(health, initialDamage, initialDamage);
+}
+
+static void checkNoDamage(CHealth & health, const int32_t initialDamage)
+{
+	checkDamage(health, initialDamage, 0);
+}
+
+static void checkHeal(CHealth & health, EHealLevel level, EHealPower power, const int32_t initialHeal, const int32_t expectedHeal)
+{
+	int32_t heal = initialHeal;
+	health.heal(heal, level, power);
+	BOOST_CHECK_EQUAL(heal, expectedHeal);
+}
+
+BOOST_AUTO_TEST_SUITE(CHealthTest_Suite)
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+	CUnitHealthInfoMock uhi;
+	CHealth health(&uhi);
+	checkEmptyHealth(health, uhi);
+
+	health.init();
+	checkFullHealth(health, uhi);
+
+	health.reset();
+	checkEmptyHealth(health, uhi);
+}
+
+BOOST_FIXTURE_TEST_CASE(damage, CUnitHealthInfoMock)
+{
+	checkNormalDamage(health, 0);
+	checkFullHealth(health, *this);
+
+	checkNormalDamage(health, maxHealth - 1);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), 1);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkNormalDamage(health, 1);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkNormalDamage(health, UNIT_HEALTH * (UNIT_AMOUNT - 1));
+	checkEmptyHealth(health, *this);
+
+	checkNoDamage(health, 1337);
+	checkEmptyHealth(health, *this);
+}
+
+BOOST_FIXTURE_TEST_CASE(heal, CUnitHealthInfoMock)
+{
+	checkNormalDamage(health, 99);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH-99);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkHeal(health, EHealLevel::HEAL, EHealPower::PERMANENT, 9, 9);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH-90);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkHeal(health, EHealLevel::RESURRECT, EHealPower::ONE_BATTLE, 40, 40);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH-50);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkHeal(health, EHealLevel::OVERHEAL, EHealPower::PERMANENT, 50, 50);
+	checkFullHealth(health, *this);
+}
+
+BOOST_FIXTURE_TEST_CASE(resurrectOneBattle, CUnitHealthInfoMock)
+{
+	checkNormalDamage(health, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkHeal(health, EHealLevel::RESURRECT, EHealPower::ONE_BATTLE, UNIT_HEALTH, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 1);
+
+	checkNormalDamage(health, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	health.init();
+
+	checkNormalDamage(health, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	health.takeResurrected();
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	health.init();
+
+	checkNormalDamage(health, UNIT_HEALTH * UNIT_AMOUNT);
+	checkEmptyHealth(health, *this);
+
+	checkHeal(health, EHealLevel::RESURRECT, EHealPower::ONE_BATTLE, UNIT_HEALTH * UNIT_AMOUNT, UNIT_HEALTH * UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), UNIT_AMOUNT);
+
+	health.takeResurrected();
+	checkEmptyHealth(health, *this);
+}
+
+BOOST_FIXTURE_TEST_CASE(resurrectPermanent, CUnitHealthInfoMock)
+{
+	checkNormalDamage(health, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkHeal(health, EHealLevel::RESURRECT, EHealPower::PERMANENT, UNIT_HEALTH, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	checkNormalDamage(health, UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getCount(), UNIT_AMOUNT - 1);
+	BOOST_CHECK_EQUAL(health.getFirstHPleft(), UNIT_HEALTH);
+	BOOST_CHECK_EQUAL(health.getResurrected(), 0);
+
+	health.init();
+
+	checkNormalDamage(health, UNIT_HEALTH * UNIT_AMOUNT);
+	checkEmptyHealth(health, *this);
+
+	checkHeal(health, EHealLevel::RESURRECT, EHealPower::PERMANENT, UNIT_HEALTH * UNIT_AMOUNT, UNIT_HEALTH * UNIT_AMOUNT);
+	checkFullHealth(health, *this);
+
+	health.takeResurrected();
+	checkFullHealth(health, *this);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+