Browse Source

Merge branch 'develop' of https://github.com/vcmi/vcmi into develop

DjWarmonger 10 years ago
parent
commit
def56051fc
48 changed files with 912 additions and 684 deletions
  1. 1 1
      AI/BattleAI/BattleAI.cpp
  2. 2 2
      client/CBitmapHandler.cpp
  3. 6 13
      client/CDefHandler.cpp
  4. 4 4
      client/CDefHandler.h
  5. 1 1
      client/Client.h
  6. 7 4
      client/Graphics.cpp
  7. 3 1
      client/battle/CBattleAnimations.cpp
  8. 67 182
      client/battle/CBattleInterface.cpp
  9. 7 3
      client/battle/CBattleInterface.h
  10. 1 0
      client/battle/CBattleInterfaceClasses.cpp
  11. 1 1
      client/battle/CCreatureAnimation.cpp
  12. 6 2
      client/gui/CAnimation.cpp
  13. 0 5
      client/gui/SDL_Extensions.h
  14. 21 18
      client/widgets/CGarrisonInt.cpp
  15. 14 19
      client/widgets/MiscWidgets.cpp
  16. 2 2
      client/widgets/TextControls.cpp
  17. 2 2
      client/windows/CQuestLog.cpp
  18. 10 10
      config/heroes/fortress.json
  19. 4 0
      config/schemas/spell.json
  20. 18 6
      config/spells/other.json
  21. 104 105
      config/spells/timed.json
  22. 40 7
      lib/BattleState.cpp
  23. 27 15
      lib/BattleState.h
  24. 1 3
      lib/CConsoleHandler.cpp
  25. 15 3
      lib/CGeneralTextHandler.cpp
  26. 2 2
      lib/Connection.h
  27. 3 0
      lib/GameConstants.h
  28. 34 53
      lib/HeroBonus.cpp
  29. 0 6
      lib/HeroBonus.h
  30. 1 1
      lib/IGameCallback.h
  31. 18 8
      lib/NetPacks.h
  32. 14 3
      lib/NetPacksLib.cpp
  33. 4 0
      lib/VCMI_lib.cbp
  34. 7 1
      lib/mapObjects/CArmedInstance.cpp
  35. 64 5
      lib/mapObjects/CGHeroInstance.cpp
  36. 11 6
      lib/mapObjects/CGHeroInstance.h
  37. 8 2
      lib/mapObjects/CGTownInstance.cpp
  38. 1 0
      lib/mapObjects/CGTownInstance.h
  39. 19 22
      lib/spells/BattleSpellMechanics.cpp
  40. 213 84
      lib/spells/CDefaultSpellMechanics.cpp
  41. 2 0
      lib/spells/CDefaultSpellMechanics.h
  42. 72 58
      lib/spells/CSpellHandler.cpp
  43. 19 8
      lib/spells/CSpellHandler.h
  44. 3 0
      lib/spells/ISpellMechanics.h
  45. 32 0
      lib/spells/Magic.h
  46. 15 14
      server/CGameHandler.cpp
  47. 1 1
      server/CGameHandler.h
  48. 5 1
      server/CQuery.cpp

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -643,7 +643,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo
 	auto attacker = AttackInfo.attacker;
 	auto enemy = AttackInfo.defender;
 
-	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacks);
+	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacksRemaining());
 	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();
 

+ 2 - 2
client/CBitmapHandler.cpp

@@ -70,7 +70,7 @@ SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size)
 			tp.r = pcx[it++];
 			tp.g = pcx[it++];
 			tp.b = pcx[it++];
-			CSDL_Ext::colorSetAlpha(tp,SDL_ALPHA_OPAQUE);
+			tp.a = SDL_ALPHA_OPAQUE;
 			ret->format->palette->colors[i] = tp;
 		}
 	}
@@ -142,7 +142,7 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 			{
 				//set correct value for alpha\unused channel
 				for (int i=0; i < ret->format->palette->ncolors; i++)
-					CSDL_Ext::colorSetAlpha(ret->format->palette->colors[i],SDL_ALPHA_OPAQUE);				
+					ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE;
 			}
 		}
 		else

+ 6 - 13
client/CDefHandler.cpp

@@ -67,7 +67,7 @@ void CDefHandler::openFromMemory(ui8 *table, const std::string & name)
 		palette[it].r = de.palette[it].R;
 		palette[it].g = de.palette[it].G;
 		palette[it].b = de.palette[it].B;
-		CSDL_Ext::colorSetAlpha(palette[it],SDL_ALPHA_OPAQUE);	
+		palette[it].a = SDL_ALPHA_OPAQUE;	
 	}
 
 	// The SDefEntryBlock starts just after the SDefEntry
@@ -122,12 +122,6 @@ void CDefHandler::openFromMemory(ui8 *table, const std::string & name)
 	}
 }
 
-void CDefHandler::expand(ui8 N,ui8 & BL, ui8 & BR)
-{
-	BL = (N & 0xE0) >> 5;
-	BR = N & 0x1F;
-}
-
 SDL_Surface * CDefHandler::getSprite (int SIndex, const ui8 * FDef, const SDL_Color * palette) const
 {
 	SDL_Surface * ret=nullptr;
@@ -180,13 +174,12 @@ SDL_Surface * CDefHandler::getSprite (int SIndex, const ui8 * FDef, const SDL_Co
 
 	BaseOffset += sizeof(SSpriteDef);
 	int BaseOffsetor = BaseOffset;
-
-	if(SDL_SetPaletteColors(ret->format->palette,palette,0,256) != 0)
-	{
-		logGlobal->errorStream() << __FUNCTION__ <<": Unable to set palette";
-		logGlobal->errorStream() << SDL_GetError();		
-	}
 	
+	SDL_Palette * p = SDL_AllocPalette(256);	
+	SDL_SetPaletteColors(p, palette, 0, 256);
+	SDL_SetSurfacePalette(ret, p);
+	SDL_FreePalette(p);	
+
 	int ftcp=0;
 
 	// If there's a margin anywhere, just blank out the whole surface.

+ 4 - 4
client/CDefHandler.h

@@ -85,7 +85,9 @@ private:
 		int group;
 	} ;
 	std::vector<SEntry> SEntries ;
-
+	
+	void openFromMemory(ui8 * table, const std::string & name);	
+	SDL_Surface * getSprite (int SIndex, const ui8 * FDef, const SDL_Color * palette) const;
 public:
 	int width, height; //width and height
 	std::string defName;
@@ -94,9 +96,7 @@ public:
 
 	CDefHandler(); //c-tor
 	~CDefHandler(); //d-tor
-	SDL_Surface * getSprite (int SIndex, const ui8 * FDef, const SDL_Color * palette) const; //saves picture with given number to "testtt.bmp"
-	static void expand(ui8 N,ui8 & BL, ui8 & BR);
-	void openFromMemory(ui8 * table, const std::string & name);
+	
 	CDefEssential * essentialize();
 
 	static CDefHandler * giveDef(const std::string & defName);

+ 1 - 1
client/Client.h

@@ -186,7 +186,7 @@ public:
 
 	void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override {};
 	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) override {};
-	bool changeStackType(const StackLocation &sl, CCreature *c) override {return false;};
+	bool changeStackType(const StackLocation &sl, const CCreature *c) override {return false;};
 	bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override {return false;};
 	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;};
 	bool eraseStack(const StackLocation &sl, bool forceRemoval = false){return false;};

+ 7 - 4
client/Graphics.cpp

@@ -58,7 +58,7 @@ void Graphics::loadPaletteAndColors()
 		col.r = pals[startPoint++];
 		col.g = pals[startPoint++];
 		col.b = pals[startPoint++];
-		CSDL_Ext::colorSetAlpha(col,SDL_ALPHA_OPAQUE);
+		col.a = SDL_ALPHA_OPAQUE;
 		startPoint++;
 		playerColorPalette[i] = col;
 	}
@@ -74,7 +74,7 @@ void Graphics::loadPaletteAndColors()
 		neutralColorPalette[i].g = reader.readUInt8();
 		neutralColorPalette[i].b = reader.readUInt8();
 		reader.readUInt8(); // this is "flags" entry, not alpha
-		CSDL_Ext::colorSetAlpha(neutralColorPalette[i], SDL_ALPHA_OPAQUE);
+		neutralColorPalette[i].a = SDL_ALPHA_OPAQUE;
 	}
 	//colors initialization
 	SDL_Color colors[]  = { 
@@ -92,8 +92,11 @@ void Graphics::loadPaletteAndColors()
 	{
 		playerColors[i] = colors[i];
 	}
-	neutralColor->r = 0x84; neutralColor->g = 0x84; neutralColor->b = 0x84; //gray
-	CSDL_Ext::colorSetAlpha(*neutralColor,SDL_ALPHA_OPAQUE);
+	//gray
+	neutralColor->r = 0x84;
+	neutralColor->g = 0x84;
+	neutralColor->b = 0x84;
+	neutralColor->a = SDL_ALPHA_OPAQUE;
 }
 
 void Graphics::initializeBattleGraphics()

+ 3 - 1
client/battle/CBattleAnimations.cpp

@@ -283,7 +283,9 @@ void CDefenceAnimation::endAnim()
 
 CDummyAnimation::CDummyAnimation(CBattleInterface * _owner, int howManyFrames) 
 : CBattleAnimation(_owner), counter(0), howMany(howManyFrames)
-{}
+{
+	logAnim->debugStream() << "Created dummy animation for " << howManyFrames <<" frames";
+}
 
 bool CDummyAnimation::init()
 {

+ 67 - 182
client/battle/CBattleInterface.cpp

@@ -360,7 +360,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet * army1, const CCreatureSe
 		{
 			idToObstacle[ID] = CDefHandler::giveDef(elem->getInfo().defName);
 			for(auto & _n : idToObstacle[ID]->ourImages)
-			{		
+			{
 				CSDL_Ext::setDefaultColorKey(_n.bitmap);
 			}
 		}
@@ -1240,15 +1240,9 @@ void CBattleInterface::displayBattleFinished()
 void CBattleInterface::spellCast( const BattleSpellCast * sc )
 {
 	const SpellID spellID(sc->id);
-	const CSpell &spell = * spellID.toSpell();
-	const std::string & spellName = spell.name;
-
-	const std::string& castSoundPath = spell.getCastSound();
-
-	std::string casterName("Something");
+	const CSpell & spell = * spellID.toSpell();
 
-	if(sc->castedByHero)
-		casterName = curInt->cb->battleGetHeroInfo(sc->side).name;
+	const std::string & castSoundPath = spell.getCastSound();
 
 	if(!castSoundPath.empty())
 		CCS->soundh->playSound(castSoundPath);
@@ -1262,19 +1256,16 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc )
 			const CStack * casterStack = curInt->cb->battleGetStackByID(casterStackID);
 			if(casterStack != nullptr)
 			{
-				casterName = casterStack->type->namePl;
-				srccoord = CClickableHex::getXYUnitAnim(casterStack->position, casterStack, this); 
+				srccoord = CClickableHex::getXYUnitAnim(casterStack->position, casterStack, this);
 				srccoord.x += 250;
 				srccoord.y += 240;
 			}
 		}
 	}
 
-	//TODO: play custom cast animation
-	{
+	//todo: play custom cast animation
+	displaySpellCast(spellID, BattleHex::INVALID, false);
 
-	}
-	
 	//playing projectile animation
 	if(sc->tile.isValid())
 	{
@@ -1304,163 +1295,33 @@ void CBattleInterface::spellCast( const BattleSpellCast * sc )
 			delete animDef;
 			addNewAnim(new CSpellEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip));
 		}
-	}	
+	}
 	waitForAnims();
-	
+
 	displaySpellHit(spellID, sc->tile);
-	
-	//queuing affect /resist animation	
-	for (auto & elem : sc->affectedCres) 
+
+	//queuing affect animation
+	for(auto & elem : sc->affectedCres)
 	{
 		BattleHex position = curInt->cb->battleGetStackByID(elem, false)->position;
-
-		if(vstd::contains(sc->resisted,elem))
-			displayEffect(78, position);
-		else
-			displaySpellEffect(spellID, position);
+		displaySpellEffect(spellID, position);
 	}
 
-	switch(sc->id)
+	//queuing additional animation
+	for(auto & elem : sc->customEffects)
 	{
-	case SpellID::SUMMON_FIRE_ELEMENTAL:
-	case SpellID::SUMMON_EARTH_ELEMENTAL:
-	case SpellID::SUMMON_WATER_ELEMENTAL:
-	case SpellID::SUMMON_AIR_ELEMENTAL:
-	case SpellID::CLONE:
-	case SpellID::REMOVE_OBSTACLE:
-		addNewAnim(new CDummyAnimation(this, 2)); //interface won't return until animation is played. TODO: make it smarter?
-		break;
-	} //switch(sc->id)
+		BattleHex position = curInt->cb->battleGetStackByID(elem.stack, false)->position;
+		displayEffect(elem.effect, position);
+	}
 
 	//displaying message in console
-	bool customSpell = false;
-	if(sc->affectedCres.size() == 1)
-	{
-		const CStack * attackedStack = curInt->cb->battleGetStackByID(*sc->affectedCres.begin(), false);
-
-		const std::string attackedName = attackedStack->getName();
-		const std::string attackedNameSing = attackedStack->getCreature()->nameSing;
-		const std::string attackedNamePl = attackedStack->getCreature()->namePl;
-
-		std::string text = CGI->generaltexth->allTexts[195];
-		if(sc->castedByHero)
-		{
-			boost::algorithm::replace_first(text, "%s", casterName);
-			boost::algorithm::replace_first(text, "%s", spellName);
-			boost::algorithm::replace_first(text, "%s", attackedNamePl); //target
-		}
-		else
-		{
-			auto getPluralText = [attackedStack](const int baseTextID) -> std::string
-			{
-				return CGI->generaltexth->allTexts[(attackedStack->count > 1 ? baseTextID+1 : baseTextID)];
-			};
-			
-			bool plural = false; //add singular / plural form of creature text if this is true
-			int textID = 0;
-			switch(sc->id)
-			{
-				case SpellID::STONE_GAZE:
-					customSpell = true;
-					plural = true;
-					textID = 558;
-					break;
-				case SpellID::POISON:
-					customSpell = true;
-					plural = true;
-					textID = 561;
-					break;
-				case SpellID::BIND:
-					customSpell = true;
-					text = CGI->generaltexth->allTexts[560];
-					boost::algorithm::replace_first(text, "%s", attackedNamePl);
-					break;//Roots and vines bind the %s to the ground!
-				case SpellID::DISEASE:
-					customSpell = true;
-					plural = true;
-					textID = 553;
-					break;
-				case SpellID::PARALYZE:
-					customSpell = true;
-					plural = true;
-					textID = 563;
-					break;
-				case SpellID::AGE:
-				{
-					customSpell = true;
-					text = getPluralText(551);
-					boost::algorithm::replace_first(text, "%s", attackedName);
-					//The %s shrivel with age, and lose %d hit points."
-					TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH));
-					bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, SpellID::AGE));
-					boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(bl->totalValue()/2));
-				}
-					break;
-				case SpellID::THUNDERBOLT:
-					text = CGI->generaltexth->allTexts[367];
-					boost::algorithm::replace_first(text, "%s", attackedNamePl);
-					console->addText(text);
-					text = CGI->generaltexth->allTexts[343].substr(1, CGI->generaltexth->allTexts[343].size() - 1); //Does %d points of damage.
-					boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(sc->dmgToDisplay)); //no more text afterwards
-					console->addText(text);
-					customSpell = true;
-					text = ""; //yeah, it's a terrible mess
-					break;
-				case SpellID::DISPEL_HELPFUL_SPELLS:
-					text = CGI->generaltexth->allTexts[555];
-					boost::algorithm::replace_first(text, "%s", attackedNamePl);
-					customSpell = true;
-					break;
-				case SpellID::DEATH_STARE:
-					customSpell = true;
-					if (sc->dmgToDisplay)
-					{
-						if (sc->dmgToDisplay > 1)
-						{
-							text = CGI->generaltexth->allTexts[119]; //%d %s die under the terrible gaze of the %s.
-							boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(sc->dmgToDisplay));
-							boost::algorithm::replace_first(text, "%s", attackedNamePl);
-						}
-						else
-						{
-							text = CGI->generaltexth->allTexts[118]; //One %s dies under the terrible gaze of the %s.
-							boost::algorithm::replace_first(text, "%s", attackedNameSing);
-						}
-						boost::algorithm::replace_first(text, "%s", casterName); //casting stack
-					}
-					else
-						text = "";
-					break;
-				default:
-					text = CGI->generaltexth->allTexts[565]; //The %s casts %s
-					boost::algorithm::replace_first(text, "%s", casterName); //casting stack
+	std::vector<std::string> logLines;
+	
+	spell.prepareBattleLog(curInt->cb.get(), sc, logLines);
+	
+	for(auto line : logLines)
+		console->addText(line);
 
-			}
-			if (plural)
-			{
-				text = getPluralText(textID);
-				boost::algorithm::replace_first(text, "%s", attackedName);
-			}
-		}
-		if (!customSpell && !sc->dmgToDisplay)
-			boost::algorithm::replace_first(text, "%s", spellName); //simple spell name
-		if (text.size())
-			console->addText(text);
-	}
-	else
-	{
-		std::string text = CGI->generaltexth->allTexts[196];
-		boost::algorithm::replace_first(text, "%s", casterName);
-		boost::algorithm::replace_first(text, "%s", spellName);
-		console->addText(text);
-	}
-	if(sc->dmgToDisplay && !customSpell)
-	{
-		std::string dmgInfo = CGI->generaltexth->allTexts[376];
-		boost::algorithm::replace_first(dmgInfo, "%s", spellName); //simple spell name
-		boost::algorithm::replace_first(dmgInfo, "%d", boost::lexical_cast<std::string>(sc->dmgToDisplay));
-		console->addText(dmgInfo); //todo: casualties (?)
-	}
 	waitForAnims();
 	//mana absorption
 	if(sc->manaGained > 0)
@@ -1517,14 +1378,14 @@ void CBattleInterface::castThisSpell(SpellID spellID)
 	assert(castingHero); // code below assumes non-null hero
 	sp = spellID.toSpell();
 	spellSelMode = ANY_LOCATION;
-	
+
 	const CSpell::TargetInfo ti = sp->getTargetInfo(castingHero->getSpellSchoolLevel(sp));
-	
+
 	if(ti.massive || ti.type == CSpell::NO_TARGET)
-		spellSelMode = NO_LOCATION;	
+		spellSelMode = NO_LOCATION;
 	else if(ti.type == CSpell::LOCATION && ti.clearAffected)
 	{
-		spellSelMode = FREE_LOCATION;		
+		spellSelMode = FREE_LOCATION;
 	}
 	else if(ti.type == CSpell::CREATURE)
 	{
@@ -1532,11 +1393,11 @@ void CBattleInterface::castThisSpell(SpellID spellID)
 			spellSelMode = selectionTypeByPositiveness(*sp);
 		else
 			spellSelMode = ANY_CREATURE;
-	}	
+	}
 	else if(ti.type == CSpell::OBSTACLE)
 	{
 		spellSelMode = OBSTACLE;
-	} 
+	}
 
 	if (spellSelMode == NO_LOCATION) //user does not have to select location
 	{
@@ -1558,33 +1419,57 @@ void CBattleInterface::displayEffect(ui32 effect, int destTile, bool areaEffect)
 	addNewAnim(new CSpellEffectAnimation(this, effect, destTile, 0, 0, false));
 }
 
+void CBattleInterface::displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile, bool areaEffect)
+{	
+	if(animation.pause > 0)
+	{
+		addNewAnim(new CDummyAnimation(this, animation.pause));	
+	}
+	else
+	{
+		addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM));	
+	}	
+}
+
+void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile, bool areaEffect)
+{
+	const CSpell * spell = spellID.toSpell();
+
+	if(spell == nullptr)
+		return;
+		
+	for(const CSpell::TAnimation & animation : spell->animationInfo.cast)
+	{
+		displaySpellAnimation(animation, destinationTile, areaEffect);
+	}	
+}
+
 void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect)
 {
 	const CSpell * spell = spellID.toSpell();
-	
+
 	if(spell == nullptr)
 		return;
 
 	for(const CSpell::TAnimation & animation : spell->animationInfo.affect)
-	{				
-		addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM));
+	{
+		displaySpellAnimation(animation, destinationTile, areaEffect);
 	}
 }
 
 void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect)
 {
 	const CSpell * spell = spellID.toSpell();
-	
+
 	if(spell == nullptr)
-		return;	
-	
+		return;
+
 	for(const CSpell::TAnimation & animation : spell->animationInfo.hit)
-	{			
-		addNewAnim(new CSpellEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM));
+	{
+		displaySpellAnimation(animation, destinationTile, areaEffect);
 	}
 }
 
-
 void CBattleInterface::battleTriggerEffect(const BattleTriggerEffect & bte)
 {
 	const CStack * stack = curInt->cb->battleGetStackByID(bte.stackID);
@@ -1712,8 +1597,8 @@ void CBattleInterface::endCastingSpell()
 {
 	assert(spellDestSelectMode);
 
-	delete spellToCast;
-	spellToCast = nullptr;
+	vstd::clear_pointer(spellToCast);
+
 	sp = nullptr;
 	spellDestSelectMode = false;
 	CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
@@ -1797,7 +1682,7 @@ void CBattleInterface::printConsoleAttacked( const CStack * defender, int dmg, i
 	{
 		if (attacker)
 			formattedText.append(" ");
-		
+
 		boost::format txt;
 		if(killed > 1)
 		{
@@ -2218,7 +2103,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
 			{
 				ui8 skill = 0;
 				if (creatureCasting)
-					skill = sactive->valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, SpellID::TELEPORT));
+					skill = sactive->getSpellSchoolLevel(SpellID(SpellID::TELEPORT).toSpell());
 				else
 					skill = getActiveHero()->getSpellSchoolLevel (CGI->spellh->objects[spellToCast->additionalInfo]);
 				//TODO: explicitely save power, skill
@@ -2283,7 +2168,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
 
 	if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default
 		currentAction = selectedAction;
-	else if (localActions.size()) //if not possible, select first available action 9they are sorted by suggested priority)
+	else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority)
 		currentAction = localActions.front();
 	else //no legal action possible
 	{

+ 7 - 3
client/battle/CBattleInterface.h

@@ -1,12 +1,12 @@
 #pragma once
 
-
-//#include "../../lib/CCreatureSet.h"
 #include "../../lib/ConstTransitivePtr.h" //may be reundant
 #include "../../lib/GameConstants.h"
 
 #include "CBattleAnimations.h"
 
+#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+
 /*
  * CBattleInterface.h, part of VCMI engine
  *
@@ -155,7 +155,7 @@ private:
 
 	shared_ptr<CPlayerInterface> tacticianInterface; //used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players
 	bool tacticsMode;
-	bool stackCanCastSpell; //if true, active stack could possibly cats some target spell
+	bool stackCanCastSpell; //if true, active stack could possibly cast some target spell
 	bool creatureCasting; //if true, stack currently aims to cats a spell
 	bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console
 	PossibleActions spellSelMode;
@@ -319,8 +319,12 @@ public:
 	void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks
 	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
 	void displayEffect(ui32 effect, int destTile, bool areaEffect = true); //displays custom effect on the battlefield
+	
+	void displaySpellCast(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s cast animation
 	void displaySpellEffect(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation
 	void displaySpellHit(SpellID spellID, BattleHex destinationTile, bool areaEffect = true); //displays spell`s affected animation
+		
+	void displaySpellAnimation(const CSpell::TAnimation & animation, BattleHex destinationTile, bool areaEffect = true);
 	
 	void battleTriggerEffect(const BattleTriggerEffect & bte);
 	void setBattleCursor(const int myNumber); //really complex and messy, sets attackingHex

+ 1 - 0
client/battle/CBattleInterfaceClasses.cpp

@@ -69,6 +69,7 @@ void CBattleConsole::showAll(SDL_Surface * to)
 
 bool CBattleConsole::addText(const std::string & text)
 {
+	logGlobal->traceStream() <<"CBattleConsole message: "<<text;
 	if(text.size()>70)
 		return false; //text too long!
 	int firstInToken = 0;

+ 1 - 1
client/battle/CCreatureAnimation.cpp

@@ -178,7 +178,7 @@ CCreatureAnimation::CCreatureAnimation(std::string name, TSpeedController contro
 		elem.r = reader.readUInt8();
 		elem.g = reader.readUInt8();
 		elem.b = reader.readUInt8();
-		CSDL_Ext::colorSetAlpha(elem,0);
+		elem.a = SDL_ALPHA_OPAQUE;
 	}
 
 	for (int i=0; i<totalBlocks; i++)

+ 6 - 2
client/gui/CAnimation.cpp

@@ -158,7 +158,7 @@ CDefFile::CDefFile(std::string Name):
 		palette[i].r = data[it++];
 		palette[i].g = data[it++];
 		palette[i].b = data[it++];
-		CSDL_Ext::colorSetAlpha(palette[i],255);	
+		palette[i].a = SDL_ALPHA_OPAQUE;
 	}
 	if (type == 71 || type == 64)//Buttons/buildings don't have shadows\semi-transparency
 		memset(palette, 0, sizeof(SDL_Color)*2);
@@ -355,7 +355,11 @@ void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_C
 	image->fullSize = FullSize;
 
 	//Prepare surface
-	SDL_SetColors(image->surf, pal, 0, 256);
+	SDL_Palette * p = SDL_AllocPalette(256);	
+	SDL_SetPaletteColors(p, pal, 0, 256);
+	SDL_SetSurfacePalette(image->surf, p);
+	SDL_FreePalette(p);	
+
 	SDL_LockSurface(image->surf);
 	lineStart = position = (ui8*)image->surf->pixels;
 }

+ 0 - 5
client/gui/SDL_Extensions.h

@@ -59,11 +59,6 @@ inline bool isShiftKeyDown()
 }
 namespace CSDL_Ext
 {
-	//todo: remove
-	STRONG_INLINE void colorSetAlpha(SDL_Color & color, Uint8 alpha)
-	{
-		color.a = alpha;
-	}
 	//todo: should this better be assignment operator?
 	STRONG_INLINE void colorAssign(SDL_Color & dest, const SDL_Color & source)
 	{

+ 21 - 18
client/widgets/CGarrisonInt.cpp

@@ -293,27 +293,30 @@ void CGarrisonSlot::clickLeft(tribool down, bool previousState)
 			redraw();
 			refr = true;
 		}
-		// we want to split
-		else if(  (owner->getSplittingMode() || LOCPLINT->shiftPressed())
-		       && (!creature || creature == selection->creature) )
-			refr = split();
-		// swap
-		else if(creature != selection->creature)
-		{
-			const CArmedInstance * selectedObj = owner->armedObjs[selection->upg];
-			if (!creature && selectedObj->stacksCount() == 1)
-				LOCPLINT->cb->splitStack(selectedObj, owner->armedObjs[upg], selection->ID, ID, myStack->count - 1);
-			else
-				LOCPLINT->cb->swapCreatures(owner->armedObjs[upg], owner->armedObjs[selection->upg], ID, selection->ID);
-		}
-		// merge
 		else
 		{
 			const CArmedInstance * selectedObj = owner->armedObjs[selection->upg];
-			if (selectedObj->stacksCount() == 1)
-				LOCPLINT->cb->splitStack(owner->armedObjs[upg], selectedObj, selection->ID, ID, 1);
-			else
-				LOCPLINT->cb->mergeStacks(owner->armedObjs[selection->upg], owner->armedObjs[upg], selection->ID, ID);
+			bool lastHeroStackSelected = false;
+			if(selectedObj->stacksCount() == 1
+				&& owner->getSelection()->upg != upg
+				&& dynamic_cast<const CGHeroInstance*>(selectedObj))
+			{
+				lastHeroStackSelected = true;
+			}
+
+			if((owner->getSplittingMode() || LOCPLINT->shiftPressed()) // split window
+				&& (!creature || creature == selection->creature))
+			{
+				refr = split();
+			}
+			else if(!creature && lastHeroStackSelected) // split all except last creature
+				LOCPLINT->cb->splitStack(selectedObj, owner->armedObjs[upg], selection->ID, ID, selection->myStack->count - 1);
+			else if(creature != selection->creature) // swap
+				LOCPLINT->cb->swapCreatures(owner->armedObjs[upg], selectedObj, ID, selection->ID);
+			else if(lastHeroStackSelected) // merge last stack to other hero stack
+				refr = split();
+			else // merge
+				LOCPLINT->cb->mergeStacks(selectedObj, owner->armedObjs[upg], selection->ID, ID);
 		}
 		if(refr)
 		{

+ 14 - 19
client/widgets/MiscWidgets.cpp

@@ -361,40 +361,35 @@ void MoraleLuckBox::set(const IBonusBearer *node)
 	const int hoverTextBase[] = {7, 4};
 	const Bonus::BonusType bonusType[] = {Bonus::LUCK, Bonus::MORALE};
 	int (IBonusBearer::*getValue[])() const = {&IBonusBearer::LuckVal, &IBonusBearer::MoraleVal};
-
-	int mrlt = -9;
-	TModDescr mrl;
+	TBonusListPtr modifierList(new BonusList());
 
 	if (node)
 	{
-		node->getModifiersWDescr(mrl, bonusType[morale]);
+		modifierList = node->getBonuses(Selector::type(bonusType[morale]));
 		bonusValue = (node->*getValue[morale])();
 	}
 	else
 		bonusValue = 0;
 
-	mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good
+	int mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good
 	hoverText = CGI->generaltexth->heroscrn[hoverTextBase[morale] - mrlt];
 	baseType = componentType[morale];
 	text = CGI->generaltexth->arraytxt[textId[morale]];
 	boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]);
-	if (!mrl.size())
-		text += CGI->generaltexth->arraytxt[noneTxtId];
+	
+	if (morale && node && (node->hasBonusOfType(Bonus::UNDEAD) 
+			|| node->hasBonusOfType(Bonus::BLOCK_MORALE) 
+			|| node->hasBonusOfType(Bonus::NON_LIVING)))
+		text += CGI->generaltexth->arraytxt[113]; //unaffected by morale		
+	else if(modifierList->empty())
+		text += CGI->generaltexth->arraytxt[noneTxtId];//no modifiers
 	else
 	{
-		//it's a creature window
-		if ((morale && node && node->hasBonusOfType(Bonus::UNDEAD)) ||
-			node->hasBonusOfType(Bonus::BLOCK_MORALE) || node->hasBonusOfType(Bonus::NON_LIVING))
-		{
-			text += CGI->generaltexth->arraytxt[113]; //unaffected by morale
-		}
-		else
+		for(const Bonus * elem : *modifierList)
 		{
-			for(auto & elem : mrl)
-			{
-				if (elem.first) //no bonuses with value 0
-					text += "\n" + elem.second;
-			}
+			if(elem->val != 0)
+				//no bonuses with value 0
+				text += "\n" + elem->Description();
 		}
 	}
 

+ 2 - 2
client/widgets/TextControls.cpp

@@ -498,10 +498,10 @@ void CTextInput::setText( const std::string &nText, bool callCb )
 
 bool CTextInput::captureThisEvent(const SDL_KeyboardEvent & key)
 {
-	if(key.keysym.sym == SDLK_RETURN || key.keysym.sym == SDLK_KP_ENTER)
+	if(key.keysym.sym == SDLK_RETURN || key.keysym.sym == SDLK_KP_ENTER || key.keysym.sym == SDLK_ESCAPE)
 		return false;
 	
-	return false;
+	return true;
 }
 
 void CTextInput::textInputed(const SDL_TextInputEvent & event)

+ 2 - 2
client/windows/CQuestLog.cpp

@@ -135,7 +135,7 @@ void CQuestLog::init()
 	minimap = new CQuestMinimap (Rect (12, 12, 169, 169));
 	// TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin
 	description = new CTextBox ("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, TOPLEFT, Colors::WHITE);
-	ok = new CButton(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], boost::bind(&CQuestLog::close,this), SDLK_RETURN);
+	ok = new CButton(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close,this), SDLK_RETURN);
 	// Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button
 	hideCompleteButton = new CToggleButton(Point(10, 396), "sysopchk.def", CButton::tooltip(texts["hideComplete"]), std::bind(&CQuestLog::toggleComplete, this, _1));
 	hideCompleteLabel = new CLabel(46, 398, FONT_MEDIUM, TOPLEFT, Colors::WHITE, texts["hideComplete"]["label"].String());
@@ -182,7 +182,7 @@ void CQuestLog::recreateLabelList()
 		auto label = make_shared<CQuestLabel>(Rect(13, 195, 149,31), FONT_SMALL, TOPLEFT, Colors::WHITE, text.toString());
 		label->disable();
 
-		label->callback = boost::bind(&CQuestLog::selectQuest, this, i, currentLabel);
+		label->callback = std::bind(&CQuestLog::selectQuest, this, i, currentLabel);
 		labels.push_back(label);
 
 		// Select latest active quest

+ 10 - 10
config/heroes/fortress.json

@@ -11,8 +11,8 @@
 		],
 		"specialties":
 		[
-			{ "type":10, "val": 1, "subtype": 5, "info": 0 }
-		],
+			{ "type":1, "val": 0, "subtype": 0, "info": 106 }
+		],		
 		"army" :
 		[
 			{
@@ -41,7 +41,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 106 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 98 }
 		]
 	},
 	"wystan":
@@ -56,7 +56,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 98 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 100 }
 		]
 	},
 	"tazar":
@@ -70,7 +70,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 100 }
+			{ "type":2, "val": 5, "subtype": 23, "info": 0 }
 		]
 	},
 	"alkin":
@@ -85,7 +85,7 @@
 		],
 		"specialties":
 		[
-			{ "type":2, "val": 5, "subtype": 23, "info": 0 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 102 }
 		]
 	},
 	"korbac":
@@ -100,7 +100,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 102 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 104 }
 		]
 	},
 	"gerwulf":
@@ -115,7 +115,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 104 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
 		]
 	},
 	"broghild":
@@ -130,7 +130,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
+			{ "type":1, "val": 0, "subtype": 0, "info": 108 }
 		]
 	},
 	"mirlanda":
@@ -145,7 +145,7 @@
 		],
 		"specialties":
 		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 108 }
+			{ "type":8, "val": 0, "subtype": 45, "info": 0 }
 		]
 	},
 	"rosic":

+ 4 - 0
config/schemas/spell.json

@@ -12,6 +12,10 @@
 			"type": "array",
 			"items":{
 				"anyOf":[
+					{
+						//dummy animation, pause, Value - frame count
+						"type": "number"
+					},
 					{
 						//assumed verticalPosition: top
 						"type": "string",

+ 18 - 6
config/spells/other.json

@@ -242,7 +242,9 @@
 	"removeObstacle" : {
 		"index" : 64,
 		"targetType" : "OBSTACLE",		
-		
+		"animation":{
+			"cast":[2]
+		},		
 		"sounds": {
 			"cast": "REMOVEOB"
 		},
@@ -258,7 +260,9 @@
 	"clone" : {
 		"index" : 65,
 		"targetType" : "CREATURE",		
-		
+		"animation":{
+			"cast":[2]
+		},			
 		"sounds": {
 			"cast": "CLONE"
 		},
@@ -278,7 +282,9 @@
 	"fireElemental" : {
 		"index" : 66,
 		"targetType" : "NO_TARGET",		
-		
+		"animation":{
+			"cast":[2]
+		},			
 		"sounds": {
 			"cast": "SUMNELM"
 		},
@@ -294,7 +300,9 @@
 	"earthElemental" : {
 		"index" : 67,
 		"targetType" : "NO_TARGET",	
-		
+		"animation":{
+			"cast":[2]
+		},			
 		"sounds": {
 			"cast": "SUMNELM"
 		},
@@ -310,7 +318,9 @@
 	"waterElemental" : {
 		"index" : 68,
 		"targetType" : "NO_TARGET",	
-		
+		"animation":{
+			"cast":[2]
+		},			
 		"sounds": {
 			"cast": "SUMNELM"
 		},
@@ -326,7 +336,9 @@
 	"airElemental" : {
 		"index" : 69,
 		"targetType" : "NO_TARGET",	
-		
+		"animation":{
+			"cast":[2]
+		},			
 		"sounds": {
 			"cast": "SUMNELM"
 		},

+ 104 - 105
config/spells/timed.json

@@ -1,17 +1,17 @@
 {
 	"shield" : {
 		"index" : 27,
-		"targetType" : "CREATURE",			
-		
+		"targetType" : "CREATURE",
+
 		"animation":{
 			"affect":["C13SPE0"]
-		},		
+		},
 		"sounds": {
 			"cast": "SHIELD"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"generalDamageReduction" : {
@@ -32,16 +32,16 @@
 	"airShield" : {
 		"index" : 28,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C01SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "AIRSHELD"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"generalDamageReduction" : {
@@ -62,19 +62,19 @@
 	"fireShield" : {
 		"index" : 29,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C05SPF0"]
-		},		
+		},
 		"sounds": {
 			"cast": "FIRESHIE"
 		},
 		// It looks that fireshield has two separate sounds
 		//			"soundfile":"FIRESHLD.wav"
-		//		
+		//
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"fireShield" : {
@@ -82,9 +82,6 @@
 						"duration" : "N_TURNS"
 					}
 				}
-			},
-			"expert":{
-				"range" : "X"
 			}
 		},
 		"flags" : {
@@ -94,10 +91,10 @@
 	"protectAir" : {
 		"index" : 30,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C11SPE0"]
-		},		
+		},
 		"sounds": {
 			"cast": "PROTECTA"
 		},
@@ -124,10 +121,10 @@
 	"protectFire" : {
 		"index" : 31,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C11SPW0"]
-		},		
+		},
 		"sounds": {
 			"cast": "PROTECTF"
 		},
@@ -154,16 +151,16 @@
 	"protectWater" : {
 		"index" : 32,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C11SPF0"]
-		},		
+		},
 		"sounds": {
 			"cast": "PROTECTW"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"spellDamageReduction" : {
@@ -184,10 +181,10 @@
 	"protectEarth" : {
 		"index" : 33,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C13SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "PROTECTE"
 		},
@@ -214,10 +211,10 @@
 	"antiMagic" : {
 		"index" : 34,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C02SPE0"]
-		},		
+		},
 		"sounds": {
 			"cast": "ANTIMAGK"
 		},
@@ -227,7 +224,7 @@
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"levelSpellImmunity" : {
-						"val" : 3,					
+						"val" : 3,
 						"type" : "LEVEL_SPELL_IMMUNITY",
 						"valueType" : "INDEPENDENT_MAX",
 						"duration" : "N_TURNS"
@@ -257,10 +254,10 @@
 	"magicMirror" : {
 		"index" : 36,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C02SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "BACKLASH"
 		},
@@ -284,16 +281,16 @@
 	"bless" : {
 		"index" : 41,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C01SPW"] //C01SPW0
-		},		
+		},
 		"sounds": {
 			"cast": "BLESS"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"alwaysMaximumDamage" : {
@@ -322,16 +319,16 @@
 	"curse" : {
 		"index" : 42,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C04SPW"]//C04SPW0
-		},		
+		},
 		"sounds": {
 			"cast": "CURSE"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"alwaysMinimumDamage" : {
@@ -362,20 +359,20 @@
 	"bloodlust" : {
 		"index" : 43,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["SP12_"] //???
-		},		
+		},
 		"sounds": {
 			"cast": "BLOODLUS"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"primarySkill" : {
-						"val" : 3,					
+						"val" : 3,
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.attack",
 						"effectRange" : "ONLY_MELEE_FIGHT",
@@ -412,10 +409,10 @@
 	"precision" : {
 		"index" : 44,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C12SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "PRECISON"
 		},
@@ -459,22 +456,22 @@
 	"weakness" : {
 		"index" : 45,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C0ACID"]
-		},		
+		},
 		"sounds": {
 			"cast": "WEAKNESS"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"primarySkill" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.attack",
-						"val" : -3,						
+						"val" : -3,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -506,22 +503,22 @@
 	"stoneSkin" : {
 		"index" : 46,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C16SPE"] //C16SPE0
-		},		
+		},
 		"sounds": {
 			"cast": "TUFFSKIN"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"primarySkill" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.defence",
-						"val" : 3,						
+						"val" : 3,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -549,11 +546,11 @@
 	"disruptingRay" : {
 		"index" : 47,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C07SPA1"],
 			"projectile":[{"defName":"C07SPA0"}]//???
-		},		
+		},
 		"sounds": {
 			"cast": "DISRUPTR"
 		},
@@ -565,7 +562,7 @@
 					"primarySkill" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.defence",
-						"val" : -3,						
+						"val" : -3,
 						"valueType" : "ADDITIVE_VALUE",
 						"duration" : "N_TURNS"
 					}
@@ -593,34 +590,34 @@
 	"prayer" : {
 		"index" : 48,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":[{"defName":"C10SPW", "verticalPosition":"bottom"}]
-		},		
+		},
 		"sounds": {
 			"cast": "PRAYER"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"attack" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.attack",
-						"val" : 2,						
+						"val" : 2,
 						"duration" : "N_TURNS"
 					},
 					"defence" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.defence",
-						"val" : 2,						
+						"val" : 2,
 						"duration" : "N_TURNS"
 					},
 					"stacksSpeed" : {
 						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
-						"val" : 2,						
+						"val" : 2,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -660,21 +657,21 @@
 	"mirth" : {
 		"index" : 49,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C09SPW0"]
-		},		
+		},
 		"sounds": {
 			"cast": "MIRTH"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"morale" : {
 						"type" : "MORALE",
-						"val" : 1,						
+						"val" : 1,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -700,7 +697,7 @@
 		},
 		"absoluteImmunity":{
 			"SIEGE_WEAPON": true,
-			"UNDEAD": true,		
+			"UNDEAD": true,
 		},
 		"immunity" : {
 			"MIND_IMMUNITY": true,
@@ -713,21 +710,21 @@
 	"sorrow" : {
 		"index" : 50,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C14SPE0"]
-		},		
+		},
 		"sounds": {
 			"cast": "SORROW"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"morale" : {
 						"type" : "MORALE",
-						"val" : -1,						
+						"val" : -1,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -753,7 +750,7 @@
 		},
 		"absoluteImmunity":{
 			"SIEGE_WEAPON": true,
-			"UNDEAD": true,		
+			"UNDEAD": true,
 		},
 		"immunity" : {
 			"MIND_IMMUNITY": true,
@@ -766,21 +763,21 @@
 	"fortune" : {
 		"index" : 51,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C09SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "FORTUNE"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"luck" : {
 						"type" : "LUCK",
-						"val" : 1,						
+						"val" : 1,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -811,21 +808,21 @@
 	"misfortune" : {
 		"index" : 52,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C10SPF0"]
-		},		
+		},
 		"sounds": {
 			"cast": "MISFORT"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"luck" : {
 						"type" : "LUCK",
-						"val" : -1,						
+						"val" : -1,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -856,22 +853,22 @@
 	"haste" : {
 		"index" : 53,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C15SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "HASTE"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"stacksSpeed" : {
 						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
-						"val" : 3,						
+						"val" : 3,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -905,22 +902,22 @@
 	"slow" : {
 		"index" : 54,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":[{"defName":"C09SPE0", "verticalPosition":"bottom"}]
-		},		
+		},
 		"sounds": {
 			"cast": "MUCKMIRE"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"stacksSpeed" : {
 						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
-						"val" : -25,						
+						"val" : -25,
 						"valueType" : "PERCENT_TO_ALL",
 						"duration" : "N_TURNS"
 					}
@@ -956,10 +953,10 @@
 	"slayer" : {
 		"index" : 55,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C13SPW0"]
-		},		
+		},
 		"sounds": {
 			"cast": "SLAYER"
 		},
@@ -1010,10 +1007,10 @@
 	"frenzy" : {
 		"index" : 56,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C08SPF0"]
-		},		
+		},
 		"sounds": {
 			"cast": "FRENZY"
 		},
@@ -1024,8 +1021,9 @@
 				"effects" : {
 					"inFrenzy" : {
 						"type" : "IN_FRENZY",
-						"val" : 100,						
-						"duration" : "STACK_GETS_TURN"
+						"val" : 100,
+						"duration" : "N_TURNS",
+						"turns" : 1
 					}
 				}
 			},
@@ -1055,21 +1053,21 @@
 	"counterstrike" : {
 		"index" : 58,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C04SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "CNTRSTRK"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"additionalRetaliation" : {
 						"type" : "ADDITIONAL_RETALIATION",
-						"val" : 1,						
+						"val" : 1,
 						"duration" : "N_TURNS"
 					}
 				}
@@ -1099,11 +1097,11 @@
 	},
 	"berserk" : {
 		"index" : 59,
-		"targetType" : "CREATURE",
-		
+		"targetType" : "LOCATION",
+
 		"animation":{
 			"affect":["C01SPF"] //C01SPF0
-		},		
+		},
 		"sounds": {
 			"cast": "BERSERK"
 		},
@@ -1114,7 +1112,8 @@
 				"effects" : {
 					"attacksNearestCreature" : {
 						"type" : "ATTACKS_NEAREST_CREATURE",
-						"duration" : "N_TURNS"
+						"duration" : "N_TURNS",
+						"turns" : 1
 					}
 				}
 			},
@@ -1164,10 +1163,10 @@
 	"hypnotize" : {
 		"index" : 60,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C10SPA0"]
-		},		
+		},
 		"sounds": {
 			"cast": "HYPNOTIZ"
 		},
@@ -1225,17 +1224,17 @@
 	},
 	"forgetfulness" : {
 		"index" : 61,
-		"targetType" : "CREATURE", 
-		
+		"targetType" : "CREATURE",
+
 		"animation":{
 			"affect":["C06SPW"]//C06SPW0
-		},		
+		},
 		"sounds": {
 			"cast": "FORGET"
 		},
 		"levels" : {
 			"base":{
-				"range" : "0",			
+				"range" : "0",
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"forgetful" : {
@@ -1292,10 +1291,10 @@
 	"blind" : {
 		"index" : 62,
 		"targetType" : "CREATURE",
-		
+
 		"animation":{
 			"affect":["C02SPF0"]
-		},		
+		},
 		"sounds": {
 			"cast": "BLIND"
 		},

+ 40 - 7
lib/BattleState.cpp

@@ -763,7 +763,7 @@ CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const
 
 CStack::CStack(const CStackInstance *Base, PlayerColor O, int I, bool AO, SlotID S)
 	: base(Base), ID(I), owner(O), slot(S), attackerOwned(AO),
-	counterAttacks(1)
+	counterAttacksPerformed(0),counterAttacksTotalCache(0), cloneID(-1)
 {
 	assert(base);
 	type = base->type;
@@ -776,7 +776,8 @@ CStack::CStack()
 	setNodeType(STACK_BATTLE);
 }
 CStack::CStack(const CStackBasicDescriptor *stack, PlayerColor O, int I, bool AO, SlotID S)
-	: base(nullptr), ID(I), owner(O), slot(S), attackerOwned(AO), counterAttacks(1)
+	: base(nullptr), ID(I), owner(O), slot(S), attackerOwned(AO), counterAttacksPerformed(0),
+	 cloneID(-1)
 {
 	type = stack->type;
 	count = baseAmount = stack->count;
@@ -794,7 +795,9 @@ void CStack::init()
 	slot = SlotID(255);
 	attackerOwned = false;
 	position = BattleHex();
-	counterAttacks = -1;
+	counterAttacksPerformed = 0;
+	counterAttacksTotalCache = 0;
+	cloneID = -1;
 }
 
 void CStack::postInit()
@@ -804,9 +807,11 @@ void CStack::postInit()
 
 	firstHPleft = MaxHealth();
 	shots = getCreature()->valOfBonuses(Bonus::SHOTS);
-	counterAttacks = 1 + valOfBonuses(Bonus::ADDITIONAL_RETALIATION);
+	counterAttacksPerformed = 0;
+	counterAttacksTotalCache = 0;
 	casts = valOfBonuses(Bonus::CASTS);
 	resurrected = 0;
+	cloneID = -1;
 }
 
 ui32 CStack::level() const
@@ -848,10 +853,10 @@ void CStack::stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse)
 
 	for(Bonus& b : tmp)
 	{
-		b.turnsRemain =  sse.turnsRemain;
+		if(b.turnsRemain == 0)
+			b.turnsRemain = sse.turnsRemain;
 		sf.push_back(b);
 	}
-
 }
 
 bool CStack::willMove(int turn /*= 0*/) const
@@ -1129,12 +1134,25 @@ bool CStack::isMeleeAttackPossible(const CStack * attacker, const CStack * defen
 bool CStack::ableToRetaliate() const //FIXME: crash after clone is killed
 {
 	return alive()
-		&& (counterAttacks > 0 || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS))
+		&& (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;
+}
+
 std::string CStack::getName() const
 {
 	return (count > 1) ? type->namePl : type->nameSing; //War machines can't use base
@@ -1152,6 +1170,21 @@ bool CStack::canBeHealed() const
 		&& !hasBonusOfType(Bonus::SIEGE_WEAPON);
 }
 
+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;
+}
+
+ui32 CStack::getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const
+{
+	//stacks does not have spellpower etc. (yet?)
+	return base;
+}
+
 bool CMP_stack::operator()( const CStack* a, const CStack* b )
 {
 	switch(phase)

+ 27 - 15
lib/BattleState.h

@@ -1,5 +1,14 @@
-#pragma once
+/*
+ * BattleState.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 
+#pragma once
 
 #include "BattleHex.h"
 #include "HeroBonus.h"
@@ -12,16 +21,7 @@
 #include "GameConstants.h"
 #include "CBattleCallback.h"
 #include "int3.h"
-
-/*
- * BattleState.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
+#include "spells/Magic.h"
 
 class CGHeroInstance;
 class CStack;
@@ -159,7 +159,7 @@ struct DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallb
 	static int battlefieldTypeToTerrain(int bfieldType); //converts above to ERM BI format
 };
 
-class DLL_LINKAGE CStack : public CBonusSystemNode, public CStackBasicDescriptor
+class DLL_LINKAGE CStack : public CBonusSystemNode, public CStackBasicDescriptor, public ISpellCaster
 {
 public:
 	const CStackInstance *base; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc)
@@ -171,11 +171,15 @@ public:
 	SlotID slot;  //slot - position in garrison (may be 255 for neutrals/called creatures)
 	bool attackerOwned; //if true, this stack is owned by attakcer (this one from left hand side of battle)
 	BattleHex position; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower
-	ui8 counterAttacks; //how many counter attacks can be performed more in this turn (by default set at the beginning of the round to 1)
+	///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
-
+	///id of alive clone of this stack clone if any
+	si32 cloneID;
 	std::set<EBattleStackState::EBattleStackState> state;
 	//overrides
 	const CCreature* getCreature() const {return type;}
@@ -191,6 +195,10 @@ public:
 	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 canMove(int turn = 0) const; //if stack can move
@@ -222,12 +230,16 @@ public:
 	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
 
+	///ISpellCaster
+	ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const override;
+	ui32 getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const override;
+	
 	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 & attackerOwned & position & state & counterAttacks
+		h & ID & baseAmount & firstHPleft & owner & slot & attackerOwned & position & state & counterAttacksPerformed
 			& shots & casts & count & resurrected;
 
 		const CArmedInstance *army = (base ? base->armyObj : nullptr);

+ 1 - 3
lib/CConsoleHandler.cpp

@@ -28,8 +28,8 @@ DLL_LINKAGE CConsoleHandler * console = nullptr;
 	#define CONSOLE_TEAL "\x1b[1;36m"
 #else
 	#include <Windows.h>
+	#include <dbghelp.h>	
 #ifndef __MINGW32__
-	#include <dbghelp.h>
 	#pragma comment(lib, "dbghelp.lib")
 #endif
 	typedef WORD TColor;
@@ -121,7 +121,6 @@ LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
 	const DWORD threadId = ::GetCurrentThreadId();
     logGlobal->errorStream() << "Thread ID: " << threadId << " [" << std::dec << std::setw(0) << threadId << "]";
 
-#ifndef __MINGW32__
 	//exception info to be placed in the dump
 	MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE};
 
@@ -140,7 +139,6 @@ LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
 	HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
     logGlobal->errorStream() << "Crash info will be put in " << mname;
 	MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, MiniDumpWithDataSegs, &meinfo, 0, 0);
-#endif
 	MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR);
 	return EXCEPTION_EXECUTE_HANDLER;
 }

+ 15 - 3
lib/CGeneralTextHandler.cpp

@@ -206,12 +206,24 @@ std::string CLegacyConfigParser::extractQuotedString()
 	{
 		ret += extractQuotedPart();
 
-		// double quote - add it to string and continue unless
-		// line terminated using tabulation
-		if (curr < end && *curr == '\"' && *curr != '\t')
+		// double quote - add it to string and continue quoted part
+		if (curr < end && *curr == '\"')
 		{
 			ret += '\"';
 		}
+		//extract normal part
+		else if(curr < end && *curr != '\t' && *curr != '\r')
+		{
+			char * begin = curr;
+
+			while (curr < end && *curr != '\t' && *curr != '\r' && *curr != '\"')//find end of string or next quoted part start
+				curr++;	
+				
+			ret += std::string(begin, curr);
+			
+			if(curr>=end || *curr != '\"')
+				return ret;
+		}		
 		else // end of string
 			return ret;
 	}

+ 2 - 2
lib/Connection.h

@@ -27,8 +27,8 @@
 #include "mapping/CCampaignHandler.h" //for CCampaignState
 #include "rmg/CMapGenerator.h" // for CMapGenOptions
 
-const ui32 version = 753;
-const ui32 minSupportedVersion = version;
+const ui32 version = 754;
+const ui32 minSupportedVersion = 753;
 
 class CISer;
 class COSer;

+ 3 - 0
lib/GameConstants.h

@@ -864,7 +864,10 @@ public:
 		WALKING_DEAD = 58,
 		WIGHTS = 60,
 		LICHES = 64,
+		BONE_DRAGON = 68,
 		TROGLODYTES = 70,
+		HYDRA = 110,
+		CHAOS_HYDRA = 111,
 		AIR_ELEMENTAL = 112,
 		EARTH_ELEMENTAL = 113,
 		FIRE_ELEMENTAL = 114,

+ 34 - 53
lib/HeroBonus.cpp

@@ -211,21 +211,8 @@ Bonus * BonusList::getFirst(const CSelector &select)
 	return nullptr;
 }
 
-void BonusList::getModifiersWDescr(TModDescr &out) const
-{
-	for (auto & elem : bonuses)
-	{
-		Bonus *b = elem;
-		out.push_back(std::make_pair(b->val, b->Description()));
-	}
-}
-
 void BonusList::getBonuses(BonusList & out, const CSelector &selector) const
 {
-// 	for(Bonus *i : *this)
-// 		if(selector(i) && i->effectRange == Bonus::NO_LIMIT)
-// 			out.push_back(i);
-
 	getBonuses(out, selector, nullptr);
 }
 
@@ -359,17 +346,6 @@ bool IBonusBearer::hasBonusOfType(Bonus::BonusType type, int subtype /*= -1*/) c
 	return hasBonus(s, cachingStr.str());
 }
 
-void IBonusBearer::getModifiersWDescr(TModDescr &out, Bonus::BonusType type, int subtype /*= -1 */) const
-{
-	std::stringstream cachingStr;
-	cachingStr << "type_" << type << "s_" << subtype;
-	getModifiersWDescr(out, subtype != -1 ? Selector::typeSubtype(type, subtype) : Selector::type(type), cachingStr.str());
-}
-
-void IBonusBearer::getModifiersWDescr(TModDescr &out, const CSelector &selector, const std::string &cachingStr /* =""*/) const
-{
-	getBonuses(selector, cachingStr)->getModifiersWDescr(out);
-}
 int IBonusBearer::getBonusesCount(Bonus::BonusSource from, int id) const
 {
 	std::stringstream cachingStr;
@@ -524,8 +500,13 @@ bool IBonusBearer::isLiving() const //TODO: theoreticaly there exists "LIVING" b
 const TBonusListPtr IBonusBearer::getSpellBonuses() const
 {
 	std::stringstream cachingStr;
-	cachingStr << "source_" << Bonus::SPELL_EFFECT;
-	return getBonuses(Selector::sourceType(Bonus::SPELL_EFFECT), Selector::anyRange(), cachingStr.str());
+	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;
+		})); 
+	return getBonuses(selector, Selector::anyRange(), cachingStr.str());
 }
 
 const Bonus * IBonusBearer::getEffect(ui16 id, int turn /*= 0*/) const
@@ -1105,12 +1086,6 @@ bool NBonus::hasOfType(const CBonusSystemNode *obj, Bonus::BonusType type, int s
 	return false;
 }
 
-void NBonus::getModifiersWDescr(const CBonusSystemNode *obj, TModDescr &out, Bonus::BonusType type, int subtype /*= -1 */)
-{
-	if(obj)
-		return obj->getModifiersWDescr(out, type, subtype);
-}
-
 int NBonus::getCount(const CBonusSystemNode *obj, Bonus::BonusSource from, int id)
 {
 	if(obj)
@@ -1127,28 +1102,34 @@ const CSpell * Bonus::sourceSpell() const
 
 std::string Bonus::Description() const
 {
-	if(description.size())
-		return description;
-
 	std::ostringstream str;
-	str << std::showpos << val << " ";
-
-	switch(source)
-	{
-	case ARTIFACT:
-		str << VLC->arth->artifacts[sid]->Name();
-		break;;
-	case SPELL_EFFECT:
-		str << SpellID(sid).toSpell()->name;
-		break;
-	case CREATURE_ABILITY:
-		str << VLC->creh->creatures[sid]->namePl;
-		break;
-	case SECONDARY_SKILL:
-		str << VLC->generaltexth->skillName[sid]/* << " secondary skill"*/;
-		break;
-	}
-
+	
+	if(description.empty())
+		switch(source)
+		{
+		case ARTIFACT:
+			str << VLC->arth->artifacts[sid]->Name();
+			break;;
+		case SPELL_EFFECT:
+			str << SpellID(sid).toSpell()->name;
+			break;
+		case CREATURE_ABILITY:
+			str << VLC->creh->creatures[sid]->namePl;
+			break;
+		case SECONDARY_SKILL:
+			str << VLC->generaltexth->skillName[sid]/* << " secondary skill"*/;
+			break;
+		default:
+			//todo: handle all possible sources
+			str << "Unknown"; 
+			break;
+		}
+	else
+		str << description;
+		
+	if(val != 0)
+		str << " " << std::showpos << val;
+	
 	return str.str();
 }
 

+ 0 - 6
lib/HeroBonus.h

@@ -23,7 +23,6 @@ class BonusList;
 typedef shared_ptr<BonusList> TBonusListPtr;
 typedef shared_ptr<ILimiter> TLimiterPtr;
 typedef shared_ptr<IPropagator> TPropagatorPtr;
-typedef std::vector<std::pair<int,std::string> > TModDescr; //modifiers values and their descriptions
 typedef std::set<CBonusSystemNode*> TNodes;
 typedef std::set<const CBonusSystemNode*> TCNodes;
 typedef std::vector<CBonusSystemNode *> TNodesVector;
@@ -448,7 +447,6 @@ public:
 	int totalValue() const; //subtype -> subtype of bonus, if -1 then any
 	void getBonuses(BonusList &out, const CSelector &selector, const CSelector &limit) const;
 	void getAllBonuses(BonusList &out) const;
-	void getModifiersWDescr(TModDescr &out) const;
 
 	void getBonuses(BonusList & out, const CSelector &selector) const;
 
@@ -579,7 +577,6 @@ public:
 	// * root is node on which call was made (nullptr will be replaced with this)
 	//interface
 	virtual const TBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const = 0;
-	void getModifiersWDescr(TModDescr &out, const CSelector &selector, const std::string &cachingStr = "") const;  //out: pairs<modifier value, modifier description>
 	int getBonusesCount(const CSelector &selector, const std::string &cachingStr = "") const;
 	int valOfBonuses(const CSelector &selector, const std::string &cachingStr = "") const;
 	bool hasBonus(const CSelector &selector, const std::string &cachingStr = "") const;
@@ -594,7 +591,6 @@ public:
 	int valOfBonuses(Bonus::BonusType type, int subtype = -1) const; //subtype -> subtype of bonus, if -1 then anyt;
 	bool hasBonusOfType(Bonus::BonusType type, int subtype = -1) const;//determines if hero has a bonus of given type (and optionally subtype)
 	bool hasBonusFrom(Bonus::BonusSource source, ui32 sourceID) const;
-	void getModifiersWDescr( TModDescr &out, Bonus::BonusType type, int subtype = -1 ) const;  //out: pairs<modifier value, modifier description>
 	int getBonusesCount(Bonus::BonusSource from, int id) const;
 
 	//various hlp functions for non-trivial values
@@ -722,8 +718,6 @@ namespace NBonus
 	//set of methods that may be safely called with nullptr objs
 	DLL_LINKAGE int valOf(const CBonusSystemNode *obj, Bonus::BonusType type, int subtype = -1); //subtype -> subtype of bonus, if -1 then any
 	DLL_LINKAGE bool hasOfType(const CBonusSystemNode *obj, Bonus::BonusType type, int subtype = -1);//determines if hero has a bonus of given type (and optionally subtype)
-	//DLL_LINKAGE const HeroBonus * get(const CBonusSystemNode *obj, int from, int id );
-	DLL_LINKAGE void getModifiersWDescr(const CBonusSystemNode *obj, TModDescr &out, Bonus::BonusType type, int subtype = -1 );  //out: pairs<modifier value, modifier description>
 	DLL_LINKAGE int getCount(const CBonusSystemNode *obj, Bonus::BonusSource from, int id);
 }
 

+ 1 - 1
lib/IGameCallback.h

@@ -61,7 +61,7 @@ public:
 	virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0;
 	virtual void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) =0;
 	virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0;
-	virtual bool changeStackType(const StackLocation &sl, CCreature *c) =0;
+	virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0;
 	virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack
 	virtual bool eraseStack(const StackLocation &sl, bool forceRemoval = false) =0;
 	virtual bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) =0;

+ 18 - 8
lib/NetPacks.h

@@ -795,7 +795,7 @@ struct ChangeStackCount : CGarrisonOperationPack  //521
 struct SetStackType : CGarrisonOperationPack  //522
 {
 	StackLocation sl;
-	CCreature *type;
+	const CCreature *type;
 
 	void applyCl(CClient *cl);
 	DLL_LINKAGE void applyGs(CGameState *gs);
@@ -986,7 +986,6 @@ struct NewTurn : public CPackForClient //101
 	std::map<PlayerColor, TResources> res; //player ID => resource value[res_id]
 	std::map<ObjectInstanceID, SetAvailableCreatures> cres;//creatures to be placed in towns
 	ui32 day;
-	bool resetBuilded;
 	ui8 specialWeek; //weekType
 	CreatureID creatureid; //for creature weeks
 
@@ -994,7 +993,7 @@ struct NewTurn : public CPackForClient //101
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & heroes & cres & res & day & resetBuilded & specialWeek & creatureid;
+		h & heroes & cres & res & day & specialWeek & creatureid;
 	}
 };
 
@@ -1478,6 +1477,18 @@ struct EndAction : public CPackForClient//3008
 
 struct BattleSpellCast : public CPackForClient//3009
 {
+	///custom effect (resistance, reflection, etc)
+	struct CustomEffect
+	{
+		/// WoG AC format
+		ui32 effect;		
+		ui32 stack;
+		template <typename Handler> void serialize(Handler &h, const int version)
+		{
+			h & effect & stack;
+		}		
+	};
+
 	BattleSpellCast(){type = 3009; casterStack = -1;};
 	DLL_LINKAGE void applyGs(CGameState *gs);
 	void applyCl(CClient *cl);
@@ -1488,13 +1499,13 @@ struct BattleSpellCast : public CPackForClient//3009
 	ui8 skill; //caster's skill level
 	ui8 manaGained; //mana channeling ability
 	BattleHex tile; //destination tile (may not be set in some global/mass spells
-	std::vector<ui32> resisted; //ids of creatures that resisted this spell
+	std::vector<CustomEffect> customEffects;
 	std::set<ui32> affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure)
 	si32 casterStack;// -1 if not cated by creature, >=0 caster stack ID
-	bool castedByHero; //if true - spell has been casted by hero, otherwise by a creature
+	bool castByHero; //if true - spell has been casted by hero, otherwise by a creature
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & dmgToDisplay & side & id & skill & manaGained & tile & resisted & affectedCres & casterStack & castedByHero;
+		h & dmgToDisplay & side & id & skill & manaGained & tile & customEffects & affectedCres & casterStack & castByHero;
 	}
 };
 
@@ -1629,10 +1640,9 @@ struct BattleSetStackProperty : public CPackForClient //3018
 {
 	BattleSetStackProperty(){type = 3018;};
 
-	enum BattleStackProperty {CASTS, ENCHANTER_COUNTER, UNBIND, CLONED};
+	enum BattleStackProperty {CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE};
 
 	DLL_LINKAGE void applyGs(CGameState *gs);
-	//void applyCl(CClient *cl){};
 
 	int stackID;
 	BattleStackProperty which;

+ 14 - 3
lib/NetPacksLib.cpp

@@ -1095,7 +1095,8 @@ DLL_LINKAGE void BattleNextRound::applyGs( CGameState *gs )
 		s->state -= EBattleStackState::HAD_MORALE;
 		s->state -= EBattleStackState::FEAR;
 		s->state -= EBattleStackState::DRAINED_MANA;
-		s->counterAttacks = 1 + s->valOfBonuses(Bonus::ADDITIONAL_RETALIATION);
+		s->counterAttacksPerformed = 0;
+		s->counterAttacksTotalCache = 0;
 		// new turn effects
 		s->battleTurnPassed();
 	}
@@ -1247,7 +1248,12 @@ DLL_LINKAGE void BattleStackAttacked::applyGs( CGameState *gs )
 	{
 		//"hide" killed creatures instead so we keep info about it
 		at->state.insert(EBattleStackState::DEAD_CLONE);
-
+		
+		for(CStack * s : gs->curB->stacks)
+		{
+			if(s->cloneID == at->ID)
+				s->cloneID = -1;
+		}
 	}
 }
 
@@ -1255,7 +1261,7 @@ DLL_LINKAGE void BattleAttack::applyGs( CGameState *gs )
 {
 	CStack *attacker = gs->curB->getStack(stackAttacking);
 	if(counter())
-		attacker->counterAttacks--;
+		attacker->counterAttacksPerformed++;
 
 	if(shot())
 	{
@@ -1603,6 +1609,11 @@ DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs)
 			stack->state.insert(EBattleStackState::CLONED);
 			break;
 		}
+		case HAS_CLONE:
+		{
+			stack->cloneID = val;
+			break;
+		}
 	}
 }
 

+ 4 - 0
lib/VCMI_lib.cbp

@@ -31,6 +31,7 @@
 					<Add option="-lboost_chrono$(#boost.libsuffix)" />
 					<Add option="-lboost_locale$(#boost.libsuffix)" />
 					<Add option="-liconv" />
+					<Add option="-ldbghelp" />
 					<Add directory="$(#sdl2.lib)" />
 					<Add directory="$(#boost.lib32)" />
 					<Add directory="$(#zlib.lib)" />
@@ -59,6 +60,7 @@
 					<Add option="-lboost_chrono$(#boost.libsuffix)" />
 					<Add option="-lboost_locale$(#boost.libsuffix)" />
 					<Add option="-liconv" />
+					<Add option="-ldbghelp" />
 					<Add directory="$(#sdl2.lib)" />
 					<Add directory="$(#boost.lib32)" />
 					<Add directory="$(#zlib.lib)" />
@@ -88,6 +90,7 @@
 					<Add option="-lboost_chrono$(#boost.libsuffix)" />
 					<Add option="-lboost_locale$(#boost.libsuffix)" />
 					<Add option="-liconv" />
+					<Add option="-ldbghelp" />
 					<Add directory="$(#sdl2.lib64)" />
 					<Add directory="$(#boost.lib64)" />
 					<Add directory="$(#zlib64.lib)" />
@@ -311,6 +314,7 @@
 		<Unit filename="spells/CreatureSpellMechanics.h" />
 		<Unit filename="spells/ISpellMechanics.cpp" />
 		<Unit filename="spells/ISpellMechanics.h" />
+		<Unit filename="spells/Magic.h" />
 		<Unit filename="spells/ViewSpellInt.cpp" />
 		<Unit filename="spells/ViewSpellInt.h" />
 		<Unit filename="vcmi_endian.h" />

+ 7 - 1
lib/mapObjects/CArmedInstance.cpp

@@ -86,11 +86,13 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 	{
 		b->val = +1;
 		b->description = VLC->generaltexth->arraytxt[115]; //All troops of one alignment +1
+		b->description = b->description.substr(0, b->description.size()-3);//trim "+1"
 	}
 	else if (!factions.empty()) // no bonus from empty garrison
 	{
 	 	b->val = 2 - factionsInArmy;
 		b->description = boost::str(boost::format(VLC->generaltexth->arraytxt[114]) % factionsInArmy % b->val); //Troops of %d alignments %d
+		b->description = b->description.substr(0, b->description.size()-2);//trim value
 	}
 	boost::algorithm::trim(b->description);
 
@@ -100,7 +102,11 @@ void CArmedInstance::updateMoraleBonusFromArmy()
  	if(hasUndead)
 	{
 		if(!undeadModifier)
-			addNewBonus(new Bonus(Bonus::PERMANENT, Bonus::MORALE, Bonus::ARMY, -1, UNDEAD_MODIFIER_ID, VLC->generaltexth->arraytxt[116]));
+		{
+			undeadModifier = new Bonus(Bonus::PERMANENT, Bonus::MORALE, Bonus::ARMY, -1, UNDEAD_MODIFIER_ID, VLC->generaltexth->arraytxt[116]);
+			undeadModifier->description = undeadModifier->description.substr(0, undeadModifier->description.size()-2);//trim value
+			addNewBonus(undeadModifier);
+		}			
 	}
 	else if(undeadModifier)
 		removeBonus(undeadModifier);

+ 64 - 5
lib/mapObjects/CGHeroInstance.cpp

@@ -21,6 +21,7 @@
 #include "../IGameCallback.h"
 #include "../CGameState.h"
 #include "../CCreatureHandler.h"
+#include "../BattleState.h"
 
 ///helpers
 static void showInfoDialog(const PlayerColor playerID, const ui32 txtID, const ui16 soundID)
@@ -216,9 +217,10 @@ CGHeroInstance::CGHeroInstance()
 	setNodeType(HERO);
 	ID = Obj::HERO;
 	tacticFormationEnabled = inTownGarrison = false;
-	mana = movement = portrait = level = -1;
+	mana = movement = portrait = -1;
 	isStanding = true;
 	moveDir = 4;
+	level = 1;
 	exp = 0xffffffff;
 	visitedTown = nullptr;
 	type = nullptr;
@@ -290,7 +292,6 @@ void CGHeroInstance::initHero()
 	}
 	assert(validTypes());
 
-	level = 1;
 	if(exp == 0xffffffff)
 	{
 		initExp();
@@ -877,15 +878,73 @@ ui8 CGHeroInstance::getSpellSchoolLevel(const CSpell * spell, int *outSelectedSc
 	
 	vstd::amax(skill, valOfBonuses(Bonus::MAGIC_SCHOOL_SKILL, 0)); //any school bonus
 	vstd::amax(skill, valOfBonuses(Bonus::SPELL, spell->id.toEnum())); //given by artifact or other effect
-	if (hasBonusOfType(Bonus::MAXED_SPELL, spell->id))//hero specialty (Daremyth, Melodia)
-		skill = 3;
+
 	assert(skill >= 0 && skill <= 3);
 	return skill;
 }
 
+ui32 CGHeroInstance::getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const
+{
+	//applying sorcery secondary skill
+
+	base *= (100.0 + valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SORCERY)) / 100.0;
+	base *= (100.0 + valOfBonuses(Bonus::SPELL_DAMAGE) + valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, spell->id.toEnum())) / 100.0;
+
+	spell->forEachSchool([&base, this](const SpellSchoolInfo & cnf, bool & stop)
+	{
+		base *= (100.0 + valOfBonuses(cnf.damagePremyBonus)) / 100.0;
+		stop = true; //only bonus from one school is used
+	});
+
+	if (affectedStack && affectedStack->getCreature()->level) //Hero specials like Solmyr, Deemer
+		base *= (100. + ((valOfBonuses(Bonus::SPECIAL_SPELL_LEV,  spell->id.toEnum()) * level) / affectedStack->getCreature()->level)) / 100.0;
+
+	return base;	
+}
+
+
 bool CGHeroInstance::canCastThisSpell(const CSpell * spell) const
 {
-	return spell->isCastableBy(this, nullptr !=getArt(ArtifactPosition::SPELLBOOK), spells);
+	if(nullptr == getArt(ArtifactPosition::SPELLBOOK))
+		return false;
+
+	const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->id);
+
+	const bool inSpellBook = vstd::contains(spells, spell->id);
+	const bool specificBonus = hasBonusOfType(Bonus::SPELL, spell->id);
+
+	bool schoolBonus = false;
+
+	spell->forEachSchool([this, &schoolBonus](const SpellSchoolInfo & cnf, bool & stop)
+	{
+		if(hasBonusOfType(cnf.knoledgeBonus))
+		{
+			schoolBonus = stop = true;
+		}
+	});
+
+	const bool levelBonus = hasBonusOfType(Bonus::SPELLS_OF_LEVEL, spell->level);
+
+    if (spell->isSpecialSpell())
+    {
+        if (inSpellBook)
+        {//hero has this spell in spellbook
+            logGlobal->errorStream() << "Special spell " << spell->name << "in spellbook.";
+        }
+        return specificBonus;
+    }
+    else if(!isAllowed)
+    {
+        if (inSpellBook)
+        {//hero has this spell in spellbook
+            logGlobal->errorStream() << "Banned spell " << spell->name << " in spellbook.";
+        }
+        return specificBonus;
+    }
+    else
+    {
+		return inSpellBook || schoolBonus || specificBonus || levelBonus;
+    }
 }
 
 /**

+ 11 - 6
lib/mapObjects/CGHeroInstance.h

@@ -2,6 +2,7 @@
 
 #include "CObjectHandler.h"
 #include "CArmedInstance.h"
+#include "../spells/Magic.h"
 
 #include "../CArtHandler.h" // For CArtifactSet
 #include "../CRandomGenerator.h"
@@ -35,7 +36,7 @@ public:
 };
 
 
-class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet
+class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public ISpellCaster
 {
 public:
 	enum ECanDig
@@ -162,14 +163,13 @@ public:
 	int maxMovePoints(bool onLand) const;
 	int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false) const;
 
-	//int getSpellSecLevel(int spell) const; //returns level of secondary ability (fire, water, earth, air magic) known to this hero and applicable to given spell; -1 if error
 	static int3 convertPosition(int3 src, bool toh3m); //toh3m=true: manifest->h3m; toh3m=false: h3m->manifest
 	double getFightingStrength() const; // takes attack / defense skill into account
 	double getMagicStrength() const; // takes knowledge / spell power skill into account
 	double getHeroStrength() const; // includes fighting and magic strength
 	ui64 getTotalStrength() const; // includes fighting strength and army strength
 	TExpType calculateXp(TExpType exp) const; //apply learning skill
-	ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const; //returns level on which given spell would be cast by this hero (0 - none, 1 - basic etc); optionally returns number of selected school by arg - 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic,
+	
 	bool canCastThisSpell(const CSpell * spell) const; //determines if this hero can cast given spell; takes into account existing spell in spellbook, existing spellbook and artifact bonuses
 	CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const;
 	void showNecromancyDialog(const CStackBasicDescriptor &raisedStack) const;
@@ -198,13 +198,18 @@ public:
 
 	CGHeroInstance();
 	virtual ~CGHeroInstance();
-	//////////////////////////////////////////////////////////////////////////
-	//
+	
+	///ArtBearer
 	ArtBearer::ArtBearer bearerType() const override;
-	//////////////////////////////////////////////////////////////////////////
 
+	///IBonusBearer
 	CBonusSystemNode *whereShouldBeAttached(CGameState *gs) override;
 	std::string nodeName() const override;
+	
+	///ISpellCaster
+	ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const override;
+	ui32 getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const override;
+	
 	void deserializationFix();
 
 	void initObj() override;

+ 8 - 2
lib/mapObjects/CGTownInstance.cpp

@@ -436,12 +436,12 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 	//other *-of-legion-like bonuses (%d to growth cumulative with grail)
 	TBonusListPtr bonuses = getBonuses(Selector::type(Bonus::CREATURE_GROWTH).And(Selector::subtype(level)));
 	for(const Bonus *b : *bonuses)
-		ret.entries.push_back(GrowthInfo::Entry(b->Description() + " %+d", b->val));
+		ret.entries.push_back(GrowthInfo::Entry(b->val, b->Description()));
 
 	//statue-of-legion-like bonus: % to base+castle
 	TBonusListPtr bonuses2 = getBonuses(Selector::type(Bonus::CREATURE_GROWTH_PERCENT));
 	for(const Bonus *b : *bonuses2)
-		ret.entries.push_back(GrowthInfo::Entry(b->Description() + " %+d", b->val * (base + castleBonus) / 100));
+		ret.entries.push_back(GrowthInfo::Entry(b->val * (base + castleBonus) / 100, b->Description()));
 
 	if(hasBuilt(BuildingID::GRAIL)) //grail - +50% to ALL (so far added) growth
 		ret.entries.push_back(GrowthInfo::Entry(subID, BuildingID::GRAIL, ret.totalGrowth() / 2));
@@ -1244,6 +1244,12 @@ GrowthInfo::Entry::Entry(int subID, BuildingID building, int _count)
 	description = boost::str(boost::format("%s %+d") % VLC->townh->factions[subID]->town->buildings.at(building)->Name() % count);
 }
 
+GrowthInfo::Entry::Entry(int _count, const std::string &fullDescription)
+	: count(_count)
+{
+	description = fullDescription;
+}
+
 CTownAndVisitingHero::CTownAndVisitingHero()
 {
 	setNodeType(TOWN_AND_VISITOR);

+ 1 - 0
lib/mapObjects/CGTownInstance.h

@@ -130,6 +130,7 @@ struct DLL_LINKAGE GrowthInfo
 		std::string description;
 		Entry(const std::string &format, int _count);
 		Entry(int subID, BuildingID building, int _count);
+		Entry(int _count, const std::string &fullDescription);
 	};
 
 	std::vector<Entry> entries;

+ 19 - 22
lib/spells/BattleSpellMechanics.cpp

@@ -93,6 +93,12 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, Battle
 	ssp.val = 0;
 	ssp.absolute = 1;
 	env->sendAndApply(&ssp);
+	
+	ssp.stackID = clonedStack->ID;
+	ssp.which = BattleSetStackProperty::HAS_CLONE;
+	ssp.val = bsa.newStackID;
+	ssp.absolute = 1;
+	env->sendAndApply(&ssp);	
 }
 
 ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const CGHeroInstance * caster, const CStack * obj) const
@@ -100,6 +106,8 @@ ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const CGHer
 	//can't clone already cloned creature
 	if(vstd::contains(obj->state, EBattleStackState::CLONED))
 		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
+	if(obj->cloneID != -1)
+		return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 	//TODO: how about stacks casting Clone?
 	//currently Clone casted by stack is assumed Expert level
 	ui8 schoolLevel;
@@ -127,26 +135,15 @@ ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const CGHer
 void CureMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const
 {
 	DefaultSpellMechanics::applyBattle(battle, packet);
-
-	for(auto stackID : packet->affectedCres)
+	doDispell(battle, packet, [](const Bonus * b) -> bool
 	{
-		if(vstd::contains(packet->resisted, stackID))
+		if(b->source == Bonus::SPELL_EFFECT)
 		{
-			logGlobal->errorStream() << "Resistance to positive spell CURE";
-			continue;
+			CSpell * sp = SpellID(b->sid).toSpell();
+			return sp->isNegative();
 		}
-
-		CStack *s = battle->getStack(stackID);
-		s->popBonuses([&](const Bonus *b) -> bool
-		{
-			if(b->source == Bonus::SPELL_EFFECT)
-			{
-				CSpell * sp = SpellID(b->sid).toSpell();
-				return sp->isNegative();
-			}
-			return false; //not a spell effect
-		});
-	}
+		return false; //not a spell effect		
+	});
 }
 
 ///DispellMechanics
@@ -310,8 +307,8 @@ ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const C
 		//TODO: what with other creatures casting hypnotize, Faerie Dragons style?
 		ui64 subjectHealth = (obj->count - 1) * obj->MaxHealth() + obj->firstHPleft;
 		//apply 'damage' bonus for hypnotize, including hero specialty
-		ui64 maxHealth = owner->calculateBonus(caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER)
-			* owner->power + owner->getPower(caster->getSpellSchoolLevel(owner)), caster, obj);
+		ui64 maxHealth = caster->getSpellBonus(owner, caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER)
+			* owner->power + owner->getPower(caster->getSpellSchoolLevel(owner)), obj);
 		if (subjectHealth > maxHealth)
 			return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 	}
@@ -470,7 +467,8 @@ ESpellCastProblem::ESpellCastProblem SacrificeMechanics::canBeCasted(const CBatt
 		//using isImmuneBy directly as this mechanics does not have overridden immunity check
 		//therefore we do not need to check caster and casting mode
 		//TODO: check that we really should check immunity for both stacks
-		const bool immune =  ESpellCastProblem::OK != owner->isImmuneBy(stack);
+		ESpellCastProblem::ESpellCastProblem res = owner->isImmuneBy(stack);
+		const bool immune =  ESpellCastProblem::OK != res && ESpellCastProblem::NOT_DECIDED != res;
 		const bool casterStack = stack->owner == player;
 
 		if(!immune && casterStack)
@@ -482,7 +480,6 @@ ESpellCastProblem::ESpellCastProblem SacrificeMechanics::canBeCasted(const CBatt
 			if(targetExists && targetToSacrificeExists)
 				break;
 		}
-
 	}
 
 	if(targetExists && targetToSacrificeExists)
@@ -571,7 +568,7 @@ void SummonMechanics::applyBattleEffects(const SpellCastEnvironment * env, Battl
 	bsa.pos = parameters.cb->getAvaliableHex(creatureToSummon, !(bool)parameters.casterSide); //TODO: unify it
 
 	//TODO stack casting -> probably power will be zero; set the proper number of creatures manually
-	int percentBonus = parameters.caster ? parameters.caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, owner->id.toEnum()) : 0;
+	int percentBonus = parameters.casterHero ? parameters.casterHero->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, owner->id.toEnum()) : 0;
 
 	bsa.amount = parameters.usedSpellPower
 		* owner->getPower(parameters.spellLvl)

+ 213 - 84
lib/spells/CDefaultSpellMechanics.cpp

@@ -15,6 +15,8 @@
 #include "../NetPacks.h"
 #include "../BattleState.h"
 
+#include "../CGeneralTextHandler.h"
+
 namespace SRSLPraserHelpers
 {
 	static int XYToHex(int x, int y)
@@ -120,7 +122,7 @@ namespace SRSLPraserHelpers
 ///DefaultSpellMechanics
 void DefaultSpellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const
 {
-	if (packet->castedByHero)
+	if (packet->castByHero)
 	{
 		if (packet->side < 2)
 		{
@@ -131,9 +133,6 @@ void DefaultSpellMechanics::applyBattle(BattleInfo * battle, const BattleSpellCa
 	//handle countering spells
 	for(auto stackID : packet->affectedCres)
 	{
-		if(vstd::contains(packet->resisted, stackID))
-			continue;
-
 		CStack * s = battle->getStack(stackID);
 		s->popBonuses([&](const Bonus * b) -> bool
 		{
@@ -230,16 +229,16 @@ void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleS
 	sc.skill = parameters.spellLvl;
 	sc.tile = parameters.destination;
 	sc.dmgToDisplay = 0;
-	sc.castedByHero = nullptr != parameters.caster;
+	sc.castByHero = nullptr != parameters.casterHero;
 	sc.casterStack = (parameters.casterStack ? parameters.casterStack->ID : -1);
 	sc.manaGained = 0;
 
 	int spellCost = 0;
 
 	//calculate spell cost
-	if(parameters.caster)
+	if(parameters.casterHero)
 	{
-		spellCost = parameters.cb->battleGetSpellCost(owner, parameters.caster);
+		spellCost = parameters.cb->battleGetSpellCost(owner, parameters.casterHero);
 
 		if(parameters.secHero && parameters.mode == ECastingMode::HERO_CASTING) //handle mana channel
 		{
@@ -259,27 +258,60 @@ void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleS
 	//must be vector, as in Chain Lightning order matters
 	std::vector<const CStack*> attackedCres; //CStack vector is somewhat more suitable than ID vector
 
-	auto creatures = owner->getAffectedStacks(parameters.cb, parameters.mode, parameters.casterColor, parameters.spellLvl, parameters.destination, parameters.caster);
+	auto creatures = owner->getAffectedStacks(parameters.cb, parameters.mode, parameters.casterColor, parameters.spellLvl, parameters.destination, parameters.casterHero);
 	std::copy(creatures.begin(), creatures.end(), std::back_inserter(attackedCres));
 
-	for (auto cre : attackedCres)
-	{
-		sc.affectedCres.insert(cre->ID);
-	}
-
+	std::vector <const CStack*> reflected;//for magic mirror
+	
 	//checking if creatures resist
-	//resistance is applied only to negative spells
+	//resistance/reflection is applied only to negative spells
 	if(owner->isNegative())
 	{
+		//it is actual spell and can be reflected to single target, no recurrence
+		const bool tryMagicMirror = parameters.mode != ECastingMode::MAGIC_MIRROR && owner->level && owner->getLevelInfo(0).range == "0";
+		std::vector <const CStack*> resisted;
 		for(auto s : attackedCres)
 		{
+			//magic resistance
 			const int prob = std::min((s)->magicResistance(), 100); //probability of resistance in %
 
 			if(env->getRandomGenerator().nextInt(99) < prob)
 			{
-				sc.resisted.push_back(s->ID);
+				resisted.push_back(s);
 			}
+			//magic mirror
+			if(tryMagicMirror)
+			{
+				const int mirrorChance = (s)->valOfBonuses(Bonus::MAGIC_MIRROR);
+				if(env->getRandomGenerator().nextInt(99) < mirrorChance)
+					reflected.push_back(s);
+			}
+		}
+
+		vstd::erase_if(attackedCres, [&resisted, reflected](const CStack * s)
+		{
+			return vstd::contains(resisted, s) || vstd::contains(reflected, s);
+		});
+
+		for(auto s : resisted)
+		{
+			BattleSpellCast::CustomEffect effect;
+			effect.effect = 78;
+			effect.stack = s->ID;
+			sc.customEffects.push_back(effect);
 		}
+		for(auto s : reflected)
+		{
+			BattleSpellCast::CustomEffect effect;
+			effect.effect = 3;
+			effect.stack = s->ID;
+			sc.customEffects.push_back(effect);
+		}		
+	}
+
+	for(auto cre : attackedCres)
+	{
+		sc.affectedCres.insert(cre->ID);
 	}
 
 	StacksInjured si;
@@ -290,12 +322,12 @@ void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleS
 	env->sendAndApply(&sc);
 
 	//spend mana
-	if(parameters.caster)
+	if(parameters.casterHero)
 	{
 		SetMana sm;
 		sm.absolute = false;
 
-		sm.hid = parameters.caster->id;
+		sm.hid = parameters.casterHero->id;
 		sm.val = -spellCost;
 
 		env->sendAndApply(&sm);
@@ -328,61 +360,143 @@ void DefaultSpellMechanics::battleCast(const SpellCastEnvironment * env, BattleS
 	}
 
 	//Magic Mirror effect
-	if(owner->isNegative() && parameters.mode != ECastingMode::MAGIC_MIRROR && owner->level && owner->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence
+	for(auto & attackedCre : reflected)
 	{
-		for(auto & attackedCre : attackedCres)
+		TStacks mirrorTargets = parameters.cb->battleGetStacksIf([this, parameters](const CStack * battleStack)
 		{
-			int mirrorChance = (attackedCre)->valOfBonuses(Bonus::MAGIC_MIRROR);
-			if(mirrorChance > env->getRandomGenerator().nextInt(99))
+			//Get all enemy stacks. Magic mirror can reflect to immune creature (with no effect)
+			return battleStack->owner == parameters.casterColor;
+		},
+		true);//turrets included
+
+		if(!mirrorTargets.empty())
+		{
+			int targetHex = (*RandomGeneratorUtil::nextItem(mirrorTargets, env->getRandomGenerator()))->position;
+
+			BattleSpellCastParameters mirrorParameters = parameters;
+			mirrorParameters.spellLvl = 0;
+			mirrorParameters.casterSide = 1-parameters.casterSide;
+			mirrorParameters.casterColor = (attackedCre)->owner;
+			mirrorParameters.casterHero = nullptr;
+			mirrorParameters.destination = targetHex;
+			mirrorParameters.secHero = parameters.casterHero;
+			mirrorParameters.mode = ECastingMode::MAGIC_MIRROR;
+			mirrorParameters.casterStack = (attackedCre);
+			mirrorParameters.selectedStack = nullptr;
+
+			battleCast(env, mirrorParameters);
+		}
+	}
+}
+
+void DefaultSpellMechanics::battleLogSingleTarget(std::vector<std::string> & logLines, const BattleSpellCast * packet, 
+	const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const
+{
+	const std::string attackedName = attackedStack->getName();
+	const std::string attackedNameSing = attackedStack->getCreature()->nameSing;
+	const std::string attackedNamePl = attackedStack->getCreature()->namePl;
+	
+	auto getPluralFormat = [attackedStack](const int baseTextID) -> boost::format
+	{
+		return boost::format(VLC->generaltexth->allTexts[(attackedStack->count > 1 ? baseTextID + 1 : baseTextID)]);
+	};
+
+	auto logSimple = [&logLines, getPluralFormat, attackedName](const int baseTextID)
+	{
+		boost::format fmt = getPluralFormat(baseTextID);
+		fmt % attackedName;
+		logLines.push_back(fmt.str());
+	};
+
+	auto logPlural = [&logLines, attackedNamePl](const int baseTextID)
+	{
+		boost::format fmt(VLC->generaltexth->allTexts[baseTextID]);
+		fmt % attackedNamePl;
+		logLines.push_back(fmt.str());
+	};
+
+	displayDamage = false; //in most following cases damage info text is custom
+	switch(owner->id)
+	{
+	case SpellID::STONE_GAZE:
+		logSimple(558);
+		break;
+	case SpellID::POISON:
+		logSimple(561);
+		break;
+	case SpellID::BIND:
+		logPlural(560);//Roots and vines bind the %s to the ground!
+		break;
+	case SpellID::DISEASE:
+		logSimple(553);
+		break;
+	case SpellID::PARALYZE:
+		logSimple(563);
+		break;
+	case SpellID::AGE:
+		{
+			boost::format text = getPluralFormat(551);
+			text % attackedName;
+			//The %s shrivel with age, and lose %d hit points."
+			TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH));
+			const int fullHP = bl->totalValue();
+			bl->remove_if(Selector::source(Bonus::SPELL_EFFECT, SpellID::AGE));
+			text % (fullHP - bl->totalValue());
+			logLines.push_back(text.str());
+		}
+		break;
+	case SpellID::THUNDERBOLT:
+		{
+			logPlural(367);
+			std::string text = VLC->generaltexth->allTexts[343].substr(1, VLC->generaltexth->allTexts[343].size() - 1); //Does %d points of damage.
+			boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(packet->dmgToDisplay)); //no more text afterwards
+			logLines.push_back(text);
+		}
+		break;
+	case SpellID::DISPEL_HELPFUL_SPELLS:
+		logPlural(555);
+		break;
+	case SpellID::DEATH_STARE:
+		if (packet->dmgToDisplay > 0)
+		{
+			std::string text;
+			if (packet->dmgToDisplay > 1)
 			{
-				std::vector<const CStack *> mirrorTargets;
-				auto battleStacks = parameters.cb->battleGetAllStacks(true);
-				for(auto & battleStack : battleStacks)
-				{
-					if(battleStack->owner == parameters.casterColor) //get enemy stacks which can be affected by this spell
-					{
-						if (ESpellCastProblem::OK == owner->isImmuneByStack(nullptr, battleStack))
-							mirrorTargets.push_back(battleStack);
-					}
-				}
-				if(!mirrorTargets.empty())
-				{
-					int targetHex = (*RandomGeneratorUtil::nextItem(mirrorTargets, env->getRandomGenerator()))->position;
-
-					BattleSpellCastParameters mirrorParameters = parameters;
-					mirrorParameters.spellLvl = 0;
-					mirrorParameters.casterSide = 1-parameters.casterSide;
-					mirrorParameters.casterColor = (attackedCre)->owner;
-					mirrorParameters.caster = nullptr;
-					mirrorParameters.destination = targetHex;
-					mirrorParameters.secHero = parameters.caster;
-					mirrorParameters.mode = ECastingMode::MAGIC_MIRROR;
-					mirrorParameters.casterStack = (attackedCre);
-					mirrorParameters.selectedStack = nullptr;
-
-					battleCast(env, mirrorParameters);
-				}
+				text = VLC->generaltexth->allTexts[119]; //%d %s die under the terrible gaze of the %s.
+				boost::algorithm::replace_first(text, "%d", boost::lexical_cast<std::string>(packet->dmgToDisplay));
+				boost::algorithm::replace_first(text, "%s", attackedNamePl);
+			}
+			else
+			{
+				text = VLC->generaltexth->allTexts[118]; //One %s dies under the terrible gaze of the %s.
+				boost::algorithm::replace_first(text, "%s", attackedNameSing);
 			}
+			boost::algorithm::replace_first(text, "%s", casterName); //casting stack
+			logLines.push_back(text);
 		}
-	}
+		break;
+	default:
+		{
+			boost::format text(VLC->generaltexth->allTexts[565]); //The %s casts %s
+			text % casterName % owner->name;
+			displayDamage = true;
+			logLines.push_back(text.str());
+		}
+		break;
+	}	
 }
 
 int DefaultSpellMechanics::calculateDuration(const CGHeroInstance * caster, int usedSpellPower) const
 {
-	if(!caster)
+	if(caster == nullptr)
 	{
 		if (!usedSpellPower)
 			return 3; //default duration of all creature spells
 		else
-			return usedSpellPower; //use creature spell power
+			return usedSpellPower; //use creature spell power			
 	}
-	switch(owner->id)
-	{
-	case SpellID::FRENZY:
-		return 1;
-	default: //other spells
+	else
 		return caster->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + caster->valOfBonuses(Bonus::SPELL_DURATION);
-	}
 }
 
 ui32 DefaultSpellMechanics::calculateHealedHP(const CGHeroInstance* caster, const CStack* stack, const CStack* sacrificedStack) const
@@ -402,13 +516,21 @@ ui32 DefaultSpellMechanics::calculateHealedHP(const CGHeroInstance* caster, cons
 		healedHealth = (spellPowerSkill + sacrificedStack->MaxHealth() + levelPower) * sacrificedStack->count;
 	else
 		healedHealth = spellPowerSkill * owner->power + levelPower; //???
-	healedHealth = owner->calculateBonus(healedHealth, caster, stack);
+	healedHealth = caster->getSpellBonus(owner, healedHealth, stack);
 	return std::min<ui32>(healedHealth, stack->MaxHealth() - stack->firstHPleft + (owner->isRisingSpell() ? stack->baseAmount * stack->MaxHealth() : 0));
 }
 
 
 void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
 {
+	int effectLevel = parameters.spellLvl;
+	{
+		//MAXED_SPELL bonus.
+		if(parameters.casterHero != nullptr)
+			if(parameters.casterHero->hasBonusOfType(Bonus::MAXED_SPELL, owner->id))
+				effectLevel = 3;
+	}
+
 	//applying effects
 	if(owner->isOffensiveSpell())
 	{
@@ -427,14 +549,11 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
 		int chainLightningModifier = 0;
 		for(auto & attackedCre : ctx.attackedCres)
 		{
-			if(vstd::contains(ctx.sc.resisted, (attackedCre)->ID)) //this creature resisted the spell
-				continue;
-
 			BattleStackAttacked bsa;
 			if(spellDamage)
 				bsa.damageAmount = spellDamage >> chainLightningModifier;
 			else
-				bsa.damageAmount =  owner->calculateDamage(parameters.caster, attackedCre, parameters.spellLvl, parameters.usedSpellPower) >> chainLightningModifier;
+				bsa.damageAmount =  owner->calculateDamage(parameters.casterHero, attackedCre, effectLevel, parameters.usedSpellPower) >> chainLightningModifier;
 
 			ctx.sc.dmgToDisplay += bsa.damageAmount;
 
@@ -459,29 +578,42 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
 			stackSpellPower =  parameters.casterStack->valOfBonuses(Bonus::CREATURE_ENCHANT_POWER);
 		}
 		SetStackEffect sse;
-		Bonus pseudoBonus;
-		pseudoBonus.sid = owner->id;
-		pseudoBonus.val = parameters.spellLvl;
-		pseudoBonus.turnsRemain = calculateDuration(parameters.caster, stackSpellPower ? stackSpellPower : parameters.usedSpellPower);
-		CStack::stackEffectToFeature(sse.effect, pseudoBonus);
+		//get default spell duration (spell power with bonuses for heroes)
+		int duration = calculateDuration(parameters.casterHero, stackSpellPower ? stackSpellPower : parameters.usedSpellPower);		
+		//generate actual stack bonuses
+		{
+			int maxDuration = 0;
+			std::vector<Bonus> tmp;
+			owner->getEffects(tmp, effectLevel);
+			for(Bonus& b : tmp)
+			{
+				//use configured duration if present
+				if(b.turnsRemain == 0)
+					b.turnsRemain = duration;
+				vstd::amax(maxDuration, b.turnsRemain);
+				sse.effect.push_back(b);
+			}
+			//if all spell effects have special duration, use it 
+			duration = maxDuration;			
+		}
+		//fix to original config: shield should display damage reduction
 		if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD)
 		{
-			sse.effect.back().val = (100 - sse.effect.back().val); //fix to original config: shield should display damage reduction
+			sse.effect.back().val = (100 - sse.effect.back().val); 
 		}
-		if(owner->id == SpellID::BIND &&  parameters.casterStack)//bind
+		//we need to know who cast Bind
+		if(owner->id == SpellID::BIND && parameters.casterStack)
 		{
-			sse.effect.back().additionalInfo =  parameters.casterStack->ID; //we need to know who casted Bind
+			sse.effect.back().additionalInfo =  parameters.casterStack->ID;
 		}
 		const Bonus * bonus = nullptr;
-		if(parameters.caster)
-			bonus = parameters.caster->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id));
+		if(parameters.casterHero)
+			bonus = parameters.casterHero->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id));
 		//TODO does hero specialty should affects his stack casting spells?
 
 		si32 power = 0;
 		for(const CStack * affected : ctx.attackedCres)
 		{
-			if(vstd::contains(ctx.sc.resisted, affected->ID)) //this creature resisted the spell
-				continue;
 			sse.stacks.push_back(affected->ID);
 
 			//Apply hero specials - peculiar enchants
@@ -512,17 +644,17 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
 					case 1: //only Coronius as yet
 					{
 						power = std::max(5 - tier, 0);
-						Bonus specialBonus = CStack::featureGenerator(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK, power, pseudoBonus.turnsRemain);
+						Bonus specialBonus = CStack::featureGenerator(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK, power, duration);
 						specialBonus.sid = owner->id;
 						sse.uniqueBonuses.push_back(std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional attack to Slayer effect
 					}
 					break;
 				}
 			}
-			if (parameters.caster && parameters.caster->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
+			if (parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
 			{
-				int damagePercent = parameters.caster->level * parameters.caster->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
-				Bonus specialBonus = CStack::featureGenerator(Bonus::CREATURE_DAMAGE, 0, damagePercent, pseudoBonus.turnsRemain);
+				int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
+				Bonus specialBonus = CStack::featureGenerator(Bonus::CREATURE_DAMAGE, 0, damagePercent, duration);
 				specialBonus.valType = Bonus::PERCENT_TO_ALL;
 				specialBonus.sid = owner->id;
 				sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus));
@@ -563,13 +695,13 @@ void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env,
 				else
 				{
 					//any typical spell (commander's cure or animate dead)
-					int healedHealth = parameters.usedSpellPower * owner->power + owner->getPower(parameters.spellLvl);
+					int healedHealth = parameters.usedSpellPower * owner->power + owner->getPower(effectLevel);
 					hi.healedHP = std::min<ui32>(healedHealth, attackedCre->MaxHealth() - attackedCre->firstHPleft + (resurrect ? attackedCre->baseAmount * attackedCre->MaxHealth() : 0));
 				}
 			}
 			else
-				hi.healedHP = calculateHealedHP(parameters.caster, attackedCre, parameters.selectedStack); //Casted by hero
-			hi.lowLevelResurrection = (parameters.spellLvl <= 1) && (owner->id != SpellID::ANIMATE_DEAD);
+				hi.healedHP = calculateHealedHP(parameters.casterHero, attackedCre, parameters.selectedStack); //Casted by hero
+			hi.lowLevelResurrection = (effectLevel <= 1) && (owner->id != SpellID::ANIMATE_DEAD);
 			shr.healedStacks.push_back(hi);
 		}
 		if(!shr.healedStacks.empty())
@@ -734,9 +866,6 @@ void DefaultSpellMechanics::doDispell(BattleInfo * battle, const BattleSpellCast
 {
 	for(auto stackID : packet->affectedCres)
 	{
-		if(vstd::contains(packet->resisted, stackID))
-			continue;
-
 		CStack *s = battle->getStack(stackID);
 		s->popBonuses(selector);
 	}	

+ 2 - 0
lib/spells/CDefaultSpellMechanics.h

@@ -48,6 +48,8 @@ public:
 	bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const override final;
 	void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const override;
 
+	void battleLogSingleTarget(std::vector<std::string> & logLines, const BattleSpellCast * packet, 
+		const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const override;	
 protected:
 	virtual void applyBattleEffects(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
 

+ 72 - 58
lib/spells/CSpellHandler.cpp

@@ -26,6 +26,7 @@
 #include "../mapObjects/CGHeroInstance.h"
 #include "../BattleState.h"
 #include "../CBattleCallback.h"
+#include "../CGameState.h"
 
 #include "ISpellMechanics.h"
 
@@ -71,7 +72,7 @@ namespace SpellConfig
 }
 
 BattleSpellCastParameters::BattleSpellCastParameters(const BattleInfo* cb)
-	: spellLvl(0), destination(BattleHex::INVALID), casterSide(0),casterColor(PlayerColor::CANNOT_DETERMINE),caster(nullptr), secHero(nullptr),
+	: spellLvl(0), destination(BattleHex::INVALID), casterSide(0),casterColor(PlayerColor::CANNOT_DETERMINE),casterHero(nullptr), secHero(nullptr),
 	usedSpellPower(0),mode(ECastingMode::HERO_CASTING), casterStack(nullptr), selectedStack(nullptr), cb(cb)
 {
 
@@ -126,38 +127,6 @@ void CSpell::battleCast(const SpellCastEnvironment * env, BattleSpellCastParamet
 	mechanics->battleCast(env, parameters);
 }
 
-bool CSpell::isCastableBy(const IBonusBearer * caster, bool hasSpellBook, const std::set<SpellID> & spellBook) const
-{
-	if(!hasSpellBook)
-		return false;
-
-	const bool inSpellBook = vstd::contains(spellBook, id);
-	const bool isBonus = caster->hasBonusOfType(Bonus::SPELL, id);
-
-	bool inTome = false;
-
-	forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop)
-	{
-		if(caster->hasBonusOfType(cnf.knoledgeBonus))
-		{
-			inTome = stop = true;
-		}
-	});
-
-    if (isSpecialSpell())
-    {
-        if (inSpellBook)
-        {//hero has this spell in spellbook
-            logGlobal->errorStream() << "Special spell in spellbook "<<name;
-        }
-        return isBonus;
-    }
-    else
-    {
-       return inSpellBook || inTome || isBonus || caster->hasBonusOfType(Bonus::SPELLS_OF_LEVEL, level);
-    }
-}
-
 const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const
 {
 	if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
@@ -169,29 +138,7 @@ const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const
 	return levels.at(level);
 }
 
-ui32 CSpell::calculateBonus(ui32 baseDamage, const CGHeroInstance * caster, const CStack * affectedCreature) const
-{
-	ui32 ret = baseDamage;
-
-	//applying sorcery secondary skill
-	if(caster)
-	{
-		ret *= (100.0 + caster->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SORCERY)) / 100.0;
-		ret *= (100.0 + caster->valOfBonuses(Bonus::SPELL_DAMAGE) + caster->valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, id.toEnum())) / 100.0;
-
-		forEachSchool([&](const SpellSchoolInfo & cnf, bool & stop)
-		{
-			ret *= (100.0 + caster->valOfBonuses(cnf.damagePremyBonus)) / 100.0;
-			stop = true; //only bonus from one school is used
-		});
-
-		if (affectedCreature && affectedCreature->getCreature()->level) //Hero specials like Solmyr, Deemer
-			ret *= (100. + ((caster->valOfBonuses(Bonus::SPECIAL_SPELL_LEV, id.toEnum()) * caster->level) / affectedCreature->getCreature()->level)) / 100.0;
-	}
-	return ret;
-}
-
-ui32 CSpell::calculateDamage(const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const
+ui32 CSpell::calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const
 {
 	ui32 ret = 0; //value to return
 
@@ -230,7 +177,9 @@ ui32 CSpell::calculateDamage(const CGHeroInstance * caster, const CStack * affec
 			ret /= 100;
 		}
 	}
-	ret = calculateBonus(ret, caster, affectedCreature);
+	
+	if(nullptr != caster) //todo: make sure that caster always present	
+		ret = caster->getSpellBonus(this, ret, affectedCreature);
 	return ret;
 }
 
@@ -551,6 +500,59 @@ ESpellCastProblem::ESpellCastProblem CSpell::isImmuneByStack(const CGHeroInstanc
 	return ESpellCastProblem::OK;
 }
 
+void CSpell::prepareBattleLog(const CBattleInfoCallback * cb,  const BattleSpellCast * packet, std::vector<std::string> & logLines) const
+{
+	bool displayDamage = true;
+	
+	std::string casterName("Something"); //todo: localize
+	
+	if(packet->castByHero)
+		casterName = cb->battleGetHeroInfo(packet->side).name;
+
+	{
+		const auto casterStackID = packet->casterStack;
+
+		if(casterStackID > 0)
+		{
+			const CStack * casterStack = cb->battleGetStackByID(casterStackID);
+			if(casterStack != nullptr)
+				casterName = casterStack->type->namePl;
+		}
+	}
+	
+	if(packet->affectedCres.size() == 1)
+	{
+		const CStack * attackedStack = cb->battleGetStackByID(*packet->affectedCres.begin(), false);
+		
+		const std::string attackedNamePl = attackedStack->getCreature()->namePl;
+		
+		if(packet->castByHero)
+		{
+			const std::string fmt = VLC->generaltexth->allTexts[195];
+			logLines.push_back(boost::to_string(boost::format(fmt) % casterName % this->name % attackedNamePl));
+		}
+		else
+		{
+			mechanics->battleLogSingleTarget(logLines, packet, casterName, attackedStack, displayDamage);		
+		}
+	}
+	else
+	{
+		boost::format text(VLC->generaltexth->allTexts[196]);
+		text % casterName % this->name;
+		logLines.push_back(text.str());		
+	}
+	
+	
+	if(packet->dmgToDisplay > 0 && displayDamage)
+	{
+		boost::format dmgInfo(VLC->generaltexth->allTexts[376]);
+		dmgInfo % this->name % packet->dmgToDisplay;
+		logLines.push_back(dmgInfo.str());
+	}
+}
+
+
 void CSpell::setIsOffensive(const bool val)
 {
 	isOffensive = val;
@@ -588,6 +590,14 @@ void CSpell::setupMechanics()
 	mechanics = ISpellMechanics::createMechanics(this);
 }
 
+///CSpell::AnimationInfo
+CSpell::AnimationItem::AnimationItem()
+	:resourceName(""),verticalPosition(VerticalPosition::TOP),pause(0)
+{
+	
+}
+
+
 ///CSpell::AnimationInfo
 CSpell::AnimationInfo::AnimationInfo()
 {
@@ -924,7 +934,6 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode & json)
 		for(const JsonNode & item : queueNode)
 		{
 			CSpell::TAnimation newItem;
-			newItem.verticalPosition = VerticalPosition::TOP;
 
 			if(item.getType() == JsonNode::DATA_STRING)
 				newItem.resourceName = item.String();
@@ -936,6 +945,11 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode & json)
 				if("bottom" == vPosStr)
 					newItem.verticalPosition = VerticalPosition::BOTTOM;
 			}
+			else if(item.getType() == JsonNode::DATA_FLOAT)
+			{
+				newItem.pause = item.Float();
+			}
+			
 			q.push_back(newItem);
 		}
 	};

+ 19 - 8
lib/spells/CSpellHandler.h

@@ -9,7 +9,7 @@
  */
 
 #pragma once
-
+#include "Magic.h"
 #include "../IHandlerBase.h"
 #include "../ConstTransitivePtr.h"
 #include "../int3.h"
@@ -66,7 +66,7 @@ public:
 	BattleHex destination;
 	ui8 casterSide;
 	PlayerColor casterColor;
-	const CGHeroInstance * caster;
+	const CGHeroInstance * casterHero; //deprecated
 	const CGHeroInstance * secHero;
 	int usedSpellPower;
 	ECastingMode::ECastingMode mode;
@@ -104,10 +104,21 @@ public:
 	{
 		std::string resourceName;
 		VerticalPosition verticalPosition;
+		int pause; 
+
+		AnimationItem();
 
 		template <typename Handler> void serialize(Handler & h, const int version)
 		{
 			h & resourceName & verticalPosition;
+			if(version >= 754)
+			{
+				h & pause;
+			}			
+			else if(!h.saving)
+			{
+				pause = 0;
+			}
 		}
 	};
 
@@ -217,8 +228,6 @@ public:
 	CSpell();
 	~CSpell();
 
-	bool isCastableBy(const IBonusBearer * caster, bool hasSpellBook, const std::set<SpellID> & spellBook) const;
-
 	std::vector<BattleHex> rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool * outDroppedHexes = nullptr ) const; //convert range to specific hexes; last optional out parameter is set to true, if spell would cover unavailable hexes (that are not included in ret)
 	ETargetType getTargetType() const; //deprecated
 
@@ -248,11 +257,8 @@ public:
 	//internal, for use only by Mechanics classes
 	ESpellCastProblem::ESpellCastProblem isImmuneBy(const IBonusBearer *obj) const;
 
-	//internal, for use only by Mechanics classes. applying secondary skills
-	ui32 calculateBonus(ui32 baseDamage, const CGHeroInstance * caster, const CStack * affectedCreature) const;
-
 	///calculate spell damage on stack taking caster`s secondary skills and affectedCreature`s bonuses into account
-	ui32 calculateDamage(const CGHeroInstance * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const;
+	ui32 calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const;
 
 	///selects from allStacks actually affected stacks
 	std::set<const CStack *> getAffectedStacks(const CBattleInfoCallback * cb, ECastingMode::ECastingMode mode, PlayerColor casterColor, int spellLvl, BattleHex destination, const CGHeroInstance * caster = nullptr) const;
@@ -322,6 +328,11 @@ public:
 	///implementation of BattleSpellCast applying
 	void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const;
 
+public:
+	///Client logic.
+	
+	void prepareBattleLog(const CBattleInfoCallback * cb, const BattleSpellCast * packet, std::vector<std::string> & logLines) const;
+
 private:
 	void setIsOffensive(const bool val);
 	void setIsRising(const bool val);

+ 3 - 0
lib/spells/ISpellMechanics.h

@@ -47,6 +47,9 @@ public:
 	virtual bool adventureCast(const SpellCastEnvironment * env, AdventureSpellCastParameters & parameters) const = 0;
 	virtual void battleCast(const SpellCastEnvironment * env, BattleSpellCastParameters & parameters) const = 0;
 
+	virtual void battleLogSingleTarget(std::vector<std::string> & logLines, const BattleSpellCast * packet, 
+		const std::string & casterName, const CStack * attackedStack, bool & displayDamage) const = 0;
+	
 	static ISpellMechanics * createMechanics(CSpell * s);
 protected:
 	CSpell * owner;

+ 32 - 0
lib/spells/Magic.h

@@ -0,0 +1,32 @@
+/*
+ * Magic.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+/**
+ * High-level interface for spells subsystem
+ */
+
+
+class CSpell;
+class CStack;
+
+class DLL_LINKAGE ISpellCaster
+{
+public:
+	virtual ~ISpellCaster(){};
+	
+	/// returns level on which given spell would be cast by this(0 - none, 1 - basic etc);
+	/// caster may not know this spell at all 
+	/// optionally returns number of selected school by arg - 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic
+	virtual ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const = 0;		
+	
+	virtual ui32 getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const = 0;
+};

+ 15 - 14
server/CGameHandler.cpp

@@ -2789,7 +2789,7 @@ bool CGameHandler::upgradeCreature( ObjectInstanceID objid, SlotID pos, Creature
 	return true;
 }
 
-bool CGameHandler::changeStackType(const StackLocation &sl, CCreature *c)
+bool CGameHandler::changeStackType(const StackLocation &sl, const CCreature *c)
 {
 	if(!sl.army->hasStackAtSlot(sl.slot))
 		COMPLAIN_RET("Cannot find a stack to change type");
@@ -3208,14 +3208,15 @@ bool CGameHandler::transformInUndead(const IMarket *market, const CGHeroInstance
 
 
 	const CStackInstance &s = army->getStack(slot);
-	int resCreature;//resulting creature - bone dragons or skeletons
 
-	if	(s.hasBonusOfType(Bonus::DRAGON_NATURE))
-		resCreature = 68;
-	else
-		resCreature = 56;
+	//resulting creature - bone dragons or skeletons
+	CreatureID resCreature = CreatureID::SKELETON;
 
-	changeStackType(StackLocation(army, slot), VLC->creh->creatures.at(resCreature));
+	if(s.hasBonusOfType(Bonus::DRAGON_NATURE)
+			|| (s.getCreatureID() == CreatureID::HYDRA)
+			|| (s.getCreatureID() == CreatureID::CHAOS_HYDRA))
+		resCreature = CreatureID::BONE_DRAGON;
+	changeStackType(StackLocation(army, slot), resCreature.toCreature());
 	return true;
 }
 
@@ -3431,7 +3432,7 @@ bool CGameHandler::makeBattleAction( BattleAction &ba )
 			//defensive stance //TODO: remove this bonus when stack becomes active
 			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->stacks.at(ba.stackNumber)->valOfBonuses(Bonus::DEFENSIVE_STANCE),
+			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));
 			sse.stacks.push_back(ba.stackNumber);
 			sendAndApply(&sse);
@@ -3880,7 +3881,7 @@ bool CGameHandler::makeBattleAction( BattleAction &ba )
 				p.mode = ECastingMode::CREATURE_ACTIVE_CASTING;
 				p.destination = destination;
 				p.casterColor = stack->owner;	
-				p.caster = nullptr;
+				p.casterHero = nullptr;
 				p.usedSpellPower = 0;	
 				p.casterStack = stack;	
 				p.selectedStack = nullptr;				
@@ -4080,7 +4081,7 @@ bool CGameHandler::makeCustomAction( BattleAction &ba )
 			parameters.destination = ba.destinationTile;
 			parameters.casterSide = ba.side;
 			parameters.casterColor =  h->tempOwner;	
-			parameters.caster = h;
+			parameters.casterHero = h;
 			parameters.secHero = secondHero;
 			
 			parameters.usedSpellPower = h->getPrimSkillLevel(PrimarySkill::SPELL_POWER);	
@@ -4239,7 +4240,7 @@ void CGameHandler::stackTurnTrigger(const CStack * st)
 					parameters.destination = BattleHex::INVALID;
 					parameters.casterSide = side;
 					parameters.casterColor = st->owner;	
-					parameters.caster = nullptr;
+					parameters.casterHero = nullptr;
 					parameters.secHero = gs->curB->getHero(gs->curB->theOtherPlayer(st->owner));
 					parameters.usedSpellPower = 0;	
 					parameters.mode = ECastingMode::ENCHANTER_CASTING;
@@ -4952,7 +4953,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta
 				parameters.destination = destination;
 				parameters.casterSide = !attacker->attackerOwned;
 				parameters.casterColor = attacker->owner;	
-				parameters.caster = nullptr;
+				parameters.casterHero = nullptr;
 				parameters.secHero = nullptr;
 
 				parameters.usedSpellPower = 0;	
@@ -4987,7 +4988,7 @@ void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat )
 		parameters.destination = gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->position;
 		parameters.casterSide = !attacker->attackerOwned;
 		parameters.casterColor = attacker->owner;	
-		parameters.caster = nullptr;
+		parameters.casterHero = nullptr;
 		parameters.secHero = nullptr;
 
 		parameters.usedSpellPower = power;	
@@ -5297,7 +5298,7 @@ void CGameHandler::runBattle()
 			parameters.destination = BattleHex::INVALID;
 			parameters.casterSide = i;
 			parameters.casterColor = h->tempOwner;	
-			parameters.caster = nullptr;
+			parameters.casterHero = nullptr;
 			parameters.secHero = gs->curB->battleGetFightingHero(1-i);
 			
 			

+ 1 - 1
server/CGameHandler.h

@@ -142,7 +142,7 @@ public:
 
 	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override;
 	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) override;
-	bool changeStackType(const StackLocation &sl, CCreature *c) override;
+	bool changeStackType(const StackLocation &sl, const CCreature *c) override;
 	bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override;
 	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override;
 	bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override;

+ 5 - 1
server/CQuery.cpp

@@ -297,7 +297,11 @@ bool CGarrisonDialogQuery::blocksPack(const CPack *pack) const
 	{
 		return !vstd::contains(ourIds, dismiss->heroID);
 	}
-
+	
+	if(auto upgrade = dynamic_cast<const UpgradeCreature*>(pack))
+	{
+		return !vstd::contains(ourIds, upgrade->id);
+	}
 	return CDialogQuery::blocksPack(pack);
 }