Browse Source

Fix configs, update docs, resolve discovered regressions

Ivan Savenko 4 months ago
parent
commit
022b0f731c

+ 24 - 4
config/bonuses.json

@@ -85,6 +85,10 @@
 	"ENEMY_DEFENCE_REDUCTION":
 	{
 	},
+	
+	"FEARFUL":
+	{
+	},
 
 	"FIRE_SHIELD":
 	{
@@ -112,6 +116,14 @@
 	"FREE_SHOOTING":
 	{
 	},
+	
+	"FULL_MAP_DARKNESS":
+	{
+	},
+	
+	"FULL_MAP_SCOUTING":
+	{
+	},
 
 	"GARGOYLE":
 	{
@@ -237,10 +249,6 @@
 		"creatureNature" : true
 	},
 
-	"MECHANICAL":
-	{
-	},
-
 	"OPENING_BATTLE_SPELL":
 	{
 	},
@@ -278,6 +286,10 @@
 	{
 		"creatureNature" : true
 	},
+	
+	"SKELETON_TRANSFORMER_TARGET":
+	{
+	},
 
 	"SHOOTER":
 	{
@@ -355,6 +367,10 @@
 	{
 	},
 
+	"TRANSMUTATION_IMMUNITY":
+	{
+	},
+
 	"UNDEAD":
 	{
 		"creatureNature" : true,
@@ -369,6 +385,10 @@
 		"hidden": true
 	},
 	
+	"VULNERABLE_FROM_BACK":
+	{
+	},
+	
 	"WIDE_BREATH":
 	{
 	},

+ 7 - 1
config/creatures/neutral.json

@@ -94,7 +94,13 @@
 				"val" : 10,
 				"propagator": "BATTLE_WIDE",
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
-				"limiters" : [ "OPPOSITE_SIDE", "LIVING" ]
+				"limiters" : [ 
+					"OPPOSITE_SIDE", 
+					{
+						"type" : "HAS_ANOTHER_BONUS_LIMITER",
+						"parameters" : [ "LIVING" ]
+					}
+				]
 			},
 			"fearfulImmune" :
 			{

+ 2 - 2
config/gameConfig.json

@@ -317,7 +317,7 @@
 			/// movement points hero can get on start of the turn when on land, depending on speed of slowest creature (0-based list)
 			"movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ],
 			/// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list)
-			"movementPointsSea" : [ 1500 ]
+			"movementPointsSea" : [ 1500 ],
 			
 			/// Base scouting range for hero without any range modifiers
 			"baseScoutingRange" : 5,
@@ -350,7 +350,7 @@
 			"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ],
 			
 			// Base scouting range for town without any range modifiers
-			"baseScoutingRange" : 5,
+			"baseScoutingRange" : 5
 		},
 
 		"combat":

+ 2 - 1
config/heroes/castle.json

@@ -94,7 +94,8 @@
 			{ "skill" : "artillery", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"tyris":

+ 2 - 1
config/heroes/dungeon.json

@@ -24,7 +24,8 @@
 			{ "skill" : "offence", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"dace":

+ 2 - 1
config/heroes/fortress.json

@@ -108,7 +108,8 @@
 			{ "skill" : "artillery", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"broghild":

+ 2 - 1
config/heroes/inferno.json

@@ -98,7 +98,8 @@
 			{ "skill" : "logistics", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"nymus":

+ 2 - 1
config/heroes/stronghold.json

@@ -24,7 +24,8 @@
 			{ "skill" : "artillery", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"jabarkas":

+ 2 - 1
config/heroes/tower.json

@@ -71,7 +71,8 @@
 			{ "skill" : "tactics", "level": "basic" }
 		],
 		"specialty" : {
-			"creature" : "ballista"
+			"creature" : "ballista",
+			"creatureLevel" : 5
 		}
 	},
 	"fafner":

+ 26 - 16
config/schemas/bonus.json

@@ -10,6 +10,32 @@
 			"type" : "boolean",
 			"description" : "If set to true, all instances of this bonus will be hidden in UI"
 		},
+
+		"creatureNature" : {
+			"type" : "boolean",
+			"description" : "If set to true, this bonus will be considered 'creature nature' bonus, and such creature won't be automatically granted LIVING bonus"
+		},
+
+		"description" : {
+			"type" : "string"
+		},
+
+		"subtypeDescriptions" : {
+			"type" : "object",
+			"description" : "Custom description string for bonus subtype",
+			"additionalProperties" : {
+				"type" : "string"
+			}
+		},
+
+		"valueDescriptions" : {
+			"type" : "object",
+			"description" : "Custom description string for bonus value",
+			"additionalProperties" : {
+				"type" : "string"
+			}
+		},
+
 		"graphics" : {
 			"type" : "object",
 			"additionalProperties" : false,
@@ -38,22 +64,6 @@
 					}
 				}
 			}
-		},
-
-		"subtypeDescriptions" : {
-			"type" : "object",
-			"description" : "Custom description string for bonus subtype",
-			"additionalProperties" : {
-				"type" : "string"
-			}
-		},
-		
-		"valueDescriptions" : {
-			"type" : "object",
-			"description" : "Custom description string for bonus value",
-			"additionalProperties" : {
-				"type" : "string"
-			}
 		}
 	}
 }

+ 74 - 48
config/schemas/bonusInstance.json

@@ -37,6 +37,78 @@
 					}
 				}
 			]
+		},
+		"updater" : 
+		{
+			"anyOf" : [
+				{
+					"type" : "string",
+					"enum" : [ "TIMES_HERO_LEVEL", "TIMES_STACK_LEVEL", "DIVIDE_STACK_LEVEL", "BONUS_OWNER_UPDATER", "TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL" ]
+				},
+				{
+					"description" : "GROWS_WITH_LEVEL updater",
+					"type" : "object",
+					"required" : ["type", "valPer20", "stepSize"],
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"const" : "GROWS_WITH_LEVEL",
+						},
+						"valPer20" : {
+							"type" : "integer",
+							"description" : "Bonus value for each 20 steps"
+						},
+						"stepSize" : {
+							"type" : "integer",
+							"minimum" : 1,
+							"description" : "Size of each step, in levels"
+						}
+					}
+				},
+				{
+					"description" : "TIMES_HERO_LEVEL updater",
+					"type" : "object",
+					"required" : ["type"],
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"const" : "TIMES_HERO_LEVEL",
+						},
+						"stepSize" : {
+							"type" : "integer",
+							"minimum" : 1,
+							"description" : "Size of each step, in levels"
+						}
+					}
+				},
+				{
+					"description" : "TIMES_STACK_SIZE updater",
+					"type" : "object",
+					"required" : ["type"],
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"const" : "TIMES_STACK_SIZE",
+						},
+						"stepSize" : {
+							"type" : "integer",
+							"minimum" : 1,
+							"description" : "Size of each step, in levels"
+						},
+						"minimum" : {
+							"type" : "integer",
+							"description" : "Minimal bonus value"
+						},
+						"maximum" : {
+							"type" : "integer",
+							"description" : "Maximal bonus value"
+						}
+					}
+				}
+			]
 		}
 	},
 	"additionalProperties" : false,
@@ -69,56 +141,10 @@
 			"enum" : [ "BATTLE_WIDE", "VISITED_TOWN_AND_VISITOR", "PLAYER_PROPAGATOR", "HERO", "TEAM_PROPAGATOR", "GLOBAL_EFFECT" ]
 		},
 		"updater" : {
-			"anyOf" : [
-				{
-					"type" : "string",
-					"enum" : [ "TIMES_HERO_LEVEL", "TIMES_STACK_LEVEL", "DIVIDE_STACK_LEVEL", "BONUS_OWNER_UPDATER", "TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL" ]
-				},
-				{
-					"description" : "updater",
-					"type" : "object",
-					"required" : ["type", "parameters"],
-					"additionalProperties" : false,
-					"properties" : {
-						"type" : {
-							"type" : "string",
-							"enum" : [ "GROWS_WITH_LEVEL" ],
-							"description" : "type"
-						},
-						"parameters" : {
-							"type" : "array",
-							"description" : "parameters",
-							"additionalItems" : true
-						}
-					}
-				}
-			]
+			"$ref" : "#/definitions/updater"
 		},
 		"propagationUpdater" : {
-			"anyOf" : [
-				{
-					"type" : "string",
-					"enum" : [ "TIMES_HERO_LEVEL", "TIMES_STACK_LEVEL", "ARMY_MOVEMENT", "BONUS_OWNER_UPDATER" ]
-				},
-				{
-					"description" : "propagationUpdater",
-					"type" : "object",
-					"required" : ["type", "parameters"],
-					"additionalProperties" : false,
-					"properties" : {
-						"type" : {
-							"type" : "string",
-							"enum" : [ "GROWS_WITH_LEVEL", "ARMY_MOVEMENT" ],
-							"description" : "type"
-						},
-						"parameters" : {
-							"type" : "array",
-							"description" : "parameters",
-							"additionalItems" : true
-						}
-					}
-				}
-			]
+			"$ref" : "#/definitions/updater"
 		},
 		"limiters" : {
 			"$ref" : "#/definitions/nestedLimiter",

+ 6 - 2
config/schemas/gameSettings.json

@@ -47,7 +47,10 @@
 				"minimalPrimarySkills" :      { "type" : "array" },
 				"movementCostBase"  :         { "type" : "number" },
 				"movementPointsLand" :        { "type" : "array" },
-				"movementPointsSea" :         { "type" : "array" }
+				"movementPointsSea" :         { "type" : "array" },
+				"specialtyCreatureGrowth" :    { "type" : "number" },
+				"specialtySecondarySkillGrowth" : { "type" : "number" },
+				"baseScoutingRange" :         { "type" : "number" }
 			}
 		},
 		"towns" : {
@@ -59,7 +62,8 @@
 				"spellResearch" :                        { "type" : "boolean" },
 				"spellResearchCost" :                    { "type" : "array" },
 				"spellResearchPerDay" :                  { "type" : "array" },
-				"spellResearchCostExponentPerResearch" : { "type" : "array" }
+				"spellResearchCostExponentPerResearch" : { "type" : "array" },
+				"baseScoutingRange" :                    { "type" : "number" }
 			}
 		},
 		"combat": {

+ 8 - 0
config/schemas/hero.json

@@ -136,6 +136,14 @@
 				"secondary" : {
 					"type" : "string",
 					"description" : "Shortcut for defining secondary skill specialty, using standard H3 rules."
+				},
+				"creatureLevel" : {
+					"type" : "integer",
+					"description" : "Assumed creature level for creature specialty"
+				},
+				"stepSize" : {
+					"type" : "integer",
+					"description" : "How creature or secondary skill specialty should grow per each step. Default is 5"
 				}
 			}
 		},

+ 4 - 0
config/schemas/mod.json

@@ -274,6 +274,10 @@
 			"description" : "List of configuration files for artifacts",
 			"$ref" : "#/definitions/fileListOrObject"
 		},
+		"spellSchools" : {
+			"description" : "List of configuration files for spell schools",
+			"$ref" : "#/definitions/fileListOrObject"
+		},
 		"spells" : {
 			"description" : "List of configuration files for spells",
 			"$ref" : "#/definitions/fileListOrObject"

+ 18 - 13
docs/modders/Bonus/Bonus_Types.md

@@ -126,6 +126,12 @@ Restores specific percentage of mana pool for affected heroes on new turn. If he
 
 - val: percentage of spell points to restore
 
+### SKELETON_TRANSFORMER_TARGET
+
+Unit affected by this bonus will be transformed into creature other than Skeleton when placed into Skeleton Transformer
+
+- subtype: type of creature to which this unit should be converted
+
 ### NONEVIL_ALIGNMENT_MIX
 
 Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale (Angelic Alliance effect)
@@ -149,6 +155,7 @@ If the hero has no free space for the target creature but has space for its upgr
 - addInfo: Requried total level of Necromancer power for this bonus to be active (val of all bonuses of this type)
 
 Example (from Cloak Of The Undead King):
+
 ```json
 {
     "type" : "IMPROVED_NECROMANCY",
@@ -445,7 +452,7 @@ Creature nature bonus. Affected unit is considered to not be alive and not affec
 
 ### MECHANICAL
 
-Creature nature bonus. Affected unit is considered to not be alive and not affected by morale and certain spells but should be repairable from engineers (factory).
+Creature nature bonus. Affected unit is considered to not be alive and not affected by morale and certain spells but should be repairable from engineers (HotA Factory).
 
 ### GARGOYLE
 
@@ -453,7 +460,7 @@ Creature nature bonus. Affected unit is considered to be a gargoyle and not affe
 
 ### UNDEAD
 
-Creature nature bonus. Affected unit is considered to be undead, which makes it immune to many effects, and also reduce morale of allied living units.
+Creature nature bonus. Affected unit is considered to be undead, which makes it immune to many effects, not affected by morale, and also reduce morale of allied living units.
 
 ### SIEGE_WEAPON
 
@@ -463,7 +470,7 @@ Creature nature bonus. Affected unit is considered to be a siege machine and can
 
 ### DRAGON_NATURE
 
-Affected unit is dragon. This bonus proved no effect, but is used as limiter several effects.
+Affected unit is dragon. This bonus provides no effects on its own, but is used as limiter for Mutare specialty
 
 ### KING
 
@@ -471,10 +478,6 @@ Affected unit will take more damage from units under Slayer spell effect
 
 - val: required skill mastery of Slayer spell to affect this unit
 
-### FEARLESS
-
-Affected unit is immune to Fear ability
-
 ### NO_LUCK
 
 Affected units can not receive good or bad luck
@@ -525,7 +528,7 @@ Affected unit will deal more damage based on movement distance (Champions)
 
 ### VULNERABLE_FROM_BACK
 
-Affected unit will receive more damage when attacked from behind. Attacked unit will not turn around to face the attacker
+When affected unit is attacked from behind, it will receive more damage when attacked and will not turn around to face the attacker
 
 - val: additional damage for attacks from behind, percentage (0-100)
 
@@ -681,6 +684,10 @@ Affected units have chance to transform attacked unit to other creature type
   - transmutationPerUnit: transformed unit will have same number of units as original stack
 - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker
 
+### TRANSMUTATION_IMMUNITY
+
+Affected unit is immune to TRANSMUTATION bonus effects
+
 ### SUMMON_GUARDIANS
 
 When battle starts, affected units will be surrounded from all side with summoned units
@@ -817,7 +824,9 @@ Affected unit has chance to deal double damage on attack (Death Knight)
 
 ### FEAR
 
-If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance (Azure Dragon). Blocked by FEARLESS bonus.
+Units affected by this bonus have a chance to skip their turn and freeze in fear (Azure Dragon).
+
+- val: chance to trigger, percentage
 
 ### HEALER
 
@@ -1104,10 +1113,6 @@ Dummy bonus that acts as marker for Dendroid's Bind ability
 
 - addInfo: ID of stack that have bound the unit
 
-### SYNERGY_TARGET
-
-Dummy skill for alternative upgrades mod
-
 ### THIEVES_GUILD_ACCESS
 
 Increases amount of information available in affected thieves guild (in town or in adventure map tavern). Does not affects adventure map object "Den of Thieves". You may want to use PLAYER_PROPAGATOR with this bonus to make its effect player wide.

+ 53 - 0
docs/modders/Entities_Format/Bonus_Types_Format.md

@@ -0,0 +1,53 @@
+# Battlefield Format
+
+WARNING: currently custom bonus types can only be used for custom "traits", for example to use them in limiters. At the moment it is not possible to provide custom mechanics for such bonus
+
+```json
+{
+	// If set to true, this bonus will be hidden in creature view
+	"hidden" : false,
+
+	// If set to true, this bonus will be considered a "creature nature" bonus
+	// If creature has no creature nature bonuses, it is considered to be a LIVING creature
+	"creatureNature" : false,
+	
+	// Generic human-readable description of this bonus
+	// Visible in creature window
+	// Can be overriden in creature abilities or artifact bonuses
+	"description" : "{Bonus Name}\nBonus description",
+	
+	"graphics" : {
+		// Generic icon of this bonus
+		// Visible in creature window
+		// Can be overriden in creature abilities or artifact bonuses
+		"icon" : "path/to/icon.png",
+		
+		// Custom icons for specific subtypes of this bonus
+		"subtypeIcons" : {
+			"spellSchool.air" : "",
+			"spellSchool.water" : "",
+		},
+		
+		// Custom icons for specific values of this bonus
+		// Note that values must be strings and wrapped in quotes
+		"valueIcons" : {
+			"1" : "",
+			"2" : "",
+		}
+	},
+	
+	// Custom descriptions for specific subtypes of this bonus
+	"subtypeDescriptions" : {
+		"spellSchool.air" : ""
+		"spellSchool.water" : "",
+	},
+	
+	// Custom descriptions for specific values of this bonus
+	// Note that values must be strings and wrapped in quotes
+	"valueDescriptions" : {
+			"1" : ""
+			"2" : ""
+		}
+	}
+}
+```

+ 16 - 0
docs/modders/Entities_Format/Spell_School_Format.md

@@ -0,0 +1,16 @@
+# Spell School Format
+
+WARNING: currently custom spell schools are only partially supported:
+
+- it is possible to use custom spell schools in bonus system
+- it is possible to make skill for specializing in such spell
+- it is possible to specify border decorations for mastery level of such spell in spellbook
+- it is NOT possible to add "bookmark" filter for spellbook for spells of such school
+
+```json
+	// Internal field for H3 schools. Do not use for mods
+	"index" : "",
+	
+	// animation file with spell borders for spell mastery levels
+	"schoolBorders" : "SplevA"
+```

+ 1 - 1
lib/CCreatureHandler.cpp

@@ -8,9 +8,9 @@
  *
  */
 #include "StdInc.h"
-#include "CBonusTypeHandler.h"
 #include "CCreatureHandler.h"
 
+#include "CBonusTypeHandler.h"
 #include "ResourceSet.h"
 #include "entities/faction/CFaction.h"
 #include "entities/faction/CTownHandler.h"

+ 5 - 6
lib/CSkillHandler.cpp

@@ -9,20 +9,19 @@
  */
 
 #include "StdInc.h"
+#include "CSkillHandler.h"
 
 #include <cctype>
 
-#include "CSkillHandler.h"
-#include "IGameSettings.h"
+#include "GameLibrary.h"
 #include "bonuses/Updaters.h"
 #include "constants/StringConstants.h"
 #include "filesystem/Filesystem.h"
-#include "json/JsonBonus.h"
-#include "json/JsonUtils.h"
 #include "modding/IdentifierStorage.h"
 #include "texts/CGeneralTextHandler.h"
 #include "texts/CLegacyConfigParser.h"
-#include "GameLibrary.h"
+#include "json/JsonBonus.h"
+#include "json/JsonUtils.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -259,7 +258,7 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 		if (bonusNode.isStruct())
 		{
 			auto bonus = JsonUtils::parseBonus(bonusNode);
-			bonus->val = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH);
+			bonus->val = 0; // set by HeroHandler on specialty load
 			bonus->updater = std::make_shared<TimesHeroLevelUpdater>();
 			bonus->valType = BonusValueType::PERCENT_TO_TARGET_TYPE;
 			bonus->targetSourceType = BonusSource::SECONDARY_SKILL;

+ 1 - 1
lib/bonuses/BonusCache.cpp

@@ -17,7 +17,7 @@
 
 #include "../GameLibrary.h"
 #include "../IGameSettings.h"
-#include "spells/SpellSchoolHandler.h"
+#include "../spells/SpellSchoolHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 0 - 1
lib/bonuses/Limiters.cpp

@@ -34,7 +34,6 @@ const std::map<std::string, TLimiterPtr> bonusLimiterMap =
 	{"SHOOTER_ONLY", std::make_shared<HasAnotherBonusLimiter>(BonusType::SHOOTER)},
 	{"DRAGON_NATURE", std::make_shared<HasAnotherBonusLimiter>(BonusType::DRAGON_NATURE)},
 	{"IS_UNDEAD", std::make_shared<HasAnotherBonusLimiter>(BonusType::UNDEAD)},
-	{"LIVING", std::make_shared<HasAnotherBonusLimiter>(BonusType::LIVING)},
 	{"CREATURE_NATIVE_TERRAIN", std::make_shared<CreatureTerrainLimiter>()},
 	{"CREATURE_FACTION", std::make_shared<AllOfLimiter>(std::initializer_list<TLimiterPtr>{std::make_shared<CreatureLevelLimiter>(), std::make_shared<FactionLimiter>()})},
 	{"SAME_FACTION", std::make_shared<FactionLimiter>()},

+ 0 - 1
lib/constants/EntityIdentifiers.h

@@ -816,7 +816,6 @@ public:
 		IMP = 42, // for Deity of Fire
 		FAMILIAR = 43, // for Deity of Fire
 		SKELETON = 56, // for Skeleton Transformer
-		BONE_DRAGON = 68, // for Skeleton Transformer
 		TROGLODYTES = 70, // for Abandoned Mine
 		MEDUSA = 76, // for Siege UI workaround
 		AIR_ELEMENTAL = 112, // for tests

+ 36 - 12
lib/entities/hero/CHeroHandler.cpp

@@ -140,13 +140,23 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const
 	}
 }
 
-/// creates standard H3 hero specialty for creatures
-std::vector<std::shared_ptr<Bonus>> CHeroHandler::createCreatureSpecialty(CreatureID cid) const
+std::vector<std::shared_ptr<Bonus>> CHeroHandler::createCreatureSpecialty(CreatureID cid, int fixedLevel, int growthPerStep) const
 {
 	std::vector<std::shared_ptr<Bonus>> result;
-
 	const auto & specCreature = *cid.toCreature();
-	int level = specCreature.hasBonusOfType(BonusType::SIEGE_WEAPON) ? 5 : specCreature.getLevel();
+
+	if (fixedLevel == 0)
+		fixedLevel = specCreature.getLevel();
+
+	if (fixedLevel == 0)
+	{
+		fixedLevel = 5;
+		logMod->warn("Creature '%s' of level 0 has hero with generic specialty! Please specify level explicitly or give creature non-zero level", specCreature.getJsonKey());
+	}
+
+	if (growthPerStep == 0)
+		growthPerStep = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_SPECIALTY_CREATURE_GROWTH);
+
 	{
 		auto bonus = std::make_shared<Bonus>();
 		bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, true));
@@ -160,17 +170,17 @@ std::vector<std::shared_ptr<Bonus>> CHeroHandler::createCreatureSpecialty(Creatu
 		auto bonus = std::make_shared<Bonus>();
 		bonus->type = BonusType::PRIMARY_SKILL;
 		bonus->subtype = BonusSubtypeID(skill);
-		bonus->val = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_SPECIALTY_CREATURE_GROWTH);
+		bonus->val = growthPerStep;
 		bonus->valType = BonusValueType::PERCENT_TO_TARGET_TYPE;
 		bonus->targetSourceType = BonusSource::CREATURE_ABILITY;
 		bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, true));
-		bonus->updater.reset(new TimesHeroLevelUpdater(level));
+		bonus->updater.reset(new TimesHeroLevelUpdater(fixedLevel));
 		result.push_back(bonus);
 	}
 	return result;
 }
 
-std::vector<std::shared_ptr<Bonus>> CHeroHandler::createSecondarySkillSpecialty(SecondarySkill skillID) const
+std::vector<std::shared_ptr<Bonus>> CHeroHandler::createSecondarySkillSpecialty(SecondarySkill skillID, int growthPerStep) const
 {
 	std::vector<std::shared_ptr<Bonus>> result;
 	const auto & skillPtr = LIBRARY->skillh->objects[skillID.getNum()];
@@ -178,8 +188,15 @@ std::vector<std::shared_ptr<Bonus>> CHeroHandler::createSecondarySkillSpecialty(
 	if (skillPtr->specialtyTargetBonuses.empty())
 		logMod->warn("Skill '%s' does not supports generic specialties!", skillPtr->getJsonKey());
 
+	if (growthPerStep == 0)
+		growthPerStep = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH);
+
 	for (const auto & bonus : skillPtr->specialtyTargetBonuses)
-		result.push_back(std::make_shared<Bonus>(*bonus));
+	{
+		auto bonusCopy = std::make_shared<Bonus>(*bonus);
+		bonusCopy->val = growthPerStep;
+		result.push_back(bonusCopy);
+	}
 
 	return result;
 }
@@ -228,9 +245,13 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) const
 	//creature specialty - alias for simplicity
 	if(!specialtyNode["creature"].isNull())
 	{
-		LIBRARY->identifiers()->requestIdentifier("creature", specialtyNode["creature"], [this, hero, prepSpec](si32 creature)
+		const JsonNode & creatureNode = specialtyNode["creature"];
+		int targetLevel = specialtyNode["creatureLevel"].Integer();
+		int stepSize = specialtyNode["stepSize"].Integer();
+
+		LIBRARY->identifiers()->requestIdentifier("creature", creatureNode, [this, hero, prepSpec, targetLevel, stepSize](si32 creature)
 		{
-			for (const auto & bonus : createCreatureSpecialty(CreatureID(creature)))
+			for (const auto & bonus : createCreatureSpecialty(CreatureID(creature), targetLevel, stepSize))
 				hero->specialty.push_back(prepSpec(bonus));
 		});
 	}
@@ -238,9 +259,12 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) const
 	//secondary skill specialty - alias for simplicity
 	if(!specialtyNode["secondary"].isNull())
 	{
-		LIBRARY->identifiers()->requestIdentifier("secondarySkill", specialtyNode["secondary"], [this, hero, prepSpec](si32 creature)
+		const JsonNode & skillNode = specialtyNode["secondary"];
+		int stepSize = specialtyNode["stepSize"].Integer();
+
+		LIBRARY->identifiers()->requestIdentifier("secondarySkill", skillNode, [this, hero, prepSpec, stepSize](si32 creature)
 		{
-			for (const auto & bonus : createSecondarySkillSpecialty(SecondarySkill(creature)))
+			for (const auto & bonus : createSecondarySkillSpecialty(SecondarySkill(creature), stepSize))
 				hero->specialty.push_back(prepSpec(bonus));
 		});
 	}

+ 2 - 2
lib/entities/hero/CHeroHandler.h

@@ -32,8 +32,8 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
 
 	void loadExperience();
 
-	std::vector<std::shared_ptr<Bonus>> createCreatureSpecialty(CreatureID cid) const;
-	std::vector<std::shared_ptr<Bonus>> createSecondarySkillSpecialty(SecondarySkill skillID) const;
+	std::vector<std::shared_ptr<Bonus>> createCreatureSpecialty(CreatureID cid, int fixedLevel, int growthPerStep) const;
+	std::vector<std::shared_ptr<Bonus>> createSecondarySkillSpecialty(SecondarySkill skillID, int growthPerStep) const;
 
 public:
 	ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount

+ 1 - 1
lib/json/JsonBonus.cpp

@@ -9,7 +9,6 @@
  */
 
 #include "StdInc.h"
-#include "CBonusTypeHandler.h"
 #include "JsonBonus.h"
 
 #include "JsonValidator.h"
@@ -19,6 +18,7 @@
 #include "../bonuses/Limiters.h"
 #include "../bonuses/Propagators.h"
 #include "../bonuses/Updaters.h"
+#include "../CBonusTypeHandler.h"
 #include "../constants/StringConstants.h"
 #include "../modding/IdentifierStorage.h"
 

+ 6 - 5
lib/mapObjects/CGHeroInstance.cpp

@@ -1047,13 +1047,14 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 	{
 		const CCreature * c = casualty.first.toCreature();
 		double raisedFromCasualty = std::min(c->getMaxHealth() / raisedUnitHealth, 1.0) * casualty.second * raisedUnitsPercentage;
-		raisedUnits += raisedFromCasualty;
+
+		if (bestCreature != selectedCreature)
+			raisedUnits += raisedFromCasualty * 2 / 3 / 100;
+		else
+			raisedUnits += raisedFromCasualty / 100;
 	}
 
-	if (bestCreature != selectedCreature)
-		return CStackBasicDescriptor(selectedCreature, std::max(static_cast<int>(raisedUnits * 2 / 3), 1));
-	else
-		return CStackBasicDescriptor(selectedCreature, std::max(static_cast<int>(raisedUnits), 1));
+	return CStackBasicDescriptor(selectedCreature, std::max(static_cast<int>(raisedUnits), 1));
 }
 
 int CGHeroInstance::getSightRadius() const

+ 2 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -11,8 +11,9 @@
 #include "StdInc.h"
 #include "CGTownInstance.h"
 
-#include "IGameSettings.h"
 #include "TownBuildingInstance.h"
+
+#include "../IGameSettings.h"
 #include "../spells/CSpellHandler.h"
 #include "../bonuses/Bonus.h"
 #include "../battle/IBattleInfoCallback.h"

+ 1 - 1
mapeditor/inspector/rewardswidget.cpp

@@ -8,12 +8,12 @@
  *
  */
 #include "StdInc.h"
-#include "CBonusTypeHandler.h"
 #include "rewardswidget.h"
 #include "ui_rewardswidget.h"
 #include "../lib/GameLibrary.h"
 #include "../lib/CSkillHandler.h"
 #include "../lib/spells/CSpellHandler.h"
+#include "../lib/CBonusTypeHandler.h"
 #include "../lib/CCreatureHandler.h"
 #include "../lib/constants/StringConstants.h"
 #include "../lib/entities/artifact/CArtifact.h"