瀏覽代碼

[Spells] More spell related refactoring
+ smart target modifier
- CREATURE_EXPERT_MASSIVE target type
* save format changed

spell format changes already documented in http://wiki.vcmi.eu/index.php?title=Spell_Format

alexvins 11 年之前
父節點
當前提交
9cac0af7be

+ 2 - 2
client/CCastleInterface.cpp

@@ -1687,13 +1687,13 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
 void CMageGuildScreen::Scroll::clickLeft(tribool down, bool previousState)
 {
 	if(down)
-		LOCPLINT->showInfoDialog(spell->descriptions[0], new CComponent(CComponent::spell,spell->id));
+		LOCPLINT->showInfoDialog(spell->getLevelInfo(0).description, new CComponent(CComponent::spell,spell->id));
 }
 
 void CMageGuildScreen::Scroll::clickRight(tribool down, bool previousState)
 {
 	if(down)
-		CRClickPopup::createAndPush(spell->descriptions[0], new CComponent(CComponent::spell, spell->id));
+		CRClickPopup::createAndPush(spell->getLevelInfo(0).description, new CComponent(CComponent::spell, spell->id));
 }
 
 void CMageGuildScreen::Scroll::hover(bool on)

+ 2 - 2
client/CSpellWindow.cpp

@@ -630,7 +630,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 			|| (!sp->combatSpell && owner->myInt->battleInt))
 		{
 			std::vector<CComponent*> hlp(1, new CComponent(CComponent::spell, mySpell, 0));
-			LOCPLINT->showInfoDialog(sp->descriptions[schoolLevel], hlp);
+			LOCPLINT->showInfoDialog(sp->getLevelInfo(schoolLevel).description, hlp);
 			return;
 		}
 
@@ -810,7 +810,7 @@ void CSpellWindow::SpellArea::clickRight(tribool down, bool previousState)
 			boost::algorithm::replace_first(dmgInfo, "%d", boost::lexical_cast<std::string>(causedDmg));
 		}
 
-		CRClickPopup::createAndPush(CGI->spellh->objects[mySpell]->descriptions[schoolLevel] + dmgInfo,
+		CRClickPopup::createAndPush(CGI->spellh->objects[mySpell]->getLevelInfo(schoolLevel).description + dmgInfo,
 		                            new CComponent(CComponent::spell, mySpell));
 	}
 }

+ 1 - 1
client/GUIClasses.cpp

@@ -977,7 +977,7 @@ std::string CComponent::getDescription()
 	case creature:   return "";
 	case artifact:   return CGI->arth->artifacts[subtype]->Description();
 	case experience: return CGI->generaltexth->allTexts[241];
-	case spell:      return CGI->spellh->objects[subtype]->descriptions[val];
+	case spell:      return CGI->spellh->objects[subtype]->getLevelInfo(val).description;
 	case morale:     return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)];
 	case luck:       return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)];
 	case building:   return CGI->townh->factions[subtype]->town->buildings[BuildingID(val)]->Description();

+ 12 - 20
client/battle/CBattleInterface.cpp

@@ -1549,31 +1549,23 @@ void CBattleInterface::castThisSpell(int spellID)
 	assert(castingHero); // code below assumes non-null hero
 	sp = CGI->spellh->objects[spellID];
 	spellSelMode = ANY_LOCATION;
-	if(sp->getTargetType() == CSpell::CREATURE)
-	{
-		spellSelMode = selectionTypeByPositiveness(*sp);
-	}
-	if(sp->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
+	
+	const CSpell::TargetInfo ti = sp->getTargetInfo(castingHero->getSpellSchoolLevel(sp));
+	
+	if(ti.massive)
+		spellSelMode = NO_LOCATION;	
+	else if(ti.type == CSpell::CREATURE)
 	{
-		if(castingHero->getSpellSchoolLevel(sp) < 3)
+		if(ti.smart)
 			spellSelMode = selectionTypeByPositiveness(*sp);
 		else
-			spellSelMode = NO_LOCATION;
-	}
-	if(sp->getTargetType() == CSpell::OBSTACLE)
+			spellSelMode = ANY_CREATURE;
+	}	
+	else if(ti.type == CSpell::OBSTACLE)
 	{
 		spellSelMode = OBSTACLE;
-	} //FIXME: Remove Obstacle has range X, unfortunatelly :(
-	else if(sp->range[ castingHero->getSpellSchoolLevel(sp) ] == "X") //spell has no range
-	{
-		spellSelMode = NO_LOCATION;
-	}
-
-	if(sp->range[ castingHero->getSpellSchoolLevel(sp) ].size() > 1) //spell has many-hex range
-	{
-		spellSelMode = ANY_LOCATION;
-	}
-
+	} 
+	//todo: move to JSON config
 	if(spellID == SpellID::FIRE_WALL  ||  spellID == SpellID::FORCE_FIELD)
 	{
 		spellSelMode = FREE_LOCATION;

+ 239 - 233
config/schemas/spell.json

@@ -1,4 +1,4 @@
-        {
+{
 
 	"type":"object",
 	"$schema": "http://json-schema.org/draft-04/schema",
@@ -7,235 +7,241 @@
 	"description" : "Format used to define new spells in VCMI",
 
 
-        "definitions" : {
-            "flags" :{
-                "type" : "object",
-                "additionalProperties" : {
-	            "type":"boolean"
-                }
-            },
-            "levelInfo":{
-                    "type": "object",
-                    "required":["range","description","cost","power","aiValue","range"],
-
-                    "additionalProperties" : false,
-		    "properties":{
-                            "description":{
-                                                 "type": "string",
-                                                 "description": "Localizable description. Use {xxx} for formatting"
-                            },
-                            "cost":{
-                                                 "type": "number",
-                                                 "description":"Cost in mana points"
-                            },
-                            "power":{
-                                                 "type": "number",
-                            },
-                            "aiValue":{
-                                                 "type": "number",
-                            },
-
-                            "range":{
-                                                 "type": "string",
-                                                 "description": "spell range description in SRSL"
-                            },
-                            "effects":{
-                                                 "type": "object",
-                                                 "description": "Timed effects",
-                                                 "additionalProperties" : {
-                                                     "$ref" : "vcmi:bonus"
-                                                 }
-                            }
-
-                    }
-            }
-        },
-
-        "required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"],
-        "additionalProperties" : false,
-
-        "properties": {
-                "index":{
-                        "type": "number",
-                        "description": "numeric id of spell required only for original spells, prohibited for new spells"
-                },
-                "type":{
-                        "type": "string",
-                        "enum": ["adventure", "combat", "ability"],
-                        "description":"Spell type"
-                },
-                "name":{
-                        "type": "string",
-                        "description": "Localizable name"
-
-                },
-                "school":{
-                        "type": "object",
-                        "description": "Spell schools",
-                        "additionalProperties": false,
-
-                        "properties":{
-
-                                "air":{"type": "boolean"},
-                                "fire":{"type": "boolean"},
-                                "earth":{"type": "boolean"},
-                                "water":{"type": "boolean"}
-                        }
-
-                },
-                "level":{
-                        "type": "number",
-                        "description": "Spell level",
-                        "minimum" : 0,
-                        "maximum" : 5
-                },
-
-                "power":{
-                        "type": "number",
-                        "description": "Base power",
-                },
-
-                "defaultGainChance":{
-                        "type": "number",
-                        "description": "Gain chance by default for all factions"
-
-                },
-
-                "gainChance":{
-                        "type": "object",
-                        "description": "Chance in % to gain for faction. NOTE: this field is merged with faction config",
-                        "additionalProperties" : {
-			     "type": "number",
-			     "minimum" : 0
-		        }
-                },
-                "targetType":{
-                          "type": "string",
-                          "enum": ["NO_TARGET","CREATURE","OBSTACLE","CREATURE_EXPERT_MASSIVE"]
-                },
-                "anim":{
-                        "type": "number",
-                        "description": "Main effect animation (AC format), -1 - none, deprecated",
-                        "minimum": -1
-                },
-                "counters":{
-                         "$ref" : "#/definitions/flags",
-                         "description": "Flags structure ids of countering spells"
-                },
-                "flags":{
-                        "type": "object",
-                        "description": "Various flags",
-                        "additionalProperties" : false,
-                        "properties":{
-                                "indifferent": {
-                                        "type":"boolean",
-                                        "description": "Spell is indifferent for target"
-                                },
-                                "negative": {
-                                        "type":"boolean",
-                                        "description": "Spell is negative for target"
-                                },
-                                "positive": {
-                                        "type":"boolean",
-                                        "description": "Spell is positive for target"
-                                },
-                                "damage": {
-                                        "type":"boolean",
-                                        "description": "Spell does damage (direct or indirect)"
-                                },
-                                "offensive": {
-                                        "type":"boolean",
-                                        "description": "Spell does direct damage (implicitly sets damage and negative)"
-                                },
-                                "rising":{
-                                        "type":"boolean",
-                                        "description": "Rising spell (implicitly sets positive)"
-                                },
-                                "special":{
-                                        "type": "boolean",
-                                        "description": "Special spell. Can be given only by Bonus::SPELL"
-                                }
-                        }
-                },
-                "immunity":{
-                        "$ref" : "#/definitions/flags",
-                         "description": "flags structure of bonus names, any one of these bonus grants immunity"
-                },
-                "absoluteImmunity":{
-                         "$ref" : "#/definitions/flags",
-                         "description": "flags structure of bonus names. Any one of these bonus grants immunity, cant be negated"
-                },
-                "limit":{
-                         "$ref" : "#/definitions/flags",
-                         "description": "flags structure of bonus names, presence of all bonuses required to be affected by"
-                },
-
-                "graphics":{
-                         "type": "object",
-                         "additionalProperties" : false,
-                         "properties":{
-                                 "iconImmune":{
-                                      "type": "string",
-                                      "description": "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)",
-                                      "format" : "imageFile"
-                                 },
-                                 "iconScenarioBonus":{
-                                      "type": "string",
-                                      "description": "Resourse path of icon for scenario bonus" ,
-                                      "format" : "imageFile"
-                                 },
-                                 "iconEffect":{
-                                      "type": "string",
-                                      "description": "Resourse path of icon for spell effects during battle" ,
-                                      "format" : "imageFile"
-                                 },
-                                 "iconBook":{
-                                      "type": "string",
-                                      "description":"Resourse path of icon for spellbook" ,
-                                      "format" : "imageFile"
-                                 },
-                                 "iconScroll":{
-                                      "type": "string",
-                                      "description": "Resourse path of icon for spell scrolls",
-                                      "format": "imageFile"
-                                 }
-                         }
-
-                },
-
-                "sounds":{
-                         "type": "object",
-                         "additionalProperties" : false,
-                         "properties":{
-                                 "cast":{
-                                      "type": "string",
-                                      "description": "Resourse path of cast sound"
-                                 }
-                         }
-                },
-
-                "levels":{
-                         "type": "object",
-                         "additionalProperties" : false,
-                         "required" : ["none", "basic", "advanced", "expert"],
-
-                         "properties":{
-                                 "none":{
-                                      "$ref" : "#/definitions/levelInfo"
-                                 },
-                                 "basic":{
-                                      "$ref" : "#/definitions/levelInfo"
-                                 },
-                                 "advanced":{
-                                      "$ref" : "#/definitions/levelInfo"
-                                 },
-                                 "expert":{
-                                      "$ref" : "#/definitions/levelInfo"
-                                 }
-                         }
-
-
-
-                }
-        }
-}
+	"definitions" : {
+		"flags" :{
+			"type" : "object",
+			"additionalProperties" : {
+			"type":"boolean"
+			}
+		},
+		"levelInfo":{
+			"type": "object",
+			"required":["range","description","cost","power","aiValue","range"],
+
+			"additionalProperties" : false,
+			"properties":{
+				"description":{
+					"type": "string",
+					"description": "Localizable description. Use {xxx} for formatting"
+				},
+				"cost":{
+					"type": "number",
+					"description":"Cost in mana points"
+				},
+				"power":{
+					"type": "number",
+				},
+				"aiValue":{
+					"type": "number",
+				},
+
+				"range":{
+					"type": "string",
+					"description": "spell range description in SRSL"
+				},
+				"effects":{
+					"type": "object",
+					"description": "Timed effects",
+					"additionalProperties" : {
+						"$ref" : "vcmi:bonus"
+					}
+				},
+				"targetModifier":{
+					"type": "object",
+					"additionalProperties": false,
+					"properties":{
+						"smart":{
+							"type": "boolean",
+							"description": "true: friendly/hostile based on positiveness; false: all targets"
+						}
+					}
+				}
+			}
+		}
+	},
+
+	"required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"],
+	"additionalProperties" : false,
+
+	"properties": {
+		"index":{
+				"type": "number",
+				"description": "numeric id of spell required only for original spells, prohibited for new spells"
+		},
+		"type":{
+				"type": "string",
+				"enum": ["adventure", "combat", "ability"],
+				"description":"Spell type"
+		},
+		"name":{
+				"type": "string",
+				"description": "Localizable name"
+
+		},
+		"school":{
+				"type": "object",
+				"description": "Spell schools",
+				"additionalProperties": false,
+
+				"properties":{
+
+						"air":{"type": "boolean"},
+						"fire":{"type": "boolean"},
+						"earth":{"type": "boolean"},
+						"water":{"type": "boolean"}
+				}
+
+		},
+		"level":{
+				"type": "number",
+				"description": "Spell level",
+				"minimum" : 0,
+				"maximum" : 5
+		},
+
+		"power":{
+				"type": "number",
+				"description": "Base power",
+		},
+
+		"defaultGainChance":{
+				"type": "number",
+				"description": "Gain chance by default for all factions"
+
+		},
+
+		"gainChance":{
+				"type": "object",
+				"description": "Chance in % to gain for faction. NOTE: this field is merged with faction config",
+				"additionalProperties" : {
+		 "type": "number",
+		 "minimum" : 0
+		}
+		},
+		"targetType":{
+				  "type": "string",
+				  "enum": ["NO_TARGET","CREATURE","OBSTACLE"]
+		},
+		"anim":{
+				"type": "number",
+				"description": "Main effect animation (AC format), -1 - none, deprecated",
+				"minimum": -1
+		},
+		"counters":{
+				 "$ref" : "#/definitions/flags",
+				 "description": "Flags structure ids of countering spells"
+		},
+		"flags":{
+				"type": "object",
+				"description": "Various flags",
+				"additionalProperties" : false,
+				"properties":{
+						"indifferent": {
+								"type":"boolean",
+								"description": "Spell is indifferent for target"
+						},
+						"negative": {
+								"type":"boolean",
+								"description": "Spell is negative for target"
+						},
+						"positive": {
+								"type":"boolean",
+								"description": "Spell is positive for target"
+						},
+						"damage": {
+								"type":"boolean",
+								"description": "Spell does damage (direct or indirect)"
+						},
+						"offensive": {
+								"type":"boolean",
+								"description": "Spell does direct damage (implicitly sets damage and negative)"
+						},
+						"rising":{
+								"type":"boolean",
+								"description": "Rising spell (implicitly sets positive)"
+						},
+						"special":{
+								"type": "boolean",
+								"description": "Special spell. Can be given only by Bonus::SPELL"
+						}
+				}
+		},
+		"immunity":{
+				"$ref" : "#/definitions/flags",
+				 "description": "flags structure of bonus names, any one of these bonus grants immunity"
+		},
+		"absoluteImmunity":{
+				 "$ref" : "#/definitions/flags",
+				 "description": "flags structure of bonus names. Any one of these bonus grants immunity, cant be negated"
+		},
+		"limit":{
+				 "$ref" : "#/definitions/flags",
+				 "description": "flags structure of bonus names, presence of all bonuses required to be affected by"
+		},
+
+		"graphics":{
+				 "type": "object",
+				 "additionalProperties" : false,
+				 "properties":{
+						 "iconImmune":{
+							  "type": "string",
+							  "description": "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)",
+							  "format" : "imageFile"
+						 },
+						 "iconScenarioBonus":{
+							  "type": "string",
+							  "description": "Resourse path of icon for scenario bonus" ,
+							  "format" : "imageFile"
+						 },
+						 "iconEffect":{
+							  "type": "string",
+							  "description": "Resourse path of icon for spell effects during battle" ,
+							  "format" : "imageFile"
+						 },
+						 "iconBook":{
+							  "type": "string",
+							  "description":"Resourse path of icon for spellbook" ,
+							  "format" : "imageFile"
+						 },
+						 "iconScroll":{
+							  "type": "string",
+							  "description": "Resourse path of icon for spell scrolls",
+							  "format": "imageFile"
+						 }
+				 }
+
+		},
+
+		"sounds":{
+			 "type": "object",
+			 "additionalProperties" : false,
+			 "properties":{
+					 "cast":{
+						  "type": "string",
+						  "description": "Resourse path of cast sound"
+					 }
+			 }
+		},
+
+		"levels":{
+			 "type": "object",
+			 "additionalProperties" : false,
+			 "required" : ["none", "basic", "advanced", "expert"],
+
+			 "properties":{
+				 "none":{
+					  "$ref" : "#/definitions/levelInfo"
+				 },
+				 "basic":{
+					  "$ref" : "#/definitions/levelInfo"
+				 },
+				 "advanced":{
+					  "$ref" : "#/definitions/levelInfo"
+				 },
+				 "expert":{
+					  "$ref" : "#/definitions/levelInfo"
+				 }
+			 }
+		}
+	}
+}

文件差異過大導致無法顯示
+ 204 - 64
config/spell_info.json


+ 78 - 70
lib/CBattleCallback.cpp

@@ -1617,10 +1617,13 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleIsImmune(const C
 
 		if (spell->isRisingSpell())
 		{
-			if (caster) //TODO: what with Archangels?
+			if(subject->count >= subject->baseAmount)
+				return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
+			
+			if (caster) //FIXME: Archangels can cast immune stack
 			{
 				auto maxHealth = calculateHealedHP (caster, spell, subject);
-				if (subject->count >= subject->baseAmount || maxHealth < subject->MaxHealth()) //must be able to rise at least one full creature
+				if (maxHealth < subject->MaxHealth()) //must be able to rise at least one full creature
 					return ESpellCastProblem::STACK_IMMUNE_TO_SPELL;
 			}
 		}
@@ -1637,13 +1640,20 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleIsImmune(const C
 	}
 	else //no target stack on this tile
 	{
-		if(spell->getTargetType() == CSpell::CREATURE
-			|| (spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE
-				&& mode == ECastingMode::HERO_CASTING //TODO why???
-				&& caster
-				&& caster->getSpellSchoolLevel(spell) < SecSkillLevel::EXPERT))
+		if(spell->getTargetType() == CSpell::CREATURE)
 		{
-			return ESpellCastProblem::WRONG_SPELL_TARGET;
+			if(caster && mode == ECastingMode::HERO_CASTING) //TODO why???
+			{
+				const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
+				
+				if(!ti.massive)
+					return ESpellCastProblem::WRONG_SPELL_TARGET;					
+			}
+			else
+			{
+ 				return ESpellCastProblem::WRONG_SPELL_TARGET;
+			}
+			
 		}
 	}
 
@@ -1688,6 +1698,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
 	{
 		bool allStacksImmune = true;
 		//we are interested only in enemy stacks when casting offensive spells
+		//TODO: should hero be able to cast non-smart negative spell if all enemy stacks are immune?
 		auto stacks = spell->isNegative() ? battleAliveStacks(!side) : battleAliveStacks();
 		for(auto stack : stacks)
 		{
@@ -1726,43 +1737,38 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
 	switch(spell->getTargetType())
 	{
 	case CSpell::CREATURE:
-	case CSpell::CREATURE_EXPERT_MASSIVE:
 		if(mode == ECastingMode::HERO_CASTING)
 		{
 			const CGHeroInstance * caster = battleGetFightingHero(side);
+			const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
 			bool targetExists = false;
 			for(const CStack * stack : battleGetAllStacks()) //dead stacks will be immune anyway
 			{
-				switch (spell->positiveness)
-				{
-				case CSpell::POSITIVE:
-					if(stack->owner == caster->getOwner())
+				bool immune = battleIsImmune(caster, spell, mode, stack->position) != ESpellCastProblem::OK;
+				bool casterStack = stack->owner == caster->getOwner();
+				
+				if(!immune)
+					switch (spell->positiveness)
 					{
-						if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
+					case CSpell::POSITIVE:
+						if(casterStack || !ti.smart)
 						{
 							targetExists = true;
 							break;
 						}
-					}
-					break;
-				case CSpell::NEUTRAL:
-					if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
-					{
-						targetExists = true;
 						break;
-					}
-					break;
-				case CSpell::NEGATIVE:
-					if(stack->owner != caster->getOwner())
-					{
-						if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
+					case CSpell::NEUTRAL:
+							targetExists = true;
+							break;
+						break;
+					case CSpell::NEGATIVE:
+						if(!casterStack || !ti.smart)
 						{
 							targetExists = true;
 							break;
 						}
+						break;
 					}
-					break;
-				}
 			}
 			if(!targetExists)
 			{
@@ -1787,31 +1793,32 @@ std::vector<BattleHex> CBattleInfoCallback::battleGetPossibleTargets(PlayerColor
 	switch(spell->getTargetType())
 	{
 	case CSpell::CREATURE:
-	case CSpell::CREATURE_EXPERT_MASSIVE:
 		{
 			const CGHeroInstance * caster = battleGetFightingHero(playerToSide(player)); //TODO
-
+			const CSpell::TargetInfo ti = spell->getTargetInfo(caster->getSpellSchoolLevel(spell));
+			
 			for(const CStack * stack : battleAliveStacks())
 			{
-				switch (spell->positiveness)
-				{
-				case CSpell::POSITIVE:
-					if(stack->owner == caster->getOwner())
-						if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
+				bool immune = battleIsImmune(caster, spell, mode, stack->position) != ESpellCastProblem::OK;
+				bool casterStack = stack->owner == caster->getOwner();
+				
+				if(!immune)
+					switch (spell->positiveness)
+					{
+					case CSpell::POSITIVE:
+						if(casterStack || ti.smart)
 							ret.push_back(stack->position);
-					break;
+						break;
 
-				case CSpell::NEUTRAL:
-					if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
+					case CSpell::NEUTRAL:
 						ret.push_back(stack->position);
-					break;
+						break;
 
-				case CSpell::NEGATIVE:
-					if(stack->owner != caster->getOwner())
-						if(battleIsImmune(caster, spell, mode, stack->position) == ESpellCastProblem::OK)
+					case CSpell::NEGATIVE:
+						if(!casterStack || ti.smart)
 							ret.push_back(stack->position);
-					break;
-				}
+						break;
+					}
 			}
 		}
 		break;
@@ -1909,7 +1916,7 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastThisSpell
 		if(deadStack && deadStack->owner != player) //you can resurrect only your own stacks //FIXME: it includes alive stacks as well
 			return ESpellCastProblem::NO_APPROPRIATE_TARGET;
 	}
-	else if(spell->getTargetType() == CSpell::CREATURE  ||  spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
+	else if(spell->getTargetType() == CSpell::CREATURE)
 	{
 		if(!aliveStack)
 			return ESpellCastProblem::NO_APPROPRIATE_TARGET;
@@ -2011,14 +2018,14 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
 	const auto attackedHexes = spell->rangeInHexes(destinationTile, skillLevel, attackerSide);
 	const bool onlyAlive = !spell->isRisingSpell(); //when casting resurrection or animate dead we should be allow to select dead stack
 
+	const CSpell::TargetInfo ti = spell->getTargetInfo(skillLevel);
 	//TODO: more generic solution for mass spells
-	if(spell->id == SpellID::DEATH_RIPPLE || spell->id == SpellID::DESTROY_UNDEAD || spell->id == SpellID::ARMAGEDDON)
+	if(spell->id == SpellID::DEATH_RIPPLE || spell->id == SpellID::DESTROY_UNDEAD )
 	{
 		for(const CStack *stack : battleGetAllStacks())
 		{
 			if((spell->id == SpellID::DEATH_RIPPLE && !stack->getCreature()->isUndead()) //death ripple
 				|| (spell->id == SpellID::DESTROY_UNDEAD && stack->getCreature()->isUndead()) //destroy undead
-				|| (spell->id == SpellID::ARMAGEDDON) //Armageddon
 				)
 			{
 				if(stack->isValidTarget())
@@ -2057,7 +2064,7 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
 			lightningHex = BattleHex::getClosestTile (stack->attackerOwned, destinationTile, possibleHexes);
 		}
 	}
-	else if (spell->range[skillLevel].size() > 1) //custom many-hex range
+	else if (spell->getLevelInfo(skillLevel).range.size() > 1) //custom many-hex range
 	{
 		for(BattleHex hex : attackedHexes)
 		{
@@ -2075,33 +2082,34 @@ std::set<const CStack*> CBattleInfoCallback::getAffectedCreatures(const CSpell *
 			}
 		}
 	}
-	else if(spell->getTargetType() == CSpell::CREATURE_EXPERT_MASSIVE)
+	else if(spell->getTargetType() == CSpell::CREATURE)
 	{
-		if(skillLevel < 3)  /*not expert */
+		if (ti.massive)
 		{
-			const CStack * st = battleGetStackByPos(destinationTile, onlyAlive);
-			if(st)
-				attackedCres.insert(st);
+			TStacks stacks = battleGetAllStacks();
+
+			vstd::erase_if(stacks,[&](const CStack * stack){
+				return ti.smart && spell->isNegative() && stack->owner == attackerOwner);
+			});
+
+			vstd::erase_if(stacks,[&](const CStack * stack){
+				return ti.smart && spell->isPositive() && stack->owner != attackerOwner);
+			});
+
+			vstd::erase_if(stacks,[&](const CStack * stack){
+				return !stack->isValidTarget(!onlyAlive);
+			});
+			
+			for (auto stack : stacks)
+				attackedCres.insert(stack);
+
 		}
 		else
 		{
-			for (auto stack : battleGetAllStacks())
-			{
-				/*if it's non negative spell and our unit or non positive spell and hostile unit */
-				if((!spell->isNegative() && stack->owner == attackerOwner)
-					||(!spell->isPositive() && stack->owner != attackerOwner )
-					)
-				{
-					if(stack->isValidTarget(!onlyAlive))
-						attackedCres.insert(stack);
-				}
-			}
-		} //if(caster->getSpellSchoolLevel(s) < 3)
-	}
-	else if(spell->getTargetType() == CSpell::CREATURE)
-	{
-		if(const CStack * st = battleGetStackByPos(destinationTile, onlyAlive))
-			attackedCres.insert(st);
+			if(const CStack * st = battleGetStackByPos(destinationTile, onlyAlive))
+				attackedCres.insert(st);			
+		}
+		
 	}
 	else //custom range from attackedHexes
 	{

+ 92 - 84
lib/CSpellHandler.cpp

@@ -23,7 +23,7 @@
 namespace SpellConfig
 {
     static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"};
-     
+
 }
 
 using namespace boost::assign;
@@ -130,12 +130,21 @@ namespace SRSLPraserHelpers
 	}
 }
 
-using namespace SRSLPraserHelpers;
+CSpell::LevelInfo::LevelInfo()
+    :description(""),cost(0),power(0),AIValue(0),smartTarget(true),range("0")
+{
+
+}
+
+CSpell::LevelInfo::~LevelInfo()
+{
+
+}
+
 
 CSpell::CSpell():
 	id(SpellID::NONE), level(0),
 	earth(false), water(false), fire(false), air(false),
-	power(0),
 	combatSpell(false), creatureAbility(false),
 	positiveness(ESpellPositiveness::NEUTRAL),
 	mainEffectAnim(-1),
@@ -143,18 +152,29 @@ CSpell::CSpell():
 	isRising(false), isDamage(false), isOffensive(false),
 	targetType(ETargetType::NO_TARGET)
 {
-
+    levels.resize(GameConstants::SPELL_SCHOOL_LEVELS);
 }
 
 CSpell::~CSpell()
 {
-	for(auto & elem : effects)
-		for(size_t j=0; j<elem.size(); j++)
-			delete elem[j];
 }
 
+const CSpell::LevelInfo & CSpell::getLevelInfo(const int level) const
+{
+	if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
+	{
+        logGlobal->errorStream() << __FUNCTION__ << " invalid school level " << level;
+        throw new std::runtime_error("Invalid school level");
+	}
+
+	return levels.at(level);
+}
+
+
 std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const
 {
+    using namespace SRSLPraserHelpers;
+
 	std::vector<BattleHex> ret;
 
 	if(id == SpellID::FIRE_WALL  ||  id == SpellID::FORCE_FIELD)
@@ -192,7 +212,7 @@ std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl,
 	}
 
 
-	std::string rng = range[schoolLvl] + ','; //copy + artificial comma for easier handling
+	std::string rng = getLevelInfo(schoolLvl).range + ','; //copy + artificial comma for easier handling
 
 	if(rng.size() >= 1 && rng[0] != 'X') //there is at lest one hex in range
 	{
@@ -201,7 +221,7 @@ std::vector<BattleHex> CSpell::rangeInHexes(BattleHex centralHex, ui8 schoolLvl,
 		bool readingFirst = true;
 		for(auto & elem : rng)
 		{
-			if(std::isdigit(elem) ) //reading numer
+			if(std::isdigit(elem) ) //reading number
 			{
 				if(readingFirst)
 					number1 += elem;
@@ -258,6 +278,20 @@ CSpell::ETargetType CSpell::getTargetType() const
 	return targetType;
 }
 
+const CSpell::TargetInfo CSpell::getTargetInfo(const int level) const
+{
+    TargetInfo info;
+
+    auto & levelInfo = getLevelInfo(level);
+
+    info.type = getTargetType();
+    info.smart = levelInfo.smartTarget;
+    info.massive = levelInfo.range == "X";
+
+    return info;
+}
+
+
 bool CSpell::isCombatSpell() const
 {
 	return combatSpell;
@@ -305,7 +339,7 @@ bool CSpell::isSpecialSpell() const
 
 bool CSpell::hasEffects() const
 {
-	return effects.size() && effects[0].size();
+	return !levels[0].effects.empty();
 }
 
 const std::string& CSpell::getIconImmune() const
@@ -322,12 +356,12 @@ const std::string& CSpell::getCastSound() const
 
 si32 CSpell::getCost(const int skillLevel) const
 {
-    return costs[skillLevel];
+    return getLevelInfo(skillLevel).cost;
 }
 
 si32 CSpell::getPower(const int skillLevel) const
 {
-    return powers[skillLevel];
+    return getLevelInfo(skillLevel).power;
 }
 
 //si32 CSpell::calculatePower(const int skillLevel) const
@@ -352,27 +386,20 @@ void CSpell::getEffects(std::vector<Bonus>& lst, const int level) const
         logGlobal->errorStream() << __FUNCTION__ << " invalid school level " << level;
 		return;
 	}
+
+    const std::vector<Bonus> & effects = levels[level].effects;
+
 	if(effects.empty())
-	{
-        logGlobal->errorStream() << __FUNCTION__ << " This spell ("  + name + ") has no bonus effects! " << name;
-		return;
-	}
-	if(effects.size() <= level)
-	{
-		logGlobal->errorStream() << __FUNCTION__ << " This spell ("  + name + ") is missing entry for level " << level;
-		return;
-	}
-	if(effects[level].empty())
     {
 		logGlobal->errorStream() << __FUNCTION__ << " This spell ("  + name + ") has no effects for level " << level;
 		return;
     }
 
-	lst.reserve(lst.size() + effects[level].size());
+	lst.reserve(lst.size() + effects.size());
 
-	for(Bonus *b : effects[level])
+	for(const Bonus & b : effects)
 	{
-		lst.push_back(Bonus(*b));
+		lst.push_back(Bonus(b));
 	}
 }
 
@@ -451,20 +478,6 @@ bool CSpell::isImmuneBy(const IBonusBearer* obj) const
 	return false;
 }
 
-void CSpell::setAttributes(const std::string& newValue)
-{
-	attributes = newValue;
-	if(attributes.find("CREATURE_TARGET_1") != std::string::npos
-		|| attributes.find("CREATURE_TARGET_2") != std::string::npos)
-		targetType = CREATURE_EXPERT_MASSIVE;
-	else if(attributes.find("CREATURE_TARGET") != std::string::npos)
-		targetType = CREATURE;
-	else if(attributes.find("OBSTACLE_TARGET") != std::string::npos)
-		targetType = OBSTACLE;
-	else
-		targetType = NO_TARGET;
-}
-
 void CSpell::setIsOffensive(const bool val)
 {
    isOffensive = val;
@@ -576,8 +589,6 @@ std::vector<JsonNode> CSpellHandler::loadLegacyData(size_t dataSize)
             else if(attributes.find("OBSTACLE_TARGET") != std::string::npos)
                 targetType = "OBSTACLE";
 
-            lineNode["targetType"].String() = targetType;
-
             //save parsed level specific data
             for(size_t i = 0; i < GameConstants::SPELL_SCHOOL_LEVELS; i++)
             {
@@ -588,6 +599,16 @@ std::vector<JsonNode> CSpellHandler::loadLegacyData(size_t dataSize)
                 level["aiValue"].Float() = AIVals[i];
             }
 
+            if(targetType == "CREATURE_EXPERT_MASSIVE")
+            {
+                lineNode["targetType"].String() = "CREATURE";
+                getLevel(3)["range"].String() = "X";
+            }
+            else
+            {
+                lineNode["targetType"].String() = targetType;
+            }
+
 		    legacyData.push_back(lineNode);
 
 
@@ -677,8 +698,6 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
 		spell->targetType = CSpell::CREATURE;
 	else if(targetType == "OBSTACLE")
 		spell->targetType = CSpell::OBSTACLE;
-	else if(targetType == "CREATURE_EXPERT_MASSIVE")
-		spell->targetType = CSpell::CREATURE_EXPERT_MASSIVE;
 
 
 	spell->mainEffectAnim = json["anim"].Float();
@@ -767,17 +786,17 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
 
 
     const JsonNode & graphicsNode = json["graphics"];
- 
+
 	spell->iconImmune = graphicsNode["iconImmune"].String();
 	spell->iconBook = graphicsNode["iconBook"].String();
 	spell->iconEffect = graphicsNode["iconEffect"].String();
 	spell->iconScenarioBonus = graphicsNode["iconScenarioBonus"].String();
 	spell->iconScroll = graphicsNode["iconScroll"].String();
-	
-	
-	
+
+
+
 	const JsonNode & soundsNode = json["sounds"];
-	
+
 	spell->castSound = soundsNode["cast"].String();
 
 
@@ -785,48 +804,37 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode& json)
 
     const int levelsCount = GameConstants::SPELL_SCHOOL_LEVELS;
 
-    spell->AIVals.resize(levelsCount);
-    spell->costs.resize(levelsCount);
-    spell->descriptions.resize(levelsCount);
-
-    spell->powers.resize(levelsCount);
-    spell->range.resize(levelsCount);
-
 	for(int levelIndex = 0; levelIndex < levelsCount; levelIndex++)
 	{
 		const JsonNode & levelNode = json["levels"][LEVEL_NAMES[levelIndex]];
-
-		spell->descriptions[levelIndex] = levelNode["description"].String();
-		spell->costs[levelIndex] = levelNode["cost"].Float();
-		spell->powers[levelIndex] = levelNode["power"].Float();
-		spell->AIVals[levelIndex] = levelNode["aiValue"].Float();
-
-		const JsonNode & effectsNode = levelNode["effects"];
-
-		if(!effectsNode.isNull())
+		
+		CSpell::LevelInfo & levelObject = spell->levels[levelIndex];
+
+		const si32 levelPower   = levelNode["power"].Float();		
+		
+		levelObject.description = levelNode["description"].String();
+		levelObject.cost        = levelNode["cost"].Float();
+		levelObject.power       = levelPower;
+		levelObject.AIValue     = levelNode["aiValue"].Float();
+		levelObject.smartTarget = levelNode["targetModifier"]["smart"].Bool();
+		
+		for(const auto & elem : levelNode["effects"].Struct())
 		{
-			if(spell->effects.empty())
-				spell->effects.resize(levelsCount);
+			const JsonNode & bonusNode = elem.second;
+			Bonus * b = JsonUtils::parseBonus(bonusNode);
+			const bool usePowerAsValue = bonusNode["val"].isNull();
 
-			for(const auto & elem : effectsNode.Struct())
-			{
-				const JsonNode & bonusNode = elem.second;
-				Bonus * b = JsonUtils::parseBonus(bonusNode);
-				const bool usePowerAsValue = bonusNode["val"].isNull();
+			//TODO: make this work. see CSpellHandler::afterLoadFinalization()
+			//b->sid = spell->id; //for all
 
-				//TODO: make this work. see CSpellHandler::afterLoadFinalization()
-				//b->sid = spell->id; //for all
-				
-				b->source = Bonus::SPELL_EFFECT;//for all
+			b->source = Bonus::SPELL_EFFECT;//for all
 
-				if(usePowerAsValue)
-				{
-					b->val = spell->powers[levelIndex];
-				}
+			if(usePowerAsValue)
+				b->val = levelPower;
 
-				spell->effects[levelIndex].push_back(b);
-			}
+			levelObject.effects.push_back(*b);
 		}
+		
 	}
 
     return spell;
@@ -836,9 +844,9 @@ void CSpellHandler::afterLoadFinalization()
 {
     //FIXME: it is a bad place for this code, should refactor loadFromJson to know object id during loading
 	for(auto spell: objects)
-		for(auto & level: spell->effects)
-			for(auto * bonus: level)
-				bonus->sid = spell->id;
+		for(auto & level: spell->levels)
+			for(auto & bonus: level.effects)
+				bonus.sid = spell->id;
 }
 
 

+ 55 - 33
lib/CSpellHandler.h

@@ -23,26 +23,49 @@ struct BattleHex;
 class DLL_LINKAGE CSpell
 {
 public:
-//    struct LevelInfo
-//    {
-//
-//    };
-//
-//    /** \brief Low level accessor. Don`t use it if absolutely necessary
-//     *
-//     * \param level. spell school level
-//     * \return Spell level info structure
-//     *
-//     */
-//    const LevelInfo& getLevelInfo(const int level);
+    struct LevelInfo
+    {
+        std::string description; //descriptions of spell for skill level
+        si32 cost; //per skill level: 0 - none, 1 - basic, etc
+        si32 power; //per skill level: 0 - none, 1 - basic, etc
+        si32 AIValue; //AI values: per skill level: 0 - none, 1 - basic, etc
+
+        bool smartTarget;
+        std::string range;
+
+        std::vector<Bonus> effects;
+
+        LevelInfo();
+        ~LevelInfo();
+
+        template <typename Handler> void serialize(Handler &h, const int version)
+        {
+            h & description & cost & power & AIValue & smartTarget & range & effects;
+        }
+    };
+
+    /** \brief Low level accessor. Don`t use it if absolutely necessary
+     *
+     * \param level. spell school level
+     * \return Spell level info structure
+     *
+     */
+    const CSpell::LevelInfo& getLevelInfo(const int level) const;
 public:
-	enum ETargetType {NO_TARGET, CREATURE, CREATURE_EXPERT_MASSIVE, OBSTACLE};
+	enum ETargetType {NO_TARGET, CREATURE, OBSTACLE};
 	enum ESpellPositiveness {NEGATIVE = -1, NEUTRAL = 0, POSITIVE = 1};
+
+	struct TargetInfo
+	{
+	    ETargetType type;
+	    bool smart;
+	    bool massive;
+	};
+
 	SpellID id;
 	std::string identifier; //???
 	std::string name;
-	std::string abbName; //abbreviated name
-	std::vector<std::string> descriptions; //descriptions of spell for skill levels: 0 - none, 1 - basic, etc
+
 	si32 level;
 	bool earth;
 	bool water;
@@ -55,7 +78,7 @@ public:
 	bool combatSpell; //is this spell combat (true) or adventure (false)
 	bool creatureAbility; //if true, only creatures can use this spell
 	si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative
-	std::vector<std::string> range; //description of spell's range in SRSL by magic school level
+
 	std::vector<SpellID> counteredSpells; //spells that are removed when effect of this spell is placed on creature (for bless-curse, haste-slow, and similar pairs)
 
 	CSpell();
@@ -63,7 +86,9 @@ public:
 
 	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)
 	si16 mainEffectAnim; //main spell effect animation, in AC format (or -1 when none)
-	ETargetType getTargetType() const;
+	ETargetType getTargetType() const; //deprecated
+
+	const CSpell::TargetInfo getTargetInfo(const int level) const;
 
 	bool isCombatSpell() const;
 	bool isAdventureSpell() const;
@@ -102,23 +127,25 @@ public:
 	* Returns resource name of icon for SPELL_IMMUNITY bonus
 	*/
 	const std::string& getIconImmune() const;
-	
+
 	const std::string& getCastSound() const;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & identifier & id & name & abbName & descriptions & level & earth & water & fire & air & power & costs
-			& powers & probabilities & AIVals & attributes & combatSpell & creatureAbility & positiveness & range & counteredSpells & mainEffectAnim;
+		h & identifier & id & name & level & earth & water & fire & air & power
+            & probabilities  & attributes & combatSpell & creatureAbility & positiveness & counteredSpells & mainEffectAnim;
 		h & isRising & isDamage & isOffensive;
 		h & targetType;
-		h & effects & immunities & limiters;
+		h & immunities & limiters;
 		h & iconImmune;
         h & absoluteImmunities & defaultProbability;
 
         h & isSpecial;
-        
-        h & castSound & iconBook & iconEffect & iconScenarioBonus & iconScroll ;
-		
+
+        h & castSound & iconBook & iconEffect & iconScenarioBonus & iconScroll;
+
+        h & levels;
+
 	}
 	friend class CSpellHandler;
 	friend class Graphics;
@@ -126,13 +153,9 @@ public:
 private:
     void setIsOffensive(const bool val);
     void setIsRising(const bool val);
-    void setAttributes(const std::string& newValue);
 
 private:
     si32 defaultProbability;
-	std::vector<si32> costs; //per skill level: 0 - none, 1 - basic, etc
-	std::vector<si32> powers; //per skill level: 0 - none, 1 - basic, etc
-	std::vector<si32> AIVals; //AI values: per skill level: 0 - none, 1 - basic, etc
 
 	bool isRising;
 	bool isDamage;
@@ -141,11 +164,8 @@ private:
 
 	std::string attributes; //reference only attributes //todo: remove or include in configuration format, currently unused
 
-
-
 	ETargetType targetType;
 
-	std::vector<std::vector<Bonus *> > effects; // [level 0-3][list of effects]
 	std::vector<Bonus::BonusType> immunities; //any of these grants immunity
 	std::vector<Bonus::BonusType> absoluteImmunities; //any of these grants immunity, cant be negated
 	std::vector<Bonus::BonusType> limiters; //all of them are required to be affected
@@ -153,14 +173,16 @@ private:
 	///graphics related stuff
 
 	std::string iconImmune;
-	
+
 	std::string iconBook;
 	std::string iconEffect;
-	std::string iconScenarioBonus;	
+	std::string iconScenarioBonus;
 	std::string iconScroll;
 
 	///sound related stuff
 	std::string castSound;
+
+	std::vector<LevelInfo> levels;
 };
 
 

+ 1 - 1
lib/Connection.h

@@ -28,7 +28,7 @@
 #include "mapping/CCampaignHandler.h" //for CCampaignState
 #include "rmg/CMapGenerator.h" // for CMapGenOptions
 
-const ui32 version = 747;
+const ui32 version = 748;
 const ui32 minSupportedVersion = version;
 
 class CConnection;

+ 40 - 40
server/CGameHandler.cpp

@@ -4082,50 +4082,50 @@ void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex
 
 	if (spell->isOffensiveSpell())
 	{
-			int spellDamage = 0;
-			if (stack && mode != ECastingMode::MAGIC_MIRROR)
+		int spellDamage = 0;
+		if (stack && mode != ECastingMode::MAGIC_MIRROR)
+		{
+			int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum());
+			if (unitSpellPower)
+				sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities
+			else //Faerie Dragon
 			{
-				int unitSpellPower = stack->valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spellID.toEnum());
-				if (unitSpellPower)
-					sc.dmgToDisplay = spellDamage = stack->count * unitSpellPower; //TODO: handle immunities
-				else //Faerie Dragon
-				{
-					usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100;
-					sc.dmgToDisplay = 0;
-				}
+				usedSpellPower = stack->valOfBonuses(Bonus::CREATURE_SPELL_POWER) * stack->count / 100;
+				sc.dmgToDisplay = 0;
 			}
-			int chainLightningModifier = 0;
-			for(auto & attackedCre : attackedCres)
-			{
-				if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell
-					continue;
+		}
+		int chainLightningModifier = 0;
+		for(auto & attackedCre : attackedCres)
+		{
+			if(vstd::contains(sc.resisted, (attackedCre)->ID)) //this creature resisted the spell
+				continue;
 
-				BattleStackAttacked bsa;
-				if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->range.at(spellLvl) == "X" || mode == ECastingMode::ENCHANTER_CASTING))
-					//display effect only upon primary target of area spell
-					//FIXME: if no stack is attacked, ther eis no animation and interface freezes
-				{
-					bsa.flags |= BattleStackAttacked::EFFECT;
-					bsa.effect = spell->mainEffectAnim;
-				}
-				if (spellDamage)
-					bsa.damageAmount = spellDamage >> chainLightningModifier;
-				else
-					bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier;
+			BattleStackAttacked bsa;
+			if ((destination > -1 && (attackedCre)->coversPos(destination)) || (spell->getLevelInfo(spellLvl).range == "X" || mode == ECastingMode::ENCHANTER_CASTING))
+				//display effect only upon primary target of area spell
+				//FIXME: if no stack is attacked, there is no animation and interface freezes
+			{
+				bsa.flags |= BattleStackAttacked::EFFECT;
+				bsa.effect = spell->mainEffectAnim;
+			}
+			if (spellDamage)
+				bsa.damageAmount = spellDamage >> chainLightningModifier;
+			else
+				bsa.damageAmount = gs->curB->calculateSpellDmg(spell, caster, attackedCre, spellLvl, usedSpellPower) >> chainLightningModifier;
 
-				sc.dmgToDisplay += bsa.damageAmount;
+			sc.dmgToDisplay += bsa.damageAmount;
 
-				bsa.stackAttacked = (attackedCre)->ID;
-				if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
-					bsa.attackerID = stack->ID;
-				else
-					bsa.attackerID = -1;
-				(attackedCre)->prepareAttacked(bsa);
-				si.stacks.push_back(bsa);
+			bsa.stackAttacked = (attackedCre)->ID;
+			if (mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
+				bsa.attackerID = stack->ID;
+			else
+				bsa.attackerID = -1;
+			(attackedCre)->prepareAttacked(bsa);
+			si.stacks.push_back(bsa);
 
-				if (spellID == SpellID::CHAIN_LIGHTNING)
-					++chainLightningModifier;
-			}
+			if (spellID == SpellID::CHAIN_LIGHTNING)
+				++chainLightningModifier;
+		}
 	}
 	
 	if (spell->hasEffects())
@@ -4456,7 +4456,7 @@ void CGameHandler::handleSpellCasting( SpellID spellID, int spellLvl, BattleHex
 	}
 
 	//Magic Mirror effect
-	if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->range.at(0) == "0") //it is actual spell and can be reflected to single target, no recurrence
+	if (spell->isNegative() && mode != ECastingMode::MAGIC_MIRROR && spell->level && spell->getLevelInfo(0).range == "0") //it is actual spell and can be reflected to single target, no recurrence
 	{
 		for(auto & attackedCre : attackedCres)
 		{
@@ -5354,7 +5354,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta
 void CGameHandler::handleAttackBeforeCasting (const BattleAttack & bat)
 {
 	const CStack * attacker = gs->curB->battleGetStackByID(bat.stackAttacking);
-	attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no detah stare / acid bretah needed?
+	attackCasting(bat, Bonus::SPELL_BEFORE_ATTACK, attacker); //no death stare / acid breath needed?
 }
 
 void CGameHandler::handleAfterAttackCasting( const BattleAttack & bat )

部分文件因文件數量過多而無法顯示