Quellcode durchsuchen

Merge pull request #1518 from rilian-la-te/arcane-intuition

Skills rework: separate bonus for each skill - part 1
Ivan Savenko vor 2 Jahren
Ursprung
Commit
17520b70ce
76 geänderte Dateien mit 2131 neuen und 1275 gelöschten Zeilen
  1. 12 6
      AI/Nullkiller/Analyzers/HeroManager.cpp
  2. 1 1
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  3. 6 12
      Mods/vcmi/config/vcmi/english.json
  4. 4 6
      Mods/vcmi/config/vcmi/german.json
  5. 4 6
      Mods/vcmi/config/vcmi/polish.json
  6. 6 12
      Mods/vcmi/config/vcmi/russian.json
  7. 6 12
      Mods/vcmi/config/vcmi/ukrainian.json
  8. 0 9
      client/battle/BattleEffectsController.cpp
  9. 1 5
      client/battle/BattleSiegeController.cpp
  10. 0 7
      client/widgets/MiscWidgets.cpp
  11. 0 17
      client/windows/CCreatureWindow.cpp
  12. 6 1
      client/windows/CHeroWindow.cpp
  13. 2 0
      client/windows/CHeroWindow.h
  14. 0 1
      client/windows/CKingdomInterface.cpp
  15. 82 35
      config/artifacts.json
  16. 11 27
      config/bonuses.json
  17. 6 2
      config/creatures/dungeon.json
  18. 8 4
      config/creatures/inferno.json
  19. 13 9
      config/creatures/necropolis.json
  20. 5 2
      config/creatures/neutral.json
  21. 16 1
      config/creatures/special.json
  22. 14 1
      config/creatures/stronghold.json
  23. 52 1
      config/defaultMods.json
  24. 2 2
      config/factions/necropolis.json
  25. 2 1
      config/gameConfig.json
  26. 34 18
      config/heroes/castle.json
  27. 14 12
      config/heroes/dungeon.json
  28. 25 21
      config/heroes/fortress.json
  29. 9 9
      config/heroes/inferno.json
  30. 13 12
      config/heroes/necropolis.json
  31. 22 18
      config/heroes/rampart.json
  32. 18 15
      config/heroes/stronghold.json
  33. 11 9
      config/heroes/tower.json
  34. 2 2
      config/objects/rewardableBonusing.json
  35. 28 0
      config/schemas/bonus.json
  36. 15 45
      config/schemas/hero.json
  37. 8 0
      config/schemas/skill.json
  38. 84 68
      config/skills.json
  39. 1 48
      config/spells/ability.json
  40. 3 1
      config/spells/other.json
  41. 236 0
      config/spells/vcmiAbility.json
  42. 10 8
      lib/CBonusTypeHandler.cpp
  43. 13 8
      lib/CCreatureHandler.cpp
  44. 1 14
      lib/CCreatureSet.cpp
  45. 14 1
      lib/CGameState.cpp
  46. 1 0
      lib/CGameState.h
  47. 29 133
      lib/CHeroHandler.cpp
  48. 0 37
      lib/CHeroHandler.h
  49. 5 0
      lib/CModHandler.cpp
  50. 2 0
      lib/CModHandler.h
  51. 31 6
      lib/CPathfinder.cpp
  52. 3 1
      lib/CPathfinder.h
  53. 6 4
      lib/CSkillHandler.cpp
  54. 13 1
      lib/CSkillHandler.h
  55. 1 1
      lib/CTownHandler.cpp
  56. 411 122
      lib/HeroBonus.cpp
  57. 94 33
      lib/HeroBonus.h
  58. 251 31
      lib/JsonNode.cpp
  59. 2 0
      lib/JsonNode.h
  60. 35 10
      lib/battle/BattleInfo.cpp
  61. 16 16
      lib/battle/CBattleInfoCallback.cpp
  62. 1 0
      lib/battle/CBattleInfoCallback.h
  63. 18 29
      lib/battle/DamageCalculator.cpp
  64. 87 67
      lib/mapObjects/CGHeroInstance.cpp
  65. 5 1
      lib/mapObjects/CGHeroInstance.h
  66. 1 8
      lib/mapObjects/CGTownInstance.cpp
  67. 3 2
      lib/mapObjects/MiscObjects.cpp
  68. 1 0
      lib/registerTypes/RegisterTypes.h
  69. 2 2
      lib/serializer/CSerializer.h
  70. 72 18
      lib/spells/TargetCondition.cpp
  71. 1 0
      lib/spells/TargetCondition.h
  72. 141 29
      lib/spells/effects/Catapult.cpp
  73. 17 0
      lib/spells/effects/Catapult.h
  74. 11 0
      lib/spells/effects/Heal.cpp
  75. 0 8
      lib/spells/effects/Timed.cpp
  76. 51 227
      server/CGameHandler.cpp

+ 12 - 6
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -71,17 +71,23 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
 	auto heroSpecial = Selector::source(Bonus::HERO_SPECIAL, hero->type->getIndex());
-	auto secondarySkillBonus = Selector::type()(Bonus::SECONDARY_SKILL_PREMY);
+	auto secondarySkillBonus = Selector::targetSourceType()(Bonus::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
+	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(Bonus::SECONDARY_SKILL));
 	float specialityScore = 0.0f;
 
-	for(auto bonus : *specialSecondarySkillBonuses)
+	for(auto bonus : *secondarySkillBonuses)
 	{
-		SecondarySkill bonusSkill = SecondarySkill(bonus->subtype);
-		float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
+		auto hasBonus = !!specialSecondarySkillBonuses->getFirst(Selector::typeSubtype(bonus->type, bonus->subtype));
 
-		if(bonusScore > 0)
-			specialityScore += bonusScore * bonusScore * bonusScore;
+		if(hasBonus)
+		{
+			SecondarySkill bonusSkill = SecondarySkill(bonus->sid);
+			float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
+
+			if(bonusScore > 0)
+				specialityScore += bonusScore * bonusScore * bonusScore;
+		}
 	}
 
 	return specialityScore;

+ 1 - 1
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -209,7 +209,7 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
 		return 1500;
 
 	auto statsValue =
-		10 * art->valOfBonuses(Bonus::LAND_MOVEMENT)
+		10 * art->valOfBonuses(Bonus::MOVEMENT, 1)
 		+ 1200 * art->valOfBonuses(Bonus::STACKS_SPEED)
 		+ 700 * art->valOfBonuses(Bonus::MORALE)
 		+ 700 * art->getAttack(false)

+ 6 - 12
Mods/vcmi/config/vcmi/english.json

@@ -200,8 +200,6 @@
 	"core.bonus.FLYING.description": "Can Fly (ignores obstacles)",
 	"core.bonus.FREE_SHOOTING.name": "Shoot Close",
 	"core.bonus.FREE_SHOOTING.description": "Can shoot in Close Combat",
-	"core.bonus.FULL_HP_REGENERATION.name": "Regeneration",
-	"core.bonus.FULL_HP_REGENERATION.description": "May regenerate to full health",
 	"core.bonus.GARGOYLE.name": "Gargoyle",
 	"core.bonus.GARGOYLE.description": "Cannot be rised or healed",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)",
@@ -211,15 +209,11 @@
 	"core.bonus.HEALER.name": "Healer",
 	"core.bonus.HEALER.description": "Heals allied units",
 	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heals ${val} hit points every round",
+	"core.bonus.HP_REGENERATION.description": "Heals ${SHval} hit points every round",
 	"core.bonus.JOUSTING.name": "Champion Charge",
-	"core.bonus.JOUSTING.description": "+5% damage per hex travelled",
-	"core.bonus.KING1.name": "King 1",
-	"core.bonus.KING1.description": "Vulnerable to basic SLAYER",
-	"core.bonus.KING2.name": "King 2",
-	"core.bonus.KING2.description": "Vulnerable to advanced SLAYER",
-	"core.bonus.KING3.name": "King 3",
-	"core.bonus.KING3.description":"Vulnerable to expert SLAYER",
+	"core.bonus.JOUSTING.description": "+${val}% damage per hex travelled",
+	"core.bonus.KING.name": "King",
+	"core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range",
@@ -232,8 +226,8 @@
 	"core.bonus.MANA_DRAIN.description": "Drains ${val} mana every turn",
 	"core.bonus.MAGIC_MIRROR.name": "Magic Mirror (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "${val}% chance to redirects an offensive spell to enemy",
-	"core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance(${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "${MR}% chance to resist enemy spell",
+	"core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance(${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% chance to resist enemy spell",
 	"core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity",
 	"core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty",

+ 4 - 6
Mods/vcmi/config/vcmi/german.json

@@ -198,8 +198,6 @@
 	"core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)",
 	"core.bonus.FREE_SHOOTING.name": "Nah schießen",
 	"core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen",
-	"core.bonus.FULL_HP_REGENERATION.name": "Regeneration",
-	"core.bonus.FULL_HP_REGENERATION.description": "Kann auf volle Lebenspunkte regenerieren",
 	"core.bonus.GARGOYLE.name": "Gargoyle",
 	"core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)",
@@ -209,9 +207,9 @@
 	"core.bonus.HEALER.name": "Heiler",
 	"core.bonus.HEALER.description": "Heilt verbündete Einheiten",
 	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde",
+	"core.bonus.HP_REGENERATION.description": "Heilt ${SHval} Trefferpunkte jede Runde",
 	"core.bonus.JOUSTING.name": "Champion Charge",
-	"core.bonus.JOUSTING.description": "+5% Schaden pro zurückgelegtem Feld",
+	"core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld",
 	"core.bonus.KING1.name": "König 1",
 	"core.bonus.KING1.description": "Anfällig für grundlegende SLAYER",
 	"core.bonus.KING2.name": "König 2",
@@ -230,8 +228,8 @@
 	"core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde",
 	"core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken",
-	"core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "${MR}% Chance, gegnerischem Zauber zu widerstehen",
+	"core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen",
 	"core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität",
 	"core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe",

+ 4 - 6
Mods/vcmi/config/vcmi/polish.json

@@ -128,8 +128,6 @@
 	"core.bonus.FLYING.description": "Może latać (ignoruje przeszkody)",
 	"core.bonus.FREE_SHOOTING.name": "Bliski Strzał",
 	"core.bonus.FREE_SHOOTING.description": "Może strzelać w zasięgu walki wręcz",
-	"core.bonus.FULL_HP_REGENERATION.name": "Regeneracja",
-	"core.bonus.FULL_HP_REGENERATION.description": "Może zregenerować się do pełni zdrowia",
 	"core.bonus.GARGOYLE.name": "Gargulec",
 	"core.bonus.GARGOYLE.description": "Nie może być wskrzeszony lub uleczony",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Zmniejsz obrażenia (${val}%)",
@@ -139,9 +137,9 @@
 	"core.bonus.HEALER.name": "Uzdrowiciel",
 	"core.bonus.HEALER.description": "Leczy sprzymierzone jednostki",
 	"core.bonus.HP_REGENERATION.name": "Regeneracja",
-	"core.bonus.HP_REGENERATION.description": "Leczy ${val} punktów zdrowia każdej rundy",
+	"core.bonus.HP_REGENERATION.description": "Leczy ${SHval} punktów zdrowia każdej rundy",
 	"core.bonus.JOUSTING.name": "Szarża Czempiona",
-	"core.bonus.JOUSTING.description": "+5% obrażeń na przebytego heksa",
+	"core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa",
 	"core.bonus.KING1.name": "Król 1",
 	"core.bonus.KING1.description": "Wrażliwy na podstawowy czar POGROMCA",
 	"core.bonus.KING2.name": "Król 2",
@@ -160,8 +158,8 @@
 	"core.bonus.MANA_DRAIN.description": "Wysysa ${val} many każdej tury",
 	"core.bonus.MAGIC_MIRROR.name": "Magiczne Zwierciadło (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "${val}% szans na odbicie ofensywnego czaru do wroga",
-	"core.bonus.MAGIC_RESISTANCE.name": "Odporność na Magię(${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "${MR}% szans na przeciwstawienie się wrogiemu czarowi",
+	"core.bonus.MAGIC_RESISTANCE.name": "Odporność na Magię(${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% szans na przeciwstawienie się wrogiemu czarowi",
 	"core.bonus.MIND_IMMUNITY.name": "Odporność na czasy umysłu",
 	"core.bonus.MIND_IMMUNITY.description": "Odporny na czary typu umysłu",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "Brak ograniczeń za odległość",

+ 6 - 12
Mods/vcmi/config/vcmi/russian.json

@@ -213,8 +213,6 @@
 	"core.bonus.FLYING.description": "Игнорирует препятствия на поле боя",
 	"core.bonus.FREE_SHOOTING.name": "Стреляет вблизи",
 	"core.bonus.FREE_SHOOTING.description": "Может стрелять в ближнем бою",
-	"core.bonus.FULL_HP_REGENERATION.name": "Регенерация",
-	"core.bonus.FULL_HP_REGENERATION.description": "Восстанавливает полное здоровье в начале своего хода",
 	"core.bonus.GARGOYLE.name": "Бескровный",
 	"core.bonus.GARGOYLE.description": "Не может быть исцелен и воскрешен",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Уменьшение урона (${val}%)",
@@ -224,15 +222,11 @@
 	"core.bonus.HEALER.name": "Целитель",
 	"core.bonus.HEALER.description": "Исцеляет дружественные юниты",
 	"core.bonus.HP_REGENERATION.name": "Регенерация",
-	"core.bonus.HP_REGENERATION.description": "Исцеляет ${val} очков здоровья каждый ход",
+	"core.bonus.HP_REGENERATION.description": "Исцеляет ${SHval} очков здоровья каждый ход",
 	"core.bonus.JOUSTING.name": "Разгон",
-	"core.bonus.JOUSTING.description": "+5% урона за каждую пройденную клетку",
-	"core.bonus.KING1.name": "Король 1",
-	"core.bonus.KING1.description": "Уязвимость к заклинанию Палач 1 ступени",
-	"core.bonus.KING2.name": "Король 2",
-	"core.bonus.KING2.description": "Уязвимость к заклинанию Палач 2 ступени",
-	"core.bonus.KING3.name": "Король 3",
-	"core.bonus.KING3.description":"Уязвимость к заклинанию Палач 3 ступени",
+	"core.bonus.JOUSTING.description": "+${val}% урона за каждую пройденную клетку",
+	"core.bonus.KING.name": "Король",
+	"core.bonus.KING.description": "Уязвимость к заклинанию Палач ${val} ступени и выше",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Иммунитет к заклинаниям 1-${val}",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Иммунитет к заклинаниям до ${val} уровня",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ограниченный радиуст стрельбы",
@@ -245,8 +239,8 @@
 	"core.bonus.MANA_DRAIN.description": "Высасывает ${val} маны каждый ход",
 	"core.bonus.MAGIC_MIRROR.name": "Волшебное зеркало (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "Шанс ${val}% отразить атакующие заклинание в противника",
-	"core.bonus.MAGIC_RESISTANCE.name": "Защита от магии (${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "Шанс ${MR}% полностью проигнорировать заклинание",
+	"core.bonus.MAGIC_RESISTANCE.name": "Защита от магии (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "Шанс ${val}% полностью проигнорировать заклинание",
 	"core.bonus.MIND_IMMUNITY.name": "Железная воля",
 	"core.bonus.MIND_IMMUNITY.description": "Иммунитет к заклинаниям, влияющим на разум",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "Игнорирует расстояние",

+ 6 - 12
Mods/vcmi/config/vcmi/ukrainian.json

@@ -188,8 +188,6 @@
 	"core.bonus.FLYING.description" : "Може літати (ігнорує перешкоди)",
 	"core.bonus.FREE_SHOOTING.name" : "Стріляє впритул",
 	"core.bonus.FREE_SHOOTING.description" : "Може стріляти в ближньому бою",
-	"core.bonus.FULL_HP_REGENERATION.name" : "Регенерація",
-	"core.bonus.FULL_HP_REGENERATION.description" : "Може регенерувати до повного здоров'я",
 	"core.bonus.GARGOYLE.name" : "Горгулья",
 	"core.bonus.GARGOYLE.description" : "Не може бути відроджена або зцілена",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Зменшує шкоду (${val}%)",
@@ -199,15 +197,11 @@
 	"core.bonus.HEALER.name" : "Цілитель",
 	"core.bonus.HEALER.description" : "Лікує союзників",
 	"core.bonus.HP_REGENERATION.name" : "Регенерація",
-	"core.bonus.HP_REGENERATION.description" : "Відновлює ${val} очок здоров'я кожного раунду",
+	"core.bonus.HP_REGENERATION.description" : "Відновлює ${SHval} очок здоров'я кожного раунду",
 	"core.bonus.JOUSTING.name" : "Турнірна перевага",
-	"core.bonus.JOUSTING.description" : "+5% шкоди за кожен пройдений гекс",
-	"core.bonus.KING1.name" : "Король 1",
-	"core.bonus.KING1.description" : "Вразливий до 1-го рівня закляття Вбивця",
-	"core.bonus.KING2.name" : "Король 2",
-	"core.bonus.KING2.description" : "Вразливий до 2-го рівня закляття Вбивця",
-	"core.bonus.KING3.name" : "Король 3",
-	"core.bonus.KING3.description" : "Вразливий до 3-го рівня закляття Вбивця",
+	"core.bonus.JOUSTING.description" : "+${val}% шкоди за кожен пройдений гекс",
+	"core.bonus.KING.name" : "Король",
+	"core.bonus.KING.description" : "Вразливий до ${val}-го рівня закляття Вбивця",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Імунітет до заклять 1-${val}",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Імунітет до заклять рівнів 1-${val}",
 	"core.bonus.LIFE_DRAIN.name" : "Висмоктує життя (${val}%)",
@@ -218,8 +212,8 @@
 	"core.bonus.MANA_DRAIN.description" : "Викрадає ${val} мани кожного ходу",
 	"core.bonus.MAGIC_MIRROR.name" : "Магічне дзеркало (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description" : "Відбиває ворожі заклинання до випадкової істоти ворога з силою в ${val}%",
-	"core.bonus.MAGIC_RESISTANCE.name" : "Опір магії (${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description" : "${MR}% шанс протистояти ворожим закляттям",
+	"core.bonus.MAGIC_RESISTANCE.name" : "Опір магії (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description" : "${val}% шанс протистояти ворожим закляттям",
 	"core.bonus.MIND_IMMUNITY.name" : "Імунітет до заклять розуму",
 	"core.bonus.MIND_IMMUNITY.description" : "Імунітет до заклять типу ",
 	"core.bonus.NO_DISTANCE_PENALTY.name" : "Немає штрафу за відстань",

+ 0 - 9
client/battle/BattleEffectsController.cpp

@@ -110,15 +110,6 @@ void BattleEffectsController::startAction(const BattleAction* action)
 		break;
 	}
 
-	//displaying special abilities
-	auto actionTarget = action->getTarget(owner.curInt->cb.get());
-	switch(action->actionType)
-	{
-		case EActionType::STACK_HEAL:
-			displayEffect(EBattleEffect::REGENERATION, "REGENER", actionTarget.at(0).hexValue);
-			break;
-	}
-
 	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 

+ 1 - 5
client/battle/BattleSiegeController.cpp

@@ -325,11 +325,7 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 		return false;
 
 	auto wallPart = owner.curInt->cb->battleHexToWallPart(hex);
-	if (!owner.curInt->cb->isWallPartPotentiallyAttackable(wallPart))
-		return false;
-
-	auto state = owner.curInt->cb->battleGetWallState(wallPart);
-	return state != EWallState::DESTROYED && state != EWallState::NONE;
+	return owner.curInt->cb->isWallPartAttackable(wallPart);
 }
 
 void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)

+ 0 - 7
client/widgets/MiscWidgets.cpp

@@ -393,18 +393,11 @@ void MoraleLuckBox::set(const IBonusBearer * node)
 	boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]);
 
 	if (morale && node && (node->hasBonusOfType(Bonus::UNDEAD)
-			|| node->hasBonusOfType(Bonus::BLOCK_MORALE)
 			|| node->hasBonusOfType(Bonus::NON_LIVING)))
 	{
 		text += CGI->generaltexth->arraytxt[113]; //unaffected by morale
 		bonusValue = 0;
 	}
-	else if(!morale && node && node->hasBonusOfType(Bonus::BLOCK_LUCK))
-	{
-		// TODO: there is no text like "Unaffected by luck" so probably we need own text
-		text += CGI->generaltexth->arraytxt[noneTxtId];
-		bonusValue = 0;
-	}
 	else if(morale && node && node->hasBonusOfType(Bonus::NO_MORALE))
 	{
 		auto noMorale = node->getBonus(Selector::type()(Bonus::NO_MORALE));

+ 0 - 17
client/windows/CCreatureWindow.cpp

@@ -768,26 +768,9 @@ void CStackWindow::initBonusesList()
 
 		//if it's possible to give any description or image for this kind of bonus
 		//TODO: figure out why half of bonuses don't have proper description
-		if(b->type == Bonus::MAGIC_RESISTANCE || (b->type == Bonus::SECONDARY_SKILL_PREMY && b->subtype == SecondarySkill::RESISTANCE))
-			continue;
 		if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty())
 			activeBonuses.push_back(bonusInfo);
 	}
-
-	//handle Magic resistance separately :/
-	int magicResistance = info->stackNode->magicResistance();//both MAGIC_RESITANCE and SECONDARY_SKILL_PREMY as one entry
-
-	if(magicResistance)
-	{
-		BonusInfo bonusInfo;
-		auto b = std::make_shared<Bonus>();
-		b->type = Bonus::MAGIC_RESISTANCE;
-
-		bonusInfo.name = VLC->getBth()->bonusToString(b, info->stackNode, false);
-		bonusInfo.description = VLC->getBth()->bonusToString(b, info->stackNode, true);
-		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
-		activeBonuses.push_back(bonusInfo);
-	}
 }
 
 void CStackWindow::initSections()

+ 6 - 1
client/windows/CHeroWindow.cpp

@@ -61,6 +61,11 @@ int64_t CHeroWithMaybePickedArtifact::getTreeVersion() const
 	return hero->getTreeVersion();  //this assumes that hero and artifact belongs to main bonus tree
 }
 
+si32 CHeroWithMaybePickedArtifact::manaLimit() const
+{
+	return hero->manaLimit();
+}
+
 CHeroWithMaybePickedArtifact::CHeroWithMaybePickedArtifact(CWindowWithArtifacts * Cww, const CGHeroInstance * Hero)
 	: hero(Hero), cww(Cww)
 {
@@ -315,7 +320,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 
 	dismissButton->block(!!curHero->visitedTown || noDismiss);
 
-	if(curHero->getSecSkillLevel(SecondarySkill::TACTICS) == 0)
+	if(curHero->valOfBonuses(Selector::type()(Bonus::BEFORE_BATTLE_REPOSITION)) == 0)
 	{
 		tacticsButton->block(true);
 	}

+ 2 - 0
client/windows/CHeroWindow.h

@@ -55,6 +55,8 @@ public:
 	TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override;
 
 	int64_t getTreeVersion() const override;
+
+	si32 manaLimit() const override;
 };
 
 class CHeroWindow : public CStatusbarWindow, public CGarrisonHolder, public CWindowWithArtifacts

+ 0 - 1
client/windows/CKingdomInterface.cpp

@@ -584,7 +584,6 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 	std::vector<const CGHeroInstance*> heroes = LOCPLINT->cb->getHeroesInfo(true);
 	for(auto & heroe : heroes)
 	{
-		totalIncome += heroe->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ESTATES));
 		totalIncome += heroe->valOfBonuses(Selector::typeSubtype(Bonus::GENERATE_RESOURCE, Res::GOLD));
 	}
 

+ 82 - 35
config/artifacts.json

@@ -810,7 +810,7 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "SIGHT_RADIOUS",
+				"type" : "SIGHT_RADIUS",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER"
 			}
@@ -822,7 +822,7 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "SIGHT_RADIOUS",
+				"type" : "SIGHT_RADIUS",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER"
 			}
@@ -834,8 +834,7 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : 12,
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "UNDEAD_RAISE_PERCENTAGE",
 				"val" : 5,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -847,8 +846,7 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : 12,
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "UNDEAD_RAISE_PERCENTAGE",
 				"val" : 10,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -860,8 +858,7 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : 12,
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "UNDEAD_RAISE_PERCENTAGE",
 				"val" : 15,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -912,10 +909,23 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.archery",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "PERCENTAGE_DAMAGE_BOOST",
+				"subtype" : 1,
 				"val" : 5,
-				"valueType" : "ADDITIVE_VALUE"
+				"valueType" : "ADDITIVE_VALUE",
+				"limiters" : [
+					{
+						"type" : "HAS_ANOTHER_BONUS_LIMITER",
+						"parameters" : [
+							"PERCENTAGE_DAMAGE_BOOST",
+							1,
+							{
+								"type" : "SECONDARY_SKILL",
+								"id" : "skill.archery"
+							}
+						]
+					}
+				]
 			}
 		],
 		"index" : 60,
@@ -925,10 +935,23 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.archery",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "PERCENTAGE_DAMAGE_BOOST",
+				"subtype" : 1,
 				"val" : 10,
-				"valueType" : "ADDITIVE_VALUE"
+				"valueType" : "ADDITIVE_VALUE",
+				"limiters" : [
+					{
+						"type" : "HAS_ANOTHER_BONUS_LIMITER",
+						"parameters" : [
+							"PERCENTAGE_DAMAGE_BOOST",
+							1,
+							{
+								"type" : "SECONDARY_SKILL",
+								"id" : "skill.archery"
+							}
+						]
+					}
+				]
 			}
 		],
 		"index" : 61,
@@ -938,10 +961,23 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.archery",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"type" : "PERCENTAGE_DAMAGE_BOOST",
+				"subtype" : 1,
 				"val" : 15,
-				"valueType" : "ADDITIVE_VALUE"
+				"valueType" : "ADDITIVE_VALUE",
+				"limiters" : [
+					{
+						"type" : "HAS_ANOTHER_BONUS_LIMITER",
+						"parameters" : [
+							"PERCENTAGE_DAMAGE_BOOST",
+							1,
+							{
+								"type" : "SECONDARY_SKILL",
+								"id" : "skill.archery"
+							}
+						]
+					}
+				]
 			}
 		],
 		"index" : 62,
@@ -951,8 +987,8 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.eagleEye",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"subtype" : 0,
+				"type" : "LEARN_BATTLE_SPELL_CHANCE",
 				"val" : 5,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -964,8 +1000,8 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.eagleEye",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"subtype" : 0,
+				"type" : "LEARN_BATTLE_SPELL_CHANCE",
 				"val" : 10,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -977,8 +1013,8 @@
 	{
 		"bonuses" : [
 			{
-				"subtype" : "skill.eagleEye",
-				"type" : "SECONDARY_SKILL_PREMY",
+				"subtype" : 0,
+				"type" : "LEARN_BATTLE_SPELL_CHANCE",
 				"val" : 15,
 				"valueType" : "ADDITIVE_VALUE"
 			}
@@ -1038,9 +1074,10 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "LAND_MOVEMENT",
+				"type" : "MOVEMENT",
+				"subtype" : 1,
 				"val" : 300,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "ADDITIVE_VALUE"
 			}
 		],
 		"index" : 70,
@@ -1050,9 +1087,10 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "SEA_MOVEMENT",
+				"type" : "MOVEMENT",
+				"subtype" : 0,
 				"val" : 1000,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "ADDITIVE_VALUE"
 			}
 		],
 		"index" : 71,
@@ -1207,9 +1245,10 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "BLOCK_MORALE",
+				"type" : "MORALE",
 				"val" : 0,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "INDEPENDENT_MIN",
+				"propagator": "BATTLE_WIDE"
 			}
 		],
 		"index" : 84,
@@ -1219,9 +1258,10 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "BLOCK_LUCK",
+				"type" : "LUCK",
 				"val" : 0,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "INDEPENDENT_MIN",
+				"propagator": "BATTLE_WIDE"
 			}
 		],
 		"index" : 85,
@@ -1394,9 +1434,10 @@
 	{
 		"bonuses" : [
 			{
-				"type" : "LAND_MOVEMENT",
+				"type" : "MOVEMENT",
+				"subtype" : 1,
 				"val" : 600,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "ADDITIVE_VALUE"
 			}
 		],
 		"index" : 98,
@@ -1742,9 +1783,10 @@
 				"valueType" : "BASE_NUMBER"
 			},
 			{
-				"type" : "SEA_MOVEMENT",
+				"type" : "MOVEMENT",
+				"subtype" : 0,
 				"val" : 500,
-				"valueType" : "BASE_NUMBER"
+				"valueType" : "ADDITIVE_VALUE"
 			},
 			{
 				"subtype" : "spell.summonBoat",
@@ -1892,6 +1934,11 @@
 	"cloakOfTheUndeadKing":
 	{
 		"bonuses" : [
+			{
+				"type" : "IMPROVED_NECROMANCY",
+				"subtype" : "creature.skeleton",
+				"addInfo" : 0
+			},
 			{
 				"type" : "IMPROVED_NECROMANCY",
 				"subtype" : "creature.walkingDead",

+ 11 - 27
config/bonuses.json

@@ -1,5 +1,4 @@
 //TODO: selector-based config
-// SECONDARY_SKILL_PREMY
 // school immunities
 // LEVEL_SPELL_IMMUNITY
 
@@ -215,14 +214,6 @@
 
 	},
 
-	"FULL_HP_REGENERATION":
-	{
-		"graphics":
-		{
-			"icon":  "zvs/Lib1.res/E_TROLL"
-		}
-	},
-
 	"GARGOYLE":
 	{
 		"graphics":
@@ -271,28 +262,22 @@
 		}
 	},
 
-	"KING1":
+	"KING":
 	{
 		"graphics":
 		{
-			"icon":  "zvs/Lib1.res/E_KING1"
+			"icon":  "zvs/Lib1.res/E_KING3"
 		}
 	},
 
-	"KING2":
+	"LEARN_BATTLE_SPELL_CHANCE":
 	{
-		"graphics":
-		{
-			"icon":  "zvs/Lib1.res/E_KING2"
-		}
+		"hidden": true
 	},
 
-	"KING3":
+	"LEARN_BATTLE_SPELL_LEVEL_LIMIT":
 	{
-		"graphics":
-		{
-			"icon":  "zvs/Lib1.res/E_KING3"
-		}
+		"hidden": true
 	},
 
 	"LEVEL_SPELL_IMMUNITY":
@@ -417,6 +402,11 @@
 		}
 	},
 
+	"PERCENTAGE_DAMAGE_BOOST":
+	{
+		"hidden": true
+	},
+
 	"RECEPTIVE":
 	{
 		"graphics":
@@ -441,12 +431,6 @@
 		}
 	},
 
-	"SECONDARY_SKILL_PREMY":
-	{
-		"hidden": true
-		//todo: selector based config
-	},
-
 	"SELF_LUCK":
 	{
 		"graphics":

+ 6 - 2
config/creatures/dungeon.json

@@ -261,7 +261,9 @@
 		{
 			"fearless" :
 			{
-				"type" : "SELF_MORALE"
+				"type" : "MORALE",
+				"val" : 1,
+				"valueType" : "INDEPENDENT_MAX"
 			}
 		 },
 		"upgrades": ["minotaurKing"],
@@ -287,7 +289,9 @@
 		{
 			"fearless" :
 			{
-				"type" : "SELF_MORALE"
+				"type" : "MORALE",
+				"val" : 1,
+				"valueType" : "INDEPENDENT_MAX"
 			}
 		 },
 		"graphics" :

+ 8 - 4
config/creatures/inferno.json

@@ -366,9 +366,11 @@
 			"descreaseLuck" :
 			{
 				"type" : "LUCK",
-				"effectRange" : "ONLY_ENEMY_ARMY",
 				"val" : -1,
-				"stacking" : "Devils"
+				"stacking" : "Devils",
+				"propagator": "BATTLE_WIDE",
+				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
 			"blockRetaliation" :
 			{
@@ -418,9 +420,11 @@
 			"descreaseLuck" :
 			{
 				"type" : "LUCK",
-				"effectRange" : "ONLY_ENEMY_ARMY",
 				"val" : -1,
-				"stacking" : "Devils"
+				"stacking" : "Devils",
+				"propagator": "BATTLE_WIDE",
+				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
 			"blockRetaliation" :
 			{

+ 13 - 9
config/creatures/necropolis.json

@@ -92,8 +92,8 @@
 		{
 			"regenerate" : 
 			{
-				"type" : "FULL_HP_REGENERATION",
-				"subtype" : 1
+				"type" : "HP_REGENERATION",
+				"val" : 50
 			}
 		},
 		"upgrades": ["wraith"],
@@ -120,8 +120,8 @@
 		{
 			"regenerate" : 
 			{
-				"type" : "FULL_HP_REGENERATION",
-				"subtype" : 1
+				"type" : "HP_REGENERATION",
+				"val" : 50
 			},
 			"drainsMana" :
 			{
@@ -342,10 +342,12 @@
 			"decreaseMorale" :
 			{
 				"type" : "MORALE",
-				"effectRange": "ONLY_ENEMY_ARMY",
 				"val" : -1,
-				"stacking" : "Undead Dragons"
-			},
+				"stacking" : "Undead Dragons",
+				"propagator": "BATTLE_WIDE",
+				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"limiters" : [ "OPPOSITE_SIDE" ]
+			}
 		},
 		"upgrades": ["ghostDragon"],
 		"graphics" :
@@ -375,9 +377,11 @@
 			"decreaseMorale" :
 			{
 				"type" : "MORALE",
-				"effectRange": "ONLY_ENEMY_ARMY",
 				"val" : -1,
-				"stacking" : "Undead Dragons"
+				"stacking" : "Undead Dragons",
+				"propagator": "BATTLE_WIDE",
+				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
 			"age" :
 			{

+ 5 - 2
config/creatures/neutral.json

@@ -400,7 +400,9 @@
 		{
 			"lucky" :
 			{
-				"type" : "SELF_LUCK"
+				"type" : "LUCK",
+				"val" : 1,
+				"valueType" : "INDEPENDENT_MAX"
 			}
 		 },
 		"graphics" :
@@ -571,7 +573,8 @@
 		{
 			"regenerates" :
 			{
-				"type" : "FULL_HP_REGENERATION"
+				"type" : "HP_REGENERATION",
+				"val" : 50
 			}
 		},
 		"graphics" :

+ 16 - 1
config/creatures/special.json

@@ -36,6 +36,9 @@
 		"index": 145,
 		"level": 0,
 		"faction": "neutral",
+		"abilities" : {
+			"siegeMachine" : { "type" : "CATAPULT", "subtype" : "spell.catapultShot" }
+		},
 		"graphics" :
 		{
 			"animation": "SMCATA.DEF",
@@ -78,7 +81,19 @@
 		"index": 147,
 		"level": 0,
 		"faction": "neutral",
-		"abilities": { "heals" : { "type" : "HEALER" } },
+		"abilities":
+		{
+			"heals" : {
+				"type" : "HEALER" ,
+				"subtype" : "spell.firstAid"
+			},
+			"power" : {
+				"type" : "SPECIFIC_SPELL_POWER",
+				"subtype" : "spell.firstAid",
+				"val" : 10,
+				"valueType" : "BASE_NUMBER"
+			}
+		},
 		"graphics" :
 		{
 			"animation": "SMTENT.DEF"

+ 14 - 1
config/creatures/stronghold.json

@@ -245,6 +245,13 @@
 		"index": 94,
 		"level": 6,
 		"faction": "stronghold",
+		"abilities" :
+		{
+			"siege" : {
+				"subtype" : "spell.cyclopsShot",
+				"type" : "CATAPULT"
+			}
+		},
 		"upgrades": ["cyclopKing"],
 		"graphics" :
 		{
@@ -271,9 +278,15 @@
 		"faction": "stronghold",
 		"abilities":
 		{
-			"siegeDoubleAttack" :
+			"siege" : {
+				"subtype" : "spell.cyclopsShot",
+				"type" : "CATAPULT"
+			},
+			"siegeLevel" :
 			{
+				"subtype" : "spell.cyclopsShot",
 				"type" : "CATAPULT_EXTRA_SHOTS",
+				"valueType" : "BASE_NUMBER",
 				"val" : 1
 			}
 		},

+ 52 - 1
config/defaultMods.json

@@ -44,5 +44,56 @@
 		"STACK_ARTIFACTS": false,
 		"COMMANDERS": false,
 		"MITHRIL": false //so far unused
-	}
+	},
+	"baseBonuses" : [
+		{
+			"type" : "SPELL_DAMAGE", //Default spell damage
+			"val" : 100,
+			"valueType" : "BASE_NUMBER"
+		},
+		{
+			"type" : "MAX_LEARNABLE_SPELL_LEVEL", //Hero can always learn level 1 and 2 spells
+			"val" : 2,
+			"valueType" : "BASE_NUMBER"
+		}
+	],
+	"heroBaseBonuses":
+	[
+		{
+			"type" : "MANA_REGENERATION", //default mana regeneration
+			"val" : 1,
+			"valueType" : "BASE_NUMBER"
+		},
+		{
+			"type" : "SIGHT_RADIUS", //default sight radius
+			"val" : 5,
+			"valueType" : "BASE_NUMBER"
+		},
+		{
+			"type" : "HERO_EXPERIENCE_GAIN_PERCENT", //default hero xp
+			"val" : 100,
+			"valueType" : "BASE_NUMBER"
+		},
+		{
+			"type" : "MANA_PER_KNOWLEDGE", //10 knowledge to 100 mana is default
+			"val" : 10,
+			"valueType" : "BASE_NUMBER"
+		},
+		{
+			"type" : "MOVEMENT", //Basic land movement
+			"subtype" : 1,
+			"val" : 1300,
+			"valueType" : "BASE_NUMBER",
+			"updater" : {
+				"type" : "ARMY_MOVEMENT", //Enable army movement bonus
+				"parameters" : [20, 3, 10, 700]
+			}
+		},
+		{
+			"type" : "MOVEMENT", //Basic sea movement
+			"subtype" : 0,
+			"val" : 1500,
+			"valueType" : "BASE_NUMBER"
+		}
+	]
 }

+ 2 - 2
config/factions/necropolis.json

@@ -181,10 +181,10 @@
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
 				"special2":       { "requires" : [ "mageGuild1" ],
-					"bonuses": [ { "type": "SECONDARY_SKILL_PREMY", "subtype": "skill.necromancy", "val": 10, "propagator": "PLAYER_PROPAGATOR" } ] },
+					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 10, "propagator": "PLAYER_PROPAGATOR" } ] },
 				"special3":       { "type" : "creatureTransformer", "requires" : [ "dwellingLvl1" ] },
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 },
-					"bonuses": [ { "type": "SECONDARY_SKILL_PREMY", "subtype": "skill.necromancy", "val": 20, "propagator": "PLAYER_PROPAGATOR" } ] },
+					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 20, "propagator": "PLAYER_PROPAGATOR" } ] },
 
 				"extraTownHall":  { "id" : 27, "requires" : [ "townHall" ], "mode" : "auto" },
 				"extraCityHall":  { "id" : 28, "requires" : [ "cityHall" ], "mode" : "auto" },

+ 2 - 1
config/gameConfig.json

@@ -78,7 +78,8 @@
 		"config/spells/offensive.json",
 		"config/spells/other.json",
 		"config/spells/timed.json",
-		"config/spells/ability.json"
+		"config/spells/ability.json",
+		"config/spells/vcmiAbility.json"
 	],
 	"skills" :
 	[

+ 34 - 18
config/heroes/castle.json

@@ -12,11 +12,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"archery" : {
-					"subtype" : "skill.archery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "PERCENTAGE_DAMAGE_BOOST",
+					"subtype" : 1,
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -62,11 +63,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"navigation" : {
-					"subtype" : "skill.navigation",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"subtype" : 0,
+					"type" : "MOVEMENT",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -85,11 +87,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"estates" : {
-					"subtype" : "skill.estates",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -150,11 +153,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"firstAid" : {
-					"subtype" : "skill.firstAid",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "spell.firstAid",
+					"type" : "SPECIFIC_SPELL_POWER",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -173,9 +177,20 @@
 		"specialty" : {
 			"bonuses" : {
 				"bless" : {
-					"addInfo" : 0,
-					"subtype" : "spell.bless",
-					"type" : "SPECIAL_BLESS_DAMAGE",
+					"type" : "GENERAL_DAMAGE_PREMY",
+					"limiters" : [
+						{
+							"type" : "HAS_ANOTHER_BONUS_LIMITER",
+							"parameters" : [
+								"GENERAL_DAMAGE_PREMY",
+								1,
+								{
+									"type" : "SPELL_EFFECT",
+									"id" : "spell.bless"
+								}
+							]
+						}
+					],
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 3
 				}
@@ -253,11 +268,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 14 - 12
config/heroes/dungeon.json

@@ -87,11 +87,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"logistics" : {
-					"subtype" : "skill.logistics",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"subtype" : 1,
+					"type" : "MOVEMENT",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -160,11 +161,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"mysticism" : {
-					"subtype" : "skill.mysticism",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_REGENERATION",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -183,11 +184,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -227,11 +228,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 25 - 21
config/heroes/fortress.json

@@ -68,11 +68,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"armorer" : {
-					"subtype" : "skill.armorer",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "GENERAL_DAMAGE_REDUCTION",
+					"subtype" : -1,
+					"targetSourceType" : "SECONDARY_SKILL",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -167,11 +168,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"mysticism" : {
-					"subtype" : "skill.mysticism",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_REGENERATION",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -190,11 +191,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"navigation" : {
-					"subtype" : "skill.navigation",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"subtype" : 0,
+					"type" : "MOVEMENT",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -213,11 +215,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"firstAid" : {
-					"subtype" : "skill.firstAid",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "spell.firstAid",
+					"type" : "SPECIFIC_SPELL_POWER",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -257,11 +260,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -280,11 +283,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"intelligence" : {
-					"subtype" : "skill.intelligence",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_PER_KNOWLEDGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -303,11 +306,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 9 - 9
config/heroes/inferno.json

@@ -128,11 +128,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"intelligence" : {
-					"subtype" : "skill.intelligence",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_PER_KNOWLEDGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -173,11 +173,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"mysticism" : {
-					"subtype" : "skill.mysticism",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_REGENERATION",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -259,11 +259,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}

+ 13 - 12
config/heroes/necropolis.json

@@ -87,11 +87,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"necromancy" : {
-					"subtype" : "skill.necromancy",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "UNDEAD_RAISE_PERCENTAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -190,11 +190,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -213,11 +213,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -278,11 +279,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"necromancy" : {
-					"subtype" : "skill.necromancy",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "UNDEAD_RAISE_PERCENTAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 22 - 18
config/heroes/rampart.json

@@ -12,11 +12,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"armorer" : {
-					"subtype" : "skill.armorer",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "GENERAL_DAMAGE_REDUCTION",
+					"subtype" : -1,
+					"targetSourceType" : "SECONDARY_SKILL",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -80,11 +81,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"resistance" : {
-					"subtype" : "skill.resistance",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "MAGIC_RESISTANCE",
+					"targetSourceType" : "SECONDARY_SKILL",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -130,11 +131,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"logistics" : {
-					"subtype" : "skill.logistics",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"subtype" : 1,
+					"type" : "MOVEMENT",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -196,11 +198,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"intelligence" : {
-					"subtype" : "skill.intelligence",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_PER_KNOWLEDGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -219,11 +221,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"firstAid" : {
-					"subtype" : "skill.firstAid",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "spell.firstAid",
+					"type" : "SPECIFIC_SPELL_POWER",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -242,11 +245,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 18 - 15
config/heroes/stronghold.json

@@ -95,11 +95,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"offence" : {
-					"subtype" : "skill.offence",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "PERCENTAGE_DAMAGE_BOOST",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}
@@ -132,11 +133,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -170,11 +171,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"logistics" : {
-					"subtype" : "skill.logistics",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"subtype" : 1,
+					"type" : "MOVEMENT",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -235,11 +237,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"sorcery" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "SPELL_DAMAGE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -258,11 +260,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 11 - 9
config/heroes/tower.json

@@ -57,11 +57,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"armorer" : {
-					"subtype" : "skill.armorer",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "GENERAL_DAMAGE_REDUCTION",
+					"subtype" : -1,
+					"targetSourceType" : "SECONDARY_SKILL",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -167,11 +168,11 @@
 		"specialty" : {
 			"bonuses" : {
 				"mysticism" : {
-					"subtype" : "skill.mysticism",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"targetSourceType" : "SECONDARY_SKILL",
+					"type" : "MANA_REGENERATION",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE"
 				}
 			}
 		}
@@ -190,11 +191,12 @@
 		"specialty" : {
 			"bonuses" : {
 				"eagleEye" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"updater" : "TIMES_HERO_LEVEL",
 					"val" : 5,
-					"valueType" : "PERCENT_TO_BASE"
+					"valueType" : "PERCENT_TO_TARGET_TYPE",
+					"targetSourceType" : "SECONDARY_SKILL"
 				}
 			}
 		}

+ 2 - 2
config/objects/rewardableBonusing.json

@@ -338,7 +338,7 @@
 						},
 						"message" : 138,
 						"movePoints" : 400,
-						"bonuses" : [ { "type" : "LAND_MOVEMENT", "val" : 400, "duration" : "ONE_WEEK"} ],
+						"bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1,  "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ],
 						"changeCreatures" : {
 							"cavalier" : "champion"
 						}
@@ -346,7 +346,7 @@
 					{
 						"message" : 137,
 						"movePoints" : 400,
-						"bonuses" : [ { "type" : "LAND_MOVEMENT", "val" : 400, "duration" : "ONE_WEEK"} ]
+						"bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1,  "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ]
 					}
 				]
 			}

+ 28 - 0
config/schemas/bonus.json

@@ -115,6 +115,30 @@
 				}
 			]
 		},
+		"propagationUpdater" : {
+			"anyOf" : [
+				{
+					"type" : "string"
+				},
+				{
+					"description" : "propagationUpdater",
+					"type" : "object",
+					"required" : ["type", "parameters"],
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"description" : "type"
+						},
+						"parameters": {
+							"type" : "array",
+							"description" : "parameters",
+							"additionalItems" : true
+						}
+					}
+				}
+			]
+		},
 		"sourceID": {
 			"type":"number",
 			"description": "sourceID"
@@ -123,6 +147,10 @@
 			"type":"string",
 			"description": "sourceType"
 		},
+		"targetSourceType": {
+			"type":"string",
+			"description": "targetSourceType"
+		},
 		"stacking" : {
 			"type" : "string",
 			"description" : "stacking"

+ 15 - 45
config/schemas/hero.json

@@ -113,55 +113,25 @@
 				}
 			}
 		},
-		"specialties" : 
-		{
-			"type" : "array",
-			"description" : "Specialty format used for OH3 heroes. Use \"specialty\" instead",
-			"additionalItems" : true
-		},
 		"specialty": {
-			"anyOf" : [
-				{
-					"type":"array",
-					"description": "Description of hero specialty using bonus system (deprecated)",
-					"items": {
-						"type" : "object",
-						"additionalProperties" : false,
-						"required" : [ "bonuses" ],
-						"properties" : {
-							"growsWithLevel" : {
-								"type" : "boolean",
-								"description" : "Specialty growth with level. Deprecated, use bonuses with updaters instead."
-							},
-							"bonuses" : {
-								"type" : "array",
-								"description" : "List of bonuses",
-								"items" : { "$ref" : "bonus.json" }
-							}
-						}
-					}
+			"type" : "object",
+			"description": "Description of hero specialty using bonus system",
+			"additionalProperties" : false,
+			"properties" : { 
+				"base" : {
+					"type" : "object",
+					"description" : "Will be merged with all bonuses."
 				},
-				{
+				"bonuses" : {
 					"type" : "object",
-					"description": "Description of hero specialty using bonus system",
-					"additionalProperties" : false,
-					"properties" : { 
-						"base" : {
-							"type" : "object",
-							"description" : "Will be merged with all bonuses."
-						},
-						"bonuses" : {
-							"type" : "object",
-							"description" : "Set of bonuses",
-							"additionalProperties" : { "$ref" : "bonus.json" }
-						},
-						"creature" : {
-							"type" : "string",
-							"description" : "Name of base creature to grant standard specialty to."
-						}
-					}
+					"description" : "Set of bonuses",
+					"additionalProperties" : { "$ref" : "bonus.json" }
+				},
+				"creature" : {
+					"type" : "string",
+					"description" : "Name of base creature to grant standard specialty to."
 				}
-			]
+			}
 		},
 		"spellbook": {
 			"type":"array",

+ 8 - 0
config/schemas/skill.json

@@ -60,6 +60,14 @@
 			"type": "string",
 			"description": "localizable skill name"
 		},
+		"obligatoryMajor":{
+			"type": "boolean",
+			"description": "This skill is major obligatory (like H3 Wisdom)"
+		},
+		"obligatoryMinor":{
+			"type": "boolean",
+			"description": "This skill is minor obligatory (like H3 Magic school)"
+		},
 		"gainChance" : {
 			"description" : "Chance for the skill to be offered on level-up (heroClass may override)",
 			"anyOf" : [

+ 84 - 68
config/skills.json

@@ -4,8 +4,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.pathfinding",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "ROUGH_TERRAIN_DISCOUNT",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -31,8 +30,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.archery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "PERCENTAGE_DAMAGE_BOOST",
+					"subtype" : 1,
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -58,9 +57,9 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.logistics",
-					"type" : "SECONDARY_SKILL_PREMY",
-					"valueType" : "BASE_NUMBER"
+					"subtype" : 1,
+					"type" : "MOVEMENT",
+					"valueType" : "PERCENT_TO_BASE"
 				}
 			}
 		},
@@ -85,7 +84,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"type" : "SIGHT_RADIOUS",
+					"type" : "SIGHT_RADIUS",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -111,8 +110,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.diplomacy",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "WANDERING_CREATURES_JOIN_BONUS",
 					"valueType" : "BASE_NUMBER"
 				},
 				"surr" : {
@@ -146,9 +144,9 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.navigation",
-					"type" : "SECONDARY_SKILL_PREMY",
-					"valueType" : "BASE_NUMBER"
+					"subtype" : 0,
+					"type" : "MOVEMENT",
+					"valueType" : "PERCENT_TO_BASE"
 				}
 			}
 		},
@@ -196,11 +194,11 @@
 	},
 	"wisdom" : {
 		"index" : 7,
+		"obligatoryMajor" : true,
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.wisdom",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "MAX_LEARNABLE_SPELL_LEVEL",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -226,8 +224,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.mysticism",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "MANA_REGENERATION",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -279,8 +276,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.ballistics",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "spell.catapultShot",
+					"type" : "CATAPULT_EXTRA_SHOTS",
 					"valueType" : "BASE_NUMBER"
 				},
 				"ctrl" : {
@@ -312,13 +309,13 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "LEARN_BATTLE_SPELL_CHANCE",
 					"valueType" : "BASE_NUMBER"
 				},
 				"val2" : {
-					"subtype" : "skill.eagleEye",
-					"type" : "SECONDARY_SKILL_VAL2",
+					"subtype" : -1,
+					"type" : "LEARN_BATTLE_SPELL_LEVEL_LIMIT",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -347,25 +344,32 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.necromancy",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "UNDEAD_RAISE_PERCENTAGE",
 					"valueType" : "BASE_NUMBER"
+				},
+				"power" : {
+					"type" : "IMPROVED_NECROMANCY",
+					"subtype" : "creature.skeleton",
+					"addInfo" : 0
 				}
 			}
 		},
 		"basic" : {
 			"effects" : {
-				"main" : { "val" : 10 }
+				"main" : { "val" : 10 },
+				"power" : { "val" : 1 }
 			}
 		},
 		"advanced" : {
 			"effects" : {
-				"main" : { "val" : 20 }
+				"main" : { "val" : 20 },
+				"power" : { "val" : 2 }
 			}
 		},
 		"expert" : {
 			"effects" : {
-				"main" : { "val" : 30 }
+				"main" : { "val" : 30 },
+				"power" : { "val" : 3 }
 			}
 		}
 	},
@@ -374,8 +378,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.estates",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "resource.gold",
+					"type" : "GENERATE_RESOURCE",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -401,11 +405,12 @@
 	},
 	"fireMagic" : {
 		"index" : 14,
+		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.fireMagic",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 2,
+					"type" : "MAGIC_SCHOOL_SKILL",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -428,11 +433,12 @@
 	},
 	"airMagic" : {
 		"index" : 15,
+		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.airMagic",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 1,
+					"type" : "MAGIC_SCHOOL_SKILL",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -455,11 +461,12 @@
 	},
 	"waterMagic" : {
 		"index" : 16,
+		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.waterMagic",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 4,
+					"type" : "MAGIC_SCHOOL_SKILL",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -482,11 +489,12 @@
 	},
 	"earthMagic" : {
 		"index" : 17,
+		"obligatoryMinor" : true,
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.earthMagic",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 8,
+					"type" : "MAGIC_SCHOOL_SKILL",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -512,8 +520,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.scholar",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : -1,
+					"type" : "LEARN_MEETING_SPELL_LIMIT",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -539,25 +547,31 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.tactics",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "BEFORE_BATTLE_REPOSITION",
+					"valueType" : "BASE_NUMBER"
+				},
+				"block" : {
+					"type" : "BEFORE_BATTLE_REPOSITION_BLOCK",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
 		},
 		"basic" : {
 			"effects" : {
-				"main" : { "val" : 2 }
+				"main" : { "val" : 2 },
+				"block" : { "val" : 2 }
 			}
 		},
 		"advanced" : {
 			"effects" : {
-				"main" : { "val" : 4 }
+				"main" : { "val" : 4 },
+				"block" : { "val" : 4 }
 			}
 		},
 		"expert" : {
 			"effects" : {
-				"main" : { "val" : 6 }
+				"main" : { "val" : 6 },
+				"block" : { "val" : 6 }
 			}
 		}
 	},
@@ -566,13 +580,13 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.artillery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "creature.ballista",
+					"type" : "BONUS_DAMAGE_CHANCE",
 					"valueType" : "BASE_NUMBER"
 				},
 				"val2" : {
-					"subtype" : "skill.artillery",
-					"type" : "SECONDARY_SKILL_VAL2",
+					"subtype" : "creature.ballista",
+					"type" : "HERO_GRANTS_ATTACKS",
 					"valueType" : "BASE_NUMBER"
 				},
 				"ctrl" : {
@@ -586,6 +600,12 @@
 					"type" : "MANUAL_CONTROL",
 					"val" : 100,
 					"valueType" : "BASE_NUMBER"
+				},
+				"damagePower" : {
+					"subtype" : "creature.ballista",
+					"type" : "BONUS_DAMAGE_PERCENTAGE",
+					"val" : 100,
+					"valueType" : "BASE_NUMBER"
 				}
 			}
 		},
@@ -613,9 +633,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.learning",
-					"type" : "SECONDARY_SKILL_PREMY",
-					"valueType" : "BASE_NUMBER"
+					"type" : "HERO_EXPERIENCE_GAIN_PERCENT",
+					"valueType" : "PERCENT_TO_BASE"
 				}
 			}
 		},
@@ -640,8 +659,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.offence",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : 0,
+					"type" : "PERCENTAGE_DAMAGE_BOOST",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -667,8 +686,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.armorer",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "GENERAL_DAMAGE_REDUCTION",
+					"subtype" : -1,
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -694,9 +713,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.intelligence",
-					"type" : "SECONDARY_SKILL_PREMY",
-					"valueType" : "BASE_NUMBER"
+					"type" : "MANA_PER_KNOWLEDGE",
+					"valueType" : "PERCENT_TO_BASE"
 				}
 			}
 		},
@@ -721,8 +739,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.sorcery",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "SPELL_DAMAGE",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -748,8 +765,7 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.resistance",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"type" : "MAGIC_RESISTANCE",
 					"valueType" : "BASE_NUMBER"
 				}
 			}
@@ -775,8 +791,8 @@
 		"base" : {
 			"effects" : {
 				"main" : {
-					"subtype" : "skill.firstAid",
-					"type" : "SECONDARY_SKILL_PREMY",
+					"subtype" : "spell.firstAid",
+					"type" : "SPECIFIC_SPELL_POWER",
 					"valueType" : "BASE_NUMBER"
 				},
 				"ctrl" : {
@@ -789,17 +805,17 @@
 		},
 		"basic" : {
 			"effects" : {
-				"main" : { "val" : 50 }
+				"main" : { "val" : 40 }
 			}
 		},
 		"advanced" : {
 			"effects" : {
-				"main" : { "val" : 75 }
+				"main" : { "val" : 65 }
 			}
 		},
 		"expert" : {
 			"effects" : {
-				"main" : { "val" : 100 }
+				"main" : { "val" : 90 }
 			}
 		}
 	}

+ 1 - 48
config/spells/ability.json

@@ -428,52 +428,5 @@
 				"bonus.DIRECT_DAMAGE_IMMUNITY" : "normal"
 			}
 		}
-	},
-	"summonDemons" : {
-		"type": "ability",
-		"targetType" : "CREATURE",
-		"name": "Summon Demons",
-		"school" : {},
-		"level": 2,
-		"power": 50,
-		"defaultGainChance": 0,
-		"gainChance": {},
-		"animation":{
-		},
-		"sounds": {
-			"cast": "RESURECT"
-		},
-		"levels" : {
-			"base": {
-				"description" : "",
-				"aiValue" : 0,
-				"power" : 40,
-				"cost" : 1,
-				"range" : "0",
-				"battleEffects":{
-					"demonSummon":{
-						"id":"demon",
-						"permanent":true,
-						"type":"core:demonSummon"
-					}
-				}
-			},
-			"none" :{},
-			"basic" :{},
-			"advanced" :{},
-			"expert" :{}
-		},
-		"flags" : {
-			"rising": true,
-			"positive": true
-		},
-		"targetCondition" : {
-			"noneOf" : {
-				"bonus.NON_LIVING" : "absolute",
-				"bonus.SIEGE_WEAPON" : "absolute",
-				"bonus.UNDEAD" : "absolute",
-				"bonus.GARGOYLE" : "absolute"
-			}
-		}
-	},
+	}
 }

+ 3 - 1
config/spells/other.json

@@ -284,7 +284,9 @@
 				"battleEffects":{
 					"catapult":{
 						"type":"core:catapult",
-						"targetsToAttack": 2
+						"targetsToAttack": 2,
+						"chanceToCrit" : 0,
+						"chanceToNormalHit" : 100
 					}
 				},
 				"range" : "X"

+ 236 - 0
config/spells/vcmiAbility.json

@@ -0,0 +1,236 @@
+{
+    "summonDemons" : {
+		"type": "ability",
+		"targetType" : "CREATURE",
+		"name": "Summon Demons",
+		"school" : {},
+		"level": 2,
+		"power": 50,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"animation":{
+		},
+		"sounds": {
+			"cast": "RESURECT"
+		},
+		"levels" : {
+			"base": {
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 40,
+				"cost" : 1,
+				"range" : "0",
+				"battleEffects":{
+					"demonSummon":{
+						"id":"demon",
+						"permanent":true,
+						"type":"core:demonSummon"
+					}
+				}
+			},
+			"none" :{},
+			"basic" :{},
+			"advanced" :{},
+			"expert" :{}
+		},
+		"flags" : {
+			"rising": true,
+			"positive": true
+		},
+		"targetCondition" : {
+			"noneOf" : {
+				"bonus.NON_LIVING" : "absolute",
+				"bonus.SIEGE_WEAPON" : "absolute",
+				"bonus.UNDEAD" : "absolute",
+				"bonus.GARGOYLE" : "absolute"
+			}
+		}
+    },
+    "firstAid" : {
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "First Aid",
+		"school" : {},
+		"level": 1,
+		"power": 10,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"animation":{
+			"affect":["SP12_"]
+		},
+
+		"sounds": {
+			"cast": "REGENER"
+		},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 10,
+				"cost" : 0,
+				"targetModifier":{"smart":true},
+				"battleEffects":{
+					"heal":{
+						"type":"core:heal",
+						"healLevel":"heal",
+						"healPower":"permanent",
+						"optional":true
+					}
+				},
+				"range" : "0"
+			},
+			"none" :{
+				"power" : 10
+			},
+			"basic" :{
+				"power" : 50
+			},
+			"advanced" :{
+				"power" : 75
+			},
+			"expert" :{
+				"power" : 100
+			}
+		},
+		"flags" : {
+			"positive": true
+		},
+		"targetCondition" : {
+			"nonMagical" : true,
+			"noneOf" : {
+				"bonus.SIEGE_WEAPON" : "absolute"
+			}
+		}
+	},
+	"catapultShot" : {
+		"targetType" : "LOCATION",
+		"type": "ability",
+		"name": "Catapult shot",
+		"school" : {},
+		"level": 1,
+		"power": 1,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 1,
+				"cost" : 0,
+				"targetModifier":{"smart":true},
+				"battleEffects":{
+					"catapult":{
+						"type":"core:catapult"
+					}
+				},
+				"range" : "0"
+			},
+			"none":{
+				"battleEffects" : {
+					"catapult" : {
+						"targetsToAttack": 1,
+						"chanceToHitKeep" : 5,
+						"chanceToHitGate" : 25,
+						"chanceToHitTower" : 10,
+						"chanceToHitWall" : 50,
+						"chanceToNormalHit" : 60,
+						"chanceToCrit" : 30
+					}
+				}
+			},
+			"basic":{
+				"battleEffects" : {
+					"catapult" : {
+						"targetsToAttack": 1,
+						"chanceToHitKeep" : 7,
+						"chanceToHitGate" : 30,
+						"chanceToHitTower" : 15,
+						"chanceToHitWall" : 60,
+						"chanceToNormalHit" : 50,
+						"chanceToCrit" : 50
+					}
+				}
+			},
+			"advanced":{
+				"battleEffects" : {
+					"catapult" : {
+						"targetsToAttack": 2,
+						"chanceToHitKeep" : 7,
+						"chanceToHitGate" : 30,
+						"chanceToHitTower" : 15,
+						"chanceToHitWall" : 60,
+						"chanceToNormalHit" : 50,
+						"chanceToCrit" : 50
+					}
+				}
+			},
+			"expert":{
+				"battleEffects" : {
+					"catapult" : {
+						"targetsToAttack": 2,
+						"chanceToHitKeep" : 10,
+						"chanceToHitGate" : 40,
+						"chanceToHitTower" : 20,
+						"chanceToHitWall" : 75,
+						"chanceToNormalHit" : 0,
+						"chanceToCrit" : 100
+					}
+				}
+			}
+		},
+		"flags" : {
+			"indifferent": true
+		},
+		"targetCondition" : {
+			"nonMagical" : true
+		}
+	},
+	"cyclopsShot" : {
+		"targetType" : "LOCATION",
+		"type": "ability",
+		"name": "Siege shot",
+		"school" : {},
+		"level": 1,
+		"power": 1,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 1,
+				"cost" : 0,
+				"targetModifier":{"smart":true},
+				"battleEffects":{
+					"catapult":{
+						"type":"core:catapult",
+						"targetsToAttack": 1,
+						"chanceToHitKeep" : 7,
+						"chanceToHitGate" : 30,
+						"chanceToHitTower" : 15,
+						"chanceToHitWall" : 60,
+						"chanceToNormalHit" : 50,
+						"chanceToCrit" : 50
+					}
+				},
+				"range" : "0"
+			},
+			"none":{},
+			"basic":{
+				"battleEffects" : {
+					"catapult" : {
+						"targetsToAttack": 2
+					}
+				}
+			},
+			"advanced":{},
+			"expert" : {}
+		},
+		"flags" : {
+			"indifferent": true
+		},
+		"targetCondition" : {
+			"nonMagical" : true
+		}
+	}
+}

+ 10 - 8
lib/CBonusTypeHandler.cpp

@@ -82,8 +82,8 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonu
 	if (text.find("${subtype.spell}") != std::string::npos)
 		boost::algorithm::replace_all(text, "${subtype.spell}", SpellID(bonus->subtype).toSpell()->getNameTranslated());
 
-	if (text.find("${MR}") != std::string::npos)
-		boost::algorithm::replace_all(text, "${MR}", std::to_string(bearer->magicResistance()));
+	if (text.find("${SHval}") != std::string::npos) //regeneration case
+		boost::algorithm::replace_all(text, "${SHval}", std::to_string(std::min(static_cast<si32>(bearer->MaxHealth()),bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))));
 
 	return text;
 }
@@ -95,12 +95,6 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bo
 
 	switch(bonus->type)
 	{
-	case Bonus::SECONDARY_SKILL_PREMY:
-		if(bonus->subtype == SecondarySkill::RESISTANCE)
-		{
-			fileName = "E_DWARF.bmp";
-		}
-		break;
 	case Bonus::SPELL_IMMUNITY:
 	{
 		fullPath = true;
@@ -170,6 +164,14 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bo
 		}
 		break;
 	}
+	case Bonus::KING:
+	{
+		if(vstd::iswithin(bonus->val, 0, 3))
+		{
+			fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp";
+		}
+		break;
+	}
 	case Bonus::GENERAL_DAMAGE_REDUCTION:
 	{
 		switch(bonus->subtype)

+ 13 - 8
lib/CCreatureHandler.cpp

@@ -469,10 +469,11 @@ void CCreatureHandler::loadCommanders()
 
 void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses)
 {
-	auto makeBonusNode = [&](std::string type) -> JsonNode
+	auto makeBonusNode = [&](std::string type, double val = 0) -> JsonNode
 	{
 		JsonNode ret;
 		ret["type"].String() = type;
+		ret["val"].Float() = val;
 		return ret;
 	};
 
@@ -484,12 +485,11 @@ void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses)
 		{"const_free_attack",      makeBonusNode("BLOCKS_RETALIATION")},
 		{"IS_UNDEAD",              makeBonusNode("UNDEAD")},
 		{"const_no_melee_penalty", makeBonusNode("NO_MELEE_PENALTY")},
-		{"const_jousting",         makeBonusNode("JOUSTING")},
-		{"KING_1",                 makeBonusNode("KING1")},
-		{"KING_2",                 makeBonusNode("KING2")},
-		{"KING_3",                 makeBonusNode("KING3")},
+		{"const_jousting",         makeBonusNode("JOUSTING", 5)},
+		{"KING_1",                 makeBonusNode("KING")}, // Slayer with no expertise
+		{"KING_2",                 makeBonusNode("KING", 2)}, // Advanced Slayer or better
+		{"KING_3",                 makeBonusNode("KING", 3)}, // Expert Slayer only
 		{"const_no_wall_penalty",  makeBonusNode("NO_WALL_PENALTY")},
-		{"CATAPULT",               makeBonusNode("CATAPULT")},
 		{"MULTI_HEADED",           makeBonusNode("ATTACKS_ALL_ADJACENT")},
 		{"IMMUNE_TO_MIND_SPELLS",  makeBonusNode("MIND_IMMUNITY")},
 		{"HAS_EXTENDED_ATTACK",    makeBonusNode("TWO_HEX_ATTACK_BREATH")}
@@ -1070,7 +1070,9 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars
 			case 'B':
 				b.type = Bonus::TWO_HEX_ATTACK_BREATH; break;
 			case 'c':
-				b.type = Bonus::JOUSTING; break;
+				b.type = Bonus::JOUSTING; 
+				b.val = 5;
+				break;
 			case 'D':
 				b.type = Bonus::ADDITIONAL_ATTACK; break;
 			case 'f':
@@ -1078,7 +1080,10 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars
 			case 'F':
 				b.type = Bonus::FLYING; break;
 			case 'm':
-				b.type = Bonus::SELF_MORALE; break;
+				b.type = Bonus::MORALE; break;
+				b.val = 1;
+				b.valType = Bonus::INDEPENDENT_MAX;
+				break;
 			case 'M':
 				b.type = Bonus::NO_MORALE; break;
 			case 'p': //Mind spells

+ 1 - 14
lib/CCreatureSet.cpp

@@ -745,11 +745,6 @@ int CStackInstance::getLevel() const
 si32 CStackInstance::magicResistance() const
 {
 	si32 val = valOfBonuses(Selector::type()(Bonus::MAGIC_RESISTANCE));
-	if (const CGHeroInstance * hero = dynamic_cast<const CGHeroInstance *>(_armyObj))
-	{
-		//resistance skill
-		val += hero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::RESISTANCE);
-	}
 	vstd::amin (val, 100);
 	return val;
 }
@@ -792,15 +787,7 @@ void CStackInstance::setType(const CCreature *c)
 }
 std::string CStackInstance::bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const
 {
-	if(Bonus::MAGIC_RESISTANCE == bonus->type)
-	{
-		return "";
-	}
-	else
-	{
-		return VLC->getBth()->bonusToString(bonus, this, description);
-	}
-
+	return VLC->getBth()->bonusToString(bonus, this, description);
 }
 
 std::string CStackInstance::bonusToGraphics(const std::shared_ptr<Bonus>& bonus) const

+ 14 - 1
lib/CGameState.cpp

@@ -740,6 +740,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow
 
 	logGlobal->debug("Initialization:");
 
+	initGlobalBonuses();
 	initPlayerStates();
 	placeCampaignHeroes();
 	initGrailPosition();
@@ -934,6 +935,19 @@ void CGameState::checkMapChecksum()
 	}
 }
 
+void CGameState::initGlobalBonuses()
+{
+	const JsonNode & baseBonuses = VLC->modh->settings.data["baseBonuses"];
+	logGlobal->debug("\tLoading global bonuses");
+	for(const auto & b : baseBonuses.Vector())
+	{
+		auto bonus = JsonUtils::parseBonus(b);
+		bonus->source = Bonus::GLOBAL;//for all
+		bonus->sid = -1; //there is one global object
+		globalEffects.addNewBonus(bonus);
+	}
+}
+
 void CGameState::initGrailPosition()
 {
 	logGlobal->debug("\tPicking grail position");
@@ -2550,7 +2564,6 @@ struct statsHLP
 		//Heroes can produce gold as well - skill, specialty or arts
 		for(auto & h : ps->heroes)
 		{
-			totalIncome += h->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ESTATES));
 			totalIncome += h->valOfBonuses(Selector::typeSubtype(Bonus::GENERATE_RESOURCE, Res::GOLD));
 
 			if(!heroOrTown)

+ 1 - 0
lib/CGameState.h

@@ -252,6 +252,7 @@ private:
 	void initNewGame(const IMapService * mapService, bool allowSavingRandomMap);
 	void initCampaign();
 	void checkMapChecksum();
+	void initGlobalBonuses();
 	void initGrailPosition();
 	void initRandomFactionsForPlayers();
 	void randomizeMapObjects();

+ 29 - 133
lib/CHeroHandler.cpp

@@ -399,7 +399,6 @@ CHeroHandler::~CHeroHandler() = default;
 
 CHeroHandler::CHeroHandler()
 {
-	loadBallistics();
 	loadExperience();
 }
 
@@ -556,12 +555,17 @@ std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo
 		AddSpecialtyForCreature(spec.additionalinfo, bonus, result);
 		break;
 	case 2: //secondary skill
-		bonus->type = Bonus::SECONDARY_SKILL_PREMY;
-		bonus->valType = Bonus::PERCENT_TO_BASE;
-		bonus->subtype = spec.subtype;
-		bonus->updater.reset(new TimesHeroLevelUpdater());
-		result.push_back(bonus);
-		break;
+		{
+			auto params = BonusParams("SECONDARY_SKILL_PREMY", "", spec.subtype);
+			bonus->type = params.type;
+			if(params.subtypeRelevant)
+				bonus->subtype = params.subtype;
+			bonus->valType = Bonus::PERCENT_TO_TARGET_TYPE;
+			bonus->targetSourceType = Bonus::SECONDARY_SKILL;
+			bonus->updater.reset(new TimesHeroLevelUpdater());
+			result.push_back(bonus);
+			break;
+		}
 	case 3: //spell damage bonus, level dependent but calculated elsewhere
 		bonus->type = Bonus::SPECIAL_SPELL_LEV;
 		bonus->subtype = spec.subtype;
@@ -604,16 +608,21 @@ std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo
 		result.push_back(bonus);
 		break;
 	case 6: //damage bonus for bless (Adela)
-		bonus->type = Bonus::SPECIAL_BLESS_DAMAGE;
-		bonus->subtype = spec.subtype; //spell id if you ever wanted to use it otherwise
-		bonus->additionalInfo = spec.additionalinfo; //damage factor
-		bonus->updater.reset(new TimesHeroLevelUpdater());
-		result.push_back(bonus);
-		break;
+		{
+			auto limiter = std::make_shared<HasAnotherBonusLimiter>(Bonus::GENERAL_DAMAGE_PREMY,Bonus::SPELL_EFFECT);
+			limiter->sid = spec.subtype; //spell id if you ever wanted to use it otherwise
+			limiter->isSourceIDRelevant = true;
+			bonus->type = Bonus::GENERAL_DAMAGE_PREMY;
+			bonus->updater.reset(new TimesHeroLevelUpdater());
+			bonus->addLimiter(limiter);
+			result.push_back(bonus);
+			break;
+		}
 	case 7: //maxed mastery for spell
-		bonus->type = Bonus::SPECIAL_FIXED_VALUE_ENCHANT;
+		bonus->type = Bonus::SPELL;
 		bonus->subtype = spec.subtype; //spell id
-		bonus->val = 3; //to match MAXED_BONUS
+		bonus->val = 3; //to match MAXED_SPELL
+		bonus->valType = Bonus::INDEPENDENT_MAX;
 		result.push_back(bonus);
 		break;
 	case 8: //peculiar spells - enchantments
@@ -673,58 +682,6 @@ std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo
 	return result;
 }
 
-// convert deprecated format
-std::vector<std::shared_ptr<Bonus>> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec, int sid)
-{
-	std::vector<std::shared_ptr<Bonus>> result;
-	for(std::shared_ptr<Bonus> oldBonus : spec.bonuses)
-	{
-		oldBonus->sid = sid;
-		if(oldBonus->type == Bonus::SPECIAL_SPELL_LEV || oldBonus->type == Bonus::SPECIAL_BLESS_DAMAGE)
-		{
-			// these bonuses used to auto-scale with hero level
-			std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*oldBonus);
-			newBonus->updater = std::make_shared<TimesHeroLevelUpdater>();
-			result.push_back(newBonus);
-		}
-		else if(spec.growsWithLevel)
-		{
-			std::shared_ptr<Bonus> newBonus = std::make_shared<Bonus>(*oldBonus);
-			switch(newBonus->type)
-			{
-			case Bonus::SECONDARY_SKILL_PREMY:
-				break; // ignore - used to be overwritten based on SPECIAL_SECONDARY_SKILL
-			case Bonus::SPECIAL_SECONDARY_SKILL:
-				newBonus->type = Bonus::SECONDARY_SKILL_PREMY;
-				newBonus->updater = std::make_shared<TimesHeroLevelUpdater>();
-				result.push_back(newBonus);
-				break;
-			case Bonus::PRIMARY_SKILL:
-				if((newBonus->subtype == PrimarySkill::ATTACK || newBonus->subtype == PrimarySkill::DEFENSE) && newBonus->limiter)
-				{
-					std::shared_ptr<CCreatureTypeLimiter> creatureLimiter = std::dynamic_pointer_cast<CCreatureTypeLimiter>(newBonus->limiter);
-					if(creatureLimiter)
-					{
-						const CCreature * cre = creatureLimiter->creature;
-						int creStat = newBonus->subtype == PrimarySkill::ATTACK ? cre->getAttack(false) : cre->getDefense(false);
-						int creLevel = cre->level ? cre->level : 5;
-						newBonus->updater = std::make_shared<GrowsWithLevelUpdater>(creStat, creLevel);
-					}
-					result.push_back(newBonus);
-				}
-				break;
-			default:
-				result.push_back(newBonus);
-			}
-		}
-		else
-		{
-			result.push_back(oldBonus);
-		}
-	}
-	return result;
-}
-
 void CHeroHandler::beforeValidate(JsonNode & object)
 {
 	//handle "base" specialty info
@@ -759,37 +716,9 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
 		return bonus;
 	};
 
-	//deprecated, used only for original specialties
-	const JsonNode & specialtiesNode = node["specialties"];
-	if (!specialtiesNode.isNull())
-	{
-		logMod->warn("Hero %s has deprecated specialties format.", hero->getNameTranslated());
-		for(const JsonNode &specialty : specialtiesNode.Vector())
-		{
-			SSpecialtyInfo spec;
-			spec.type =           static_cast<si32>(specialty["type"].Integer());
-			spec.val =            static_cast<si32>(specialty["val"].Integer());
-			spec.subtype =        static_cast<si32>(specialty["subtype"].Integer());
-			spec.additionalinfo = static_cast<si32>(specialty["info"].Integer());
-			//we convert after loading completes, to have all identifiers for json logging
-			hero->specDeprecated.push_back(spec);
-		}
-	}
-	//new(er) format, using bonus system
+	//new format, using bonus system
 	const JsonNode & specialtyNode = node["specialty"];
-	if(specialtyNode.getType() == JsonNode::JsonType::DATA_VECTOR)
-	{
-		//deprecated middle-aged format
-		for(const JsonNode & specialty : node["specialty"].Vector())
-		{
-			SSpecialtyBonus hs;
-			hs.growsWithLevel = specialty["growsWithLevel"].Bool();
-			for (const JsonNode & bonus : specialty["bonuses"].Vector())
-				hs.bonuses.push_back(prepSpec(JsonUtils::parseBonus(bonus)));
-			hero->specialtyDeprecated.push_back(hs);
-		}
-	}
-	else if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT)
+	if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT)
 	{
 		//creature specialty - alias for simplicity
 		if(!specialtyNode["creature"].isNull())
@@ -809,6 +738,8 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node)
 				hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second)));
 		}
 	}
+	else
+		logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated());
 }
 
 void CHeroHandler::loadExperience()
@@ -846,35 +777,6 @@ static std::string genRefName(std::string input)
 	return input;
 }
 
-void CHeroHandler::loadBallistics()
-{
-	CLegacyConfigParser ballParser("DATA/BALLIST.TXT");
-
-	ballParser.endLine(); //header
-	ballParser.endLine();
-
-	do
-	{
-		ballParser.readString();
-		ballParser.readString();
-
-		CHeroHandler::SBallisticsLevelInfo bli;
-		bli.keep   = static_cast<ui8>(ballParser.readNumber());
-		bli.tower  = static_cast<ui8>(ballParser.readNumber());
-		bli.gate   = static_cast<ui8>(ballParser.readNumber());
-		bli.wall   = static_cast<ui8>(ballParser.readNumber());
-		bli.shots  = static_cast<ui8>(ballParser.readNumber());
-		bli.noDmg  = static_cast<ui8>(ballParser.readNumber());
-		bli.oneDmg = static_cast<ui8>(ballParser.readNumber());
-		bli.twoDmg = static_cast<ui8>(ballParser.readNumber());
-		bli.sum    = static_cast<ui8>(ballParser.readNumber());
-		ballistics.push_back(bli);
-
-		assert(bli.noDmg + bli.oneDmg + bli.twoDmg == 100 && bli.sum == 100);
-	}
-	while (ballParser.endLine());
-}
-
 std::vector<JsonNode> CHeroHandler::loadLegacyData(size_t dataSize)
 {
 	objects.resize(dataSize);
@@ -950,7 +852,7 @@ void CHeroHandler::afterLoadFinalization()
 			bonus->sid = hero->getIndex();
 		}
 
-		if(hero->specDeprecated.size() > 0 || hero->specialtyDeprecated.size() > 0)
+		if(hero->specDeprecated.size() > 0)
 		{
 			logMod->debug("Converting specialty format for hero %s(%s)", hero->getNameTranslated(), FactionID::encode(hero->heroClass->faction));
 			std::vector<std::shared_ptr<Bonus>> convertedBonuses;
@@ -959,13 +861,7 @@ void CHeroHandler::afterLoadFinalization()
 				for(std::shared_ptr<Bonus> b : SpecialtyInfoToBonuses(spec, hero->ID.getNum()))
 					convertedBonuses.push_back(b);
 			}
-			for(const SSpecialtyBonus & spec : hero->specialtyDeprecated)
-			{
-				for(std::shared_ptr<Bonus> b : SpecialtyBonusToBonuses(spec, hero->ID.getNum()))
-					convertedBonuses.push_back(b);
-			}
 			hero->specDeprecated.clear();
-			hero->specialtyDeprecated.clear();
 			// store and create json for logging
 			std::vector<JsonNode> specVec;
 			std::vector<std::string> specNames;

+ 0 - 37
lib/CHeroHandler.h

@@ -44,18 +44,6 @@ struct SSpecialtyInfo
 	}
 };
 
-struct SSpecialtyBonus
-/// temporary hold
-{
-	ui8 growsWithLevel;
-	BonusList bonuses;
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & growsWithLevel;
-		h & bonuses;
-	}
-};
-
 class DLL_LINKAGE CHero : public HeroType
 {
 	friend class CHeroHandler;
@@ -85,7 +73,6 @@ public:
 	CHeroClass * heroClass;
 	std::vector<std::pair<SecondarySkill, ui8> > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert)
 	std::vector<SSpecialtyInfo> specDeprecated;
-	std::vector<SSpecialtyBonus> specialtyDeprecated;
 	BonusList specialty;
 	std::set<SpellID> spells;
 	bool haveSpellBook;
@@ -147,7 +134,6 @@ public:
 
 // convert deprecated format
 std::vector<std::shared_ptr<Bonus>> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid = 0);
-std::vector<std::shared_ptr<Bonus>> SpecialtyBonusToBonuses(const SSpecialtyBonus & spec, int sid = 0);
 
 class DLL_LINKAGE CHeroClass : public HeroClass
 {
@@ -267,7 +253,6 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
 	void loadHeroSpecialty(CHero * hero, const JsonNode & node);
 
 	void loadExperience();
-	void loadBallistics();
 
 public:
 	CHeroClassHandler classes;
@@ -275,27 +260,6 @@ public:
 	//default costs of going through terrains. -1 means terrain is impassable
 	std::map<TerrainId, int> terrCosts;
 
-	struct SBallisticsLevelInfo
-	{
-		ui8 keep, tower, gate, wall; //chance to hit in percent (eg. 87 is 87%)
-		ui8 shots; //how many shots we have
-		ui8 noDmg, oneDmg, twoDmg; //chances for shot dealing certain dmg in percent (eg. 87 is 87%); must sum to 100
-		ui8 sum; //I don't know if it is useful for anything, but it's in config file
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & keep;
-			h & tower;
-			h & gate;
-			h & wall;
-			h & shots;
-			h & noDmg;
-			h & oneDmg;
-			h & twoDmg;
-			h & sum;
-		}
-	};
-	std::vector<SBallisticsLevelInfo> ballistics; //info about ballistics ability per level; [0] - none; [1] - basic; [2] - adv; [3] - expert
-
 	ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount
 	ui64 reqExp(ui32 level) const; //calculates experience required for given level
 
@@ -316,7 +280,6 @@ public:
 		h & classes;
 		h & objects;
 		h & expPerLevel;
-		h & ballistics;
 		h & terrCosts;
 	}
 

+ 5 - 0
lib/CModHandler.cpp

@@ -812,6 +812,11 @@ void CModHandler::loadConfigFromFile (std::string name)
 	logMod->debug("\tCOMMANDERS\t%d", static_cast<int>(modules.COMMANDERS));
 	modules.MITHRIL = gameModules["MITHRIL"].Bool();
 	logMod->debug("\tMITHRIL\t%d", static_cast<int>(modules.MITHRIL));
+
+	const JsonNode & baseBonuses = VLC->modh->settings.data["heroBaseBonuses"];
+	logMod->debug("\tLoading base hero bonuses");
+	for(const auto & b : baseBonuses.Vector())
+		heroBaseBonuses.emplace_back(JsonUtils::parseBonus(b));
 }
 
 // currentList is passed by value to get current list of depending mods

+ 2 - 0
lib/CModHandler.h

@@ -354,6 +354,8 @@ public:
 	void load();
 	void afterLoad(bool onlyEssential);
 
+	std::vector<std::shared_ptr<Bonus>> heroBaseBonuses; //these bonuses will be applied to every hero on map
+
 	struct DLL_LINKAGE hardcodedFeatures
 	{
 		JsonNode data;

+ 31 - 6
lib/CPathfinder.cpp

@@ -1011,11 +1011,14 @@ TurnInfo::BonusCache::BonusCache(TConstBonusListPtr bl)
 	flyingMovementVal = bl->valOfBonuses(Selector::type()(Bonus::FLYING_MOVEMENT));
 	waterWalking = static_cast<bool>(bl->getFirst(Selector::type()(Bonus::WATER_WALKING)));
 	waterWalkingVal = bl->valOfBonuses(Selector::type()(Bonus::WATER_WALKING));
-	pathfindingVal = bl->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::PATHFINDING));
+	pathfindingVal = bl->valOfBonuses(Selector::type()(Bonus::ROUGH_TERRAIN_DISCOUNT));
 }
 
-TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn)
-	: hero(Hero), maxMovePointsLand(-1), maxMovePointsWater(-1)
+TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn):
+	hero(Hero),
+	maxMovePointsLand(-1),
+	maxMovePointsWater(-1),
+	turn(turn)
 {
 	bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, nullptr, "");
 	bonusCache = std::make_unique<BonusCache>(bonuses);
@@ -1068,9 +1071,8 @@ int TurnInfo::valOfBonuses(Bonus::BonusType type, int subtype) const
 		return bonusCache->flyingMovementVal;
 	case Bonus::WATER_WALKING:
 		return bonusCache->waterWalkingVal;
-	case Bonus::SECONDARY_SKILL_PREMY:
-		if (subtype == SecondarySkill::PATHFINDING)
-			return bonusCache->pathfindingVal;
+	case Bonus::ROUGH_TERRAIN_DISCOUNT:
+		return bonusCache->pathfindingVal;
 	}
 
 	return bonuses->valOfBonuses(Selector::type()(type).And(Selector::subtype()(subtype)));
@@ -1086,6 +1088,29 @@ int TurnInfo::getMaxMovePoints(const EPathfindingLayer layer) const
 	return layer == EPathfindingLayer::SAIL ? maxMovePointsWater : maxMovePointsLand;
 }
 
+void TurnInfo::updateHeroBonuses(Bonus::BonusType type, const CSelector& sel) const
+{
+	switch(type)
+	{
+	case Bonus::FREE_SHIP_BOARDING:
+		bonusCache->freeShipBoarding = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::FREE_SHIP_BOARDING)));
+		break;
+	case Bonus::FLYING_MOVEMENT:
+		bonusCache->flyingMovement = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::FLYING_MOVEMENT)));
+		bonusCache->flyingMovementVal = bonuses->valOfBonuses(Selector::type()(Bonus::FLYING_MOVEMENT));
+		break;
+	case Bonus::WATER_WALKING:
+		bonusCache->waterWalking = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::WATER_WALKING)));
+		bonusCache->waterWalkingVal = bonuses->valOfBonuses(Selector::type()(Bonus::WATER_WALKING));
+		break;
+	case Bonus::ROUGH_TERRAIN_DISCOUNT:
+		bonusCache->pathfindingVal = bonuses->valOfBonuses(Selector::type()(Bonus::ROUGH_TERRAIN_DISCOUNT));
+		break;
+	default:
+		bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, nullptr, "");
+	}
+}
+
 CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options)
 	: CGameInfoCallback(gs, boost::optional<PlayerColor>()), turn(-1), hero(Hero), options(Options), owner(Hero->tempOwner)
 {

+ 3 - 1
lib/CPathfinder.h

@@ -525,15 +525,17 @@ struct DLL_LINKAGE TurnInfo
 	std::unique_ptr<BonusCache> bonusCache;
 
 	const CGHeroInstance * hero;
-	TConstBonusListPtr bonuses;
+	mutable TConstBonusListPtr bonuses;
 	mutable int maxMovePointsLand;
 	mutable int maxMovePointsWater;
 	TerrainId nativeTerrain;
+	int turn;
 
 	TurnInfo(const CGHeroInstance * Hero, const int Turn = 0);
 	bool isLayerAvailable(const EPathfindingLayer layer) const;
 	bool hasBonusOfType(const Bonus::BonusType type, const int subtype = -1) const;
 	int valOfBonuses(const Bonus::BonusType type, const int subtype = -1) const;
+	void updateHeroBonuses(Bonus::BonusType type, const CSelector& sel) const;
 	int getMaxMovePoints(const EPathfindingLayer layer) const;
 };
 

+ 6 - 4
lib/CSkillHandler.cpp

@@ -33,8 +33,8 @@ CSkill::LevelInfo::~LevelInfo()
 {
 }
 
-CSkill::CSkill(SecondarySkill id, std::string identifier)
-	: id(id), identifier(identifier)
+CSkill::CSkill(SecondarySkill id, std::string identifier, bool obligatoryMajor, bool obligatoryMinor)
+	: id(id), identifier(identifier), obligatoryMajor(obligatoryMajor), obligatoryMinor(obligatoryMinor)
 {
 	gainChance[0] = gainChance[1] = 0; //affects CHeroClassHandler::afterLoadFinalization()
 	levels.resize(NSecondarySkill::levels.size() - 1);
@@ -207,8 +207,11 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode &
 {
 	assert(identifier.find(':') == std::string::npos);
 	assert(!scope.empty());
+	bool major, minor;
 
-	CSkill * skill = new CSkill(SecondarySkill((si32)index), identifier);
+	major = json["obligatoryMajor"].Bool();
+	minor = json["obligatoryMinor"].Bool();
+	CSkill * skill = new CSkill(SecondarySkill((si32)index), identifier, major, minor);
 	skill->modScope = scope;
 
 	VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"].String());
@@ -242,7 +245,6 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode &
 		skillAtLevel.iconLarge = levelNode["images"]["large"].String();
 	}
 	logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id);
-	logMod->trace("%s", skill->toString());
 
 	return skill;
 }

+ 13 - 1
lib/CSkillHandler.h

@@ -51,9 +51,15 @@ private:
 	std::string identifier;
 
 public:
-	CSkill(SecondarySkill id = SecondarySkill::DEFAULT, std::string identifier = "default");
+	CSkill(SecondarySkill id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false);
 	~CSkill();
 
+	enum class Obligatory : ui8
+	{
+		MAJOR = 0,
+		MINOR = 1,
+	};
+
 	int32_t getIndex() const override;
 	int32_t getIconIndex() const override;
 	std::string getJsonKey() const override;
@@ -70,6 +76,7 @@ public:
 	LevelInfo & at(int level);
 
 	std::string toString() const;
+	bool obligatory(Obligatory val) const { return val == Obligatory::MAJOR ? obligatoryMajor : obligatoryMinor; };
 
 	std::array<si32, 2> gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes
 
@@ -82,11 +89,16 @@ public:
 		h & identifier;
 		h & gainChance;
 		h & levels;
+		h & obligatoryMajor;
+		h & obligatoryMinor;
 	}
 
 	friend class CSkillHandler;
 	friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill);
 	friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info);
+private:
+	bool obligatoryMajor;
+	bool obligatoryMinor;
 };
 
 class DLL_LINKAGE CSkillHandler: public CHandlerBase<SecondarySkill, Skill, CSkill, SkillService>

+ 1 - 1
lib/CTownHandler.cpp

@@ -521,7 +521,7 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building)
 			b = createBonus(building, Bonus::PRIMARY_SKILL, +2, PrimarySkill::DEFENSE);
 			break;
 		case BuildingSubID::LIGHTHOUSE:
-			b = createBonus(building, Bonus::SEA_MOVEMENT, +500, playerPropagator);
+			b = createBonus(building, Bonus::MOVEMENT, +500, playerPropagator, 0);
 			break;
 		}
 	}

+ 411 - 122
lib/HeroBonus.cpp

@@ -32,7 +32,9 @@ VCMI_LIB_NAMESPACE_BEGIN
 #define FOREACH_RED_CHILD(pname) 	TNodes lchildren; getRedChildren(lchildren); for(CBonusSystemNode *pname : lchildren)
 
 #define BONUS_NAME(x) { #x, Bonus::x },
-	const std::map<std::string, Bonus::BonusType> bonusNameMap = { BONUS_LIST };
+	const std::map<std::string, Bonus::BonusType> bonusNameMap = {
+		BONUS_LIST
+	};
 #undef BONUS_NAME
 
 #define BONUS_VALUE(x) { #x, Bonus::x },
@@ -65,7 +67,6 @@ const std::map<std::string, Bonus::LimitEffect> bonusLimitEffect =
 	BONUS_ITEM(NO_LIMIT)
 	BONUS_ITEM(ONLY_DISTANCE_FIGHT)
 	BONUS_ITEM(ONLY_MELEE_FIGHT)
-	BONUS_ITEM(ONLY_ENEMY_ARMY)
 };
 
 const std::map<std::string, TLimiterPtr> bonusLimiterMap =
@@ -92,7 +93,28 @@ const std::map<std::string, TPropagatorPtr> bonusPropagatorMap =
 const std::map<std::string, TUpdaterPtr> bonusUpdaterMap =
 {
 	{"TIMES_HERO_LEVEL", std::make_shared<TimesHeroLevelUpdater>()},
-	{"TIMES_STACK_LEVEL", std::make_shared<TimesStackLevelUpdater>()}
+	{"TIMES_STACK_LEVEL", std::make_shared<TimesStackLevelUpdater>()},
+	{"ARMY_MOVEMENT", std::make_shared<ArmyMovementUpdater>()},
+	{"BONUS_OWNER_UPDATER", std::make_shared<OwnerUpdater>()}
+};
+
+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"
 };
 
 ///CBonusProxy
@@ -452,13 +474,21 @@ void BonusList::stackBonuses()
 
 int BonusList::totalValue() const
 {
-	int base = 0;
-	int percentToBase = 0;
-	int percentToAll = 0;
-	int additive = 0;
-	int indepMax = 0;
+	struct BonusCollection
+	{
+		int base = 0;
+		int percentToBase = 0;
+		int percentToAll = 0;
+		int additive = 0;
+		int percentToSource;
+		int indepMin = std::numeric_limits<int>::max();
+		int indepMax = std::numeric_limits<int>::min();
+	};
+
+	auto percent = [](int base, int percent) -> int {return (base * (100 + percent)) / 100; };
+	std::array <BonusCollection, Bonus::BonusSource::NUM_BONUS_SOURCE> sources = {};
+	BonusCollection any;
 	bool hasIndepMax = false;
-	int indepMin = 0;
 	bool hasIndepMin = false;
 
 	for(std::shared_ptr<Bonus> b : bonuses)
@@ -466,49 +496,52 @@ int BonusList::totalValue() const
 		switch(b->valType)
 		{
 		case Bonus::BASE_NUMBER:
-			base += b->val;
+			sources[b->source].base += b->val;
 			break;
 		case Bonus::PERCENT_TO_ALL:
-			percentToAll += b->val;
+			sources[b->source].percentToAll += b->val;
 			break;
 		case Bonus::PERCENT_TO_BASE:
-			percentToBase += b->val;
+			sources[b->source].percentToBase += b->val;
+			break;
+		case Bonus::PERCENT_TO_SOURCE:
+			sources[b->source].percentToSource += b->val;
+			break;
+		case Bonus::PERCENT_TO_TARGET_TYPE:
+			sources[b->targetSourceType].percentToSource += b->val;
 			break;
 		case Bonus::ADDITIVE_VALUE:
-			additive += b->val;
+			sources[b->source].additive += b->val;
 			break;
 		case Bonus::INDEPENDENT_MAX:
-			if (!hasIndepMax)
-			{
-				indepMax = b->val;
-				hasIndepMax = true;
-			}
-			else
-			{
-				vstd::amax(indepMax, b->val);
-			}
+			hasIndepMax = true;
+			vstd::amax(sources[b->source].indepMax, b->val);
 			break;
 		case Bonus::INDEPENDENT_MIN:
-			if (!hasIndepMin)
-			{
-				indepMin = b->val;
-				hasIndepMin = true;
-			}
-			else
-			{
-				vstd::amin(indepMin, b->val);
-			}
+			hasIndepMin = true;
+			vstd::amin(sources[b->source].indepMin, b->val);
 			break;
 		}
 	}
-	int modifiedBase = base + (base * percentToBase) / 100;
-	modifiedBase += additive;
-	int valFirst = (modifiedBase * (100 + percentToAll)) / 100;
+	for(auto src : sources)
+	{
+		any.base += percent(src.base ,src.percentToSource);
+		any.percentToBase += percent(src.percentToBase, src.percentToSource);
+		any.percentToAll += percent(src.percentToAll, src.percentToSource);
+		any.additive += percent(src.additive, src.percentToSource);
+		if(hasIndepMin)
+			vstd::amin(any.indepMin, percent(src.indepMin, src.percentToSource));
+		if(hasIndepMax)
+			vstd::amax(any.indepMax, percent(src.indepMin, src.percentToSource));
+	}
+	any.base = percent(any.base, any.percentToBase);
+	any.base += any.additive;
+	auto valFirst = percent(any.base ,any.percentToAll);
 
 	if(hasIndepMin && hasIndepMax)
-		assert(indepMin < indepMax);
+		assert(any.indepMin < any.indepMax);
 
-	const int notIndepBonuses = (int)boost::count_if(bonuses, [](const std::shared_ptr<Bonus>& b)
+	const int notIndepBonuses = (int)std::count_if(bonuses.cbegin(), bonuses.cend(), [](const std::shared_ptr<Bonus>& b)
 	{
 		return b->valType != Bonus::INDEPENDENT_MAX && b->valType != Bonus::INDEPENDENT_MIN;
 	});
@@ -516,16 +549,16 @@ int BonusList::totalValue() const
 	if (hasIndepMax)
 	{
 		if(notIndepBonuses)
-			vstd::amax(valFirst, indepMax);
+			vstd::amax(valFirst, any.indepMax);
 		else
-			valFirst = indepMax;
+			valFirst = any.indepMax;
 	}
 	if (hasIndepMin)
 	{
 		if(notIndepBonuses)
-			vstd::amin(valFirst, indepMin);
+			vstd::amin(valFirst, any.indepMin);
 		else
-			valFirst = indepMin;
+			valFirst = any.indepMin;
 	}
 
 	return valFirst;
@@ -551,18 +584,13 @@ std::shared_ptr<const Bonus> BonusList::getFirst(const CSelector &selector) cons
 	return nullptr;
 }
 
-void BonusList::getBonuses(BonusList & out, const CSelector &selector) const
-{
-	getBonuses(out, selector, nullptr);
-}
-
 void BonusList::getBonuses(BonusList & out, const CSelector &selector, const CSelector &limit) const
 {
 	out.reserve(bonuses.size());
 	for (auto & b : bonuses)
 	{
 		//add matching bonuses that matches limit predicate or have NO_LIMIT if no given predicate
-		auto noFightLimit = b->effectRange == Bonus::NO_LIMIT || b->effectRange == Bonus::ONLY_ENEMY_ARMY;
+		auto noFightLimit = b->effectRange == Bonus::NO_LIMIT;
 		if(selector(b.get()) && ((!limit && noFightLimit) || ((bool)limit && limit(b.get()))))
 			out.push_back(b);
 	}
@@ -639,20 +667,15 @@ CSelector IBonusBearer::anaffectedByMoraleSelector =
 Selector::type()(Bonus::NON_LIVING)
 .Or(Selector::type()(Bonus::UNDEAD))
 .Or(Selector::type()(Bonus::SIEGE_WEAPON))
-.Or(Selector::type()(Bonus::NO_MORALE))
-.Or(Selector::type()(Bonus::BLOCK_MORALE));
+.Or(Selector::type()(Bonus::NO_MORALE));
 
 CSelector IBonusBearer::moraleSelector = Selector::type()(Bonus::MORALE);
 CSelector IBonusBearer::luckSelector = Selector::type()(Bonus::LUCK);
-CSelector IBonusBearer::selfMoraleSelector = Selector::type()(Bonus::SELF_MORALE);
-CSelector IBonusBearer::selfLuckSelector = Selector::type()(Bonus::SELF_LUCK);
 
 IBonusBearer::IBonusBearer()
 	:anaffectedByMorale(this, anaffectedByMoraleSelector),
 	moraleValue(this, moraleSelector, 0),
-	luckValue(this, luckSelector, 0),
-	selfMorale(this, selfMoraleSelector),
-	selfLuck(this, selfLuckSelector)
+	luckValue(this, luckSelector, 0)
 {
 }
 
@@ -727,9 +750,6 @@ int IBonusBearer::MoraleVal() const
 
 	int ret = moraleValue.getValue();
 
-	if(selfMorale.getHasBonus()) //eg. minotaur
-		vstd::amax(ret, +1);
-
 	return vstd::abetween(ret, -3, +3);
 }
 
@@ -740,9 +760,6 @@ int IBonusBearer::LuckVal() const
 
 	int ret = luckValue.getValue();
 
-	if(selfLuck.getHasBonus()) //eg. halfling
-		vstd::amax(ret, +1);
-
 	return vstd::abetween(ret, -3, +3);
 }
 
@@ -756,9 +773,6 @@ int IBonusBearer::MoraleValAndBonusList(TConstBonusListPtr & bonusList) const
 	}
 	int ret = moraleValue.getValueAndList(bonusList);
 
-	if(selfMorale.getHasBonus()) //eg. minotaur
-		vstd::amax(ret, +1);
-
 	return vstd::abetween(ret, -3, +3);
 }
 
@@ -772,9 +786,6 @@ int IBonusBearer::LuckValAndBonusList(TConstBonusListPtr & bonusList) const
 	}
 	int ret = luckValue.getValueAndList(bonusList);
 
-	if(selfLuck.getHasBonus()) //eg. halfling
-		vstd::amax(ret, +1);
-
 	return vstd::abetween(ret, -3, +3);
 }
 
@@ -820,9 +831,7 @@ int IBonusBearer::getMaxDamage(bool ranged) const
 
 si32 IBonusBearer::manaLimit() const
 {
-	return si32(getPrimSkillLevel(PrimarySkill::KNOWLEDGE)
-		* (100.0 + valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::INTELLIGENCE))
-		/ 10.0);
+	return 0;
 }
 
 int IBonusBearer::getPrimSkillLevel(PrimarySkill::PrimarySkill id) const
@@ -951,7 +960,7 @@ void CBonusSystemNode::getAllParents(TCNodes & out) const //retrieves list of pa
 	}
 }
 
-void CBonusSystemNode::getAllBonusesRec(BonusList &out) const
+void CBonusSystemNode::getAllBonusesRec(BonusList &out, const CSelector & selector) const
 {
 	//out has been reserved sufficient capacity at getAllBonuses() call
 
@@ -971,13 +980,14 @@ void CBonusSystemNode::getAllBonusesRec(BonusList &out) const
 
 	for (auto parent : lparents)
 	{
-		parent->getAllBonusesRec(beforeUpdate);
+		parent->getAllBonusesRec(beforeUpdate, selector);
 	}
 	bonuses.getAllBonuses(beforeUpdate);
 
 	for(const auto & b : beforeUpdate)
 	{
-		auto updated = b->updater
+		//We should not run updaters on non-selected bonuses
+		auto updated = selector(b.get()) && b->updater
 			? getUpdatedBonus(b, b->updater)
 			: b;
 
@@ -1014,7 +1024,7 @@ TConstBonusListPtr CBonusSystemNode::getAllBonuses(const CSelector &selector, co
 			cachedBonuses.clear();
 			cachedRequests.clear();
 
-			getAllBonusesRec(allBonuses);
+			getAllBonusesRec(allBonuses, Selector::all);
 			limitBonuses(allBonuses, cachedBonuses);
 			cachedBonuses.stackBonuses();
 
@@ -1056,7 +1066,7 @@ TConstBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelector
 
 	// Get bonus results without caching enabled.
 	BonusList beforeLimiting, afterLimiting;
-	getAllBonusesRec(beforeLimiting);
+	getAllBonusesRec(beforeLimiting, selector);
 
 	if(!root || root == this)
 	{
@@ -1067,7 +1077,7 @@ TConstBonusListPtr CBonusSystemNode::getAllBonusesWithoutCaching(const CSelector
 		//We want to limit our query against an external node. We get all its bonuses,
 		// add the ones we're considering and see if they're cut out by limiters
 		BonusList rootBonuses, limitedRootBonuses;
-		getAllBonusesRec(rootBonuses);
+		getAllBonusesRec(rootBonuses, selector);
 
 		for(auto b : beforeLimiting)
 			rootBonuses.push_back(b);
@@ -1634,12 +1644,9 @@ JsonNode subtypeToJson(Bonus::BonusType type, int subtype)
 	{
 	case Bonus::PRIMARY_SKILL:
 		return JsonUtils::stringNode("primSkill." + PrimarySkill::names[subtype]);
-	case Bonus::SECONDARY_SKILL_PREMY:
-		return JsonUtils::stringNode(CSkillHandler::encodeSkillWithType(subtype));
 	case Bonus::SPECIAL_SPELL_LEV:
 	case Bonus::SPECIFIC_SPELL_DAMAGE:
-	case Bonus::SPECIAL_BLESS_DAMAGE:
-	case Bonus::MAXED_SPELL:
+	case Bonus::SPELL:
 	case Bonus::SPECIAL_PECULIAR_ENCHANT:
 	case Bonus::SPECIAL_ADD_VALUE_ENCHANT:
 	case Bonus::SPECIAL_FIXED_VALUE_ENCHANT:
@@ -1708,7 +1715,9 @@ JsonNode Bonus::toJsonNode() const
 	if(turnsRemain != 0)
 		root["turns"].Integer() = turnsRemain;
 	if(source != OTHER)
-		root["source"].String() = vstd::findKey(bonusSourceMap, source);
+		root["sourceType"].String() = vstd::findKey(bonusSourceMap, source);
+	if(targetSourceType != OTHER)
+		root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType);
 	if(sid != 0)
 		root["sourceID"].Integer() = sid;
 	if(val != 0)
@@ -1740,12 +1749,9 @@ std::string Bonus::nameForBonus() const
 	{
 	case Bonus::PRIMARY_SKILL:
 		return PrimarySkill::names[subtype];
-	case Bonus::SECONDARY_SKILL_PREMY:
-		return CSkillHandler::encodeSkill(subtype);
 	case Bonus::SPECIAL_SPELL_LEV:
 	case Bonus::SPECIFIC_SPELL_DAMAGE:
-	case Bonus::SPECIAL_BLESS_DAMAGE:
-	case Bonus::MAXED_SPELL:
+	case Bonus::SPELL:
 	case Bonus::SPECIAL_PECULIAR_ENCHANT:
 	case Bonus::SPECIAL_ADD_VALUE_ENCHANT:
 	case Bonus::SPECIAL_FIXED_VALUE_ENCHANT:
@@ -1761,6 +1767,244 @@ std::string Bonus::nameForBonus() const
 	}
 }
 
+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 = Bonus::ROUGH_TERRAIN_DISCOUNT;
+		else if(deprecatedSubtype == SecondarySkill::DIPLOMACY || deprecatedSubtypeStr == "skill.diplomacy")
+			type = Bonus::WANDERING_CREATURES_JOIN_BONUS;
+		else if(deprecatedSubtype == SecondarySkill::WISDOM || deprecatedSubtypeStr == "skill.wisdom")
+			type = Bonus::MAX_LEARNABLE_SPELL_LEVEL;
+		else if(deprecatedSubtype == SecondarySkill::MYSTICISM || deprecatedSubtypeStr == "skill.mysticism")
+			type = Bonus::MANA_REGENERATION;
+		else if(deprecatedSubtype == SecondarySkill::NECROMANCY || deprecatedSubtypeStr == "skill.necromancy")
+			type = Bonus::UNDEAD_RAISE_PERCENTAGE;
+		else if(deprecatedSubtype == SecondarySkill::LEARNING || deprecatedSubtypeStr == "skill.learning")
+			type = Bonus::HERO_EXPERIENCE_GAIN_PERCENT;
+		else if(deprecatedSubtype == SecondarySkill::RESISTANCE || deprecatedSubtypeStr == "skill.resistance")
+			type = Bonus::MAGIC_RESISTANCE;
+		else if(deprecatedSubtype == SecondarySkill::EAGLE_EYE || deprecatedSubtypeStr == "skill.eagleEye")
+			type = Bonus::LEARN_BATTLE_SPELL_CHANCE;
+		else if(deprecatedSubtype == SecondarySkill::INTELLIGENCE || deprecatedSubtypeStr == "skill.intelligence")
+		{
+			type = Bonus::MANA_PER_KNOWLEDGE;
+			valueType = Bonus::PERCENT_TO_BASE;
+			valueTypeRelevant = true;
+		}
+		else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery")
+			type = Bonus::SPELL_DAMAGE;
+		else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar")
+			type = Bonus::LEARN_MEETING_SPELL_LIMIT;
+		else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery")
+		{
+			subtype = 1;
+			subtypeRelevant = true;
+			type = Bonus::PERCENTAGE_DAMAGE_BOOST;
+		}
+		else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence")
+		{
+			subtype = 0;
+			subtypeRelevant = true;
+			type = Bonus::PERCENTAGE_DAMAGE_BOOST;
+		}
+		else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer")
+		{
+			subtype = -1;
+			subtypeRelevant = true;
+			type = Bonus::GENERAL_DAMAGE_REDUCTION;
+		}
+		else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation")
+		{
+			subtype = 0;
+			subtypeRelevant = true;
+			valueType = Bonus::PERCENT_TO_BASE;
+			valueTypeRelevant = true;
+			type = Bonus::MOVEMENT;
+		}
+		else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics")
+		{
+			subtype = 0;
+			subtypeRelevant = true;
+			valueType = Bonus::PERCENT_TO_BASE;
+			valueTypeRelevant = true;
+			type = Bonus::MOVEMENT;
+		}
+		else if(deprecatedSubtype == SecondarySkill::ESTATES || deprecatedSubtypeStr == "skill.estates")
+		{
+			type = Bonus::GENERATE_RESOURCE;
+			subtype = Res::GOLD;
+			subtypeRelevant = true;
+		}
+		else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic")
+		{
+			type = Bonus::MAGIC_SCHOOL_SKILL;
+			subtypeRelevant = true;
+			subtype = 4;
+		}
+		else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic")
+		{
+			type = Bonus::MAGIC_SCHOOL_SKILL;
+			subtypeRelevant = true;
+			subtype = 1;
+		}
+		else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic")
+		{
+			type = Bonus::MAGIC_SCHOOL_SKILL;
+			subtypeRelevant = true;
+			subtype = 2;
+		}
+		else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic")
+		{
+			type = Bonus::MAGIC_SCHOOL_SKILL;
+			subtypeRelevant = true;
+			subtype = 8;
+		}
+		else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery")
+		{
+			type = Bonus::BONUS_DAMAGE_CHANCE;
+			subtypeRelevant = true;
+			subtypeStr = "core:creature.ballista";
+		}
+		else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid")
+		{
+			type = Bonus::SPECIFIC_SPELL_POWER;
+			subtypeRelevant = true;
+			subtypeStr = "core:spell.firstAid";
+		}
+		else if (deprecatedSubtype == SecondarySkill::BALLISTICS || deprecatedSubtypeStr == "skill.ballistics")
+		{
+			type = Bonus::CATAPULT_EXTRA_SHOTS;
+			subtypeRelevant = true;
+			subtypeStr = "core:spell.catapultShot";
+		}
+		else
+			isConverted = false;
+	}
+	else if (deprecatedTypeStr == "SECONDARY_SKILL_VAL2")
+	{
+		if(deprecatedSubtype == SecondarySkill::EAGLE_EYE || deprecatedSubtypeStr == "skill.eagleEye")
+			type = Bonus::LEARN_BATTLE_SPELL_LEVEL_LIMIT;
+		else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery")
+		{
+			type = Bonus::HERO_GRANTS_ATTACKS;
+			subtypeRelevant = true;
+			subtypeStr = "core:creature.ballista";
+		}
+		else
+			isConverted = false;
+	}
+	else if (deprecatedTypeStr == "SEA_MOVEMENT")
+	{
+		subtype = 0;
+		subtypeRelevant = true;
+		valueType = Bonus::ADDITIVE_VALUE;
+		valueTypeRelevant = true;
+		type = Bonus::MOVEMENT;
+	}
+	else if (deprecatedTypeStr == "LAND_MOVEMENT")
+	{
+		subtype = 1;
+		subtypeRelevant = true;
+		valueType = Bonus::ADDITIVE_VALUE;
+		valueTypeRelevant = true;
+		type = Bonus::MOVEMENT;
+	}
+	else if (deprecatedTypeStr == "MAXED_SPELL")
+	{
+		type = Bonus::SPELL;
+		subtypeStr = deprecatedSubtypeStr;
+		subtypeRelevant = true;
+		valueType = Bonus::INDEPENDENT_MAX;
+		valueTypeRelevant = true;
+		val = 3;
+		valRelevant = true;
+	}
+	else if (deprecatedTypeStr == "FULL_HP_REGENERATION")
+	{
+		type = Bonus::HP_REGENERATION;
+		val = 100000; //very high value to always chose stack health
+		valRelevant = true;
+	}
+	else if (deprecatedTypeStr == "KING1")
+	{
+		type = Bonus::KING;
+		val = 0;
+		valRelevant = true;
+	}
+	else if (deprecatedTypeStr == "KING2")
+	{
+		type = Bonus::KING;
+		val = 2;
+		valRelevant = true;
+	}
+	else if (deprecatedTypeStr == "KING3")
+	{
+		type = Bonus::KING;
+		val = 3;
+		valRelevant = true;
+	}
+	else if (deprecatedTypeStr == "SIGHT_RADIOUS")
+		type = Bonus::SIGHT_RADIUS;
+	else if (deprecatedTypeStr == "SELF_MORALE")
+	{
+		type = Bonus::MORALE;
+		val = 1;
+		valRelevant = true;
+		valueType = Bonus::INDEPENDENT_MAX;
+		valueTypeRelevant = true;
+	}
+	else if (deprecatedTypeStr == "SELF_LUCK")
+	{
+		type = Bonus::LUCK;
+		val = 1;
+		valRelevant = true;
+		valueType = Bonus::INDEPENDENT_MAX;
+		valueTypeRelevant = true;
+	}
+	else
+		isConverted = false;
+}
+
+const JsonNode & BonusParams::toJson()
+{
+	assert(isConverted);
+	if(ret.isNull())
+	{
+		ret["type"].String() = vstd::findKey(bonusNameMap, type);
+		if(subtypeRelevant && !subtypeStr.empty())
+			ret["subtype"].String() = subtypeStr;
+		else if(subtypeRelevant)
+			ret["subtype"].Integer() = subtype;
+		if(valueTypeRelevant)
+			ret["valueType"].String() = vstd::findKey(bonusValueMap, valueType);
+		if(valRelevant)
+			ret["val"].Float() = val;
+		if(targetTypeRelevant)
+			ret["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetType);
+		jsonCreated = true;
+	}
+	return ret;
+};
+
+CSelector BonusParams::toSelector()
+{
+	assert(isConverted);
+	if(subtypeRelevant && !subtypeStr.empty())
+		JsonUtils::resolveIdentifier(subtype, toJson(), "subtype");
+
+	auto ret = Selector::type()(type);
+	if(subtypeRelevant)
+		ret = ret.And(Selector::subtype()(subtype));
+	if(valueTypeRelevant)
+		ret = ret.And(Selector::valueType(valueType));
+	if(targetTypeRelevant)
+		ret = ret.And(Selector::targetSourceType()(targetType));
+	return ret;
+}
+
 Bonus::Bonus(Bonus::BonusDuration Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype)
 	: duration((ui16)Duration), type(Type), subtype(Subtype), source(Src), val(Val), sid(ID), description(Desc)
 {
@@ -1768,6 +2012,7 @@ Bonus::Bonus(Bonus::BonusDuration Duration, BonusType Type, BonusSource Src, si3
 	valType = ADDITIVE_VALUE;
 	effectRange = NO_LIMIT;
 	boost::algorithm::trim(description);
+	targetSourceType = OTHER;
 }
 
 Bonus::Bonus(Bonus::BonusDuration Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype, ValueType ValType)
@@ -1775,6 +2020,7 @@ Bonus::Bonus(Bonus::BonusDuration Duration, BonusType Type, BonusSource Src, si3
 {
 	turnsRemain = 0;
 	effectRange = NO_LIMIT;
+	targetSourceType = OTHER;
 }
 
 Bonus::Bonus()
@@ -1789,6 +2035,7 @@ Bonus::Bonus()
 	val = 0;
 	source = OTHER;
 	sid = 0;
+	targetSourceType = OTHER;
 }
 
 std::shared_ptr<Bonus> Bonus::addPropagator(TPropagatorPtr Propagator)
@@ -1823,6 +2070,12 @@ namespace Selector
 		return ssourceType;
 	}
 
+	DLL_LINKAGE CSelectFieldEqual<Bonus::BonusSource> & targetSourceType()
+	{
+		static CSelectFieldEqual<Bonus::BonusSource> ssourceType(&Bonus::targetSourceType);
+		return ssourceType;
+	}
+
 	DLL_LINKAGE CSelectFieldEqual<Bonus::LimitEffect> & effectRange()
 	{
 		static CSelectFieldEqual<Bonus::LimitEffect> seffectRange(&Bonus::effectRange);
@@ -2022,20 +2275,36 @@ JsonNode CCreatureTypeLimiter::toJsonNode() const
 }
 
 HasAnotherBonusLimiter::HasAnotherBonusLimiter( Bonus::BonusType bonus )
-	: type(bonus), subtype(0), isSubtypeRelevant(false)
+	: type(bonus), subtype(0), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false)
 {
 }
 
 HasAnotherBonusLimiter::HasAnotherBonusLimiter( Bonus::BonusType bonus, TBonusSubtype _subtype )
-	: type(bonus), subtype(_subtype), isSubtypeRelevant(true)
+	: type(bonus), subtype(_subtype), isSubtypeRelevant(true), isSourceRelevant(false), isSourceIDRelevant(false)
+{
+}
+
+HasAnotherBonusLimiter::HasAnotherBonusLimiter(Bonus::BonusType bonus, Bonus::BonusSource src)
+	: type(bonus), source(src), isSubtypeRelevant(false), isSourceRelevant(true), isSourceIDRelevant(false)
+{
+}
+
+HasAnotherBonusLimiter::HasAnotherBonusLimiter(Bonus::BonusType bonus, TBonusSubtype _subtype, Bonus::BonusSource src)
+	: type(bonus), subtype(_subtype), isSubtypeRelevant(true), source(src), isSourceRelevant(true), isSourceIDRelevant(false)
 {
 }
 
 int HasAnotherBonusLimiter::limit(const BonusLimitationContext &context) const
 {
-	CSelector mySelector = isSubtypeRelevant
-							? Selector::typeSubtype(type, subtype)
-							: Selector::type()(type);
+	//TODO: proper selector config with parsing of JSON
+	auto mySelector = Selector::type()(type);
+
+	if(isSubtypeRelevant)
+		mySelector = mySelector.And(Selector::subtype()(subtype));
+	if(isSourceRelevant && isSourceIDRelevant)
+		mySelector = mySelector.And(Selector::source(source, sid));
+	else if (isSourceRelevant)
+		mySelector = mySelector.And(Selector::sourceTypeSel(source));
 
 	//if we have a bonus of required type accepted, limiter should accept also this bonus
 	if(context.alreadyAccepted.getFirst(mySelector))
@@ -2070,11 +2339,14 @@ JsonNode HasAnotherBonusLimiter::toJsonNode() const
 {
 	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
 	std::string typeName = vstd::findKey(bonusNameMap, type);
+	auto sourceTypeName = vstd::findKey(bonusSourceMap, source);
 
 	root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER";
 	root["parameters"].Vector().push_back(JsonUtils::stringNode(typeName));
 	if(isSubtypeRelevant)
 		root["parameters"].Vector().push_back(JsonUtils::intNode(subtype));
+	if(isSourceRelevant)
+		root["parameters"].Vector().push_back(JsonUtils::stringNode(sourceTypeName));
 
 	return root;
 }
@@ -2393,40 +2665,6 @@ std::shared_ptr<Bonus> Bonus::addUpdater(TUpdaterPtr Updater)
 	return this->shared_from_this();
 }
 
-// Update ONLY_ENEMY_ARMY bonuses from old saves to make them workable.
-// Also, we should foreseen possible errors in bonus configuration and fix them.
-void Bonus::updateOppositeBonuses()
-{
-	if(effectRange != Bonus::ONLY_ENEMY_ARMY)
-		return;
-
-	if(propagator)
-	{
-		if(propagator->getPropagatorType() != CBonusSystemNode::BATTLE)
-		{
-			logMod->error("Wrong Propagator will be ignored: The 'ONLY_ENEMY_ARMY' effectRange is only compatible with the 'BATTLE_WIDE' propagator.");
-			propagator.reset(new CPropagatorNodeType(CBonusSystemNode::BATTLE));
-		}
-	}
-	else
-	{
-		propagator = std::make_shared<CPropagatorNodeType>(CBonusSystemNode::BATTLE);
-	}
-	if(limiter)
-	{
-		if(!dynamic_cast<OppositeSideLimiter*>(limiter.get()))
-		{
-			logMod->error("Wrong Limiter will be ignored: The 'ONLY_ENEMY_ARMY' effectRange is only compatible with the 'OPPOSITE_SIDE' limiter.");
-			limiter.reset(new OppositeSideLimiter());
-		}
-	}
-	else
-	{
-		limiter = std::make_shared<OppositeSideLimiter>();
-	}
-	propagationUpdater = std::make_shared<OwnerUpdater>();
-}
-
 IUpdater::~IUpdater()
 {
 }
@@ -2513,6 +2751,57 @@ JsonNode TimesHeroLevelUpdater::toJsonNode() const
 	return JsonUtils::stringNode("TIMES_HERO_LEVEL");
 }
 
+ArmyMovementUpdater::ArmyMovementUpdater():
+	base(20),
+	divider(3),
+	multiplier(10),
+	max(700)
+{
+}
+
+ArmyMovementUpdater::ArmyMovementUpdater(int base, int divider, int multiplier, int max):
+	base(base),
+	divider(divider),
+	multiplier(multiplier),
+	max(max)
+{
+}
+
+std::shared_ptr<Bonus> ArmyMovementUpdater::createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const
+{
+	if(b->type == Bonus::MOVEMENT && context.getNodeType() == CBonusSystemNode::HERO)
+	{
+		auto speed = static_cast<const CGHeroInstance &>(context).getLowestCreatureSpeed();
+		si32 armySpeed = speed * base / divider;
+		auto counted = armySpeed * multiplier;
+		auto newBonus = std::make_shared<Bonus>(*b);
+		newBonus->source = Bonus::ARMY;
+		newBonus->val += vstd::amin(counted, max);
+		return newBonus;
+	}
+	if(b->type != Bonus::MOVEMENT)
+		logGlobal->error("ArmyMovementUpdater should only be used for MOVEMENT bonus!");
+	return b;
+}
+
+std::string ArmyMovementUpdater::toString() const
+{
+	return "ArmyMovementUpdater";
+}
+
+JsonNode ArmyMovementUpdater::toJsonNode() const
+{
+	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
+
+	root["type"].String() = "ARMY_MOVEMENT";
+	root["parameters"].Vector().push_back(JsonUtils::intNode(base));
+	root["parameters"].Vector().push_back(JsonUtils::intNode(divider));
+	root["parameters"].Vector().push_back(JsonUtils::intNode(multiplier));
+	root["parameters"].Vector().push_back(JsonUtils::intNode(max));
+
+	return root;
+}
+
 TimesStackLevelUpdater::TimesStackLevelUpdater()
 {
 }

+ 94 - 33
lib/HeroBonus.h

@@ -59,6 +59,12 @@ public:
 		return [thisCopy, rhs](const Bonus *b) mutable { return thisCopy(b) || rhs(b); };
 	}
 
+	CSelector Not() const
+	{
+		auto thisCopy = *this;
+		return [thisCopy](const Bonus *b) mutable { return !thisCopy(b); };
+	}
+
 	bool operator()(const Bonus *b) const
 	{
 		return TBase::operator()(b);
@@ -165,17 +171,14 @@ public:
 #define BONUS_LIST										\
 	BONUS_NAME(NONE) 									\
 	BONUS_NAME(LEVEL_COUNTER) /* for commander artifacts*/ \
-	BONUS_NAME(MOVEMENT) /*both water/land*/			\
-	BONUS_NAME(LAND_MOVEMENT) \
-	BONUS_NAME(SEA_MOVEMENT) \
+	BONUS_NAME(MOVEMENT) /*Subtype is 1 - land, 0 - sea*/ \
 	BONUS_NAME(MORALE) \
 	BONUS_NAME(LUCK) \
 	BONUS_NAME(PRIMARY_SKILL) /*uses subtype to pick skill; additional info if set: 1 - only melee, 2 - only distance*/  \
-	BONUS_NAME(SIGHT_RADIOUS) /*the correct word is RADIUS, but this one's already used in mods */\
+	BONUS_NAME(SIGHT_RADIUS) \
 	BONUS_NAME(MANA_REGENERATION) /*points per turn apart from normal (1 + mysticism)*/  \
 	BONUS_NAME(FULL_MANA_REGENERATION) /*all mana points are replenished every day*/  \
 	BONUS_NAME(NONEVIL_ALIGNMENT_MIX) /*good and neutral creatures can be mixed without morale penalty*/  \
-	BONUS_NAME(SECONDARY_SKILL_PREMY) /*%*/  \
 	BONUS_NAME(SURRENDER_DISCOUNT) /*%*/  \
 	BONUS_NAME(STACKS_SPEED)  /*additional info - percent of speed bonus applied after direct bonuses; >0 - added, <0 - subtracted to this part*/ \
 	BONUS_NAME(FLYING_MOVEMENT) /*value - penalty percentage*/ \
@@ -187,8 +190,6 @@ public:
 	BONUS_NAME(WATER_WALKING) /*value - penalty percentage*/ \
 	BONUS_NAME(NEGATE_ALL_NATURAL_IMMUNITIES) \
 	BONUS_NAME(STACK_HEALTH) \
-	BONUS_NAME(BLOCK_MORALE) \
-	BONUS_NAME(BLOCK_LUCK) \
 	BONUS_NAME(FIRE_SPELLS) \
 	BONUS_NAME(AIR_SPELLS) \
 	BONUS_NAME(WATER_SPELLS) \
@@ -202,10 +203,9 @@ public:
 	BONUS_NAME(MAGIC_SCHOOL_SKILL) /* //eg. for magic plains terrain, subtype: school of magic (0 - all, 1 - fire, 2 - air, 4 - water, 8 - earth), value - level*/ \
 	BONUS_NAME(FREE_SHOOTING) /*stacks can shoot even if otherwise blocked (sharpshooter's bow effect)*/ \
 	BONUS_NAME(OPENING_BATTLE_SPELL) /*casts a spell at expert level at beginning of battle, val - spell power, subtype - spell id*/ \
-	BONUS_NAME(IMPROVED_NECROMANCY) /* raise more powerful creatures: subtype - creature type raised, addInfo - [required necromancy level, required stack level] */ \
+	BONUS_NAME(IMPROVED_NECROMANCY) /* raise more powerful creatures: subtype - creature type raised, addInfo - [required necromancy level, required stack level], val - necromancy level for this purpose */ \
 	BONUS_NAME(CREATURE_GROWTH_PERCENT) /*increases growth of all units in all towns, val - percentage*/ \
 	BONUS_NAME(FREE_SHIP_BOARDING) /*movement points preserved with ship boarding and landing*/  \
-	BONUS_NAME(NO_TYPE)									\
 	BONUS_NAME(FLYING)									\
 	BONUS_NAME(SHOOTER)									\
 	BONUS_NAME(CHARGE_IMMUNITY)							\
@@ -214,9 +214,7 @@ public:
 	BONUS_NAME(NO_MELEE_PENALTY)						\
 	BONUS_NAME(JOUSTING) /*for champions*/				\
 	BONUS_NAME(HATE) /*eg. angels hate devils, subtype - ID of hated creature, val - damage bonus percent */ \
-	BONUS_NAME(KING1)									\
-	BONUS_NAME(KING2)									\
-	BONUS_NAME(KING3)									\
+	BONUS_NAME(KING) /* val - required slayer bonus val to affect */\
 	BONUS_NAME(MAGIC_RESISTANCE) /*in % (value)*/		\
 	BONUS_NAME(CHANGES_SPELL_COST_FOR_ALLY) /*in mana points (value) , eg. mage*/ \
 	BONUS_NAME(CHANGES_SPELL_COST_FOR_ENEMY) /*in mana points (value) , eg. pegasus */ \
@@ -245,16 +243,14 @@ public:
 	BONUS_NAME(FIRE_SHIELD)								\
 	BONUS_NAME(UNDEAD)									\
 	BONUS_NAME(HP_REGENERATION) /*creature regenerates val HP every new round*/					\
-	BONUS_NAME(FULL_HP_REGENERATION) /*first creature regenerates all HP every new round; subtype 0 - animation 4 (trolllike), 1 - animation 47 (wightlike)*/		\
 	BONUS_NAME(MANA_DRAIN) /*value - spell points per turn*/ \
 	BONUS_NAME(LIFE_DRAIN)								\
 	BONUS_NAME(DOUBLE_DAMAGE_CHANCE) /*value in %, eg. dread knight*/ \
 	BONUS_NAME(RETURN_AFTER_STRIKE)						\
-	BONUS_NAME(SELF_MORALE) /*eg. minotaur*/			\
 	BONUS_NAME(SPELLCASTER) /*subtype - spell id, value - level of school, additional info - weighted chance. use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER for calculating the power*/ \
 	BONUS_NAME(CATAPULT)								\
 	BONUS_NAME(ENEMY_DEFENCE_REDUCTION) /*in % (value) eg. behemots*/ \
-	BONUS_NAME(GENERAL_DAMAGE_REDUCTION) /* shield / air shield effect */ \
+	BONUS_NAME(GENERAL_DAMAGE_REDUCTION) /* shield / air shield effect, also armorer skill/petrify effect for subtype -1*/ \
 	BONUS_NAME(GENERAL_ATTACK_REDUCTION) /*eg. while stoned or blinded - in %,// subtype not used, use ONLY_MELEE_FIGHT / DISTANCE_FIGHT*/ \
 	BONUS_NAME(DEFENSIVE_STANCE) /* val - bonus to defense while defending */ \
 	BONUS_NAME(ATTACKS_ALL_ADJACENT) /*eg. hydra*/		\
@@ -262,7 +258,6 @@ public:
 	BONUS_NAME(FEAR)									\
 	BONUS_NAME(FEARLESS)								\
 	BONUS_NAME(NO_DISTANCE_PENALTY)						\
-	BONUS_NAME(SELF_LUCK) /*halfling*/					\
 	BONUS_NAME(ENCHANTER)/* for Enchanter spells, val - skill level, subtype - spell id, additionalInfo - cooldown */ \
 	BONUS_NAME(HEALER)									\
 	BONUS_NAME(SIEGE_WEAPON)							\
@@ -280,12 +275,9 @@ public:
 	BONUS_NAME(NO_LUCK) /*eg. when fighting on cursed ground*/	\
 	BONUS_NAME(NO_MORALE) /*eg. when fighting on cursed ground*/ \
 	BONUS_NAME(DARKNESS) /*val = radius */ \
-	BONUS_NAME(SPECIAL_SECONDARY_SKILL) /*subtype = id, val = value per level in percent*/ \
 	BONUS_NAME(SPECIAL_SPELL_LEV) /*subtype = id, val = value per level in percent*/\
-	BONUS_NAME(SPELL_DAMAGE) /*val = value*/\
+	BONUS_NAME(SPELL_DAMAGE) /*val = value, now works for sorcery*/\
 	BONUS_NAME(SPECIFIC_SPELL_DAMAGE) /*subtype = id of spell, val = value*/\
-	BONUS_NAME(SPECIAL_BLESS_DAMAGE) /*val = spell (bless), additionalInfo = value per level in percent*/\
-	BONUS_NAME(MAXED_SPELL) /*val = id. deprecated in favour of SPECIAL_FIXED_VALUE_ENCHANT*/\
 	BONUS_NAME(SPECIAL_PECULIAR_ENCHANT) /*blesses and curses with id = val dependent on unit's level, subtype = 0 or 1 for Coronius*/\
 	BONUS_NAME(SPECIAL_UPGRADE) /*subtype = base, additionalInfo = target */\
 	BONUS_NAME(DRAGON_NATURE) \
@@ -313,10 +305,9 @@ public:
 	BONUS_NAME(SOUL_STEAL) /*val - number of units gained per enemy killed, subtype = 0 - gained units survive after battle, 1 - they do not*/ \
 	BONUS_NAME(TRANSMUTATION) /*val - chance to trigger in %, subtype = 0 - resurrection based on HP, 1 - based on unit count, additional info - target creature ID (attacker default)*/\
 	BONUS_NAME(SUMMON_GUARDIANS) /*val - amount in % of stack count, subtype = creature ID*/\
-	BONUS_NAME(CATAPULT_EXTRA_SHOTS) /*val - number of additional shots, requires CATAPULT bonus to work*/\
+	BONUS_NAME(CATAPULT_EXTRA_SHOTS) /*val - power of catapult effect, requires CATAPULT bonus to work*/\
 	BONUS_NAME(RANGED_RETALIATION) /*allows shooters to perform ranged retaliation*/\
 	BONUS_NAME(BLOCKS_RANGED_RETALIATION) /*disallows ranged retaliation for shooter unit, BLOCKS_RETALIATION bonus is for melee retaliation only*/\
-  	BONUS_NAME(SECONDARY_SKILL_VAL2) /*for secondary skills that have multiple effects, like eagle eye (max level and chance)*/  \
 	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 */\
@@ -331,6 +322,21 @@ public:
 	BONUS_NAME(SPECIAL_FIXED_VALUE_ENCHANT) /*specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix.*/\
 	BONUS_NAME(TOWN_MAGIC_WELL) /*one-time pseudo-bonus to implement Magic Well in the town*/\
 	BONUS_NAME(LIMITED_SHOOTING_RANGE) /*limits range of shooting creatures, doesn't adjust any other mechanics (half vs full damage etc). val - range in hexes, additional info - optional new range for broken arrow mechanic */\
+	BONUS_NAME(LEARN_BATTLE_SPELL_CHANCE) /*skill-agnostic eagle eye chance. subtype = 0 - from enemy, 1 - TODO: from entire battlefield*/\
+	BONUS_NAME(LEARN_BATTLE_SPELL_LEVEL_LIMIT) /*skill-agnostic eagle eye limit, subtype - school (-1 for all), others TODO*/\
+	BONUS_NAME(PERCENTAGE_DAMAGE_BOOST) /*skill-agnostic archery and offence, subtype is 0 for offence and 1 for archery*/\
+	BONUS_NAME(LEARN_MEETING_SPELL_LIMIT) /*skill-agnostic scholar, subtype is -1 for all, TODO for others (> 0)*/\
+	BONUS_NAME(ROUGH_TERRAIN_DISCOUNT) /*skill-agnostic pathfinding*/\
+	BONUS_NAME(WANDERING_CREATURES_JOIN_BONUS) /*skill-agnostic diplomacy*/\
+	BONUS_NAME(BEFORE_BATTLE_REPOSITION) /*skill-agnostic tactics, bonus for allowing tactics*/\
+	BONUS_NAME(BEFORE_BATTLE_REPOSITION_BLOCK) /*skill-agnostic tactics, bonus for blocking opposite tactics. For now donble side tactics is TODO.*/\
+	BONUS_NAME(HERO_EXPERIENCE_GAIN_PERCENT) /*skill-agnostic learning, and we can use it as a global effect also*/\
+	BONUS_NAME(UNDEAD_RAISE_PERCENTAGE) /*Percentage of killed enemy creatures to be raised after battle as undead*/\
+	BONUS_NAME(MANA_PER_KNOWLEDGE) /*Percentage rate of translating 10 hero knowledge to mana, used to intelligence and global bonus*/\
+	BONUS_NAME(HERO_GRANTS_ATTACKS) /*If hero can grant additional attacks to creature, value is number of attacks, subtype is creatureID*/\
+	BONUS_NAME(BONUS_DAMAGE_PERCENTAGE) /*If hero can grant conditional damage to creature, value is percentage, subtype is creatureID*/\
+	BONUS_NAME(BONUS_DAMAGE_CHANCE) /*If hero can grant additional damage to creature, value is chance, subtype is creatureID*/\
+	BONUS_NAME(MAX_LEARNABLE_SPELL_LEVEL) /*This can work as wisdom before. val = max learnable spell level*/\
 	/* end of list */
 
 
@@ -351,6 +357,7 @@ public:
 	BONUS_SOURCE(SPECIAL_WEEK)\
 	BONUS_SOURCE(STACK_EXPERIENCE)\
 	BONUS_SOURCE(COMMANDER) /*TODO: consider using simply STACK_INSTANCE */\
+	BONUS_SOURCE(GLOBAL) /*used for base bonuses which all heroes or all stacks should have*/\
 	BONUS_SOURCE(OTHER) /*used for defensive stance and default value of spell level limit*/
 
 #define BONUS_VALUE_LIST \
@@ -358,6 +365,8 @@ public:
 	BONUS_VALUE(BASE_NUMBER)\
 	BONUS_VALUE(PERCENT_TO_ALL)\
 	BONUS_VALUE(PERCENT_TO_BASE)\
+	BONUS_VALUE(PERCENT_TO_SOURCE) /*Adds value only to bonuses with same source*/\
+	BONUS_VALUE(PERCENT_TO_TARGET_TYPE) /*Adds value only to bonuses with SourceType target*/\
 	BONUS_VALUE(INDEPENDENT_MAX) /*used for SPELL bonus */\
 	BONUS_VALUE(INDEPENDENT_MIN) //used for SECONDARY_SKILL_PREMY bonus
 
@@ -390,13 +399,13 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 #define BONUS_SOURCE(x) x,
 		BONUS_SOURCE_LIST
 #undef BONUS_SOURCE
+		NUM_BONUS_SOURCE /*This is a dummy value, which will be always last*/
 	};
 
 	enum LimitEffect
 	{
 		NO_LIMIT = 0,
 		ONLY_DISTANCE_FIGHT=1, ONLY_MELEE_FIGHT, //used to mark bonuses for attack/defense primary skills from spells like Precision (distance only)
-		ONLY_ENEMY_ARMY
 	};
 
 	enum ValueType
@@ -413,6 +422,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 	TBonusSubtype subtype; //-1 if not applicable - 4 bytes
 
 	BonusSource source;//source type" uses BonusSource values - what gave that bonus
+	BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE.
 	si32 val;
 	ui32 sid; //source id: id of object/artifact/spell
 	ValueType valType;
@@ -450,6 +460,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 		h & propagator;
 		h & updater;
 		h & propagationUpdater;
+		h & targetSourceType;
 	}
 
 	template <typename Ptr>
@@ -517,11 +528,32 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>
 	std::shared_ptr<Bonus> addLimiter(TLimiterPtr Limiter); //returns this for convenient chain-calls
 	std::shared_ptr<Bonus> addPropagator(TPropagatorPtr Propagator); //returns this for convenient chain-calls
 	std::shared_ptr<Bonus> addUpdater(TUpdaterPtr Updater); //returns this for convenient chain-calls
-	void updateOppositeBonuses();
 };
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus);
 
+struct DLL_LINKAGE BonusParams {
+	bool isConverted;
+	Bonus::BonusType type = Bonus::NONE;
+	TBonusSubtype subtype = -1;
+	std::string subtypeStr = "";
+	bool subtypeRelevant = false;
+	Bonus::ValueType valueType = Bonus::BASE_NUMBER;
+	bool valueTypeRelevant = false;
+	si32 val = 0;
+	bool valRelevant = false;
+	Bonus::BonusSource targetType = Bonus::SECONDARY_SKILL;
+	bool targetTypeRelevant = false;
+
+	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;
+};
+
 class DLL_LINKAGE BonusList
 {
 public:
@@ -568,11 +600,9 @@ public:
 	// BonusList functions
 	void stackBonuses();
 	int totalValue() const;
-	void getBonuses(BonusList &out, const CSelector &selector, const CSelector &limit) const;
+	void getBonuses(BonusList &out, const CSelector &selector, const CSelector &limit = nullptr) const;
 	void getAllBonuses(BonusList &out) const;
 
-	void getBonuses(BonusList & out, const CSelector &selector) const;
-
 	//special find functions
 	std::shared_ptr<Bonus> getFirst(const CSelector &select);
 	std::shared_ptr<const Bonus> getFirst(const CSelector &select) const;
@@ -676,10 +706,6 @@ private:
 	CTotalsProxy moraleValue;
 	static CSelector luckSelector;
 	CTotalsProxy luckValue;
-	static CSelector selfMoraleSelector;
-	CCheckProxy selfMorale;
-	static CSelector selfLuckSelector;
-	CCheckProxy selfLuck;
 
 public:
 	//new bonusing node interface
@@ -726,7 +752,7 @@ public:
 	virtual si32 magicResistance() const;
 	ui32 Speed(int turn = 0, bool useBind = false) const; //get speed of creature with all modificators
 
-	si32 manaLimit() const; //maximum mana value for this hero (basically 10*knowledge)
+	virtual si32 manaLimit() const; //maximum mana value for this hero (basically 10*knowledge)
 	int getPrimSkillLevel(PrimarySkill::PrimarySkill id) const;
 
 	virtual int64_t getTreeVersion() const = 0;
@@ -763,7 +789,7 @@ private:
 	mutable std::map<std::string, TBonusListPtr > cachedRequests;
 	mutable boost::mutex sync;
 
-	void getAllBonusesRec(BonusList &out) const;
+	void getAllBonusesRec(BonusList &out, const CSelector & selector) const;
 	TConstBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr) const;
 	std::shared_ptr<Bonus> getUpdatedBonus(const std::shared_ptr<Bonus> & b, const TUpdaterPtr updater) const;
 
@@ -1041,10 +1067,16 @@ class DLL_LINKAGE HasAnotherBonusLimiter : public ILimiter //applies only to nod
 public:
 	Bonus::BonusType type;
 	TBonusSubtype subtype;
+	Bonus::BonusSource source;
+	si32 sid;
 	bool isSubtypeRelevant; //check for subtype only if this is true
+	bool isSourceRelevant; //check for bonus source only if this is true
+	bool isSourceIDRelevant; //check for bonus source only if this is true
 
 	HasAnotherBonusLimiter(Bonus::BonusType bonus = Bonus::NONE);
 	HasAnotherBonusLimiter(Bonus::BonusType bonus, TBonusSubtype _subtype);
+	HasAnotherBonusLimiter(Bonus::BonusType bonus, Bonus::BonusSource src);
+	HasAnotherBonusLimiter(Bonus::BonusType bonus, TBonusSubtype _subtype, Bonus::BonusSource src);
 
 	int limit(const BonusLimitationContext &context) const override;
 	virtual std::string toString() const override;
@@ -1056,6 +1088,10 @@ public:
 		h & type;
 		h & subtype;
 		h & isSubtypeRelevant;
+		h & source;
+		h & isSourceRelevant;
+		h & sid;
+		h & isSourceIDRelevant;
 	}
 };
 
@@ -1168,6 +1204,7 @@ namespace Selector
 	extern DLL_LINKAGE CSelectFieldEqual<TBonusSubtype> & subtype();
 	extern DLL_LINKAGE CSelectFieldEqual<CAddInfo> & info();
 	extern DLL_LINKAGE CSelectFieldEqual<Bonus::BonusSource> & sourceType();
+	extern DLL_LINKAGE CSelectFieldEqual<Bonus::BonusSource> & targetSourceType();
 	extern DLL_LINKAGE CSelectFieldEqual<Bonus::LimitEffect> & effectRange();
 	extern DLL_LINKAGE CWillLastTurns turns;
 	extern DLL_LINKAGE CWillLastDays days;
@@ -1202,6 +1239,7 @@ extern DLL_LINKAGE const std::map<std::string, Bonus::LimitEffect> bonusLimitEff
 extern DLL_LINKAGE const std::map<std::string, TLimiterPtr> bonusLimiterMap;
 extern DLL_LINKAGE const std::map<std::string, TPropagatorPtr> bonusPropagatorMap;
 extern DLL_LINKAGE const std::map<std::string, TUpdaterPtr> bonusUpdaterMap;
+extern DLL_LINKAGE const std::set<std::string> deprecatedBonusSet;
 
 // BonusList template that requires full interface of CBonusSystemNode
 template <class InputIterator>
@@ -1278,6 +1316,29 @@ public:
 	virtual JsonNode toJsonNode() const override;
 };
 
+class DLL_LINKAGE ArmyMovementUpdater : public IUpdater
+{
+public:
+	si32 base;
+	si32 divider;
+	si32 multiplier;
+	si32 max;
+	ArmyMovementUpdater();
+	ArmyMovementUpdater(int base, int divider, int multiplier, int max);
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<IUpdater &>(*this);
+		h & base;
+		h & divider;
+		h & multiplier;
+		h & max;
+	}
+
+	std::shared_ptr<Bonus> createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const override;
+	virtual std::string toString() const override;
+	virtual JsonNode toJsonNode() const override;
+};
+
 class DLL_LINKAGE OwnerUpdater : public IUpdater
 {
 public:

+ 251 - 31
lib/JsonNode.cpp

@@ -708,10 +708,34 @@ std::shared_ptr<ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
 				{
 					std::shared_ptr<HasAnotherBonusLimiter> bonusLimiter = std::make_shared<HasAnotherBonusLimiter>();
 					bonusLimiter->type = it->second;
+					auto findSource = [&](const JsonNode & parameter)
+					{
+						if(parameter.getType() == JsonNode::JsonType::DATA_STRUCT)
+						{
+							auto sourceIt = bonusSourceMap.find(parameter["type"].String());
+							if(sourceIt != bonusSourceMap.end())
+							{
+								bonusLimiter->source = sourceIt->second;
+								bonusLimiter->isSourceRelevant = true;
+								if(!parameter["id"].isNull()) {
+									resolveIdentifier(parameter["id"], bonusLimiter->sid);
+									bonusLimiter->isSourceIDRelevant = true;
+								}
+							}
+						}
+						return false;
+					};
 					if(parameters.size() > 1)
 					{
-						resolveIdentifier(parameters[1], bonusLimiter->subtype);
-						bonusLimiter->isSubtypeRelevant = true;
+						if(findSource(parameters[1]) && parameters.size() == 2)
+							return bonusLimiter;
+						else
+						{
+							resolveIdentifier(parameters[1], bonusLimiter->subtype);
+							bonusLimiter->isSubtypeRelevant = true;
+							if(parameters.size() > 2)
+								findSource(parameters[2]);
+						}
 					}
 					return bonusLimiter;
 				}
@@ -781,26 +805,124 @@ std::shared_ptr<Bonus> JsonUtils::parseBuildingBonus(const JsonNode &ability, Bu
 	return b;
 }
 
+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.toJson());
+		auto params = BonusParams(ability["type"].String(),
+											ability["subtype"].isString() ? ability["subtype"].String() : "",
+											   ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1);
+		if(params.isConverted)
+		{
+			if(!params.valRelevant) {
+				params.val = static_cast<si32>(ability["val"].Float());
+				params.valRelevant = true;
+				if(params.type == Bonus::SPECIFIC_SPELL_POWER) //First Aid value should be substracted by 10
+					params.val -= 10; //Base First Aid value
+			}
+			Bonus::ValueType valueType = Bonus::ADDITIVE_VALUE;
+			if(!ability["valueType"].isNull())
+				valueType = bonusValueMap.find(ability["valueType"].String())->second;
+
+			if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && valueType == Bonus::PERCENT_TO_BASE) //assume secondary skill special
+			{
+				params.valueType = Bonus::PERCENT_TO_TARGET_TYPE;
+				params.targetType = Bonus::SECONDARY_SKILL;
+				params.targetTypeRelevant = true;
+			}
+
+			if(!params.valueTypeRelevant) {
+				params.valueType = valueType;
+				params.valueTypeRelevant = true;
+			}
+			logMod->warn("Please, use this bonus:\n%s\nConverted sucessfully!", params.toJson().toJson());
+			return params;
+		}
+		else
+			logMod->error("Cannot convert bonus!\n%s", ability.toJson());
+	}
+	BonusParams ret;
+	ret.isConverted = false;
+	return ret;
+}
+
+static TUpdaterPtr parseUpdater(const JsonNode & updaterJson)
+{
+	switch(updaterJson.getType())
+	{
+	case JsonNode::JsonType::DATA_STRING:
+		return parseByMap(bonusUpdaterMap, &updaterJson, "updater type ");
+		break;
+	case JsonNode::JsonType::DATA_STRUCT:
+		if(updaterJson["type"].String() == "GROWS_WITH_LEVEL")
+		{
+			std::shared_ptr<GrowsWithLevelUpdater> updater = std::make_shared<GrowsWithLevelUpdater>();
+			const JsonVector param = updaterJson["parameters"].Vector();
+			updater->valPer20 = static_cast<int>(param[0].Integer());
+			if(param.size() > 1)
+				updater->stepSize = static_cast<int>(param[1].Integer());
+			return updater;
+		}
+		else if (updaterJson["type"].String() == "ARMY_MOVEMENT")
+		{
+			std::shared_ptr<ArmyMovementUpdater> updater = std::make_shared<ArmyMovementUpdater>();
+			if(updaterJson["parameters"].isVector())
+			{
+				const auto & param = updaterJson["parameters"].Vector();
+				if(param.size() < 4)
+					logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!");
+				else
+				{
+					updater->base = static_cast<si32>(param.at(0).Integer());
+					updater->divider = static_cast<si32>(param.at(1).Integer());
+					updater->multiplier = static_cast<si32>(param.at(2).Integer());
+					updater->max = static_cast<si32>(param.at(3).Integer());
+				}
+				return updater;
+			}
+		}
+		else
+			logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String());
+		break;
+	}
+	return nullptr;
+}
+
 bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 {
 	const JsonNode *value;
 
 	std::string type = ability["type"].String();
 	auto it = bonusNameMap.find(type);
+	auto params = std::make_unique<BonusParams>(false);
 	if (it == bonusNameMap.end())
 	{
-		logMod->error("Error: invalid ability type %s.", type);
-		return false;
+		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;
+		b->valType = params->valueType;
+		if(params->targetTypeRelevant)
+			b->targetSourceType = params->targetType;
 	}
-	b->type = it->second;
+	else
+		b->type = it->second;
 
-	resolveIdentifier(b->subtype, ability, "subtype");
+	resolveIdentifier(b->subtype, params->isConverted ? params->toJson() : ability, "subtype");
 
-	b->val = static_cast<si32>(ability["val"].Float());
+	if(!params->isConverted)
+	{
+		b->val = static_cast<si32>(ability["val"].Float());
 
-	value = &ability["valueType"];
-	if (!value->isNull())
-		b->valType = static_cast<Bonus::ValueType>(parseByMapN(bonusValueMap, value, "value type "));
+		value = &ability["valueType"];
+		if (!value->isNull())
+			b->valType = static_cast<Bonus::ValueType>(parseByMapN(bonusValueMap, value, "value type "));
+	}
 
 	b->stacking = ability["stacking"].String();
 
@@ -845,10 +967,14 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 		}
 	}
 
-	value = &ability["source"];
+	value = &ability["sourceType"];
 	if (!value->isNull())
 		b->source = static_cast<Bonus::BonusSource>(parseByMap(bonusSourceMap, value, "source type "));
 
+	value = &ability["targetSourceType"];
+	if (!value->isNull())
+		b->targetSourceType = static_cast<Bonus::BonusSource>(parseByMap(bonusSourceMap, value, "target type "));
+
 	value = &ability["limiters"];
 	if (!value->isNull())
 		b->limiter = parseLimiter(*value);
@@ -859,30 +985,124 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 
 	value = &ability["updater"];
 	if(!value->isNull())
+		b->addUpdater(parseUpdater(*value));
+	value = &ability["propagationUpdater"];
+	if(!value->isNull())
+		b->propagationUpdater = parseUpdater(*value);
+	return true;
+}
+
+CSelector JsonUtils::parseSelector(const JsonNode & ability)
+{
+	CSelector ret = Selector::all;
+
+	// Recursive parsers for anyOf, allOf, noneOf
+	const auto * value = &ability["allOf"];
+	if(value->isVector())
+	{
+		for(const auto & andN : value->Vector())
+			ret = ret.And(parseSelector(andN));
+	}
+
+	value = &ability["anyOf"];
+	if(value->isVector())
+	{
+		CSelector base = Selector::none;
+		for(const auto & andN : value->Vector())
+			base.Or(parseSelector(andN));
+		
+		ret = ret.And(base);
+	}
+
+	value = &ability["noneOf"];
+	if(value->isVector())
 	{
-		const JsonNode & updaterJson = *value;
-		switch(updaterJson.getType())
+		CSelector base = Selector::all;
+		for(const auto & andN : value->Vector())
+			base.And(parseSelector(andN));
+		
+		ret = ret.And(base.Not());
+	}
+
+	// Actual selector parser
+	value = &ability["type"];
+	if(value->isString())
+	{
+		auto it = bonusNameMap.find(value->String());
+		if(it != bonusNameMap.end())
+			ret = ret.And(Selector::type()(it->second));
+	}
+	value = &ability["subtype"];
+	if(!value->isNull())
+	{
+		TBonusSubtype subtype;
+		resolveIdentifier(subtype, ability, "subtype");
+		ret = ret.And(Selector::subtype()(subtype));
+	}
+	value = &ability["sourceType"];
+	Bonus::BonusSource src = Bonus::OTHER; //Fixes for GCC false maybe-uninitialized
+	si32 id = 0;
+	auto sourceIDRelevant = false;
+	auto sourceTypeRelevant = false;
+	if(value->isString())
+	{
+		auto it = bonusSourceMap.find(value->String());
+		if(it != bonusSourceMap.end())
 		{
-		case JsonNode::JsonType::DATA_STRING:
-			b->addUpdater(parseByMap(bonusUpdaterMap, &updaterJson, "updater type "));
-			break;
-		case JsonNode::JsonType::DATA_STRUCT:
-			if(updaterJson["type"].String() == "GROWS_WITH_LEVEL")
-			{
-				std::shared_ptr<GrowsWithLevelUpdater> updater = std::make_shared<GrowsWithLevelUpdater>();
-				const JsonVector param = updaterJson["parameters"].Vector();
-				updater->valPer20 = static_cast<int>(param[0].Integer());
-				if(param.size() > 1)
-					updater->stepSize = static_cast<int>(param[1].Integer());
-				b->addUpdater(updater);
-			}
-			else
-				logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String());
-			break;
+			src = it->second;
+			sourceTypeRelevant = true;
 		}
+
 	}
-	b->updateOppositeBonuses();
-	return true;
+	value = &ability["sourceID"];
+	if(!value->isNull())
+	{
+		sourceIDRelevant = true;
+		resolveIdentifier(id, ability, "sourceID");
+	}
+
+	if(sourceIDRelevant && sourceTypeRelevant)
+		ret = ret.And(Selector::source(src, id));
+	else if(sourceTypeRelevant)
+		ret = ret.And(Selector::sourceTypeSel(src));
+
+	
+	value = &ability["targetSourceType"];
+	if(value->isString())
+	{
+		auto it = bonusSourceMap.find(value->String());
+		if(it != bonusSourceMap.end())
+			ret = ret.And(Selector::targetSourceType()(it->second));
+	}
+	value = &ability["valueType"];
+	if(value->isString())
+	{
+		auto it = bonusValueMap.find(value->String());
+		if(it != bonusValueMap.end())
+			ret = ret.And(Selector::valueType(it->second));
+	}
+	CAddInfo info;
+	value = &ability["addInfo"];
+	if(!value->isNull())
+	{
+		resolveAddInfo(info, ability["addInfo"]);
+		ret = ret.And(Selector::info()(info));
+	}
+	value = &ability["effectRange"];
+	if(value->isString())
+	{
+		auto it = bonusLimitEffect.find(value->String());
+		if(it != bonusLimitEffect.end())
+			ret = ret.And(Selector::effectRange()(it->second));
+	}
+	value = &ability["lastsTurns"];
+	if(value->isNumber())
+		ret = ret.And(Selector::turns(value->Integer()));
+	value = &ability["lastsDays"];
+	if(value->isNumber())
+		ret = ret.And(Selector::days(value->Integer()));
+
+	return ret;
 }
 
 //returns first Key with value equal to given one

+ 2 - 0
lib/JsonNode.h

@@ -17,6 +17,7 @@ typedef std::map <std::string, JsonNode> JsonMap;
 typedef std::vector <JsonNode> JsonVector;
 
 struct Bonus;
+class CSelector;
 class ResourceID;
 class CAddInfo;
 class ILimiter;
@@ -175,6 +176,7 @@ namespace JsonUtils
 	DLL_LINKAGE std::shared_ptr<Bonus> parseBuildingBonus(const JsonNode &ability, BuildingID building, std::string description);
 	DLL_LINKAGE bool parseBonus(const JsonNode &ability, Bonus *placement);
 	DLL_LINKAGE std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter);
+	DLL_LINKAGE CSelector parseSelector(const JsonNode &ability);
 	DLL_LINKAGE void resolveIdentifier(si32 &var, const JsonNode &node, std::string name);
 	DLL_LINKAGE void resolveIdentifier(const JsonNode &node, si32 &var);
 	DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node);

+ 35 - 10
lib/battle/BattleInfo.cpp

@@ -483,22 +483,47 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	//tactics
 	bool isTacticsAllowed = !creatureBank; //no tactics in creature banks
 
-	int tacticLvls[2] = {0};
-	for(int i = 0; i < ARRAY_COUNT(tacticLvls); i++)
+	constexpr int sideSize = 2;
+
+	std::array<int, sideSize> battleRepositionHex = {};
+	std::array<int, sideSize> battleRepositionHexBlock = {};
+	for(int i = 0; i < sideSize; i++)
 	{
 		if(heroes[i])
-			tacticLvls[i] += heroes[i]->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::TACTICS));
+		{
+			battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(Bonus::BEFORE_BATTLE_REPOSITION));
+			battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(Bonus::BEFORE_BATTLE_REPOSITION_BLOCK));
+		}
 	}
-	int tacticsSkillDiff = tacticLvls[0] - tacticLvls[1];
+	int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER];
+	int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER];
 
-	if(tacticsSkillDiff && isTacticsAllowed)
+	/* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides
+	   when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this
+	   will not happen because tactics block opposite tactics on same value.
+	   TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if
+	   double tactics will be implemented.
+	*/
+
+	if(isTacticsAllowed)
 	{
-		curB->tacticsSide = tacticsSkillDiff < 0;
-		//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
-		curB->tacticDistance = 1 + std::abs(tacticsSkillDiff);
+		if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0)
+			logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
+		if(tacticsSkillDiffAttacker > 0)
+		{
+			curB->tacticsSide = BattleSide::ATTACKER;
+			//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
+			curB->tacticDistance = 1 + tacticsSkillDiffAttacker;
+		}
+		else if(tacticsSkillDiffDefender > 0)
+		{
+			curB->tacticsSide = BattleSide::DEFENDER;
+			//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
+			curB->tacticDistance = 1 + tacticsSkillDiffDefender;
+		}
+		else
+			curB->tacticDistance = 0;
 	}
-	else
-		curB->tacticDistance = 0;
 
 	return curB;
 }

+ 16 - 16
lib/battle/CBattleInfoCallback.cpp

@@ -142,11 +142,7 @@ bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, Bat
 		if (wallPart == EWallPart::INDESTRUCTIBLE_PART)
 			return true; // always blocks ranged attacks
 
-		assert(isWallPartPotentiallyAttackable(wallPart));
-
-		EWallState state = battleGetWallState(wallPart);
-
-		return state != EWallState::DESTROYED;
+		return isWallPartAttackable(wallPart);
 	};
 
 	auto needWallPenalty = [&](BattleHex from, BattleHex dest)
@@ -1417,6 +1413,18 @@ bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) co
 																	wallPart != EWallPart::INVALID;
 }
 
+bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const
+{
+	RETURN_IF_NOT_BATTLE(false);
+
+	if(isWallPartPotentiallyAttackable(wallPart))
+	{
+		auto wallState = battleGetWallState(wallPart);
+		return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED);
+	}
+	return false;
+}
+
 std::vector<BattleHex> CBattleInfoCallback::getAttackableBattleHexes() const
 {
 	std::vector<BattleHex> attackableBattleHexes;
@@ -1424,14 +1432,8 @@ std::vector<BattleHex> CBattleInfoCallback::getAttackableBattleHexes() const
 
 	for(const auto & wallPartPair : wallParts)
 	{
-		if(isWallPartPotentiallyAttackable(wallPartPair.second))
-		{
-			auto wallState = battleGetWallState(wallPartPair.second);
-			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
-			{
-				attackableBattleHexes.emplace_back(wallPartPair.first);
-			}
-		}
+		if(isWallPartAttackable(wallPartPair.second))
+			attackableBattleHexes.emplace_back(wallPartPair.first);
 	}
 
 	return attackableBattleHexes;
@@ -1611,9 +1613,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		{
 			const auto * kingMonster = getAliveEnemy([&](const CStack * stack) -> bool //look for enemy, non-shooting stack
 			{
-				const auto isKing = Selector::type()(Bonus::KING1)
-									.Or(Selector::type()(Bonus::KING2))
-									.Or(Selector::type()(Bonus::KING3));
+				const auto isKing = Selector::type()(Bonus::KING);
 
 				return stack->hasBonus(isKing);
 			});

+ 1 - 0
lib/battle/CBattleInfoCallback.h

@@ -133,6 +133,7 @@ public:
 	BattleHex wallPartToBattleHex(EWallPart part) const;
 	EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found
 	bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not
+	bool isWallPartAttackable(EWallPart wallPart) const; // returns true if the wall part is actually attackable, false if not
 	std::vector<BattleHex> getAttackableBattleHexes() const;
 
 	si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned

+ 18 - 29
lib/battle/DamageCalculator.cpp

@@ -133,24 +133,12 @@ int DamageCalculator::getActorAttackSlayer() const
 	static const auto selectorSlayer = Selector::type()(Bonus::SLAYER);
 
 	auto slayerEffects = info.attacker->getBonuses(selectorSlayer, cachingStrSlayer);
+	auto slayerAffected = info.defender->unitType()->valOfBonuses(Selector::type()(Bonus::KING));
 
 	if(std::shared_ptr<const Bonus> slayerEffect = slayerEffects->getFirst(Selector::all))
 	{
-		std::vector<int32_t> affectedIds;
 		const auto spLevel = slayerEffect->val;
-		const CCreature * defenderType = info.defender->unitType();
-		bool isAffected = false;
-
-		for(const auto & b : defenderType->getBonusList())
-		{
-			if((b->type == Bonus::KING3 && spLevel >= 3) || //expert
-				(b->type == Bonus::KING2 && spLevel >= 2) || //adv +
-				(b->type == Bonus::KING1 && spLevel >= 0)) //none or basic +
-			{
-				isAffected = true;
-				break;
-			}
-		}
+		bool isAffected = spLevel >= slayerAffected;
 
 		if(isAffected)
 		{
@@ -213,18 +201,16 @@ double DamageCalculator::getAttackBlessFactor() const
 
 double DamageCalculator::getAttackOffenseArcheryFactor() const
 {
+	
 	if(info.shooting)
 	{
-		const std::string cachingStrArchery = "type_SECONDARY_SKILL_PREMYs_ARCHERY";
-		static const auto selectorArchery = Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ARCHERY);
+		const std::string cachingStrArchery = "type_PERCENTAGE_DAMAGE_BOOSTs_1";
+		static const auto selectorArchery = Selector::typeSubtype(Bonus::PERCENTAGE_DAMAGE_BOOST, 1);
 		return info.attacker->valOfBonuses(selectorArchery, cachingStrArchery) / 100.0;
 	}
-	else
-	{
-		const std::string cachingStrOffence = "type_SECONDARY_SKILL_PREMYs_OFFENCE";
-		static const auto selectorOffence = Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::OFFENCE);
-		return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0;
-	}
+	const std::string cachingStrOffence = "type_PERCENTAGE_DAMAGE_BOOSTs_0";
+	static const auto selectorOffence = Selector::typeSubtype(Bonus::PERCENTAGE_DAMAGE_BOOST, 0);
+	return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0;
 }
 
 double DamageCalculator::getAttackLuckFactor() const
@@ -243,8 +229,11 @@ double DamageCalculator::getAttackDeathBlowFactor() const
 
 double DamageCalculator::getAttackDoubleDamageFactor() const
 {
-	if(info.doubleDamage)
-		return 1.0;
+	if(info.doubleDamage) {
+		const auto cachingStr = "type_BONUS_DAMAGE_PERCENTAGEs_" + std::to_string(info.attacker->creatureIndex());
+		const auto selector = Selector::typeSubtype(Bonus::BONUS_DAMAGE_PERCENTAGE, info.attacker->creatureIndex());
+		return info.attacker->valOfBonuses(selector, cachingStr) / 100.0;
+	}
 	return 0.0;
 }
 
@@ -258,7 +247,7 @@ double DamageCalculator::getAttackJoustingFactor() const
 
 	//applying jousting bonus
 	if(info.chargeDistance > 0 && info.attacker->hasBonus(selectorJousting, cachingStrJousting) && !info.defender->hasBonus(selectorChargeImmunity, cachingStrChargeImmunity))
-		return info.chargeDistance * 0.05;
+		return info.chargeDistance * (info.attacker->valOfBonuses(selectorJousting))/100.0;
 	return 0.0;
 }
 
@@ -291,8 +280,8 @@ double DamageCalculator::getDefenseSkillFactor() const
 
 double DamageCalculator::getDefenseArmorerFactor() const
 {
-	const std::string cachingStrArmorer = "type_SECONDARY_SKILL_PREMYs_ARMORER";
-	static const auto selectorArmorer = Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ARMORER);
+	const std::string cachingStrArmorer = "type_GENERAL_DAMAGE_REDUCTIONs_N1_NsrcSPELL_EFFECT";
+	static const auto selectorArmorer = Selector::typeSubtype(Bonus::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(Bonus::SPELL_EFFECT).Not());
 	return info.defender->valOfBonuses(selectorArmorer, cachingStrArmorer) / 100.0;
 
 }
@@ -396,8 +385,8 @@ double DamageCalculator::getDefenseForgetfulnessFactor() const
 double DamageCalculator::getDefensePetrificationFactor() const
 {
 	// Creatures that are petrified by a Basilisk's Petrifying attack or a Medusa's Stone gaze take 50% damage (R8 = 0.50) from ranged and melee attacks. Taking damage also deactivates the effect.
-	const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1";
-	static const auto selectorAllReduction = Selector::typeSubtype(Bonus::GENERAL_DAMAGE_REDUCTION, -1);
+	const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1_srcSPELL_EFFECT";
+	static const auto selectorAllReduction = Selector::typeSubtype(Bonus::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(Bonus::SPELL_EFFECT));
 
 	return info.defender->valOfBonuses(selectorAllReduction, cachingStrAllReduction) / 100.0;
 }

+ 87 - 67
lib/mapObjects/CGHeroInstance.cpp

@@ -76,7 +76,7 @@ ui32 CGHeroInstance::getTileCost(const TerrainTile & dest, const TerrainTile & f
 	{
 
 		ret = VLC->heroh->terrCosts[from.terType->getId()];
-		ret -= ti->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::PATHFINDING);
+		ret -= ti->valOfBonuses(Bonus::ROUGH_TERRAIN_DISCOUNT);
 		if(ret < GameConstants::BASE_MOVEMENT_COST)
 			ret = GameConstants::BASE_MOVEMENT_COST;
 	}
@@ -188,32 +188,27 @@ int CGHeroInstance::maxMovePoints(bool onLand) const
 	return maxMovePointsCached(onLand, &ti);
 }
 
-int CGHeroInstance::maxMovePointsCached(bool onLand, const TurnInfo * ti) const
+int CGHeroInstance::getLowestCreatureSpeed() const
 {
-	int base = 0;
-
-	if(onLand)
-	{
-		// used function is f(x) = 66.6x + 1300, rounded to second digit, where x is lowest speed in army
-		static constexpr int baseSpeed = 1300; // base speed from creature with 0 speed
-
-		int armySpeed = lowestSpeed(this) * 20 / 3;
+	return lowestCreatureSpeed;
+}
 
-		base = armySpeed * 10 + baseSpeed; // separate *10 is intentional to receive same rounding as in h3
-		vstd::abetween(base, 1500, 2000); // base speed is limited by these values
-	}
-	else
+void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const
+{
+	auto realLowestSpeed = lowestSpeed(this);
+	if(lowestCreatureSpeed != realLowestSpeed)
 	{
-		base = 1500; //on water base movement is always 1500 (speed of army doesn't matter)
+		lowestCreatureSpeed = realLowestSpeed;
+		//Let updaters run again
+		treeHasChanged();
+		ti->updateHeroBonuses(Bonus::MOVEMENT, Selector::subtype()(!!onLand));
 	}
+}
 
-	const Bonus::BonusType bt = onLand ? Bonus::LAND_MOVEMENT : Bonus::SEA_MOVEMENT;
-	const int bonus = ti->valOfBonuses(Bonus::MOVEMENT) + ti->valOfBonuses(bt);
-
-	const int subtype = onLand ? SecondarySkill::LOGISTICS : SecondarySkill::NAVIGATION;
-	const double modifier = ti->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, subtype) / 100.0;
-
-	return static_cast<int>(base * (1 + modifier)) + bonus;
+int CGHeroInstance::maxMovePointsCached(bool onLand, const TurnInfo * ti) const
+{
+	updateArmyMovementBonus(onLand, ti);
+	return ti->valOfBonuses(Bonus::MOVEMENT, !!onLand);
 }
 
 CGHeroInstance::CGHeroInstance():
@@ -226,7 +221,8 @@ CGHeroInstance::CGHeroInstance():
 	portrait(UNINITIALIZED_PORTRAIT),
 	level(1),
 	exp(UNINITIALIZED_EXPERIENCE),
-	sex(std::numeric_limits<ui8>::max())
+	sex(std::numeric_limits<ui8>::max()),
+	lowestCreatureSpeed(0)
 {
 	setNodeType(HERO);
 	ID = Obj::HERO;
@@ -311,6 +307,20 @@ void CGHeroInstance::initHero(CRandomGenerator & rand)
 		levelUpAutomatically(rand);
 	}
 
+	// load base hero bonuses, TODO: per-map loading of base hero bonuses
+	// must be done separately from global bonuses since recruitable heroes in taverns 
+	// are not attached to global bonus node but need access to some global bonuses
+	// e.g. MANA_PER_KNOWLEDGE for correct preview and initial state after recruit	for(const auto & ob : VLC->modh->heroBaseBonuses)
+	// or MOVEMENT to compute initial movement before recruiting is finished
+	for(const auto & ob : VLC->modh->heroBaseBonuses)
+	{
+		auto bonus = ob;
+		bonus->source = Bonus::HERO_BASE_SKILL;
+		bonus->sid = id.getNum();
+		bonus->duration = Bonus::PERMANENT;
+		addNewBonus(bonus);
+	}
+
 	if (VLC->modh->modules.COMMANDERS && !commander)
 	{
 		commander = new CCommanderInstance(type->heroClass->commander->idNumber);
@@ -509,10 +519,6 @@ void CGHeroInstance::initObj(CRandomGenerator & rand)
 	//copy active (probably growing) bonuses from hero prototype to hero object
 	for(const std::shared_ptr<Bonus> & b : type->specialty)
 		addNewBonus(b);
-	//dito for old-style bonuses -> compatibility for old savegames
-	for(SSpecialtyBonus & sb : type->specialtyDeprecated)
-		for(const std::shared_ptr<Bonus> & b : sb.bonuses)
-			addNewBonus(b);
 	for(SSpecialtyInfo & spec : type->specDeprecated)
 		for(const std::shared_ptr<Bonus> & b : SpecialtyInfoToBonuses(spec, type->getIndex()))
 			addNewBonus(b);
@@ -571,7 +577,7 @@ ui64 CGHeroInstance::getTotalStrength() const
 
 TExpType CGHeroInstance::calculateXp(TExpType exp) const
 {
-	return static_cast<TExpType>(exp * (100 + valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::LEARNING)) / 100.0);
+	return static_cast<TExpType>(exp * (valOfBonuses(Bonus::HERO_EXPERIENCE_GAIN_PERCENT)) / 100.0);
 }
 
 int32_t CGHeroInstance::getCasterUnitId() const
@@ -585,9 +591,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t
 
 	spell->forEachSchool([&, this](const spells::SchoolInfo & cnf, bool & stop)
 	{
-		int32_t thisSchool = std::max<int32_t>(
-			valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, cnf.skill),
-			valOfBonuses(Bonus::MAGIC_SCHOOL_SKILL, 1 << (static_cast<ui8>(cnf.id)))); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies)
+		int32_t thisSchool = valOfBonuses(Bonus::MAGIC_SCHOOL_SKILL, 1 << (static_cast<ui8>(cnf.id))); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies)
 		if(thisSchool > skill)
 		{
 			skill = thisSchool;
@@ -608,8 +612,8 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base,
 {
 	//applying sorcery secondary skill
 
-	base = static_cast<int64_t>(base * (100 + valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SORCERY)) / 100.0);
-	base = static_cast<int64_t>(base * (100 + valOfBonuses(Bonus::SPELL_DAMAGE) + valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0);
+	base = static_cast<int64_t>(base * (valOfBonuses(Bonus::SPELL_DAMAGE)) / 100.0);
+	base = static_cast<int64_t>(base * (100 + valOfBonuses(Bonus::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0);
 
 	int maxSchoolBonus = 0;
 
@@ -772,23 +776,27 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const
  */
 CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &battleResult) const
 {
-	const ui8 necromancyLevel = getSecSkillLevel(SecondarySkill::NECROMANCY);
+	bool hasImprovedNecromancy = hasBonusOfType(Bonus::IMPROVED_NECROMANCY);
+
 	// need skill or cloak of undead king - lesser artifacts don't work without skill
-	if (necromancyLevel > 0 || hasBonusOfType(Bonus::IMPROVED_NECROMANCY))
+	if (hasImprovedNecromancy)
 	{
-		double necromancySkill = valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::NECROMANCY) / 100.0;
+		double necromancySkill = valOfBonuses(Bonus::UNDEAD_RAISE_PERCENTAGE) / 100.0;
+		const ui8 necromancyLevel = valOfBonuses(Bonus::IMPROVED_NECROMANCY);
 		vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all...
 		const std::map<ui32,si32> &casualties = battleResult.casualties[!battleResult.winner];
 		// figure out what to raise - pick strongest creature meeting requirements
-		CreatureID creatureTypeRaised = CreatureID::SKELETON;
+		auto creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode
 		int requiredCasualtyLevel = 1;
 		TConstBonusListPtr improvedNecromancy = getBonuses(Selector::type()(Bonus::IMPROVED_NECROMANCY));
 		if(!improvedNecromancy->empty())
 		{
-			auto getCreatureID = [necromancyLevel](const std::shared_ptr<Bonus> & bonus) -> CreatureID
+			auto getCreatureID = [](const std::shared_ptr<Bonus> & bonus) -> CreatureID
 			{
-				const CreatureID legacyTypes[] = {CreatureID::SKELETON, CreatureID::WALKING_DEAD, CreatureID::WIGHTS, CreatureID::LICHES};
-				return CreatureID(bonus->subtype >= 0 ? bonus->subtype : legacyTypes[necromancyLevel]);
+				assert(bonus->subtype >=0);
+				if(bonus->subtype >= 0)
+					return CreatureID(bonus->subtype);
+				return CreatureID::NONE;
 			};
 			int maxCasualtyLevel = 1;
 			for(const auto & casualty : casualties)
@@ -821,6 +829,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 				requiredCasualtyLevel = std::max(topPick->additionalInfo[1], 1);
 			}
 		}
+		assert(creatureTypeRaised != CreatureID::NONE);
 		// raise upgraded creature (at 2/3 rate) if no space available otherwise
 		if(getSlotFor(creatureTypeRaised) == SlotID())
 		{
@@ -885,7 +894,7 @@ int3 CGHeroInstance::getSightCenter() const
 
 int CGHeroInstance::getSightRadius() const
 {
-	return 5 + valOfBonuses(Bonus::SIGHT_RADIOUS); // scouting gives SIGHT_RADIUS bonus
+	return valOfBonuses(Bonus::SIGHT_RADIUS); // scouting gives SIGHT_RADIUS bonus
 }
 
 si32 CGHeroInstance::manaRegain() const
@@ -893,7 +902,7 @@ si32 CGHeroInstance::manaRegain() const
 	if (hasBonusOfType(Bonus::FULL_MANA_REGENERATION))
 		return manaLimit();
 
-	return 1 + valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::MYSTICISM) + valOfBonuses(Bonus::MANA_REGENERATION); //1 + Mysticism level
+	return valOfBonuses(Bonus::MANA_REGENERATION);
 }
 
 si32 CGHeroInstance::getManaNewTurn() const
@@ -973,6 +982,12 @@ std::string CGHeroInstance::nodeName() const
 	return "Hero " + getNameTextID();
 }
 
+si32 CGHeroInstance::manaLimit() const
+{
+	return si32(getPrimSkillLevel(PrimarySkill::KNOWLEDGE)
+		* (valOfBonuses(Bonus::MANA_PER_KNOWLEDGE)));
+}
+
 std::string CGHeroInstance::getNameTranslated() const
 {
 	if (!nameCustom.empty())
@@ -1059,7 +1074,7 @@ const std::set<SpellID> & CGHeroInstance::getSpellsInSpellbook() const
 
 int CGHeroInstance::maxSpellLevel() const
 {
-	return std::min(GameConstants::SPELL_LEVELS, 2 + valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::WISDOM)));
+	return std::min(GameConstants::SPELL_LEVELS, valOfBonuses(Selector::type()(Bonus::MAX_LEARNABLE_SPELL_LEVEL)));
 }
 
 void CGHeroInstance::deserializationFix()
@@ -1135,28 +1150,41 @@ ArtBearer::ArtBearer CGHeroInstance::bearerType() const
 std::vector<SecondarySkill> CGHeroInstance::getLevelUpProposedSecondarySkills() const
 {
 	std::vector<SecondarySkill> obligatorySkills; //hero is offered magic school or wisdom if possible
-	if (!skillsInfo.wisdomCounter)
-	{
-		if (canLearnSkill(SecondarySkill::WISDOM))
-			obligatorySkills.emplace_back(SecondarySkill::WISDOM);
-	}
-	if (!skillsInfo.magicSchoolCounter)
-	{
-		std::vector<SecondarySkill> ss =
-		{
-			SecondarySkill::FIRE_MAGIC, SecondarySkill::AIR_MAGIC, SecondarySkill::WATER_MAGIC, SecondarySkill::EARTH_MAGIC
-		};
 
+	auto getObligatorySkills = [](CSkill::Obligatory obl){
+		std::vector<SecondarySkill> obligatory = {};
+		for(int i = 0; i < VLC->skillh->size(); i++)
+			if((*VLC->skillh)[SecondarySkill(i)]->obligatory(obl))
+			{
+				obligatory.emplace_back(i);
+				break;
+			}
+		return obligatory;
+	};
+
+	auto selectObligatorySkill = [&](std::vector<SecondarySkill>& ss) -> void
+	{
 		std::shuffle(ss.begin(), ss.end(), skillsInfo.rand.getStdGenerator());
 
 		for(const auto & skill : ss)
 		{
-			if (canLearnSkill(skill)) //only schools hero doesn't know yet
+			if (canLearnSkill(skill)) //only skills hero doesn't know yet
 			{
 				obligatorySkills.push_back(skill);
 				break; //only one
 			}
 		}
+	};
+
+	if (!skillsInfo.wisdomCounter)
+	{
+		auto obligatory = getObligatorySkills(CSkill::Obligatory::MAJOR);
+		selectObligatorySkill(obligatory);
+	}
+	if (!skillsInfo.magicSchoolCounter)
+	{
+		auto obligatory = getObligatorySkills(CSkill::Obligatory::MINOR);
+		selectObligatorySkill(obligatory);
 	}
 
 	std::vector<SecondarySkill> skills;
@@ -1329,20 +1357,12 @@ void CGHeroInstance::levelUp(const std::vector<SecondarySkill> & skills)
 	//deterministic secondary skills
 	skillsInfo.magicSchoolCounter = (skillsInfo.magicSchoolCounter + 1) % maxlevelsToMagicSchool();
 	skillsInfo.wisdomCounter = (skillsInfo.wisdomCounter + 1) % maxlevelsToWisdom();
-	if(vstd::contains(skills, SecondarySkill::WISDOM))
+	for(const auto & skill : skills)
 	{
-		skillsInfo.resetWisdomCounter();
-	}
-
-	SecondarySkill spellSchools[] = {
-		SecondarySkill::FIRE_MAGIC, SecondarySkill::AIR_MAGIC, SecondarySkill::WATER_MAGIC, SecondarySkill::EARTH_MAGIC};
-	for(const auto & skill : spellSchools)
-	{
-		if(vstd::contains(skills, skill))
-		{
+		if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MAJOR))
+			skillsInfo.resetWisdomCounter();
+		if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MINOR))
 			skillsInfo.resetMagicSchoolCounter();
-			break;
-		}
 	}
 
 	//update specialty and other bonuses that scale with level

+ 5 - 1
lib/mapObjects/CGHeroInstance.h

@@ -49,6 +49,7 @@ class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator,
 
 private:
 	std::set<SpellID> spells; //known spells (spell IDs)
+	mutable int lowestCreatureSpeed;
 
 public:
 
@@ -170,7 +171,7 @@ public:
 
 	ui32 getTileCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const; //move cost - applying pathfinding skill, road and terrain modifiers. NOT includes diagonal move penalty, last move levelling
 	TerrainId getNativeTerrain() const;
-	ui32 getLowestCreatureSpeed() const;
+	int getLowestCreatureSpeed() const;
 	si32 manaRegain() const; //how many points of mana can hero regain "naturally" in one day
 	si32 getManaNewTurn() const; //calculate how much mana this hero is going to have the next day
 	int getCurrentLuck(int stack=-1, bool town=false) const;
@@ -210,6 +211,8 @@ public:
 	int maxMovePoints(bool onLand) const;
 	//cached version is much faster, TurnInfo construction is costly
 	int maxMovePointsCached(bool onLand, const TurnInfo * ti) const;
+	//update army movement bonus
+	void updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const;
 
 	int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const;
 
@@ -256,6 +259,7 @@ public:
 	///IBonusBearer
 	CBonusSystemNode & whereShouldBeAttached(CGameState * gs) override;
 	std::string nodeName() const override;
+	si32 manaLimit() const override;
 
 	CBonusSystemNode * whereShouldBeAttachedOnSiege(const bool isBattleOutsideTown) const;
 	CBonusSystemNode * whereShouldBeAttachedOnSiege(CGameState * gs);

+ 1 - 8
lib/mapObjects/CGTownInstance.cpp

@@ -1225,13 +1225,6 @@ void CGTownInstance::recreateBuildingsBonuses()
 
 		for(auto & bonus : building->buildingBonuses)
 		{
-			if(bonus->limiter && bonus->effectRange == Bonus::ONLY_ENEMY_ARMY) //ONLY_ENEMY_ARMY is only mark for OppositeSide limiter to avoid extra dynamic_cast.
-			{
-				auto bCopy = std::make_shared<Bonus>(*bonus); //just a copy of the shared_ptr has been changed and reassigned.
-				bCopy->limiter = std::make_shared<OppositeSideLimiter>(this->getOwner());
-				addNewBonus(bCopy);
-				continue;
-			}
 			if(bonus->propagator != nullptr && bonus->propagator->getPropagatorType() == ALL_CREATURES)
 				VLC->creh->addBonusForAllCreatures(bonus);
 			else
@@ -1676,7 +1669,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 			if(!h->hasBonusFrom(Bonus::OBJECT, Obj::STABLES)) //does not stack with advMap Stables
 			{
 				GiveBonus gb;
-				gb.bonus = Bonus(Bonus::ONE_WEEK, Bonus::LAND_MOVEMENT, Bonus::OBJECT, 600, 94, VLC->generaltexth->arraytxt[100]);
+				gb.bonus = Bonus(Bonus::ONE_WEEK, Bonus::MOVEMENT, Bonus::OBJECT, 600, 94, VLC->generaltexth->arraytxt[100], 1);
 				gb.id = heroID.getNum();
 				cb->giveHeroBonus(&gb);
 

+ 3 - 2
lib/mapObjects/MiscObjects.cpp

@@ -311,7 +311,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
 	if(count*2 > totalCount)
 		sympathy++; // 2 - hero have similar creatures more that 50%
 
-	int diplomacy = h->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::DIPLOMACY);
+	int diplomacy = h->valOfBonuses(Bonus::WANDERING_CREATURES_JOIN_BONUS);
 	int charisma = powerFactor + diplomacy + sympathy;
 
 	if(charisma < character)
@@ -2154,12 +2154,13 @@ void CGLighthouse::initObj(CRandomGenerator & rand)
 void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const
 {
 	GiveBonus gb(GiveBonus::PLAYER);
-	gb.bonus.type = Bonus::SEA_MOVEMENT;
+	gb.bonus.type = Bonus::MOVEMENT;
 	gb.bonus.val = 500;
 	gb.id = player.getNum();
 	gb.bonus.duration = Bonus::PERMANENT;
 	gb.bonus.source = Bonus::OBJECT;
 	gb.bonus.sid = id.getNum();
+	gb.bonus.subtype = 1;
 
 	// FIXME: This is really dirty hack
 	// Proper fix would be to make CGLighthouse into bonus system node

+ 1 - 0
lib/registerTypes/RegisterTypes.h

@@ -137,6 +137,7 @@ void registerTypesMapObjectTypes(Serializer &s)
 	s.template registerType<IUpdater, TimesHeroLevelUpdater>();
 	s.template registerType<IUpdater, TimesStackLevelUpdater>();
 	s.template registerType<IUpdater, OwnerUpdater>();
+	s.template registerType<IUpdater, ArmyMovementUpdater>();
 
 	s.template registerType<ILimiter, AnyOfLimiter>();
 	s.template registerType<ILimiter, NoneOfLimiter>();

+ 2 - 2
lib/serializer/CSerializer.h

@@ -14,8 +14,8 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-const ui32 SERIALIZATION_VERSION = 818;
-const ui32 MINIMAL_SERIALIZATION_VERSION = 818;
+const ui32 SERIALIZATION_VERSION = 819;
+const ui32 MINIMAL_SERIALIZATION_VERSION = 819;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 
 class CHero;

+ 72 - 18
lib/spells/TargetCondition.cpp

@@ -57,22 +57,34 @@ protected:
 	virtual bool check(const Mechanics * m, const battle::Unit * target) const = 0;
 };
 
-class BonusCondition : public TargetConditionItemBase
+class SelectorCondition : public TargetConditionItemBase
 {
 public:
-	BonusCondition(BonusTypeID type_)
-		: type(type_)
+	SelectorCondition(const CSelector & csel):
+		sel(csel)
+	{
+	}
+	SelectorCondition(const CSelector & csel, si32 minVal, si32 maxVal):
+		sel(csel),
+		minVal(minVal),
+		maxVal(maxVal)
 	{
 	}
 
 protected:
 	bool check(const Mechanics * m, const battle::Unit * target) const override
 	{
-		return target->hasBonus(Selector::type()(type));
+		if(target->hasBonus(sel)) {
+			auto b = target->valOfBonuses(sel,"");
+			return b >= minVal && b <= maxVal;
+		}
+		return false;
 	}
 
 private:
-	BonusTypeID type;
+	CSelector sel;
+	si32 minVal = std::numeric_limits<si32>::min();
+	si32 maxVal = std::numeric_limits<si32>::max();
 };
 
 class CreatureCondition : public TargetConditionItemBase
@@ -318,8 +330,16 @@ public:
 
 			auto it = bonusNameMap.find(identifier);
 			if(it != bonusNameMap.end())
-				return std::make_shared<BonusCondition>(it->second);
-			else
+				return std::make_shared<SelectorCondition>(Selector::type()(it->second));
+			
+			auto params = BonusParams(identifier, "", -1);
+			if(params.isConverted)
+			{
+				if(params.valRelevant)
+					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);
 		}
 		else if(type == "creature")
@@ -352,6 +372,26 @@ public:
 		return Object();
 	}
 
+	Object createFromJsonStruct(const JsonNode & jsonStruct) const override
+	{	
+		auto type = jsonStruct["type"].String();
+		auto parameters = jsonStruct["parameters"];
+		if(type == "selector")
+		{
+			auto minVal = std::numeric_limits<si32>::min();
+			auto maxVal = std::numeric_limits<si32>::max();
+			if(parameters["minVal"].isNumber())
+				minVal = parameters["minVal"].Integer();
+			if(parameters["maxVal"].isNumber())
+				maxVal = parameters["maxVal"].Integer();
+			auto sel = JsonUtils::parseSelector(parameters);
+			return std::make_shared<SelectorCondition>(sel, minVal, maxVal);
+		}
+
+		logMod->error("Invalid type %s in spell target condition.", type);
+		return Object();
+	}
+
 	Object createReceptiveFeature() const override
 	{
 		static std::shared_ptr<TargetConditionItem> condition = std::make_shared<ReceptiveFeatureCondition>();
@@ -389,6 +429,7 @@ bool TargetCondition::isReceptive(const Mechanics * m, const battle::Unit * targ
 
 void TargetCondition::serializeJson(JsonSerializeFormat & handler, const ItemFactory * itemFactory)
 {
+	bool isNonMagical = false;
 	if(handler.saving)
 	{
 		logGlobal->error("Spell target condition saving is not supported");
@@ -399,13 +440,18 @@ void TargetCondition::serializeJson(JsonSerializeFormat & handler, const ItemFac
 	normal.clear();
 	negation.clear();
 
-	absolute.push_back(itemFactory->createAbsoluteLevel());
 	absolute.push_back(itemFactory->createAbsoluteSpell());
-	normal.push_back(itemFactory->createElemental());
-	normal.push_back(itemFactory->createNormalLevel());
-	normal.push_back(itemFactory->createNormalSpell());
-	negation.push_back(itemFactory->createReceptiveFeature());
-	negation.push_back(itemFactory->createImmunityNegation());
+
+	handler.serializeBool("nonMagical", isNonMagical);
+	if(!isNonMagical)
+	{
+		absolute.push_back(itemFactory->createAbsoluteLevel());
+		normal.push_back(itemFactory->createElemental());
+		normal.push_back(itemFactory->createNormalLevel());
+		normal.push_back(itemFactory->createNormalSpell());
+		negation.push_back(itemFactory->createReceptiveFeature());
+		negation.push_back(itemFactory->createImmunityNegation());
+	}
 
 	{
 		auto anyOf = handler.enterStruct("anyOf");
@@ -457,16 +503,24 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo
 			isAbsolute = true;
 		else if(value.String() == "normal")
 			isAbsolute = false;
+		else if(value.isStruct()) //assume conditions have a new struct format
+			isAbsolute = value["absolute"].Bool();
 		else
 			continue;
 
-		std::string scope;
-		std::string type;
-		std::string identifier;
+		std::shared_ptr<TargetConditionItem> item;
+		if(value.isStruct())
+			item = itemFactory->createFromJsonStruct(value);
+		else
+		{
+			std::string scope;
+			std::string type;
+			std::string identifier;
 
-		CModHandler::parseIdentifier(keyValue.first, scope, type, identifier);
+			CModHandler::parseIdentifier(keyValue.first, scope, type, identifier);
 
-		std::shared_ptr<TargetConditionItem> item = itemFactory->createConfigurable(scope, type, identifier);
+			std::shared_ptr<TargetConditionItem> item = itemFactory->createConfigurable(scope, type, identifier);
+		}
 
 		if(item)
 		{

+ 1 - 0
lib/spells/TargetCondition.h

@@ -51,6 +51,7 @@ public:
 	virtual Object createNormalSpell() const = 0;
 
 	virtual Object createConfigurable(std::string scope, std::string type, std::string identifier) const = 0;
+	virtual Object createFromJsonStruct(const JsonNode & jsonStruct) const = 0;
 
 	virtual Object createReceptiveFeature() const = 0;
 	virtual Object createImmunityNegation() const = 0;

+ 141 - 29
lib/spells/effects/Catapult.cpp

@@ -57,38 +57,26 @@ bool Catapult::applicable(Problem & problem, const Mechanics * m) const
 	return !attackableBattleHexes.empty() || m->adaptProblem(ESpellCastProblem::NO_APPROPRIATE_TARGET, problem);
 }
 
-void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & /* eTarget */) const
+void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & eTarget) const
 {
-	//start with all destructible parts
-	static const std::set<EWallPart> potentialTargets =
-	{
-		EWallPart::KEEP,
-		EWallPart::BOTTOM_TOWER,
-		EWallPart::BOTTOM_WALL,
-		EWallPart::BELOW_GATE,
-		EWallPart::OVER_GATE,
-		EWallPart::UPPER_WALL,
-		EWallPart::UPPER_TOWER,
-		EWallPart::GATE
-	};
-
-	assert(potentialTargets.size() == size_t(EWallPart::PARTS_COUNT));
+	if(m->isMassive())
+		applyMassive(server, m); // Like earthquake
+	else
+		applyTargeted(server, m, eTarget); // Like catapult shots
+}
 
-	std::set<EWallPart> allowedTargets;
 
-	for (auto const & target : potentialTargets)
-	{
-		auto state = m->battle()->battleGetWallState(target);
+void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const
+{
+	//start with all destructible parts
+	std::vector<EWallPart> allowedTargets = getPotentialTargets(m, true, true);
 
-		if(state != EWallState::DESTROYED && state != EWallState::NONE)
-			allowedTargets.insert(target);
-	}
 	assert(!allowedTargets.empty());
 	if (allowedTargets.empty())
 		return;
 
 	CatapultAttack ca;
-	ca.attacker = -1;
+	ca.attacker = m->caster->getCasterUnitId();
 
 	for(int i = 0; i < targetsToAttack; i++)
 	{
@@ -97,7 +85,6 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
 		// Potential overshots (more hits on same targets than remaining HP) are allowed
 		EWallPart target = *RandomGeneratorUtil::nextItem(allowedTargets, *server->getRNG());
 
-
 		auto attackInfo = ca.attackedParts.begin();
 		for ( ; attackInfo != ca.attackedParts.end(); ++attackInfo)
 			if ( attackInfo->attackedPart == target )
@@ -105,8 +92,8 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
 
 		if (attackInfo == ca.attackedParts.end()) // new part
 		{
-			CatapultAttack::AttackInfo newInfo{};
-			newInfo.damageDealt = 1;
+			CatapultAttack::AttackInfo newInfo;
+			newInfo.damageDealt = getRandomDamage(server);
 			newInfo.attackedPart = target;
 			newInfo.destinationTile = m->battle()->wallPartToBattleHex(target);
 			ca.attackedParts.push_back(newInfo);
@@ -114,12 +101,96 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
 		}
 		else // already damaged before, update damage
 		{
-			attackInfo->damageDealt += 1;
+			attackInfo->damageDealt += getRandomDamage(server);
 		}
 	}
-
 	server->apply(&ca);
 
+	removeTowerShooters(server, m);
+}
+
+void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
+{
+	assert(!target.empty());
+	auto destination = target.at(0).hexValue;
+	auto desiredTarget = m->battle()->battleHexToWallPart(destination);
+
+	for(int i = 0; i < targetsToAttack; i++)
+	{
+		auto actualTarget = EWallPart::INVALID;
+
+		if ( m->battle()->isWallPartAttackable(desiredTarget) &&
+				server->getRNG()->getInt64Range(0, 99)() < getCatapultHitChance(desiredTarget))
+		{
+			actualTarget = desiredTarget;
+		}
+		else
+		{
+			std::vector<EWallPart> potentialTargets = getPotentialTargets(m, false, false);
+
+			if (potentialTargets.empty())
+				break; // everything is gone, can't attack anymore
+
+			actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, *server->getRNG());
+		}
+		assert(actualTarget != EWallPart::INVALID);
+
+		CatapultAttack::AttackInfo attack;
+		attack.attackedPart = actualTarget;
+		attack.destinationTile = m->battle()->wallPartToBattleHex(actualTarget);
+		attack.damageDealt = getRandomDamage(server);
+
+		CatapultAttack ca; //package for clients
+		ca.attacker = m->caster->getCasterUnitId();
+		ca.attackedParts.push_back(attack);
+		server->apply(&ca);
+		removeTowerShooters(server, m);
+	}
+}
+
+int Catapult::getCatapultHitChance(EWallPart part) const
+{
+	switch(part)
+	{
+	case EWallPart::GATE:
+		return gate;
+	case EWallPart::KEEP:
+		return keep;
+	case EWallPart::BOTTOM_TOWER:
+	case EWallPart::UPPER_TOWER:
+		return tower;
+	case EWallPart::BOTTOM_WALL:
+	case EWallPart::BELOW_GATE:
+	case EWallPart::OVER_GATE:
+	case EWallPart::UPPER_WALL:
+		return wall;
+	default:
+		return 0;
+	}
+}
+
+int Catapult::getRandomDamage (ServerCallback * server) const
+{
+	std::array<int, 3> damageChances = { noDmg, hit, crit }; //dmgChance[i] - chance for doing i dmg when hit is successful
+	int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
+	int damageRandom = server->getRNG()->getInt64Range(0, totalChance - 1)();
+	int dealtDamage = 0;
+
+	//calculating dealt damage
+	for (int damage = 0; damage < damageChances.size(); ++damage)
+	{
+		if (damageRandom <= damageChances[damage])
+		{
+			dealtDamage = damage;
+			break;
+		}
+		damageRandom -= damageChances[damage];
+	}
+	return dealtDamage;
+}
+
+void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) const
+{
 	BattleUnitsChanged removeUnits;
 
 	for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER })
@@ -158,10 +229,51 @@ void Catapult::apply(ServerCallback * server, const Mechanics * m, const EffectT
 		server->apply(&removeUnits);
 }
 
+std::vector<EWallPart> Catapult::getPotentialTargets(const Mechanics * m, bool bypassGateCheck, bool bypassTowerCheck) const
+{
+	std::vector<EWallPart> potentialTargets;
+	constexpr std::array<EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
+	constexpr std::array<EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
+	constexpr EWallPart gates = EWallPart::GATE;
+
+	// in H3, catapult under automatic control will attack objects in following order:
+	// walls, gates, towers
+	for (auto & part : walls)
+		if (m->battle()->isWallPartAttackable(part))
+			potentialTargets.push_back(part);
+
+	if ((potentialTargets.empty() || bypassGateCheck) && (m->battle()->isWallPartAttackable(gates)))
+		potentialTargets.push_back(gates);
+
+	if (potentialTargets.empty() || bypassTowerCheck)
+		for (auto & part : towers)
+			if (m->battle()->isWallPartAttackable(part))
+				potentialTargets.push_back(part);
+
+	return potentialTargets;
+}
+
+void Catapult::adjustHitChance()
+{
+	vstd::abetween(keep, 0, 100);
+	vstd::abetween(tower, 0, 100);
+	vstd::abetween(gate, 0, 100);
+	vstd::abetween(wall, 0, 100);
+	vstd::abetween(crit, 0, 100);
+	vstd::abetween(hit, 0, 100 - crit);
+	vstd::amin(noDmg, 100 - hit - crit);
+}
+
 void Catapult::serializeJsonEffect(JsonSerializeFormat & handler)
 {
-	//TODO: add configuration unifying with Catapult ability
 	handler.serializeInt("targetsToAttack", targetsToAttack);
+	handler.serializeInt("chanceToHitKeep", keep);
+	handler.serializeInt("chanceToHitGate", gate);
+	handler.serializeInt("chanceToHitTower", tower);
+	handler.serializeInt("chanceToHitWall", wall);
+	handler.serializeInt("chanceToNormalHit", hit);
+	handler.serializeInt("chanceToCrit", crit);
+	adjustHitChance();
 }
 
 

+ 17 - 0
lib/spells/effects/Catapult.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "LocationEffect.h"
+#include "../../GameConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -29,6 +30,22 @@ protected:
 	void serializeJsonEffect(JsonSerializeFormat & handler) override;
 private:
 	int targetsToAttack = 0;
+	//Ballistics percentage
+	int gate = 0;
+	int keep = 0;
+	int tower = 0;
+	int wall = 0;
+	//Damage percentage, used for both ballistics and earthquake
+	int hit = 0;
+	int crit = 0;
+	int noDmg = 0;
+	int getCatapultHitChance(EWallPart part) const;
+	int getRandomDamage(ServerCallback * server) const;
+	void adjustHitChance();
+	void applyMassive(ServerCallback * server, const Mechanics * m) const;
+	void applyTargeted(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const;
+	void removeTowerShooters(ServerCallback * server, const Mechanics * m) const;
+	std::vector<EWallPart> getPotentialTargets(const Mechanics * m, bool bypassGateCheck, bool bypassTowerCheck) const;
 };
 
 }

+ 11 - 0
lib/spells/effects/Heal.cpp

@@ -15,6 +15,7 @@
 
 #include "../../NetPacks.h"
 #include "../../battle/IBattleState.h"
+#include "../../battle/CUnitState.h"
 #include "../../battle/CBattleInfoCallback.h"
 #include "../../battle/Unit.h"
 #include "../../serializer/JsonSerializeFormat.h"
@@ -128,6 +129,16 @@ void Heal::prepareHealEffect(int64_t value, BattleUnitsChanged & pack, BattleLog
 				resurrectText.addReplacement(resurrectedCount);
 				logMessage.lines.push_back(std::move(resurrectText));
 			}
+			else if (unitHPgained > 0 && m->caster->getCasterUnitId() >= 0) //Show text about healed HP if healed by unit
+			{
+				MetaString healText;
+				auto casterUnit = dynamic_cast<const battle::CUnitState*>(m->caster)->acquire();
+				healText.addTxt(MetaString::GENERAL_TXT, 414);
+				casterUnit->addNameReplacement(healText, false);
+				state->addNameReplacement(healText, false);
+				healText.addReplacement((int)unitHPgained);
+				logMessage.lines.push_back(std::move(healText));
+			}
 
 			if(unitHPgained > 0)
 			{

+ 0 - 8
lib/spells/effects/Timed.cpp

@@ -197,14 +197,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 			}
 		}
 
-		if(casterHero && casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, m->getSpellIndex())) //TODO: better handling of bonus percentages
-		{
-			int damagePercent = casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, m->getSpellIndex()) / tier;
-			Bonus specialBonus(Bonus::N_TURNS, Bonus::GENERAL_DAMAGE_PREMY, Bonus::SPELL_EFFECT, damagePercent, m->getSpellIndex());
-			specialBonus.turnsRemain = duration;
-			buffer.push_back(specialBonus);
-		}
-
 		if(cumulative)
 			sse.toAdd.emplace_back(affected->unitId(), buffer);
 		else

+ 51 - 227
server/CGameHandler.cpp

@@ -682,11 +682,8 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh
 	sss.abs = abs;
 	sendAndApply(&sss);
 
-	if (which == SecondarySkill::WISDOM)
-	{
-		if (hero->visitedTown)
-			giveSpells(hero->visitedTown, hero);
-	}
+	if (hero->visitedTown)
+		giveSpells(hero->visitedTown, hero);
 }
 
 void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender)
@@ -743,9 +740,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 
 	if (finishingBattle->winnerHero)
 	{
-		if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_VAL2, SecondarySkill::EAGLE_EYE))
+		if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(Bonus::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1))
 		{
-			double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::EAGLE_EYE);
+			double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::LEARN_BATTLE_SPELL_CHANCE, 0);
 			for(auto & spellId : gs->curB->sides.at(!battleResult.data->winner).usedSpellsHistory)
 			{
 				auto spell = spellId.toSpell(VLC->spells());
@@ -1028,20 +1025,15 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 
 	const int attackerLuck = attacker->LuckVal();
 
-	auto sideHeroBlocksLuck = [](const SideInBattle &side){ return NBonus::hasOfType(side.hero, Bonus::BLOCK_LUCK); };
-
-	if (!vstd::contains_if (gs->curB->sides, sideHeroBlocksLuck))
+	if (attackerLuck > 0  && getRandomGenerator().nextInt(23) < attackerLuck)
 	{
-		if (attackerLuck > 0  && getRandomGenerator().nextInt(23) < attackerLuck)
+		bat.flags |= BattleAttack::LUCKY;
+	}
+	if (VLC->modh->settings.data["hardcodedFeatures"]["NEGATIVE_LUCK"].Bool()) // negative luck enabled
+	{
+		if (attackerLuck < 0 && getRandomGenerator().nextInt(23) < abs(attackerLuck))
 		{
-			bat.flags |= BattleAttack::LUCKY;
-		}
-		if (VLC->modh->settings.data["hardcodedFeatures"]["NEGATIVE_LUCK"].Bool()) // negative luck enabled
-		{
-			if (attackerLuck < 0 && getRandomGenerator().nextInt(23) < abs(attackerLuck))
-			{
-				bat.flags |= BattleAttack::UNLUCKY;
-			}
+			bat.flags |= BattleAttack::UNLUCKY;
 		}
 	}
 
@@ -1050,14 +1042,12 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 		bat.flags |= BattleAttack::DEATH_BLOW;
 	}
 
-	if (attacker->getCreature()->idNumber == CreatureID::BALLISTA)
+	const auto * owner = gs->curB->getHero(attacker->owner);
+	if(owner)
 	{
-		const CGHeroInstance * owner = gs->curB->getHero(attacker->owner);
-		int chance = owner->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ARTILLERY);
+		int chance = owner->valOfBonuses(Bonus::BONUS_DAMAGE_CHANCE, attacker->creatureIndex());
 		if (chance > getRandomGenerator().nextInt(99))
-		{
 			bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG;
-		}
 	}
 
 	int64_t drainedLife = 0;
@@ -1937,8 +1927,6 @@ void CGameHandler::newTurn()
 
 			if (!firstTurn) //not first day
 			{
-				n.res[elem.first][Res::GOLD] += h->valOfBonuses(Selector::typeSubtype(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::ESTATES)); //estates
-
 				for (int k = 0; k < GameConstants::RESOURCE_QUANTITY; k++)
 				{
 					n.res[elem.first][k] += h->valOfBonuses(Bonus::GENERATE_RESOURCE, k);
@@ -2781,8 +2769,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t
 {
 	const CGHeroInstance * h1 = getHero(fromHero);
 	const CGHeroInstance * h2 = getHero(toHero);
-	int h1_scholarSpellLevel = h1->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SCHOLAR);
-	int h2_scholarSpellLevel = h2->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::SCHOLAR);
+	int h1_scholarSpellLevel = h1->valOfBonuses(Bonus::LEARN_MEETING_SPELL_LIMIT, -1);
+	int h2_scholarSpellLevel = h2->valOfBonuses(Bonus::LEARN_MEETING_SPELL_LIMIT, -1);
 
 	if (h1_scholarSpellLevel < h2_scholarSpellLevel)
 	{
@@ -4750,6 +4738,14 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			//attack
 			int totalAttacks = stack->totalAttacks.getMeleeValue();
 
+			//TODO: move to CUnitState
+			const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side);
+			if(attackingHero)
+			{
+				totalAttacks += attackingHero->valOfBonuses(Bonus::HERO_GRANTS_ATTACKS, stack->creatureIndex());
+			}
+
+
 			const bool firstStrike = destinationStack->hasBonusOfType(Bonus::FIRST_STRIKE);
 			const bool retaliation = destinationStack->ableToRetaliate();
 			for (int i = 0; i < totalAttacks; ++i)
@@ -4826,25 +4822,17 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			{
 				makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true);
 			}
+			//allow more than one additional attack
+
+			int totalRangedAttacks = stack->totalAttacks.getRangedValue();
 
 			//TODO: move to CUnitState
-			//extra shot(s) for ballista, based on artillery skill
-			if(stack->creatureIndex() == CreatureID::BALLISTA)
+			const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side);
+			if(attackingHero)
 			{
-				const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
-
-				if(attackingHero)
-				{
-					int ballistaBonusAttacks = attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_VAL2, SecondarySkill::ARTILLERY);
-					while(destinationStack->alive() && ballistaBonusAttacks-- > 0)
-					{
-						makeAttack(stack, destinationStack, 0, destination, false, true, false);
-					}
-				}
+				totalRangedAttacks += attackingHero->valOfBonuses(Bonus::HERO_GRANTS_ATTACKS, stack->creatureIndex());
 			}
-			//allow more than one additional attack
 
-			int totalRangedAttacks = stack->totalAttacks.getRangedValue();
 
 			for(int i = 1; i < totalRangedAttacks; ++i)
 			{
@@ -4861,150 +4849,20 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 		}
 	case EActionType::CATAPULT:
 		{
-			//TODO: unify with spells::effects:Catapult
-			auto getCatapultHitChance = [](EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int
-			{
-				switch(part)
-				{
-				case EWallPart::GATE:
-					return sbi.gate;
-				case EWallPart::KEEP:
-					return sbi.keep;
-				case EWallPart::BOTTOM_TOWER:
-				case EWallPart::UPPER_TOWER:
-					return sbi.tower;
-				case EWallPart::BOTTOM_WALL:
-				case EWallPart::BELOW_GATE:
-				case EWallPart::OVER_GATE:
-				case EWallPart::UPPER_WALL:
-					return sbi.wall;
-				default:
-					return 0;
-				}
-			};
-
-			auto getBallisticsInfo = [this, &ba] (const CStack * actor)
-			{
-				const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
-
-				if(actor->getCreature()->idNumber == CreatureID::CATAPULT)
-					return VLC->heroh->ballistics.at(attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::BALLISTICS));
-				else
-				{
-					//by design use advanced ballistics parameters with this bonus present, upg. cyclops use advanced ballistics, nonupg. use basic in OH3
-					int ballisticsLevel = actor->hasBonusOfType(Bonus::CATAPULT_EXTRA_SHOTS) ? 2 : 1;
-
-					auto parameters = VLC->heroh->ballistics.at(ballisticsLevel);
-					parameters.shots = 1 + std::max(actor->valOfBonuses(Bonus::CATAPULT_EXTRA_SHOTS), 0);
-
-					return parameters;
-				}
-			};
-
-			auto isWallPartAttackable = [this] (EWallPart part)
-			{
-				return (gs->curB->si.wallState[part] == EWallState::REINFORCED || gs->curB->si.wallState[part] == EWallState::INTACT || gs->curB->si.wallState[part] == EWallState::DAMAGED);
-			};
-
-			CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters = getBallisticsInfo(stack);
-
 			auto wrapper = wrapAction(ba);
-			auto destination = target.empty() ? BattleHex(BattleHex::INVALID) : target.at(0).hexValue;
-			auto desiredTarget = gs->curB->battleHexToWallPart(destination);
-
-			for (int shotNumber=0; shotNumber<stackBallisticsParameters.shots; ++shotNumber)
+			const CStack * shooter = gs->curB->battleGetStackByID(ba.stackNumber);
+			std::shared_ptr<const Bonus> catapultAbility = stack->getBonusLocalFirst(Selector::type()(Bonus::CATAPULT));
+			if(!catapultAbility || catapultAbility->subtype < 0)
 			{
-				auto actualTarget = EWallPart::INVALID;
-
-				if ( isWallPartAttackable(desiredTarget) &&
-					 getRandomGenerator().nextInt(99) < getCatapultHitChance(desiredTarget, stackBallisticsParameters))
-				{
-					actualTarget = desiredTarget;
-				}
-				else
-				{
-					static const std::array<EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
-					static const std::array<EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
-					static const EWallPart gates = EWallPart::GATE;
-
-					// in H3, catapult under automatic control will attack objects in following order:
-					// walls, gates, towers
-					std::vector<EWallPart> potentialTargets;
-					for (auto & part : walls )
-						if (isWallPartAttackable(part))
-							potentialTargets.push_back(part);
-
-					if (potentialTargets.empty() && isWallPartAttackable(gates))
-							potentialTargets.push_back(gates);
-
-					if (potentialTargets.empty())
-						for (auto & part : towers )
-							if (isWallPartAttackable(part))
-								potentialTargets.push_back(part);
-
-					if (potentialTargets.empty())
-						break; // everything is gone, can't attack anymore
-
-					actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, getRandomGenerator());
-				}
-				assert(actualTarget != EWallPart::INVALID);
-
-				std::array<int, 3> damageChances = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
-				int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
-				int damageRandom = getRandomGenerator().nextInt(totalChance - 1);
-				int dealtDamage = 0;
-
-				//calculating dealt damage
-				for (int damage = 0; damage < damageChances.size(); ++damage)
-				{
-					if (damageRandom <= damageChances[damage])
-					{
-						dealtDamage = damage;
-						break;
-					}
-					damageRandom -= damageChances[damage];
-				}
-
-				CatapultAttack::AttackInfo attack;
-				attack.attackedPart = actualTarget;
-				attack.destinationTile = gs->curB->wallPartToBattleHex(actualTarget);
-				attack.damageDealt = dealtDamage;
-
-				CatapultAttack ca; //package for clients
-				ca.attacker = ba.stackNumber;
-				ca.attackedParts.push_back(attack);
-				sendAndApply(&ca);
-
-				logGlobal->trace("Catapult attacks %d dealing %d damage", (int)attack.attackedPart, (int)attack.damageDealt);
-
-				//removing creatures in turrets / keep if one is destroyed
-				if (gs->curB->si.wallState[actualTarget] == EWallState::DESTROYED && (actualTarget == EWallPart::KEEP || actualTarget == EWallPart::BOTTOM_TOWER || actualTarget == EWallPart::UPPER_TOWER))
-				{
-					int posRemove = -1;
-					switch(actualTarget)
-					{
-					case EWallPart::KEEP:
-						posRemove = BattleHex::CASTLE_CENTRAL_TOWER;
-						break;
-					case EWallPart::BOTTOM_TOWER:
-						posRemove = BattleHex::CASTLE_BOTTOM_TOWER;
-						break;
-					case EWallPart::UPPER_TOWER:
-						posRemove = BattleHex::CASTLE_UPPER_TOWER;
-						break;
-					}
-
-					for(auto & elem : gs->curB->stacks)
-					{
-						if(elem->initialPosition == posRemove)
-						{
-							BattleUnitsChanged removeUnits;
-							removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE);
-							sendAndApply(&removeUnits);
-							break;
-						}
-					}
-				}
+				complain("We do not know how to shoot :P");
+			}
+			else
+			{
+				const CSpell * spell = SpellID(catapultAbility->subtype).toSpell();
+				spells::BattleCast parameters(gs->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult
+				auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(Bonus::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype));
+				parameters.setSpellLevel(shotLevel);
+				parameters.cast(spellEnv, target);
 			}
 			//finish by scope guard
 			break;
@@ -5012,7 +4870,6 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 		case EActionType::STACK_HEAL: //healing with First Aid Tent
 		{
 			auto wrapper = wrapAction(ba);
-			const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
 			const CStack * healer = gs->curB->battleGetStackByID(ba.stackNumber);
 
 			if(target.size() < 1)
@@ -5023,48 +4880,23 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 			}
 
 			const battle::Unit * destStack = nullptr;
+			std::shared_ptr<const Bonus> healerAbility = stack->getBonusLocalFirst(Selector::type()(Bonus::HEALER));
 
 			if(target.at(0).unitValue)
 				destStack = target.at(0).unitValue;
 			else
 				destStack = gs->curB->battleGetStackByPos(target.at(0).hexValue);
 
-			if(healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER))
+			if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0)
 			{
 				complain("There is either no healer, no destination, or healer cannot heal :P");
 			}
 			else
 			{
-				int64_t toHeal = healer->getCount() * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID));
-
-				//TODO: allow resurrection for mods
-				auto state = destStack->acquireState();
-				state->heal(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT);
-
-				if(toHeal == 0)
-				{
-					logGlobal->warn("Nothing to heal");
-				}
-				else
-				{
-					BattleUnitsChanged pack;
-
-					BattleLogMessage message;
-
-					MetaString text;
-					text.addTxt(MetaString::GENERAL_TXT, 414);
-					healer->addNameReplacement(text, false);
-					destStack->addNameReplacement(text, false);
-					text.addReplacement((int)toHeal);
-					message.lines.push_back(text);
-
-					UnitChanges info(state->unitId(), UnitChanges::EOperation::RESET_STATE);
-					info.healthDelta = toHeal;
-					state->save(info.data);
-					pack.changedStacks.push_back(info);
-					sendAndApply(&pack);
-					sendAndApply(&message);
-				}
+				const CSpell * spell = SpellID(healerAbility->subtype).toSpell();
+				spells::BattleCast parameters(gs->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent
+				parameters.setSpellLevel(0);
+				parameters.cast(spellEnv, target);
 			}
 			break;
 		}
@@ -6704,9 +6536,7 @@ void CGameHandler::runBattle()
 						bte.effect = Bonus::HP_REGENERATION;
 
 						const int32_t lostHealth = stack->MaxHealth() - stack->getFirstHPleft();
-						if(stack->hasBonusOfType(Bonus::FULL_HP_REGENERATION))
-							bte.val = lostHealth;
-						else if(stack->hasBonusOfType(Bonus::HP_REGENERATION))
+						if(stack->hasBonusOfType(Bonus::HP_REGENERATION))
 							bte.val = std::min(lostHealth, stack->valOfBonuses(Bonus::HP_REGENERATION));
 
 						if(bte.val) // anything to heal
@@ -6736,10 +6566,7 @@ void CGameHandler::runBattle()
 
 			//check for bad morale => freeze
 			int nextStackMorale = next->MoraleVal();
-			if (nextStackMorale < 0 &&
-				!(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE)
-				   || NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses)
-				)
+			if (nextStackMorale < 0)
 			{
 				if (getRandomGenerator().nextInt(23) < -2 * nextStackMorale)
 				{
@@ -6927,10 +6754,7 @@ void CGameHandler::runBattle()
 						&& !next->waited()
 						&& !next->fear
 						&&  next->alive()
-						&&  nextStackMorale > 0
-						&& !(NBonus::hasOfType(gs->curB->battleGetFightingHero(0), Bonus::BLOCK_MORALE)
-							|| NBonus::hasOfType(gs->curB->battleGetFightingHero(1), Bonus::BLOCK_MORALE)) //checking if gs->curB->heroes have (or don't have) morale blocking bonuses
-						)
+						&&  nextStackMorale > 0)
 					{
 						if(getRandomGenerator().nextInt(23) < nextStackMorale) //this stack hasn't got morale this turn
 						{