瀏覽代碼

Merge pull request #379 from henningkoehlernz/hero_specialty_scaling

Hero specialty scaling
ArseniyShestakov 7 年之前
父節點
當前提交
7fd090786c

+ 1 - 1
AUTHORS

@@ -68,4 +68,4 @@ Piotr Wójcik aka Chocimier, <[email protected]>
    * Various bug fixes
 
 Henning Koehler, <[email protected]>
-   * skill modding
+   * skill modding, bonus updaters

+ 1 - 0
ChangeLog

@@ -27,6 +27,7 @@ MODS:
 * Improve support for WoG commander artifacts and skill descriptions
 * Added basic support for secondary skill modding
 * Map object sounds can now be configured via json
+* Added bonus updaters for hero specialties
 
 SOUND:
 * Fixed many mising or wrong pickup and visit sounds for map objects

+ 18 - 0
Global.h

@@ -369,6 +369,24 @@ namespace vstd
 		return std::find(c.begin(),c.end(),i);
 	}
 
+	//returns first key that maps to given value if present, returns success via found if provided
+	template <typename Key, typename T>
+	Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)
+	{
+		for(auto iter = map.cbegin(); iter != map.cend(); iter++)
+		{
+			if(iter->second == value)
+			{
+				if(found)
+					*found = true;
+				return iter->first;
+			}
+		}
+		if(found)
+			*found = false;
+		return Key();
+	}
+
 	//removes element i from container c, returns false if c does not contain i
 	template <typename Container, typename Item>
 	typename Container::size_type operator-=(Container &c, const Item &i)

+ 121 - 64
config/heroes/castle.json

@@ -9,10 +9,17 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "archery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 1, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"archery" : {
+					"subtype" : "skill.archery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"valeska":
 	{
@@ -24,10 +31,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "archery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 2 }
-		]
+		"specialty" : {
+			"creature" : "archer"
+		}
 	},
 	"edric":
 	{
@@ -39,10 +45,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "armorer", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 4 }
-		]
+		"specialty" : {
+			"creature" : "griffin"
+		}
 	},
 	"sylvia":
 	{
@@ -54,10 +59,17 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "navigation", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 2, "subtype": 5, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"navigation" : {
+					"subtype" : "skill.navigation",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"lordHaart":
 	{
@@ -70,10 +82,17 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "estates", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 13, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"estates" : {
+					"subtype" : "skill.estates",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"sorsha":
 	{
@@ -85,10 +104,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 6 }
-		]
+		"specialty" : {
+			"creature" : "swordsman"
+		}
 	},
 	"christian":
 	{
@@ -100,10 +118,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "artillery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"tyris":
 	{
@@ -115,10 +132,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 10 }
-		]
+		"specialty" : {
+			"creature" : "cavalier"
+		}
 	},
 	"rion":
 	{
@@ -131,10 +147,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "firstAid", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 27, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"firstAid" : {
+					"subtype" : "skill.firstAid",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"adela":
 	{
@@ -147,10 +170,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "diplomacy", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":6, "val": 3, "subtype": 41, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"bless" : {
+					"addInfo" : 0,
+					"subtype" : "spell.bless",
+					"type" : "SPECIAL_BLESS_DAMAGE",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"cuthbert":
 	{
@@ -163,10 +193,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "estates", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 45, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"weakness" : {
+					"addInfo" : 0,
+					"subtype" : "spell.weakness",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"adelaide":
 	{
@@ -178,10 +213,16 @@
 		[
 			{ "skill" : "wisdom", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 20, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"frostRing" : {
+					"subtype" : "spell.frostRing",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"ingham":
 	{
@@ -194,10 +235,9 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 8 }
-		]
+		"specialty" : {
+			"creature" : "monk"
+		}
 	},
 	"sanya":
 	{
@@ -210,10 +250,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"loynis":
 	{
@@ -226,10 +273,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 48, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"prayer" : {
+					"addInfo" : 0,
+					"subtype" : "spell.prayer",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"caitlin":
 	{
@@ -242,9 +294,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	}
 }

+ 242 - 74
config/heroes/conflux.json

@@ -9,11 +9,22 @@
 			{ "skill" : "artillery", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 3, "subtype": 1, "info": 120 },
-			{ "type":4, "val": 3, "subtype": 2, "info": 120 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "psychicElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				],
+				"type" : "PRIMARY_SKILL",
+				"val" : 3
+			},
+			"bonuses" : {
+				"attack" : { "subtype" : "primSkill.attack" },
+				"defence" : { "subtype" : "primSkill.defence" }
+			}
+		}
 	},
 	"thunar":
 	{
@@ -25,12 +36,32 @@
 			{ "skill" : "estates", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 2, "subtype": 1, "info": 113 },
-			{ "type":4, "val": 1, "subtype": 2, "info": 113 },
-			{ "type":4, "val": 5, "subtype": 4, "info": 113 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "earthElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"health" : {
+					"type" : "STACK_HEALTH",
+					"val" : 5
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 1
+				}
+			}
+		}
 	},
 	"ignissa":
 	{
@@ -42,12 +73,33 @@
 			{ "skill" : "artillery", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 1, "subtype": 1, "info": 114 },
-			{ "type":4, "val": 2, "subtype": 1, "info": 114 },
-			{ "type":4, "val": 2, "subtype": 3, "info": 114 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "fireElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"damage" : {
+					"subtype" : 0,
+					"type" : "CREATURE_DAMAGE",
+					"val" : 2
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 1
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				}
+			}
+		}
 	},
 	"lacus":
 	{
@@ -58,10 +110,21 @@
 		[
 			{ "skill" : "tactics", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 2, "subtype": 1, "info": 115 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"attack" : {
+					"limiters" : [
+						{
+							"parameters" : [ "waterElemental", true ],
+							"type" : "CREATURE_TYPE_LIMITER"
+						}
+					],
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				}
+			}
+		}
 	},
 	"monere":
 	{
@@ -73,11 +136,22 @@
 			{ "skill" : "logistics", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 3, "subtype": 1, "info": 120 },
-			{ "type":4, "val": 3, "subtype": 2, "info": 120 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "psychicElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				],
+				"type" : "PRIMARY_SKILL",
+				"val" : 3
+			},
+			"bonuses" : {
+				"attack" : { "subtype" : "primSkill.attack" },
+				"defence" : { "subtype" : "primSkill.defence" }
+			}
+		}
 	},
 	"erdamon":
 	{
@@ -89,12 +163,32 @@
 			{ "skill" : "estates", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 2, "subtype": 1, "info": 113 },
-			{ "type":4, "val": 1, "subtype": 2, "info": 113 },
-			{ "type":4, "val": 5, "subtype": 4, "info": 113 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "earthElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"health" : {
+					"type" : "STACK_HEALTH",
+					"val" : 5
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 1
+				}
+			}
+		}
 	},
 	"fiur":
 	{
@@ -105,12 +199,33 @@
 		[
 			{ "skill" : "offence", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 1, "subtype": 1, "info": 114 },
-			{ "type":4, "val": 2, "subtype": 1, "info": 114 },
-			{ "type":4, "val": 2, "subtype": 3, "info": 114 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "fireElemental", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"damage" : {
+					"subtype" : 0,
+					"type" : "CREATURE_DAMAGE",
+					"val" : 2
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 1
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				}
+			}
+		}
 	},
 	"kalt":
 	{
@@ -122,10 +237,21 @@
 			{ "skill" : "tactics", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 2, "subtype": 1, "info": 115 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"attack" : {
+					"limiters" : [
+						{
+							"parameters" : [ "waterElemental", true ],
+							"type" : "CREATURE_TYPE_LIMITER"
+						}
+					],
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				}
+			}
+		}
 	},
 	"luna":
 	{
@@ -138,10 +264,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "fireMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":5, "val": 100, "subtype": 13, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"fireWall" : {
+					"subtype" : "spell.fireWall",
+					"type" : "SPECIFIC_SPELL_DAMAGE",
+					"val" : 100,
+					"valueType" : "BASE_NUMBER"
+				}
+			}
+		}
 	},
 	"brissa":
 	{
@@ -154,10 +286,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "airMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 53, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"haste" : {
+					"addInfo" : 0,
+					"subtype" : "spell.haste",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"ciele":
 	{
@@ -170,10 +307,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "waterMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":5, "val": 50, "subtype": 15, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"magicArrow" : {
+					"subtype" : "spell.magicArrow",
+					"type" : "SPECIFIC_SPELL_DAMAGE",
+					"val" : 50,
+					"valueType" : "BASE_NUMBER"
+				}
+			}
+		}
 	},
 	"labetha":
 	{
@@ -186,10 +329,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "earthMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 46, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"stoneSkin" : {
+					"addInfo" : 0,
+					"subtype" : "spell.stoneSkin",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"inteus":
 	{
@@ -202,10 +350,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "fireMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 43, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"bloodlust" : {
+					"addInfo" : 0,
+					"subtype" : "spell.bloodlust",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"aenain":
 	{
@@ -218,10 +371,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "airMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 47, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"disruptingRay" : {
+					"addInfo" : 0,
+					"subtype" : "spell.disruptingRay",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"gelare":
 	{
@@ -234,10 +392,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "waterMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	},
 	"grindan":
 	{
@@ -250,9 +413,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "earthMagic", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	}
 }

+ 119 - 64
config/heroes/dungeon.json

@@ -9,10 +9,9 @@
 			{ "skill" : "scouting", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 72 }
-		]
+		"specialty" : {
+			"creature" : "harpy"
+		}
 	},
 	"arlach":
 	{
@@ -24,10 +23,9 @@
 			{ "skill" : "artillery", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"dace":
 	{
@@ -39,10 +37,9 @@
 			{ "skill" : "tactics", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 78 }
-		]
+		"specialty" : {
+			"creature" : "minotaur"
+		}
 	},
 	"ajit":
 	{
@@ -54,10 +51,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 74 }
-		]
+		"specialty" : {
+			"creature" : "beholder"
+		}
 	},
 	"damacon":
 	{
@@ -68,10 +64,15 @@
 		[
 			{ "skill" : "offence", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	},
 	"gunnar":
 	{
@@ -83,10 +84,17 @@
 			{ "skill" : "logistics", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 2, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"logistics" : {
+					"subtype" : "skill.logistics",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"synca":
 	{
@@ -98,10 +106,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 80 }
-		]
+		"specialty" : {
+			"creature" : "manticore"
+		}
 	},
 	"shakti":
 	{
@@ -113,10 +120,9 @@
 			{ "skill" : "tactics", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 70 }
-		]
+		"specialty" : {
+			"creature" : "troglodyte"
+		}
 	},
 	"alamar":
 	{
@@ -129,10 +135,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 38, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"resurrection" : {
+					"subtype" : "spell.resurrection",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"jaegar":
 	{
@@ -145,10 +157,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 8, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"mysticism" : {
+					"subtype" : "skill.mysticism",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"malekith":
 	{
@@ -161,10 +180,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"jeddite":
 	{
@@ -176,10 +202,16 @@
 		[
 			{ "skill" : "wisdom", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 38, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"resurrection" : {
+					"subtype" : "spell.resurrection",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"geon":
 	{
@@ -192,10 +224,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"deemer":
 	{
@@ -208,10 +247,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scouting", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 23, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"meteorShower" : {
+					"subtype" : "spell.meteorShower",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"sephinroth":
 	{
@@ -224,10 +269,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 1, "subtype": 4, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"crystal" : {
+					"subtype" : "resource.crystal",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 1
+				}
+			}
+		}
 	},
 	"darkstorn":
 	{
@@ -240,9 +290,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 46, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"stoneSkin" : {
+					"addInfo" : 0,
+					"subtype" : "spell.stoneSkin",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	}
 }

+ 116 - 64
config/heroes/fortress.json

@@ -9,10 +9,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 106 }
-		],		
+		"specialty" : {
+			"creature" : "basilisk"
+		},
 		"army" :
 		[
 			{
@@ -39,10 +38,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 98 }
-		]
+		"specialty" : {
+			"creature" : "gnoll"
+		}
 	},
 	"wystan":
 	{
@@ -54,10 +52,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "archery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 100 }
-		]
+		"specialty" : {
+			"creature" : "lizardman"
+		}
 	},
 	"tazar":
 	{
@@ -68,10 +65,17 @@
 		[
 			{ "skill" : "armorer", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 23, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"armorer" : {
+					"subtype" : "skill.armorer",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"alkin":
 	{
@@ -83,10 +87,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 102 }
-		]
+		"specialty" : {
+			"creature" : "gorgon"
+		}
 	},
 	"korbac":
 	{
@@ -98,10 +101,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "pathfinding", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 104 }
-		]
+		"specialty" : {
+			"creature" : "serpentFly"
+		}
 	},
 	"gerwulf":
 	{
@@ -113,10 +115,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "artillery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"broghild":
 	{
@@ -128,10 +129,9 @@
 			{ "skill" : "armorer", "level": "basic" },
 			{ "skill" : "scouting", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 108 }
-		]
+		"specialty" : {
+			"creature" : "wyvern"
+		}
 	},
 	"mirlanda":
 	{
@@ -143,10 +143,15 @@
 		[
 			{ "skill" : "wisdom", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 45, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"weakness" : {
+					"addInfo" : 0,
+					"subtype" : "spell.weakness",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"rosic":
 	{
@@ -159,10 +164,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 8, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"mysticism" : {
+					"subtype" : "skill.mysticism",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"voy":
 	{
@@ -175,10 +187,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "navigation", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 2, "subtype": 5, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"navigation" : {
+					"subtype" : "skill.navigation",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"verdish":
 	{
@@ -191,10 +210,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "firstAid", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 27, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"firstAid" : {
+					"subtype" : "skill.firstAid",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"merist":
 	{
@@ -207,10 +233,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 46, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"stoneSkin" : {
+					"addInfo" : 0,
+					"subtype" : "spell.stoneSkin",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"styg":
 	{
@@ -223,10 +254,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"andra":
 	{
@@ -239,10 +277,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 24, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"intelligence" : {
+					"subtype" : "skill.intelligence",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"tiva":
 	{
@@ -255,9 +300,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	}
 }

+ 110 - 64
config/heroes/inferno.json

@@ -8,10 +8,9 @@
 		[
 			{ "skill" : "scouting", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 46 }
-		]
+		"specialty" : {
+			"creature" : "hellHound"
+		}
 	},
 	"rashka":
 	{
@@ -23,10 +22,9 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 52 }
-		]
+		"specialty" : {
+			"creature" : "efreet"
+		}
 	},
 	"marius":
 	{
@@ -37,10 +35,9 @@
 		[
 			{ "skill" : "armorer", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 48 }
-		]
+		"specialty" : {
+			"creature" : "demon"
+		}
 	},
 	"ignatius":
 	{
@@ -52,10 +49,9 @@
 			{ "skill" : "tactics", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 42 }
-		]
+		"specialty" : {
+			"creature" : "imp"
+		}
 	},
 	"octavia":
 	{
@@ -67,10 +63,15 @@
 			{ "skill" : "scholar", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	},
 	"calh":
 	{
@@ -82,10 +83,9 @@
 			{ "skill" : "archery", "level": "basic" },
 			{ "skill" : "scouting", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 42 }
-		]
+		"specialty" : {
+			"creature" : "gog"
+		}
 	},
 	"pyre":
 	{
@@ -97,10 +97,9 @@
 			{ "skill" : "artillery", "level": "basic" },
 			{ "skill" : "logistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"nymus":
 	{
@@ -111,10 +110,9 @@
 		[
 			{ "skill" : "offence", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 50 }
-		]
+		"specialty" : {
+			"creature" : "pitFiend"
+		}
 	},
 	"ayden":
 	{
@@ -127,10 +125,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 24, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"intelligence" : {
+					"subtype" : "skill.intelligence",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"xyron":
 	{
@@ -143,10 +148,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 22, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"inferno" : {
+					"subtype" : "spell.inferno",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"axsis":
 	{
@@ -159,10 +170,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 8, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"mysticism" : {
+					"subtype" : "skill.mysticism",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"olema":
 	{
@@ -175,10 +193,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "ballistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 45, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"weakness" : {
+					"addInfo" : 0,
+					"subtype" : "spell.weakness",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"calid":
 	{
@@ -191,10 +214,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 1, "subtype": 3, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sulfur" : {
+					"subtype" : "resource.sulfur",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 1
+				}
+			}
+		}
 	},
 	"ash":
 	{
@@ -207,10 +235,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 43, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"bloodlust" : {
+					"addInfo" : 0,
+					"subtype" : "spell.bloodlust",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"zydar":
 	{
@@ -223,10 +256,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"xarfax":
 	{
@@ -239,9 +279,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 21, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"fireball" : {
+					"subtype" : "spell.fireball",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	}
 }

+ 119 - 64
config/heroes/necropolis.json

@@ -10,10 +10,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 58 }
-		]
+		"specialty" : {
+			"creature" : "walkingDead"
+		}
 	},
 	"vokial":
 	{
@@ -26,10 +25,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "artillery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 62 }
-		]
+		"specialty" : {
+			"creature" : "vampire"
+		}
 	},
 	"moandor":
 	{
@@ -42,10 +40,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 64 }
-		]
+		"specialty" : {
+			"creature" : "lich"
+		}
 	},
 	"charna":
 	{
@@ -58,10 +55,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 60 }
-		]
+		"specialty" : {
+			"creature" : "wight"
+		}
 	},
 	"tamika":
 	{
@@ -74,10 +70,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 66 }
-		]
+		"specialty" : {
+			"creature" : "blackKnight"
+		}
 	},
 	"isra":
 	{
@@ -89,10 +84,17 @@
 		[
 			{ "skill" : "necromancy", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 12, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"necromancy" : {
+					"subtype" : "skill.necromancy",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"clavius":
 	{
@@ -105,10 +107,15 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	},
 	"galthran":
 	{
@@ -121,10 +128,9 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "armorer", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 56 }
-		]
+		"specialty" : {
+			"creature" : "skeleton"
+		}
 	},
 	"septienna":
 	{
@@ -137,10 +143,16 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 24, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"deathRipple" : {
+					"subtype" : "spell.deathRipple",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"aislinn":
 	{
@@ -153,10 +165,16 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "wisdom", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 23, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"meteorShower" : {
+					"subtype" : "spell.meteorShower",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"sandro":
 	{
@@ -169,10 +187,17 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"nimbus":
 	{
@@ -185,10 +210,17 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"thant":
 	{
@@ -201,10 +233,16 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 39, "subtype": 0, "info": 3 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"animateDead" : {
+					"subtype" : "spell.animateDead",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"xsi":
 	{
@@ -217,10 +255,15 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "learning", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 46, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"stoneSkin" : {
+					"addInfo" : 0,
+					"subtype" : "spell.stoneSkin",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"vidomina":
 	{
@@ -232,10 +275,17 @@
 		[
 			{ "skill" : "necromancy", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 12, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"necromancy" : {
+					"subtype" : "skill.necromancy",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"nagash":
 	{
@@ -248,9 +298,14 @@
 			{ "skill" : "necromancy", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	}
 }

+ 127 - 64
config/heroes/rampart.json

@@ -9,10 +9,17 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "armorer", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 23, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"armorer" : {
+					"subtype" : "skill.armorer",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"ufretin":
 	{
@@ -24,10 +31,9 @@
 			{ "skill" : "luck", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 16 }
-		]
+		"specialty" : {
+			"creature" : "dwarf"
+		}
 	},
 	"jenova":
 	{
@@ -38,10 +44,15 @@
 		[
 			{ "skill" : "archery", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	},
 	"ryland":
 	{
@@ -53,10 +64,9 @@
 			{ "skill" : "diplomacy", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 22 }
-		]
+		"specialty" : {
+			"creature" : "dendroidGuard"
+		}
 	},
 	"thorgrim":
 	{
@@ -67,10 +77,17 @@
 		[
 			{ "skill" : "resistance", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 26, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"resistance" : {
+					"subtype" : "skill.resistance",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"ivor":
 	{
@@ -82,10 +99,9 @@
 			{ "skill" : "archery", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 18 }
-		]
+		"specialty" : {
+			"creature" : "woodElf"
+		}
 	},
 	"clancy":
 	{
@@ -97,10 +113,9 @@
 			{ "skill" : "pathfinding", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 24 }
-		]
+		"specialty" : {
+			"creature" : "unicorn"
+		}
 	},
 	"kyrre":
 	{
@@ -112,10 +127,17 @@
 			{ "skill" : "archery", "level": "basic" },
 			{ "skill" : "logistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 2, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"logistics" : {
+					"subtype" : "skill.logistics",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"coronius":
 	{
@@ -128,10 +150,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 55, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"slayer" : {
+					"addInfo" : 1,
+					"subtype" : "spell.slayer",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"uland":
 	{
@@ -144,10 +171,16 @@
 			{ "skill" : "wisdom", "level": "advanced" },
 			{ "skill" : "ballistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 37, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"cure" : {
+					"subtype" : "spell.cure",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"elleshar":
 	{
@@ -160,10 +193,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 24, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"intelligence" : {
+					"subtype" : "skill.intelligence",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"gem":
 	{
@@ -176,10 +216,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "firstAid", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 27, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"firstAid" : {
+					"subtype" : "skill.firstAid",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"malcom":
 	{
@@ -192,10 +239,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"melodia":
 	{
@@ -208,10 +262,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "luck", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":7, "val": 0, "subtype": 51, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"fortune" : {
+					"subtype" : "spell.fortune",
+					"type" : "MAXED_SPELL"
+				}
+			}
+		}
 	},
 	"alagar":
 	{
@@ -224,10 +282,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 16, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"iceBolt" : {
+					"subtype" : "spell.iceBolt",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"aeris":
 	{
@@ -240,9 +304,8 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scouting", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 20 }
-		]
+		"specialty" : {
+			"creature" : "pegasus"
+		}
 	}
 }

+ 156 - 59
config/heroes/special.json

@@ -10,10 +10,14 @@
 		[
 			{ "skill" : "leadership", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":12, "val": 2, "subtype": 0, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"speed" : {
+					"type" : "STACKS_SPEED",
+					"val" : 2
+				}
+			}
+		}
 	},
 	"adrienne":
 	{
@@ -25,11 +29,9 @@
 		"skills":
 		[
 			{ "skill" : "wisdom", "level": "basic" },
-			{ "skill" : "fireMagic", "level": "expert" } ],
-		"specialties":
-		[
-			{ "type":11, "val": 14, "subtype": 0, "info": 3 }
-		]
+			{ "skill" : "fireMagic", "level": "expert" }
+		],
+		"specialty" : { "bonuses" : {  } } // has expert fire magic as "specialty"
 	},
 	"catherine":
 	{
@@ -42,10 +44,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 4 }
-		]
+		"specialty" : {
+			"creature" : "swordsman"
+		}
 	},
 	"dracon":
 	{
@@ -58,11 +59,18 @@
 		[
 			{ "skill" : "wisdom", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":9, "val": 0, "subtype": 8, "info": 136 },
-			{ "type":9, "val": 0, "subtype": 34, "info": 136 }
-		]
+		"specialty" : {
+			"base" : {
+				"addInfo" : "creature.enchanter",
+				"type" : "SPECIAL_UPGRADE"
+			},
+			"bonuses" : {
+				"archMage2enchanter" : { "subtype" : "creature.archMage" },
+				"mage2enchanter" : { "subtype" : "creature.mage" },
+				"monk2enchanter" : { "subtype" : "creature.monk" },
+				"zealot2enchanter" : { "subtype" : "creature.zealot" }
+			}
+		}
 	},
 	"gelu":
 	{
@@ -75,11 +83,18 @@
 			{ "skill" : "archery", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":9, "val": 0, "subtype": 2, "info": 137 },
-			{ "type":9, "val": 0, "subtype": 18, "info": 137 }
-		]
+		"specialty" : {
+			"base" : {
+				"addInfo" : "creature.sharpshooter",
+				"type" : "SPECIAL_UPGRADE"
+			},
+			"bonuses" : {
+				"archer2sharpshooter" : { "subtype" : "creature.archer" },
+				"grandElf2sharpshooter" : { "subtype" : "creature.grandElf" },
+				"marksman2sharpshooter" : { "subtype" : "creature.marksman" },
+				"woodElf2sharpshooter" : { "subtype" : "creature.woodElf" }
+			}
+		}
 	},
 	"kilgor":
 	{
@@ -91,12 +106,33 @@
 		[
 			{ "skill" : "offence", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 5,  "subtype": 1, "info": 96 },
-			{ "type":4, "val": 5,  "subtype": 2, "info": 96 },
-			{ "type":4, "val": 10, "subtype": 3, "info": 96 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "behemoth", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"damage" : {
+					"subtype" : 0,
+					"type" : "CREATURE_DAMAGE",
+					"val" : 10
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 5
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 5
+				}
+			}
+		}
 	},
 	"undeadHaart": // undead version of Lord Haart
 	{
@@ -109,12 +145,33 @@
 		[
 			{ "skill" : "necromancy", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 5,  "subtype": 1, "info": 66 },
-			{ "type":4, "val": 5,  "subtype": 2, "info": 66 },
-			{ "type":4, "val": 10, "subtype": 3, "info": 66 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "blackKnight", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"damage" : {
+					"subtype" : 0,
+					"type" : "CREATURE_DAMAGE",
+					"val" : 10
+				},
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 5
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 5
+				}
+			}
+		}
 	},
 	"mutare":
 	{
@@ -128,11 +185,22 @@
 			{ "skill" : "estates", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":13, "val": 5, "subtype": 1, "info": 0 },
-			{ "type":13, "val": 5, "subtype": 2, "info": 0 }
-		]
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "DRAGON_NATURE" ],
+						"type" : "HAS_ANOTHER_BONUS_LIMITER"
+					}
+				],
+				"type" : "PRIMARY_SKILL",
+				"val" : 5
+			},
+			"bonuses" : {
+				"attack" : { "subtype" : "primSkill.attack" },
+				"defence" : { "subtype" : "primSkill.defence" }
+			}
+		}
 	},
 	"roland":
 	{
@@ -145,10 +213,9 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "armorer", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 4 }
-		]
+		"specialty" : {
+			"creature" : "griffin"
+		}
 	},
 	"mutareDrake":
 	{
@@ -162,11 +229,22 @@
 			{ "skill" : "estates", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":13, "val": 1, "subtype": 1, "info": 5 },
-			{ "type":13, "val": 1, "subtype": 1, "info": 5 }
-		],
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "DRAGON_NATURE" ],
+						"type" : "HAS_ANOTHER_BONUS_LIMITER"
+					}
+				],
+				"type" : "PRIMARY_SKILL",
+				"val" : 5
+			},
+			"bonuses" : {
+				"attack" : { "subtype" : "primSkill.attack" },
+				"defence" : { "subtype" : "primSkill.defence" }
+			}
+		},
 		"army" :
 		[
 			{
@@ -194,10 +272,9 @@
 			{ "skill" : "tactics", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 90 }
-		],
+		"specialty" : {
+			"creature" : "ogre"
+		},
 		"army" :
 		[
 			{
@@ -225,12 +302,32 @@
 			{ "skill" : "leadership", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":4, "val": 4, "subtype": 1, "info": 54 },
-			{ "type":4, "val": 2, "subtype": 2, "info": 54 },
-			{ "type":4, "val": 1, "subtype": 5, "info": 54 }
-		],
+		"specialty" : {
+			"base" : {
+				"limiters" : [
+					{
+						"parameters" : [ "devil", true ],
+						"type" : "CREATURE_TYPE_LIMITER"
+					}
+				]
+			},
+			"bonuses" : {
+				"attack" : {
+					"subtype" : "primSkill.attack",
+					"type" : "PRIMARY_SKILL",
+					"val" : 4
+				},
+				"defence" : {
+					"subtype" : "primSkill.defence",
+					"type" : "PRIMARY_SKILL",
+					"val" : 2
+				},
+				"speed" : {
+					"type" : "STACKS_SPEED",
+					"val" : 1
+				}
+			}
+		},
 		"army" :
 		[
 			{

+ 106 - 64
config/heroes/stronghold.json

@@ -9,10 +9,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "ballistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 94 }
-		]
+		"specialty" : {
+			"creature" : "cyclop"
+		}
 	},
 	"gurnisson":
 	{
@@ -24,10 +23,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "artillery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"jabarkas":
 	{
@@ -39,10 +37,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "archery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 88 }
-		]
+		"specialty" : {
+			"creature" : "orc"
+		}
 	},
 	"shiva":
 	{
@@ -54,10 +51,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "scouting", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 92 }
-		]
+		"specialty" : {
+			"creature" : "roc"
+		}
 	},
 	"gretchin":
 	{
@@ -69,10 +65,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "pathfinding", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 84 }
-		]
+		"specialty" : {
+			"creature" : "goblin"
+		}
 	},
 	"krellion":
 	{
@@ -84,10 +79,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 90 }
-		]
+		"specialty" : {
+			"creature" : "ogre"
+		}
 	},
 	"cragHack":
 	{
@@ -98,10 +92,17 @@
 		[
 			{ "skill" : "offence", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 22, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"offence" : {
+					"subtype" : "skill.offence",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"tyraxor":
 	{
@@ -113,10 +114,9 @@
 			{ "skill" : "offence", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 86 }
-		]
+		"specialty" : {
+			"creature" : "goblinWolfRider"
+		}
 	},
 	"gird":
 	{
@@ -129,10 +129,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"vey":
 	{
@@ -145,10 +152,9 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "leadership", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 90 }
-		]
+		"specialty" : {
+			"creature" : "ogre"
+		}
 	},
 	"dessa":
 	{
@@ -161,10 +167,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "logistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 2, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"logistics" : {
+					"subtype" : "skill.logistics",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"terek":
 	{
@@ -177,10 +190,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 53, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"haste" : {
+					"addInfo" : 0,
+					"subtype" : "spell.haste",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"zubin":
 	{
@@ -193,10 +211,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "artillery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 44, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"precision" : {
+					"addInfo" : 0,
+					"subtype" : "spell.precision",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"gundula":
 	{
@@ -209,10 +232,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 25, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"sorcery" : {
+					"subtype" : "skill.sorcery",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"oris":
 	{
@@ -225,10 +255,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"saurug":
 	{
@@ -241,9 +278,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 1, "subtype": 5, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gems" : {
+					"subtype" : "resource.gems",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 1
+				}
+			}
+		}
 	}
 }

+ 109 - 64
config/heroes/tower.json

@@ -10,10 +10,9 @@
 			{ "skill" : "scouting", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 30 }
-		]
+		"specialty" : {
+			"creature" : "stoneGargoyle"
+		}
 	},
 	"thane":
 	{
@@ -25,10 +24,9 @@
 		[
 			{ "skill" : "scholar", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 36 }
-		]
+		"specialty" : {
+			"creature" : "genie"
+		}
 	},
 	"josephine":
 	{
@@ -41,10 +39,9 @@
 			{ "skill" : "mysticism", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 32 }
-		]
+		"specialty" : {
+			"creature" : "ironGolem"
+		}
 	},
 	"neela":
 	{
@@ -57,10 +54,17 @@
 			{ "skill" : "scholar", "level": "basic" },
 			{ "skill" : "armorer", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 23, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"armorer" : {
+					"subtype" : "skill.armorer",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"torosar ":
 	{
@@ -73,10 +77,9 @@
 			{ "skill" : "mysticism", "level": "basic" },
 			{ "skill" : "tactics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 146 }
-		]
+		"specialty" : {
+			"creature" : "ballista"
+		}
 	},
 	"fafner":
 	{
@@ -89,10 +92,9 @@
 			{ "skill" : "scholar", "level": "basic" },
 			{ "skill" : "resistance", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 38 }
-		]
+		"specialty" : {
+			"creature" : "naga"
+		}
 	},
 	"rissa":
 	{
@@ -105,10 +107,15 @@
 			{ "skill" : "mysticism", "level": "basic" },
 			{ "skill" : "offence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 1, "subtype": 1, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"mercury" : {
+					"subtype" : "resource.mercury",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 1
+				}
+			}
+		}
 	},
 	"iona":
 	{
@@ -121,10 +128,9 @@
 			{ "skill" : "scholar", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 36 }
-		]
+		"specialty" : {
+			"creature" : "genie"
+		}
 	},
 	"astral":
 	{
@@ -136,10 +142,16 @@
 		[
 			{ "skill" : "wisdom", "level": "advanced" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 60, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"hypnotize" : {
+					"subtype" : "spell.hypnotize",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"halon":
 	{
@@ -152,10 +164,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "mysticism", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 8, "info": 1 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"mysticism" : {
+					"subtype" : "skill.mysticism",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"serena":
 	{
@@ -168,10 +187,17 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":2, "val": 5, "subtype": 11, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"eagleEye" : {
+					"subtype" : "skill.eagleEye",
+					"type" : "SECONDARY_SKILL_PREMY",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 5,
+					"valueType" : "PERCENT_TO_BASE"
+				}
+			}
+		}
 	},
 	"daremyth":
 	{
@@ -184,10 +210,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "intelligence", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":7, "val": 0, "subtype": 51, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"fortune" : {
+					"subtype" : "spell.fortune",
+					"type" : "MAXED_SPELL"
+				}
+			}
+		}
 	},
 	"theodorus":
 	{
@@ -200,10 +230,9 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "ballistics", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":1, "val": 0, "subtype": 0, "info": 34 }
-		]
+		"specialty" : {
+			"creature" : "mage"
+		}
 	},
 	"solmyr":
 	{
@@ -216,10 +245,16 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "sorcery", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":3, "val": 3, "subtype": 19, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"chainLightning" : {
+					"subtype" : "spell.chainLightning",
+					"type" : "SPECIAL_SPELL_LEV",
+					"updater" : "TIMES_HERO_LEVEL",
+					"val" : 3
+				}
+			}
+		}
 	},
 	"cyra":
 	{
@@ -232,10 +267,15 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "diplomacy", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":8, "val": 0, "subtype": 53, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"haste" : {
+					"addInfo" : 0,
+					"subtype" : "spell.haste",
+					"type" : "SPECIAL_PECULIAR_ENCHANT"
+				}
+			}
+		}
 	},
 	"aine":
 	{
@@ -248,9 +288,14 @@
 			{ "skill" : "wisdom", "level": "basic" },
 			{ "skill" : "scholar", "level": "basic" }
 		],
-		"specialties":
-		[
-			{ "type":10, "val": 350, "subtype": 6, "info": 0 }
-		]
+		"specialty" : {
+			"bonuses" : {
+				"gold" : {
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
+					"val" : 350
+				}
+			}
+		}
 	}
 }

+ 24 - 0
config/schemas/bonus.json

@@ -70,6 +70,30 @@
 				}
 			]
 		},
+		"updater" : {
+			"anyOf" : [
+				{
+					"type" : "string"
+				},
+				{
+					"description" : "updater",
+					"type" : "object",
+					"required" : ["type", "parameters"],
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"description" : "type"
+						},
+						"parameters": {
+							"type" : "array",
+							"description" : "parameters",
+							"additionalItems" : true
+						}
+					}
+				}
+			]
+		},
 		"sourceID": {
 			"type":"number",
 			"description": "sourceID"

+ 40 - 16
config/schemas/hero.json

@@ -115,24 +115,48 @@
 			"additionalItems" : true
 		},
 		"specialty": {
-			"type":"array",
-			"description": "Description of hero specialty using bonus system",	
-			"items": {
-				"type":"object",
-				"additionalProperties" : false,
-				"required" : [ "bonuses" ],
-				"properties":{
-					"growsWithLevel" : {
-						"type" : "boolean",
-						"description" : "Specialty growth with level, so far only SECONDARY_SKILL_PREMY and PRIMATY SKILL with creature limiter can grow"
-					},
-					"bonuses": {
-						"type":"array",
-						"description": "List of bonuses",
-						"items": { "$ref" : "vcmi:bonus" }
+			"anyOf" : [
+				{
+					"type":"array",
+					"description": "Description of hero specialty using bonus system (deprecated)",
+					"items": {
+						"type" : "object",
+						"additionalProperties" : false,
+						"required" : [ "bonuses" ],
+						"properties" : {
+							"growsWithLevel" : {
+								"type" : "boolean",
+								"description" : "Specialty growth with level. Deprecated, use bonuses with updaters instead."
+							},
+							"bonuses" : {
+								"type" : "array",
+								"description" : "List of bonuses",
+								"items" : { "$ref" : "vcmi:bonus" }
+							}
+						}
+					}
+				},
+				{
+					"type" : "object",
+					"description": "Description of hero specialty using bonus system",
+					"additionalProperties" : false,
+					"properties" : { 
+						"base" : {
+							"type" : "object",
+							"description" : "Will be merged with all bonuses."
+						},
+						"bonuses" : {
+							"type" : "object",
+							"description" : "Set of bonuses",
+							"additionalProperties" : { "$ref" : "vcmi:bonus" }
+						},
+						"creature" : {
+							"type" : "string",
+							"description" : "Name of base creature to grant standard specialty to."
+						}
 					}
 				}
-			}
+			]
 		},
 		"spellbook": {
 			"type":"array",

+ 2 - 1
config/skills.json

@@ -226,7 +226,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"type" : "MANA_REGENERATION",
+					"subtype" : "skill.mysticism",
+					"type" : "SECONDARY_SKILL_PREMY",
 					"valueType" : "BASE_NUMBER"
 				}
 			}

+ 355 - 17
lib/CHeroHandler.cpp

@@ -379,31 +379,310 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node)
 	}
 }
 
-void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
+// add standard creature specialty to result
+void AddSpecialtyForCreature(int creatureID, std::shared_ptr<Bonus> bonus, std::vector<std::shared_ptr<Bonus>> &result)
+{
+	const CCreature &specBaseCreature = *VLC->creh->creatures[creatureID]; //base creature in which we have specialty
+
+	bonus->limiter.reset(new CCreatureTypeLimiter(specBaseCreature, true));
+	bonus->type = Bonus::STACKS_SPEED;
+	bonus->valType = Bonus::ADDITIVE_VALUE;
+	bonus->val = 1;
+	result.push_back(bonus);
+
+	// attack and defense may differ for upgraded creatures => separate bonuses
+	std::vector<int> specTargets;
+	specTargets.push_back(creatureID);
+	specTargets.insert(specTargets.end(), specBaseCreature.upgrades.begin(), specBaseCreature.upgrades.end());
+
+	for(int cid : specTargets)
+	{
+		const CCreature &specCreature = *VLC->creh->creatures[cid];
+		bonus = std::make_shared<Bonus>(*bonus);
+		bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false));
+		bonus->type = Bonus::PRIMARY_SKILL;
+		bonus->val = 0;
+
+		int stepSize = specCreature.level ? specCreature.level : 5;
+
+		bonus->subtype = PrimarySkill::ATTACK;
+		bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize));
+		result.push_back(bonus);
+
+		bonus = std::make_shared<Bonus>(*bonus);
+		bonus->subtype = PrimarySkill::DEFENSE;
+		bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefence(false), stepSize));
+		result.push_back(bonus);
+	}
+}
+
+// convert deprecated format
+std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid)
+{
+	std::vector<std::shared_ptr<Bonus>> result;
+
+	std::shared_ptr<Bonus> bonus = std::make_shared<Bonus>();
+	bonus->duration = Bonus::PERMANENT;
+	bonus->source = Bonus::HERO_SPECIAL;
+	bonus->sid = sid;
+	bonus->val = spec.val;
+
+	switch (spec.type)
+	{
+	case 1: //creature specialty
+		AddSpecialtyForCreature(spec.additionalinfo, bonus, result);
+		break;
+	case 2: //secondary skill
+		bonus->type = Bonus::SECONDARY_SKILL_PREMY;
+		bonus->valType = Bonus::PERCENT_TO_BASE;
+		bonus->subtype = spec.subtype;
+		bonus->updater.reset(new TimesHeroLevelUpdater());
+		result.push_back(bonus);
+		break;
+	case 3: //spell damage bonus, level dependent but calculated elsewhere
+		bonus->type = Bonus::SPECIAL_SPELL_LEV;
+		bonus->subtype = spec.subtype;
+		bonus->updater.reset(new TimesHeroLevelUpdater());
+		result.push_back(bonus);
+		break;
+	case 4: //creature stat boost
+		switch (spec.subtype)
+		{
+		case 1:
+			bonus->type = Bonus::PRIMARY_SKILL;
+			bonus->subtype = PrimarySkill::ATTACK;
+			break;
+		case 2:
+			bonus->type = Bonus::PRIMARY_SKILL;
+			bonus->subtype = PrimarySkill::DEFENSE;
+			break;
+		case 3:
+			bonus->type = Bonus::CREATURE_DAMAGE;
+			bonus->subtype = 0; //both min and max
+			break;
+		case 4:
+			bonus->type = Bonus::STACK_HEALTH;
+			break;
+		case 5:
+			bonus->type = Bonus::STACKS_SPEED;
+			break;
+		default:
+			logMod->warn("Unknown subtype for specialty 4");
+			return result;
+		}
+		bonus->valType = Bonus::ADDITIVE_VALUE;
+		bonus->limiter.reset(new CCreatureTypeLimiter(*VLC->creh->creatures[spec.additionalinfo], true));
+		result.push_back(bonus);
+		break;
+	case 5: //spell damage bonus in percent
+		bonus->type = Bonus::SPECIFIC_SPELL_DAMAGE;
+		bonus->valType = Bonus::BASE_NUMBER; //current spell system is screwed
+		bonus->subtype = spec.subtype; //spell id
+		result.push_back(bonus);
+		break;
+	case 6: //damage bonus for bless (Adela)
+		bonus->type = Bonus::SPECIAL_BLESS_DAMAGE;
+		bonus->subtype = spec.subtype; //spell id if you ever wanted to use it otherwise
+		bonus->additionalInfo = spec.additionalinfo; //damage factor
+		bonus->updater.reset(new TimesHeroLevelUpdater());
+		result.push_back(bonus);
+		break;
+	case 7: //maxed mastery for spell
+		bonus->type = Bonus::MAXED_SPELL;
+		bonus->subtype = spec.subtype; //spell id
+		result.push_back(bonus);
+		break;
+	case 8: //peculiar spells - enchantments
+		bonus->type = Bonus::SPECIAL_PECULIAR_ENCHANT;
+		bonus->subtype = spec.subtype; //spell id
+		bonus->additionalInfo = spec.additionalinfo; //0, 1 for Coronius
+		result.push_back(bonus);
+		break;
+	case 9: //upgrade creatures
+		{
+			const auto &creatures = VLC->creh->creatures;
+			bonus->type = Bonus::SPECIAL_UPGRADE;
+			bonus->subtype = spec.subtype; //base id
+			bonus->additionalInfo = spec.additionalinfo; //target id
+			result.push_back(bonus);
+			//propagate for regular upgrades of base creature
+			for(auto cre_id : creatures[spec.subtype]->upgrades)
+			{
+				std::shared_ptr<Bonus> upgradeUpgradedVersion = std::make_shared<Bonus>(*bonus);
+				upgradeUpgradedVersion->subtype = cre_id;
+				result.push_back(upgradeUpgradedVersion);
+			}
+		}
+		break;
+	case 10: //resource generation
+		bonus->type = Bonus::GENERATE_RESOURCE;
+		bonus->subtype = spec.subtype;
+		result.push_back(bonus);
+		break;
+	case 11: //starting skill with mastery (Adrienne)
+		logMod->warn("Secondary skill mastery is no longer supported as specialty.");
+		break;
+	case 12: //army speed
+		bonus->type = Bonus::STACKS_SPEED;
+		result.push_back(bonus);
+		break;
+	case 13: //Dragon bonuses (Mutare)
+		bonus->type = Bonus::PRIMARY_SKILL;
+		bonus->valType = Bonus::ADDITIVE_VALUE;
+		switch(spec.subtype)
+		{
+		case 1:
+			bonus->subtype = PrimarySkill::ATTACK;
+			break;
+		case 2:
+			bonus->subtype = PrimarySkill::DEFENSE;
+			break;
+		}
+		bonus->limiter.reset(new HasAnotherBonusLimiter(Bonus::DRAGON_NATURE));
+		result.push_back(bonus);
+		break;
+	default:
+		logMod->warn("Unknown hero specialty %d", spec.type);
+		break;
+	}
+
+	return result;
+}
+
+// convert deprecated format
+std::vector<std::shared_ptr<Bonus>> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec)
+{
+	std::vector<std::shared_ptr<Bonus>> result;
+	for(std::shared_ptr<Bonus> oldBonus : spec.bonuses)
+	{
+		if(oldBonus->type == Bonus::SPECIAL_SPELL_LEV || oldBonus->type == Bonus::SPECIAL_BLESS_DAMAGE)
+		{
+			// these bonuses used to auto-scale with hero level
+			std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*oldBonus);
+			newBonus->updater = std::make_shared<TimesHeroLevelUpdater>();
+			result.push_back(newBonus);
+		}
+		else if(spec.growsWithLevel)
+		{
+			std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*oldBonus);
+			switch(newBonus->type)
+			{
+			case Bonus::SECONDARY_SKILL_PREMY:
+				break; // ignore - used to be overwritten based on SPECIAL_SECONDARY_SKILL
+			case Bonus::SPECIAL_SECONDARY_SKILL:
+				newBonus->type = Bonus::SECONDARY_SKILL_PREMY;
+				newBonus->updater = std::make_shared<TimesHeroLevelUpdater>();
+				result.push_back(newBonus);
+				break;
+			case Bonus::PRIMARY_SKILL:
+				if((newBonus->subtype == PrimarySkill::ATTACK || newBonus->subtype == PrimarySkill::DEFENSE) && newBonus->limiter)
+				{
+					const std::shared_ptr<CCreatureTypeLimiter> creatureLimiter = std::dynamic_pointer_cast<CCreatureTypeLimiter>(newBonus->limiter);
+					if(creatureLimiter)
+					{
+						const CCreature * cre = creatureLimiter->creature;
+						int creStat = newBonus->subtype == PrimarySkill::ATTACK ? cre->getAttack(false) : cre->getDefence(false);
+						int creLevel = cre->level ? cre->level : 5;
+						newBonus->updater = std::make_shared<GrowsWithLevelUpdater>(creStat, creLevel);
+					}
+					result.push_back(newBonus);
+				}
+				break;
+			default:
+				result.push_back(newBonus);
+			}
+		}
+		else
+		{
+			result.push_back(oldBonus);
+		}
+	}
+	return result;
+}
+
+void CHeroHandler::beforeValidate(JsonNode & object)
 {
-	//deprecated, used only for original spciealties
-	for(const JsonNode &specialty : node["specialties"].Vector())
+	//handle "base" specialty info
+	JsonNode & specialtyNode = object["specialty"];
+	if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT)
 	{
-		SSpecialtyInfo spec;
+		const JsonNode & base = specialtyNode["base"];
+		if(!base.isNull())
+		{
+			if(specialtyNode["bonuses"].isNull())
+			{
+				logMod->warn("specialty has base without bonuses");
+			}
+			else
+			{
+				JsonMap & bonuses = specialtyNode["bonuses"].Struct();
+				for(std::pair<std::string, JsonNode> keyValue : bonuses)
+					JsonUtils::inherit(bonuses[keyValue.first], base);
+			}
+		}
+	}
+}
 
-		spec.type = specialty["type"].Float();
-		spec.val = specialty["val"].Float();
-		spec.subtype = specialty["subtype"].Float();
-		spec.additionalinfo = specialty["info"].Float();
+void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
+{
+	int sid = hero->ID.getNum();
+	auto prepSpec = [=](std::shared_ptr<Bonus> bonus)
+	{
+		bonus->duration = Bonus::PERMANENT;
+		bonus->source = Bonus::HERO_SPECIAL;
+		bonus->sid = sid;
+		return bonus;
+	};
 
-		hero->spec.push_back(spec); //put a copy of dummy
+	//deprecated, used only for original specialties
+	const JsonNode & specialtiesNode = node["specialties"];
+	if (!specialtiesNode.isNull())
+	{
+		logMod->warn("Hero %s has deprecated specialties format.", hero->identifier);
+		for(const JsonNode &specialty : specialtiesNode.Vector())
+		{
+			SSpecialtyInfo spec;
+			spec.type = specialty["type"].Integer();
+			spec.val = specialty["val"].Integer();
+			spec.subtype = specialty["subtype"].Integer();
+			spec.additionalinfo = specialty["info"].Integer();
+			//we convert after loading completes, to have all identifiers for json logging
+			hero->specDeprecated.push_back(spec);
+		}
+	}
+	//new(er) format, using bonus system
+	const JsonNode & specialtyNode = node["specialty"];
+	if(specialtyNode.getType() == JsonNode::JsonType::DATA_VECTOR)
+	{
+		//deprecated middle-aged format
+		for(const JsonNode & specialty : node["specialty"].Vector())
+		{
+			SSpecialtyBonus hs;
+			hs.growsWithLevel = specialty["growsWithLevel"].Bool();
+			for (const JsonNode & bonus : specialty["bonuses"].Vector())
+				hs.bonuses.push_back(prepSpec(JsonUtils::parseBonus(bonus)));
+			hero->specialtyDeprecated.push_back(hs);
+		}
 	}
-	//new format, using bonus system
-	for(const JsonNode &specialty : node["specialty"].Vector())
+	else if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT)
 	{
-		SSpecialtyBonus hs;
-		hs.growsWithLevel = specialty["growsWithLevel"].Bool();
-		for (const JsonNode & bonus : specialty["bonuses"].Vector())
+		//creature specialty - alias for simplicity
+		if(!specialtyNode["creature"].isNull())
 		{
-			auto b = JsonUtils::parseBonus(bonus);
-			hs.bonuses.push_back (b);
+			VLC->modh->identifiers.requestIdentifier("creature", specialtyNode["creature"], [hero](si32 creature) {
+				// use legacy format for delayed conversion (must have all creature data loaded, also for upgrades)
+				SSpecialtyInfo spec;
+				spec.type = 1;
+				spec.additionalinfo = creature;
+				hero->specDeprecated.push_back(spec);
+			});
+		}
+		if(!specialtyNode["bonuses"].isNull())
+		{
+			//proper new format
+			for(auto keyValue : specialtyNode["bonuses"].Struct())
+				hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second)));
 		}
-		hero->specialty.push_back (hs); //now, how to get CGHeroInstance from it?
 	}
 }
 
@@ -561,6 +840,65 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod
 	VLC->modh->identifiers.registerObject(scope, "hero", name, object->ID.getNum());
 }
 
+void CHeroHandler::afterLoadFinalization()
+{
+	for(ConstTransitivePtr<CHero> hero : heroes)
+	{
+		if(hero->specDeprecated.size() > 0 || hero->specialtyDeprecated.size() > 0)
+		{
+			logMod->debug("Converting specialty format for hero %s(%s)", hero->identifier, VLC->townh->encodeFaction(hero->heroClass->faction));
+			std::vector<std::shared_ptr<Bonus>> convertedBonuses;
+			for(const SSpecialtyInfo & spec : hero->specDeprecated)
+			{
+				for(std::shared_ptr<Bonus> b : SpecialtyInfoToBonuses(spec, hero->ID.getNum()))
+					convertedBonuses.push_back(b);
+			}
+			for(const SSpecialtyBonus & spec : hero->specialtyDeprecated)
+			{
+				for(std::shared_ptr<Bonus> b : SpecialtyBonusToBonuses(spec))
+					convertedBonuses.push_back(b);
+			}
+			hero->specDeprecated.clear();
+			hero->specialtyDeprecated.clear();
+			// store and create json for logging
+			std::vector<JsonNode> specVec;
+			std::vector<std::string> specNames;
+			for(std::shared_ptr<Bonus> bonus : convertedBonuses)
+			{
+				hero->specialty.push_back(bonus);
+				specVec.push_back(bonus->toJsonNode());
+				// find fitting & unique bonus name
+				std::string bonusName = bonus->nameForBonus();
+				if(vstd::contains(specNames, bonusName))
+				{
+					int suffix = 2;
+					while(vstd::contains(specNames, bonusName + std::to_string(suffix)))
+						suffix++;
+					bonusName += std::to_string(suffix);
+				}
+				specNames.push_back(bonusName);
+			}
+			// log new format for easy copy-and-paste
+			JsonNode specNode(JsonNode::JsonType::DATA_STRUCT);
+			if(specVec.size() > 1)
+			{
+				JsonNode base = JsonUtils::intersect(specVec);
+				if(base.containsBaseData())
+				{
+					specNode["base"] = base;
+					for(JsonNode & node : specVec)
+						node = JsonUtils::difference(node, base);
+				}
+			}
+			// add json for bonuses
+			specNode["bonuses"].Struct();
+			for(int i = 0; i < specVec.size(); i++)
+				specNode["bonuses"][specNames[i]] = specVec[i];
+			logMod->trace("\"specialty\" : %s", specNode.toJson(true));
+		}
+	}
+}
+
 ui32 CHeroHandler::level (ui64 experience) const
 {
 	return boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel);

+ 18 - 4
lib/CHeroHandler.h

@@ -71,8 +71,9 @@ public:
 
 	CHeroClass * heroClass;
 	std::vector<std::pair<SecondarySkill, ui8> > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert)
-	std::vector<SSpecialtyInfo> spec;
-	std::vector<SSpecialtyBonus> specialty;
+	std::vector<SSpecialtyInfo> specDeprecated;
+	std::vector<SSpecialtyBonus> specialtyDeprecated;
+	BonusList specialty;
 	std::set<SpellID> spells;
 	bool haveSpellBook;
 	bool special; // hero is special and won't be placed in game (unless preset on map), e.g. campaign heroes
@@ -98,8 +99,15 @@ public:
 		h & initialArmy;
 		h & heroClass;
 		h & secSkillsInit;
-		h & spec;
-		h & specialty;
+		if(version >= 781)
+		{
+			h & specialty;
+		}
+		else
+		{
+			h & specDeprecated;
+			h & specialtyDeprecated;
+		}
 		h & spells;
 		h & haveSpellBook;
 		h & sex;
@@ -120,6 +128,10 @@ public:
 	}
 };
 
+// convert deprecated format
+std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid);
+std::vector<std::shared_ptr<Bonus>> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec);
+
 class DLL_LINKAGE CHeroClass
 {
 public:
@@ -289,8 +301,10 @@ public:
 
 	std::vector<JsonNode> loadLegacyData(size_t dataSize) override;
 
+	void beforeValidate(JsonNode & object);
 	void loadObject(std::string scope, std::string name, const JsonNode & data) override;
 	void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
+	void afterLoadFinalization() override;
 
 	CHeroHandler();
 	~CHeroHandler();

+ 301 - 6
lib/HeroBonus.cpp

@@ -20,6 +20,7 @@
 #include "CSkillHandler.h"
 #include "CStack.h"
 #include "CArtHandler.h"
+#include "StringConstants.h"
 
 #define FOREACH_PARENT(pname) 	TNodes lparents; getParents(lparents); for(CBonusSystemNode *pname : lparents)
 #define FOREACH_CPARENT(pname) 	TCNodes lparents; getParents(lparents); for(const CBonusSystemNode *pname : lparents)
@@ -80,6 +81,12 @@ const std::map<std::string, TPropagatorPtr> bonusPropagatorMap =
 	{"GLOBAL_EFFECT", std::make_shared<CPropagatorNodeType>(CBonusSystemNode::GLOBAL_EFFECTS)}
 }; //untested
 
+const std::map<std::string, TUpdaterPtr> bonusUpdaterMap =
+{
+	{"TIMES_HERO_LEVEL", std::make_shared<TimesHeroLevelUpdater>()},
+	{"TIMES_STACK_LEVEL", std::make_shared<TimesStackLevelUpdater>()}
+};
+
 ///CBonusProxy
 CBonusProxy::CBonusProxy(const IBonusBearer * Target, CSelector Selector)
 	: cachedLast(0),
@@ -579,22 +586,28 @@ void CBonusSystemNode::getParents(TNodes &out)
 
 void CBonusSystemNode::getBonusesRec(BonusList &out, const CSelector &selector, const CSelector &limit) const
 {
+	BonusList beforeUpdate;
 	FOREACH_CPARENT(p)
 	{
-		p->getBonusesRec(out, selector, limit);
+		p->getBonusesRec(beforeUpdate, selector, limit);
 	}
+	bonuses.getBonuses(beforeUpdate, selector, limit);
 
-	bonuses.getBonuses(out, selector, limit);
+	for(auto b : beforeUpdate)
+		out.push_back(update(b));
 }
 
 void CBonusSystemNode::getAllBonusesRec(BonusList &out) const
 {
+	BonusList beforeUpdate;
 	FOREACH_CPARENT(p)
 	{
-		p->getAllBonusesRec(out);
+		p->getAllBonusesRec(beforeUpdate);
 	}
+	bonuses.getAllBonuses(beforeUpdate);
 
-	bonuses.getAllBonuses(out);
+	for(auto b : beforeUpdate)
+		out.push_back(update(b));
 }
 
 const TBonusListPtr CBonusSystemNode::getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root, const std::string &cachingStr) const
@@ -686,6 +699,13 @@ const TBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelecto
 	return ret;
 }
 
+const std::shared_ptr<Bonus> CBonusSystemNode::update(const std::shared_ptr<Bonus> b) const
+{
+	if(b->updater)
+		return b->updater->update(b, *this);
+	return b;
+}
+
 CBonusSystemNode::CBonusSystemNode()
 	: bonuses(true),
 	exportedBonuses(true),
@@ -782,7 +802,7 @@ void CBonusSystemNode::popBonuses(const CSelector &s)
 		child->popBonuses(s);
 }
 
-void CBonusSystemNode::updateBonuses(const CSelector &s)
+void CBonusSystemNode::reduceBonusDurations(const CSelector &s)
 {
 	BonusList bl;
 	exportedBonuses.getBonuses(bl, s, Selector::all);
@@ -794,7 +814,7 @@ void CBonusSystemNode::updateBonuses(const CSelector &s)
 	}
 
 	for(CBonusSystemNode *child : children)
-		child->updateBonuses(s);
+		child->reduceBonusDurations(s);
 }
 
 void CBonusSystemNode::addNewBonus(const std::shared_ptr<Bonus>& b)
@@ -1142,6 +1162,85 @@ std::string Bonus::Description() const
 	return str.str();
 }
 
+JsonNode subtypeToJson(Bonus::BonusType type, int subtype)
+{
+	switch(type)
+	{
+	case Bonus::PRIMARY_SKILL:
+		return JsonUtils::stringNode("primSkill." + PrimarySkill::names[subtype]);
+	case Bonus::SECONDARY_SKILL_PREMY:
+		return JsonUtils::stringNode("skill." + NSecondarySkill::names[subtype]);
+	case Bonus::SPECIAL_SPELL_LEV:
+	case Bonus::SPECIFIC_SPELL_DAMAGE:
+	case Bonus::SPECIAL_BLESS_DAMAGE:
+	case Bonus::MAXED_SPELL:
+	case Bonus::SPECIAL_PECULIAR_ENCHANT:
+		return JsonUtils::stringNode("spell." + (*VLC->spellh)[SpellID::ESpellID(subtype)]->identifier);
+	case Bonus::SPECIAL_UPGRADE:
+		return JsonUtils::stringNode("creature." + CreatureID::encode(subtype));
+	case Bonus::GENERATE_RESOURCE:
+		return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]);
+	default:
+		return JsonUtils::intNode(subtype);
+	}
+}
+
+JsonNode additionalInfoToJson(Bonus::BonusType type, int addInfo)
+{
+	switch(type)
+	{
+	case Bonus::SPECIAL_UPGRADE:
+		return JsonUtils::stringNode("creature." + CreatureID::encode(addInfo));
+	default:
+		return JsonUtils::intNode(addInfo);
+	}
+}
+
+JsonNode Bonus::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+
+	root["type"].String() = vstd::findKey(bonusNameMap, type);
+	if(subtype != -1)
+		root["subtype"] = subtypeToJson(type, subtype);
+	if(additionalInfo != -1)
+		root["addInfo"] = additionalInfoToJson(type, additionalInfo);
+	if(val != 0)
+		root["val"].Integer() = val;
+	if(valType != ADDITIVE_VALUE)
+		root["valueType"].String() = vstd::findKey(bonusValueMap, valType);
+	if(limiter)
+		root["limiters"].Vector().push_back(limiter->toJsonNode());
+	if(updater)
+		root["updater"] = updater->toJsonNode();
+	return root;
+}
+
+std::string Bonus::nameForBonus() const
+{
+	switch(type)
+	{
+	case Bonus::PRIMARY_SKILL:
+		return PrimarySkill::names[subtype];
+	case Bonus::SECONDARY_SKILL_PREMY:
+		return NSecondarySkill::names[subtype];
+	case Bonus::SPECIAL_SPELL_LEV:
+	case Bonus::SPECIFIC_SPELL_DAMAGE:
+	case Bonus::SPECIAL_BLESS_DAMAGE:
+	case Bonus::MAXED_SPELL:
+	case Bonus::SPECIAL_PECULIAR_ENCHANT:
+		return (*VLC->spellh)[SpellID::ESpellID(subtype)]->identifier;
+	case Bonus::SPECIAL_UPGRADE:
+		return CreatureID::encode(subtype) + "2" + CreatureID::encode(additionalInfo);
+	case Bonus::GENERATE_RESOURCE:
+		return GameConstants::RESOURCE_NAMES[subtype];
+	case Bonus::STACKS_SPEED:
+		return "speed";
+	default:
+		return vstd::findKey(bonusNameMap, type);
+	}
+}
+
 Bonus::Bonus(ui16 Dur, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype)
 	: duration(Dur), type(Type), subtype(Subtype), source(Src), val(Val), sid(ID), description(Desc)
 {
@@ -1306,6 +1405,11 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus)
 	printField(effectRange);
 #undef printField
 
+	if(bonus.limiter)
+		out << "\tLimiter: " << bonus.limiter->toString() << "\n";
+	if(bonus.updater)
+		out << "\tUpdater: " << bonus.updater->toString() << "\n";
+
 	return out;
 }
 
@@ -1341,6 +1445,18 @@ int ILimiter::limit(const BonusLimitationContext &context) const /*return true t
 	return false;
 }
 
+std::string ILimiter::toString() const
+{
+	return typeid(*this).name();
+}
+
+JsonNode ILimiter::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+	root["type"].String() = toString();
+	return root;
+}
+
 int CCreatureTypeLimiter::limit(const BonusLimitationContext &context) const
 {
 	const CCreature *c = retrieveCreature(&context.node);
@@ -1366,6 +1482,26 @@ void CCreatureTypeLimiter::setCreature (CreatureID id)
 	creature = VLC->creh->creatures[id];
 }
 
+std::string CCreatureTypeLimiter::toString() const
+{
+	char buf[100];
+	sprintf(buf, "CCreatureTypeLimiter(creature=%s, includeUpgrades=%s)",
+		creature->identifier.c_str(),
+		(includeUpgrades ? "true" : "false"));
+	return std::string(buf);
+}
+
+JsonNode CCreatureTypeLimiter::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+
+	root["type"].String() = "CREATURE_TYPE_LIMITER";
+	root["parameters"].Vector().push_back(JsonUtils::stringNode(creature->identifier));
+	root["parameters"].Vector().push_back(JsonUtils::boolNode(includeUpgrades));
+
+	return root;
+}
+
 HasAnotherBonusLimiter::HasAnotherBonusLimiter( Bonus::BonusType bonus )
 	: type(bonus), subtype(0), isSubtypeRelevant(false)
 {
@@ -1390,6 +1526,32 @@ int HasAnotherBonusLimiter::limit(const BonusLimitationContext &context) const
 	return NOT_SURE;
 }
 
+std::string HasAnotherBonusLimiter::toString() const
+{
+	char buf[100];
+
+	std::string typeName = vstd::findKey(bonusNameMap, type);
+	if(isSubtypeRelevant)
+		sprintf(buf, "HasAnotherBonusLimiter(type=%s, subtype=%d)",	typeName.c_str(), subtype);
+	else
+		sprintf(buf, "HasAnotherBonusLimiter(type=%s)",	typeName.c_str());
+
+	return std::string(buf);
+}
+
+JsonNode HasAnotherBonusLimiter::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+	std::string typeName = vstd::findKey(bonusNameMap, type);
+
+	root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER";
+	root["parameters"].Vector().push_back(JsonUtils::stringNode(typeName));
+	if(isSubtypeRelevant)
+		root["parameters"].Vector().push_back(JsonUtils::intNode(subtype));
+
+	return root;
+}
+
 IPropagator::~IPropagator()
 {
 
@@ -1543,3 +1705,136 @@ void LimiterList::add( TLimiterPtr limiter )
 {
 	limiters.push_back(limiter);
 }
+
+// Updaters
+
+std::shared_ptr<Bonus> Bonus::addUpdater(TUpdaterPtr Updater)
+{
+	updater = Updater;
+	return this->shared_from_this();
+}
+
+IUpdater::~IUpdater()
+{
+}
+
+const std::shared_ptr<Bonus> IUpdater::update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const
+{
+	return b;
+}
+
+std::string IUpdater::toString() const
+{
+	return typeid(*this).name();
+}
+
+JsonNode IUpdater::toJsonNode() const
+{
+	return JsonNode(JsonNode::JsonType::DATA_NULL);
+}
+
+GrowsWithLevelUpdater::GrowsWithLevelUpdater() : valPer20(0), stepSize(1)
+{
+}
+
+GrowsWithLevelUpdater::GrowsWithLevelUpdater(int valPer20, int stepSize) : valPer20(valPer20), stepSize(stepSize)
+{
+}
+
+const std::shared_ptr<Bonus> GrowsWithLevelUpdater::update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const
+{
+	if(context.getNodeType() == CBonusSystemNode::HERO)
+	{
+		int level = static_cast<const CGHeroInstance &>(context).level;
+		int steps = stepSize ? level / stepSize : level;
+		//rounding follows format for HMM3 creature specialty bonus
+		int newVal = (valPer20 * steps + 19) / 20;
+		//return copy of bonus with updated val
+		std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*b);
+		newBonus->val = newVal;
+		return newBonus;
+	}
+	return b;
+}
+
+std::string GrowsWithLevelUpdater::toString() const
+{
+	return boost::str(boost::format("GrowsWithLevelUpdater(valPer20=%d, stepSize=%d)") % valPer20 % stepSize);
+}
+
+JsonNode GrowsWithLevelUpdater::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+
+	root["type"].String() = "GROWS_WITH_LEVEL";
+	root["parameters"].Vector().push_back(JsonUtils::intNode(valPer20));
+	if(stepSize > 1)
+		root["parameters"].Vector().push_back(JsonUtils::intNode(stepSize));
+
+	return root;
+}
+
+TimesHeroLevelUpdater::TimesHeroLevelUpdater()
+{
+}
+
+const std::shared_ptr<Bonus> TimesHeroLevelUpdater::update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const
+{
+	if(context.getNodeType() == CBonusSystemNode::HERO)
+	{
+		int level = static_cast<const CGHeroInstance &>(context).level;
+		std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*b);
+		newBonus->val *= level;
+		return newBonus;
+	}
+	return b;
+}
+
+std::string TimesHeroLevelUpdater::toString() const
+{
+	return "TimesHeroLevelUpdater";
+}
+
+JsonNode TimesHeroLevelUpdater::toJsonNode() const
+{
+	return JsonUtils::stringNode("TIMES_HERO_LEVEL");
+}
+
+TimesStackLevelUpdater::TimesStackLevelUpdater()
+{
+}
+
+const std::shared_ptr<Bonus> TimesStackLevelUpdater::update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const
+{
+	if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE)
+	{
+		int level = static_cast<const CStackInstance &>(context).getLevel();
+		std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*b);
+		newBonus->val *= level;
+		return newBonus;
+	}
+	else if(context.getNodeType() == CBonusSystemNode::STACK_BATTLE)
+	{
+		const CStack & stack = static_cast<const CStack &>(context);
+		//only update if stack doesn't have an instance (summons, war machines)
+		//otherwise we'd end up multiplying twice
+		if(stack.base == nullptr)
+		{
+			int level = stack.type->level;
+			std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*b);
+			newBonus->val *= level;
+			return newBonus;
+		}
+	}
+	return b;
+}
+
+std::string TimesStackLevelUpdater::toString() const
+{
+	return "TimesStackLevelUpdater";
+}
+
+JsonNode TimesStackLevelUpdater::toJsonNode() const
+{
+	return JsonUtils::stringNode("TIMES_STACK_LEVEL");
+}

+ 90 - 5
lib/HeroBonus.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "GameConstants.h"
+#include "JsonNode.h"
 
 class CCreature;
 struct Bonus;
@@ -17,11 +18,13 @@ class IBonusBearer;
 class CBonusSystemNode;
 class ILimiter;
 class IPropagator;
+class IUpdater;
 class BonusList;
 
 typedef std::shared_ptr<BonusList> TBonusListPtr;
 typedef std::shared_ptr<ILimiter> TLimiterPtr;
 typedef std::shared_ptr<IPropagator> TPropagatorPtr;
+typedef std::shared_ptr<IUpdater> TUpdaterPtr;
 typedef std::set<CBonusSystemNode*> TNodes;
 typedef std::set<const CBonusSystemNode*> TCNodes;
 typedef std::vector<CBonusSystemNode *> TNodesVector;
@@ -204,14 +207,14 @@ private:
 	BONUS_NAME(NO_LUCK) /*eg. when fighting on cursed ground*/	\
 	BONUS_NAME(NO_MORALE) /*eg. when fighting on cursed ground*/ \
 	BONUS_NAME(DARKNESS) /*val = radius */ \
-	BONUS_NAME(SPECIAL_SECONDARY_SKILL) /*val = id, additionalInfo = value per level in percent*/ \
-	BONUS_NAME(SPECIAL_SPELL_LEV) /*val = id, additionalInfo = value per level in percent*/\
+	BONUS_NAME(SPECIAL_SECONDARY_SKILL) /*subtype = id, val = value per level in percent*/ \
+	BONUS_NAME(SPECIAL_SPELL_LEV) /*subtype = id, val = value per level in percent*/\
 	BONUS_NAME(SPELL_DAMAGE) /*val = value*/\
 	BONUS_NAME(SPECIFIC_SPELL_DAMAGE) /*subtype = id of spell, val = value*/\
 	BONUS_NAME(SPECIAL_BLESS_DAMAGE) /*val = spell (bless), additionalInfo = value per level in percent*/\
 	BONUS_NAME(MAXED_SPELL) /*val = id*/\
 	BONUS_NAME(SPECIAL_PECULIAR_ENCHANT) /*blesses and curses with id = val dependent on unit's level, subtype = 0 or 1 for Coronius*/\
-	BONUS_NAME(SPECIAL_UPGRADE) /*val = base, additionalInfo = target */\
+	BONUS_NAME(SPECIAL_UPGRADE) /*subtype = base, additionalInfo = target */\
 	BONUS_NAME(DRAGON_NATURE) \
 	BONUS_NAME(CREATURE_DAMAGE)/*subtype 0 = both, 1 = min, 2 = max*/\
 	BONUS_NAME(EXP_MULTIPLIER)/* val - percent of additional exp gained by stack/commander (base value 100)*/\
@@ -340,6 +343,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 
 	TLimiterPtr limiter;
 	TPropagatorPtr propagator;
+	TUpdaterPtr updater;
 
 	std::string description;
 
@@ -362,6 +366,10 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 		h & effectRange;
 		h & limiter;
 		h & propagator;
+		if(version >= 781)
+		{
+			h & updater;
+		}
 	}
 
 	template <typename Ptr>
@@ -419,9 +427,12 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 	}
 
 	std::string Description() const;
+	JsonNode toJsonNode() const;
+	std::string nameForBonus() const; // generate suitable name for bonus - e.g. for storing in json struct
 
 	std::shared_ptr<Bonus> addLimiter(TLimiterPtr Limiter); //returns this for convenient chain-calls
 	std::shared_ptr<Bonus> addPropagator(TPropagatorPtr Propagator); //returns this for convenient chain-calls
+	std::shared_ptr<Bonus> addUpdater(TUpdaterPtr Updater); //returns this for convenient chain-calls
 };
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus);
@@ -583,6 +594,8 @@ public:
 	virtual ~ILimiter();
 
 	virtual int limit(const BonusLimitationContext &context) const; //0 - accept bonus; 1 - drop bonus; 2 - delay (drops eventually)
+	virtual std::string toString() const;
+	virtual JsonNode toJsonNode() const;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -663,6 +676,7 @@ private:
 	void getBonusesRec(BonusList &out, const CSelector &selector, const CSelector &limit) const;
 	void getAllBonusesRec(BonusList &out) const;
 	const TBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr) const;
+	const std::shared_ptr<Bonus> update(const std::shared_ptr<Bonus> b) const;
 
 public:
 	explicit CBonusSystemNode();
@@ -703,7 +717,7 @@ public:
 	///removes bonuses by selector
 	void popBonuses(const CSelector &s);
 	///updates count of remaining turns and removes outdated bonuses by selector
-	void updateBonuses(const CSelector &s);
+	void reduceBonusDurations(const CSelector &s);
 	virtual std::string bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const {return "";}; //description or bonus name
 	virtual std::string nodeName() const;
 
@@ -847,6 +861,8 @@ public:
 	void setCreature (CreatureID id);
 
 	int limit(const BonusLimitationContext &context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -867,6 +883,8 @@ public:
 	HasAnotherBonusLimiter(Bonus::BonusType bonus, TBonusSubtype _subtype);
 
 	int limit(const BonusLimitationContext &context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -997,7 +1015,7 @@ extern DLL_LINKAGE const std::map<std::string, ui16> bonusDurationMap;
 extern DLL_LINKAGE const std::map<std::string, Bonus::LimitEffect> bonusLimitEffect;
 extern DLL_LINKAGE const std::map<std::string, TLimiterPtr> bonusLimiterMap;
 extern DLL_LINKAGE const std::map<std::string, TPropagatorPtr> bonusPropagatorMap;
-
+extern DLL_LINKAGE const std::map<std::string, TUpdaterPtr> bonusUpdaterMap;
 
 // BonusList template that requires full interface of CBonusSystemNode
 template <class InputIterator>
@@ -1006,3 +1024,70 @@ void BonusList::insert(const int position, InputIterator first, InputIterator la
 	bonuses.insert(bonuses.begin() + position, first, last);
 	changed();
 }
+
+// observers for updating bonuses based on certain events (e.g. hero gaining level)
+
+class DLL_LINKAGE IUpdater
+{
+public:
+	virtual ~IUpdater();
+
+	virtual const std::shared_ptr<Bonus> update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const;
+	virtual std::string toString() const;
+	virtual JsonNode toJsonNode() const;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+	}
+};
+
+class DLL_LINKAGE GrowsWithLevelUpdater : public IUpdater
+{
+public:
+	int valPer20;
+	int stepSize;
+
+	GrowsWithLevelUpdater();
+	GrowsWithLevelUpdater(int valPer20, int stepSize = 1);
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<IUpdater &>(*this);
+		h & valPer20;
+		h & stepSize;
+	}
+
+	const std::shared_ptr<Bonus> update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
+};
+
+class DLL_LINKAGE TimesHeroLevelUpdater : public IUpdater
+{
+public:
+	TimesHeroLevelUpdater();
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<IUpdater &>(*this);
+	}
+
+	const std::shared_ptr<Bonus> update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
+};
+
+class DLL_LINKAGE TimesStackLevelUpdater : public IUpdater
+{
+public:
+	TimesStackLevelUpdater();
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<IUpdater &>(*this);
+	}
+
+	const std::shared_ptr<Bonus> update(const std::shared_ptr<Bonus> b, const CBonusSystemNode & context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
+};

+ 27 - 14
lib/JsonDetail.cpp

@@ -32,19 +32,22 @@ void JsonWriter::writeContainer(Iterator begin, Iterator end)
 
 	while (begin != end)
 	{
-		out<<",\n";
+		out << (compactMode ? ", " : ",\n");
 		writeEntry(begin++);
 	}
 
-	out<<"\n";
+	out << (compactMode ? "" : "\n");
 	prefix.resize(prefix.size()-1);
 }
 
 void JsonWriter::writeEntry(JsonMap::const_iterator entry)
 {
-	if (!entry->second.meta.empty())
-		out << prefix << " // " << entry->second.meta << "\n";
-	out << prefix;
+	if(!compactMode)
+	{
+		if (!entry->second.meta.empty())
+			out << prefix << " // " << entry->second.meta << "\n";
+		out << prefix;
+	}
 	writeString(entry->first);
 	out << " : ";
 	writeNode(entry->second);
@@ -52,9 +55,12 @@ void JsonWriter::writeEntry(JsonMap::const_iterator entry)
 
 void JsonWriter::writeEntry(JsonVector::const_iterator entry)
 {
-	if (!entry->meta.empty())
-		out << prefix << " // " << entry->meta << "\n";
-	out << prefix;
+	if(!compactMode)
+	{
+		if (!entry->meta.empty())
+			out << prefix << " // " << entry->meta << "\n";
+		out << prefix;
+	}
 	writeNode(*entry);
 }
 
@@ -94,6 +100,10 @@ void JsonWriter::writeString(const std::string &string)
 
 void JsonWriter::writeNode(const JsonNode &node)
 {
+	bool originalMode = compactMode;
+	if(compact && !compactMode && node.isCompact())
+		compactMode = true;
+
 	switch(node.getType())
 	{
 		break; case JsonNode::JsonType::DATA_NULL:
@@ -112,21 +122,24 @@ void JsonWriter::writeNode(const JsonNode &node)
 			writeString(node.String());
 
 		break; case JsonNode::JsonType::DATA_VECTOR:
-			out << "[" << "\n";
+			out << "[" << (compactMode ? " " : "\n");
 			writeContainer(node.Vector().begin(), node.Vector().end());
-			out << prefix << "]";
+			out << (compactMode ? " " : prefix) << "]";
 
 		break; case JsonNode::JsonType::DATA_STRUCT:
-			out << "{" << "\n";
+			out << "{" << (compactMode ? " " : "\n");
 			writeContainer(node.Struct().begin(), node.Struct().end());
-			out << prefix << "}";
+			out << (compactMode ? " " : prefix) << "}";
+
 		break; case JsonNode::JsonType::DATA_INTEGER:
 			out << node.Integer();
 	}
+
+	compactMode = originalMode;
 }
 
-JsonWriter::JsonWriter(std::ostream & output)
-	: out(output)
+JsonWriter::JsonWriter(std::ostream & output, bool compact)
+	: out(output), compact(compact)
 {
 }
 

+ 5 - 1
lib/JsonDetail.h

@@ -16,6 +16,10 @@ class JsonWriter
 	//prefix for each line (tabulation)
 	std::string prefix;
 	std::ostream & out;
+	//sets whether compact nodes are written in single-line format
+	bool compact;
+	//tracks whether we are currently using single-line format
+	bool compactMode = false;
 public:
 	template<typename Iterator>
 	void writeContainer(Iterator begin, Iterator end);
@@ -23,7 +27,7 @@ public:
 	void writeEntry(JsonVector::const_iterator entry);
 	void writeString(const std::string & string);
 	void writeNode(const JsonNode & node);
-	JsonWriter(std::ostream & output);
+	JsonWriter(std::ostream & output, bool compact = false);
 };
 
 //Tiny string class that uses const char* as data for speed, members are private

+ 189 - 7
lib/JsonNode.cpp

@@ -214,6 +214,50 @@ bool JsonNode::isNumber() const
 	return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT;
 }
 
+bool JsonNode::containsBaseData() const
+{
+	switch(type)
+	{
+	case JsonType::DATA_NULL:
+		return false;
+	case JsonType::DATA_STRUCT:
+		for(auto elem : *data.Struct)
+		{
+			if(elem.second.containsBaseData())
+				return true;
+		}
+		return false;
+	default:
+		//other types (including vector) cannot be extended via merge
+		return true;
+	}
+}
+
+bool JsonNode::isCompact() const
+{
+	switch(type)
+	{
+	case JsonType::DATA_VECTOR:
+		for(JsonNode & elem : *data.Vector)
+		{
+			if(!elem.isCompact())
+				return false;
+		}
+		return true;
+	case JsonType::DATA_STRUCT:
+		{
+			int propertyCount = data.Struct->size();
+			if(propertyCount == 0)
+				return true;
+			else if(propertyCount == 1)
+				return data.Struct->begin()->second.isCompact();
+		}
+		return false;
+	default:
+		return true;
+	}
+}
+
 void JsonNode::clear()
 {
 	setType(JsonType::DATA_NULL);
@@ -367,10 +411,10 @@ JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer)
 	return ::resolvePointer(*this, jsonPointer);
 }
 
-std::string JsonNode::toJson() const
+std::string JsonNode::toJson(bool compact) const
 {
 	std::ostringstream out;
-	JsonWriter writer(out);
+	JsonWriter writer(out, compact);
 	writer.writeNode(*this);
 	out << "\n";
 	return out.str();
@@ -395,7 +439,7 @@ std::shared_ptr<Bonus> JsonUtils::parseBonus (const JsonVector &ability_vec) //T
 	auto it = bonusNameMap.find(type);
 	if (it == bonusNameMap.end())
 	{
-		logMod->error("Error: invalid ability type %s", type);
+		logMod->error("Error: invalid ability type %s.", type);
 		return b;
 	}
 	b->type = it->second;
@@ -413,7 +457,7 @@ const T & parseByMap(const std::map<std::string, T> & map, const JsonNode * val,
 		auto it = map.find(type);
 		if (it == map.end())
 		{
-			logMod->error("Error: invalid %s%s", err, type);
+			logMod->error("Error: invalid %s%s.", err, type);
 			return defaultValue;
 		}
 		else
@@ -445,7 +489,7 @@ void JsonUtils::resolveIdentifier(si32 &var, const JsonNode &node, std::string n
 				});
 				break;
 			default:
-				logMod->error("Error! Wrong identifier used for value of %s", name);
+				logMod->error("Error! Wrong identifier used for value of %s.", name);
 		}
 	}
 }
@@ -489,7 +533,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 	auto it = bonusNameMap.find(type);
 	if (it == bonusNameMap.end())
 	{
-		logMod->error("Error: invalid ability type %s", type);
+		logMod->error("Error: invalid ability type %s.", type);
 		return false;
 	}
 	b->type = it->second;
@@ -580,7 +624,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 							auto it = bonusNameMap.find(anotherBonusType);
 							if (it == bonusNameMap.end())
 							{
-								logMod->error("Error: invalid ability type %s", anotherBonusType);
+								logMod->error("Error: invalid ability type %s.", anotherBonusType);
 								continue;
 							}
 							l2->type = it->second;
@@ -603,6 +647,31 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 	if (!value->isNull())
 		b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type ");
 
+	value = &ability["updater"];
+	if(!value->isNull())
+	{
+		const JsonNode & updaterJson = *value;
+		switch(updaterJson.getType())
+		{
+		case JsonNode::JsonType::DATA_STRING:
+			b->addUpdater(parseByMap(bonusUpdaterMap, &updaterJson, "updater type "));
+			break;
+		case JsonNode::JsonType::DATA_STRUCT:
+			if(updaterJson["type"].String() == "GROWS_WITH_LEVEL")
+			{
+				std::shared_ptr<GrowsWithLevelUpdater> updater = std::make_shared<GrowsWithLevelUpdater>();
+				const JsonVector param = updaterJson["parameters"].Vector();
+				updater->valPer20 = param[0].Integer();
+				if(param.size() > 1)
+					updater->stepSize = param[1].Integer();
+				b->addUpdater(updater);
+			}
+			else
+				logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String());
+			break;
+		}
+	}
+
 	return true;
 }
 
@@ -725,6 +794,7 @@ bool JsonUtils::validate(const JsonNode &node, std::string schemaName, std::stri
 	{
 		logMod->warn("Data in %s is invalid!", dataName);
 		logMod->warn(log);
+		logMod->trace("%s json: %s", dataName, node.toJson(true));
 	}
 	return log.empty();
 }
@@ -822,6 +892,90 @@ void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base)
 	descendant.swap(inheritedNode);
 }
 
+JsonNode JsonUtils::intersect(const std::vector<JsonNode> & nodes, bool pruneEmpty)
+{
+	if(nodes.size() == 0)
+		return nullNode;
+
+	JsonNode result = nodes[0];
+	for(int i = 1; i < nodes.size(); i++)
+	{
+		if(result.isNull())
+			break;
+		result = JsonUtils::intersect(result, nodes[i], pruneEmpty);
+	}
+	return result;
+}
+
+JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty)
+{
+	if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT)
+	{
+		// intersect individual properties
+		JsonNode result(JsonNode::JsonType::DATA_STRUCT);
+		for(auto property : a.Struct())
+		{
+			if(vstd::contains(b.Struct(), property.first))
+			{
+				JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second);
+				if(pruneEmpty && !propertyIntersect.containsBaseData())
+					continue;
+				result[property.first] = propertyIntersect;
+			}
+		}
+		return result;
+	}
+	else
+	{
+		// not a struct - same or different, no middle ground
+		if(a == b)
+			return a;
+	}
+	return nullNode;
+}
+
+JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base)
+{
+	auto addsInfo = [](JsonNode diff) -> bool
+	{
+		switch(diff.getType())
+		{
+		case JsonNode::JsonType::DATA_NULL:
+			return false;
+		case JsonNode::JsonType::DATA_STRUCT:
+			return diff.Struct().size() > 0;
+		default:
+			return true;
+		}
+	};
+
+	if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT)
+	{
+		// subtract individual properties
+		JsonNode result(JsonNode::JsonType::DATA_STRUCT);
+		for(auto property : node.Struct())
+		{
+			if(vstd::contains(base.Struct(), property.first))
+			{
+				const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second);
+				if(addsInfo(propertyDifference))
+					result[property.first] = propertyDifference;
+			}
+			else
+			{
+				result[property.first] = property.second;
+			}
+		}
+		return result;
+	}
+	else
+	{
+		if(node == base)
+			return nullNode;
+	}
+	return node;
+}
+
 JsonNode JsonUtils::assembleFromFiles(std::vector<std::string> files)
 {
 	bool isValid;
@@ -860,3 +1014,31 @@ JsonNode JsonUtils::assembleFromFiles(std::string filename)
 	}
 	return result;
 }
+
+DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value)
+{
+	JsonNode node;
+	node.Bool() = value;
+	return node;
+}
+
+DLL_LINKAGE JsonNode JsonUtils::floatNode(double value)
+{
+	JsonNode node;
+	node.Float() = value;
+	return node;
+}
+
+DLL_LINKAGE JsonNode JsonUtils::stringNode(std::string value)
+{
+	JsonNode node;
+	node.String() = value;
+	return node;
+}
+
+DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value)
+{
+	JsonNode node;
+	node.Integer() = value;
+	return node;
+}

+ 27 - 1
lib/JsonNode.h

@@ -75,6 +75,10 @@ public:
 
 	bool isNull() const;
 	bool isNumber() const;
+	/// true if node contains not-null data that cannot be extended via merging
+	/// used for generating common base node from multiple nodes (e.g. bonuses)
+	bool containsBaseData() const;
+	bool isCompact() const;
 	/// removes all data from node and sets type to null
 	void clear();
 
@@ -110,7 +114,7 @@ public:
 	JsonNode & operator[](std::string child);
 	const JsonNode & operator[](std::string child) const;
 
-	std::string toJson() const;
+	std::string toJson(bool compact = false) const;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -188,6 +192,22 @@ namespace JsonUtils
      */
 	DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base);
 
+	/**
+	 * @brief construct node representing the common structure of input nodes
+	 * @param pruneEmpty - omit common properties whose intersection is empty
+	 * different types: null
+	 * struct: recursive intersect on common properties
+	 * other: input if equal, null otherwise
+	 */
+	DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true);
+	DLL_LINKAGE JsonNode intersect(const std::vector<JsonNode> & nodes, bool pruneEmpty = true);
+
+	/**
+	 * @brief construct node representing the difference "node - base"
+	 * merging difference with base gives node
+	 */
+	DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base);
+
 	/**
 	 * @brief generate one Json structure from multiple files
 	 * @param files - list of filenames with parts of json structure
@@ -220,6 +240,12 @@ namespace JsonUtils
 	/// get schema by json URI: vcmi:<name of file in schemas directory>#<entry in file, optional>
 	/// example: schema "vcmi:settings" is used to check user settings
 	DLL_LINKAGE const JsonNode & getSchema(std::string URI);
+
+	/// for easy construction of JsonNodes; helps with inserting primitives into vector node
+	DLL_LINKAGE JsonNode boolNode(bool value);
+	DLL_LINKAGE JsonNode floatNode(double value);
+	DLL_LINKAGE JsonNode stringNode(std::string value);
+	DLL_LINKAGE JsonNode intNode(si64 value);
 }
 
 namespace JsonDetail

+ 2 - 2
lib/NetPacksLib.cpp

@@ -1085,8 +1085,8 @@ DLL_LINKAGE void NewTurn::applyGs(CGameState *gs)
 
 	// Update bonuses before doing anything else so hero don't get more MP than needed
 	gs->globalEffects.popBonuses(Bonus::OneDay); //works for children -> all game objs
-	gs->globalEffects.updateBonuses(Bonus::NDays);
-	gs->globalEffects.updateBonuses(Bonus::OneWeek);
+	gs->globalEffects.reduceBonusDurations(Bonus::NDays);
+	gs->globalEffects.reduceBonusDurations(Bonus::OneWeek);
 	//TODO not really a single root hierarchy, what about bonuses placed elsewhere? [not an issue with H3 mechanics but in the future...]
 
 	for(NewTurn::Hero h : heroes) //give mana/movement point

+ 1 - 1
lib/battle/BattleInfo.cpp

@@ -765,7 +765,7 @@ void BattleInfo::nextRound(int32_t roundNr)
 	for(CStack * s : stacks)
 	{
 		// new turn effects
-		s->updateBonuses(Bonus::NTurns);
+		s->reduceBonusDurations(Bonus::NTurns);
 
 		s->afterNewRound();
 	}

+ 30 - 244
lib/mapObjects/CGHeroInstance.cpp

@@ -496,9 +496,6 @@ void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter()
 void CGHeroInstance::initObj(CRandomGenerator & rand)
 {
 	blockVisit = true;
-	auto  hs = new HeroSpecial();
-	hs->setNodeType(CBonusSystemNode::SPECIALTY);
-	attachTo(hs); //do we ever need to detach it?
 
 	if(!type)
 		initHero(rand); //TODO: set up everything for prison before specialties are configured
@@ -514,246 +511,23 @@ void CGHeroInstance::initObj(CRandomGenerator & rand)
 			appearance = customApp.get();
 	}
 
-	for(const auto &spec : type->spec) //TODO: unfity with bonus system
-	{
-		auto bonus = std::make_shared<Bonus>();
-		bonus->val = spec.val;
-		bonus->sid = id.getNum(); //from the hero, specialty has no unique id
-		bonus->duration = Bonus::PERMANENT;
-		bonus->source = Bonus::HERO_SPECIAL;
-		switch (spec.type)
-		{
-			case 1:// creature specialty
-				{
-					hs->growsWithLevel = true;
-
-					const CCreature &specCreature = *VLC->creh->creatures[spec.additionalinfo]; //creature in which we have specialty
-
-					//bonus->additionalInfo = spec.additionalinfo; //creature id, should not be used again - this works only with limiter
-					bonus->limiter.reset(new CCreatureTypeLimiter (specCreature, true)); //with upgrades
-					bonus->type = Bonus::PRIMARY_SKILL;
-					bonus->valType = Bonus::ADDITIVE_VALUE;
-
-					bonus->subtype = PrimarySkill::ATTACK;
-					hs->addNewBonus(bonus);
-
-					bonus = std::make_shared<Bonus>(*bonus);
-					bonus->subtype = PrimarySkill::DEFENSE;
-					hs->addNewBonus(bonus);
-					//values will be calculated later
-
-					bonus = std::make_shared<Bonus>(*bonus);
-					bonus->type = Bonus::STACKS_SPEED;
-					bonus->val = 1; //+1 speed
-					hs->addNewBonus(bonus);
-				}
-				break;
-			case 2://secondary skill
-				hs->growsWithLevel = true;
-				bonus->type = Bonus::SPECIAL_SECONDARY_SKILL; //needs to be recalculated with level, based on this value
-				bonus->valType = Bonus::BASE_NUMBER; // to receive nonzero value
-				bonus->subtype = spec.subtype; //skill id
-				bonus->val = spec.val; //value per level, in percent
-				hs->addNewBonus(bonus);
-				bonus = std::make_shared<Bonus>(*bonus);
-
-				switch (spec.additionalinfo)
-				{
-					case 0: //normal
-						bonus->valType = Bonus::PERCENT_TO_BASE;
-						break;
-					case 1: //when it's navigation or there's no 'base' at all
-						bonus->valType = Bonus::PERCENT_TO_ALL;
-						break;
-				}
-				bonus->type = Bonus::SECONDARY_SKILL_PREMY; //value will be calculated later
-				hs->addNewBonus(bonus);
-				break;
-			case 3://spell damage bonus, level dependent but calculated elsewhere
-				bonus->type = Bonus::SPECIAL_SPELL_LEV;
-				bonus->subtype = spec.subtype;
-				hs->addNewBonus(bonus);
-				break;
-			case 4://creature stat boost
-				switch (spec.subtype)
-				{
-					case 1://attack
-						bonus->type = Bonus::PRIMARY_SKILL;
-						bonus->subtype = PrimarySkill::ATTACK;
-						break;
-					case 2://defense
-						bonus->type = Bonus::PRIMARY_SKILL;
-						bonus->subtype = PrimarySkill::DEFENSE;
-						break;
-					case 3:
-						bonus->type = Bonus::CREATURE_DAMAGE;
-						bonus->subtype = 0; //both min and max
-						break;
-					case 4://hp
-						bonus->type = Bonus::STACK_HEALTH;
-						break;
-					case 5:
-						bonus->type = Bonus::STACKS_SPEED;
-						break;
-					default:
-						continue;
-				}
-				bonus->additionalInfo = spec.additionalinfo; //creature id
-				bonus->valType = Bonus::ADDITIVE_VALUE;
-				bonus->limiter.reset(new CCreatureTypeLimiter (*VLC->creh->creatures[spec.additionalinfo], true));
-				hs->addNewBonus(bonus);
-				break;
-			case 5://spell damage bonus in percent
-				bonus->type = Bonus::SPECIFIC_SPELL_DAMAGE;
-				bonus->valType = Bonus::BASE_NUMBER; // current spell system is screwed
-				bonus->subtype = spec.subtype; //spell id
-				hs->addNewBonus(bonus);
-				break;
-			case 6://damage bonus for bless (Adela)
-				bonus->type = Bonus::SPECIAL_BLESS_DAMAGE;
-				bonus->subtype = spec.subtype; //spell id if you ever wanted to use it otherwise
-				bonus->additionalInfo = spec.additionalinfo; //damage factor
-				hs->addNewBonus(bonus);
-				break;
-			case 7://maxed mastery for spell
-				bonus->type = Bonus::MAXED_SPELL;
-				bonus->subtype = spec.subtype; //spell i
-				hs->addNewBonus(bonus);
-				break;
-			case 8://peculiar spells - enchantments
-				bonus->type = Bonus::SPECIAL_PECULIAR_ENCHANT;
-				bonus->subtype = spec.subtype; //spell id
-				bonus->additionalInfo = spec.additionalinfo;//0, 1 for Coronius
-				hs->addNewBonus(bonus);
-				break;
-			case 9://upgrade creatures
-			{
-				const auto &creatures = VLC->creh->creatures;
-				bonus->type = Bonus::SPECIAL_UPGRADE;
-				bonus->subtype = spec.subtype; //base id
-				bonus->additionalInfo = spec.additionalinfo; //target id
-				hs->addNewBonus(bonus);
-				bonus = std::make_shared<Bonus>(*bonus);
-
-				for(auto cre_id : creatures[spec.subtype]->upgrades)
-				{
-					bonus->subtype = cre_id; //propagate for regular upgrades of base creature
-					hs->addNewBonus(bonus);
-					bonus = std::make_shared<Bonus>(*bonus);
-				}
-				break;
-			}
-			case 10://resource generation
-				bonus->type = Bonus::GENERATE_RESOURCE;
-				bonus->subtype = spec.subtype;
-				hs->addNewBonus(bonus);
-				break;
-			case 11://starting skill with mastery (Adrienne)
-				setSecSkillLevel(SecondarySkill(spec.val), spec.additionalinfo, true);
-				break;
-			case 12://army speed
-				bonus->type = Bonus::STACKS_SPEED;
-				hs->addNewBonus(bonus);
-				break;
-			case 13://Dragon bonuses (Mutare)
-				bonus->type = Bonus::PRIMARY_SKILL;
-				bonus->valType = Bonus::ADDITIVE_VALUE;
-				switch (spec.subtype)
-				{
-					case 1:
-						bonus->subtype = PrimarySkill::ATTACK;
-						break;
-					case 2:
-						bonus->subtype = PrimarySkill::DEFENSE;
-						break;
-				}
-				bonus->limiter.reset(new HasAnotherBonusLimiter(Bonus::DRAGON_NATURE));
-				hs->addNewBonus(bonus);
-				break;
-			default:
-				logGlobal->warn("Unexpected hero %s specialty %d", type->name, spec.type);
-				break;
-		}
-	}
-	specialty.push_back(hs); //will it work?
-
-	for (auto hs2 : type->specialty) //copy active (probably growing) bonuses from hero prootype to hero object
-	{
-		auto  hs = new HeroSpecial();
-		attachTo(hs); //do we ever need to detach it?
-
-		hs->setNodeType(CBonusSystemNode::SPECIALTY);
-		for (auto bonus : hs2.bonuses)
-		{
-			hs->addNewBonus (bonus);
-		}
-		hs->growsWithLevel = hs2.growsWithLevel;
-
-		specialty.push_back(hs); //will it work?
-	}
+	//copy active (probably growing) bonuses from hero prototype to hero object
+	for(std::shared_ptr<Bonus> b : type->specialty)
+		addNewBonus(b);
+	//dito for old-style bonuses -> compatibility for old savegames
+	for(SSpecialtyBonus & sb : type->specialtyDeprecated)
+		for(std::shared_ptr<Bonus> b : sb.bonuses)
+			addNewBonus(b);
+	for(SSpecialtyInfo & spec : type->specDeprecated)
+		for(std::shared_ptr<Bonus> b : SpecialtyInfoToBonuses(spec, type->ID.getNum()))
+			addNewBonus(b);
 
 	//initialize bonuses
 	recreateSecondarySkillsBonuses();
-	Updatespecialty();
 
 	mana = manaLimit(); //after all bonuses are taken into account, make sure this line is the last one
 	type->name = name;
 }
-void CGHeroInstance::Updatespecialty() //TODO: calculate special value of bonuses on-the-fly?
-{
-	for (auto hs : specialty)
-	{
-		if (hs->growsWithLevel)
-		{
-			//const auto &creatures = VLC->creh->creatures;
-
-			for(auto& b : hs->getBonusList())
-			{
-				switch (b->type)
-				{
-					case Bonus::SECONDARY_SKILL_PREMY:
-						b->val = (hs->valOfBonuses(Bonus::SPECIAL_SECONDARY_SKILL, b->subtype) * level);
-						break; //use only hero skills as bonuses to avoid feedback loop
-					case Bonus::PRIMARY_SKILL: //for creatures, that is
-					{
-						const CCreature * cre = nullptr;
-						int creLevel = 0;
-						if (auto creatureLimiter = std::dynamic_pointer_cast<CCreatureTypeLimiter>(b->limiter)) //TODO: more general eveluation of bonuses?
-						{
-							cre = creatureLimiter->creature;
-							creLevel = cre->level;
-							if (!creLevel)
-							{
-								creLevel = 5; //treat ballista as tier 5
-							}
-						}
-						else //no creature found, can't calculate value
-						{
-							logGlobal->warn("Primary skill specialty growth supported only with creature type limiters");
-							break;
-						}
-
-						double primSkillModifier = (int)(level / creLevel) / 20.0;
-						int param;
-						switch (b->subtype)
-						{
-							case PrimarySkill::ATTACK:
-								param = cre->getPrimSkillLevel(PrimarySkill::ATTACK);
-								break;
-							case PrimarySkill::DEFENSE:
-								param = cre->getPrimSkillLevel(PrimarySkill::DEFENSE);
-								break;
-							default:
-								continue;
-						}
-						b->val = ceil(param * (1 + primSkillModifier)) - param; //yep, overcomplicated but matches original
-						break;
-					}
-				}
-			}
-		}
-	}
-}
 
 void CGHeroInstance::recreateSecondarySkillsBonuses()
 {
@@ -766,6 +540,23 @@ void CGHeroInstance::recreateSecondarySkillsBonuses()
 			updateSkill(SecondarySkill(skill_info.first), level);
 }
 
+void CGHeroInstance::recreateSpecialtyBonuses(std::vector<HeroSpecial *> & specialtyDeprecated)
+{
+	auto HeroSpecialToSpecialtyBonus = [](HeroSpecial & hs) -> SSpecialtyBonus
+	{
+		SSpecialtyBonus sb;
+		sb.growsWithLevel = hs.growsWithLevel;
+		sb.bonuses = hs.getBonusList();
+		return sb;
+	};
+
+	for(HeroSpecial * hs : specialtyDeprecated)
+	{
+		for(std::shared_ptr<Bonus> b : SpecialtyBonusToBonuses(HeroSpecialToSpecialtyBonus(*hs)))
+			addNewBonus(b);
+	}
+}
+
 void CGHeroInstance::updateSkill(SecondarySkill which, int val)
 {
 	auto skillBonus = (*VLC->skillh)[which]->getBonus(val);
@@ -863,7 +654,7 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base,
 	base *= (100.0 + maxSchoolBonus) / 100.0;
 
 	if(affectedStack && affectedStack->creatureLevel() > 0) //Hero specials like Solmyr, Deemer
-		base *= (100. + ((valOfBonuses(Bonus::SPECIAL_SPELL_LEV, spell->getIndex()) * level) / affectedStack->creatureLevel())) / 100.0;
+		base *= (100. + valOfBonuses(Bonus::SPECIAL_SPELL_LEV, spell->getIndex()) / affectedStack->creatureLevel()) / 100.0;
 
 	return base;
 }
@@ -1214,11 +1005,6 @@ int CGHeroInstance::maxSpellLevel() const
 void CGHeroInstance::deserializationFix()
 {
 	artDeserializationFix(this);
-
-	for (auto hs : specialty)
-	{
-		attachTo (hs);
-	}
 }
 
 CBonusSystemNode * CGHeroInstance::whereShouldBeAttached(CGameState *gs)
@@ -1471,8 +1257,8 @@ void CGHeroInstance::levelUp(std::vector<SecondarySkill> skills)
 		}
 	}
 
-	//specialty
-	Updatespecialty();
+	//update specialty and other bonuses that scale with level
+	treeHasChanged();
 }
 
 void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand)

+ 10 - 5
lib/mapObjects/CGHeroInstance.h

@@ -95,7 +95,8 @@ public:
 		}
 	} patrol;
 
-	struct DLL_LINKAGE HeroSpecial : CBonusSystemNode
+	// deprecated - used only for loading of old saves
+	struct HeroSpecial : CBonusSystemNode
 	{
 		bool growsWithLevel;
 
@@ -108,8 +109,6 @@ public:
 		}
 	};
 
-	std::vector<HeroSpecial*> specialty;
-
 	struct DLL_LINKAGE SecondarySkillsInfo
 	{
 		//skills are determined, initialized at map start
@@ -215,7 +214,6 @@ public:
 	void pushPrimSkill(PrimarySkill::PrimarySkill which, int val);
 	ui8 maxlevelsToMagicSchool() const;
 	ui8 maxlevelsToWisdom() const;
-	void Updatespecialty();
 	void recreateSecondarySkillsBonuses();
 	void updateSkill(SecondarySkill which, int val);
 
@@ -269,6 +267,7 @@ protected:
 
 private:
 	void levelUpAutomatically(CRandomGenerator & rand);
+	void recreateSpecialtyBonuses(std::vector<HeroSpecial*> & specialtyDeprecated);
 
 public:
 	std::string getHeroTypeName() const;
@@ -297,7 +296,13 @@ public:
 		h & visitedTown;
 		h & boat;
 		h & type;
-		h & specialty;
+		if(version < 781)
+		{
+			std::vector<HeroSpecial*> specialtyDeprecated;
+			h & specialtyDeprecated;
+			if(!h.saving)
+				recreateSpecialtyBonuses(specialtyDeprecated);
+		}
 		h & commander;
 		h & visitedObjects;
 		BONUS_TREE_DESERIALIZATION_FIX

+ 6 - 0
lib/registerTypes/RegisterTypes.h

@@ -135,6 +135,12 @@ void registerTypesMapObjectTypes(Serializer &s)
 	REGISTER_GENERIC_HANDLER(CGWitchHut);
 
 #undef REGISTER_GENERIC_HANDLER
+
+	s.template registerType<IUpdater, GrowsWithLevelUpdater>();
+	s.template registerType<IUpdater, TimesHeroLevelUpdater>();
+	s.template registerType<IUpdater, TimesStackLevelUpdater>();
+	//new types (other than netpacks) must register here
+	//order of type registration is critical for loading old savegames
 }
 
 template<typename Serializer>

+ 1 - 1
lib/serializer/CSerializer.h

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

+ 1 - 1
lib/spells/effects/Timed.cpp

@@ -219,7 +219,7 @@ void Timed::prepareEffects(SetStackEffect & sse, const Mechanics * m, const Effe
 		}
 		if(casterHero && casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, m->getSpellIndex())) //TODO: better handling of bonus percentages
 		{
-			int damagePercent = casterHero->level * casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, m->getSpellIndex()) / tier;
+			int damagePercent = casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, m->getSpellIndex()) / tier;
 			Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, m->getSpellIndex(), 0, Bonus::PERCENT_TO_ALL);
 			specialBonus.turnsRemain = duration;
 			buffer.push_back(specialBonus);