Laserlicht hai 1 ano
pai
achega
f94f0a3274

+ 4 - 1
Mods/vcmi/config/vcmi/english.json

@@ -61,7 +61,10 @@
 
 	"vcmi.spellResearch.canNotAfford" : "You can't afford to research a spell.",
 	"vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.",
-	"vcmi.spellResearch.pay" : "Would you like to research this new spell and replace the old one?",
+	"vcmi.spellResearch.pay" : "Would you like to research this new spell and replace the old one or skip this spell?",
+	"vcmi.spellResearch.research" : "Research this Spell",
+	"vcmi.spellResearch.skip" : "Skip this Spell",
+	"vcmi.spellResearch.abort" : "Abort",
 
 	"vcmi.mainMenu.serverConnecting" : "Connecting...",
 	"vcmi.mainMenu.serverAddressEnter" : "Enter address:",

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

@@ -60,7 +60,10 @@
 
 	"vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, einen Zauberspruch zu erforschen.",
 	"vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.",
-	"vcmi.spellResearch.pay" : "Möchtet Ihr diesen neuen Zauberspruch erforschen und den alten ersetzen?",
+	"vcmi.spellResearch.pay" : "Möchtet Ihr diesen neuen Zauberspruch erforschen und den alten ersetzen oder diesen überspringen?",
+	"vcmi.spellResearch.research" : "Erforsche diesen Zauberspruch",
+	"vcmi.spellResearch.skip" : "Überspringe diesen Zauberspruch",
+	"vcmi.spellResearch.abort" : "Abbruch",
 
 	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
 	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",

+ 24 - 5
client/windows/CCastleInterface.cpp

@@ -2047,7 +2047,8 @@ 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)) * (town->spellResearchCounter + 1);
+		auto costExponent = LOCPLINT->cb->getSettings().getDouble(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH);
+		auto cost = (costBase + costPerLevel * (level + 1)) * std::pow(town->spellResearchCounter + 1, costExponent);
 
 		std::vector<std::shared_ptr<CComponent>> resComps;
 		resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, town->spells[level].at(town->spellsAtLevel(level, false))));
@@ -2057,10 +2058,28 @@ void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 			resComps.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i->resType, i->resVal, CComponent::ESize::medium));
 		}
 
-		if(LOCPLINT->cb->getResourceAmount().canAfford(cost))
-			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);
+		auto showSpellResearchDialog = [this, resComps, town, cost](){
+			std::vector<std::pair<AnimationPath, CFunctionList<void()>>> pom;
+			pom.emplace_back(AnimationPath::builtin("ibuy30.DEF"), nullptr);
+			pom.emplace_back(AnimationPath::builtin("hsbtns4.DEF"), nullptr);
+			pom.emplace_back(AnimationPath::builtin("ICANCEL.DEF"), nullptr);
+			auto temp = std::make_shared<CInfoWindow>(CGI->generaltexth->translate("vcmi.spellResearch.pay"), LOCPLINT->playerID, resComps, pom);
+
+			temp->buttons[0]->addCallback([this, resComps, town, cost](){ 
+				if(LOCPLINT->cb->getResourceAmount().canAfford(cost))
+					LOCPLINT->cb->spellResearch(town, spell->id, true);
+				else
+					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.canNotAfford"), resComps);
+			});
+			temp->buttons[0]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.research")); });
+			temp->buttons[1]->addCallback([this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, false); });
+			temp->buttons[1]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.skip")); });
+			temp->buttons[2]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.abort")); });
+
+			GH.windows().pushWindow(temp);
+		};
+
+		showSpellResearchDialog();
 	}
 	else
 		LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(ComponentType::SPELL, spell->id));

+ 2 - 2
config/gameConfig.json

@@ -318,8 +318,8 @@
 			"spellResearchCostBase": { "gold": 1000 },
 			// Costs depends on level for an spell research
 			"spellResearchCostPerLevel": { "wood" : 2, "mercury": 2, "ore": 2, "sulfur": 2, "crystal": 2, "gems": 2 },
-			// Factor for increasing cost for each research
-			"spellResearchCostFactorPerResearch": 2.0
+			// Exponent for increasing cost for each research
+			"spellResearchCostExponentPerResearch": 1.5
 		},
 
 		"combat":

+ 6 - 6
config/schemas/gameSettings.json

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

+ 68 - 68
lib/GameSettings.cpp

@@ -37,74 +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::TOWNS_SPELL_RESEARCH_COST_FACTOR_PER_RESEARCH,    "towns",     "spellResearchCostFactorPerResearch" },
+		{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_EXPONENT_PER_RESEARCH,  "towns",     "spellResearchCostExponentPerResearch" },
 	};
 
 void GameSettings::loadBase(const JsonNode & input)

+ 1 - 1
lib/IGameSettings.h

@@ -82,7 +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,
+	TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH,
 
 	OPTIONS_COUNT,
 	OPTIONS_BEGIN = BONUSES_GLOBAL

+ 1 - 1
lib/networkPacks/PacksForServer.h

@@ -310,7 +310,7 @@ struct DLL_LINKAGE SpellResearch : public CPackForServer
 {
 	SpellResearch() = default;
 	SpellResearch(const ObjectInstanceID & TID, SpellID spellAtSlot, bool accepted)
-		: tid(TID), spellAtSlot(spellAtSlot)
+		: tid(TID), spellAtSlot(spellAtSlot), accepted(accepted)
 	{
 	}
 	ObjectInstanceID tid;

+ 13 - 3
server/CGameHandler.cpp

@@ -2257,22 +2257,32 @@ bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool
 	
 	if(level == -1 && complain("Spell for replacement not found!"))
 		return false;
+
+	auto spells = t->spells.at(level);
 	
 	int daysSinceLastResearch = gs->getDate(Date::DAY) - t->lastSpellResearchDay;
 	if(!daysSinceLastResearch && complain("Already researched today!"))
 		return false;
 
+	if(!accepted)
+	{
+		auto it = spells.begin() + t->spellsAtLevel(level, false);
+		std::rotate(it, it + 1, spells.end()); // move to end
+		setResearchedSpells(t, level, spells, accepted);
+		return true;
+	}
+
 	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)) * (t->spellResearchCounter + 1);
+	auto costExponent = getSettings().getDouble(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH);
+
+	auto cost = (costBase + costPerLevel * (level + 1)) * std::pow(t->spellResearchCounter + 1, costExponent);
 
 	if(!getPlayerState(t->getOwner())->resources.canAfford(cost) && complain("Spell replacement cannot be afforded!"))
 		return false;
 
 	giveResources(t->getOwner(), -cost);
 
-	auto spells = t->spells.at(level);
-
 	std::swap(spells.at(t->spellsAtLevel(level, false)), spells.at(vstd::find_pos(spells, spellAtSlot)));
 	auto it = spells.begin() + t->spellsAtLevel(level, false);
 	std::rotate(it, it + 1, spells.end()); // move to end