diff --git a/AUTHORS b/AUTHORS index ec6df8cab..01434349c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -68,4 +68,4 @@ Piotr Wójcik aka Chocimier, * Various bug fixes Henning Koehler, - * skill modding + * skill modding, bonus updaters diff --git a/ChangeLog b/ChangeLog index 34f83e7a6..47362e5c3 100644 --- a/ChangeLog +++ b/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 diff --git a/Global.h b/Global.h index 55d215499..5d79c834e 100644 --- a/Global.h +++ b/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 + Key findKey(const std::map & 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::size_type operator-=(Container &c, const Item &i) diff --git a/config/heroes/castle.json b/config/heroes/castle.json index 89b3bc66f..5a8088d14 100644 --- a/config/heroes/castle.json +++ b/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 + } + } + } } } diff --git a/config/heroes/conflux.json b/config/heroes/conflux.json index 64b5aef86..345d5f3ea 100644 --- a/config/heroes/conflux.json +++ b/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 + } + } + } } } diff --git a/config/heroes/dungeon.json b/config/heroes/dungeon.json index 3e3b93ab3..a47b2202e 100644 --- a/config/heroes/dungeon.json +++ b/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" + } + } + } } } diff --git a/config/heroes/fortress.json b/config/heroes/fortress.json index b4c72f8b3..bf163ddb3 100644 --- a/config/heroes/fortress.json +++ b/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" + } + } + } } } diff --git a/config/heroes/inferno.json b/config/heroes/inferno.json index 0019d5aa6..611e66871 100644 --- a/config/heroes/inferno.json +++ b/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 + } + } + } } } diff --git a/config/heroes/necropolis.json b/config/heroes/necropolis.json index d64feb6f9..b24bfd8fd 100644 --- a/config/heroes/necropolis.json +++ b/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 + } + } + } } } diff --git a/config/heroes/rampart.json b/config/heroes/rampart.json index 7d4282fa5..80a8bbde6 100644 --- a/config/heroes/rampart.json +++ b/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" + } } } diff --git a/config/heroes/special.json b/config/heroes/special.json index 841260f44..a46cf317b 100644 --- a/config/heroes/special.json +++ b/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" : [ { diff --git a/config/heroes/stronghold.json b/config/heroes/stronghold.json index a02ef9366..073cc6ff2 100644 --- a/config/heroes/stronghold.json +++ b/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 + } + } + } } } diff --git a/config/heroes/tower.json b/config/heroes/tower.json index b57de5b8a..ff839df68 100644 --- a/config/heroes/tower.json +++ b/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 + } + } + } } } diff --git a/config/schemas/bonus.json b/config/schemas/bonus.json index ec103bcb7..7e6f64852 100644 --- a/config/schemas/bonus.json +++ b/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" diff --git a/config/schemas/hero.json b/config/schemas/hero.json index 923ed9fbe..d88163dc6 100644 --- a/config/schemas/hero.json +++ b/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", diff --git a/config/skills.json b/config/skills.json index 3e9258281..1cc0466a6 100644 --- a/config/skills.json +++ b/config/skills.json @@ -226,7 +226,8 @@ "base" : { "effects" : { "main" : { - "type" : "MANA_REGENERATION", + "subtype" : "skill.mysticism", + "type" : "SECONDARY_SKILL_PREMY", "valueType" : "BASE_NUMBER" } } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 301f31d7d..00bec6032 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -379,31 +379,310 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) } } +// add standard creature specialty to result +void AddSpecialtyForCreature(int creatureID, std::shared_ptr bonus, std::vector> &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 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->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->subtype = PrimarySkill::DEFENSE; + bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefence(false), stepSize)); + result.push_back(bonus); + } +} + +// convert deprecated format +std::vector> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid) +{ + std::vector> result; + + std::shared_ptr bonus = std::make_shared(); + 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 upgradeUpgradedVersion = std::make_shared(*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> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec) +{ + std::vector> result; + for(std::shared_ptr 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 newBonus = std::make_shared(*oldBonus); + newBonus->updater = std::make_shared(); + result.push_back(newBonus); + } + else if(spec.growsWithLevel) + { + std::shared_ptr newBonus = std::make_shared(*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(); + result.push_back(newBonus); + break; + case Bonus::PRIMARY_SKILL: + if((newBonus->subtype == PrimarySkill::ATTACK || newBonus->subtype == PrimarySkill::DEFENSE) && newBonus->limiter) + { + const std::shared_ptr creatureLimiter = std::dynamic_pointer_cast(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(creStat, creLevel); + } + result.push_back(newBonus); + } + break; + default: + result.push_back(newBonus); + } + } + else + { + result.push_back(oldBonus); + } + } + return result; +} + +void CHeroHandler::beforeValidate(JsonNode & object) +{ + //handle "base" specialty info + JsonNode & specialtyNode = object["specialty"]; + if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) + { + 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 keyValue : bonuses) + JsonUtils::inherit(bonuses[keyValue.first], base); + } + } + } +} + void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) { - //deprecated, used only for original spciealties - for(const JsonNode &specialty : node["specialties"].Vector()) + int sid = hero->ID.getNum(); + auto prepSpec = [=](std::shared_ptr bonus) { - SSpecialtyInfo spec; + bonus->duration = Bonus::PERMANENT; + bonus->source = Bonus::HERO_SPECIAL; + bonus->sid = sid; + return bonus; + }; - spec.type = specialty["type"].Float(); - spec.val = specialty["val"].Float(); - spec.subtype = specialty["subtype"].Float(); - spec.additionalinfo = specialty["info"].Float(); - - hero->spec.push_back(spec); //put a copy of dummy - } - //new format, using bonus system - for(const JsonNode &specialty : node["specialty"].Vector()) + //deprecated, used only for original specialties + const JsonNode & specialtiesNode = node["specialties"]; + if (!specialtiesNode.isNull()) { - SSpecialtyBonus hs; - hs.growsWithLevel = specialty["growsWithLevel"].Bool(); - for (const JsonNode & bonus : specialty["bonuses"].Vector()) + logMod->warn("Hero %s has deprecated specialties format.", hero->identifier); + for(const JsonNode &specialty : specialtiesNode.Vector()) { - auto b = JsonUtils::parseBonus(bonus); - hs.bonuses.push_back (b); + 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); + } + } + else if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) + { + //creature specialty - alias for simplicity + if(!specialtyNode["creature"].isNull()) + { + 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 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> convertedBonuses; + for(const SSpecialtyInfo & spec : hero->specDeprecated) + { + for(std::shared_ptr b : SpecialtyInfoToBonuses(spec, hero->ID.getNum())) + convertedBonuses.push_back(b); + } + for(const SSpecialtyBonus & spec : hero->specialtyDeprecated) + { + for(std::shared_ptr b : SpecialtyBonusToBonuses(spec)) + convertedBonuses.push_back(b); + } + hero->specDeprecated.clear(); + hero->specialtyDeprecated.clear(); + // store and create json for logging + std::vector specVec; + std::vector specNames; + for(std::shared_ptr 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); diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 8461eb2a4..65fab7b67 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -71,8 +71,9 @@ public: CHeroClass * heroClass; std::vector > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) - std::vector spec; - std::vector specialty; + std::vector specDeprecated; + std::vector specialtyDeprecated; + BonusList specialty; std::set 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> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid); +std::vector> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec); + class DLL_LINKAGE CHeroClass { public: @@ -289,8 +301,10 @@ public: std::vector 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(); diff --git a/lib/HeroBonus.cpp b/lib/HeroBonus.cpp index 70af0144f..eaaeb66d2 100644 --- a/lib/HeroBonus.cpp +++ b/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 bonusPropagatorMap = {"GLOBAL_EFFECT", std::make_shared(CBonusSystemNode::GLOBAL_EFFECTS)} }; //untested +const std::map bonusUpdaterMap = +{ + {"TIMES_HERO_LEVEL", std::make_shared()}, + {"TIMES_STACK_LEVEL", std::make_shared()} +}; + ///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 CBonusSystemNode::update(const std::shared_ptr 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& 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::addUpdater(TUpdaterPtr Updater) +{ + updater = Updater; + return this->shared_from_this(); +} + +IUpdater::~IUpdater() +{ +} + +const std::shared_ptr IUpdater::update(const std::shared_ptr 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 GrowsWithLevelUpdater::update(const std::shared_ptr b, const CBonusSystemNode & context) const +{ + if(context.getNodeType() == CBonusSystemNode::HERO) + { + int level = static_cast(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 newBonus = std::make_shared(*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 TimesHeroLevelUpdater::update(const std::shared_ptr b, const CBonusSystemNode & context) const +{ + if(context.getNodeType() == CBonusSystemNode::HERO) + { + int level = static_cast(context).level; + std::shared_ptr newBonus = std::make_shared(*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 TimesStackLevelUpdater::update(const std::shared_ptr b, const CBonusSystemNode & context) const +{ + if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE) + { + int level = static_cast(context).getLevel(); + std::shared_ptr newBonus = std::make_shared(*b); + newBonus->val *= level; + return newBonus; + } + else if(context.getNodeType() == CBonusSystemNode::STACK_BATTLE) + { + const CStack & stack = static_cast(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 newBonus = std::make_shared(*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"); +} diff --git a/lib/HeroBonus.h b/lib/HeroBonus.h index 9446f1e13..d71d96f77 100644 --- a/lib/HeroBonus.h +++ b/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 TBonusListPtr; typedef std::shared_ptr TLimiterPtr; typedef std::shared_ptr TPropagatorPtr; +typedef std::shared_ptr TUpdaterPtr; typedef std::set TNodes; typedef std::set TCNodes; typedef std::vector 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 TLimiterPtr limiter; TPropagatorPtr propagator; + TUpdaterPtr updater; std::string description; @@ -362,6 +366,10 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this h & effectRange; h & limiter; h & propagator; + if(version >= 781) + { + h & updater; + } } template @@ -419,9 +427,12 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this } 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 addLimiter(TLimiterPtr Limiter); //returns this for convenient chain-calls std::shared_ptr addPropagator(TPropagatorPtr Propagator); //returns this for convenient chain-calls + std::shared_ptr 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 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 update(const std::shared_ptr 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, 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 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 void serialize(Handler &h, const int version) { @@ -997,7 +1015,7 @@ extern DLL_LINKAGE const std::map bonusDurationMap; extern DLL_LINKAGE const std::map bonusLimitEffect; extern DLL_LINKAGE const std::map bonusLimiterMap; extern DLL_LINKAGE const std::map bonusPropagatorMap; - +extern DLL_LINKAGE const std::map bonusUpdaterMap; // BonusList template that requires full interface of CBonusSystemNode template @@ -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 update(const std::shared_ptr b, const CBonusSystemNode & context) const; + virtual std::string toString() const; + virtual JsonNode toJsonNode() const; + + template 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 void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & valPer20; + h & stepSize; + } + + const std::shared_ptr update(const std::shared_ptr 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 void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } + + const std::shared_ptr update(const std::shared_ptr 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 void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } + + const std::shared_ptr update(const std::shared_ptr b, const CBonusSystemNode & context) const override; + virtual std::string toString() const override; + virtual JsonNode toJsonNode() const override; +}; diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index c16276c76..f5a7503d1 100644 --- a/lib/JsonDetail.cpp +++ b/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) { } diff --git a/lib/JsonDetail.h b/lib/JsonDetail.h index 49fcb4e2b..5095eabd9 100644 --- a/lib/JsonDetail.h +++ b/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 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 diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 09e8d5bd6..c13270b41 100644 --- a/lib/JsonNode.cpp +++ b/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 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 & 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 updater = std::make_shared(); + 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 & 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 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; +} diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 58ba2f9d9..e53e78fdd 100644 --- a/lib/JsonNode.h +++ b/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 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 & 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:# /// 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 diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c4b25e44a..396e05c28 100644 --- a/lib/NetPacksLib.cpp +++ b/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 diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 8f9ccbb44..32c260659 100644 --- a/lib/battle/BattleInfo.cpp +++ b/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(); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index fd930a9d8..606b7f8ed 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/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->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->subtype = PrimarySkill::DEFENSE; - hs->addNewBonus(bonus); - //values will be calculated later - - bonus = std::make_shared(*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); - - 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); - - 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); - } - 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 b : type->specialty) + addNewBonus(b); + //dito for old-style bonuses -> compatibility for old savegames + for(SSpecialtyBonus & sb : type->specialtyDeprecated) + for(std::shared_ptr b : sb.bonuses) + addNewBonus(b); + for(SSpecialtyInfo & spec : type->specDeprecated) + for(std::shared_ptr 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(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 & 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 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 skills) } } - //specialty - Updatespecialty(); + //update specialty and other bonuses that scale with level + treeHasChanged(); } void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index ef59c01be..b7037c890 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/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 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 & specialtyDeprecated); public: std::string getHeroTypeName() const; @@ -297,7 +296,13 @@ public: h & visitedTown; h & boat; h & type; - h & specialty; + if(version < 781) + { + std::vector specialtyDeprecated; + h & specialtyDeprecated; + if(!h.saving) + recreateSpecialtyBonuses(specialtyDeprecated); + } h & commander; h & visitedObjects; BONUS_TREE_DESERIALIZATION_FIX diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 9c4960fc3..310d0c92b 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -135,6 +135,12 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGWitchHut); #undef REGISTER_GENERIC_HANDLER + + s.template registerType(); + s.template registerType(); + s.template registerType(); + //new types (other than netpacks) must register here + //order of type registration is critical for loading old savegames } template diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index f358b6e6c..a13d0bd63 100644 --- a/lib/serializer/CSerializer.h +++ b/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"; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index da640817a..eed18f9ed 100644 --- a/lib/spells/effects/Timed.cpp +++ b/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);