瀏覽代碼

Merge pull request #5768 from IvanSavenko/skill_shortcut

Implement shortcut for hero skill specialties
Ivan Savenko 4 月之前
父節點
當前提交
403a7f782e

+ 5 - 49
config/heroes/castle.json

@@ -10,16 +10,7 @@
 			{ "skill" : "archery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"archery" : {
-					"type" : "PERCENTAGE_DAMAGE_BOOST",
-					"subtype" : "damageTypeRanged",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "archery"
 		}
 	},
 	"valeska":
@@ -61,16 +52,7 @@
 			{ "skill" : "navigation", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"navigation" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"subtype" : "heroMovementSea",
-					"type" : "MOVEMENT",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "navigation"
 		}
 	},
 	"lordHaart":
@@ -84,16 +66,7 @@
 			{ "skill" : "estates", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"estates" : {
-					"subtype" : "resource.gold",
-					"type" : "GENERATE_RESOURCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "estates"
 		}
 	},
 	"sorsha":
@@ -150,16 +123,7 @@
 			{ "skill" : "firstAid", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"firstAid" : {
-					"subtype" : "spell.firstAid",
-					"type" : "SPECIFIC_SPELL_POWER",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "firstAid"
 		}
 	},
 	"adela":
@@ -265,15 +229,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"loynis":

+ 4 - 38
config/heroes/dungeon.json

@@ -85,16 +85,7 @@
 			{ "skill" : "tactics", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"logistics" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"subtype" : "heroMovementLand",
-					"type" : "MOVEMENT",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "logistics"
 		}
 	},
 	"synca":
@@ -159,15 +150,7 @@
 			{ "skill" : "mysticism", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"mysticism" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_REGENERATION",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "mysticism"
 		}
 	},
 	"malekith":
@@ -182,16 +165,7 @@
 			{ "skill" : "sorcery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"sorcery" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "SPELL_DAMAGE",
-					"subtype" : "spellSchool.any",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "sorcery"
 		}
 	},
 	"jeddite":
@@ -227,15 +201,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"deemer":

+ 7 - 67
config/heroes/fortress.json

@@ -66,16 +66,7 @@
 			{ "skill" : "armorer", "level": "advanced" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"armorer" : {
-					"type" : "GENERAL_DAMAGE_REDUCTION",
-					"subtype" : "damageTypeAll",
-					"targetSourceType" : "SECONDARY_SKILL",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "armorer"
 		}
 	},
 	"alkin":
@@ -166,15 +157,7 @@
 			{ "skill" : "mysticism", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"mysticism" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_REGENERATION",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "mysticism"
 		}
 	},
 	"voy":
@@ -189,16 +172,7 @@
 			{ "skill" : "navigation", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"navigation" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"subtype" : "heroMovementSea",
-					"type" : "MOVEMENT",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "navigation"
 		}
 	},
 	"verdish":
@@ -213,16 +187,7 @@
 			{ "skill" : "firstAid", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"firstAid" : {
-					"subtype" : "spell.firstAid",
-					"type" : "SPECIFIC_SPELL_POWER",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "firstAid"
 		}
 	},
 	"merist":
@@ -258,16 +223,7 @@
 			{ "skill" : "sorcery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"sorcery" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "SPELL_DAMAGE",
-					"subtype" : "spellSchool.any",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "sorcery"
 		}
 	},
 	"andra":
@@ -282,15 +238,7 @@
 			{ "skill" : "intelligence", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"intelligence" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_PER_KNOWLEDGE_PERCENTAGE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "intelligence"
 		}
 	},
 	"tiva":
@@ -305,15 +253,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	}
 }

+ 3 - 28
config/heroes/inferno.json

@@ -126,15 +126,7 @@
 			{ "skill" : "intelligence", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"intelligence" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_PER_KNOWLEDGE_PERCENTAGE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "intelligence"
 		}
 	},
 	"xyron":
@@ -171,15 +163,7 @@
 			{ "skill" : "mysticism", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"mysticism" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_REGENERATION",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "mysticism"
 		}
 	},
 	"olema":
@@ -257,16 +241,7 @@
 			{ "skill" : "sorcery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"sorcery" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "SPELL_DAMAGE",
-					"subtype" : "spellSchool.any",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "sorcery"
 		}
 	},
 	"xarfax":

+ 4 - 37
config/heroes/necropolis.json

@@ -85,15 +85,7 @@
 			{ "skill" : "necromancy", "level": "advanced" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"necromancy" : {
-					"type" : "UNDEAD_RAISE_PERCENTAGE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "necromancy"
 		}
 	},
 	"clavius":
@@ -188,16 +180,7 @@
 			{ "skill" : "sorcery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"sorcery" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "SPELL_DAMAGE",
-					"subtype" : "spellSchool.any",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "sorcery"
 		}
 	},
 	"nimbus":
@@ -212,15 +195,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"thant":
@@ -277,15 +252,7 @@
 			{ "skill" : "necromancy", "level": "advanced" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"necromancy" : {
-					"type" : "UNDEAD_RAISE_PERCENTAGE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "necromancy"
 		}
 	},
 	"nagash":

+ 6 - 57
config/heroes/rampart.json

@@ -10,16 +10,7 @@
 			{ "skill" : "armorer", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"armorer" : {
-					"type" : "GENERAL_DAMAGE_REDUCTION",
-					"subtype" : "damageTypeAll",
-					"targetSourceType" : "SECONDARY_SKILL",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "armorer"
 		}
 	},
 	"ufretin":
@@ -79,15 +70,7 @@
 			{ "skill" : "resistance", "level": "advanced" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"resistance" : {
-					"type" : "MAGIC_RESISTANCE",
-					"targetSourceType" : "SECONDARY_SKILL",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "resistance"
 		}
 	},
 	"ivor":
@@ -129,16 +112,7 @@
 			{ "skill" : "logistics", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"logistics" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"subtype" : "heroMovementLand",
-					"type" : "MOVEMENT",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "logistics"
 		}
 	},
 	"coronius":
@@ -196,15 +170,7 @@
 			{ "skill" : "intelligence", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"intelligence" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_PER_KNOWLEDGE_PERCENTAGE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "intelligence"
 		}
 	},
 	"gem":
@@ -219,16 +185,7 @@
 			{ "skill" : "firstAid", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"firstAid" : {
-					"subtype" : "spell.firstAid",
-					"type" : "SPECIFIC_SPELL_POWER",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "firstAid"
 		}
 	},
 	"malcom":
@@ -243,15 +200,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"melodia":

+ 5 - 49
config/heroes/stronghold.json

@@ -93,16 +93,7 @@
 			{ "skill" : "offence", "level": "advanced" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"offence" : {
-					"subtype" : "damageTypeMelee",
-					"type" : "PERCENTAGE_DAMAGE_BOOST",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "offence"
 		}
 	},
 	"tyraxor":
@@ -131,16 +122,7 @@
 			{ "skill" : "sorcery", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"sorcery" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "SPELL_DAMAGE",
-					"subtype" : "spellSchool.any",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "sorcery"
 		}
 	},
 	"vey":
@@ -170,16 +152,7 @@
 			{ "skill" : "logistics", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"logistics" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"subtype" : "heroMovementLand",
-					"type" : "MOVEMENT",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "logistics"
 		}
 	},
 	"terek":
@@ -236,16 +209,7 @@
 			{ "skill" : "offence", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"offence" : {
-					"subtype" : "damageTypeMelee",
-					"type" : "PERCENTAGE_DAMAGE_BOOST",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "offence"
 		}
 	},
 	"oris":
@@ -260,15 +224,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"saurug":

+ 3 - 28
config/heroes/tower.json

@@ -55,16 +55,7 @@
 			{ "skill" : "armorer", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"armorer" : {
-					"type" : "GENERAL_DAMAGE_REDUCTION",
-					"subtype" : "damageTypeAll",
-					"targetSourceType" : "SECONDARY_SKILL",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "armorer"
 		}
 	},
 	"torosar":
@@ -167,15 +158,7 @@
 			{ "skill" : "mysticism", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"mysticism" : {
-					"targetSourceType" : "SECONDARY_SKILL",
-					"type" : "MANA_REGENERATION",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE"
-				}
-			}
+			"secondary" : "mysticism"
 		}
 	},
 	"serena":
@@ -190,15 +173,7 @@
 			{ "skill" : "eagleEye", "level": "basic" }
 		],
 		"specialty" : {
-			"bonuses" : {
-				"eagleEye" : {
-					"type" : "LEARN_BATTLE_SPELL_CHANCE",
-					"updater" : "TIMES_HERO_LEVEL",
-					"val" : 5,
-					"valueType" : "PERCENT_TO_TARGET_TYPE",
-					"targetSourceType" : "SECONDARY_SKILL"
-				}
-			}
+			"secondary" : "eagleEye"
 		}
 	},
 	"daremyth":

+ 4 - 0
config/schemas/hero.json

@@ -132,6 +132,10 @@
 				"creature" : {
 					"type" : "string",
 					"description" : "Shortcut for defining creature specialty, using standard H3 rules."
+				},
+				"secondary" : {
+					"type" : "string",
+					"description" : "Shortcut for defining secondary skill specialty, using standard H3 rules."
 				}
 			}
 		},

+ 6 - 1
config/schemas/skill.json

@@ -43,7 +43,7 @@
 			}
 		}
 	},
-	"required" : ["name", "basic", "advanced", "expert"],
+	"required" : ["name", "basic", "advanced", "expert", "specialty", "gainChance" ],
 	"properties" : {
 		"name" : {
 			"type" : "string",
@@ -61,6 +61,11 @@
 			"type" : "boolean",
 			"description" : "This skill is minor obligatory (like H3 Magic school)"
 		},
+		"specialty" : {
+			"type" : "array",
+			"description" : "List of bonuses that are affected by hero specialty",
+			"items" : { "type" : "string" }
+		},
 		"gainChance" : {
 			"description" : "Chance for the skill to be offered on level-up (heroClass may override)",
 			"type" : "object",

+ 60 - 0
config/skills.json

@@ -1,6 +1,7 @@
 {
 	"pathfinding" : {
 		"index" : 0,
+		"specialty" : [], // no generic specialty
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -27,6 +28,9 @@
 	},
 	"archery" : {
 		"index" : 1,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -54,6 +58,9 @@
 	},
 	"logistics" : {
 		"index" : 2,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -81,6 +88,9 @@
 	},
 	"scouting" : {
 		"index" : 3,
+		"specialty" : [ // NOTE: no such specialists in H3, hota uses different logic
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -107,6 +117,9 @@
 	},
 	"diplomacy" : {
 		"index" : 4,
+		"specialty" : [ // NOTE: no such specialists in H3
+			"surr"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -141,6 +154,9 @@
 	},
 	"navigation" : {
 		"index" : 5,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -169,6 +185,7 @@
 	},
 	"leadership" : {
 		"index" : 6,
+		"specialty" : [], // generic specialty not applicable
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -195,6 +212,7 @@
 	},
 	"wisdom" : {
 		"index" : 7,
+		"specialty" : [], // generic specialty not applicable
 		"obligatoryMajor" : true,
 		"base" : {
 			"effects" : {
@@ -222,6 +240,9 @@
 	},
 	"mysticism" : {
 		"index" : 8,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -248,6 +269,7 @@
 	},
 	"luck" : {
 		"index" : 9,
+		"specialty" : [], // generic specialty not applicable
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -274,6 +296,7 @@
 	},
 	"ballistics" : {
 		"index" : 10,
+		"specialty" : [], // generic specialty not applicable
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -307,6 +330,9 @@
 	},
 	"eagleEye" : {
 		"index" : 11,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -340,6 +366,9 @@
 	},
 	"necromancy" : {
 		"index" : 12,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -374,6 +403,9 @@
 	},
 	"estates" : {
 		"index" : 13,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -401,6 +433,7 @@
 	},
 	"fireMagic" : {
 		"index" : 14,
+		"specialty" : [], // generic specialty not applicable
 		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
@@ -429,6 +462,7 @@
 	},
 	"airMagic" : {
 		"index" : 15,
+		"specialty" : [], // generic specialty not applicable
 		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
@@ -457,6 +491,7 @@
 	},
 	"waterMagic" : {
 		"index" : 16,
+		"specialty" : [], // generic specialty not applicable
 		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
@@ -485,6 +520,7 @@
 	},
 	"earthMagic" : {
 		"index" : 17,
+		"specialty" : [], // generic specialty not applicable
 		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
@@ -513,6 +549,7 @@
 	},
 	"scholar" : {
 		"index" : 18,
+		"specialty" : [], // generic specialty not applicable
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -539,6 +576,7 @@
 	},
 	"tactics" : {
 		"index" : 19,
+		"specialty" : [], // generic specialty not applicable
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -572,6 +610,7 @@
 	},
 	"artillery" : {
 		"index" : 20,
+		"specialty" : [], // generic specialty not applicable, H3 heroes specialize in ballista creature instead
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -625,6 +664,9 @@
 	},
 	"learning" : {
 		"index" : 21,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -651,6 +693,9 @@
 	},
 	"offence" : {
 		"index" : 22,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -678,6 +723,9 @@
 	},
 	"armorer" : {
 		"index" : 23,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -705,6 +753,9 @@
 	},
 	"intelligence" : {
 		"index" : 24,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -731,6 +782,9 @@
 	},
 	"sorcery" : {
 		"index" : 25,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -758,6 +812,9 @@
 	},
 	"resistance" : {
 		"index" : 26,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {
@@ -784,6 +841,9 @@
 	},
 	"firstAid" : {
 		"index" : 27,
+		"specialty" : [
+			"main"
+		],
 		"base" : {
 			"effects" : {
 				"main" : {

+ 5 - 2
docs/modders/Entities_Format/Hero_Type_Format.md

@@ -129,8 +129,11 @@ In order to make functional hero you also need:
 			"someBonus" : {Bonus Format},
 			"anotherOne" : {Bonus Format}
 		},
-		// Optional. Shortcut for defining creature specialty, using standard H3 rules
-		"creature" : "griffin"
+		// Shortcut for defining creature specialty, using standard H3 rules
+		"creature" : "griffin",
+
+		// Shortcut for defining specialty in secondary skill, using standard H3 rules
+		"secondary" : "offence"
 	}
 }
 ```

+ 5 - 0
docs/modders/Entities_Format/Secondary_Skill_Format.md

@@ -26,6 +26,11 @@
 		"advanced": {Skill level format},
 		"expert":   {Skill level format},
 		
+		// Names of bonuses of the skill that are affected by default secondary skill specialty of a hero
+		"specialty" : [
+			"main"
+		],
+		
 		// Chance for the skill to be offered on level-up (heroClass may override)
 		"gainChance" : {
 			// Chance for hero classes with might affinity

+ 18 - 3
lib/CSkillHandler.cpp

@@ -14,16 +14,14 @@
 
 #include "CSkillHandler.h"
 
+#include "bonuses/Updaters.h"
 #include "constants/StringConstants.h"
 #include "filesystem/Filesystem.h"
 #include "json/JsonBonus.h"
 #include "json/JsonUtils.h"
 #include "modding/IdentifierStorage.h"
-#include "modding/ModUtility.h"
-#include "modding/ModScope.h"
 #include "texts/CGeneralTextHandler.h"
 #include "texts/CLegacyConfigParser.h"
-#include "texts/TextOperations.h"
 #include "GameLibrary.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -253,6 +251,23 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 		skillAtLevel.iconMedium = levelNode["images"]["medium"].String();
 		skillAtLevel.iconLarge = levelNode["images"]["large"].String();
 	}
+
+	for(const auto & b : json["specialty"].Vector())
+	{
+		const auto & bonusNode = json["basic"]["effects"][b.String()];
+
+		if (bonusNode.isStruct())
+		{
+			auto bonus = JsonUtils::parseBonus(bonusNode);
+			bonus->val = 5; // default H3 value, hardcoded for now
+			bonus->updater = std::make_shared<TimesHeroLevelUpdater>();
+			bonus->valType = BonusValueType::PERCENT_TO_TARGET_TYPE;
+			bonus->targetSourceType = BonusSource::SECONDARY_SKILL;
+			skill->specialtyTargetBonuses.push_back(bonus);
+		}
+		else
+			logMod->warn("Failed to load speciality bonus '%s' for skill '%s'", b.String(), identifier);
+	}
 	logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum());
 
 	return skill;

+ 3 - 0
lib/CSkillHandler.h

@@ -71,6 +71,9 @@ public:
 
 	std::array<si32, 2> gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes
 
+	/// Bonuses that should be given to hero that specializes in this skill
+	std::vector<std::shared_ptr<const Bonus>> specialtyTargetBonuses;
+
 	void updateFrom(const JsonNode & data);
 	void serializeJson(JsonSerializeFormat & handler);
 

+ 29 - 20
lib/entities/hero/CHeroHandler.cpp

@@ -21,6 +21,7 @@
 #include "../../json/JsonBonus.h"
 #include "../../json/JsonUtils.h"
 #include "../../modding/IdentifierStorage.h"
+#include "../../CSkillHandler.h"
 #include "../../texts/CGeneralTextHandler.h"
 #include "../../texts/CLegacyConfigParser.h"
 
@@ -140,7 +141,7 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const
 }
 
 /// creates standard H3 hero specialty for creatures
-static std::vector<std::shared_ptr<Bonus>> createCreatureSpecialty(CreatureID cid)
+std::vector<std::shared_ptr<Bonus>> CHeroHandler::createCreatureSpecialty(CreatureID cid) const
 {
 	std::vector<std::shared_ptr<Bonus>> result;
 
@@ -174,6 +175,19 @@ static std::vector<std::shared_ptr<Bonus>> createCreatureSpecialty(CreatureID ci
 		bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize));
 		result.push_back(bonus);
 	}
+	return result;
+}
+
+std::vector<std::shared_ptr<Bonus>> CHeroHandler::createSecondarySkillSpecialty(SecondarySkill skillID) const
+{
+	std::vector<std::shared_ptr<Bonus>> result;
+	const auto & skillPtr = LIBRARY->skillh->objects[skillID.getNum()];
+
+	if (skillPtr->specialtyTargetBonuses.empty())
+		logMod->warn("Skill '%s' does not supports generic specialties!", skillPtr->getJsonKey());
+
+	for (const auto & bonus : skillPtr->specialtyTargetBonuses)
+		result.push_back(std::make_shared<Bonus>(*bonus));
 
 	return result;
 }
@@ -201,15 +215,7 @@ void CHeroHandler::beforeValidate(JsonNode & object)
 	}
 }
 
-void CHeroHandler::afterLoadFinalization()
-{
-	for(const auto & functor : callAfterLoadFinalization)
-		functor();
-
-	callAfterLoadFinalization.clear();
-}
-
-void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
+void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) const
 {
 	auto prepSpec = [=](std::shared_ptr<Bonus> bonus)
 	{
@@ -230,18 +236,21 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
 	//creature specialty - alias for simplicity
 	if(!specialtyNode["creature"].isNull())
 	{
-		JsonNode creatureNode = specialtyNode["creature"];
-
-		std::function<void()> specialtyLoader = [creatureNode, hero, prepSpec]
+		LIBRARY->identifiers()->requestIdentifier("creature", specialtyNode["creature"], [this, hero, prepSpec](si32 creature)
 		{
-			LIBRARY->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature)
-			{
-				for (const auto & bonus : createCreatureSpecialty(CreatureID(creature)))
-					hero->specialty.push_back(prepSpec(bonus));
-			});
-		};
+			for (const auto & bonus : createCreatureSpecialty(CreatureID(creature)))
+				hero->specialty.push_back(prepSpec(bonus));
+		});
+	}
 
-		callAfterLoadFinalization.push_back(specialtyLoader);
+	//secondary skill specialty - alias for simplicity
+	if(!specialtyNode["secondary"].isNull())
+	{
+		LIBRARY->identifiers()->requestIdentifier("secondarySkill", specialtyNode["secondary"], [this, hero, prepSpec](si32 creature)
+		{
+			for (const auto & bonus : createSecondarySkillSpecialty(SecondarySkill(creature)))
+				hero->specialty.push_back(prepSpec(bonus));
+		});
 	}
 
 	for(const auto & keyValue : specialtyNode["bonuses"].Struct())

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

@@ -28,11 +28,12 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
 	/// helpers for loading to avoid huge load functions
 	void loadHeroArmy(CHero * hero, const JsonNode & node) const;
 	void loadHeroSkills(CHero * hero, const JsonNode & node) const;
-	void loadHeroSpecialty(CHero * hero, const JsonNode & node);
+	void loadHeroSpecialty(CHero * hero, const JsonNode & node) const;
 
 	void loadExperience();
 
-	std::vector<std::function<void()>> callAfterLoadFinalization;
+	std::vector<std::shared_ptr<Bonus>> createCreatureSpecialty(CreatureID cid) const;
+	std::vector<std::shared_ptr<Bonus>> createSecondarySkillSpecialty(SecondarySkill skillID) const;
 
 public:
 	ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount
@@ -44,7 +45,6 @@ public:
 	void beforeValidate(JsonNode & object) override;
 	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();