Browse Source

custom spell schools in spell book

Laserlicht 3 months ago
parent
commit
3e6162a37a

+ 2 - 0
Mods/vcmi/Content/config/english.json

@@ -98,6 +98,8 @@
 	"vcmi.randomMap.description.monster.strong" : "strong",
 	"vcmi.randomMap.description.monster.strong" : "strong",
 
 
 	"vcmi.spellBook.search" : "search...",
 	"vcmi.spellBook.search" : "search...",
+	"vcmi.spellBook.tab.hover" : "%s Spells",
+	"vcmi.spellBook.tab.help" : "Turn to view %s spells",
 
 
 	"vcmi.spellResearch.canNotAfford" : "You can't afford to replace {%SPELL1} with {%SPELL2}. But you can still discard this spell and continue spell research.",
 	"vcmi.spellResearch.canNotAfford" : "You can't afford to replace {%SPELL1} with {%SPELL2}. But you can still discard this spell and continue spell research.",
 	"vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.",
 	"vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.",

+ 2 - 0
Mods/vcmi/Content/config/german.json

@@ -98,6 +98,8 @@
 	"vcmi.randomMap.description.monster.strong" : "Stark",
 	"vcmi.randomMap.description.monster.strong" : "Stark",
 
 
 	"vcmi.spellBook.search" : "suchen...",
 	"vcmi.spellBook.search" : "suchen...",
+	"vcmi.spellBook.tab.hover" : "%szauber",
+	"vcmi.spellBook.tab.help" : "Zu %szaubersprüchen blättern",
 
 
 	"vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, {%SPELL1} durch {%SPELL2} zu ersetzen. Aber Ihr könnt diesen Zauberspruch trotzdem verwerfen und die Zauberspruchforschung fortsetzen.",
 	"vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, {%SPELL1} durch {%SPELL2} zu ersetzen. Aber Ihr könnt diesen Zauberspruch trotzdem verwerfen und die Zauberspruchforschung fortsetzen.",
 	"vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.",
 	"vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.",

+ 2 - 2
client/PlayerLocalState.cpp

@@ -400,8 +400,8 @@ void PlayerLocalState::deserialize(const JsonNode & source)
 	{
 	{
 		spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
 		spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
 		spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
 		spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
-		spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
-		spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
+		spellbookSettings.spellbookLastTabBattle = SpellSchool(source["spellbook"]["tabBattle"].Integer());
+		spellbookSettings.spellbookLastTabAdvmap = SpellSchool(source["spellbook"]["tabAdvmap"].Integer());
 	}
 	}
 
 
 	// append any owned heroes / towns that were not present in loaded state
 	// append any owned heroes / towns that were not present in loaded state

+ 4 - 2
client/PlayerLocalState.h

@@ -9,6 +9,8 @@
  */
  */
 #pragma once
 #pragma once
 
 
+#include "../lib/constants/EntityIdentifiers.h"
+
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
 class CGHeroInstance;
 class CGHeroInstance;
@@ -28,8 +30,8 @@ struct PlayerSpellbookSetting
 	//on which page we left spellbook
 	//on which page we left spellbook
 	int spellbookLastPageBattle = 0;
 	int spellbookLastPageBattle = 0;
 	int spellbookLastPageAdvmap = 0;
 	int spellbookLastPageAdvmap = 0;
-	int spellbookLastTabBattle = 4;
-	int spellbookLastTabAdvmap = 4;
+	SpellSchool spellbookLastTabBattle = SpellSchool::ANY;
+	SpellSchool spellbookLastTabAdvmap = SpellSchool::ANY;
 };
 };
 
 
 /// Class that contains potentially serializeable state of a local player
 /// Class that contains potentially serializeable state of a local player

+ 15 - 0
client/render/AssetGenerator.cpp

@@ -49,6 +49,8 @@ void AssetGenerator::initialize()
 	imageFiles[ImagePath::builtin("CampaignBackground7.png")] = [this]() { return createCampaignBackground(7); };
 	imageFiles[ImagePath::builtin("CampaignBackground7.png")] = [this]() { return createCampaignBackground(7); };
 	imageFiles[ImagePath::builtin("CampaignBackground8.png")] = [this]() { return createCampaignBackground(8); };
 	imageFiles[ImagePath::builtin("CampaignBackground8.png")] = [this]() { return createCampaignBackground(8); };
 
 
+	imageFiles[ImagePath::builtin("SpelTabNone.png")] = [this](){ return createSpellTabNone();};
+
 	for (PlayerColor color(0); color < PlayerColor::PLAYER_LIMIT; ++color)
 	for (PlayerColor color(0); color < PlayerColor::PLAYER_LIMIT; ++color)
 		imageFiles[ImagePath::builtin("DialogBoxBackground_" + color.toString())] = [this, color](){ return createPlayerColoredBackground(color);};
 		imageFiles[ImagePath::builtin("DialogBoxBackground_" + color.toString())] = [this, color](){ return createPlayerColoredBackground(color);};
 
 
@@ -306,6 +308,19 @@ AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground(int selection
 	return image;
 	return image;
 }
 }
 
 
+AssetGenerator::CanvasPtr AssetGenerator::createSpellTabNone() const
+{
+	auto img1 = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("SPELTAB"), EImageBlitMode::COLORKEY)->getImage(0);
+	auto img2 = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("SPELTAB"), EImageBlitMode::COLORKEY)->getImage(4);
+	
+	auto image = ENGINE->renderHandler().createImage(img1->dimensions(), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
+	canvas.draw(img1, Point(0, img1->height() / 2), Rect(0, img1->height() / 2, img1->width(), img1->height() / 2));
+	canvas.draw(img2, Point(0, 0), Rect(0, 0, img2->width(), img2->height() / 2));
+
+	return image;
+}
+
 AssetGenerator::CanvasPtr AssetGenerator::createChroniclesCampaignImages(int chronicle) const
 AssetGenerator::CanvasPtr AssetGenerator::createChroniclesCampaignImages(int chronicle) const
 {
 {
 	auto imgPathBg = ImagePath::builtin("chronicles_" + std::to_string(chronicle) + "/GamSelBk");
 	auto imgPathBg = ImagePath::builtin("chronicles_" + std::to_string(chronicle) + "/GamSelBk");

+ 1 - 0
client/render/AssetGenerator.h

@@ -50,6 +50,7 @@ private:
 	CanvasPtr createPlayerColoredBackground(const PlayerColor & player) const;
 	CanvasPtr createPlayerColoredBackground(const PlayerColor & player) const;
 	CanvasPtr createCombatUnitNumberWindow(float multR, float multG, float multB) const;
 	CanvasPtr createCombatUnitNumberWindow(float multR, float multG, float multB) const;
 	CanvasPtr createCampaignBackground(int selection) const;
 	CanvasPtr createCampaignBackground(int selection) const;
+	CanvasPtr createSpellTabNone() const;
 	CanvasPtr createChroniclesCampaignImages(int chronicle) const;
 	CanvasPtr createChroniclesCampaignImages(int chronicle) const;
 	CanvasPtr createPaletteShiftedImage(const AnimationPath & source, const std::vector<PaletteAnimation> & animation, int frameIndex, int paletteShiftCounter) const;
 	CanvasPtr createPaletteShiftedImage(const AnimationPath & source, const std::vector<PaletteAnimation> & animation, int frameIndex, int paletteShiftCounter) const;
 
 

+ 115 - 38
client/windows/CSpellWindow.cpp

@@ -25,6 +25,8 @@
 #include "../gui/Shortcut.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../media/IVideoPlayer.h"
 #include "../media/IVideoPlayer.h"
+#include "../render/CAnimation.h"
+#include "../render/IRenderHandler.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CTextInput.h"
 #include "../widgets/CTextInput.h"
@@ -56,6 +58,20 @@ static const std::array schoolTabOrder =
 	SpellSchool::ANY
 	SpellSchool::ANY
 };
 };
 
 
+int getAnimFrameFromSchool(SpellSchool school)
+{
+    auto it = std::find(schoolTabOrder.begin(), schoolTabOrder.end(), school);
+    if (it != schoolTabOrder.end())
+        return std::distance(schoolTabOrder.begin(), it);
+	else
+        return -1;
+}
+
+bool isLegacySpellSchool(SpellSchool school)
+{
+	return getAnimFrameFromSchool(school) != -1;
+}
+
 CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner)
 CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner)
 {
 {
 	addUsedEvents(LCLICK | SHOW_POPUP | HOVER);
 	addUsedEvents(LCLICK | SHOW_POPUP | HOVER);
@@ -66,6 +82,20 @@ CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::functio
 	owner = _owner;
 	owner = _owner;
 }
 }
 
 
+CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function<void()> funcL, std::string textId, CSpellWindow * _owner)
+{
+	addUsedEvents(LCLICK | SHOW_POPUP | HOVER);
+	pos = myRect;
+	onLeft = funcL;
+	auto hoverTextTmp = MetaString::createFromTextID("vcmi.spellBook.tab.hover");
+	hoverTextTmp.replaceTextID(textId);
+	hoverText = hoverTextTmp.toString();
+	auto helpTextTmp = MetaString::createFromTextID("vcmi.spellBook.tab.help");
+	helpTextTmp.replaceTextID(textId);
+	helpText = helpTextTmp.toString();
+	owner = _owner;
+}
+
 void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition)
 void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition)
 {
 {
 	onLeft();
 	onLeft();
@@ -109,7 +139,7 @@ public:
 CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, const std::function<void(SpellID)> & onSpellSelect):
 CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, const std::function<void(SpellID)> & onSpellSelect):
 	CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)),
 	CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)),
 	battleSpellsOnly(openOnBattleSpells),
 	battleSpellsOnly(openOnBattleSpells),
-	selectedTab(4),
+	selectedTab(SpellSchool::ANY),
 	currentPage(0),
 	currentPage(0),
 	myHero(_myHero),
 	myHero(_myHero),
 	myInt(_myInt),
 	myInt(_myInt),
@@ -125,6 +155,15 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 {
 {
 	OBJECT_CONSTRUCTION;
 	OBJECT_CONSTRUCTION;
 
 
+	for(const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects())
+		if(
+			!isLegacySpellSchool(schoolId) &&
+			customSpellSchools.size() < MAX_CUSTOM_SPELL_SCHOOLS &&
+			!LIBRARY->spellSchoolHandler->getById(schoolId)->getSchoolBookmarkPath().empty() &&
+			!LIBRARY->spellSchoolHandler->getById(schoolId)->getSchoolHeaderPath().empty()
+		)
+			customSpellSchools.push_back(schoolId);
+
 	if(isBigSpellbook)
 	if(isBigSpellbook)
 	{
 	{
 		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
 		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
@@ -166,7 +205,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 	leftCorner = std::make_shared<CPicture>(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT);
 	leftCorner = std::make_shared<CPicture>(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT);
 	rightCorner = std::make_shared<CPicture>(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT);
 	rightCorner = std::make_shared<CPicture>(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT);
 
 
-	schoolTab = std::make_shared<CAnimImage>(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524 + offR, 88);
+	schoolTab = std::make_shared<CAnimImage>(AnimationPath::builtin("SpelTab"), getAnimFrameFromSchool(selectedTab), 0, 524 + offR, 88);
+	for(int i = 0; i < customSpellSchools.size(); i++)
+		schoolTabCustom.push_back(std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(customSpellSchools[i])->getSchoolBookmarkPath(), i == 0 ? 0 : 1, 0, isBigSpellbook ? 0 : 15, 93 + 62 * i));
 	schoolPicture = std::make_shared<CAnimImage>(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT);
 	schoolPicture = std::make_shared<CAnimImage>(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT);
 
 
 	mana = std::make_shared<CLabel>(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana));
 	mana = std::make_shared<CLabel>(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana));
@@ -181,11 +222,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this),    453, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this),    453, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb,    this),    452, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb,    this),    452, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb,      this),    459, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb,      this),    459, this));
-	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 0),   std::bind(&CSpellWindow::selectSchool,   this, 0), 454, this));
-	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 57),  std::bind(&CSpellWindow::selectSchool,   this, 3), 457, this));
-	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool,   this, 1), 455, this));
-	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool,   this, 2), 456, this));
-	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool,   this, 4), 458, this));
+	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 0),   std::bind(&CSpellWindow::selectSchool,   this, SpellSchool::AIR), 454, this));
+	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 57),  std::bind(&CSpellWindow::selectSchool,   this, SpellSchool::EARTH), 457, this));
+	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool,   this, SpellSchool::FIRE), 455, this));
+	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool,   this, SpellSchool::WATER), 456, this));
+	interactiveAreas.push_back(std::make_shared<InteractiveArea>( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool,   this, SpellSchool::ANY), 458, this));
+	for(int i = 0; i < customSpellSchools.size(); i++)
+		interactiveAreas.push_back(std::make_shared<InteractiveArea>(Rect(schoolTabCustom[i]->pos.topLeft(), Point(80, 60)), std::bind(&CSpellWindow::selectSchool, this, customSpellSchools[i]), LIBRARY->spellSchoolHandler->getById(customSpellSchools[i])->getNameTextID(), this));
 
 
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect(  97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h,  leftCorner->pos.w  ), std::bind(&CSpellWindow::fLcornerb, this), 450, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect(  97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h,  leftCorner->pos.w  ), std::bind(&CSpellWindow::fLcornerb, this), 450, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this));
 	interactiveAreas.push_back(std::make_shared<InteractiveArea>( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this));
@@ -215,11 +258,19 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 		}
 		}
 	}
 	}
 
 
-	selectedTab = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastTabBattle : myInt->localState->getSpellbookSettings().spellbookLastTabAdvmap;
-	schoolTab->setFrame(selectedTab, 0);
+	SpellSchool school = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastTabBattle : myInt->localState->getSpellbookSettings().spellbookLastTabAdvmap;
+	bool schoolFound = false;
+	for(const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects()) // check if spellschool exists -> if not, then keep any
+		if(schoolId == school)
+			schoolFound = true;
+	if(schoolFound)
+		selectedTab = school;
+	setSchoolImages(selectedTab);
 	int cp = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastPageBattle : myInt->localState->getSpellbookSettings().spellbookLastPageAdvmap;
 	int cp = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastPageBattle : myInt->localState->getSpellbookSettings().spellbookLastPageAdvmap;
 	// spellbook last page battle index is not reset after battle, so this needs to stay here
 	// spellbook last page battle index is not reset after battle, so this needs to stay here
 	vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1));
 	vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1));
+	if(!schoolFound)
+		cp = 0;
 	setCurrentPage(cp);
 	setCurrentPage(cp);
 	computeSpellsPerArea();
 	computeSpellsPerArea();
 	addUsedEvents(KEYBOARD);
 	addUsedEvents(KEYBOARD);
@@ -273,30 +324,26 @@ void CSpellWindow::processSpells()
 	SpellbookSpellSorter spellsorter;
 	SpellbookSpellSorter spellsorter;
 	std::sort(mySpells.begin(), mySpells.end(), spellsorter);
 	std::sort(mySpells.begin(), mySpells.end(), spellsorter);
 
 
-	//initializing sizes of spellbook's parts
-	for(auto & elem : sitesPerTabAdv)
-		elem = 0;
-	for(auto & elem : sitesPerTabBattle)
-		elem = 0;
-
 	for(const auto spell : mySpells)
 	for(const auto spell : mySpells)
 	{
 	{
-		int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv;
+		auto& sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv;
 
 
-		++sitesPerOurTab[4];
+		++sitesPerOurTab[SpellSchool::ANY];
 
 
 		spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop)
 		spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop)
 		{
 		{
-			++sitesPerOurTab[school.getNum()];
+			++sitesPerOurTab[school];
 		});
 		});
 	}
 	}
-	if(sitesPerTabAdv[4] % spellsPerPage == 0)
-		sitesPerTabAdv[4]/=spellsPerPage;
+	if(sitesPerTabAdv[SpellSchool::ANY] % spellsPerPage == 0)
+		sitesPerTabAdv[SpellSchool::ANY]/=spellsPerPage;
 	else
 	else
-		sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1;
+		sitesPerTabAdv[SpellSchool::ANY] = sitesPerTabAdv[SpellSchool::ANY]/spellsPerPage + 1;
 
 
-	for(int v=0; v<4; ++v)
+	for(const auto v : LIBRARY->spellSchoolHandler->getAllObjects())
 	{
 	{
+		if(v == SpellSchool::ANY)
+			continue;
 		if(sitesPerTabAdv[v] <= spellsPerPage - 2)
 		if(sitesPerTabAdv[v] <= spellsPerPage - 2)
 			sitesPerTabAdv[v] = 1;
 			sitesPerTabAdv[v] = 1;
 		else
 		else
@@ -308,13 +355,15 @@ void CSpellWindow::processSpells()
 		}
 		}
 	}
 	}
 
 
-	if(sitesPerTabBattle[4] % spellsPerPage == 0)
-		sitesPerTabBattle[4]/=spellsPerPage;
+	if(sitesPerTabBattle[SpellSchool::ANY] % spellsPerPage == 0)
+		sitesPerTabBattle[SpellSchool::ANY]/=spellsPerPage;
 	else
 	else
-		sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1;
+		sitesPerTabBattle[SpellSchool::ANY] = sitesPerTabBattle[SpellSchool::ANY]/spellsPerPage + 1;
 
 
-	for(int v=0; v<4; ++v)
+	for(const auto v : LIBRARY->spellSchoolHandler->getAllObjects())
 	{
 	{
+		if(v == SpellSchool::ANY)
+			continue;
 		if(sitesPerTabBattle[v] <= spellsPerPage - 2)
 		if(sitesPerTabBattle[v] <= spellsPerPage - 2)
 			sitesPerTabBattle[v] = 1;
 			sitesPerTabBattle[v] = 1;
 		else
 		else
@@ -382,7 +431,7 @@ void CSpellWindow::fmanaPtsb()
 {
 {
 }
 }
 
 
-void CSpellWindow::selectSchool(int school)
+void CSpellWindow::selectSchool(SpellSchool school)
 {
 {
 	if(selectedTab != school)
 	if(selectedTab != school)
 	{
 	{
@@ -391,7 +440,7 @@ void CSpellWindow::selectSchool(int school)
 		else
 		else
 			turnPageRight();
 			turnPageRight();
 		selectedTab = school;
 		selectedTab = school;
-		schoolTab->setFrame(selectedTab, 0);
+		setSchoolImages(selectedTab);
 		setCurrentPage(0);
 		setCurrentPage(0);
 	}
 	}
 	computeSpellsPerArea();
 	computeSpellsPerArea();
@@ -431,14 +480,14 @@ void CSpellWindow::computeSpellsPerArea()
 	for(const CSpell * spell : mySpells)
 	for(const CSpell * spell : mySpells)
 	{
 	{
 		if(spell->isCombat() ^ !battleSpellsOnly
 		if(spell->isCombat() ^ !battleSpellsOnly
-		   && ((selectedTab == 4) || spell->schools.count(schoolTabOrder.at(selectedTab)))
+		   && ((selectedTab == SpellSchool::ANY) || spell->schools.count(selectedTab))
 			)
 			)
 		{
 		{
 			spellsCurSite.push_back(spell);
 			spellsCurSite.push_back(spell);
 		}
 		}
 	}
 	}
 
 
-	if(selectedTab == 4)
+	if(selectedTab == SpellSchool::ANY)
 	{
 	{
 		if(spellsCurSite.size() > spellsPerPage)
 		if(spellsCurSite.size() > spellsPerPage)
 		{
 		{
@@ -449,7 +498,7 @@ void CSpellWindow::computeSpellsPerArea()
 			}
 			}
 		}
 		}
 	}
 	}
-	else //selectedTab == 0, 1, 2 or 3
+	else
 	{
 	{
 		if(spellsCurSite.size() > spellsPerPage - 2)
 		if(spellsCurSite.size() > spellsPerPage - 2)
 		{
 		{
@@ -468,7 +517,7 @@ void CSpellWindow::computeSpellsPerArea()
 		}
 		}
 	}
 	}
 	//applying
 	//applying
-	if(selectedTab == 4 || currentPage != 0)
+	if(selectedTab == SpellSchool::ANY || currentPage != 0)
 	{
 	{
 		for(size_t c=0; c<spellsPerPage; ++c)
 		for(size_t c=0; c<spellsPerPage; ++c)
 		{
 		{
@@ -497,12 +546,40 @@ void CSpellWindow::computeSpellsPerArea()
 	redraw();
 	redraw();
 }
 }
 
 
+void CSpellWindow::setSchoolImages(SpellSchool school)
+{
+	OBJECT_CONSTRUCTION;
+
+	schoolTabAnyDisabled.reset();
+	if(isLegacySpellSchool(school))
+	{
+		schoolTab->setFrame(getAnimFrameFromSchool(school), 0);
+		schoolTab->visible = true;
+	}
+	else
+	{
+		schoolTabAnyDisabled = std::make_shared<CPicture>(ImagePath::builtin("SpelTabNone.png"), 524 + offR, 88);
+		schoolTab->visible = false;
+	}
+
+	auto it = std::find(customSpellSchools.begin(), customSpellSchools.end(), school);
+	int pos = (it == customSpellSchools.end()) ? -1 : std::distance(customSpellSchools.begin(), it);
+	for(int i = 0; i < schoolTabCustom.size(); i++)
+		schoolTabCustom[i]->setFrame(i == pos ? 0 : 1, 0);
+
+	schoolPicture->visible = school != SpellSchool::ANY && currentPage == 0 && isLegacySpellSchool(school);
+	if(school != SpellSchool::ANY && isLegacySpellSchool(school))
+		schoolPicture->setFrame(getAnimFrameFromSchool(school), 0);
+	
+	schoolPictureCustom.reset();
+	if(!isLegacySpellSchool(school))
+		schoolPictureCustom = std::make_shared<CPicture>(LIBRARY->spellSchoolHandler->getById(school)->getSchoolHeaderPath(), 117 + offL, 74 + offT);
+}
+
 void CSpellWindow::setCurrentPage(int value)
 void CSpellWindow::setCurrentPage(int value)
 {
 {
 	currentPage = value;
 	currentPage = value;
-	schoolPicture->visible = selectedTab!=4 && currentPage == 0;
-	if(selectedTab != 4)
-		schoolPicture->setFrame(selectedTab, 0);
+	setSchoolImages(selectedTab);
 
 
 	if (currentPage != 0)
 	if (currentPage != 0)
 		leftCorner->enable();
 		leftCorner->enable();
@@ -555,7 +632,7 @@ void CSpellWindow::keyPressed(EShortcut key)
 		case EShortcut::MOVE_DOWN:
 		case EShortcut::MOVE_DOWN:
 		{
 		{
 			bool down = key == EShortcut::MOVE_DOWN;
 			bool down = key == EShortcut::MOVE_DOWN;
-			static const int schoolsOrder[] = { 0, 3, 1, 2, 4 };
+			static const SpellSchool schoolsOrder[] = { SpellSchool::AIR, SpellSchool::EARTH, SpellSchool::FIRE, SpellSchool::WATER, SpellSchool::ANY };
 			int index = -1;
 			int index = -1;
 			while(schoolsOrder[++index] != selectedTab);
 			while(schoolsOrder[++index] != selectedTab);
 			index += (down ? 1 : -1);
 			index += (down ? 1 : -1);
@@ -745,13 +822,13 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell)
 			OBJECT_CONSTRUCTION;
 			OBJECT_CONSTRUCTION;
 
 
 			schoolBorder.reset();
 			schoolBorder.reset();
-			if (owner->selectedTab >= 4)
+			if (!isLegacySpellSchool(owner->selectedTab) || owner->selectedTab == SpellSchool::ANY)
 			{
 			{
 				if (whichSchool.hasValue())
 				if (whichSchool.hasValue())
 					schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(whichSchool)->getSpellBordersPath(), schoolLevel);
 					schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(whichSchool)->getSpellBordersPath(), schoolLevel);
 			}
 			}
 			else
 			else
-				schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(schoolTabOrder.at(owner->selectedTab))->getSpellBordersPath(), schoolLevel);
+				schoolBorder = std::make_shared<CAnimImage>(LIBRARY->spellSchoolHandler->getById(owner->selectedTab)->getSpellBordersPath(), schoolLevel);
 		}
 		}
 
 
 		ColorRGBA firstLineColor, secondLineColor;
 		ColorRGBA firstLineColor, secondLineColor;

+ 13 - 4
client/windows/CSpellWindow.h

@@ -67,13 +67,17 @@ class CSpellWindow : public CWindowObject, public IVideoHolder
 		void hover(bool on) override;
 		void hover(bool on) override;
 
 
 		InteractiveArea(const Rect &myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner);
 		InteractiveArea(const Rect &myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner);
+		InteractiveArea(const Rect &myRect, std::function<void()> funcL, std::string textId, CSpellWindow * _owner);
 	};
 	};
 
 
 	std::shared_ptr<CPicture> leftCorner;
 	std::shared_ptr<CPicture> leftCorner;
 	std::shared_ptr<CPicture> rightCorner;
 	std::shared_ptr<CPicture> rightCorner;
 
 
 	std::shared_ptr<CAnimImage> schoolTab;
 	std::shared_ptr<CAnimImage> schoolTab;
+	std::vector<std::shared_ptr<CAnimImage>> schoolTabCustom;
+	std::shared_ptr<CPicture> schoolTabAnyDisabled;
 	std::shared_ptr<CAnimImage> schoolPicture;
 	std::shared_ptr<CAnimImage> schoolPicture;
+	std::shared_ptr<CPicture> schoolPictureCustom;
 
 
 	std::array<std::shared_ptr<SpellArea>, 24> spellAreas;
 	std::array<std::shared_ptr<SpellArea>, 24> spellAreas;
 	std::shared_ptr<CLabel> mana;
 	std::shared_ptr<CLabel> mana;
@@ -98,11 +102,14 @@ class CSpellWindow : public CWindowObject, public IVideoHolder
 	int offT;
 	int offT;
 	int offB;
 	int offB;
 
 
-	int sitesPerTabAdv[5];
-	int sitesPerTabBattle[5];
+	std::map<SpellSchool, int> sitesPerTabAdv;
+	std::map<SpellSchool, int> sitesPerTabBattle;
+
+	const int MAX_CUSTOM_SPELL_SCHOOLS = 5;
+	std::vector<SpellSchool> customSpellSchools;
 
 
 	bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed
 	bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed
-	uint8_t selectedTab; // 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools
+	SpellSchool selectedTab;
 	int currentPage; //changes when corners are clicked
 	int currentPage; //changes when corners are clicked
 	std::vector<const CSpell *> mySpells; //all spels in this spellbook
 	std::vector<const CSpell *> mySpells; //all spels in this spellbook
 
 
@@ -113,6 +120,8 @@ class CSpellWindow : public CWindowObject, public IVideoHolder
 	void searchInput();
 	void searchInput();
 	void computeSpellsPerArea(); //recalculates spellAreas::mySpell
 	void computeSpellsPerArea(); //recalculates spellAreas::mySpell
 
 
+	void setSchoolImages(SpellSchool school);
+
 	void setCurrentPage(int value);
 	void setCurrentPage(int value);
 	void turnPageLeft();
 	void turnPageLeft();
 	void turnPageRight();
 	void turnPageRight();
@@ -135,7 +144,7 @@ public:
 	void fLcornerb();
 	void fLcornerb();
 	void fRcornerb();
 	void fRcornerb();
 
 
-	void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools
+	void selectSchool(SpellSchool school);
 	int pagesWithinCurrentTab();
 	int pagesWithinCurrentTab();
 
 
 	void keyPressed(EShortcut key) override;
 	void keyPressed(EShortcut key) override;

+ 15 - 1
config/schemas/spellSchool.json

@@ -3,17 +3,31 @@
 	"$schema" : "http://json-schema.org/draft-04/schema",
 	"$schema" : "http://json-schema.org/draft-04/schema",
 	"title" : "VCMI spell school format",
 	"title" : "VCMI spell school format",
 	"description" : "Format used to define new spell schools in VCMI",
 	"description" : "Format used to define new spell schools in VCMI",
-	"required" : [ "schoolBorders" ],
+	"required" : [ "schoolBorders", "name" ],
 	"additionalProperties" : false,
 	"additionalProperties" : false,
 	"properties" : {
 	"properties" : {
 		"index" : {
 		"index" : {
 			"type" : "number",
 			"type" : "number",
 			"description" : "numeric id of h3 spell school, prohibited for new schools"
 			"description" : "numeric id of h3 spell school, prohibited for new schools"
 		},
 		},
+		"name" : {
+			"type" : "string",
+			"description" : "Localizable name of this school"
+		},
 		"schoolBorders" : {
 		"schoolBorders" : {
 			"type" : "string",
 			"type" : "string",
 			"description" : "File with frame borders of mastery levels for spells of this spell school in spellbook",
 			"description" : "File with frame borders of mastery levels for spells of this spell school in spellbook",
 			"format" : "animationFile"
 			"format" : "animationFile"
+		},
+		"schoolBookmark" : {
+			"type" : "string",
+			"description" : "File with bookmark symbol (first frame unselected, second is selected)",
+			"format" : "animationFile"
+		},
+		"schoolHeader" : {
+			"type" : "string",
+			"description" : "Header of scool for spellbook",
+			"format" : "imageFile"
 		}
 		}
 	}
 	}
 }
 }

+ 7 - 2
docs/modders/Entities_Format/Spell_School_Format.md

@@ -5,12 +5,17 @@ WARNING: currently custom spell schools are only partially supported:
 - it is possible to use custom spell schools in bonus system
 - it is possible to use custom spell schools in bonus system
 - it is possible to make skill for specializing in such spell
 - it is possible to make skill for specializing in such spell
 - it is possible to specify border decorations for mastery level of such spell in spellbook
 - it is possible to specify border decorations for mastery level of such spell in spellbook
-- it is NOT possible to add "bookmark" filter for spellbook for spells of such school
 
 
 ```json
 ```json
 	// Internal field for H3 schools. Do not use for mods
 	// Internal field for H3 schools. Do not use for mods
 	"index" : "",
 	"index" : "",
 	
 	
 	// animation file with spell borders for spell mastery levels
 	// animation file with spell borders for spell mastery levels
-	"schoolBorders" : "SplevA"
+	"schoolBorders" : "SplevA",
+	
+	// animation file with bookmark symbol (first frame unselected, second is selected)
+	"schoolBookmark" : "schoolBookmark",
+	
+	// image file for feader of scool for spellbook
+	"schoolHeader" : "SchoolHeader"
 ```
 ```

+ 19 - 1
lib/spells/SpellSchoolHandler.cpp

@@ -11,10 +11,23 @@
 #include "StdInc.h"
 #include "StdInc.h"
 #include "SpellSchoolHandler.h"
 #include "SpellSchoolHandler.h"
 
 
+#include "../GameLibrary.h"
 #include "../json/JsonNode.h"
 #include "../json/JsonNode.h"
+#include "../texts/CGeneralTextHandler.h"
+#include "../texts/TextIdentifier.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
+std::string spells::SpellSchoolType::getNameTextID() const
+{
+	return TextIdentifier( "spellSchool", modScope, identifier, "name" ).get();
+}
+
+std::string spells::SpellSchoolType::getNameTranslated() const
+{
+	return LIBRARY->generaltexth->translate(getNameTextID());
+}
+
 std::vector<JsonNode> SpellSchoolHandler::loadLegacyData()
 std::vector<JsonNode> SpellSchoolHandler::loadLegacyData()
 {
 {
 	objects.resize(4);
 	objects.resize(4);
@@ -27,8 +40,13 @@ std::shared_ptr<spells::SpellSchoolType> SpellSchoolHandler::loadObjectImpl(std:
 	auto ret = std::make_shared<spells::SpellSchoolType>();
 	auto ret = std::make_shared<spells::SpellSchoolType>();
 
 
 	ret->id = SpellSchool(index);
 	ret->id = SpellSchool(index);
-	ret->jsonName = name;
+	ret->modScope = scope;
+	ret->identifier = name;
 	ret->spellBordersPath = AnimationPath::fromJson(data["schoolBorders"]);
 	ret->spellBordersPath = AnimationPath::fromJson(data["schoolBorders"]);
+	ret->schoolBookmarkPath = AnimationPath::fromJson(data["schoolBookmark"]);
+	ret->schoolHeaderPath = ImagePath::fromJson(data["schoolHeader"]);
+
+	LIBRARY->generaltexth->registerString(scope, ret->getNameTextID(), data["name"]);
 
 
 	return ret;
 	return ret;
 }
 }

+ 24 - 5
lib/spells/SpellSchoolHandler.h

@@ -10,6 +10,8 @@
 
 
 #pragma once
 #pragma once
 
 
+#include <vcmi/EntityService.h>
+#include <vcmi/Entity.h>
 #include "../constants/EntityIdentifiers.h"
 #include "../constants/EntityIdentifiers.h"
 #include "../IHandlerBase.h"
 #include "../IHandlerBase.h"
 #include "../filesystem/ResourcePath.h"
 #include "../filesystem/ResourcePath.h"
@@ -21,18 +23,22 @@ class SpellSchoolHandler;
 namespace spells
 namespace spells
 {
 {
 
 
-class DLL_LINKAGE SpellSchoolType
+class DLL_LINKAGE SpellSchoolType : public EntityT<SpellSchool>
 {
 {
 	friend class VCMI_LIB_WRAP_NAMESPACE(SpellSchoolHandler);
 	friend class VCMI_LIB_WRAP_NAMESPACE(SpellSchoolHandler);
 
 
 	SpellSchool id; //backlink
 	SpellSchool id; //backlink
-	std::string jsonName;
 	AnimationPath spellBordersPath;
 	AnimationPath spellBordersPath;
+	AnimationPath schoolBookmarkPath;
+	ImagePath schoolHeaderPath;
+
+	std::string identifier;
+	std::string modScope;
 
 
 public:
 public:
 	std::string getJsonKey() const
 	std::string getJsonKey() const
 	{
 	{
-		return jsonName;
+		return identifier;
 	}
 	}
 
 
 	AnimationPath getSpellBordersPath() const
 	AnimationPath getSpellBordersPath() const
@@ -40,10 +46,23 @@ public:
 		return spellBordersPath;
 		return spellBordersPath;
 	}
 	}
 
 
-	int getIndex() const
+	AnimationPath getSchoolBookmarkPath() const
 	{
 	{
-		return id.getNum();
+		return schoolBookmarkPath;
 	}
 	}
+
+	ImagePath getSchoolHeaderPath() const
+	{
+		return schoolHeaderPath;
+	}
+
+	int32_t getIndex() const override { return id.getNum(); }
+	SpellSchool getId() const override { return id;}
+	int32_t getIconIndex() const override { return 0; }
+	std::string getModScope() const override { return modScope; };
+	void registerIcons(const IconRegistar & cb) const override {};
+	std::string getNameTextID() const override;
+	std::string getNameTranslated() const override;
 };
 };
 
 
 }
 }