Ver código fonte

Cumulative spell effects

* Added experimental support for cumulative effects for ENCHANTED bonus
* Updated and fixed SPECIAL_PECULIAR_ENCHANT processing
* Initial implementation of cumulative spell effects.
* Scheme for new spell feature - cumulative bonus.
AlexVinS 9 anos atrás
pai
commit
0f5202689e

+ 3 - 5
AI/BattleAI/BattleAI.cpp

@@ -259,11 +259,9 @@ void CBattleAI::attemptCastingSpell()
 			{
 				StackWithBonuses swb;
 				swb.stack = sta;
-				Bonus pseudoBonus;
-				pseudoBonus.sid = ps.spell->id;
-				pseudoBonus.val = skillLevel;
-				pseudoBonus.turnsRemain = 1; //TODO
-				CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus);
+				//todo: handle effect actualization in HypotheticChangesToBattleState
+				ps.spell->getEffects(swb.bonusesToAdd, skillLevel, false, hero->getEnchantPower(ps.spell));
+				ps.spell->getEffects(swb.bonusesToAdd, skillLevel, true, hero->getEnchantPower(ps.spell));
 				HypotheticChangesToBattleState state;
 				state.bonusesOfStacks[swb.stack] = &swb;
 				PotentialTargets pt(swb.stack, state);

+ 3 - 0
ChangeLog

@@ -3,6 +3,9 @@
 GENERAL:
 * Spectator mode was implemented through command-line options
 
+SPELLS:
+* Implemented cumulative effects for spells
+
 0.98 -> 0.99
 
 GENERAL:

+ 11 - 19
client/battle/CBattleInterface.cpp

@@ -1359,52 +1359,44 @@ void CBattleInterface::spellCast(const BattleSpellCast *sc)
 
 void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
 {
-	if (sse.effect.back().sid == -1 && sse.stacks.size() == 1 && sse.effect.size() == 2)
+	if(sse.stacks.size() == 1 && sse.effect.size() == 2 && sse.effect.back().sid == -1)
 	{
 		const Bonus & bns = sse.effect.front();
-		if (bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL)
+		if(bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL)
 		{
 			//defensive stance
 			const CStack *stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin());
 			int txtid = 120;
 
-			if (stack->count != 1)
+			if(stack->count != 1)
 				txtid++; //move to plural text
 
 			BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE)));
-			defenseBonuses.remove_if (Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance
+			defenseBonuses.remove_if(Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance
 			int val = stack->Defense() - defenseBonuses.totalValue();
-			auto txt = boost::format (CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val;
+			auto txt = boost::format(CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val;
 			console->addText(boost::to_string(txt));
 		}
 	}
 
-	if (activeStack != nullptr) //it can be -1 when a creature casts effect
-	{
+	if(activeStack != nullptr)
 		redrawBackgroundWithHexes(activeStack);
-	}
 }
 
-CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell *spell, const ISpellCaster *caster, ECastingMode::ECastingMode mode) const
+CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell * spell, const ISpellCaster * caster, ECastingMode::ECastingMode mode) const
 {
 	PossibleActions spellSelMode = ANY_LOCATION;
 
 	const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode);
 
-	if (ti.massive || ti.type == CSpell::NO_TARGET)
+	if(ti.massive || ti.type == CSpell::NO_TARGET)
 		spellSelMode = NO_LOCATION;
-	else if (ti.type == CSpell::LOCATION && ti.clearAffected)
-	{
+	else if(ti.type == CSpell::LOCATION && ti.clearAffected)
 		spellSelMode = FREE_LOCATION;
-	}
-	else if (ti.type == CSpell::CREATURE)
-	{
+	else if(ti.type == CSpell::CREATURE)
 		spellSelMode = AIMED_SPELL_CREATURE;
-	}
-	else if (ti.type == CSpell::OBSTACLE)
-	{
+	else if(ti.type == CSpell::OBSTACLE)
 		spellSelMode = OBSTACLE;
-	}
 
 	return spellSelMode;
 }

+ 26 - 19
config/schemas/spell.json

@@ -19,25 +19,25 @@
 					{
 						//assumed verticalPosition: top
 						"type": "string",
-						"format": "defFile"				
+						"format": "defFile"
 					},
 					{
 						"type": "object",
 						"properties":{
 							"verticalPosition": {"type":"string", "enum":["top","bottom"]},
 							"defName": {"type":"string", "format": "defFile"}
-						},					
-						"additionalProperties" : false				
+						},
+						"additionalProperties" : false
 					}
-				]					
-			}			
+				]
+			}
 		},
 		"animation":{
 			"type": "object",
 			"additionalProperties" : false,
 			"properties":{
-				"affect":{"$ref" : "#/definitions/animationQueue"},				
-				"hit":{"$ref" : "#/definitions/animationQueue"},				
+				"affect":{"$ref" : "#/definitions/animationQueue"},
+				"hit":{"$ref" : "#/definitions/animationQueue"},
 				"cast":{"$ref" : "#/definitions/animationQueue"},
 				"projectile":{
 					"type":"array",
@@ -46,11 +46,11 @@
 						"properties":{
 							"minimumAngle": {"type":"number", "minimum" : 0},
 							"defName": {"type":"string", "format": "defFile"}
-						},					
-						"additionalProperties" : false						
+						},
+						"additionalProperties" : false
 					}
-				}				
-			}		
+				}
+			}
 		},
 		"flags" :{
 			"type" : "object",
@@ -85,7 +85,14 @@
 				},
 				"effects":{
 					"type": "object",
-					"description": "Timed effects",
+					"description": "Timed effects (updated by prolongation)",
+					"additionalProperties" : {
+						"$ref" : "vcmi:bonus"
+					}
+				},
+				"cumulativeEffects":{
+					"type": "object",
+					"description": "Timed effects (updated by unique bonus)",
 					"additionalProperties" : {
 						"$ref" : "vcmi:bonus"
 					}
@@ -107,16 +114,16 @@
 						{
 							"type": "boolean",
 							"description": "LOCATION target only. All affected hexes/tile must be clear"
-						}						
+						}
 					}
 				}
 			}
 		},
-		
+
 		"texts":{
 			"type": "object",
-			
-			
+
+
 			"additionalProperties" : false
 		}
 	},
@@ -239,9 +246,9 @@
 				 "$ref" : "#/definitions/flags",
 				 "description": "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated."
 		},
-		
+
 		"animation":{"$ref": "#/definitions/animation"},
-		
+
 		"graphics":{
 				 "type": "object",
 				 "additionalProperties" : false,
@@ -291,7 +298,7 @@
 			 "additionalProperties" : false,
 			 "required" : ["none", "basic", "advanced", "expert"],
 
-			 "properties":{	
+			 "properties":{
 				"base":{
 					"type": "object",
 					"description": "will be merged with all levels",

+ 2 - 1
config/spells/ability.json

@@ -101,6 +101,7 @@
 		"levels" : {
 			"base":{
 				"range" : "0",
+				//no cumulative effect even with mods here
 				"effects" : {
 					"bindEffect" : {
 						"val" : 0,
@@ -335,7 +336,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -3,
 						"type" : "PRIMARY_SKILL",

+ 5 - 3
config/spells/timed.json

@@ -13,6 +13,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
+				//no cumulative effect even with mods here
 				"effects" : {
 					"generalDamageReduction" : {
 						"type" : "GENERAL_DAMAGE_REDUCTION",
@@ -43,6 +44,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
+				//no cumulative effect even with mods here
 				"effects" : {
 					"generalDamageReduction" : {
 						"type" : "GENERAL_DAMAGE_REDUCTION",
@@ -558,7 +560,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.defence",
@@ -569,14 +571,14 @@
 				}
 			},
 			"advanced":{
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -4
 					}
 				}
 			},
 			"expert":{
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -5
 					}

+ 0 - 15
lib/CStack.cpp

@@ -104,21 +104,6 @@ si32 CStack::magicResistance() const
 	return magicResistance;
 }
 
-void CStack::stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse)
-{
-	const CSpell * sp = SpellID(sse.sid).toSpell();
-
-	std::vector<Bonus> tmp;
-	sp->getEffects(tmp, sse.val);
-
-	for(Bonus& b : tmp)
-	{
-		if(b.turnsRemain == 0)
-			b.turnsRemain = sse.turnsRemain;
-		sf.push_back(b);
-	}
-}
-
 bool CStack::willMove(int turn /*= 0*/) const
 {
 	return ( turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING) )

+ 0 - 1
lib/CStack.h

@@ -62,7 +62,6 @@ public:
 	ui32 calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const;
 	ui32 level() const;
 	si32 magicResistance() const override; //include aura of resistance
-	static void stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse);
 	std::vector<si32> activeSpells() const; //returns vector of active spell IDs sorted by time of cast
 	const CGHeroInstance *getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise
 	ui32 totalHealth() const; // total health for all creatures in stack;

+ 8 - 0
lib/NetPacks.h

@@ -1519,11 +1519,19 @@ struct SetStackEffect : public CPackForClient
 	void applyCl(CClient *cl);
 
 	std::vector<ui32> stacks; //affected stacks (IDs)
+
+	//regular effects
 	std::vector<Bonus> effect; //bonuses to apply
 	std::vector<std::pair<ui32, Bonus> > uniqueBonuses; //bonuses per single stack
+
+	//cumulative effects
+	std::vector<Bonus> cumulativeEffects; //bonuses to apply
+	std::vector<std::pair<ui32, Bonus> > cumulativeUniqueBonuses; //bonuses per single stack
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & stacks & effect & uniqueBonuses;
+		h & cumulativeEffects & cumulativeUniqueBonuses;
 	}
 };
 

+ 22 - 7
lib/NetPacksLib.cpp

@@ -1554,33 +1554,39 @@ void actualizeEffect(CStack * s, const std::vector<Bonus> & ef)
 
 DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs)
 {
-	if(effect.empty())
+	if(effect.empty() && cumulativeEffects.empty())
 	{
 		logGlobal->errorStream() << "Trying to apply SetStackEffect with no effects";
 		return;
 	}
 
-	int spellid = effect.begin()->sid; //effects' source ID
+	si32 spellid = effect.empty() ? cumulativeEffects.begin()->sid : effect.begin()->sid; //effects' source ID
 
-	auto processEffect = [spellid, this](CStack * sta, const Bonus & effect)
+	auto processEffect = [spellid, this](CStack * sta, const Bonus & effect, bool cumulative)
 	{
-		if(!sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype)))
-			|| spellid == SpellID::DISRUPTING_RAY || spellid == SpellID::ACID_BREATH_DEFENSE)
+		if(cumulative || !sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype))))
 		{
 			//no such effect or cumulative - add new
 			logBonus->traceStream() << sta->nodeName() << " receives a new bonus: " << effect.Description();
 			sta->addNewBonus(std::make_shared<Bonus>(effect));
 		}
 		else
+		{
+			logBonus->traceStream() << sta->nodeName() << " updated bonus: " << effect.Description();
 			actualizeEffect(sta, effect);
+		}
 	};
 
 	for(ui32 id : stacks)
 	{
 		CStack *s = gs->curB->getStack(id);
 		if(s)
+		{
 			for(const Bonus & fromEffect : effect)
-				processEffect(s, fromEffect);
+				processEffect(s, fromEffect, false);
+			for(const Bonus & fromEffect : cumulativeEffects)
+				processEffect(s, fromEffect, true);
+		}
 		else
 			logNetwork->errorStream() << "Cannot find stack " << id;
 	}
@@ -1589,7 +1595,16 @@ DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs)
 	{
 		CStack *s = gs->curB->getStack(para.first);
 		if(s)
-			processEffect(s, para.second);
+			processEffect(s, para.second, false);
+		else
+			logNetwork->errorStream() << "Cannot find stack " << para.first;
+	}
+
+	for(auto & para : cumulativeUniqueBonuses)
+	{
+		CStack *s = gs->curB->getStack(para.first);
+		if(s)
+			processEffect(s, para.second, true);
 		else
 			logNetwork->errorStream() << "Cannot find stack " << para.first;
 	}

+ 1 - 1
lib/serializer/CSerializer.h

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

+ 2 - 1
lib/spells/AdventureSpellMechanics.cpp

@@ -79,11 +79,12 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(const SpellCastE
 {
 	if(owner->hasEffects())
 	{
+		//todo: cumulative effects support
 		const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
 
 		std::vector<Bonus> bonuses;
 
-		owner->getEffects(bonuses, schoolLevel);
+		owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner));
 
 		for(Bonus b : bonuses)
 		{

+ 104 - 91
lib/spells/CDefaultSpellMechanics.cpp

@@ -445,118 +445,131 @@ void DefaultSpellMechanics::battleLogDefault(std::vector<MetaString> & logLines,
 
 void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
 {
-	//applying effects
 	if(owner->isOffensiveSpell())
-	{
-		const int rawDamage = parameters.getEffectValue();
-		int chainLightningModifier = 0;
-		for(auto & attackedCre : ctx.attackedCres)
-		{
-			BattleStackAttacked bsa;
-			bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier;
-			ctx.addDamageToDisplay(bsa.damageAmount);
+		defaultDamageEffect(env, parameters, ctx);
 
-			bsa.stackAttacked = (attackedCre)->ID;
-			if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
-				bsa.attackerID = parameters.casterStack->ID;
-			else
-				bsa.attackerID = -1;
-			(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator());
-			ctx.si.stacks.push_back(bsa);
+	if(owner->hasEffects())
+		defaultTimedEffect(env, parameters, ctx);
+}
 
-			if(owner->id == SpellID::CHAIN_LIGHTNING)
-				++chainLightningModifier;
-		}
+void DefaultSpellMechanics::defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
+{
+	const int rawDamage = parameters.getEffectValue();
+	int chainLightningModifier = 0;
+	for(auto & attackedCre : ctx.attackedCres)
+	{
+		BattleStackAttacked bsa;
+		bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier;
+		ctx.addDamageToDisplay(bsa.damageAmount);
+
+		bsa.stackAttacked = (attackedCre)->ID;
+		if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
+			bsa.attackerID = parameters.casterStack->ID;
+		else
+			bsa.attackerID = -1;
+		(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator());
+		ctx.si.stacks.push_back(bsa);
+
+		if(owner->id == SpellID::CHAIN_LIGHTNING)
+			++chainLightningModifier;
 	}
+}
 
-	if(owner->hasEffects())
+void DefaultSpellMechanics::defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
+{
+	SetStackEffect sse;
+	//get default spell duration (spell power with bonuses for heroes)
+	int duration = parameters.enchantPower;
+	//generate actual stack bonuses
 	{
-		SetStackEffect sse;
-		//get default spell duration (spell power with bonuses for heroes)
-		int duration = parameters.enchantPower;
-		//generate actual stack bonuses
-		{
-			int maxDuration = 0;
-			std::vector<Bonus> tmp;
-			owner->getEffects(tmp, parameters.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);
-		}
-		//we need to know who cast Bind
-		if(owner->id == SpellID::BIND && parameters.casterStack)
-		{
-			sse.effect.back().additionalInfo =  parameters.casterStack->ID;
-		}
-		std::shared_ptr<Bonus> bonus = nullptr;
-		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 maxDuration = 0;
+
+		owner->getEffects(sse.effect, parameters.effectLevel, false, duration, &maxDuration);
+		owner->getEffects(sse.cumulativeEffects, parameters.effectLevel, true, duration, &maxDuration);
+
+		//if all spell effects have special duration, use it later for special bonuses
+		duration = maxDuration;
+	}
+	//fix to original config: shield should display damage reduction
+	if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD)
+	{
+		sse.effect.at(sse.effect.size() - 1).val = (100 - sse.effect.back().val);
+	}
+	//we need to know who cast Bind
+	else if(owner->id == SpellID::BIND && parameters.casterStack)
+	{
+		sse.effect.at(sse.effect.size() - 1).additionalInfo = parameters.casterStack->ID;
+	}
+	std::shared_ptr<Bonus> bonus = nullptr;
+	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?
 
+	for(const CStack * affected : ctx.attackedCres)
+	{
 		si32 power = 0;
-		for(const CStack * affected : ctx.attackedCres)
-		{
-			sse.stacks.push_back(affected->ID);
+		sse.stacks.push_back(affected->ID);
 
-			//Apply hero specials - peculiar enchants
-			const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines)
-			if(bonus)
+		//Apply hero specials - peculiar enchants
+		const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines)
+		if(bonus)
+		{
+			switch(bonus->additionalInfo)
 			{
-				switch(bonus->additionalInfo)
+			case 0: //normal
 				{
-					case 0: //normal
+					switch(tier)
+					{
+					case 1:
+					case 2:
+						power = 3;
+						break;
+					case 3:
+					case 4:
+						power = 2;
+						break;
+					case 5:
+					case 6:
+						power = 1;
+						break;
+					}
+					for(const Bonus & b : sse.effect)
 					{
-						switch(tier)
-						{
-							case 1: case 2:
-								power = 3;
-							break;
-							case 3: case 4:
-								power = 2;
-							break;
-							case 5: case 6:
-								power = 1;
-							break;
-						}
-						Bonus specialBonus(sse.effect.back());
+						Bonus specialBonus(b);
 						specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely
-						sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional premy to given effect
+						specialBonus.turnsRemain = duration;
+						sse.uniqueBonuses.push_back(std::pair<ui32, Bonus>(affected->ID, specialBonus)); //additional premy to given effect
 					}
-					break;
-					case 1: //only Coronius as yet
+					for(const Bonus & b : sse.cumulativeEffects)
 					{
-						power = std::max(5 - tier, 0);
-						Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK);
+						Bonus specialBonus(b);
+						specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely
 						specialBonus.turnsRemain = duration;
-						sse.uniqueBonuses.push_back(std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional attack to Slayer effect
+						sse.cumulativeUniqueBonuses.push_back(std::pair<ui32, Bonus>(affected->ID, specialBonus)); //additional premy to given effect
 					}
-					break;
 				}
-			}
-			if (parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
-			{
-				int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
-				Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL);
-				specialBonus.turnsRemain = duration;
-				sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus));
+				break;
+			case 1: //only Coronius as yet
+				{
+					power = std::max(5 - tier, 0);
+					Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK);
+					specialBonus.turnsRemain = duration;
+					sse.uniqueBonuses.push_back(std::pair<ui32,Bonus>(affected->ID, specialBonus)); //additional attack to Slayer effect
+				}
+				break;
 			}
 		}
-
-		if(!sse.stacks.empty())
-			env->sendAndApply(&sse);
+		if(parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
+		{
+			int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
+			Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL);
+			specialBonus.turnsRemain = duration;
+			sse.uniqueBonuses.push_back(std::pair<ui32,Bonus>(affected->ID, specialBonus));
+		}
 	}
+
+	if(!sse.stacks.empty())
+		env->sendAndApply(&sse);
 }
 
 std::vector<BattleHex> DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const

+ 3 - 0
lib/spells/CDefaultSpellMechanics.h

@@ -75,6 +75,9 @@ protected:
 protected:
 	void doDispell(BattleInfo * battle, const BattleSpellCast * packet, const CSelector & selector) const;
 	bool canDispell(const IBonusBearer * obj, const CSelector & selector, const std::string &cachingStr = "") const;
+
+	void defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
+	void defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
 private:
 	void cast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, std::vector <const CStack*> & reflected) const;
 

+ 36 - 14
lib/spells/CSpellHandler.cpp

@@ -350,7 +350,7 @@ bool CSpell::isSpecialSpell() const
 
 bool CSpell::hasEffects() const
 {
-	return !levels[0].effects.empty();
+	return !levels[0].effects.empty() || !levels[0].cumulativeEffects.empty();
 }
 
 const std::string & CSpell::getIconImmune() const
@@ -382,7 +382,7 @@ si32 CSpell::getProbability(const TFaction factionId) const
 	return probabilities.at(factionId);
 }
 
-void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
+void CSpell::getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> maxDuration/* = boost::none*/) const
 {
 	if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
 	{
@@ -390,19 +390,29 @@ void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
 		return;
 	}
 
-	const std::vector<Bonus> & effects = levels[level].effects;
+	const auto & levelObject = levels.at(level);
 
-	if(effects.empty())
+	if(levelObject.effects.empty() && levelObject.cumulativeEffects.empty())
 	{
 		logGlobal->errorStream() << __FUNCTION__ << " This spell ("  + name + ") has no effects for level " << level;
 		return;
 	}
 
+	const auto & effects = cumulative ? levelObject.cumulativeEffects : levelObject.effects;
+
 	lst.reserve(lst.size() + effects.size());
 
-	for(const Bonus & b : effects)
+	for(const auto b : effects)
 	{
-		lst.push_back(Bonus(b));
+		Bonus nb(*b);
+
+		//use configured duration if present
+		if(nb.turnsRemain == 0)
+			nb.turnsRemain = duration;
+		if(maxDuration)
+			vstd::amax(*(maxDuration.get()), nb.turnsRemain);
+
+		lst.push_back(nb);
 	}
 }
 
@@ -1029,9 +1039,25 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode & json, const std::string &
 			if(usePowerAsValue)
 				b->val = levelPower;
 
-			levelObject.effectsTmp.push_back(b);
+			levelObject.effects.push_back(b);
 		}
 
+		for(const auto & elem : levelNode["cumulativeEffects"].Struct())
+		{
+			const JsonNode & bonusNode = elem.second;
+			auto b = JsonUtils::parseBonus(bonusNode);
+			const bool usePowerAsValue = bonusNode["val"].isNull();
+
+			//TODO: make this work. see CSpellHandler::afterLoadFinalization()
+			//b->sid = spell->id; //for all
+
+			b->source = Bonus::SPELL_EFFECT;//for all
+
+			if(usePowerAsValue)
+				b->val = levelPower;
+
+			levelObject.cumulativeEffects.push_back(b);
+		}
 	}
 
 	return spell;
@@ -1044,14 +1070,10 @@ void CSpellHandler::afterLoadFinalization()
 	{
 		for(auto & level: spell->levels)
 		{
-			for(auto bonus : level.effectsTmp)
-			{
-				level.effects.push_back(*bonus);
-			}
-			level.effectsTmp.clear();
-
 			for(auto & bonus: level.effects)
-				bonus.sid = spell->id;
+				bonus->sid = spell->id;
+			for(auto & bonus: level.cumulativeEffects)
+				bonus->sid = spell->id;
 		}
 		spell->setup();
 	}

+ 31 - 7
lib/spells/CSpellHandler.h

@@ -130,16 +130,35 @@ public:
 		bool clearAffected;
 		std::string range;
 
-		std::vector<Bonus> effects;
-
-		std::vector<std::shared_ptr<Bonus>> effectsTmp; //TODO: this should replace effects
+		std::vector<std::shared_ptr<Bonus>> effects;
+		std::vector<std::shared_ptr<Bonus>> cumulativeEffects;
 
 		LevelInfo();
 		~LevelInfo();
 
 		template <typename Handler> void serialize(Handler &h, const int version)
 		{
-			h & description & cost & power & AIValue & smartTarget & range & effects;
+			h & description & cost & power & AIValue & smartTarget & range;
+
+			if(version >= 773)
+			{
+				h & effects & cumulativeEffects;
+			}
+			else
+			{
+				//all old effects treated as not cumulative, special cases handled by CSpell::serialize
+				std::vector<Bonus> old;
+				h & old;
+
+				if(!h.saving)
+				{
+					effects.clear();
+					cumulativeEffects.clear();
+					for(const Bonus & oldBonus : old)
+						effects.push_back(std::make_shared<Bonus>(oldBonus));
+				}
+			}
+
 			h & clearTarget & clearAffected;
 		}
 	};
@@ -180,7 +199,7 @@ public:
 
 	si32 level;
 
-	std::map<ESpellSchool, bool> school; //todo: use this instead of separate boolean fields
+	std::map<ESpellSchool, bool> school;
 
 	si32 power; //spell's power
 
@@ -215,8 +234,7 @@ public:
 	bool isSpecialSpell() const;
 
 	bool hasEffects() const;
-	void getEffects(std::vector<Bonus> &lst, const int level) const;
-
+	void getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> maxDuration = boost::none) const;
 
 	///calculate spell damage on stack taking caster`s secondary skills and affectedCreature`s bonuses into account
 	ui32 calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const;
@@ -264,6 +282,12 @@ public:
 
 		if(!h.saving)
 			setup();
+		//backward compatibility
+		//can not be added to level structure as level structure does not know spell id
+		if(!h.saving && version < 773)
+			if(id == SpellID::DISRUPTING_RAY || id == SpellID::ACID_BREATH_DEFENSE)
+				for(auto & level : levels)
+					std::swap(level.effects, level.cumulativeEffects);
 	}
 	friend class CSpellHandler;
 	friend class Graphics;

+ 20 - 13
server/CGameHandler.cpp

@@ -4489,30 +4489,32 @@ bool CGameHandler::makeCustomAction(BattleAction &ba)
 }
 
 
-void CGameHandler::stackAppearTrigger(const CStack *st)
+void CGameHandler::stackEnchantedTrigger(const CStack * st)
 {
 	auto bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTED)));
-	for (auto b : bl)
+	for(auto b : bl)
 	{
 		SetStackEffect sse;
 		int val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype));
-		if (val > 3)
+		if(val > 3)
 		{
-			for (auto s : gs->curB->battleGetAllStacks())
+			for(auto s : gs->curB->battleGetAllStacks())
 			{
-				if (battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
+				if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
 					sse.stacks.push_back (s->ID);
 			}
 		}
 		else
 			sse.stacks.push_back (st->ID);
 
-		Bonus pseudoBonus;
-		pseudoBonus.sid = b->subtype;
-		pseudoBonus.val = ((val > 3) ?  (val - 3) : val);
-		pseudoBonus.turnsRemain = 50;
-		st->stackEffectToFeature(sse.effect, pseudoBonus);
-		if (sse.effect.size())
+		const CSpell * sp = SpellID(b->subtype).toSpell();
+		const int level = ((val > 3) ?  (val - 3) : val);
+
+		sp->getEffects(sse.effect, level, false, 50);
+		//this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle
+		sp->getEffects(sse.cumulativeEffects, level, true, 50);
+
+		if(!sse.effect.empty() || !sse.cumulativeEffects.empty())
 			sendAndApply(&sse);
 	}
 }
@@ -4526,7 +4528,6 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 	bte.additionalInfo = 0;
 	if (st->alive())
 	{
-		stackAppearTrigger(st);
 		//unbind
 		if (st->hasBonus(Selector::type(Bonus::BIND_EFFECT)))
 		{
@@ -5724,7 +5725,7 @@ void CGameHandler::runBattle()
 			}
 		}
 
-		stackAppearTrigger(stack);
+		stackEnchantedTrigger(stack);
 	}
 
 	//spells opening battle
@@ -5766,6 +5767,12 @@ void CGameHandler::runBattle()
 
 		const BattleInfo & curB = *gs->curB;
 
+		for(auto stack : curB.stacks)
+		{
+			if(stack->alive() && curB.round > 1)
+				stackEnchantedTrigger(stack);
+		}
+
 		//stack loop
 
 		const CStack *next;

+ 1 - 1
server/CGameHandler.h

@@ -196,7 +196,7 @@ public:
 	bool makeBattleAction(BattleAction &ba);
 	bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
 	bool makeCustomAction(BattleAction &ba);
-	void stackAppearTrigger(const CStack *stack);
+	void stackEnchantedTrigger(const CStack * stack);
 	void stackTurnTrigger(const CStack *stack);
 	void handleDamageFromObstacle(const CObstacleInstance &obstacle, const CStack * curStack); //checks if obstacle is land mine and handles possible consequences
 	void removeObstacle(const CObstacleInstance &obstacle);