1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-29 00:41:38 +02:00

Implement shortcut for hero skill specialties

Should make changes to secondary skill bonuses less mod-breaking, and
reduce chances of incorrectly defined specialties in mods
This commit is contained in:
Ivan Savenko
2025-06-03 17:32:15 +03:00
parent 4fda657296
commit eb9a5bb71b
17 changed files with 170 additions and 382 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@
} }
} }
}, },
"required" : ["name", "basic", "advanced", "expert"], "required" : ["name", "basic", "advanced", "expert", "specialty", "gainChance" ],
"properties" : { "properties" : {
"name" : { "name" : {
"type" : "string", "type" : "string",
@ -61,6 +61,11 @@
"type" : "boolean", "type" : "boolean",
"description" : "This skill is minor obligatory (like H3 Magic school)" "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" : { "gainChance" : {
"description" : "Chance for the skill to be offered on level-up (heroClass may override)", "description" : "Chance for the skill to be offered on level-up (heroClass may override)",
"type" : "object", "type" : "object",

View File

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

View File

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

View File

@ -26,6 +26,11 @@
"advanced": {Skill level format}, "advanced": {Skill level format},
"expert": {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) // Chance for the skill to be offered on level-up (heroClass may override)
"gainChance" : { "gainChance" : {
// Chance for hero classes with might affinity // Chance for hero classes with might affinity

View File

@ -14,16 +14,14 @@
#include "CSkillHandler.h" #include "CSkillHandler.h"
#include "bonuses/Updaters.h"
#include "constants/StringConstants.h" #include "constants/StringConstants.h"
#include "filesystem/Filesystem.h" #include "filesystem/Filesystem.h"
#include "json/JsonBonus.h" #include "json/JsonBonus.h"
#include "json/JsonUtils.h" #include "json/JsonUtils.h"
#include "modding/IdentifierStorage.h" #include "modding/IdentifierStorage.h"
#include "modding/ModUtility.h"
#include "modding/ModScope.h"
#include "texts/CGeneralTextHandler.h" #include "texts/CGeneralTextHandler.h"
#include "texts/CLegacyConfigParser.h" #include "texts/CLegacyConfigParser.h"
#include "texts/TextOperations.h"
#include "GameLibrary.h" #include "GameLibrary.h"
VCMI_LIB_NAMESPACE_BEGIN 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.iconMedium = levelNode["images"]["medium"].String();
skillAtLevel.iconLarge = levelNode["images"]["large"].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()); logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum());
return skill; return skill;

View File

@ -71,6 +71,9 @@ public:
std::array<si32, 2> gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes 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 updateFrom(const JsonNode & data);
void serializeJson(JsonSerializeFormat & handler); void serializeJson(JsonSerializeFormat & handler);

View File

@ -21,6 +21,7 @@
#include "../../json/JsonBonus.h" #include "../../json/JsonBonus.h"
#include "../../json/JsonUtils.h" #include "../../json/JsonUtils.h"
#include "../../modding/IdentifierStorage.h" #include "../../modding/IdentifierStorage.h"
#include "../../CSkillHandler.h"
#include "../../texts/CGeneralTextHandler.h" #include "../../texts/CGeneralTextHandler.h"
#include "../../texts/CLegacyConfigParser.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 /// 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; 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)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize));
result.push_back(bonus); 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; return result;
} }
@ -201,15 +215,7 @@ void CHeroHandler::beforeValidate(JsonNode & object)
} }
} }
void CHeroHandler::afterLoadFinalization() void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) const
{
for(const auto & functor : callAfterLoadFinalization)
functor();
callAfterLoadFinalization.clear();
}
void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
{ {
auto prepSpec = [=](std::shared_ptr<Bonus> bonus) auto prepSpec = [=](std::shared_ptr<Bonus> bonus)
{ {
@ -230,18 +236,21 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
//creature specialty - alias for simplicity //creature specialty - alias for simplicity
if(!specialtyNode["creature"].isNull()) if(!specialtyNode["creature"].isNull())
{ {
JsonNode creatureNode = specialtyNode["creature"]; LIBRARY->identifiers()->requestIdentifier("creature", specialtyNode["creature"], [this, hero, prepSpec](si32 creature)
std::function<void()> specialtyLoader = [creatureNode, hero, prepSpec]
{ {
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()) for(const auto & keyValue : specialtyNode["bonuses"].Struct())

View File

@ -28,11 +28,12 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
/// helpers for loading to avoid huge load functions /// helpers for loading to avoid huge load functions
void loadHeroArmy(CHero * hero, const JsonNode & node) const; void loadHeroArmy(CHero * hero, const JsonNode & node) const;
void loadHeroSkills(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(); 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: public:
ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount
@ -44,7 +45,6 @@ public:
void beforeValidate(JsonNode & object) override; 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) override;
void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
void afterLoadFinalization() override;
CHeroHandler(); CHeroHandler();
~CHeroHandler(); ~CHeroHandler();