Browse Source

rename; introduce factor

Laserlicht 1 year ago
parent
commit
d929bfb9d1

+ 2 - 2
CCallback.cpp

@@ -249,9 +249,9 @@ int CBattleCallback::sendRequest(const CPackForServer * request)
 	return requestID;
 }
 
-void CCallback::spellResearch( const CGTownInstance *town, SpellID spellAtSlot )
+void CCallback::spellResearch( const CGTownInstance *town, SpellID spellAtSlot, bool accepted )
 {
-	SpellResearch pack(town->id, spellAtSlot);
+	SpellResearch pack(town->id, spellAtSlot, accepted);
 	sendRequest(&pack);
 }
 

+ 2 - 2
CCallback.h

@@ -78,7 +78,7 @@ public:
 	virtual bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
 	virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
 	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
-	virtual void spellResearch(const CGTownInstance *town, SpellID spellAtSlot)=0;
+	virtual void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted)=0;
 	virtual void swapGarrisonHero(const CGTownInstance *town)=0;
 
 	virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
@@ -188,7 +188,7 @@ public:
 	bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
 	bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
 	void endTurn() override;
-	void spellResearch(const CGTownInstance *town, SpellID spellAtSlot) override;
+	void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted) override;
 	void swapGarrisonHero(const CGTownInstance *town) override;
 	void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
 	void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;

+ 1 - 1
client/Client.h

@@ -159,7 +159,7 @@ public:
 	friend class CBattleCallback; //handling players actions
 
 	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> & spells) override {};
-	void setTownSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells) override {};
+	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override {};
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
 	void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {};
 	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};

+ 1 - 1
client/ClientNetPackVisitors.h

@@ -37,7 +37,7 @@ public:
 	void visitHeroVisitCastle(HeroVisitCastle & pack) override;
 	void visitSetMana(SetMana & pack) override;
 	void visitSetMovePoints(SetMovePoints & pack) override;
-	void visitSetTownSpells(SetTownSpells & pack) override;
+	void visitSetResearchedSpells(SetResearchedSpells & pack) override;
 	void visitFoWChange(FoWChange & pack) override;
 	void visitChangeStackCount(ChangeStackCount & pack) override;
 	void visitSetStackType(SetStackType & pack) override;

+ 1 - 1
client/NetPacksClient.cpp

@@ -173,7 +173,7 @@ void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
-void ApplyClientNetPackVisitor::visitSetTownSpells(SetTownSpells & pack)
+void ApplyClientNetPackVisitor::visitSetResearchedSpells(SetResearchedSpells & pack)
 {
 	for(const auto & win : GH.windows().findWindows<CMageGuildScreen>())
 		win->updateSpells();

+ 2 - 2
client/windows/CCastleInterface.cpp

@@ -2047,7 +2047,7 @@ void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 
 		auto costBase = TResources(LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_BASE));
 		auto costPerLevel = TResources(LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_PER_LEVEL));
-		auto cost = costBase + costPerLevel * (level + 1);
+		auto cost = (costBase + costPerLevel * (level + 1)) * (town->spellResearchCounter + 1);
 
 		std::vector<std::shared_ptr<CComponent>> resComps;
 		resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, town->spells[level].at(town->spellsAtLevel(level, false))));
@@ -2058,7 +2058,7 @@ void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 		}
 
 		if(LOCPLINT->cb->getResourceAmount().canAfford(cost))
-			LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.spellResearch.pay"), [this, town](){ LOCPLINT->cb->spellResearch(town, spell->id); }, nullptr, resComps);
+			LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.spellResearch.pay"), [this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, true); }, nullptr, resComps);
 		else
 			LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.canNotAfford"), resComps);
 	}

+ 4 - 2
config/gameConfig.json

@@ -302,7 +302,7 @@
 			"backpackSize"		: -1,
 			// if heroes are invitable in tavern
 			"tavernInvite"            : false,
-			// minimai primary skills for heroes
+			// minimal primary skills for heroes
 			"minimalPrimarySkills": [ 0, 0, 1, 1]
 		},
 
@@ -317,7 +317,9 @@
 			// Base cost for an spell research
 			"spellResearchCostBase": { "gold": 1000 },
 			// Costs depends on level for an spell research
-			"spellResearchCostPerLevel": { "wood" : 2, "mercury": 2, "ore": 2, "sulfur": 2, "crystal": 2, "gems": 2 }
+			"spellResearchCostPerLevel": { "wood" : 2, "mercury": 2, "ore": 2, "sulfur": 2, "crystal": 2, "gems": 2 },
+			// Factor for increasing cost for each research
+			"spellResearchCostFactorPerResearch": 2.0
 		},
 
 		"combat":

+ 6 - 5
config/schemas/gameSettings.json

@@ -51,11 +51,12 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"properties" : {
-				"buildingsPerTurnCap"  :      { "type" : "number" },
-				"startingDwellingChances" :   { "type" : "array" },
-				"spellResearch" :             { "type" : "boolean" },
-				"spellResearchCostBase" :     { "type" : "object" },
-				"spellResearchCostPerLevel" : { "type" : "object" }
+				"buildingsPerTurnCap"  :               { "type" : "number" },
+				"startingDwellingChances" :            { "type" : "array" },
+				"spellResearch" :                      { "type" : "boolean" },
+				"spellResearchCostBase" :              { "type" : "object" },
+				"spellResearchCostPerLevel" :          { "type" : "object" },
+				"spellResearchCostFactorPerResearch" : { "type" : "number" }
 			}
 		},
 		"combat": {

+ 68 - 67
lib/GameSettings.cpp

@@ -37,73 +37,74 @@ GameSettings::GameSettings() = default;
 GameSettings::~GameSettings() = default;
 
 const std::vector<GameSettings::SettingOption> GameSettings::settingProperties = {
-		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"            },
-		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                           },
-		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                          },
-		{EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX,  "combat",    "areaShotCanTargetEmptyHex"        },
-		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"          },
-		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,  "combat",    "attackPointDamageFactorCap"       },
-		{EGameSettings::COMBAT_BAD_LUCK_DICE,                   "combat",    "badLuckDice"                      },
-		{EGameSettings::COMBAT_BAD_MORALE_DICE,                 "combat",    "badMoraleDice"                    },
-		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR,     "combat",    "defensePointDamageFactor"         },
-		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat",    "defensePointDamageFactorCap"      },
-		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                  "combat",    "goodLuckDice"                     },
-		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                "combat",    "goodMoraleDice"                   },
-		{EGameSettings::COMBAT_LAYOUTS,                         "combat",    "layouts"                          },
-		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,      "combat",    "oneHexTriggersObstacles"          },
-		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,   "creatures", "allowAllForDoubleMonth"           },
-		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,   "creatures", "allowRandomSpecialWeeks"          },
-		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,       "creatures", "dailyStackExperience"             },
-		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,            "creatures", "weeklyGrowthCap"                  },
-		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"              },
-		{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,    "spells",    "dimensionDoorExposesTerrainType"  },
-		{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS,   "spells",    "dimensionDoorFailureSpendsPoints" },
-		{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells",    "dimensionDoorOnlyToUncoveredTiles"},
-		{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,  "spells",    "dimensionDoorTournamentRulesLimit"},
-		{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS,         "spells",    "dimensionDoorTriggersGuards"      },
-		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"            },
-		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"              },
-		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"                   },
-		{EGameSettings::HEROES_BACKPACK_CAP,                    "heroes",    "backpackSize"                     },
-		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,          "heroes",    "minimalPrimarySkills"             },
-		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"                },
-		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"                },
-		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"        },
-		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,         "heroes",    "startingStackChances"             },
-		{EGameSettings::HEROES_TAVERN_INVITE,                   "heroes",    "tavernInvite"                     },
-		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"                 },
-		{EGameSettings::MAP_FORMAT_CHRONICLES,                  "mapFormat", "chronicles"                       },
-		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"                   },
-		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"                  },
-		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                         },
-		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"             },
-		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"                    },
-		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,    "markets",   "blackMarketRestockPeriod"         },
-		{EGameSettings::MODULE_COMMANDERS,                      "modules",   "commanders"                       },
-		{EGameSettings::MODULE_STACK_ARTIFACT,                  "modules",   "stackArtifact"                    },
-		{EGameSettings::MODULE_STACK_EXPERIENCE,                "modules",   "stackExperience"                  },
-		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"                    },
-		{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES,          "pathfinder", "originalFlyRules"                },
-		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom"         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"               },
-		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,               "pathfinder", "useWhirlpool"                    },
-		{EGameSettings::TEXTS_ARTIFACT,                         "textData",  "artifact"                         },
-		{EGameSettings::TEXTS_CREATURE,                         "textData",  "creature"                         },
-		{EGameSettings::TEXTS_FACTION,                          "textData",  "faction"                          },
-		{EGameSettings::TEXTS_HERO,                             "textData",  "hero"                             },
-		{EGameSettings::TEXTS_HERO_CLASS,                       "textData",  "heroClass"                        },
-		{EGameSettings::TEXTS_OBJECT,                           "textData",  "object"                           },
-		{EGameSettings::TEXTS_RIVER,                            "textData",  "river"                            },
-		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                             },
-		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                            },
-		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                          },
-		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,           "towns",     "buildingsPerTurnCap"              },
-		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,        "towns",     "startingDwellingChances"          },
-		{EGameSettings::TOWNS_SPELL_RESEARCH,                   "towns",     "spellResearch"                    },
-		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_BASE,         "towns",     "spellResearchCostBase"            },
-		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_PER_LEVEL,    "towns",     "spellResearchCostPerLevel"        },
+		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,                    "banks",     "showGuardsComposition"              },
+		{EGameSettings::BONUSES_GLOBAL,                                   "bonuses",   "global"                             },
+		{EGameSettings::BONUSES_PER_HERO,                                 "bonuses",   "perHero"                            },
+		{EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX,            "combat",    "areaShotCanTargetEmptyHex"          },
+		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,                "combat",    "attackPointDamageFactor"            },
+		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP,            "combat",    "attackPointDamageFactorCap"         },
+		{EGameSettings::COMBAT_BAD_LUCK_DICE,                             "combat",    "badLuckDice"                        },
+		{EGameSettings::COMBAT_BAD_MORALE_DICE,                           "combat",    "badMoraleDice"                      },
+		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR,               "combat",    "defensePointDamageFactor"           },
+		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP,           "combat",    "defensePointDamageFactorCap"        },
+		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                            "combat",    "goodLuckDice"                       },
+		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                          "combat",    "goodMoraleDice"                     },
+		{EGameSettings::COMBAT_LAYOUTS,                                   "combat",    "layouts"                            },
+		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,                "combat",    "oneHexTriggersObstacles"            },
+		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,             "creatures", "allowAllForDoubleMonth"             },
+		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,             "creatures", "allowRandomSpecialWeeks"            },
+		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,                 "creatures", "dailyStackExperience"               },
+		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,                      "creatures", "weeklyGrowthCap"                    },
+		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,                  "creatures", "weeklyGrowthPercent"                },
+		{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,              "spells",    "dimensionDoorExposesTerrainType"    },
+		{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS,             "spells",    "dimensionDoorFailureSpendsPoints"   },
+		{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,           "spells",    "dimensionDoorOnlyToUncoveredTiles"  },
+		{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,            "spells",    "dimensionDoorTournamentRulesLimit"  },
+		{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS,                   "spells",    "dimensionDoorTriggersGuards"        },
+		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,                "dwellings", "accumulateWhenNeutral"              },
+		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,                  "dwellings", "accumulateWhenOwned"                },
+		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,                       "dwellings", "mergeOnRecruit"                     },
+		{EGameSettings::HEROES_BACKPACK_CAP,                              "heroes",    "backpackSize"                       },
+		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,                    "heroes",    "minimalPrimarySkills"               },
+		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,                     "heroes",    "perPlayerOnMapCap"                  },
+		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,                      "heroes",    "perPlayerTotalCap"                  },
+		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,             "heroes",    "retreatOnWinWithoutTroops"          },
+		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,                   "heroes",    "startingStackChances"               },
+		{EGameSettings::HEROES_TAVERN_INVITE,                             "heroes",    "tavernInvite"                       },
+		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,                     "mapFormat", "armageddonsBlade"                   },
+		{EGameSettings::MAP_FORMAT_CHRONICLES,                            "mapFormat", "chronicles"                         },
+		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,                     "mapFormat", "hornOfTheAbyss"                     },
+		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,                   "mapFormat", "inTheWakeOfGods"                    },
+		{EGameSettings::MAP_FORMAT_JSON_VCMI,                             "mapFormat", "jsonVCMI"                           },
+		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,                "mapFormat", "restorationOfErathia"               },
+		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,                       "mapFormat", "shadowOfDeath"                      },
+		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,              "markets",   "blackMarketRestockPeriod"           },
+		{EGameSettings::MODULE_COMMANDERS,                                "modules",   "commanders"                         },
+		{EGameSettings::MODULE_STACK_ARTIFACT,                            "modules",   "stackArtifact"                      },
+		{EGameSettings::MODULE_STACK_EXPERIENCE,                          "modules",   "stackExperience"                    },
+		{EGameSettings::PATHFINDER_IGNORE_GUARDS,                         "pathfinder", "ignoreGuards"                      },
+		{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES,                    "pathfinder", "originalFlyRules"                  },
+		{EGameSettings::PATHFINDER_USE_BOAT,                              "pathfinder", "useBoat"                           },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM,           "pathfinder", "useMonolithOneWayRandom"           },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,           "pathfinder", "useMonolithOneWayUnique"           },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,                  "pathfinder", "useMonolithTwoWay"                 },
+		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,                         "pathfinder", "useWhirlpool"                      },
+		{EGameSettings::TEXTS_ARTIFACT,                                   "textData",  "artifact"                           },
+		{EGameSettings::TEXTS_CREATURE,                                   "textData",  "creature"                           },
+		{EGameSettings::TEXTS_FACTION,                                    "textData",  "faction"                            },
+		{EGameSettings::TEXTS_HERO,                                       "textData",  "hero"                               },
+		{EGameSettings::TEXTS_HERO_CLASS,                                 "textData",  "heroClass"                          },
+		{EGameSettings::TEXTS_OBJECT,                                     "textData",  "object"                             },
+		{EGameSettings::TEXTS_RIVER,                                      "textData",  "river"                              },
+		{EGameSettings::TEXTS_ROAD,                                       "textData",  "road"                               },
+		{EGameSettings::TEXTS_SPELL,                                      "textData",  "spell"                              },
+		{EGameSettings::TEXTS_TERRAIN,                                    "textData",  "terrain"                            },
+		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,                     "towns",     "buildingsPerTurnCap"                },
+		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,                  "towns",     "startingDwellingChances"            },
+		{EGameSettings::TOWNS_SPELL_RESEARCH,                             "towns",     "spellResearch"                      },
+		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_BASE,                   "towns",     "spellResearchCostBase"              },
+		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_PER_LEVEL,              "towns",     "spellResearchCostPerLevel"          },
+		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_FACTOR_PER_RESEARCH,    "towns",     "spellResearchCostFactorPerResearch" },
 	};
 
 void GameSettings::loadBase(const JsonNode & input)

+ 1 - 1
lib/IGameCallback.h

@@ -94,7 +94,7 @@ public:
 	virtual void showInfoDialog(InfoWindow * iw) = 0;
 
 	virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells)=0;
-	virtual void setTownSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells)=0;
+	virtual void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted)=0;
 	virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0;
 	virtual void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) = 0;
 	virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0;

+ 1 - 0
lib/IGameSettings.h

@@ -82,6 +82,7 @@ enum class EGameSettings
 	TOWNS_SPELL_RESEARCH,
 	TOWNS_SPELL_RESEARCH_COST_BASE,
 	TOWNS_SPELL_RESEARCH_COST_PER_LEVEL,
+	TOWNS_SPELL_RESEARCH_COST_FACTOR_PER_RESEARCH,
 
 	OPTIONS_COUNT,
 	OPTIONS_BEGIN = BONUSES_GLOBAL

+ 1 - 0
lib/mapObjects/CGTownInstance.cpp

@@ -270,6 +270,7 @@ CGTownInstance::CGTownInstance(IGameCallback *cb):
 	identifier(0),
 	alignmentToPlayer(PlayerColor::NEUTRAL),
 	lastSpellResearchDay(0),
+	spellResearchCounter(0),
 	spellResearchAllowed(true)
 {
 	this->setNodeType(CBonusSystemNode::TOWN);

+ 5 - 0
lib/mapObjects/CGTownInstance.h

@@ -74,6 +74,7 @@ public:
 	std::vector<CCastleEvent> events;
 	std::pair<si32, si32> bonusValue;//var to store town bonuses (rampart = resources from mystic pond, factory = save debts);
 	int lastSpellResearchDay;
+	int spellResearchCounter;
 	bool spellResearchAllowed;
 
 	//////////////////////////////////////////////////////////////////////////
@@ -96,7 +97,11 @@ public:
 		h & events;
 
 		if (h.version >= Handler::Version::SPELL_RESEARCH)
+		{
 			h & lastSpellResearchDay;
+			h & spellResearchCounter;
+			h & spellResearchAllowed;
+		}
 
 		if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS)
 		{

+ 1 - 1
lib/networkPacks/NetPackVisitor.h

@@ -42,7 +42,7 @@ public:
 	virtual void visitSetSecSkill(SetSecSkill & pack) {}
 	virtual void visitHeroVisitCastle(HeroVisitCastle & pack) {}
 	virtual void visitChangeSpells(ChangeSpells & pack) {}
-	virtual void visitSetTownSpells(SetTownSpells & pack) {}
+	virtual void visitSetResearchedSpells(SetResearchedSpells & pack) {}
 	virtual void visitSetMana(SetMana & pack) {}
 	virtual void visitSetMovePoints(SetMovePoints & pack) {}
 	virtual void visitFoWChange(FoWChange & pack) {}

+ 5 - 3
lib/networkPacks/NetPacksLib.cpp

@@ -162,9 +162,9 @@ void ChangeSpells::visitTyped(ICPackVisitor & visitor)
 	visitor.visitChangeSpells(*this);
 }
 
-void SetTownSpells::visitTyped(ICPackVisitor & visitor)
+void SetResearchedSpells::visitTyped(ICPackVisitor & visitor)
 {
-	visitor.visitSetTownSpells(*this);
+	visitor.visitSetResearchedSpells(*this);
 }
 void SetMana::visitTyped(ICPackVisitor & visitor)
 {
@@ -939,12 +939,14 @@ void ChangeSpells::applyGs(CGameState *gs)
 			hero->removeSpellFromSpellbook(sid);
 }
 
-void SetTownSpells::applyGs(CGameState *gs)
+void SetResearchedSpells::applyGs(CGameState *gs)
 {
 	CGTownInstance *town = gs->getTown(tid);
 
 	town->spells[level] = spells;
 	town->lastSpellResearchDay = gs->getDate(Date::DAY);
+	if(accepted)
+		town->spellResearchCounter++;
 }
 
 void SetMana::applyGs(CGameState *gs)

+ 3 - 1
lib/networkPacks/PacksForClient.h

@@ -288,7 +288,7 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient
 	}
 };
 
-struct DLL_LINKAGE SetTownSpells : public CPackForClient
+struct DLL_LINKAGE SetResearchedSpells : public CPackForClient
 {
 	void applyGs(CGameState * gs) override;
 
@@ -297,12 +297,14 @@ struct DLL_LINKAGE SetTownSpells : public CPackForClient
 	ui8 level = 0;
 	ObjectInstanceID tid;
 	std::vector<SpellID> spells;
+	bool accepted;
 
 	template <typename Handler> void serialize(Handler & h)
 	{
 		h & level;
 		h & tid;
 		h & spells;
+		h & accepted;
 	}
 };
 

+ 3 - 1
lib/networkPacks/PacksForServer.h

@@ -309,12 +309,13 @@ struct DLL_LINKAGE RazeStructure : public BuildStructure
 struct DLL_LINKAGE SpellResearch : public CPackForServer
 {
 	SpellResearch() = default;
-	SpellResearch(const ObjectInstanceID & TID, SpellID spellAtSlot)
+	SpellResearch(const ObjectInstanceID & TID, SpellID spellAtSlot, bool accepted)
 		: tid(TID), spellAtSlot(spellAtSlot)
 	{
 	}
 	ObjectInstanceID tid;
 	SpellID spellAtSlot;
+	bool accepted;
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -323,6 +324,7 @@ struct DLL_LINKAGE SpellResearch : public CPackForServer
 		h & static_cast<CPackForServer &>(*this);
 		h & tid;
 		h & spellAtSlot;
+		h & accepted;
 	}
 };
 

+ 1 - 1
lib/serializer/RegisterTypes.h

@@ -289,7 +289,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<LobbyForceSetPlayer>(239);
 	s.template registerType<LobbySetExtraOptions>(240);
 	s.template registerType<SpellResearch>(241);
-	s.template registerType<SetTownSpells>(242);
+	s.template registerType<SetResearchedSpells>(242);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 6 - 5
server/CGameHandler.cpp

@@ -1235,12 +1235,13 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st
 	sendAndApply(&cs);
 }
 
-void CGameHandler::setTownSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells)
+void CGameHandler::setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted)
 {
-	SetTownSpells cs;
+	SetResearchedSpells cs;
 	cs.tid = town->id;
 	cs.spells = spells;
 	cs.level = level;
+	cs.accepted = accepted;
 	sendAndApply(&cs);
 }
 
@@ -2242,7 +2243,7 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid)
 	return true;
 }
 
-bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot)
+bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted)
 {
 	CGTownInstance *t = gs->getTown(tid);
 
@@ -2263,7 +2264,7 @@ bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot)
 
 	auto costBase = TResources(getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_BASE));
 	auto costPerLevel = TResources(getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_PER_LEVEL));
-	auto cost = costBase + costPerLevel * (level + 1);
+	auto cost = (costBase + costPerLevel * (level + 1)) * (t->spellResearchCounter + 1);
 
 	if(!getPlayerState(t->getOwner())->resources.canAfford(cost) && complain("Spell replacement cannot be afforded!"))
 		return false;
@@ -2276,7 +2277,7 @@ bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot)
 	auto it = spells.begin() + t->spellsAtLevel(level, false);
 	std::rotate(it, it + 1, spells.end()); // move to end
 
-	setTownSpells(t, level, spells);
+	setResearchedSpells(t, level, spells, accepted);
 
 	if(t->visitingHero)
 		giveSpells(t, t->visitingHero);

+ 2 - 2
server/CGameHandler.h

@@ -107,7 +107,7 @@ public:
 	//from IGameCallback
 	//do sth
 	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override;
-	void setTownSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells) override;
+	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override;
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override;
 	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override;
 	void giveExperience(const CGHeroInstance * hero, TExpType val) override;
@@ -219,7 +219,7 @@ public:
 	bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings
 	bool visitTownBuilding(ObjectInstanceID tid, BuildingID bid);
 	bool razeStructure(ObjectInstanceID tid, BuildingID bid);
-	bool spellResearch(ObjectInstanceID tid, SpellID spellAtSlot);
+	bool spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted);
 	bool disbandCreature( ObjectInstanceID id, SlotID pos );
 	bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player);
 	bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot);

+ 1 - 1
server/NetPacksServer.cpp

@@ -143,7 +143,7 @@ void ApplyGhNetPackVisitor::visitSpellResearch(SpellResearch & pack)
 	gh.throwIfWrongOwner(&pack, pack.tid);
 	gh.throwIfPlayerNotActive(&pack);
 	
-	result = gh.spellResearch(pack.tid, pack.spellAtSlot);
+	result = gh.spellResearch(pack.tid, pack.spellAtSlot, pack.accepted);
 }
 
 void ApplyGhNetPackVisitor::visitVisitTownBuilding(VisitTownBuilding & pack)

+ 1 - 1
test/mock/mock_IGameCallback.h

@@ -44,7 +44,7 @@ public:
 	void showInfoDialog(InfoWindow * iw) override {}
 
 	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override {}
-	void setTownSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells) override {}
+	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override {}
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}
 	void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}
 	void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {}