Răsfoiți Sursa

Merge pull request #5776 from IvanSavenko/modding_improvements

Modding improvements
Ivan Savenko 5 luni în urmă
părinte
comite
713dbfb1f7
100 a modificat fișierele cu 1203 adăugiri și 1156 ștergeri
  1. 2 8
      Mods/vcmi/Content/config/chinese.json
  2. 3 3
      Mods/vcmi/Content/config/czech.json
  3. 3 3
      Mods/vcmi/Content/config/english.json
  4. 3 3
      Mods/vcmi/Content/config/french.json
  5. 3 3
      Mods/vcmi/Content/config/german.json
  6. 3 3
      Mods/vcmi/Content/config/hungarian.json
  7. 3 3
      Mods/vcmi/Content/config/italian.json
  8. 3 3
      Mods/vcmi/Content/config/polish.json
  9. 2 3
      Mods/vcmi/Content/config/portuguese.json
  10. 4 3
      Mods/vcmi/Content/config/russian.json
  11. 3 3
      Mods/vcmi/Content/config/spanish.json
  12. 2 3
      Mods/vcmi/Content/config/swedish.json
  13. 3 3
      Mods/vcmi/Content/config/ukrainian.json
  14. 3 3
      Mods/vcmi/Content/config/vietnamese.json
  15. 1 1
      client/CPlayerInterface.cpp
  16. 2 2
      client/battle/BattleEffectsController.cpp
  17. 3 1
      client/battle/BattleStacksController.cpp
  18. 1 3
      client/widgets/MiscWidgets.cpp
  19. 18 17
      client/windows/CSpellWindow.cpp
  20. 42 12
      config/bonuses.json
  21. 10 0
      config/creatures/dungeon.json
  22. 10 0
      config/creatures/fortress.json
  23. 39 4
      config/creatures/neutral.json
  24. 10 0
      config/creatures/rampart.json
  25. 10 1
      config/factions/conflux.json
  26. 13 2
      config/factions/tower.json
  27. 17 8
      config/gameConfig.json
  28. 2 1
      config/heroes/castle.json
  29. 2 1
      config/heroes/dungeon.json
  30. 2 1
      config/heroes/fortress.json
  31. 2 1
      config/heroes/inferno.json
  32. 2 1
      config/heroes/stronghold.json
  33. 2 1
      config/heroes/tower.json
  34. 26 16
      config/schemas/bonus.json
  35. 74 48
      config/schemas/bonusInstance.json
  36. 6 2
      config/schemas/gameSettings.json
  37. 8 0
      config/schemas/hero.json
  38. 4 0
      config/schemas/mod.json
  39. 19 0
      config/schemas/spellSchool.json
  40. 7 5
      config/schemas/townBuilding.json
  41. 21 0
      config/spellSchools.json
  42. 67 27
      docs/modders/Bonus/Bonus_Types.md
  43. 37 15
      docs/modders/Bonus/Bonus_Updaters.md
  44. 53 0
      docs/modders/Entities_Format/Bonus_Types_Format.md
  45. 16 0
      docs/modders/Entities_Format/Spell_School_Format.md
  46. 5 10
      docs/modders/Entities_Format/Town_Building_Format.md
  47. 2 0
      include/vcmi/FactionMember.h
  48. 11 14
      lib/BasicTypes.cpp
  49. 42 22
      lib/CBonusTypeHandler.cpp
  50. 11 4
      lib/CBonusTypeHandler.h
  51. 14 20
      lib/CCreatureHandler.cpp
  52. 2 2
      lib/CMakeLists.txt
  53. 5 6
      lib/CSkillHandler.cpp
  54. 2 0
      lib/GameLibrary.cpp
  55. 2 0
      lib/GameLibrary.h
  56. 4 0
      lib/GameSettings.cpp
  57. 6 3
      lib/IGameSettings.h
  58. 10 0
      lib/battle/DamageCalculator.cpp
  59. 1 0
      lib/battle/DamageCalculator.h
  60. 3 4
      lib/bonuses/Bonus.cpp
  61. 1 2
      lib/bonuses/Bonus.h
  62. 6 9
      lib/bonuses/BonusCache.cpp
  63. 1 1
      lib/bonuses/BonusCache.h
  64. 0 6
      lib/bonuses/BonusEnum.cpp
  65. 9 6
      lib/bonuses/BonusEnum.h
  66. 12 4
      lib/bonuses/BonusList.cpp
  67. 0 374
      lib/bonuses/BonusParams.cpp
  68. 0 38
      lib/bonuses/BonusParams.h
  69. 8 5
      lib/bonuses/IBonusBearer.cpp
  70. 2 1
      lib/bonuses/IBonusBearer.h
  71. 3 2
      lib/bonuses/Limiters.cpp
  72. 38 2
      lib/bonuses/Updaters.cpp
  73. 37 12
      lib/bonuses/Updaters.h
  74. 1 1
      lib/callback/CGameInfoCallback.cpp
  75. 8 7
      lib/constants/EntityIdentifiers.cpp
  76. 0 3
      lib/constants/EntityIdentifiers.h
  77. 2 1
      lib/constants/NumericConstants.h
  78. 0 8
      lib/entities/building/CBuilding.cpp
  79. 2 10
      lib/entities/building/CBuilding.h
  80. 17 3
      lib/entities/faction/CTownHandler.cpp
  81. 38 22
      lib/entities/hero/CHeroHandler.cpp
  82. 2 2
      lib/entities/hero/CHeroHandler.h
  83. 0 6
      lib/gameState/CGameState.cpp
  84. 3 3
      lib/gameState/GameStatePackVisitor.cpp
  85. 56 96
      lib/json/JsonBonus.cpp
  86. 1 4
      lib/mapObjects/CArmedInstance.cpp
  87. 66 68
      lib/mapObjects/CGHeroInstance.cpp
  88. 14 11
      lib/mapObjects/CGTownInstance.cpp
  89. 5 2
      lib/mapping/CMap.h
  90. 2 0
      lib/modding/ContentTypeHandler.cpp
  91. 3 21
      lib/modding/IdentifierStorage.cpp
  92. 2 2
      lib/modding/IdentifierStorage.h
  93. 1 1
      lib/networkPacks/PacksForClientBattle.h
  94. 2 1
      lib/serializer/ESerializationVersion.h
  95. 26 83
      lib/spells/CSpellHandler.cpp
  96. 3 16
      lib/spells/CSpellHandler.h
  97. 60 0
      lib/spells/SpellSchoolHandler.cpp
  98. 72 0
      lib/spells/SpellSchoolHandler.h
  99. 5 16
      lib/spells/TargetCondition.cpp
  100. 9 5
      mapeditor/inspector/rewardswidget.cpp

+ 2 - 8
Mods/vcmi/Content/config/chinese.json

@@ -629,11 +629,8 @@
 
 	"mapObject.core.hillFort.object.description" : "升级生物,1-4级生物升级比城镇中更便宜。",
 
-	"artifact.core.orbOfVulnerability.bonus.noResistance" : "{Orb of Vulnerability}\nNegates natural magic resistance of all creatures on the battlefield",
-	"creatures.core.angel.bonus.raisesMorale" : "{Increases allies morale}\nAngels and Archangels increase allies Morale by one",
-	"creatures.core.devil.bonus.decreaseLuck" : "{Reduces enemy luck}\nDevils and Archdevils reduce enemy luck by one",
-	"creatures.core.boneDragon.bonus.decreaseMorale" : "{Reduces enemy morale}\nBone Dragons and Ghost Dragons lower the morale of enemy units by one",
-	"creatures.core.marksman.bonus.extraAttack" : "{Shoots twice}\nThis unit can shoot twice",
+	"creatures.core.azureDragon.bonus.fearful": "{恐惧}\n使得敌方一只部队恐惧",
+	"creatures.core.azureDragon.bonus.fearless": "{无惧}\n免疫恐惧特质",
 
 	"core.bonus.ADDITIONAL_ATTACK.description": "{双击}\n生物可以攻击两次",
 	"core.bonus.ADDITIONAL_RETALIATION.description": "{额外反击}\n每回合额外获得${val}次反击机会",
@@ -654,8 +651,6 @@
 	"core.bonus.ENCHANTER.description": "{施法者}\n每回合群体施放${subtype.spell}",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "{忽略攻击 (${val}%)}\n被攻击时,进攻方${val}%的攻击力将被无视。",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "{忽略防御 (${val}%)}\n发动攻击时,防御方${val}%的防御力将被无视。",
-	"core.bonus.FEAR.description": "{恐惧}\n使得敌方一只部队恐惧",
-	"core.bonus.FEARLESS.description": "{无惧}\n免疫恐惧特质",
 	"core.bonus.FEROCITY.description": "{凶猛追击}\n杀死任意生物后额外攻击${val}次",
 	"core.bonus.FIRE_SHIELD.description": "{烈火神盾 (${val}%)}\n反弹部分受到的近战伤害",
 	"core.bonus.FIRST_STRIKE.description.bonusSubtype.damageTypeMelee" : "{抢先反击}\n该生物的反击将会在被近战攻击前进行",
@@ -717,7 +712,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water": "{水系免疫}\n此单位免疫所有水系魔法",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "{魔法免疫}\n此单位免疫所有魔法",
 	"core.bonus.SUMMON_GUARDIANS.description": "{召唤守卫}\n战斗开始时召唤${subtype.creature}(${val}%)",
-	"core.bonus.SYNERGY_TARGET.description": "{协同攻击}\n生物受到协助攻击的影响",
 	"core.bonus.THREE_HEADED_ATTACK.description": "{半环攻击}\n攻击三格邻接单位",
 	"core.bonus.TRANSMUTATION.description": "{变形术}\n${val}%机会将被攻击单位变成其他生物",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "{吐息}\n吐息攻击(2格范围)",

+ 3 - 3
Mods/vcmi/Content/config/czech.json

@@ -634,6 +634,9 @@
 	"creatures.core.devil.bonus.decreaseLuck" : "{Snižuje štěstí nepřátel}\nĎáblové a arciďáblové snižují štěstí nepřátel o 1 bod",
 	"creatures.core.boneDragon.bonus.decreaseMorale" : "{Snižuje morálku nepřátel}\nKostění a přizrační draci snižují morálku nepřátel o 1 bod",
 	"creatures.core.marksman.bonus.extraAttack" : "{Střílí dvakrát}\nTato jednotka může vystřelit dvakrát za kolo",
+	"creatures.core.azureDragon.bonus.fearful" : "{Strach}\nVyvolává strach u nepřátelské jednotky",
+	"creatures.core.azureDragon.bonus.fearless" : "{Nebojácnost}\nImunní vůči schopnosti Strach",
+
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Dvojitý útok}\nÚtočí dvakrát", // TODO: alternative descriptions for melee/ranged effect range
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Další odvetné útoky}\nMůže odvetně zaútočit ${val} krát navíc",
@@ -654,8 +657,6 @@
 	"core.bonus.ENCHANTER.description" : "{Zaklínač}\nMůže každé kolo sesílat masové kouzlo ${subtype.spell}",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ignorování útoku (${val}%)}\nPři útoku je ignorováno ${val}% útočníkovy síly",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ignorování obrany (${val}%)}\nPří útoku nebude bráno v potaz ${val}% bodů obrany obránce",
-	"core.bonus.FEAR.description" : "{Strach}\nVyvolává strach u nepřátelské jednotky",
-	"core.bonus.FEARLESS.description" : "{Nebojácnost}\nImunní vůči schopnosti Strach",
 	"core.bonus.FEROCITY.description" : "{Zuřivost}\nÚtočí ${val} krát navíc, pokud někoho zabije",
 	"core.bonus.FIRE_SHIELD.description" : "{Ohnivý štít (${val}%)}\nOdrazí část zranění při útoku z blízka",
 	"core.bonus.FIRST_STRIKE.description.bonusSubtype.damageTypeMelee" : "{První úder}\nJednotka zasazuje protiúder dříve, než je zasažena v boji na blízko",
@@ -717,7 +718,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Imunita vůči kouzlům}\nJednotka je imunní vůči všem kouzlům.",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Vodní imunita}\nJednotka je imunní vůči všem kouzlům magie vody.",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Přivolání ochránců}\nNa začátku bitvy přivolá ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Synergizovatelný}\nTato jednotka je náchylná k synergickým efektům",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Tříhlavý útok}\nÚtočí na tři sousední jednotky",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutace}\n${val}% šance na přeměnu napadené jednotky na jiný typ",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Dech}\nÚtok dechem (dosah 2 polí)",

+ 3 - 3
Mods/vcmi/Content/config/english.json

@@ -634,6 +634,8 @@
 	"creatures.core.devil.bonus.decreaseLuck" : "{Reduces enemy luck}\nDevils and Archdevils reduce enemy luck by one",
 	"creatures.core.boneDragon.bonus.decreaseMorale" : "{Reduces enemy morale}\nBone Dragons and Ghost Dragons lower the morale of enemy units by one",
 	"creatures.core.marksman.bonus.extraAttack" : "{Shoots twice}\nThis unit can shoot twice",
+	"creatures.core.azureDragon.bonus.fearful" : "{Fear}\nEnemy units have a 10% chance of freezing in fear",
+	"creatures.core.azureDragon.bonus.fearless" : "{Fearless}\nImmune to Fear ability",
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Additional attacks}\nUnit can attack an additional {$val} times", // TODO: alternative descriptions for melee/ranged effect range
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Additional retaliations}\nUnit can retaliate ${val} extra times",
@@ -654,8 +656,6 @@
 	"core.bonus.ENCHANTER.description" : "{Enchanter}\nCan cast ${subtype.spell} every turn",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ignore Attack (${val}%) }\nWhen being attacked, ${val}% of the attacker's attack is ignored",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ignore Defense (${val}%) }\nWhen attacking, ${val}% of the defender's defense is ignored",
-	"core.bonus.FEAR.description" : "{Fear}\nEnemy units have a 10% chance of freezing in fear",
-	"core.bonus.FEARLESS.description" : "{Fearless}\nImmune to Fear ability",
 	"core.bonus.FEROCITY.description" : "{Ferocity}\nAttacks ${val} additional times if killed anybody",
 	"core.bonus.FIRE_SHIELD.description" : "{Fire Shield (${val}%) }\nThe unit reflects ${val} of the melee damage received",
 	"core.bonus.FIRST_STRIKE.description.bonusSubtype.damageTypeMelee" : "{First Strike}\nThe unit retaliates before being attacked in melee",
@@ -699,6 +699,7 @@
 	"core.bonus.REVENGE.description" : "{Revenge}\nDeals extra damage based on attacker's lost health in battle",
 	"core.bonus.SHOOTER.description" : "{Shoots}\nThis unit can use its ammo to perform ranged attacks",
 	"core.bonus.SHOOTS_ALL_ADJACENT.description" : "{Shoot all around}\nThis unit’s ranged attacks strike all targets in a small area",
+	"core.bonus.SKELETON_TRANSFORMER_TARGET.description" : "{Skeleton Transformation}\nThe Skeleton Transformer will turn this unit into a ${subtype.creature}",
 	"core.bonus.SOUL_STEAL.description" : "{Soul Steal}\nGains ${val} new creatures for each enemy killed",
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "{Cast After Attack}\nHas a ${val}% chance to cast ${subtype.spell} after it attacks",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "{Cast Before Attack}\nHas a ${val}% chance to cast ${subtype.spell} before it attacks",
@@ -717,7 +718,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.fire"  : "{Immune to Fire Magic}\nImmune to all spells from the school of Fire Magic",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Immune to Water Magic}\nImmune to all spells from the school of Water Magic",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Summon guardians}\nAt the start of battle summons ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Synergizable}\nThis creature is vulnerable to synergy effect",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Three-headed attack}\nAttacks three adjacent units",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutation}\n${val}% chance to transform attacked unit to a different type",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Breath Attack}\nAttacks by this unit will also hit any unit positioned immediately behind the target",

+ 3 - 3
Mods/vcmi/Content/config/french.json

@@ -180,6 +180,9 @@
 	"vcmi.stackExperience.rank.8" : "Élite",
 	"vcmi.stackExperience.rank.9" : "Maître",
 	"vcmi.stackExperience.rank.10" : "As",
+	
+	"creatures.core.azureDragon.bonus.fearful" : "{Peur}\nProvoque la peur sur une pile ennemie",
+	"creatures.core.azureDragon.bonus.fearless" : "{Intrépide}\nImmunité à la peur",
 
 	"core.bonus.ADDITIONAL_ATTACK.description": "{Double frappe}\nAttaque deux fois",
 	"core.bonus.ADDITIONAL_RETALIATION.description": "{Représailles supplémentaires}\nPeut riposter ${val} fois de plus",
@@ -198,8 +201,6 @@
 	"core.bonus.ENCHANTED.description": "{Enchanté}\nAffecté par ${subtype.spell} permanent",
 	"core.bonus.ENCHANTER.description": "{Enchanteur}\nPeut lancer en masse ${subtype.spell} à chaque tour",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "{Ignorer la défense (${val}%)}\nLors de l'attaque, ${val}% de la défense du défenseur est ignorée",
-	"core.bonus.FEAR.description": "{Peur}\nProvoque la peur sur une pile ennemie",
-	"core.bonus.FEARLESS.description": "{Intrépide}\nImmunité à la peur",
 	"core.bonus.FIRE_SHIELD.description": "{Bouclier de feu (${val}%)}\nReflète une partie des dégâts de mêlée",
 	"core.bonus.FIRST_STRIKE.description": "{Premier coup}\nCette créature riposte avant d'être attaquée",
 	"core.bonus.FLYING.description": "{Vol}\nVole en se déplaçant (ignore les obstacles)",
@@ -240,7 +241,6 @@
 	"core.bonus.SPELL_LIKE_ATTACK.description": "{Attaque semblable à un sort}\nAttaque avec ${subtype.spell}",
 	"core.bonus.SPELL_RESISTANCE_AURA.description": "{Aura de résistance}\nLes piles à proximité obtiennent ${val}% de résistance magique",
 	"core.bonus.SUMMON_GUARDIANS.description": "{Invoquer des gardiens}\nAu début de la bataille, invoque ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description": "{Synergique}\nCette créature est vulnérable à l'effet de synergie",
 	"core.bonus.THREE_HEADED_ATTACK.description": "{Attaque à trois têtes}\nAttaque trois unités adjacentes",
 	"core.bonus.TRANSMUTATION.description": "{Transmutation}\n${val}% de chances de transformer l'unité attaquée en un type différent",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "{Souffle}\nAttaque de souffle (portée de 2 hexagones)",

+ 3 - 3
Mods/vcmi/Content/config/german.json

@@ -619,6 +619,9 @@
 	
 	"mapObject.core.hillFort.object.description" : "Aufwertungen von Kreaturen. Die Stufen 1 - 4 sind billiger als in der zugehörigen Stadt.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Furcht}\nVerursacht Furcht bei einem gegnerischen Stapel",
+	"creatures.core.azureDragon.bonus.fearless" : "{Furchtlos}\nimmun gegen die Fähigkeit Furcht",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Doppelschlag}\nGreift zweimal an",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Zusätzliche Vergeltungsmaßnahmen}\nKann ${val} zusätzliche Male vergelten",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Rundum angreifen}\nGreift alle benachbarten Gegner an",
@@ -638,8 +641,6 @@
 	"core.bonus.ENCHANTER.description" : "{Verzauberer}\nKann jede Runde eine Masse von ${subtype.spell} zaubern",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Angriff ignorieren (${val}%)}\nBei Angriff, wird ${val}% des Angreifers ignoriert.",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ignoriere Verteidigung (${val}%)}\nIgnoriert einen Teil der Verteidigung für den Angriff",
-	"core.bonus.FEARLESS.description" : "{Furchtlos}\nimmun gegen die Fähigkeit Furcht",
-	"core.bonus.FEAR.description" : "{Furcht}\nVerursacht Furcht bei einem gegnerischen Stapel",
 	"core.bonus.FEROCITY.description" : "{Wildheit}\nGreift ${val} zusätzliche Male an, wenn jemand getötet wird",
 	"core.bonus.FIRE_SHIELD.description" : "{Feuerschild (${val}%)}\nReflektiert einen Teil des Nahkampfschadens",
 	"core.bonus.FIRST_STRIKE.description" : "{Erstschlag}\nDiese Kreatur greift zuerst an, anstatt zu vergelten",
@@ -694,7 +695,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Wasser-Immunität}\nImmunität gegen Zauber der Wasser-Schule",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Zauber-Immunität}\nImmunität gegen alle Zauber-Schulen",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Wächter beschwören}\nBeschwört bei Kampfbeginn ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Synergierbar}\nDiese Kreatur ist anfällig für Synergieeffekte",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Dreiköpfiger Angriff}\nGreift drei benachbarte Einheiten an",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutation}\n${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Atem}\nAtem-Angriff (2-Hex-Bereich)",

+ 3 - 3
Mods/vcmi/Content/config/hungarian.json

@@ -606,6 +606,9 @@
 	
 	"mapObject.core.hillFort.object.description" : "Lények fejlesztése. Az 1-4. szint olcsóbb, mint az adott városban.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Félelem}\nFélelmet kelt az ellenséges egységekben",
+	"creatures.core.azureDragon.bonus.fearless" : "{Félelem nélküli}\nImmunis a félelem képességre",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Dupla csapás}\nKétszer támad",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{További visszatámadások}\nMég ${val} alkalommal visszatámadhat",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Mindenirányú támadás}\nMinden szomszédos ellenséget támad",
@@ -625,8 +628,6 @@
 	"core.bonus.ENCHANTER.description" : "{Varázsló}\nTömeges ${subtype.spell} varázslatot használ minden körben",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Támadás figyelmen kívül hagyása (${val}%)}\nTámadáskor az ellenség támadásának ${val}%-át figyelmen kívül hagyja",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Védelem figyelmen kívül hagyása (${val}%)}\nTámadáskor az ellenség védekezésének ${val}%-át figyelmen kívül hagyja",
-	"core.bonus.FEARLESS.description" : "{Félelem nélküli}\nImmunis a félelem képességre",
-	"core.bonus.FEAR.description" : "{Félelem}\nFélelmet kelt az ellenséges egységekben",
 	"core.bonus.FEROCITY.description" : "{Vadság}\n${val} további alkalommal támad, ha bárkit megölt",
 	"core.bonus.FIRE_SHIELD.description" : "{Tűzpajzs (${val}%)}\nVisszaver egy részét a közelharci sebzésnek",
 	"core.bonus.FIRST_STRIKE.description" : "{Első csapás}\nEz a lény még a támadás előtt visszatámad",
@@ -681,7 +682,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Varázslatimmunitás}\nEz az egység immunis minden varázslattal szemben",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Víz immunitás}\nEz az egység immunis minden víz mágia iskolájához tartozó varázslattal szemben",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Őrök idézése}\nA csata kezdetén idézi a(z) ${subtype.creature} lényeket (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Szinkronizálható}\nEz a lény érzékeny a szinergiahatásokra",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Háromfejű támadás}\nHárom szomszédos egységet támad",
 	"core.bonus.TRANSMUTATION.description" : "{Átalakítás}\n${val}% eséllyel az ellenfelet más egységtípussá alakítja",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Lélegzet}\nLélegzési támadás (2 hatszögnyi távolság)",

+ 3 - 3
Mods/vcmi/Content/config/italian.json

@@ -607,6 +607,9 @@
 	
 	"mapObject.core.hillFort.object.description" : "Aggiorna le creature. I livelli 1 - 4 sono meno costosi rispetto alla città associata.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Paura}\nProvoca paura su una pila nemica",
+	"creatures.core.azureDragon.bonus.fearless" : "{Impavido}\nImmune all'abilità Paura",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Doppio colpo}\nAttacca due volte",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Ritorsioni aggiuntive}\nPuò contrattaccare ${val} volte in più",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Attacco a 360°}\nAttacca tutti i nemici adiacenti",
@@ -626,8 +629,6 @@
 	"core.bonus.ENCHANTER.description" : "{Incantatore}\nPuò lanciare l'incantesimo ${subtype.spell} ogni turno",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ignora attacco (${val}%)}\nQuando viene attaccata, ignora il ${val}% dell'attacco dell'avversario",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ignora difesa (${val}%)}\nQuando attacca, ignora il ${val}% della difesa dell'avversario",
-	"core.bonus.FEARLESS.description" : "{Impavido}\nImmune all'abilità Paura",
-	"core.bonus.FEAR.description" : "{Paura}\nProvoca paura su una pila nemica",
 	"core.bonus.FEROCITY.description" : "{Ferocia}\nAttacca ${val} volte aggiuntive se uccide qualcuno",
 	"core.bonus.FIRE_SHIELD.description" : "{Scudo di fuoco (${val}%)}\nRiflette una parte dei danni da mischia",
 	"core.bonus.FIRST_STRIKE.description" : "{Primo colpo}\nQuesta creatura contrattacca prima di essere attaccata",
@@ -682,7 +683,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Immunità agli incantesimi}\nQuesta unità è immune a tutti gli incantesimi",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Immunità all'acqua}\nQuesta unità è immune a tutti gli incantesimi della scuola dell'Acqua",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Evoca guardiani}\nAll'inizio della battaglia evoca ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Sinergizzabile}\nQuesta creatura è vulnerabile all'effetto sinergico",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Attacco a tre teste}\nAttacca tre unità adiacenti",
 	"core.bonus.TRANSMUTATION.description" : "{Trasmutazione}\n${val}% di possibilità di trasformare l'unità attaccata in un altro tipo",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Soffio}\nAttacco a soffio (raggio di 2 esagoni)",

+ 3 - 3
Mods/vcmi/Content/config/polish.json

@@ -629,6 +629,9 @@
 	
 	"mapObject.core.hillFort.object.description" : "Ulepsza jednostki. Koszt ulepszenia dla poziomów 1 - 4 jest bardziej korzystny niż w mieście.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Strach}\nWzbudza strach na wrogim stworzeniu",
+	"creatures.core.azureDragon.bonus.fearless" : "{Nieustraszony}\nOdporny na strach",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Podwójne Uderzenie}\nAtakuje dwa razy",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Dodatkowy odwet}\n${val} dodatkowy kontratak",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Obrotowy atak}\nAtakuje wszystkich sąsiadujących wrogów",
@@ -648,8 +651,6 @@
 	"core.bonus.ENCHANTER.description" : "{Czarodziej}\nRzuca czar ${subtype.spell}",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ignoruje Atak (${val}%)}\nPrzy zostaniu zaatakowanym ignoruje ${val}% ataku wroga",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Osłabienie Obrony (${val}%)}\nOsłabia obronę wroga podczas ataku",
-	"core.bonus.FEARLESS.description" : "{Nieustraszony}\nOdporny na strach",
-	"core.bonus.FEAR.description" : "{Strach}\nWzbudza strach na wrogim stworzeniu",
 	"core.bonus.FEROCITY.description" : "{Dzikość}\nDodatkowe ${val} ataków jeżeli zabito kogokolwiek",
 	"core.bonus.FIRE_SHIELD.description" : "{Ognista tarcza (${val}%)}\nOdbija część obrażeń z walki wręcz",
 	"core.bonus.FIRST_STRIKE.description" : "{Pierwsze Uderzenie}\nTo stworzenie atakuje pierwsze w ramach kontrataku",
@@ -695,7 +696,6 @@
 	"core.bonus.SPELL_LIKE_ATTACK.description" : "{Atak czaropodobny}\nAtakuje z użyciem ${subtype.spell}",
 	"core.bonus.SPELL_RESISTANCE_AURA.description" : "{O ${val}% słabszy}\nefekt czarów dla pobl. stwor.",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Wzywa na początku walki}\n${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Synergiczny}\nTo stworzenie jest podatne na efekt synergii",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Atak trzema głowami}\nAtakuje trzy sąsiadujące jednostki",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutacja}\n${val}% szans aby przetransformować atakowaną jednostkę na inny typ",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Zionięcie}\nAtak zionący (zasięg 2 heksów)",

+ 2 - 3
Mods/vcmi/Content/config/portuguese.json

@@ -632,6 +632,8 @@
 	"creatures.core.devil.bonus.decreaseLuck" : "{Reduz a sorte do inimigo}\nDiabos e Arquidiabos reduzem a sorte do inimigo em um",
 	"creatures.core.boneDragon.bonus.decreaseMorale" : "{Reduz a moral do inimigo}\nDragões Esqueletos e Dragões Fantasmas diminuem a moral das unidades inimigas em um",
 	"creatures.core.marksman.bonus.extraAttack" : "{Atira duas vezes}\nEsta unidade pode atirar duas vezes",
+	"creatures.core.azureDragon.bonus.fearful" : "{Medo}\nCausa Medo em uma pilha inimiga",
+	"creatures.core.azureDragon.bonus.fearless" : "{Destemido}\nImune à habilidade de Medo",
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Ataque Duplo}\nAtaca duas vezes",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Contra-ataques Adicionais}\nPode contra-atacar ${val} vezes extras",
@@ -652,8 +654,6 @@
 	"core.bonus.ENCHANTER.description" : "{Encantador}\nPode lançar ${subtype.spell} em massa a cada turno",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ignorar Ataque (${val}%)}\nAo ser atacado, ${val}% do ataque do agressor é ignorado",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ignorar Defesa (${val}%)}\nAo atacar, ${val}% da defesa do defensor é ignorada",
-	"core.bonus.FEAR.description" : "{Medo}\nCausa Medo em uma pilha inimiga",
-	"core.bonus.FEARLESS.description" : "{Destemido}\nImune à habilidade de Medo",
 	"core.bonus.FEROCITY.description" : "{Ferocidade}\nAtaca ${val} vezes adicionais se matar alguém",
 	"core.bonus.FIRE_SHIELD.description" : "{Escudo de Fogo (${val}%)}\nReflete parte do dano corpo a corpo",
 	"core.bonus.FIRST_STRIKE.description.bonusSubtype.damageTypeMelee" : "{Primeiro Ataque}\nA unidade contra-atacará antes de ser atacada em combate corpo a corpo",
@@ -715,7 +715,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Imunidade a Feitiços}\nEsta unidade é imune a todos os feitiços",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Imunidade a Água}\nEsta unidade é imune a todos os feitiços da escola de Água",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Invocar Guardas}\nNo início da batalha, invoca ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Alvo Sinergético}\nEsta criatura é vulnerável ao efeito de sinergia",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Ataque das Três Cabeças}\nAtaca três unidades adjacentes",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutação}\n${val}% de chance de transformar a unidade atacada em um tipo diferente",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Sopro}\nAtaque de Sopro (alcança 2 hexágonos)",

+ 4 - 3
Mods/vcmi/Content/config/russian.json

@@ -623,8 +623,12 @@
 	"core.seerhut.quest.reachDate.visit.3" : "Закрыто до %s.",
 	"core.seerhut.quest.reachDate.visit.4" : "Закрыто до %s.",
 	"core.seerhut.quest.reachDate.visit.5" : "Закрыто до %s.",
+
 	"mapObject.core.hillFort.object.description" : "Улучшить существ. Уровни 1-4 стоят дешевле, чем в соответствующем городе.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Страх}\nЗаставляет вражеских существ цепенеть от страха",
+	"creatures.core.azureDragon.bonus.fearless" : "{Бесстрашный}\nИммунитет к страху",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Двойной удар}\nБьет дважды",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Дополнительные ответные атаки}\nОтвечает на атаку дополнительно ${val} раз",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Атака вокруг}\nАтакует все окружающие отряды",
@@ -644,8 +648,6 @@
 	"core.bonus.ENCHANTER.description" : "{Заклинатель (массовое)}\nМожет применять массовое ${subtype.spell} каждый ход",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Игнорирует атаку (${val}%)}\nПри атаке ${val}%, атаки атакующего игнорируются",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Игнорирует броню (${val}%)}\nИгнорирует часть Защиты при атаке",
-	"core.bonus.FEARLESS.description" : "{Бесстрашный}\nИммунитет к страху",
-	"core.bonus.FEAR.description" : "{Страх}\nЗаставляет вражеских существ цепенеть от страха",
 	"core.bonus.FEROCITY.description" : "{Свирепость}\nАтакует дополнительно ${val} раз, если кого-то убивает",
 	"core.bonus.FIRE_SHIELD.description" : "{Огненный щит (${val}%)}\nНаносит огнем часть полученного урона",
 	"core.bonus.FIRST_STRIKE.description" : "{Первый удар}\nСущество бьет первым даже при ответной атаке",
@@ -700,7 +702,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.fire" : "{Иммунитет к Огню}\nОтряд невосприимчив ко всем заклинаниям школы Огня",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Иммунитет к Воде}\nОтряд невосприимчив ко всем заклинаниям школы Воды",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Призыв стражей}\nВ начале битвы призывает ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Синергия}\nСущество уязвимо к эффектам синергии",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Трехсторонняя атака}\nАтака трех отрядов с передней стороны",
 	"core.bonus.TRANSMUTATION.description" : "{Трансмутатор}\nШанс ${val}% превратить атакующий отряд в отряд другого типа",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Дыхание}\nАтака дыханием (радиус в 2 гекса)",

+ 3 - 3
Mods/vcmi/Content/config/spanish.json

@@ -337,6 +337,9 @@
 	"vcmi.stackExperience.rank.9" : "Maestro",
 	"vcmi.stackExperience.rank.10" : "As",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Miedo}\nCausa miedo a un grupo enemigo",
+	"creatures.core.azureDragon.bonus.fearless" : "{Inmune al miedo}\nInmune a la habilidad de miedo",
+
 	"core.bonus.ADDITIONAL_ATTACK.description": "{Doble Ataque}\nAtaca dos veces",
 	"core.bonus.ADDITIONAL_RETALIATION.description": "{Contrataques adicionales}\nPuede contratacar ${val} veces adicionales",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description": "{Ataque en todas las direcciones}\nAtaca a todos los enemigos adyacentes",
@@ -355,8 +358,6 @@
 	"core.bonus.ENCHANTER.description": "{Encantador}\nPuede lanzar ${subtype.spell} masivo cada turno",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "{Ignorar ataque (${val}%)}\nAl ser atacado, ${val}% del daño del atacante es ignorado",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "{Ignorar Defensa (${val}%)}\nIgnora una parte de la defensa al atacar",
-	"core.bonus.FEAR.description": "{Miedo}\nCausa miedo a un grupo enemigo",
-	"core.bonus.FEARLESS.description": "{Inmune al miedo}\nInmune a la habilidad de miedo",
 	"core.bonus.FEROCITY.description": "{Ferocidad}\nAtaca ${val} veces adicionales en caso de eliminar a alguien",
 	"core.bonus.FIRE_SHIELD.description": "{Escudo de Fuego (${val}%)}\nRefleja una parte del daño cuerpo a cuerpo",
 	"core.bonus.FIRST_STRIKE.description": "{Primer Ataque}\nEsta criatura ataca primero en lugar de contratacar",
@@ -399,7 +400,6 @@
 	"core.bonus.SPELL_LIKE_ATTACK.description": "{Ataque similar a hechizo}\nAtaca con ${subtype.spell}",
 	"core.bonus.SPELL_RESISTANCE_AURA.description": "{Aura de resistencia}\nLas unidades cercanas obtienen una resistencia mágica del ${val}%",
 	"core.bonus.SUMMON_GUARDIANS.description": "{Invocar guardianes}\nAl comienzo de la batalla invoca ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description": "{Sinergia}\nEsta criatura es vulnerable al efecto de sinergia",
 	"core.bonus.THREE_HEADED_ATTACK.description": "{Ataque de tres cabezas}\nAtaca a tres unidades adyacentes",
 	"core.bonus.TRANSMUTATION.description": "{Transmutación}\n${val}% de probabilidad de transformar la unidad atacada en otro tipo",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "{Aliento}\nAtaque de aliento (rango de 2 hexágonos)",

+ 2 - 3
Mods/vcmi/Content/config/swedish.json

@@ -632,6 +632,8 @@
 	"creatures.core.devil.bonus.decreaseLuck"            : "{Minskar fiendens lycka}\nDjävular och ärkedjävular minskar fiendens lycka med en poäng.",
 	"creatures.core.boneDragon.bonus.decreaseMorale"     : "{Minskar fiendens moral}\nBen- och spökdrakar minskar fiendens moral med en poäng.",
 	"creatures.core.marksman.bonus.extraAttack"          : "{Skjuter två gånger}\nDenna enhet kan skjuta två gånger per runda.",
+	"creatures.core.azureDragon.bonus.fearful" : "{Rädsla}\nOrsakar rädsla på ett fiendeförband",
+	"creatures.core.azureDragon.bonus.fearless" : "{Orädd}\nImmun mot rädsla.",
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Dubbelslag}\nAttackerar två gånger.", // Att göra: separata beskrivningar för närstrid/fjärrstrid
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Ytterligare motattacker}\nKan slå tillbaka ${val} extra gång(er).",
@@ -652,8 +654,6 @@
 	"core.bonus.ENCHANTER.description" : "{Förtrollare}\nKastar mass-${subtype.spell} varje turomgång.",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Avfärda attack (${val}%)}\nIgnorerar ${val}% av angriparens attack.",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Förbigå försvar (${val}%)}\nAttacker ignorerar ${val}% av fiendens försvar.",
-	"core.bonus.FEAR.description" : "{Rädsla}\nOrsakar rädsla på ett fiendeförband.",
-	"core.bonus.FEARLESS.description" : "{Orädd}\nImmun mot rädsla.",
 	"core.bonus.FEROCITY.description" : "{Vildsint}\n+${val} extra attack(er) om någon dödas.",
 	"core.bonus.FIRE_SHIELD.description" : "{Eldsköld (${val}%)}\nReflekterar en del av närstridsskadorna.",
 	"core.bonus.FIRST_STRIKE.description.bonusSubtype.damageTypeMelee" : "{Första slaget}\nRetalierar innan den träffas i närstrid.",
@@ -715,7 +715,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Immun mot all magi}\nEnheten är helt immun mot all magi.",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Immun mot vattenmagi}\nEnheten är helt immun mot all vattenmagi.",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Åkalla väktare}\nVid strid åkallas: ${subtype.creature} ${val}%.",
-	"core.bonus.SYNERGY_TARGET.description" : "{Synergibar}\nDenna varelse är sårbar för synergieffekt.",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Trehövdad attack}\nAttackerar upp till tre enheter framför sig.",
 	"core.bonus.TRANSMUTATION.description" : "{Transmutation}\n${val}% chans att förvandla angripen enhet.",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Dödlig andedräkt}\nAndningsattack (2 rutors räckvidd).",

+ 3 - 3
Mods/vcmi/Content/config/ukrainian.json

@@ -624,6 +624,8 @@
 	"creatures.core.devil.bonus.decreaseLuck" : "{Зменшує ворожу удачу}\nДияволи та Архідияволи зменшують удачу ворога на одиницю",
 	"creatures.core.boneDragon.bonus.decreaseMorale" : "{Знижує ворожий бойовий дух}\nКістяні і Примарні дракони знижують бойовий дух ворожих загонів на одиницю",
 	"creatures.core.marksman.bonus.extraAttack" : "{Стріляє двічі}\nЦей загін може стріляти двічі",
+	"creatures.core.azureDragon.bonus.fearful" : "{Страх}\nВорожі загони мають 10% шанс завмерти від страху",
+	"creatures.core.azureDragon.bonus.fearless" : "{Безстрашний}\nІмунітет до страху",
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Додаткові удари}\nАтакує двічі",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Додаткові відплати}\nЗагін може нанести ${val} додаткових ударів у відповідь",
@@ -644,8 +646,6 @@
 	"core.bonus.ENCHANTER.description" : "{Чарівник}\nМоже застосовувати закляття ${subtype.spell} кожного ходу",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Ігнорує ${val}% атаки}\nПри нападі ігнорується ${val}% атаки нападника",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Ігнорує ${val}% захисту}\nПри атаці ігнорується ${val}% захисту суперника",
-	"core.bonus.FEAR.description" : "{Страх}\nВорожі загони мають 10% шанс завмерти від страху",
-	"core.bonus.FEARLESS.description" : "{Безстрашний}\nІмунітет до страху",
 	"core.bonus.FEROCITY.description" : "{Лютість}\nЯкщо цей загін вбиває когось, то атакує ${val} разів додатково",
 	"core.bonus.FIRE_SHIELD.description" : "{Вогняний щит (${val}%) }\nЗагін повертає ${val} отриманої шкоди від ближнього бою назад атакуючому",
 	"core.bonus.FIRST_STRIKE.description" : "{Перший удар}\nЗагін завдає удару у відповідь до того, як його атакують",
@@ -689,6 +689,7 @@
 	"core.bonus.REVENGE.description" : "{Помста}\nЗавдає додаткової шкоди залежно від втраченого здоров'я в бою",
 	"core.bonus.SHOOTER.description" : "{Стрілок}\nЦей загін використовує боєприпаси для ведення дальніх атак",
 	"core.bonus.SHOOTS_ALL_ADJACENT.description" : "{Стріляйте по площі}\nДальні атаки цієї істоти вражають всі цілі на невеликій площі",
+	"core.bonus.SKELETON_TRANSFORMER_TARGET.description" : "{Перетворення у скелет}\nМашина скелетів перетворить цей загін на загін ${subtype.creature}",
 	"core.bonus.SOUL_STEAL.description" : "{Викрадення душ}\nОтримує ${val} нових істот за кожного вбитого ворога",
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "{Закляття після атаки}\nЗастосовує ${subtype.spell} з вірогідністю ${val}% після атаки",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "{Закляття перед атакою}\nЗастосовує ${subtype.spell} з вірогідністю ${val}% перед атакою",
@@ -707,7 +708,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.fire" : "{Імунітет до Вогню}\nНа цей загін не діють жодні закляття школи Вогню",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Імунітет до Води}\nНа цей загін не діють жодні закляття школи Води",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Закликає охоронців}\nНа початку бою викликає ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Синергізм}\nЦя істота вразлива до ефекту синергії",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Триголова атака}\nАтакує до трьох сусідніх загонів",
 	"core.bonus.TRANSMUTATION.description" : "{Трансмутація}\n${val}% шанс перетворити атакованого юніта в інший тип",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Атака подихом}\nАтаки цього загону також вражатимуть загін, розташований позаду цілі",

+ 3 - 3
Mods/vcmi/Content/config/vietnamese.json

@@ -605,6 +605,9 @@
 	
 	"mapObject.core.hillFort.object.description" : "Nâng cấp quân cấp 1 - 4 với chi phí thấp hơn ở trong thành.",
 
+	"creatures.core.azureDragon.bonus.fearful" : "{Sợ hãi}\nGây sợ hãi cho một đạo quân địch",
+	"creatures.core.azureDragon.bonus.fearless" : "{Không sợ}\nKháng lại kỹ năng gây sợ hãi",
+
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Đánh 2 lần}\nTấn công hai lần",
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Phản đòn thêm}\nCó thể phàn đòn thêm ${val} lần",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "{Đánh xung quanh}\nTấn công tất cả kẻ địch xung quanh",
@@ -624,8 +627,6 @@
 	"core.bonus.ENCHANTER.description" : "{Niệm phép}\nCó thể dùng phép mass ${subtype.spell} mỗi lượt",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "{Giảm tấn công (${val}%)}\nKhi bị tấn công, giảm ${val}% tấn công của kẻ địch",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "{Giảm phòng thủ (${val}%)}\nKhi tấn công, giảm ${val}% phòng thủ của kẻ địch",
-	"core.bonus.FEARLESS.description" : "{Không sợ}\nKháng lại kỹ năng gây sợ hãi",
-	"core.bonus.FEAR.description" : "{Sợ hãi}\nGây sợ hãi cho một đạo quân địch",
 	"core.bonus.FEROCITY.description" : "{Hung ác}\nTấn công thêm ${val} lần nữa nếu giết chết kẻ địch",
 	"core.bonus.FIRE_SHIELD.description" : "{Khiên lửa (${val}%)}\nPhản lại một phần sát thương khi cận chiến",
 	"core.bonus.FIRST_STRIKE.description" : "{Đòn đánh phủ đầu}\nĐạo quân này phản đòn trước khi bị tấn công",
@@ -680,7 +681,6 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "{Kháng phép}\nĐơn vị này kháng tất cả phép thuật",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.spellSchool.water" : "{Kháng phép nước}\nĐơn vị này kháng tất cả phép nước",
 	"core.bonus.SUMMON_GUARDIANS.description" : "{Gọi bảo vệ}\nKhi bắt đầu trận sẽ triệu hồi ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.description" : "{Hợp lực}\nQuân này dễ bị ảnh hưởng bởi nhiều hiệu ứng",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "{Ba đầu}\nTấn công cả quân liền kề mục tiêu",
 	"core.bonus.TRANSMUTATION.description" : "{Biến đổi}\nCó ${val}% cơ hội biến đổi quân mục tiêu thành dạng khác",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "{Đánh hai ô}\nTấn công bằng hơi thở (xuyên 2 ô)",

+ 1 - 1
client/CPlayerInterface.cpp

@@ -892,7 +892,7 @@ void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const Batt
 
 	battleInt->effectsController->battleTriggerEffect(bte);
 
-	if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN))
+	if(bte.effect == BonusType::MANA_DRAIN)
 	{
 		const CGHeroInstance * manaDrainedHero = GAME->interface()->cb->getHero(ObjectInstanceID(bte.additionalInfo));
 		battleInt->windowObject->heroManaPointsChanged(manaDrainedHero);

+ 2 - 2
client/battle/BattleEffectsController.cpp

@@ -66,7 +66,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 		return;
 	}
 	//don't show animation when no HP is regenerated
-	switch(static_cast<BonusType>(bte.effect))
+	switch(bte.effect)
 	{
 		case BonusType::HP_REGENERATION:
 			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition(), 0.5);
@@ -77,7 +77,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 		case BonusType::POISON:
 			displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition());
 			break;
-		case BonusType::FEAR:
+		case BonusType::FEARFUL:
 			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition(), 0.5);
 			break;
 		case BonusType::MORALE:

+ 3 - 1
client/battle/BattleStacksController.cpp

@@ -442,7 +442,9 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 
 		// FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applied - petrification is already removed
 		// if (needsReverse && !attackedInfo.defender->isFrozen())
-		if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN)
+		if (needsReverse &&
+		   stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN &&
+		   !attackedInfo.defender->hasBonusOfType(BonusType::VULNERABLE_FROM_BACK))
 		{
 			owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [this, attackedInfo]()
 			{

+ 1 - 3
client/widgets/MiscWidgets.cpp

@@ -585,9 +585,7 @@ void MoraleLuckBox::set(const AFactionMember * node)
 	text = LIBRARY->generaltexth->arraytxt[textId[morale]];
 	boost::algorithm::replace_first(text,"%s",LIBRARY->generaltexth->arraytxt[neutralDescr[morale]-mrlt]);
 
-	if (morale && node && (node->getBonusBearer()->hasBonusOfType(BonusType::UNDEAD)
-			|| node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING)
-			|| node->getBonusBearer()->hasBonusOfType(BonusType::MECHANICAL)))
+	if (morale && node && node->unaffectedByMorale())
 	{
 		text += LIBRARY->generaltexth->arraytxt[113]; //unaffected by morale
 		component.value = 0;

+ 18 - 17
client/windows/CSpellWindow.cpp

@@ -33,7 +33,6 @@
 #include "../widgets/VideoWidget.h"
 #include "../adventureMap/AdventureMapInterface.h"
 
-
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/GameConstants.h"
 #include "../../lib/GameLibrary.h"
@@ -42,11 +41,21 @@
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
+#include "../../lib/spells/SpellSchoolHandler.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/TextOperations.h"
-
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
+// Ordering of spell school tabs in SpelTab.def
+static const std::array schoolTabOrder =
+{
+	SpellSchool::AIR,
+	SpellSchool::FIRE,
+	SpellSchool::WATER,
+	SpellSchool::EARTH,
+	SpellSchool::ANY
+};
+
 CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner)
 {
 	addUsedEvents(LCLICK | SHOW_POPUP | HOVER);
@@ -85,12 +94,11 @@ public:
 		if(A->getLevel() > B->getLevel())
 			return false;
 
-
-		for(auto schoolId = 0; schoolId < GameConstants::DEFAULT_SCHOOLS; schoolId++)
+		for (const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects())
 		{
-			if(A->school.at(SpellSchool(schoolId)) && !B->school.at(SpellSchool(schoolId)))
+			if(A->schools.count(schoolId) && !B->schools.count(schoolId))
 				return true;
-			if(!A->school.at(SpellSchool(schoolId)) && B->school.at(SpellSchool(schoolId)))
+			if(!A->schools.count(schoolId) && B->schools.count(schoolId))
 				return false;
 		}
 
@@ -423,7 +431,7 @@ void CSpellWindow::computeSpellsPerArea()
 	for(const CSpell * spell : mySpells)
 	{
 		if(spell->isCombat() ^ !battleSpellsOnly
-			&& ((selectedTab == 4) || spell->school.at(SpellSchool(selectedTab)))
+		   && ((selectedTab == 4) || spell->schools.count(schoolTabOrder.at(selectedTab)))
 			)
 		{
 			spellsCurSite.push_back(spell);
@@ -726,7 +734,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell)
 	mySpell = spell;
 	if(mySpell)
 	{
-		SpellSchool whichSchool; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic,
+		SpellSchool whichSchool;
 		schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool);
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 
@@ -736,21 +744,14 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell)
 		{
 			OBJECT_CONSTRUCTION;
 
-			static const std::array schoolBorders = {
-				AnimationPath::builtin("SplevA.def"),
-				AnimationPath::builtin("SplevF.def"),
-				AnimationPath::builtin("SplevW.def"),
-				AnimationPath::builtin("SplevE.def")
-			};
-
 			schoolBorder.reset();
 			if (owner->selectedTab >= 4)
 			{
 				if (whichSchool.hasValue())
-					schoolBorder = std::make_shared<CAnimImage>(schoolBorders.at(whichSchool.getNum()), schoolLevel);
+					schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(whichSchool)->getSpellBordersPath(), schoolLevel);
 			}
 			else
-				schoolBorder = std::make_shared<CAnimImage>(schoolBorders.at(owner->selectedTab), schoolLevel);
+				schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(schoolTabOrder.at(owner->selectedTab))->getSpellBordersPath(), schoolLevel);
 		}
 
 		ColorRGBA firstLineColor, secondLineColor;

+ 42 - 12
config/bonuses.json

@@ -85,6 +85,10 @@
 	"ENEMY_DEFENCE_REDUCTION":
 	{
 	},
+	
+	"FEARFUL":
+	{
+	},
 
 	"FIRE_SHIELD":
 	{
@@ -98,14 +102,6 @@
 		}
 	},
 
-	"FEAR":
-	{
-	},
-
-	"FEARLESS":
-	{
-	},
-
 	"FEROCITY":
 	{
 	},
@@ -120,9 +116,18 @@
 	"FREE_SHOOTING":
 	{
 	},
+	
+	"FULL_MAP_DARKNESS":
+	{
+	},
+	
+	"FULL_MAP_SCOUTING":
+	{
+	},
 
 	"GARGOYLE":
 	{
+		"creatureNature" : true,
 	},
 
 	"GENERAL_DAMAGE_REDUCTION":
@@ -178,6 +183,11 @@
 	"LIMITED_SHOOTING_RANGE":
 	{
 	},
+	
+	"LIVING":
+	{
+		"creatureNature" : true
+	},
 
 	"MANA_CHANNELING":
 	{
@@ -194,6 +204,11 @@
 	"MAGIC_RESISTANCE":
 	{
 	},
+	
+	"MECHANICAL":
+	{
+		"creatureNature" : true
+	},
 
 	"MIND_IMMUNITY":
 	{
@@ -231,10 +246,7 @@
 
 	"NON_LIVING":
 	{
-	},
-
-	"MECHANICAL":
-	{
+		"creatureNature" : true
 	},
 
 	"OPENING_BATTLE_SPELL":
@@ -269,6 +281,15 @@
 	"REVENGE":
 	{
 	},
+	
+	"SIEGE_WEAPON":
+	{
+		"creatureNature" : true
+	},
+	
+	"SKELETON_TRANSFORMER_TARGET":
+	{
+	},
 
 	"SHOOTER":
 	{
@@ -346,8 +367,13 @@
 	{
 	},
 
+	"TRANSMUTATION_IMMUNITY":
+	{
+	},
+
 	"UNDEAD":
 	{
+		"creatureNature" : true,
 	},
 	
 	"UNLIMITED_RETALIATIONS":
@@ -359,6 +385,10 @@
 		"hidden": true
 	},
 	
+	"VULNERABLE_FROM_BACK":
+	{
+	},
+	
 	"WIDE_BREATH":
 	{
 	},

+ 10 - 0
config/creatures/dungeon.json

@@ -429,6 +429,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -474,6 +479,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"

+ 10 - 0
config/creatures/fortress.json

@@ -349,6 +349,11 @@
 			{
 				"type" : "ATTACKS_ALL_ADJACENT"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"noRetaliation" :
 			{
 				"type" : "BLOCKS_RETALIATION"
@@ -385,6 +390,11 @@
 			{
 				"type" : "ATTACKS_ALL_ADJACENT"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"noRetaliation" :
 			{
 				"type" : "BLOCKS_RETALIATION"

+ 39 - 4
config/creatures/neutral.json

@@ -75,6 +75,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -83,13 +88,28 @@
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
 			},
-			"fear" :
-			{
-				"type" : "FEAR"
+			"fearful" :
+			{
+				"type" : "FEARFUL",
+				"val" : 10,
+				"propagator": "BATTLE_WIDE",
+				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"description" : "PLACEHOLDER",
+				"limiters" : [ 
+					"OPPOSITE_SIDE", 
+					{
+						"type" : "HAS_ANOTHER_BONUS_LIMITER",
+						"parameters" : [ "LIVING" ]
+					}
+				]
 			},
 			"fearless" :
 			{
-				"type" : "FEARLESS"
+				"type" : "FEARFUL",
+				"valueType" : "INDEPENDENT_MAX",
+				"description" : "PLACEHOLDER",
+				"val" : 0
+				
 			},
 			"spellImmunity" :
 			{
@@ -128,6 +148,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"crystals" :
 			{
 				"type" : "SPECIAL_CRYSTAL_GENERATION"
@@ -169,6 +194,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -277,6 +307,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"

+ 10 - 0
config/creatures/rampart.json

@@ -366,6 +366,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -411,6 +416,11 @@
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"dragonSkeleton" : 
+			{
+				"type" : "SKELETON_TRANSFORMER_TARGET",
+				"subtype" : "boneDragon"
+			},
 			"canFly" :
 			{
 				"type" : "FLYING"

+ 10 - 1
config/factions/conflux.json

@@ -178,7 +178,16 @@
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
-				"special2":       { "requires" : [ "mageGuild1" ], "marketModes" : ["resource-skill"] },
+				"special2":       {
+					"requires" : [ "mageGuild1" ],
+					"marketModes" : ["resource-skill"],
+					"marketOffer" : [
+						"fireMagic",
+						"airMagic",
+						"waterMagic",
+						"earthMagic"
+					]
+				},
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }},
 				"extraTownHall":  { "id" : 27, "requires" : [ "townHall" ], "mode" : "auto" },
 				"extraCityHall":  { "id" : 28, "requires" : [ "cityHall" ], "mode" : "auto" },

+ 13 - 2
config/factions/tower.json

@@ -171,7 +171,12 @@
 				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl2" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" },
-				"special2":       { "height" : "high", "requires" : [ "fort" ] },
+				"special2":       { 
+					"requires" : [ "fort" ],
+					"bonuses": [
+						{ "type": "SIGHT_RADIUS", "val": 15 }, // 5 base + 15 bonus = 20 tiles range
+					]
+				},
 				"special3":       { "type" : "library", "requires" : [ "mageGuild1" ] },
 				"special4":       {
 					"requires" : [ "mageGuild1" ],
@@ -185,7 +190,13 @@
 						]
 					}
 				},
-				"grail":          { "height" : "skyship",  "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] },
+				"grail":          { 
+					"produce" : { "gold": 5000 },
+					"bonuses": [
+						{ "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 },
+						{ "type": "FULL_MAP_SCOUTING" }
+					]
+				},
 
 				"dwellingLvl1":   { "id" : 30, "requires" : [ "fort" ] },
 				"dwellingLvl2":   { "id" : 31, "requires" : [ "dwellingLvl1" ] },

+ 17 - 8
config/gameConfig.json

@@ -99,6 +99,10 @@
 		"config/spells/vcmiAbility.json",
 		"config/spells/moats.json"
 	],
+	"spellSchools" :
+	[
+		"config/spellSchools.json"
+	],
 	"skills" :
 	[
 		"config/skills.json"
@@ -313,7 +317,15 @@
 			/// movement points hero can get on start of the turn when on land, depending on speed of slowest creature (0-based list)
 			"movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ],
 			/// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list)
-			"movementPointsSea" : [ 1500 ]
+			"movementPointsSea" : [ 1500 ],
+			
+			/// Base scouting range for hero without any range modifiers
+			"baseScoutingRange" : 5,
+
+			/// Strength of generic secondary skill specialties ( "secondary" : "skillName" ) per level
+			"specialtySecondarySkillGrowth" : 5,
+			/// Strength of generic creature specialties ( "creature" : "creatureName" ) per level
+			"specialtyCreatureGrowth" : 5
 		},
 
 		"towns":
@@ -335,7 +347,10 @@
 			// How much researchs/skips per day are possible? (array index is spell tier)
 			"spellResearchPerDay": [ 2, 2, 2, 2, 1 ],
 			// Exponent for increasing cost for each research (factor 1 disables this; array index is spell tier)
-			"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ]
+			"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ],
+			
+			// Base scouting range for town without any range modifiers
+			"baseScoutingRange" : 5
 		},
 
 		"combat":
@@ -581,12 +596,6 @@
 					"val" : 1,
 					"valueType" : "BASE_NUMBER"
 				},
-				"sightRadius" :
-				{
-					"type" : "SIGHT_RADIUS", //default sight radius
-					"val" : 5,
-					"valueType" : "BASE_NUMBER"
-				},
 				"experienceGain" : 
 				{
 					"type" : "HERO_EXPERIENCE_GAIN_PERCENT", //default hero xp

+ 2 - 1
config/heroes/castle.json

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

+ 2 - 1
config/heroes/dungeon.json

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

+ 2 - 1
config/heroes/fortress.json

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

+ 2 - 1
config/heroes/inferno.json

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

+ 2 - 1
config/heroes/stronghold.json

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

+ 2 - 1
config/heroes/tower.json

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

+ 26 - 16
config/schemas/bonus.json

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

+ 74 - 48
config/schemas/bonusInstance.json

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

+ 6 - 2
config/schemas/gameSettings.json

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

+ 8 - 0
config/schemas/hero.json

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

+ 4 - 0
config/schemas/mod.json

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

+ 19 - 0
config/schemas/spellSchool.json

@@ -0,0 +1,19 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI spell school format",
+	"description" : "Format used to define new spell schools in VCMI",
+	"required" : [ "schoolBorders" ],
+	"additionalProperties" : false,
+	"properties" : {
+		"index" : {
+			"type" : "number",
+			"description" : "numeric id of h3 spell school, prohibited for new schools"
+		},
+		"schoolBorders" : {
+			"type" : "string",
+			"description" : "File with frame borders of mastery levels for spells of this spell school in spellbook",
+			"format" : "animationFile"
+		}
+	}
+}

+ 7 - 5
config/schemas/townBuilding.json

@@ -44,11 +44,6 @@
 			"enum" : [ "normal", "auto", "special", "grail" ],
 			"description" : "Mode in which this building will be built"
 		},
-		"height" : {
-			"type" : "string",
-			"enum" : [ "skyship", "high", "average", "low"],
-			"description" : "Height for lookout towers and some grails"
-		},
 		"requires" : {
 			"$ref" : "#/definitions/buildingRequirement",
 			"description" : "List of town buildings that must be built before this one"
@@ -133,6 +128,13 @@
 				"enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"],
 			},
 			"description" : "List of modes available in this market"
+		},
+		"marketOffer" : {
+			"type" : "array",
+			"items" : {
+				"type" : "string"
+			},
+			"description" : "List of predefined items available on market from this building"
 		}
 	}
 }

+ 21 - 0
config/spellSchools.json

@@ -0,0 +1,21 @@
+{
+	"air" : {
+		"index" : 0,
+		"schoolBorders" : "SplevA"
+	},
+	
+	"fire" : {
+		"index" : 1,
+		"schoolBorders" : "SplevF"
+	},
+	
+	"earth" : {
+		"index" : 2,
+		"schoolBorders" : "SplevE"
+	},
+	
+	"water" : {
+		"index" : 3,
+		"schoolBorders" : "SplevW"
+	}
+}

+ 67 - 27
docs/modders/Bonus/Bonus_Types.md

@@ -27,12 +27,6 @@ Changes mastery level of spells of affected heroes and units. Examples are magic
 - subtype: school of magic
 - val: level
 
-### DARKNESS
-
-On each turn, hides area in fog of war around affected town for all players other than town owner. Currently does not work for any entities other than towns.
-
-- val: radius in tiles
-
 ## Player bonuses
 
 Intended to be setup as global effect, AI cheat etc.
@@ -99,6 +93,27 @@ Reveal area of fog of war around affected heroes when hero is recruited or moves
 
 - val: radius in tiles
 
+### DARKNESS
+
+On each turn, hides area in fog of war around affected objects for all players other than town owner. Areas within scouting range of owned objects are not affected
+
+NOTE: when used by heroes, effect would still activate only on new turn, and not on every hero movement
+
+- val: radius in tiles
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
+### FULL_MAP_SCOUTING
+
+On each turn, reveals entire map for owner of the bonus
+
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
+### FULL_MAP_DARKNESS
+
+On each turn, hides entire map in fog of war for all players other than town owner. Areas within scouting range of owned objects are not affected
+
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
 ### MANA_REGENERATION
 
 Restores specific amount of mana points for affected heroes on new turn
@@ -111,6 +126,12 @@ Restores specific percentage of mana pool for affected heroes on new turn. If he
 
 - val: percentage of spell points to restore
 
+### SKELETON_TRANSFORMER_TARGET
+
+Unit affected by this bonus will be transformed into creature other than Skeleton when placed into Skeleton Transformer
+
+- subtype: type of creature to which this unit should be converted
+
 ### NONEVIL_ALIGNMENT_MIX
 
 Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale (Angelic Alliance effect)
@@ -123,18 +144,24 @@ Changes surrender cost for affected heroes
 
 ### IMPROVED_NECROMANCY
 
-Allows to raise different creatures than Skeletons after battle.
+Bonus allows the hero to raise specific creatures from corpses after battle.
+
+If the hero has multiple bonuses of the same type, the game will select the unit with the higher level. If the units have the same level, the game will select the unit with the higher market value (the total cost of the unit in gold, including converted resources).
+
+If the hero has no free space for the target creature but has space for its upgrade (including subsequent upgrades), the upgraded unit will be raised instead at a rate of two-thirds.
 
 - subtype: creature raised
 - val: Necromancer power
-- addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert)
-- Example (from Cloak Of The Undead King):
+- addInfo: Requried total level of Necromancer power for this bonus to be active (val of all bonuses of this type)
+
+Example (from Cloak Of The Undead King):
 
 ```json
 {
     "type" : "IMPROVED_NECROMANCY",
     "subtype" : "creature.walkingDead",
-    "addInfo" : 1
+    "addInfo" : 1, // requires 1 val of IMPROVED_NECROMANCY from other source, e.g. skill
+    "val" : 0 // does not provides levels of necromancer power on its own
 }
 ```
 
@@ -411,31 +438,39 @@ Increases starting amount of shots that unit has in battle
 
 ## Creature abilities
 
-## Static abilities and immunities
+## Creature Natures
+
+### LIVING
+
+Affected unit is considered to be alive. Automatically granted to any unit that does not have any other creature nature bonus
+
+Living units can be affected by TRANSMUTATION, LIFE_DRAIN, and SOUL_STEAL bonuses
 
 ### NON_LIVING
 
-Affected unit is considered to not be alive and not affected by morale and certain spells
+Creature nature bonus. Affected unit is considered to not be alive and not affected by morale and certain spells
 
 ### MECHANICAL
 
-Affected unit is considered to not be alive and not affected by morale and certain spells but should be repairable from engineers (factory).
+Creature nature bonus. Affected unit is considered to not be alive and not affected by morale and certain spells but should be repairable from engineers (HotA Factory).
 
 ### GARGOYLE
 
-Affected unit is considered to be a gargoyle and not affected by certain spells
+Creature nature bonus. Affected unit is considered to be a gargoyle and not affected by certain spells
 
 ### UNDEAD
 
-Affected unit is considered to be undead, which makes it immune to many effects, and also reduce morale of allied living units.
+Creature nature bonus. Affected unit is considered to be undead, which makes it immune to many effects, not affected by morale, and also reduce morale of allied living units.
 
 ### SIEGE_WEAPON
 
-Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. All War Machines should have this bonus.
+Creature nature bonus. Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. All War Machines should have this bonus.
+
+## Static abilities and immunities
 
 ### DRAGON_NATURE
 
-Affected unit is dragon. This bonus proved no effect, but is used as limiter several effects.
+Affected unit is dragon. This bonus provides no effects on its own, but is used as limiter for Mutare specialty
 
 ### KING
 
@@ -443,10 +478,6 @@ Affected unit will take more damage from units under Slayer spell effect
 
 - val: required skill mastery of Slayer spell to affect this unit
 
-### FEARLESS
-
-Affected unit is immune to Fear ability
-
 ### NO_LUCK
 
 Affected units can not receive good or bad luck
@@ -495,6 +526,12 @@ Affected unit will deal more damage based on movement distance (Champions)
 
 - val: additional damage per passed tile, percentage
 
+### VULNERABLE_FROM_BACK
+
+When affected unit is attacked from behind, it will receive more damage when attacked and will not turn around to face the attacker
+
+- val: additional damage for attacks from behind, percentage (0-100)
+
 ### HATE
 
 Affected unit will deal more damage when attacking specific creature
@@ -639,7 +676,7 @@ Affected unit will gain new creatures for each enemy killed by this unit
 
 ### TRANSMUTATION
 
-Affected units have chance to transform attacked unit to other creature type
+Affected units have chance to transform attacked, living unit to other creature type, unless attacked unit is under TRANSMUTATION_IMMUNITY bonus
 
 - val: chance for ability to trigger, percentage
 - subtype:
@@ -647,6 +684,10 @@ Affected units have chance to transform attacked unit to other creature type
   - transmutationPerUnit: transformed unit will have same number of units as original stack
 - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker
 
+### TRANSMUTATION_IMMUNITY
+
+Affected unit is immune to TRANSMUTATION bonus effects
+
 ### SUMMON_GUARDIANS
 
 When battle starts, affected units will be surrounded from all side with summoned units
@@ -783,7 +824,9 @@ Affected unit has chance to deal double damage on attack (Death Knight)
 
 ### FEAR
 
-If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance (Azure Dragon). Blocked by FEARLESS bonus.
+Units affected by this bonus have a chance to skip their turn and freeze in fear (Azure Dragon).
+
+- val: chance to trigger, percentage
 
 ### HEALER
 
@@ -921,6 +964,7 @@ Affected unit is permanently enchanted with a spell, that is cast again every tu
 Affected unit is immune to all spell with level below or equal to value of this bonus
 
 - val: level of spell up to which this unit is immune to
+- addInfo: if set to 1, this will be "absolute immunity" that can not be negated by Orb of Vulnerability
 
 ### MAGIC_RESISTANCE
 
@@ -1070,10 +1114,6 @@ Dummy bonus that acts as marker for Dendroid's Bind ability
 
 - addInfo: ID of stack that have bound the unit
 
-### SYNERGY_TARGET
-
-Dummy skill for alternative upgrades mod
-
 ### THIEVES_GUILD_ACCESS
 
 Increases amount of information available in affected thieves guild (in town or in adventure map tavern). Does not affects adventure map object "Den of Thieves". You may want to use PLAYER_PROPAGATOR with this bonus to make its effect player wide.

+ 37 - 15
docs/modders/Bonus/Bonus_Updaters.md

@@ -8,16 +8,15 @@ Check the files in `config/heroes/` for additional usage examples.
 
 ## GROWS_WITH_LEVEL
 
-- Type: Complex
-- Parameters: valPer20, stepSize=1
-- Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
+Effect: Updates val to `ceil(valPer20 * floor(heroLevel / stepSize) / 20)`
 
 Example: The following updater will cause a bonus to grow by 6 for every 40 levels. At first level, rounding will cause the bonus to be 0.
 
 ```json
 "updater" : {
-    "parameters" : [ 6, 2 ],
-    "type" : "GROWS_WITH_LEVEL"
+    "type" : "GROWS_WITH_LEVEL",
+    "valPer20" : 6,
+    "stepSize" : 2
 }
 ```
 
@@ -25,8 +24,9 @@ Example: The following updater will cause a bonus to grow by 3 for every 20 leve
 
 ```json
 "updater" : {
-    "parameters" : [ 3 ],
-    "type" : "GROWS_WITH_LEVEL"
+    "type" : "GROWS_WITH_LEVEL",
+    "valPer20" : 3,
+    "stepSize" : 1
 }
 ```
 
@@ -37,15 +37,22 @@ Remarks:
 
 ## TIMES_HERO_LEVEL
 
-- Type: Simple
-- Effect: Updates val to `val * heroLevel`
+Effect: Updates val to `val * heroLevel / stepSize`
 
 Usage: `"updater" : "TIMES_HERO_LEVEL"`
 
+Usage with stepSize greater than one:
+
+```json
+"updater" : {
+    "type" : "TIMES_HERO_LEVEL",
+    "stepSize" : 2
+}
+```
+
 ## TIMES_STACK_LEVEL
 
-- Type: Simple
-- Effect: Updates val to `val * stackLevel`, where `stackLevel` is level of stack (Pikeman is level 1, Angel is level 7)
+Updates val to `val * stackLevel`, where `stackLevel` is level of stack (Pikeman is level 1, Angel is level 7)
 
 Usage:
 
@@ -55,8 +62,7 @@ Remark: The stack level for war machines is 0.
 
 ## DIVIDE_STACK_LEVEL
 
-- Type: Simple
-- Effect: Updates val to `val / stackLevel`, where `stackLevel` is level of stack (Pikeman is level 1, Angel is level 7)
+Updates val to `val / stackLevel`, where `stackLevel` is level of stack (Pikeman is level 1, Angel is level 7)
 
 Usage:
 
@@ -66,8 +72,7 @@ Remark: The stack level for war machines is 0.
 
 ## TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL
 
-- Type: Simple
-- Effect: Same effect as `TIMES_HERO_LEVEL` combined with `DIVIDE_STACK_LEVEL`: `val * heroLevel / stackLevel`
+Effect: Same effect as `TIMES_HERO_LEVEL` combined with `DIVIDE_STACK_LEVEL`: `val * heroLevel / stackLevel`
 
 Intended to be used as hero bonus (such as specialty, skill, or artifact), for bonuses that affect units (Example: Adela Bless specialty)
 
@@ -75,6 +80,23 @@ Usage:
 
 `"updater" : "TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL"`
 
+## TIMES_STACK_SIZE
+
+Effect: Updates val to `val = clamp(val * floor(stackSize / stepSize), minimum, maximum)`, where stackSize is total number of creatures in current unit stack
+
+Parameters `minimum` and `maximum` are optional and can be dropped if not needed
+
+Example:
+
+```json
+"updater" : {
+    "type" : "TIMES_STACK_SIZE",
+    "minimum" : 0,
+    "maximum" : 100,
+    "stepSize" : 2
+}
+```
+
 ## BONUS_OWNER_UPDATER
 
 Helper updater for proper functionality of `OPPOSITE_SIDE` limiter

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

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

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

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

+ 5 - 10
docs/modders/Entities_Format/Town_Building_Format.md

@@ -150,16 +150,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	// Generally only needs to be specified for "special" buildings
 	// See 'List of unique town buildings' section below for detailed description of this field
 	"type" : "",
-	
-	// If set, building will have Lookout Tower logic - extend sight radius of a town.
-	// Possible values: 
-	// low - increases town sight radius by 5 tiles
-	// average - sight radius extended by 15 tiles
-	// high - sight radius extended by 20 tiles
-	// skyship - entire map will be revealed
-	// If not set, building will not affect sight radius of a town
-	"height" : "average"
-	
+
 	// Resources produced each day by this building
 	"produce" : { 
 		"sulfur" : 1,
@@ -213,6 +204,10 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	
 	// If the building is a market, it requires market mode.
 	"marketModes" : [ "resource-resource", "resource-player" ],
+	
+	// Required if building offers resource-skill trade.
+	// NOTE: multiple resource-skill buildings in the same town are not supported
+	"marketOffer" : [ "fireMagic", "airMagic", "waterMagic", "earthMagic" ],
 }
 ```
 

+ 2 - 0
include/vcmi/FactionMember.h

@@ -61,6 +61,8 @@ public:
 	*/
 	int moraleValAndBonusList(std::shared_ptr<const BonusList> & bonusList) const;
 	int luckValAndBonusList(std::shared_ptr<const BonusList> & bonusList) const;
+
+	bool unaffectedByMorale() const;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 11 - 14
lib/BasicTypes.cpp

@@ -69,6 +69,15 @@ int AFactionMember::getMaxDamage(bool ranged) const
 	return getBonusBearer()->valOfBonuses(selector, cachingStr);
 }
 
+bool AFactionMember::unaffectedByMorale() const
+{
+	static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::MECHANICAL)).Or(Selector::type()(BonusType::UNDEAD))
+													   .Or(Selector::type()(BonusType::SIEGE_WEAPON)).Or(Selector::type()(BonusType::NO_MORALE));
+
+	static const std::string cachingStrUn = "AFactionMember::unaffectedByMoraleSelector";
+	return getBonusBearer()->hasBonus(unaffectedByMoraleSelector, cachingStrUn);
+}
+
 int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 {
 	int32_t maxGoodMorale = LIBRARY->engineSettings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_CHANCE).size();
@@ -81,12 +90,7 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 		return maxGoodMorale;
 	}
 
-	static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::MECHANICAL)).Or(Selector::type()(BonusType::UNDEAD))
-													.Or(Selector::type()(BonusType::SIEGE_WEAPON)).Or(Selector::type()(BonusType::NO_MORALE));
-
-	static const std::string cachingStrUn = "AFactionMember::unaffectedByMoraleSelector";
-	auto unaffected = getBonusBearer()->hasBonus(unaffectedByMoraleSelector, cachingStrUn);
-	if(unaffected)
+	if(unaffectedByMorale())
 	{
 		if(bonusList && !bonusList->empty())
 			bonusList = std::make_shared<const BonusList>();
@@ -186,14 +190,7 @@ ui32 ACreature::getMovementRange(int turn) const
 
 bool ACreature::isLiving() const //TODO: theoreticaly there exists "LIVING" bonus in stack experience documentation
 {
-	static const std::string cachingStr = "ACreature::isLiving";
-	static const CSelector selector = Selector::type()(BonusType::UNDEAD)
-		.Or(Selector::type()(BonusType::NON_LIVING))
-		.Or(Selector::type()(BonusType::MECHANICAL))
-		.Or(Selector::type()(BonusType::GARGOYLE))
-		.Or(Selector::type()(BonusType::SIEGE_WEAPON));
-
-	return !getBonusBearer()->hasBonus(selector, cachingStr);
+	return getBonusBearer()->hasBonusOfType(BonusType::LIVING);
 }
 
 

+ 42 - 22
lib/CBonusTypeHandler.cpp

@@ -24,16 +24,10 @@
 #include "texts/CGeneralTextHandler.h"
 #include "json/JsonUtils.h"
 
-template class std::vector<VCMI_LIB_WRAP_NAMESPACE(CBonusType)>;
-
 VCMI_LIB_NAMESPACE_BEGIN
 
 ///CBonusType
 
-CBonusType::CBonusType():
-	hidden(true)
-{}
-
 std::string CBonusType::getDescriptionTextID() const
 {
 	return TextIdentifier( "core", "bonus", identifier, "description").get();
@@ -45,21 +39,24 @@ CBonusTypeHandler::CBonusTypeHandler()
 {
 	//register predefined bonus types
 
-	#define BONUS_NAME(x) \
-	do { \
-		bonusTypes.push_back(CBonusType()); \
-	} while(0);
+#define BONUS_NAME(x) { #x },
+	builtinBonusNames = {
+		BONUS_LIST
+	};
+#undef BONUS_NAME
 
+	for (int i = 0; i < builtinBonusNames.size(); ++i)
+		bonusTypes.push_back(std::make_shared<CBonusType>());
 
-	BONUS_LIST;
-	#undef BONUS_NAME
+	for (int i = 0; i < builtinBonusNames.size(); ++i)
+		registerObject(ModScope::scopeBuiltin(), "bonus", builtinBonusNames[i], i);
 }
 
 CBonusTypeHandler::~CBonusTypeHandler() = default;
 
 std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonus, const IBonusBearer * bearer) const
 {
-	const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)];
+	const CBonusType & bt = *bonusTypes.at(vstd::to_underlying(bonus->type));
 	int bonusValue = bearer->valOfBonuses(bonus->type, bonus->subtype);
 	if(bt.hidden)
 		return "";
@@ -93,7 +90,7 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonu
 
 ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const
 {
-	const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)];
+	const CBonusType & bt = *bonusTypes.at(vstd::to_underlying(bonus->type));
 
 	if (bonus->type == BonusType::SPELL_IMMUNITY && bonus->subtype.as<SpellID>().hasValue())
 	{
@@ -117,18 +114,21 @@ std::vector<JsonNode> CBonusTypeHandler::loadLegacyData()
 
 void CBonusTypeHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
 {
-	auto it = bonusNameMap.find(name);
-
-	if(it == bonusNameMap.end())
+	if (vstd::contains(builtinBonusNames, name))
 	{
-		logBonus->warn("Unrecognized bonus name! (%s)", name);
+		//h3 bonus
+		BonusType bonus = static_cast<BonusType>(vstd::find_pos(builtinBonusNames, name));
+		CBonusType & bt =*bonusTypes.at(vstd::to_underlying(bonus));
+		loadItem(data, bt, name);
+		logBonus->trace("Loaded bonus type %s", name);
 	}
 	else
 	{
-		CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)];
-
-		loadItem(data, bt, name);
-		logBonus->trace("Loaded bonus type %s", name);
+		// new bonus
+		registerObject(scope, "bonus", name, bonusTypes.size());
+		bonusTypes.push_back(std::make_shared<CBonusType>());
+		loadItem(data, *bonusTypes.back(), name);
+		logBonus->trace("New bonus type %s", name);
 	}
 }
 
@@ -141,6 +141,7 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
 {
 	dest.identifier = name;
 	dest.hidden = source["hidden"].Bool(); //Null -> false
+	dest.creatureNature = source["creatureNature"].Bool(); //Null -> false
 
 	if (!dest.hidden)
 		LIBRARY->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"]);
@@ -185,4 +186,23 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
 	}
 }
 
+const std::string & CBonusTypeHandler::bonusToString(BonusType bonus) const
+{
+	return bonusTypes.at(static_cast<int>(bonus))->identifier;
+}
+
+bool CBonusTypeHandler::isCreatureNatureBonus(BonusType bonus) const
+{
+	return bonusTypes.at(static_cast<int>(bonus))->creatureNature;
+}
+
+std::vector<BonusType> CBonusTypeHandler::getAllObjets() const
+{
+	std::vector<BonusType> ret;
+	for (int i = 0; i < bonusTypes.size(); ++i)
+		ret.push_back(static_cast<BonusType>(i));
+
+	return ret;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 11 - 4
lib/CBonusTypeHandler.h

@@ -19,10 +19,10 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class JsonNode;
 
-class DLL_LINKAGE CBonusType
+class DLL_LINKAGE CBonusType : boost::noncopyable
 {
 public:
-	CBonusType();
+	CBonusType() = default;
 
 	std::string getDescriptionTextID() const;
 
@@ -36,11 +36,13 @@ private:
 	std::map<int, std::string> valueDescriptions;
 	std::string identifier;
 
-	bool hidden;
+	bool creatureNature = false;
+	bool hidden = true;
 };
 
 class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler
 {
+	std::vector<std::string> builtinBonusNames;
 public:
 	CBonusTypeHandler();
 	virtual ~CBonusTypeHandler();
@@ -52,10 +54,15 @@ public:
 	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;
 
+	const std::string & bonusToString(BonusType bonus) const;
+
+	bool isCreatureNatureBonus(BonusType bonus) const;
+
+	std::vector<BonusType> getAllObjets() const;
 private:
 	void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const;
 
-	std::vector<CBonusType> bonusTypes; //index = BonusType
+	std::vector<std::shared_ptr<CBonusType> > bonusTypes; //index = BonusType
 };
 
 VCMI_LIB_NAMESPACE_END

+ 14 - 20
lib/CCreatureHandler.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "CCreatureHandler.h"
 
+#include "CBonusTypeHandler.h"
 #include "ResourceSet.h"
 #include "entities/faction/CFaction.h"
 #include "entities/faction/CTownHandler.h"
@@ -917,24 +918,6 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c
 			}
 		}
 	}
-	else
-	{
-		for(const JsonNode &ability : config["abilities"].Vector())
-		{
-			if(ability.getType() == JsonNode::JsonType::DATA_VECTOR)
-			{
-				logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey());
-			}
-			else
-			{
-				auto b = JsonUtils::parseBonus(ability);
-				b->source = BonusSource::CREATURE_ABILITY;
-				b->sid = BonusSourceID(creature->getId());
-				b->duration = BonusDuration::PERMANENT;
-				creature->addNewBonus(b);
-			}
-		}
-	}
 
 	LIBRARY->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction)
 	{
@@ -1076,7 +1059,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars
 		b.subtype = BonusCustomSubtype::deathStareGorgon;
 		break;
 	case 'F':
-		b.type = BonusType::FEAR; break;
+		b.type = BonusType::FEARFUL; break;
 	case 'g':
 		b.type = BonusType::SPELL_DAMAGE_REDUCTION;
 		b.subtype = BonusSubtypeID(SpellSchool::ANY);
@@ -1105,7 +1088,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars
 			case 'D':
 				b.type = BonusType::ADDITIONAL_ATTACK; break;
 			case 'f':
-				b.type = BonusType::FEARLESS; break;
+				b.type = BonusType::FEARFUL; break;
 			case 'F':
 				b.type = BonusType::FLYING; break;
 			case 'm':
@@ -1364,7 +1347,18 @@ CCreatureHandler::~CCreatureHandler()
 
 void CCreatureHandler::afterLoadFinalization()
 {
+	for(auto & creature : objects)
+	{
+		if (!creature)
+			continue;
+
+		auto natureBonuses = creature->getBonuses([](const Bonus * b){
+				return LIBRARY->bth->isCreatureNatureBonus(b->type);
+		});
 
+		if (natureBonuses->empty())
+			creature->addNewBonus(std::make_shared<Bonus>(BonusDuration::PERMANENT, BonusType::LIVING, BonusSource::CREATURE_ABILITY, 0, BonusSourceID(creature->getId())));
+	}
 }
 
 std::set<CreatureID> CCreatureHandler::getDefaultAllowed() const

+ 2 - 2
lib/CMakeLists.txt

@@ -69,7 +69,6 @@ set(lib_MAIN_SRCS
 	bonuses/BonusCache.cpp
 	bonuses/BonusEnum.cpp
 	bonuses/BonusList.cpp
-	bonuses/BonusParams.cpp
 	bonuses/BonusSelector.cpp
 	bonuses/BonusCustomTypes.cpp
 	bonuses/CBonusSystemNode.cpp
@@ -256,6 +255,7 @@ set(lib_MAIN_SRCS
 	spells/ObstacleCasterProxy.cpp
 	spells/Problem.cpp
 	spells/ProxyCaster.cpp
+	spells/SpellSchoolHandler.cpp
 	spells/TargetCondition.cpp
 	spells/ViewSpellInt.cpp
 
@@ -450,7 +450,6 @@ set(lib_MAIN_HEADERS
 	bonuses/BonusCache.h
 	bonuses/BonusEnum.h
 	bonuses/BonusList.h
-	bonuses/BonusParams.h
 	bonuses/BonusSelector.h
 	bonuses/BonusCustomTypes.h
 	bonuses/CBonusSystemNode.h
@@ -701,6 +700,7 @@ set(lib_MAIN_HEADERS
 	spells/ObstacleCasterProxy.h
 	spells/Problem.h
 	spells/ProxyCaster.h
+	spells/SpellSchoolHandler.h
 	spells/TargetCondition.h
 	spells/ViewSpellInt.h
 

+ 5 - 6
lib/CSkillHandler.cpp

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

+ 2 - 0
lib/GameLibrary.cpp

@@ -18,6 +18,7 @@
 #include "RiverHandler.h"
 #include "TerrainHandler.h"
 #include "spells/CSpellHandler.h"
+#include "spells/SpellSchoolHandler.h"
 #include "spells/effects/Registry.h"
 #include "CSkillHandler.h"
 #include "entities/artifact/CArtHandler.h"
@@ -178,6 +179,7 @@ void GameLibrary::initializeLibrary()
 	createHandler(biomeHandler);
 	createHandler(objh);
 	createHandler(objtypeh);
+	createHandler(spellSchoolHandler);
 	createHandler(spellh);
 	createHandler(skillh);
 	createHandler(terviewh);

+ 2 - 0
lib/GameLibrary.h

@@ -40,6 +40,7 @@ class IHandlerBase;
 class IGameSettings;
 class GameSettings;
 class CIdentifierStorage;
+class SpellSchoolHandler;
 
 #if SCRIPTING_ENABLED
 namespace scripting
@@ -78,6 +79,7 @@ public:
 	std::unique_ptr<CHeroClassHandler> heroclassesh;
 	std::unique_ptr<CCreatureHandler> creh;
 	std::unique_ptr<CSpellHandler> spellh;
+	std::unique_ptr<SpellSchoolHandler> spellSchoolHandler;
 	std::unique_ptr<CSkillHandler> skillh;
 	// TODO: Remove ObjectHandler altogether?
 	std::unique_ptr<CObjectHandler> objh;

+ 4 - 0
lib/GameSettings.cpp

@@ -77,6 +77,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,                  "dwellings", "accumulateWhenOwned"                  },
 		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,                       "dwellings", "mergeOnRecruit"                       },
 		{EGameSettings::HEROES_BACKPACK_CAP,                              "heroes",    "backpackSize"                         },
+		{EGameSettings::HEROES_BASE_SCOUNTING_RANGE,                      "heroes",    "baseScoutingRange"                    },
 		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,                    "heroes",    "minimalPrimarySkills"                 },
 		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,                     "heroes",    "perPlayerOnMapCap"                    },
 		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,                      "heroes",    "perPlayerTotalCap"                    },
@@ -86,6 +87,8 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::HEROES_MOVEMENT_COST_BASE,                        "heroes",    "movementCostBase"                     },
 		{EGameSettings::HEROES_MOVEMENT_POINTS_LAND,                      "heroes",    "movementPointsLand"                   },
 		{EGameSettings::HEROES_MOVEMENT_POINTS_SEA,                       "heroes",    "movementPointsSea"                    },
+		{EGameSettings::HEROES_SPECIALTY_CREATURE_GROWTH,                 "heroes",    "specialtyCreatureGrowth"              },
+		{EGameSettings::HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH,          "heroes",    "specialtySecondarySkillGrowth"        },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,                     "mapFormat", "armageddonsBlade"                     },
 		{EGameSettings::MAP_FORMAT_CHRONICLES,                            "mapFormat", "chronicles"                           },
 		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,                     "mapFormat", "hornOfTheAbyss"                       },
@@ -116,6 +119,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::TEXTS_ROAD,                                       "textData",  "road"                                 },
 		{EGameSettings::TEXTS_SPELL,                                      "textData",  "spell"                                },
 		{EGameSettings::TEXTS_TERRAIN,                                    "textData",  "terrain"                              },
+		{EGameSettings::TOWNS_BASE_SCOUNTING_RANGE,                       "towns",     "baseScoutingRange"                    },
 		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,                     "towns",     "buildingsPerTurnCap"                  },
 		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,                  "towns",     "startingDwellingChances"              },
 		{EGameSettings::TOWNS_SPELL_RESEARCH,                             "towns",     "spellResearch"                        },

+ 6 - 3
lib/IGameSettings.h

@@ -50,6 +50,7 @@ enum class EGameSettings
 	DWELLINGS_ACCUMULATE_WHEN_OWNED,
 	DWELLINGS_MERGE_ON_RECRUIT,
 	HEROES_BACKPACK_CAP,
+	HEROES_BASE_SCOUNTING_RANGE,
 	HEROES_MINIMAL_PRIMARY_SKILLS,
 	HEROES_PER_PLAYER_ON_MAP_CAP,
 	HEROES_PER_PLAYER_TOTAL_CAP,
@@ -59,6 +60,9 @@ enum class EGameSettings
 	HEROES_MOVEMENT_COST_BASE,
 	HEROES_MOVEMENT_POINTS_LAND,
 	HEROES_MOVEMENT_POINTS_SEA,
+	HEROES_SPECIALTY_CREATURE_GROWTH,
+	HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH,
+	INTERFACE_PLAYER_COLORED_BACKGROUND,
 	MAP_FORMAT_ARMAGEDDONS_BLADE,
 	MAP_FORMAT_CHRONICLES,
 	MAP_FORMAT_HORN_OF_THE_ABYSS,
@@ -91,14 +95,13 @@ enum class EGameSettings
 	TEXTS_TERRAIN,
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_STARTING_DWELLING_CHANCES,
-	INTERFACE_PLAYER_COLORED_BACKGROUND,
+	TOWNS_BASE_SCOUNTING_RANGE,
 	TOWNS_SPELL_RESEARCH,
 	TOWNS_SPELL_RESEARCH_COST,
 	TOWNS_SPELL_RESEARCH_PER_DAY,
 	TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH,
 
-	OPTIONS_COUNT,
-	OPTIONS_BEGIN = BONUSES_GLOBAL
+	OPTIONS_COUNT
 };
 
 class DLL_LINKAGE IGameSettings

+ 10 - 0
lib/battle/DamageCalculator.cpp

@@ -275,6 +275,15 @@ double DamageCalculator::getAttackJoustingFactor() const
 	return 0.0;
 }
 
+double DamageCalculator::getAttackFromBackFactor() const
+{
+	int value = info.defender->valOfBonuses(BonusType::VULNERABLE_FROM_BACK);
+
+	if (value != 0 && callback.isToReverse(info.attacker, info.defender, info.attackerPos, info.defenderPos))
+		return value / 100.0;
+	return 0;
+}
+
 double DamageCalculator::getAttackHateFactor() const
 {
 	//assume that unit have only few HATE features and cache them all
@@ -462,6 +471,7 @@ std::vector<double> DamageCalculator::getAttackFactors() const
 		getAttackBlessFactor(),
 		getAttackLuckFactor(),
 		getAttackJoustingFactor(),
+		getAttackFromBackFactor(),
 		getAttackDeathBlowFactor(),
 		getAttackDoubleDamageFactor(),
 		getAttackHateFactor(),

+ 1 - 0
lib/battle/DamageCalculator.h

@@ -52,6 +52,7 @@ class DLL_LINKAGE DamageCalculator
 	double getAttackDoubleDamageFactor() const;
 	double getAttackHateFactor() const;
 	double getAttackRevengeFactor() const;
+	double getAttackFromBackFactor() const;
 
 	double getDefenseSkillFactor() const;
 	double getDefenseArmorerFactor() const;

+ 3 - 4
lib/bonuses/Bonus.cpp

@@ -14,6 +14,7 @@
 #include "Updaters.h"
 #include "Propagators.h"
 
+#include "../CBonusTypeHandler.h"
 #include "../CCreatureHandler.h"
 #include "../CCreatureSet.h"
 #include "../CSkillHandler.h"
@@ -172,7 +173,7 @@ JsonNode Bonus::toJsonNode() const
 {
 	JsonNode root;
 	// only add values that might reasonably be found in config files
-	root["type"].String() = vstd::findKey(bonusNameMap, type);
+	root["type"].String() = LIBRARY->bth->bonusToString(type);
 	if(subtype != BonusSubtypeID())
 		root["subtype"].String() = subtype.toString();
 	if(additionalInfo != CAddInfo::NONE)
@@ -243,9 +244,7 @@ std::shared_ptr<Bonus> Bonus::addPropagator(const TPropagatorPtr & Propagator)
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus)
 {
-	for(const auto & i : bonusNameMap)
-	if(i.second == bonus.type)
-		out << "\tType: " << i.first << " \t";
+	out << "\tType: " << LIBRARY->bth->bonusToString(bonus.type) << " \t";
 
 #define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n"
 	printField(val);

+ 1 - 2
lib/bonuses/Bonus.h

@@ -65,9 +65,8 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 	BonusValueType valType = BonusValueType::ADDITIVE_VALUE; // 1 byte
 	BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus - 1 byte
 	BonusSource targetSourceType = BonusSource::OTHER;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. - 1 byte
-	BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte
 	BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; // 1 byte
-	// 1 bytes padding
+	BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 2 bytes
 
 	BonusSubtypeID subtype;
 	BonusSourceID sid; //source id: id of object/artifact/spell

+ 6 - 9
lib/bonuses/BonusCache.cpp

@@ -17,6 +17,7 @@
 
 #include "../GameLibrary.h"
 #include "../IGameSettings.h"
+#include "../spells/SpellSchoolHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -59,22 +60,18 @@ bool BonusValueCache::hasBonus() const
 
 MagicSchoolMasteryCache::MagicSchoolMasteryCache(const IBonusBearer * target)
 	:target(target)
+	,schools(LIBRARY->spellSchoolHandler->getAllObjects().size() + 1)
 {}
 
 void MagicSchoolMasteryCache::update() const
 {
 	static const CSelector allBonusesSelector = Selector::type()(BonusType::MAGIC_SCHOOL_SKILL);
-	static const std::array schoolsSelector = {
-		Selector::subtype()(SpellSchool::ANY),
-		Selector::subtype()(SpellSchool::AIR),
-		Selector::subtype()(SpellSchool::FIRE),
-		Selector::subtype()(SpellSchool::WATER),
-		Selector::subtype()(SpellSchool::EARTH),
-	};
 
 	auto list = target->getBonuses(allBonusesSelector);
-	for (int i = 0; i < schoolsSelector.size(); ++i)
-		schools[i] = list->valOfBonuses(schoolsSelector[i]);
+	schools[0] = list->valOfBonuses(Selector::subtype()(SpellSchool::ANY));
+
+	for (int i = 1; i < schools.size(); ++i)
+		schools[i] = list->valOfBonuses(Selector::subtype()(SpellSchool(i-1)));
 
 	version = target->getTreeVersion();
 }

+ 1 - 1
lib/bonuses/BonusCache.h

@@ -171,7 +171,7 @@ class MagicSchoolMasteryCache
 {
 	const IBonusBearer * target;
 	mutable std::atomic<int32_t> version = 0;
-	mutable std::array<std::atomic<int32_t>, 4+1> schools;
+	mutable std::vector<std::atomic<int32_t>> schools;
 
 	void update() const;
 public:

+ 0 - 6
lib/bonuses/BonusEnum.cpp

@@ -14,12 +14,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-#define BONUS_NAME(x) { #x, BonusType::x },
-	const std::map<std::string, BonusType> bonusNameMap = {
-		BONUS_LIST
-	};
-#undef BONUS_NAME
-
 #define BONUS_VALUE(x) { #x, BonusValueType::x },
 	const std::map<std::string, BonusValueType> bonusValueMap = { BONUS_VALUE_LIST };
 #undef BONUS_VALUE

+ 9 - 6
lib/bonuses/BonusEnum.h

@@ -91,8 +91,8 @@ class JsonNode;
 	BONUS_NAME(DEFENSIVE_STANCE) /* val - bonus to defense while defending */ \
 	BONUS_NAME(ATTACKS_ALL_ADJACENT) /*eg. hydra*/		\
 	BONUS_NAME(MORE_DAMAGE_FROM_SPELL) /*value - damage increase in %, subtype - spell id*/ \
-	BONUS_NAME(FEAR)									\
-	BONUS_NAME(FEARLESS)								\
+	BONUS_NAME(FEARFUL)									\
+	BONUS_NAME(LIVING)									\
 	BONUS_NAME(NO_DISTANCE_PENALTY)						\
 	BONUS_NAME(ENCHANTER)/* for Enchanter spells, val - skill level, subtype - spell id, additionalInfo - cooldown */ \
 	BONUS_NAME(HEALER)									\
@@ -142,13 +142,13 @@ class JsonNode;
 	BONUS_NAME(MANUAL_CONTROL) /* manually control warmachine with id = subtype, chance = val */  \
 	BONUS_NAME(WIDE_BREATH) /* initial desigh: dragon breath affecting multiple nearby hexes */\
 	BONUS_NAME(FIRST_STRIKE) /* first counterattack, then attack if possible */\
-	BONUS_NAME(SYNERGY_TARGET) /* dummy skill for alternative upgrades mod */\
+	BONUS_NAME(VULNERABLE_FROM_BACK) /*bonus damage for attacks from behind*/\
 	BONUS_NAME(SHOOTS_ALL_ADJACENT) /* H4 Cyclops-like shoot (attacks all hexes neighbouring with target) without spell-like mechanics */\
 	BONUS_NAME(BLOCK_MAGIC_BELOW) /*blocks casting spells of the level < value */ \
 	BONUS_NAME(DESTRUCTION) /*kills extra units after hit, subtype = 0 - kill percentage of units, 1 - kill amount, val = chance in percent to trigger, additional info - amount/percentage to kill*/ \
 	BONUS_NAME(SPECIAL_CRYSTAL_GENERATION) /*crystal dragon crystal generation*/ \
 	BONUS_NAME(NO_SPELLCAST_BY_DEFAULT) /*spellcast will not be default attack option for this creature*/ \
-	BONUS_NAME(GARGOYLE) /* gargoyle is special than NON_LIVING, cannot be rised or healed */ \
+	BONUS_NAME(SKELETON_TRANSFORMER_TARGET) /* for skeleton transformer */ \
 	BONUS_NAME(SPECIAL_ADD_VALUE_ENCHANT) /*specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add*/\
 	BONUS_NAME(SPECIAL_FIXED_VALUE_ENCHANT) /*specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix.*/\
 	BONUS_NAME(THIEVES_GUILD_ACCESS) \
@@ -189,6 +189,9 @@ class JsonNode;
 	BONUS_NAME(MULTIHEX_ENEMY_ATTACK) /*eg. dragons*/	\
 	BONUS_NAME(MULTIHEX_ANIMATION) /*eg. dragons*/	\
 	BONUS_NAME(STACK_EXPERIENCE_GAIN_PERCENT) /*modifies all stack experience gains*/\
+	BONUS_NAME(FULL_MAP_SCOUTING) /*Skyship*/\
+	BONUS_NAME(FULL_MAP_DARKNESS) /*opposite to Skyship*/\
+	BONUS_NAME(TRANSMUTATION_IMMUNITY) /*blocks TRANSMUTATION bonus*/\
 	/* end of list */
 
 
@@ -223,11 +226,12 @@ class JsonNode;
 	BONUS_VALUE(INDEPENDENT_MIN) //used for SECONDARY_SKILL_PREMY bonus
 
 
-enum class BonusType : uint8_t
+enum class BonusType : uint16_t
 {
 #define BONUS_NAME(x) x,
     BONUS_LIST
 #undef BONUS_NAME
+    BUILTIN_BONUSES_COUNT
 };
 namespace BonusDuration  //when bonus is automatically removed
 {
@@ -272,7 +276,6 @@ enum class BonusValueType : uint8_t
 #undef BONUS_VALUE
 };
 
-extern DLL_LINKAGE const std::map<std::string, BonusType> bonusNameMap;
 extern DLL_LINKAGE const std::map<std::string, BonusValueType> bonusValueMap;
 extern DLL_LINKAGE const std::map<std::string, BonusSource> bonusSourceMap;
 extern DLL_LINKAGE const std::map<std::string, BonusDuration::Type> bonusDurationMap;

+ 12 - 4
lib/bonuses/BonusList.cpp

@@ -69,7 +69,11 @@ int BonusList::totalValue(int baseValue) const
 		int indepMax = std::numeric_limits<int>::min();
 	};
 
-	auto applyPercentage = [](int base, int percent) -> int {
+	auto applyPercentageRoundUp = [](int base, int percent) -> int {
+		return (static_cast<int64_t>(base) * (100 + percent) + 99) / 100;
+	};
+
+	auto applyPercentageRoundDown = [](int base, int percent) -> int {
 		return (static_cast<int64_t>(base) * (100 + percent)) / 100;
 	};
 
@@ -96,7 +100,11 @@ int BonusList::totalValue(int baseValue) const
 	for(const auto & b : bonuses)
 	{
 		int sourceIndex = vstd::to_underlying(b->source);
-		int valModified	= applyPercentage(b->val, percentToSource[sourceIndex]);
+		// Workaround: creature hero specialties in H3 is the only place that uses rounding up in bonuses
+		// TODO: try to find more elegant solution?
+		int valModified	= b->source == BonusSource::CREATURE_ABILITY ?
+			applyPercentageRoundUp(b->val, percentToSource[sourceIndex]):
+			applyPercentageRoundDown(b->val, percentToSource[sourceIndex]);
 
 		switch(b->valType)
 		{
@@ -123,9 +131,9 @@ int BonusList::totalValue(int baseValue) const
 		}
 	}
 
-	accumulated.base = applyPercentage(accumulated.base, accumulated.percentToBase);
+	accumulated.base = applyPercentageRoundDown(accumulated.base, accumulated.percentToBase);
 	accumulated.base += accumulated.additive;
-	auto valFirst = applyPercentage(accumulated.base ,accumulated.percentToAll);
+	auto valFirst = applyPercentageRoundDown(accumulated.base ,accumulated.percentToAll);
 
 	if(indexMinCount && indexMaxCount && accumulated.indepMin < accumulated.indepMax)
 		accumulated.indepMax = accumulated.indepMin;

+ 0 - 374
lib/bonuses/BonusParams.cpp

@@ -1,374 +0,0 @@
-/*
- * BonusParams.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#include "StdInc.h"
-
-#include "BonusEnum.h"
-#include "BonusParams.h"
-#include "BonusSelector.h"
-
-#include "../ResourceSet.h"
-#include "../GameLibrary.h"
-#include "../modding/IdentifierStorage.h"
-#include "../modding/ModScope.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-const std::set<std::string> deprecatedBonusSet = {
-	"SECONDARY_SKILL_PREMY",
-	"SECONDARY_SKILL_VAL2",
-	"MAXED_SPELL",
-	"LAND_MOVEMENT",
-	"SEA_MOVEMENT",
-	"SIGHT_RADIOUS",
-	"NO_TYPE",
-	"SPECIAL_SECONDARY_SKILL",
-	"FULL_HP_REGENERATION",
-	"KING1",
-	"KING2",
-	"KING3",
-	"BLOCK_MORALE",
-	"BLOCK_LUCK",
-	"SELF_MORALE",
-	"SELF_LUCK",
-	"DIRECT_DAMAGE_IMMUNITY",
-	"AIR_SPELL_DMG_PREMY",
-	"EARTH_SPELL_DMG_PREMY"
-	"FIRE_SPELL_DMG_PREMY"
-	"WATER_SPELL_DMG_PREMY",
-	"FIRE_SPELLS",
-	"AIR_SPELLS",
-	"WATER_SPELLS",
-	"EARTH_SPELLS",
-	"FIRE_IMMUNITY",
-	"AIR_IMMUNITY",
-	"WATER_IMMUNITY",
-	"EARTH_IMMUNITY"
-};
-
-BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSubtypeStr, int deprecatedSubtype):
-	isConverted(true)
-{
-	if(deprecatedTypeStr == "SECONDARY_SKILL_PREMY" || deprecatedTypeStr == "SPECIAL_SECONDARY_SKILL")
-	{
-		if(deprecatedSubtype == SecondarySkill::PATHFINDING || deprecatedSubtypeStr == "skill.pathfinding")
-			type = BonusType::ROUGH_TERRAIN_DISCOUNT;
-		else if(deprecatedSubtype == SecondarySkill::DIPLOMACY || deprecatedSubtypeStr == "skill.diplomacy")
-			type = BonusType::WANDERING_CREATURES_JOIN_BONUS;
-		else if(deprecatedSubtype == SecondarySkill::WISDOM || deprecatedSubtypeStr == "skill.wisdom")
-			type = BonusType::MAX_LEARNABLE_SPELL_LEVEL;
-		else if(deprecatedSubtype == SecondarySkill::MYSTICISM || deprecatedSubtypeStr == "skill.mysticism")
-			type = BonusType::MANA_REGENERATION;
-		else if(deprecatedSubtype == SecondarySkill::NECROMANCY || deprecatedSubtypeStr == "skill.necromancy")
-			type = BonusType::UNDEAD_RAISE_PERCENTAGE;
-		else if(deprecatedSubtype == SecondarySkill::LEARNING || deprecatedSubtypeStr == "skill.learning")
-			type = BonusType::HERO_EXPERIENCE_GAIN_PERCENT;
-		else if(deprecatedSubtype == SecondarySkill::RESISTANCE || deprecatedSubtypeStr == "skill.resistance")
-			type = BonusType::MAGIC_RESISTANCE;
-		else if(deprecatedSubtype == SecondarySkill::EAGLE_EYE || deprecatedSubtypeStr == "skill.eagleEye")
-			type = BonusType::LEARN_BATTLE_SPELL_CHANCE;
-		else if(deprecatedSubtype == SecondarySkill::SCOUTING || deprecatedSubtypeStr == "skill.scouting")
-			type = BonusType::SIGHT_RADIUS;
-		else if(deprecatedSubtype == SecondarySkill::INTELLIGENCE || deprecatedSubtypeStr == "skill.intelligence")
-		{
-			type = BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE;
-			valueType = BonusValueType::PERCENT_TO_BASE;
-		}
-		else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery")
-		{
-			type = BonusType::SPELL_DAMAGE;
-			subtype = BonusSubtypeID(SpellSchool::ANY);
-		}
-		else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar")
-			type = BonusType::LEARN_MEETING_SPELL_LIMIT;
-		else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery")
-		{
-			subtype = BonusCustomSubtype::damageTypeRanged;
-			type = BonusType::PERCENTAGE_DAMAGE_BOOST;
-		}
-		else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence")
-		{
-			subtype = BonusCustomSubtype::damageTypeMelee;
-			type = BonusType::PERCENTAGE_DAMAGE_BOOST;
-		}
-		else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer")
-		{
-			subtype = BonusCustomSubtype::damageTypeAll;
-			type = BonusType::GENERAL_DAMAGE_REDUCTION;
-		}
-		else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation")
-		{
-			subtype = BonusCustomSubtype::heroMovementSea;
-			valueType = BonusValueType::PERCENT_TO_BASE;
-			type = BonusType::MOVEMENT;
-		}
-		else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics")
-		{
-			subtype = BonusCustomSubtype::heroMovementLand;
-			valueType = BonusValueType::PERCENT_TO_BASE;
-			type = BonusType::MOVEMENT;
-		}
-		else if(deprecatedSubtype == SecondarySkill::ESTATES || deprecatedSubtypeStr == "skill.estates")
-		{
-			type = BonusType::GENERATE_RESOURCE;
-			subtype = BonusSubtypeID(GameResID(EGameResID::GOLD));
-		}
-		else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic")
-		{
-			type = BonusType::MAGIC_SCHOOL_SKILL;
-			subtype = BonusSubtypeID(SpellSchool::AIR);
-		}
-		else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic")
-		{
-			type = BonusType::MAGIC_SCHOOL_SKILL;
-			subtype = BonusSubtypeID(SpellSchool::WATER);
-		}
-		else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic")
-		{
-			type = BonusType::MAGIC_SCHOOL_SKILL;
-			subtype = BonusSubtypeID(SpellSchool::FIRE);
-		}
-		else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic")
-		{
-			type = BonusType::MAGIC_SCHOOL_SKILL;
-			subtype = BonusSubtypeID(SpellSchool::EARTH);
-		}
-		else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery")
-		{
-			type = BonusType::BONUS_DAMAGE_CHANCE;
-			subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA));
-		}
-		else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid")
-		{
-			type = BonusType::SPECIFIC_SPELL_POWER;
-			subtype = SpellID(*LIBRARY->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "firstAid"));
-		}
-		else if (deprecatedSubtype == SecondarySkill::BALLISTICS || deprecatedSubtypeStr == "skill.ballistics")
-		{
-			type = BonusType::CATAPULT_EXTRA_SHOTS;
-			subtype = SpellID(*LIBRARY->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "catapultShot"));
-		}
-		else
-			isConverted = false;
-	}
-	else if (deprecatedTypeStr == "SECONDARY_SKILL_VAL2")
-	{
-		if(deprecatedSubtype == SecondarySkill::EAGLE_EYE || deprecatedSubtypeStr == "skill.eagleEye")
-			type = BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT;
-		else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery")
-		{
-			type = BonusType::HERO_GRANTS_ATTACKS;
-			subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA));
-		}
-		else
-			isConverted = false;
-	}
-	else if (deprecatedTypeStr == "SEA_MOVEMENT")
-	{
-		subtype = BonusCustomSubtype::heroMovementSea;
-		valueType = BonusValueType::ADDITIVE_VALUE;
-		type = BonusType::MOVEMENT;
-	}
-	else if (deprecatedTypeStr == "LAND_MOVEMENT")
-	{
-		subtype = BonusCustomSubtype::heroMovementLand;
-		valueType = BonusValueType::ADDITIVE_VALUE;
-		type = BonusType::MOVEMENT;
-	}
-	else if (deprecatedTypeStr == "MAXED_SPELL")
-	{
-		type = BonusType::SPELL;
-		subtype = SpellID(*LIBRARY->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", deprecatedSubtypeStr));
-		valueType = BonusValueType::INDEPENDENT_MAX;
-		val = 3;
-	}
-	else if (deprecatedTypeStr == "FULL_HP_REGENERATION")
-	{
-		type = BonusType::HP_REGENERATION;
-		val = 100000; //very high value to always chose stack health
-	}
-	else if (deprecatedTypeStr == "KING1")
-	{
-		type = BonusType::KING;
-		val = 0;
-	}
-	else if (deprecatedTypeStr == "KING2")
-	{
-		type = BonusType::KING;
-		val = 2;
-	}
-	else if (deprecatedTypeStr == "KING3")
-	{
-		type = BonusType::KING;
-		val = 3;
-	}
-	else if (deprecatedTypeStr == "SIGHT_RADIOUS")
-		type = BonusType::SIGHT_RADIUS;
-	else if (deprecatedTypeStr == "SELF_MORALE")
-	{
-		type = BonusType::MORALE;
-		val = 1;
-		valueType = BonusValueType::INDEPENDENT_MAX;
-	}
-	else if (deprecatedTypeStr == "SELF_LUCK")
-	{
-		type = BonusType::LUCK;
-		val = 1;
-		valueType = BonusValueType::INDEPENDENT_MAX;
-	}
-	else if (deprecatedTypeStr == "DIRECT_DAMAGE_IMMUNITY")
-	{
-		type = BonusType::SPELL_DAMAGE_REDUCTION;
-		subtype = BonusSubtypeID(SpellSchool::ANY);
-		val = 100;
-	}
-	else if (deprecatedTypeStr == "AIR_SPELL_DMG_PREMY")
-	{
-		type = BonusType::SPELL_DAMAGE;
-		subtype = BonusSubtypeID(SpellSchool::AIR);
-	}
-	else if (deprecatedTypeStr == "FIRE_SPELL_DMG_PREMY")
-	{
-		type = BonusType::SPELL_DAMAGE;
-		subtype = BonusSubtypeID(SpellSchool::FIRE);
-	}
-	else if (deprecatedTypeStr == "WATER_SPELL_DMG_PREMY")
-	{
-		type = BonusType::SPELL_DAMAGE;
-		subtype = BonusSubtypeID(SpellSchool::WATER);
-	}
-	else if (deprecatedTypeStr == "EARTH_SPELL_DMG_PREMY")
-	{
-		type = BonusType::SPELL_DAMAGE;
-		subtype = BonusSubtypeID(SpellSchool::EARTH);
-	}
-	else if (deprecatedTypeStr == "AIR_SPELLS")
-	{
-		type = BonusType::SPELLS_OF_SCHOOL;
-		subtype = BonusSubtypeID(SpellSchool::AIR);
-	}
-	else if (deprecatedTypeStr == "FIRE_SPELLS")
-	{
-		type = BonusType::SPELLS_OF_SCHOOL;
-		subtype = BonusSubtypeID(SpellSchool::FIRE);
-	}
-	else if (deprecatedTypeStr == "WATER_SPELLS")
-	{
-		type = BonusType::SPELLS_OF_SCHOOL;
-		subtype = BonusSubtypeID(SpellSchool::WATER);
-	}
-	else if (deprecatedTypeStr == "EARTH_SPELLS")
-	{
-		type = BonusType::SPELLS_OF_SCHOOL;
-		subtype = BonusSubtypeID(SpellSchool::EARTH);
-	}
-	else if (deprecatedTypeStr == "AIR_IMMUNITY")
-	{
-		subtype = BonusSubtypeID(SpellSchool::AIR);
-		switch(deprecatedSubtype)
-		{
-			case 0:
-				type = BonusType::SPELL_SCHOOL_IMMUNITY;
-				break;
-			case 1:
-				type = BonusType::NEGATIVE_EFFECTS_IMMUNITY;
-				break;
-			default:
-				type = BonusType::SPELL_DAMAGE_REDUCTION;
-				val = 100;
-		}
-	}
-	else if (deprecatedTypeStr == "FIRE_IMMUNITY")
-	{
-		subtype = BonusSubtypeID(SpellSchool::FIRE);
-		switch(deprecatedSubtype)
-		{
-			case 0:
-				type = BonusType::SPELL_SCHOOL_IMMUNITY;
-				break;
-			case 1:
-				type = BonusType::NEGATIVE_EFFECTS_IMMUNITY;
-				break;
-			default:
-				type = BonusType::SPELL_DAMAGE_REDUCTION;
-				val = 100;
-		}
-	}
-	else if (deprecatedTypeStr == "WATER_IMMUNITY")
-	{
-		subtype = BonusSubtypeID(SpellSchool::WATER);
-		switch(deprecatedSubtype)
-		{
-			case 0:
-				type = BonusType::SPELL_SCHOOL_IMMUNITY;
-				break;
-			case 1:
-				type = BonusType::NEGATIVE_EFFECTS_IMMUNITY;
-				break;
-			default:
-				type = BonusType::SPELL_DAMAGE_REDUCTION;
-				val = 100;
-		}
-	}
-	else if (deprecatedTypeStr == "EARTH_IMMUNITY")
-	{
-		subtype = BonusSubtypeID(SpellSchool::EARTH);
-		switch(deprecatedSubtype)
-		{
-			case 0:
-				type = BonusType::SPELL_SCHOOL_IMMUNITY;
-				break;
-			case 1:
-				type = BonusType::NEGATIVE_EFFECTS_IMMUNITY;
-				break;
-			default:
-				type = BonusType::SPELL_DAMAGE_REDUCTION;
-				val = 100;
-		}
-	}
-	else
-		isConverted = false;
-}
-
-const JsonNode & BonusParams::toJson()
-{
-	assert(isConverted);
-	if(ret.isNull())
-	{
-		ret["type"].String() = vstd::findKey(bonusNameMap, type);
-		if(subtype)
-			ret["subtype"].String() = subtype->toString();
-		if(valueType)
-			ret["valueType"].String() = vstd::findKey(bonusValueMap, *valueType);
-		if(val)
-			ret["val"].Float() = *val;
-		if(targetType)
-			ret["targetSourceType"].String() = vstd::findKey(bonusSourceMap, *targetType);
-		jsonCreated = true;
-	}
-	ret.setModScope(ModScope::scopeGame());
-	return ret;
-};
-
-CSelector BonusParams::toSelector()
-{
-	assert(isConverted);
-
-	auto ret = Selector::type()(type);
-	if(subtype)
-		ret = ret.And(Selector::subtype()(*subtype));
-	if(valueType)
-		ret = ret.And(Selector::valueType(*valueType));
-	if(targetType)
-		ret = ret.And(Selector::targetSourceType()(*targetType));
-	return ret;
-}
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 38
lib/bonuses/BonusParams.h

@@ -1,38 +0,0 @@
-/*
- * BonusParams.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "Bonus.h"
-
-#include "../GameConstants.h"
-#include "../json/JsonNode.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct DLL_LINKAGE BonusParams {
-	bool isConverted;
-	BonusType type = BonusType::NONE;
-	std::optional<BonusSubtypeID> subtype = std::nullopt;
-	std::optional<BonusValueType> valueType = std::nullopt;
-	std::optional<si32> val = std::nullopt;
-	std::optional<BonusSource> targetType = std::nullopt;
-
-	BonusParams(bool isConverted = true) : isConverted(isConverted) {};
-	BonusParams(std::string deprecatedTypeStr, std::string deprecatedSubtypeStr = "", int deprecatedSubtype = 0);
-	const JsonNode & toJson();
-	CSelector toSelector();
-private:
-	JsonNode ret;
-	bool jsonCreated = false;
-};
-
-extern DLL_LINKAGE const std::set<std::string> deprecatedBonusSet;
-
-VCMI_LIB_NAMESPACE_END

+ 8 - 5
lib/bonuses/IBonusBearer.cpp

@@ -15,10 +15,10 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-int IBonusBearer::valOfBonuses(const CSelector &selector, const std::string &cachingStr) const
+int IBonusBearer::valOfBonuses(const CSelector &selector, const std::string &cachingStr, int baseValue) const
 {
 	TConstBonusListPtr hlp = getAllBonuses(selector, nullptr, cachingStr);
-	return hlp->totalValue();
+	return hlp->totalValue(baseValue);
 }
 
 bool IBonusBearer::hasBonus(const CSelector &selector, const std::string &cachingStr) const
@@ -63,14 +63,17 @@ TConstBonusListPtr IBonusBearer::getBonusesOfType(BonusType type, BonusSubtypeID
 	return getBonuses(s, cachingStr);
 }
 
-int IBonusBearer::valOfBonuses(BonusType type) const
+int IBonusBearer::applyBonuses(BonusType type, int baseValue) const
 {
 	//This part is performance-critical
 	std::string cachingStr = "type_" + std::to_string(static_cast<int>(type));
-
 	CSelector s = Selector::type()(type);
+	return valOfBonuses(s, cachingStr, baseValue);
+}
 
-	return valOfBonuses(s, cachingStr);
+int IBonusBearer::valOfBonuses(BonusType type) const
+{
+	return applyBonuses(type, 0);
 }
 
 bool IBonusBearer::hasBonusOfType(BonusType type) const

+ 2 - 1
lib/bonuses/IBonusBearer.h

@@ -21,7 +21,7 @@ public:
 	IBonusBearer() = default;
 	virtual ~IBonusBearer() = default;
 	virtual TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const = 0;
-	int valOfBonuses(const CSelector &selector, const std::string &cachingStr = {}) const;
+	int valOfBonuses(const CSelector &selector, const std::string &cachingStr = {}, int baseValue = 0) const;
 	bool hasBonus(const CSelector &selector, const std::string &cachingStr = {}) const;
 	bool hasBonus(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const;
 	TConstBonusListPtr getBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const;
@@ -30,6 +30,7 @@ public:
 	std::shared_ptr<const Bonus> getBonus(const CSelector &selector) const; //returns any bonus visible on node that matches (or nullptr if none matches)
 
 	//Optimized interface (with auto-caching)
+	int applyBonuses(BonusType type, int baseValue) const; //subtype -> subtype of bonus;
 	int valOfBonuses(BonusType type) const; //subtype -> subtype of bonus;
 	bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype)
 	int valOfBonuses(BonusType type, BonusSubtypeID subtype) const; //subtype -> subtype of bonus;

+ 3 - 2
lib/bonuses/Limiters.cpp

@@ -12,6 +12,7 @@
 #include "Limiters.h"
 #include "Updaters.h"
 
+#include "../CBonusTypeHandler.h"
 #include "../GameLibrary.h"
 #include "../entities/faction/CFaction.h"
 #include "../entities/faction/CTownHandler.h"
@@ -188,7 +189,7 @@ ILimiter::EDecision HasAnotherBonusLimiter::limit(const BonusLimitationContext &
 
 std::string HasAnotherBonusLimiter::toString() const
 {
-	std::string typeName = vstd::findKey(bonusNameMap, type);
+	std::string typeName = LIBRARY->bth->bonusToString(type);
 	if(isSubtypeRelevant)
 	{
 		boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%s)");
@@ -206,7 +207,7 @@ std::string HasAnotherBonusLimiter::toString() const
 JsonNode HasAnotherBonusLimiter::toJsonNode() const
 {
 	JsonNode root;
-	std::string typeName = vstd::findKey(bonusNameMap, type);
+	std::string typeName = LIBRARY->bth->bonusToString(type);
 	auto sourceTypeName = vstd::findKey(bonusSourceMap, source);
 
 	root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER";

+ 38 - 2
lib/bonuses/Updaters.cpp

@@ -33,7 +33,9 @@ JsonNode IUpdater::toJsonNode() const
 	return JsonNode();
 }
 
-GrowsWithLevelUpdater::GrowsWithLevelUpdater(int valPer20, int stepSize) : valPer20(valPer20), stepSize(stepSize)
+GrowsWithLevelUpdater::GrowsWithLevelUpdater(int valPer20, int stepSize)
+	: valPer20(valPer20)
+	, stepSize(stepSize)
 {
 }
 
@@ -76,7 +78,7 @@ std::shared_ptr<Bonus> TimesHeroLevelUpdater::createUpdatedBonus(const std::shar
 	{
 		int level = dynamic_cast<const CGHeroInstance &>(context).level;
 		auto newBonus = std::make_shared<Bonus>(*b);
-		newBonus->val *= level;
+		newBonus->val *= level / stepSize;
 		return newBonus;
 	}
 	return b;
@@ -113,6 +115,40 @@ JsonNode TimesHeroLevelDivideStackLevelUpdater::toJsonNode() const
 	return JsonNode("TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL");
 }
 
+std::shared_ptr<Bonus> TimesStackSizeUpdater::apply(const std::shared_ptr<Bonus> & b, int count) const
+{
+	auto newBonus = std::make_shared<Bonus>(*b);
+	newBonus->val *= std::clamp(count / stepSize, minimum, maximum);
+	newBonus->updater = nullptr; // prevent double-apply
+	return newBonus;
+}
+
+std::shared_ptr<Bonus> TimesStackSizeUpdater::createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const
+{
+	if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE || context.getNodeType() == CBonusSystemNode::COMMANDER)
+	{
+		int count = dynamic_cast<const CStackInstance &>(context).getCount();
+		return apply(b, count);
+	}
+
+	if(context.getNodeType() == CBonusSystemNode::STACK_BATTLE)
+	{
+		const auto & stack = dynamic_cast<const CStack &>(context);
+		return apply(b, stack.getCount());
+	}
+	return b;
+}
+
+std::string TimesStackSizeUpdater::toString() const
+{
+	return "TimesStackSizeUpdater";
+}
+
+JsonNode TimesStackSizeUpdater::toJsonNode() const
+{
+	return JsonNode("TIMES_STACK_SIZE");
+}
+
 std::shared_ptr<Bonus> TimesStackLevelUpdater::apply(const std::shared_ptr<Bonus> & b, int level) const
 {
 	auto newBonus = std::make_shared<Bonus>(*b);

+ 37 - 12
lib/bonuses/Updaters.h

@@ -64,10 +64,20 @@ public:
 
 class DLL_LINKAGE TimesHeroLevelUpdater : public IUpdater
 {
+	int stepSize = 1;
 public:
+	TimesHeroLevelUpdater() = default;
+	TimesHeroLevelUpdater(int stepSize)
+		: stepSize(stepSize)
+	{
+		assert(stepSize > 0);
+	}
+
 	template <typename Handler> void serialize(Handler & h)
 	{
 		h & static_cast<IUpdater &>(*this);
+		if (h.hasFeature(Handler::Version::UNIVERSITY_CONFIG))
+			h & stepSize;
 	}
 
 	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const override;
@@ -75,15 +85,39 @@ public:
 	JsonNode toJsonNode() const override;
 };
 
-class DLL_LINKAGE TimesStackLevelUpdater : public IUpdater
+class DLL_LINKAGE TimesStackSizeUpdater : public IUpdater
 {
-	std::shared_ptr<Bonus> apply(const std::shared_ptr<Bonus> & b, int level) const;
+	std::shared_ptr<Bonus> apply(const std::shared_ptr<Bonus> & b, int count) const;
+
+	int minimum;
+	int maximum;
+	int stepSize;
 public:
+	TimesStackSizeUpdater() = default;
+	TimesStackSizeUpdater(int minimum, int maximum, int stepSize)
+		: minimum(minimum)
+		, maximum(maximum)
+		, stepSize(stepSize)
+	{}
+
+	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const override;
+	std::string toString() const override;
+	JsonNode toJsonNode() const override;
+
 	template <typename Handler> void serialize(Handler & h)
 	{
 		h & static_cast<IUpdater &>(*this);
+		h & minimum;
+		h & maximum;
+		h & stepSize;
 	}
+};
 
+class DLL_LINKAGE TimesStackLevelUpdater : public IUpdater
+{
+	std::shared_ptr<Bonus> apply(const std::shared_ptr<Bonus> & b, int level) const;
+
+public:
 	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const override;
 	std::string toString() const override;
 	JsonNode toJsonNode() const override;
@@ -92,12 +126,8 @@ public:
 class DLL_LINKAGE DivideStackLevelUpdater : public IUpdater
 {
 	std::shared_ptr<Bonus> apply(const std::shared_ptr<Bonus> & b, int level) const;
-public:
-	template <typename Handler> void serialize(Handler & h)
-	{
-		h & static_cast<IUpdater &>(*this);
-	}
 
+public:
 	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const override;
 	std::string toString() const override;
 	JsonNode toJsonNode() const override;
@@ -125,11 +155,6 @@ public:
 class DLL_LINKAGE OwnerUpdater : public IUpdater
 {
 public:
-	template <typename Handler> void serialize(Handler& h)
-	{
-		h & static_cast<IUpdater &>(*this);
-	}
-
 	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus>& b, const CBonusSystemNode& context) const override;
 	std::string toString() const override;
 	JsonNode toJsonNode() const override;

+ 1 - 1
lib/callback/CGameInfoCallback.cpp

@@ -903,7 +903,7 @@ void CGameInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 		logGlobal->error("Illegal call to getTilesInRange!");
 		return;
 	}
-	if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map
+	if(radious == GameConstants::FULL_MAP_RANGE)
 		getAllTiles (tiles, player, -1, [](auto * tile){return true;});
 	else
 	{

+ 8 - 7
lib/constants/EntityIdentifiers.cpp

@@ -29,9 +29,10 @@
 #include "modding/IdentifierStorage.h"
 #include "modding/ModScope.h"
 #include "GameLibrary.h"
-#include "CCreatureHandler.h"//todo: remove
-#include "spells/CSpellHandler.h" //todo: remove
-#include "CSkillHandler.h"//todo: remove
+#include "CCreatureHandler.h"
+#include "spells/CSpellHandler.h"
+#include "spells/SpellSchoolHandler.h"
+#include "CSkillHandler.h"
 #include "entities/artifact/CArtifact.h"
 #include "entities/faction/CFaction.h"
 #include "entities/hero/CHero.h"
@@ -39,7 +40,7 @@
 #include "mapObjectConstructors/AObjectTypeHandler.h"
 #include "constants/StringConstants.h"
 #include "texts/CGeneralTextHandler.h"
-#include "TerrainHandler.h" //TODO: remove
+#include "TerrainHandler.h"
 #include "RiverHandler.h"
 #include "RoadHandler.h"
 #include "BattleFieldHandler.h"
@@ -77,8 +78,8 @@ const TeamID TeamID::NO_TEAM(-1);
 const SpellSchool SpellSchool::ANY(-1);
 const SpellSchool SpellSchool::AIR(0);
 const SpellSchool SpellSchool::FIRE(1);
-const SpellSchool SpellSchool::WATER(2);
-const SpellSchool SpellSchool::EARTH(3);
+const SpellSchool SpellSchool::EARTH(2);
+const SpellSchool SpellSchool::WATER(3);
 
 const FactionID FactionID::NONE(-2);
 const FactionID FactionID::DEFAULT(-1);
@@ -595,7 +596,7 @@ std::string SpellSchool::encode(const si32 index)
 	if (index == ANY.getNum())
 		return "any";
 
-	return SpellConfig::SCHOOL[index].jsonName;
+	return LIBRARY->spellSchoolHandler->getById(SpellSchool(index))->getJsonKey();
 }
 
 std::string SpellSchool::entityType()

+ 0 - 3
lib/constants/EntityIdentifiers.h

@@ -816,11 +816,8 @@ public:
 		IMP = 42, // for Deity of Fire
 		FAMILIAR = 43, // for Deity of Fire
 		SKELETON = 56, // for Skeleton Transformer
-		BONE_DRAGON = 68, // for Skeleton Transformer
 		TROGLODYTES = 70, // for Abandoned Mine
 		MEDUSA = 76, // for Siege UI workaround
-		HYDRA = 110, // for Skeleton Transformer
-		CHAOS_HYDRA = 111, // for Skeleton Transformer
 		AIR_ELEMENTAL = 112, // for tests
 		FIRE_ELEMENTAL = 114, // for tests
 		PSYCHIC_ELEMENTAL = 120, // for hardcoded ability

+ 2 - 1
lib/constants/NumericConstants.h

@@ -25,7 +25,6 @@ namespace GameConstants
 	constexpr int CREATURES_PER_TOWN = 8; //without upgrades
 	constexpr int SPELL_LEVELS = 5;
 	constexpr int SPELL_SCHOOL_LEVELS = 4;
-	constexpr int DEFAULT_SCHOOLS = 4;
 	constexpr int CRE_LEVELS = 10; // number of creature experience levels
 
 	constexpr int HERO_GOLD_COST = 2500;
@@ -54,6 +53,8 @@ namespace GameConstants
 	constexpr int TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD = 144*144*2; //map tiles count threshold for 2 dimension door casts with tournament rules
 	constexpr int KINGDOM_WINDOW_HEROES_SLOTS = 4;
 	constexpr int INFO_WINDOW_ARTIFACTS_MAX_ITEMS = 14;
+
+	constexpr int FULL_MAP_RANGE = std::numeric_limits<int>::max();
 }
 
 VCMI_LIB_NAMESPACE_END

+ 0 - 8
lib/entities/building/CBuilding.cpp

@@ -25,14 +25,6 @@ const std::map<std::string, CBuilding::EBuildMode> CBuilding::MODES =
 		{ "grail", CBuilding::BUILD_GRAIL }
 };
 
-const std::map<std::string, CBuilding::ETowerHeight> CBuilding::TOWER_TYPES =
-	{
-		{ "low", CBuilding::HEIGHT_LOW },
-		{ "average", CBuilding::HEIGHT_AVERAGE },
-		{ "high", CBuilding::HEIGHT_HIGH },
-		{ "skyship", CBuilding::HEIGHT_SKYSHIP }
-};
-
 BuildingTypeUniqueID CBuilding::getUniqueTypeID() const
 {
 	return BuildingTypeUniqueID(town->faction->getId(), bid);

+ 2 - 10
lib/entities/building/CBuilding.h

@@ -15,6 +15,7 @@
 #include "../../LogicalExpression.h"
 #include "../../ResourceSet.h"
 #include "../../bonuses/BonusList.h"
+#include "../../networkPacks/TradeItem.h"
 #include "../../rewardable/Info.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -39,6 +40,7 @@ public:
 	ArtifactID warMachine;
 	TownFortifications fortifications;
 	std::set<EMarketMode> marketModes;
+	std::vector<TradeItemBuy> marketOffer;
 
 	BuildingID bid; //structure ID
 	BuildingID upgrade; /// indicates that building "upgrade" can be improved by this, -1 = empty
@@ -58,17 +60,7 @@ public:
 		BUILD_GRAIL    // 3 - grail - building requires grail to be built
 	} mode;
 
-	enum ETowerHeight // for lookup towers and some grails
-	{
-		HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability
-		HEIGHT_LOW = 10,     // low lookout tower, but castle without lookout tower gives radius 5
-		HEIGHT_AVERAGE = 15,
-		HEIGHT_HIGH = 20,    // such tower is in the Tower town
-		HEIGHT_SKYSHIP = std::numeric_limits<int>::max()  // grail, open entire map
-	} height;
-
 	static const std::map<std::string, CBuilding::EBuildMode> MODES;
-	static const std::map<std::string, CBuilding::ETowerHeight> TOWER_TYPES;
 
 	CBuilding() : town(nullptr), mode(BUILD_NORMAL) {};
 

+ 17 - 3
lib/entities/faction/CTownHandler.cpp

@@ -283,8 +283,6 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 		? CBuilding::BUILD_GRAIL
 		: vstd::find_or(CBuilding::MODES, source["mode"].String(), CBuilding::BUILD_NORMAL);
 
-	ret->height = vstd::find_or(CBuilding::TOWER_TYPES, source["height"].String(), CBuilding::HEIGHT_NO_TOWER);
-
 	ret->identifier = stringID;
 	ret->modScope = source.getModScope();
 	ret->town = town;
@@ -386,7 +384,23 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	for(const auto & element : source["marketModes"].Vector())
 	{
 		if(MappedKeys::MARKET_NAMES_TO_TYPES.count(element.String()))
-			ret->marketModes.insert(MappedKeys::MARKET_NAMES_TO_TYPES.at(element.String()));
+		{
+			auto mode = MappedKeys::MARKET_NAMES_TO_TYPES.at(element.String());
+			ret->marketModes.insert(mode);
+
+			if (mode == EMarketMode::RESOURCE_SKILL)
+			{
+				const auto & items = source["marketOffer"].Vector();
+				ret->marketOffer.resize(items.size());
+				for (int i = 0; i < items.size(); ++i)
+				{
+					LIBRARY->identifiers()->requestIdentifier("secondarySkill", items[i], [ret, i](si32 identifier)
+					{
+						ret->marketOffer[i] = SecondarySkill(identifier);
+					});
+				}
+			}
+		}
 	}
 
 	registerObject(source.getModScope(), ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum());

+ 38 - 22
lib/entities/hero/CHeroHandler.cpp

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

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

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

+ 0 - 6
lib/gameState/CGameState.cpp

@@ -787,12 +787,6 @@ void CGameState::initTowns(vstd::RNG & randomGenerator)
 	if (campaign)
 		campaign->initTowns();
 
-	map->townUniversitySkills.clear();
-	map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::FIRE_MAGIC));
-	map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::AIR_MAGIC));
-	map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::WATER_MAGIC));
-	map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::EARTH_MAGIC));
-
 	for (const auto & townID : map->getAllTowns())
 	{
 		auto vti = getTown(townID);

+ 3 - 3
lib/gameState/GameStatePackVisitor.cpp

@@ -1191,7 +1191,7 @@ void GameStatePackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack)
 {
 	CStack * st = gs.getBattle(pack.battleID)->getStack(pack.stackID);
 	assert(st);
-	switch(static_cast<BonusType>(pack.effect))
+	switch(pack.effect)
 	{
 		case BonusType::HP_REGENERATION:
 		{
@@ -1218,11 +1218,11 @@ void GameStatePackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack)
 		case BonusType::ENCHANTER:
 		case BonusType::MORALE:
 			break;
-		case BonusType::FEAR:
+		case BonusType::FEARFUL:
 			st->fear = true;
 			break;
 		default:
-			logNetwork->error("Unrecognized trigger effect type %d", pack.effect);
+			logNetwork->error("Unrecognized trigger effect type %d", static_cast<int>(pack.effect));
 	}
 }
 

+ 56 - 96
lib/json/JsonBonus.cpp

@@ -15,10 +15,10 @@
 
 #include "../texts/CGeneralTextHandler.h"
 #include "../GameLibrary.h"
-#include "../bonuses/BonusParams.h"
 #include "../bonuses/Limiters.h"
 #include "../bonuses/Propagators.h"
 #include "../bonuses/Updaters.h"
+#include "../CBonusTypeHandler.h"
 #include "../constants/StringConstants.h"
 #include "../modding/IdentifierStorage.h"
 
@@ -115,6 +115,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
 		case BonusType::HATE:
 		case BonusType::SUMMON_GUARDIANS:
 		case BonusType::MANUAL_CONTROL:
+		case BonusType::SKELETON_TRANSFORMER_TARGET:
 		{
 			LIBRARY->identifiers()->requestIdentifier( "creature", node, [&subtype](int32_t identifier)
 			{
@@ -185,14 +186,14 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso
 			break;
 		}
 		default:
-			for(const auto & i : bonusNameMap)
-				if(i.second == type)
-					logMod->warn("Bonus type %s does not supports subtypes!", i.first );
+		{
+			logMod->warn("Bonus type %s does not supports subtypes!", LIBRARY->bth->bonusToString(type));
 			subtype =  BonusSubtypeID();
+		}
 	}
 }
 
-static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & node)
+static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & value)
 {
 	const auto & getFirstValue = [](const JsonNode & jsonNode) -> const JsonNode &
 	{
@@ -202,7 +203,6 @@ static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & no
 			return jsonNode;
 	};
 
-	const JsonNode & value = node["addInfo"];
 	if (value.isNull())
 		return;
 
@@ -220,6 +220,10 @@ static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & no
 		case BonusType::PRIMARY_SKILL:
 		case BonusType::ENCHANTER:
 		case BonusType::SPECIAL_PECULIAR_ENCHANT:
+		case BonusType::SPELL_IMMUNITY:
+		case BonusType::DARKNESS:
+		case BonusType::FULL_MAP_SCOUTING:
+		case BonusType::FULL_MAP_DARKNESS:
 			// 1 number
 			var = getFirstValue(value).Integer();
 			break;
@@ -259,10 +263,7 @@ static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & no
 			}
 			break;
 		default:
-			for(const auto & i : bonusNameMap)
-				if(i.second == type)
-					logMod->warn("Bonus type %s does not supports addInfo!", i.first );
-
+			logMod->warn("Bonus type %s does not supports addInfo!", LIBRARY->bth->bonusToString(type) );
 	}
 }
 
@@ -370,33 +371,6 @@ static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource
 	}
 }
 
-static BonusParams convertDeprecatedBonus(const JsonNode &ability)
-{
-	if(vstd::contains(deprecatedBonusSet, ability["type"].String()))
-	{
-		logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toString());
-		auto params = BonusParams(ability["type"].String(),
-											ability["subtype"].isString() ? ability["subtype"].String() : "",
-											   ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1);
-		if(params.isConverted)
-		{
-			if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special
-			{
-				params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE;
-				params.targetType = BonusSource::SECONDARY_SKILL;
-			}
-
-			logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toString());
-			return params;
-		}
-		else
-			logMod->error("Cannot convert bonus!\n%s", ability.toString());
-	}
-	BonusParams ret;
-	ret.isConverted = false;
-	return ret;
-}
-
 static TUpdaterPtr parseUpdater(const JsonNode & updaterJson)
 {
 	const std::map<std::string, std::shared_ptr<IUpdater>> bonusUpdaterMap =
@@ -422,12 +396,29 @@ static TUpdaterPtr parseUpdater(const JsonNode & updaterJson)
 	case JsonNode::JsonType::DATA_STRUCT:
 		if(updaterJson["type"].String() == "GROWS_WITH_LEVEL")
 		{
-			auto updater = std::make_shared<GrowsWithLevelUpdater>();
+			// MOD COMPATIBILITY - parameters is deprecated in 1.7
 			const JsonVector param = updaterJson["parameters"].Vector();
-			updater->valPer20 = static_cast<int>(param[0].Integer());
-			if(param.size() > 1)
-				updater->stepSize = static_cast<int>(param[1].Integer());
-			return updater;
+			int valPer20 = updaterJson["valPer20"].isNull() ? param[0].Integer() : updaterJson["valPer20"].Integer();
+			int stepSize = updaterJson["stepSize"].isNull() ? param[1].Integer() : updaterJson["stepSize"].Integer();
+
+			return std::make_shared<GrowsWithLevelUpdater>(valPer20, std::max(1, stepSize));
+		}
+		if(updaterJson["type"].String() == "TIMES_HERO_LEVEL")
+		{
+			int stepSize = updaterJson["stepSize"].Integer();
+			return std::make_shared<TimesHeroLevelUpdater>(std::max(1, stepSize));
+		}
+		if(updaterJson["type"].String() == "TIMES_STACK_SIZE")
+		{
+			int minimum = updaterJson["minimum"].isNull() ? std::numeric_limits<int>::min() : updaterJson["minimum"].Integer();
+			int maximum = updaterJson["maximum"].isNull() ? std::numeric_limits<int>::max() : updaterJson["maximum"].Integer();
+			int stepSize = updaterJson["stepSize"].Integer();
+			if (minimum > maximum)
+			{
+				logMod->warn("TIMES_STACK_SIZE updater: minimum value (%d) is above maximum value(%d)!", minimum, maximum);
+				return std::make_shared<TimesStackSizeUpdater>(maximum, minimum, std::max(1, stepSize));
+			}
+			return std::make_shared<TimesStackSizeUpdater>(minimum, maximum, std::max(1, stepSize));
 		}
 		else
 			logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String());
@@ -441,17 +432,16 @@ VCMI_LIB_NAMESPACE_BEGIN
 std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonVector & ability_vec)
 {
 	auto b = std::make_shared<Bonus>();
-	std::string type = ability_vec[0].String();
-	auto it = bonusNameMap.find(type);
-	if (it == bonusNameMap.end())
-	{
-		logMod->error("Error: invalid ability type %s.", type);
-		return b;
-	}
-	b->type = it->second;
 
+	const JsonNode & typeNode = ability_vec[0];
+	const JsonNode & subtypeNode = ability_vec[2];
+
+	LIBRARY->identifiers()->requestIdentifier("bonus", typeNode, [b, subtypeNode](si32 bonusID)
+	{
+		b->type = static_cast<BonusType>(bonusID);
+		loadBonusSubtype(b->subtype, b->type, subtypeNode);
+	});
 	b->val = static_cast<si32>(ability_vec[1].Float());
-	loadBonusSubtype(b->subtype, b->type, ability_vec[2]);
 	b->additionalInfo = static_cast<si32>(ability_vec[3].Float());
 	b->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer)
 	b->turnsRemain = 0;
@@ -532,16 +522,10 @@ std::shared_ptr<const ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter
 
 				if (!parameters[0].isNull())
 				{
-					std::string anotherBonusType = parameters[0].String();
-					auto it = bonusNameMap.find(anotherBonusType);
-					if(it != bonusNameMap.end())
-					{
-						bonusLimiter->type = it->second;
-					}
-					else
+					LIBRARY->identifiers()->requestIdentifier("bonus", parameters[0], [bonusLimiter](si32 bonusID)
 					{
-						logMod->error("Error: invalid ability type %s.", anotherBonusType);
-					}
+						bonusLimiter->type = static_cast<BonusType>(bonusID);
+					});
 				}
 
 				auto findSource = [&](const JsonNode & parameter)
@@ -654,42 +638,23 @@ std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability, const Text
 bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b, const TextIdentifier & descriptionID)
 {
 	const JsonNode * value = nullptr;
+	const JsonNode & subtypeNode = ability["subtype"];
+	const JsonNode & addinfoNode = ability["addInfo"];
 
-	std::string type = ability["type"].String();
-	auto it = bonusNameMap.find(type);
-	auto params = std::make_unique<BonusParams>(false);
-	if (it == bonusNameMap.end())
+	LIBRARY->identifiers()->requestIdentifier("bonus", ability["type"], [b, subtypeNode, addinfoNode](si32 bonusID)
 	{
-		params = std::make_unique<BonusParams>(convertDeprecatedBonus(ability));
-		if(!params->isConverted)
-		{
-			logMod->error("Error: invalid ability type %s.", type);
-			return false;
-		}
-		b->type = params->type;
-		b->val = params->val.value_or(0);
-		b->valType = params->valueType.value_or(BonusValueType::ADDITIVE_VALUE);
-		if(params->targetType)
-			b->targetSourceType = params->targetType.value();
-	}
-	else
-		b->type = it->second;
-
-	loadBonusSubtype(b->subtype, b->type, params->isConverted ? params->toJson()["subtype"] : ability["subtype"]);
+		b->type = static_cast<BonusType>(bonusID);
+		loadBonusSubtype(b->subtype, b->type, subtypeNode);
+		loadBonusAddInfo(b->additionalInfo, b->type, addinfoNode);
+	});
 
-	if(!params->isConverted)
-	{
-		b->val = static_cast<si32>(ability["val"].Float());
+	b->val = static_cast<si32>(ability["val"].Float());
 
-		value = &ability["valueType"];
-		if (!value->isNull())
-			b->valType = static_cast<BonusValueType>(parseByMapN(bonusValueMap, value, "value type "));
-	}
+	value = &ability["valueType"];
+	if (!value->isNull())
+		b->valType = static_cast<BonusValueType>(parseByMapN(bonusValueMap, value, "value type "));
 
 	b->stacking = ability["stacking"].String();
-
-	loadBonusAddInfo(b->additionalInfo, b->type, ability);
-
 	b->turnsRemain = static_cast<si32>(ability["turns"].Float());
 
 	if(!ability["description"].isNull())
@@ -812,12 +777,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability)
 	value = &ability["type"];
 	if(value->isString())
 	{
-		auto it = bonusNameMap.find(value->String());
-		if(it != bonusNameMap.end())
-		{
-			type = it->second;
-			ret = ret.And(Selector::type()(it->second));
-		}
+		ret = ret.And(Selector::type()(static_cast<BonusType>(*LIBRARY->identifiers()->getIdentifier("bonus", value->String()))));
 	}
 	value = &ability["subtype"];
 	if(!value->isNull() && type != BonusType::NONE)

+ 1 - 4
lib/mapObjects/CArmedInstance.cpp

@@ -70,9 +70,6 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 	std::set<FactionID> factions;
 	bool hasUndead = false;
 
-	const std::string undeadCacheKey = "type_UNDEAD";
-	static const CSelector undeadSelector = Selector::type()(BonusType::UNDEAD);
-
 	for(const auto & slot : Slots())
 	{
 		const auto * creature  = slot.second->getCreatureID().toEntity(LIBRARY);
@@ -82,7 +79,7 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 		if (!hasUndead)
 		{
 			//this is costly check, let's skip it at first undead
-			hasUndead |= slot.second->hasBonus(undeadSelector, undeadCacheKey);
+			hasUndead |= slot.second->hasBonusOfType(BonusType::UNDEAD);
 		}
 	}
 

+ 66 - 68
lib/mapObjects/CGHeroInstance.cpp

@@ -981,88 +981,86 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned
  */
 CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &battleResult) const
 {
-	bool hasImprovedNecromancy = hasBonusOfType(BonusType::IMPROVED_NECROMANCY);
+	TConstBonusListPtr improvedNecromancy = getBonusesOfType(BonusType::IMPROVED_NECROMANCY);
 
 	// need skill or cloak of undead king - lesser artifacts don't work without skill
-	if (hasImprovedNecromancy)
-	{
-		double necromancySkill = valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE) / 100.0;
-		const ui8 necromancyLevel = valOfBonuses(BonusType::IMPROVED_NECROMANCY);
-		vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all...
-		const std::map<CreatureID,si32> &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)];
-		if(casualties.empty())
-			return CStackBasicDescriptor();
-		// figure out what to raise - pick strongest creature meeting requirements
-		CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode
-		int requiredCasualtyLevel = 1;
-		TConstBonusListPtr improvedNecromancy = getBonusesOfType(BonusType::IMPROVED_NECROMANCY);
-		if(!improvedNecromancy->empty())
+	if (improvedNecromancy->empty())
+		return CStackBasicDescriptor();
+
+	int raisedUnitsPercentage = std::clamp(valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE), 0, 100);
+	if (raisedUnitsPercentage == 0)
+		return CStackBasicDescriptor();
+
+	const std::map<CreatureID,si32> &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)];
+	if(casualties.empty())
+		return CStackBasicDescriptor();
+
+	// figure out what to raise - pick strongest creature meeting requirements
+	CreatureID bestCreature = CreatureID::NONE;
+	int necromancerPower = improvedNecromancy->totalValue();
+
+	// pick best bonus available
+	for(const std::shared_ptr<Bonus> & newPick : *improvedNecromancy)
+	{
+		// addInfo[0] = required necromancy skill
+		if(newPick->additionalInfo[0] > necromancerPower)
+			continue;
+
+		CreatureID newCreature = newPick->subtype.as<CreatureID>();;
+
+		if(!bestCreature.hasValue())
 		{
-			int maxCasualtyLevel = 1;
-			for(const auto & casualty : casualties)
-				vstd::amax(maxCasualtyLevel, LIBRARY->creatures()->getById(casualty.first)->getLevel());
-			// pick best bonus available
-			std::shared_ptr<Bonus> topPick;
-			for(const std::shared_ptr<Bonus> & newPick : *improvedNecromancy)
-			{
-				// addInfo[0] = required necromancy skill, addInfo[1] = required casualty level
-				if(newPick->additionalInfo[0] > necromancyLevel || newPick->additionalInfo[1] > maxCasualtyLevel)
-					continue;
-				if(!topPick)
-				{
-					topPick = newPick;
-				}
-				else
-				{
-					auto quality = [](const std::shared_ptr<Bonus> & pick) -> std::tuple<int, int, int>
-					{
-						const auto * c = pick->subtype.as<CreatureID>().toCreature();
-						return std::tuple<int, int, int> {c->getLevel(), static_cast<int>(c->getFullRecruitCost().marketValue()), -pick->additionalInfo[1]};
-					};
-					if(quality(topPick) < quality(newPick))
-						topPick = newPick;
-				}
-			}
-			if(topPick)
-			{
-				creatureTypeRaised = topPick->subtype.as<CreatureID>();
-				requiredCasualtyLevel = std::max(topPick->additionalInfo[1], 1);
-			}
+			bestCreature = newCreature;
 		}
-		assert(creatureTypeRaised != CreatureID::NONE);
-		// raise upgraded creature (at 2/3 rate) if no space available otherwise
-		if(getSlotFor(creatureTypeRaised) == SlotID())
+		else
 		{
-			for (const auto & slot : Slots())
+			auto quality = [](CreatureID pick) -> std::tuple<int, int>
 			{
-				if (creatureTypeRaised.toCreature()->isMyDirectOrIndirectUpgrade(slot.second->getCreature()))
-				{
-					creatureTypeRaised = slot.second->getCreatureID();
-					necromancySkill *= 2/3.0;
-					break;
-				}
-			}
+				const auto * c = pick.toCreature();
+				return std::tuple<int, int> {c->getLevel(), static_cast<int>(c->getFullRecruitCost().marketValue())};
+			};
+			if(quality(bestCreature) < quality(newCreature))
+				bestCreature = newCreature;
 		}
-		// calculate number of creatures raised - low level units contribute at 50% rate
-		const double raisedUnitHealth = creatureTypeRaised.toCreature()->getMaxHealth();
-		double raisedUnits = 0;
-		for(const auto & casualty : casualties)
+	}
+
+	assert(bestCreature != CreatureID::NONE);
+	CreatureID selectedCreature = bestCreature;
+
+	// raise upgraded creature (at 2/3 rate) if no space available otherwise
+	if(getSlotFor(selectedCreature) == SlotID())
+	{
+		for (const auto & slot : Slots())
 		{
-			const CCreature * c = casualty.first.toCreature();
-			double raisedFromCasualty = std::min(c->getMaxHealth() / raisedUnitHealth, 1.0) * casualty.second * necromancySkill;
-			if(c->getLevel() < requiredCasualtyLevel)
-				raisedFromCasualty *= 0.5;
-			raisedUnits += raisedFromCasualty;
+			if (selectedCreature.toCreature()->isMyDirectOrIndirectUpgrade(slot.second->getCreature()))
+			{
+				selectedCreature = slot.second->getCreatureID();
+				break;
+			}
 		}
-		return CStackBasicDescriptor(creatureTypeRaised, std::max(static_cast<int>(raisedUnits), 1));
 	}
 
-	return CStackBasicDescriptor();
+	// calculate number of creatures raised - low level units contribute at 50% rate
+	const double raisedUnitHealth = selectedCreature.toCreature()->getMaxHealth();
+	double raisedUnits = 0;
+	for(const auto & casualty : casualties)
+	{
+		const CCreature * c = casualty.first.toCreature();
+		double raisedFromCasualty = std::min(c->getMaxHealth() / raisedUnitHealth, 1.0) * casualty.second * raisedUnitsPercentage;
+
+		if (bestCreature != selectedCreature)
+			raisedUnits += raisedFromCasualty * 2 / 3 / 100;
+		else
+			raisedUnits += raisedFromCasualty / 100;
+	}
+
+	return CStackBasicDescriptor(selectedCreature, std::max(static_cast<int>(raisedUnits), 1));
 }
 
 int CGHeroInstance::getSightRadius() const
 {
-	return valOfBonuses(BonusType::SIGHT_RADIUS); // scouting gives SIGHT_RADIUS bonus
+	int baseValue = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_BASE_SCOUNTING_RANGE);
+	return applyBonuses(BonusType::SIGHT_RADIUS, baseValue);
 }
 
 si32 CGHeroInstance::manaRegain() const
@@ -1792,7 +1790,7 @@ bool CGHeroInstance::isMissionCritical() const
 
 void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance & stack) const
 {
-	TConstBonusListPtr lista = getBonusesOfType(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.getId()));
+	TConstBonusListPtr lista = stack.getBonusesOfType(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.getId()));
 	for(const auto & it : *lista)
 	{
 		auto nid = CreatureID(it->additionalInfo[0]);

+ 14 - 11
lib/mapObjects/CGTownInstance.cpp

@@ -12,6 +12,8 @@
 #include "CGTownInstance.h"
 
 #include "TownBuildingInstance.h"
+
+#include "../IGameSettings.h"
 #include "../spells/CSpellHandler.h"
 #include "../bonuses/Bonus.h"
 #include "../battle/IBattleInfoCallback.h"
@@ -42,17 +44,10 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-int CGTownInstance::getSightRadius() const //returns sight distance
+int CGTownInstance::getSightRadius() const
 {
-	auto ret = CBuilding::HEIGHT_NO_TOWER;
-
-	for(const auto & bid : builtBuildings)
-	{
-		auto height = getTown()->buildings.at(bid)->height;
-		if(ret < height)
-			ret = height;
-	}
-	return ret;
+	int baseValue = LIBRARY->engineSettings()->getInteger(EGameSettings::TOWNS_BASE_SCOUNTING_RANGE);
+	return applyBonuses(BonusType::SIGHT_RADIUS, baseValue);
 }
 
 void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
@@ -683,7 +678,15 @@ std::vector<TradeItemBuy> CGTownInstance::availableItemsIds(EMarketMode mode) co
 	}
 	else if ( mode == EMarketMode::RESOURCE_SKILL )
 	{
-		return cb->gameState().getMap().townUniversitySkills;
+		for (const auto & buildingID : builtBuildings)
+		{
+			const auto * buildingPtr = getTown()->buildings.at(buildingID).get();
+			if (vstd::contains(buildingPtr->marketModes, mode))
+				return buildingPtr->marketOffer;
+		}
+
+		logMod->warn("Town has resource-skill trade but has no skills to offer!");
+		return {};
 	}
 	else
 		return IMarket::availableItemsIds(mode);

+ 5 - 2
lib/mapping/CMap.h

@@ -272,7 +272,6 @@ public:
 	std::map<TeamID, ui8> obelisksVisited; //map: team_id => how many obelisks has been visited
 
 	std::vector<ArtifactID> townMerchantArtifacts;
-	std::vector<TradeItemBuy> townUniversitySkills;
 
 	void overrideGameSettings(const JsonNode & input);
 	void overrideGameSetting(EGameSettings option, const JsonNode & input);
@@ -345,7 +344,11 @@ public:
 		h & obeliskCount;
 		h & obelisksVisited;
 		h & townMerchantArtifacts;
-		h & townUniversitySkills;
+		if (!h.hasFeature(Handler::Version::UNIVERSITY_CONFIG))
+		{
+			std::vector<TradeItemBuy> townUniversitySkills;
+			h & townUniversitySkills;
+		}
 
 		h & instanceNames;
 		h & *gameSettings;

+ 2 - 0
lib/modding/ContentTypeHandler.cpp

@@ -39,6 +39,7 @@
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../rmg/CRmgTemplateStorage.h"
 #include "../spells/CSpellHandler.h"
+#include "../spells/SpellSchoolHandler.h"
 #include "../GameLibrary.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -248,6 +249,7 @@ void CContentHandler::init()
 	handlers.insert(std::make_pair("objects", ContentTypeHandler(LIBRARY->objtypeh.get(), "object")));
 	handlers.insert(std::make_pair("heroes", ContentTypeHandler(LIBRARY->heroh.get(), "hero")));
 	handlers.insert(std::make_pair("spells", ContentTypeHandler(LIBRARY->spellh.get(), "spell")));
+	handlers.insert(std::make_pair("spellSchools", ContentTypeHandler(LIBRARY->spellSchoolHandler.get(), "spellSchool")));
 	handlers.insert(std::make_pair("skills", ContentTypeHandler(LIBRARY->skillh.get(), "skill")));
 	handlers.insert(std::make_pair("templates", ContentTypeHandler(LIBRARY->tplh.get(), "template")));
 #if SCRIPTING_ENABLED

+ 3 - 21
lib/modding/IdentifierStorage.cpp

@@ -23,10 +23,6 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 CIdentifierStorage::CIdentifierStorage()
 {
-	//TODO: moddable spell schools
-	for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i)
-		registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id.getNum());
-
 	registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool::ANY.getNum());
 
 	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
@@ -87,27 +83,13 @@ CIdentifierStorage::CIdentifierStorage()
 	registerObject(ModScope::scopeBuiltin(), "spell", "spellbook_preset", SpellID::SPELLBOOK_PRESET);
 }
 
-void CIdentifierStorage::checkIdentifier(std::string & ID)
+void CIdentifierStorage::checkIdentifier(const std::string & ID)
 {
 	if (boost::algorithm::ends_with(ID, "."))
-		logMod->warn("BIG WARNING: identifier %s seems to be broken!", ID);
-	else
-	{
-		size_t pos = 0;
-		do
-		{
-			if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase
-			{
-				logMod->warn("Warning: identifier %s is not in camelCase!", ID);
-				ID[pos] = std::tolower(ID[pos]);// Try to fix the ID
-			}
-			pos = ID.find('.', pos);
-		}
-		while(pos++ != std::string::npos);
-	}
+		logMod->error("BIG WARNING: identifier %s seems to be broken!", ID);
 }
 
-void CIdentifierStorage::requestIdentifier(ObjectCallback callback) const
+void CIdentifierStorage::requestIdentifier(const ObjectCallback & callback) const
 {
 	checkIdentifier(callback.type);
 	checkIdentifier(callback.name);

+ 2 - 2
lib/modding/IdentifierStorage.h

@@ -67,9 +67,9 @@ class DLL_LINKAGE CIdentifierStorage
 	void debugDumpIdentifiers();
 
 	/// Check if identifier can be valid (camelCase, point as separator)
-	static void checkIdentifier(std::string & ID);
+	static void checkIdentifier(const std::string & ID);
 
-	void requestIdentifier(ObjectCallback callback) const;
+	void requestIdentifier(const ObjectCallback & callback) const;
 	bool resolveIdentifier(const ObjectCallback & callback) const;
 	std::vector<ObjectData> getPossibleIdentifiers(const ObjectCallback & callback) const;
 

+ 1 - 1
lib/networkPacks/PacksForClientBattle.h

@@ -488,7 +488,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient
 {
 	BattleID battleID = BattleID::NONE;
 	int stackID = 0;
-	int effect = 0; //use corresponding Bonus type
+	BonusType effect = BonusType::NONE;
 	int val = 0;
 	int additionalInfo = 0;
 

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -45,8 +45,9 @@ enum class ESerializationVersion : int32_t
 	CUSTOM_BONUS_ICONS, // support for custom icons in bonuses
 	SERVER_STATISTICS, // statistics now only saved on server
 	OPPOSITE_SIDE_LIMITER_OWNER, // opposite side limiter no longer stores owner in itself
+	UNIVERSITY_CONFIG, // town university is configurable
 
-	CURRENT = OPPOSITE_SIDE_LIMITER_OWNER,
+	CURRENT = UNIVERSITY_CONFIG,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");

+ 26 - 83
lib/spells/CSpellHandler.cpp

@@ -21,56 +21,23 @@
 
 #include "../constants/StringConstants.h"
 
-#include "../battle/BattleInfo.h"
+#include "../CBonusTypeHandler.h"
 #include "../battle/CBattleInfoCallback.h"
 #include "../battle/Unit.h"
 #include "../json/JsonBonus.h"
 #include "../json/JsonUtils.h"
 #include "../GameLibrary.h"
-#include "../mapObjects/CGHeroInstance.h" //todo: remove
 #include "../modding/IdentifierStorage.h"
-#include "../modding/ModUtility.h"
-#include "../serializer/CSerializer.h"
 #include "../texts/CLegacyConfigParser.h"
 #include "../texts/CGeneralTextHandler.h"
 
 #include "ISpellMechanics.h"
+#include "bonuses/BonusSelector.h"
+#include "spells/SpellSchoolHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-namespace SpellConfig
-{
-static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"};
-
-const spells::SchoolInfo SCHOOL[4] =
-{
-	{
-		SpellSchool::AIR,
-		"air"
-	},
-	{
-		SpellSchool::FIRE,
-		"fire"
-	},
-	{
-		SpellSchool::WATER,
-		"water"
-	},
-	{
-		SpellSchool::EARTH,
-		"earth"
-	}
-};
-
-//order as described in http://bugs.vcmi.eu/view.php?id=91
-static const SpellSchool SCHOOL_ORDER[4] =
-{
-	SpellSchool::AIR,  //=0
-	SpellSchool::FIRE, //=1
-	SpellSchool::EARTH,//=3(!)
-	SpellSchool::WATER //=2(!)
-};
-} //namespace SpellConfig
+static constexpr std::array LEVEL_NAMES = {"none", "basic", "advanced", "expert"};
 
 ///CSpell
 CSpell::CSpell():
@@ -132,7 +99,7 @@ int64_t CSpell::calculateDamage(const spells::Caster * caster) const
 
 bool CSpell::hasSchool(SpellSchool which) const
 {
-	return school.count(which) && school.at(which);
+	return schools.count(which);
 }
 
 bool CSpell::canBeCast(const CBattleInfoCallback * cb, spells::Mode mode, const spells::Caster * caster) const
@@ -158,13 +125,11 @@ spells::AimType CSpell::getTargetType() const
 void CSpell::forEachSchool(const std::function<void(const SpellSchool &, bool &)>& cb) const
 {
 	bool stop = false;
-	for(auto iter : SpellConfig::SCHOOL_ORDER)
+	for(auto schoolID : LIBRARY->spellSchoolHandler->getAllObjects())
 	{
-		const spells::SchoolInfo & cnf = SpellConfig::SCHOOL[iter.getNum()];
-		if(school.at(cnf.id))
+		if(schools.count(schoolID))
 		{
-			cb(cnf.id, stop);
-
+			cb(schoolID, stop);
 			if(stop)
 				break;
 		}
@@ -189,8 +154,8 @@ std::string CSpell::getNameTranslated() const
 
 std::string CSpell::getDescriptionTextID(int32_t level) const
 {
-	TextIdentifier id("spell", modScope, identifier, "description", SpellConfig::LEVEL_NAMES[level]);
-	return id.get();
+	TextIdentifier textID("spell", modScope, identifier, "description", LEVEL_NAMES[level]);
+	return textID.get();
 }
 
 std::string CSpell::getDescriptionTranslated(int32_t level) const
@@ -473,26 +438,14 @@ JsonNode CSpell::convertTargetCondition(const BTVector & immunity, const BTVecto
 	static const std::string CONDITION_NORMAL = "normal";
 	static const std::string CONDITION_ABSOLUTE = "absolute";
 
-#define BONUS_NAME(x) { BonusType::x, #x },
-	static const std::map<BonusType, std::string> bonusNameRMap = { BONUS_LIST };
-#undef BONUS_NAME
-
 	JsonNode res;
 
 	auto convertVector = [&](const std::string & targetName, const BTVector & source, const std::string & value)
 	{
 		for(auto bonusType : source)
 		{
-			auto iter = bonusNameRMap.find(bonusType);
-			if(iter != bonusNameRMap.end())
-			{
-				auto fullId = ModUtility::makeFullIdentifier("", "bonus", iter->second);
-				res[targetName][fullId].String() = value;
-			}
-			else
-			{
-				logGlobal->error("Invalid bonus type %d", static_cast<int32_t>(bonusType));
-			}
+			std::string bonusName = LIBRARY->bth->bonusToString(bonusType);
+			res[targetName][bonusName].String() = value;
 		}
 	};
 
@@ -592,7 +545,6 @@ bool DLL_LINKAGE isInScreenRange(const int3 & center, const int3 & pos)
 ///CSpellHandler
 std::vector<JsonNode> CSpellHandler::loadLegacyData()
 {
-	using namespace SpellConfig;
 	std::vector<JsonNode> legacyData;
 
 	CLegacyConfigParser parser(TextPath::builtin("DATA/SPTRAITS.TXT"));
@@ -768,8 +720,6 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 	assert(identifier.find(':') == std::string::npos);
 	assert(!scope.empty());
 
-	using namespace SpellConfig;
-
 	SpellID id(static_cast<si32>(index));
 
 	auto spell = std::make_shared<CSpell>();
@@ -794,11 +744,15 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 
 	logMod->trace("%s: loading spell %s", __FUNCTION__, spell->getNameTranslated());
 
-	const auto schoolNames = json["school"];
-
-	for(const spells::SchoolInfo & info : SpellConfig::SCHOOL)
+	for(const auto & schoolJson : json["school"].Struct())
 	{
-		spell->school[info.id] = schoolNames[info.jsonName].Bool();
+		if (schoolJson.second.Bool())
+		{
+			LIBRARY->identifiers()->requestIdentifier(schoolJson.second.getModScope(), "spellSchool", schoolJson.first, [spell](si32 schoolID)
+			{
+				spell->schools.emplace(schoolID);
+			});
+		}
 	}
 
 	spell->castOnSelf = json["canCastOnSelf"].Bool();
@@ -886,28 +840,17 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 
 	spell->onlyOnWaterMap = json["onlyOnWaterMap"].Bool();
 
-	auto findBonus = [&](const std::string & name, std::vector<BonusType> & vec)
-	{
-		auto it = bonusNameMap.find(name);
-		if(it == bonusNameMap.end())
-		{
-			logMod->error("Spell %s: invalid bonus name %s", spell->getNameTranslated(), name);
-		}
-		else
-		{
-			vec.push_back(static_cast<BonusType>(it->second));
-		}
-	};
-
 	auto readBonusStruct = [&](const std::string & name, std::vector<BonusType> & vec)
 	{
 		for(auto bonusData: json[name].Struct())
 		{
-			const std::string bonusId = bonusData.first;
-			const bool flag = bonusData.second.Bool();
+			if(!bonusData.second.Bool())
+				continue;
 
-			if(flag)
-				findBonus(bonusId, vec);
+			LIBRARY->identifiers()->requestIdentifier(bonusData.second.getModScope(), "bonus", bonusData.first, [&vec](si32 bonusID)
+			{
+				vec.push_back(static_cast<BonusType>(bonusID));
+			});
 		}
 	};
 

+ 3 - 16
lib/spells/CSpellHandler.h

@@ -35,21 +35,8 @@ namespace test
 
 namespace spells
 {
-
-class ISpellMechanicsFactory;
-class IBattleCast;
-
-struct SchoolInfo
-{
-	SpellSchool id; //backlink
-	std::string jsonName;
-};
-
-}
-
-namespace SpellConfig
-{
-	extern const spells::SchoolInfo SCHOOL[4];
+	class ISpellMechanicsFactory;
+	class IBattleCast;
 }
 
 enum class VerticalPosition : ui8{TOP, CENTER, BOTTOM};
@@ -148,7 +135,7 @@ public:
 	using BTVector = std::vector<BonusType>;
 
 
-	std::map<SpellSchool, bool> school;
+	std::set<SpellSchool> schools;
 	std::map<FactionID, si32> probabilities; //% chance to gain for castles
 
 	bool onlyOnWaterMap; //Spell will be banned on maps without water

+ 60 - 0
lib/spells/SpellSchoolHandler.cpp

@@ -0,0 +1,60 @@
+/*
+ * SpellSchoolHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "SpellSchoolHandler.h"
+
+#include "../json/JsonNode.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+std::vector<JsonNode> SpellSchoolHandler::loadLegacyData()
+{
+	objects.resize(4);
+
+	return std::vector<JsonNode>(4, JsonNode(JsonMap()));
+}
+
+std::shared_ptr<spells::SpellSchoolType> SpellSchoolHandler::loadObjectImpl(std::string scope, std::string name, const JsonNode & data, size_t index)
+{
+	auto ret = std::make_shared<spells::SpellSchoolType>();
+
+	ret->id = SpellSchool(index);
+	ret->jsonName = name;
+	ret->spellBordersPath = AnimationPath::fromJson(data["schoolBorders"]);
+
+	return ret;
+}
+
+/// loads single object into game. Scope is namespace of this object, same as name of source mod
+void SpellSchoolHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
+{
+	objects.push_back(loadObjectImpl(scope, name, data, objects.size()));
+	registerObject(scope, "spellSchool", name, objects.back()->getIndex());
+}
+
+void SpellSchoolHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
+{
+	assert(objects[index] == nullptr); // ensure that this id was not loaded before
+	objects[index] = loadObjectImpl(scope, name, data, index);
+	registerObject(scope, "spellSchool", name, objects[index]->getIndex());
+}
+
+std::vector<SpellSchool> SpellSchoolHandler::getAllObjects() const
+{
+	std::vector<SpellSchool> result;
+
+	for (const auto & school : objects)
+		result.push_back(school->id);
+
+	return result;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 72 - 0
lib/spells/SpellSchoolHandler.h

@@ -0,0 +1,72 @@
+/*
+ * SpellSchoolHandler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+#include "../constants/EntityIdentifiers.h"
+#include "../IHandlerBase.h"
+#include "../filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class SpellSchoolHandler;
+
+namespace spells
+{
+
+class DLL_LINKAGE SpellSchoolType
+{
+	friend class VCMI_LIB_WRAP_NAMESPACE(SpellSchoolHandler);
+
+	SpellSchool id; //backlink
+	std::string jsonName;
+	AnimationPath spellBordersPath;
+
+public:
+	std::string getJsonKey() const
+	{
+		return jsonName;
+	}
+
+	AnimationPath getSpellBordersPath() const
+	{
+		return spellBordersPath;
+	}
+
+	int getIndex() const
+	{
+		return id.getNum();
+	}
+};
+
+}
+
+class DLL_LINKAGE SpellSchoolHandler : public IHandlerBase
+{
+	std::shared_ptr<spells::SpellSchoolType> loadObjectImpl(std::string scope, std::string name, const JsonNode & data, size_t index);
+public:
+	std::vector<JsonNode> loadLegacyData() override;
+
+	/// loads single object into game. Scope is namespace of this object, same as name of source mod
+	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;
+
+	std::vector<SpellSchool> getAllObjects() const;
+
+	const spells::SpellSchoolType * getById(SpellSchool index) const
+	{
+		return objects.at(index).get();
+	}
+
+private:
+	std::vector<std::shared_ptr<spells::SpellSchoolType>> objects;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 5 - 16
lib/spells/TargetCondition.cpp

@@ -15,7 +15,6 @@
 #include "../CBonusTypeHandler.h"
 #include "../battle/CBattleInfoCallback.h"
 #include "../battle/Unit.h"
-#include "../bonuses/BonusParams.h"
 #include "../bonuses/BonusList.h"
 #include "../json/JsonBonus.h"
 #include "../modding/IdentifierStorage.h"
@@ -356,21 +355,11 @@ public:
 	{
 		if(type == "bonus")
 		{
-			//TODO: support custom bonus types
-
-			auto it = bonusNameMap.find(identifier);
-			if(it != bonusNameMap.end())
-				return std::make_shared<SelectorCondition>(Selector::type()(it->second));
-
-			auto params = BonusParams(identifier, "", -1);
-			if(params.isConverted)
-			{
-				if(params.val)
-					return std::make_shared<SelectorCondition>(params.toSelector(), *params.val, *params.val);
-				return std::make_shared<SelectorCondition>(params.toSelector());
-			}
-
-				logMod->error("Invalid bonus type %s in spell target condition.", identifier);
+			std::optional bonusID(LIBRARY->identifiers()->getIdentifier(scope, "bonus", identifier, true));
+			if (bonusID)
+				return std::make_shared<SelectorCondition>(Selector::type()(static_cast<BonusType>(*bonusID)));
+			else
+				logMod->error("Invalid bonus %s type in spell target condition.", identifier);
 		}
 		else if(type == "creature")
 		{

+ 9 - 5
mapeditor/inspector/rewardswidget.cpp

@@ -13,10 +13,13 @@
 #include "../lib/GameLibrary.h"
 #include "../lib/CSkillHandler.h"
 #include "../lib/spells/CSpellHandler.h"
+#include "../lib/CBonusTypeHandler.h"
 #include "../lib/CCreatureHandler.h"
 #include "../lib/constants/StringConstants.h"
 #include "../lib/entities/artifact/CArtifact.h"
 #include "../lib/mapping/CMap.h"
+#include "../lib/modding/IdentifierStorage.h"
+#include "../lib/modding/ModScope.h"
 #include "../lib/rewardable/Configuration.h"
 #include "../lib/rewardable/Limiter.h"
 #include "../lib/rewardable/Reward.h"
@@ -183,8 +186,8 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) :
 	//fill bonuses
 	for(auto & s : bonusDurationMap)
 		ui->bonusDuration->addItem(QString::fromStdString(s.first));
-	for(auto & s : bonusNameMap)
-		ui->bonusType->addItem(QString::fromStdString(s.first));
+	for(auto & s : LIBRARY->bth->getAllObjets())
+		ui->bonusType->addItem(QString::fromStdString(LIBRARY->bth->bonusToString(s)));
 	
 	//set default values
 	if(dynamic_cast<CGPandoraBox*>(&object))
@@ -340,7 +343,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index)
 	for(int i = 0; i < ui->bonuses->rowCount(); ++i)
 	{
 		auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString());
-		auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString());
+		auto typ = static_cast<BonusType>(*LIBRARY->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "bonus", ui->bonuses->item(i, 1)->text().toStdString()));
 		auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt();
 		vinfo.reward.heroBonuses.emplace_back(dur, typ, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(object.id));
 	}
@@ -490,7 +493,7 @@ void RewardsWidget::loadCurrentVisitInfo(int index)
 			}
 		}
 		
-		auto typ = vstd::findKey(bonusNameMap, i.type);
+		std::string typ = LIBRARY->bth->bonusToString(i.type);
 		for(int i = 0; i < ui->bonusType->count(); ++i)
 		{
 			if(ui->bonusType->itemText(i) == QString::fromStdString(typ))
@@ -816,7 +819,8 @@ void RewardsDelegate::updateModelData(QAbstractItemModel * model, const QModelIn
 		QStringList bonusesList;
 		for (auto & bonus : vinfo.reward.heroBonuses)
 		{
-			bonusesList += QString("%1 %2 (%3)").arg(QString::fromStdString(vstd::findKey(bonusDurationMap, bonus.duration))).arg(QString::fromStdString(vstd::findKey(bonusNameMap, bonus.type))).arg(bonus.val);
+			std::string bonusName = LIBRARY->bth->bonusToString(bonus.type);
+			bonusesList += QString("%1 %2 (%3)").arg(QString::fromStdString(vstd::findKey(bonusDurationMap, bonus.duration))).arg(QString::fromStdString(bonusName)).arg(bonus.val);
 		}
 		textList += QObject::tr("Bonuses: %1").arg(bonusesList.join(", "));
 	}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff