1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00

new dialog

This commit is contained in:
Laserlicht 2024-09-28 15:51:53 +02:00
parent d929bfb9d1
commit f94f0a3274
9 changed files with 123 additions and 88 deletions

View File

@ -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:",

View File

@ -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:",

View File

@ -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));

View File

@ -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":

View File

@ -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": {

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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