浏览代码

Merge pull request #5560 from GeorgeK1ng/campaigns

[1.7] Campaigns configurations improvements
Ivan Savenko 6 月之前
父节点
当前提交
dd823d1681
共有 30 个文件被更改,包括 285 次插入71 次删除
  1. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-back-down.png
  2. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-back-hover.png
  3. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-back-normal.png
  4. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-next-down.png
  5. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-next-hover.png
  6. 二进制
      Mods/vcmi/Content/Sprites/campaigns/arrow-next-normal.png
  7. 20 0
      Mods/vcmi/Content/Sprites/campaigns/back.json
  8. 20 0
      Mods/vcmi/Content/Sprites/campaigns/next.json
  9. 0 1
      Mods/vcmi/Content/config/chinese.json
  10. 3 1
      Mods/vcmi/Content/config/czech.json
  11. 0 1
      Mods/vcmi/Content/config/english.json
  12. 0 1
      Mods/vcmi/Content/config/german.json
  13. 0 1
      Mods/vcmi/Content/config/hungarian.json
  14. 0 1
      Mods/vcmi/Content/config/italian.json
  15. 0 1
      Mods/vcmi/Content/config/polish.json
  16. 0 1
      Mods/vcmi/Content/config/portuguese.json
  17. 0 1
      Mods/vcmi/Content/config/russian.json
  18. 0 1
      Mods/vcmi/Content/config/spanish.json
  19. 0 1
      Mods/vcmi/Content/config/swedish.json
  20. 0 1
      Mods/vcmi/Content/config/ukrainian.json
  21. 0 1
      Mods/vcmi/Content/config/vietnamese.json
  22. 4 0
      client/gui/Shortcut.h
  23. 4 0
      client/gui/ShortcutHandler.cpp
  24. 124 10
      client/mainmenu/CCampaignScreen.cpp
  25. 10 0
      client/mainmenu/CCampaignScreen.h
  26. 0 16
      client/mainmenu/CMainMenu.cpp
  27. 88 23
      client/render/AssetGenerator.cpp
  28. 1 1
      client/render/AssetGenerator.h
  29. 7 8
      config/campaignSets.json
  30. 4 0
      config/shortcutsConfig.json

二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-back-down.png


二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-back-hover.png


二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-back-normal.png


二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-next-down.png


二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-next-hover.png


二进制
Mods/vcmi/Content/Sprites/campaigns/arrow-next-normal.png


+ 20 - 0
Mods/vcmi/Content/Sprites/campaigns/back.json

@@ -0,0 +1,20 @@
+{
+	"basepath" : "campaigns/",
+ 
+	"images" :
+	[
+		{
+			"frame": 0,
+			"file" : "arrow-back-normal"
+		},
+		{
+			"frame": 1,
+			"file" : "arrow-back-down"
+		},
+		{
+			"frame": 2,
+			"file" : "arrow-back-hover"
+		}
+	]
+
+}

+ 20 - 0
Mods/vcmi/Content/Sprites/campaigns/next.json

@@ -0,0 +1,20 @@
+{
+	"basepath" : "campaigns/",
+ 
+	"images" :
+	[
+		{
+			"frame": 0,
+			"file" : "arrow-next-normal"
+		},
+		{
+			"frame": 1,
+			"file" : "arrow-next-down"
+		},
+		{
+			"frame": 2,
+			"file" : "arrow-next-hover"
+		}
+	]
+
+}

+ 0 - 1
Mods/vcmi/Content/config/chinese.json

@@ -226,7 +226,6 @@
 	"vcmi.lobby.pvp.versus" : "对战",
 
 	"vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
 	"vcmi.client.errors.modLoadingFailure" : "{加载mod失败}\n\n加载mod发生致命错误!游戏可能无法正常进行,也可能崩溃。请升级或禁用下列mod:\n\n",
 	"vcmi.server.errors.disconnected" : "{网络错误}\n\n与游戏服务器的连接已断开!",
 	"vcmi.server.errors.playerLeft" : "{玩家离开}\n\n%s玩家已断开游戏!", //%s -> player color

+ 3 - 1
Mods/vcmi/Content/config/czech.json

@@ -179,6 +179,7 @@
 	"vcmi.lobby.match.duel" : "Hra s %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d hráčů",
 	"vcmi.lobby.room.create.hover" : "Vytvořit novou místnost",
+	"vcmi.lobby.room.create.help" : "Vytvoř novou místnost v online lobby, ke které se mohou připojit ostatní hráči.",
 	"vcmi.lobby.room.players.limit" : "Omezení počtu hráčů",
 	"vcmi.lobby.room.description.public" : "Jakýkoliv hráč se může připojit do veřejné místnosti.",
 	"vcmi.lobby.room.description.private" : "Pouze pozvaní hráči se mohou připojit do soukromé místnosti.",
@@ -201,6 +202,8 @@
 	"vcmi.lobby.preview.error.mods" : "Použváte jinou sadu modifikací.",
 	"vcmi.lobby.preview.error.version" : "Používáte jinou verzi VCMI.",
 	"vcmi.lobby.channel.add" : "Přidat kanál",
+	"vcmi.lobby.channel.sendMessage.hover" : "Odeslat zprávu",
+	"vcmi.lobby.channel.sendMessage.help" : "Odeslat zprávu",
 	"vcmi.lobby.room.new" : "Nová hra",
 	"vcmi.lobby.room.load" : "Načíst hru",
 	"vcmi.lobby.room.type" : "Druh místnosti",
@@ -223,7 +226,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Neplatná mapa nebo kampaň}\n\nChyba při startu hry! Vybraná mapa nebo kampaň může být neplatná nebo poškozená. Důvod:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.",
 	"vcmi.client.errors.modLoadingFailure" : "{Chyba při načítání modifikací}\n\nPři načítání modifikací byly nalezeny kritické problémy! Hra nemusí fungovat správně nebo může spadnout! Aktualizujte nebo deaktivujte následující modifikace:\n\n",
 	"vcmi.server.errors.disconnected" : "{Chyba sítě}\n\nPřipojení k hernímu serveru bylo ztraceno!",
 	"vcmi.server.errors.playerLeft" : "{Hráč opustil hru}\n\nHráč %s se odpojil ze hry!", //%s -> player color

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

@@ -226,7 +226,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
 	"vcmi.client.errors.modLoadingFailure" : "{Mod loading failure}\n\nCritical issues found when loading mods! Game may not work correctly or crash! Please update or disable following mods:\n\n",
 	"vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!",
 	"vcmi.server.errors.playerLeft" : "{Player Left}\n\n%s player have disconnected from the game!", //%s -> player color

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

@@ -226,7 +226,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Ungültige Karte oder Kampagne}\n\nDas Spiel konnte nicht gestartet werden! Die ausgewählte Karte oder Kampagne ist möglicherweise ungültig oder beschädigt. Grund:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.",
 	"vcmi.client.errors.modLoadingFailure" : "{Mod Ladefehler}\n\nKritische Probleme beim Laden von Mods gefunden! Das Spiel könnte nicht korrekt funktionieren oder abstürzen! Bitte aktualisiere oder deaktiviere folgende Mods:\n\n",
 	"vcmi.server.errors.disconnected" : "{Netzwerkfehler}\n\nDie Verbindung zum Spielserver wurde unterbrochen!",
 	"vcmi.server.errors.playerLeft" : "{Verlassen eines Spielers}\n\n%s Spieler hat die Verbindung zum Spiel unterbrochen!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/hungarian.json

@@ -219,7 +219,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Érvénytelen térkép vagy kampány}\n\nNem sikerült elindítani a játékot! A kiválasztott térkép vagy kampány érvénytelen vagy sérült lehet. Indok:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Hiányzó adatfájlok}\n\nNem találhatók kampány adatfájlok! Lehet, hogy hiányos vagy sérült Heroes 3 adatfájlokat használ. Telepítse újra az adatokat.",
 	"vcmi.client.errors.modLoadingFailure" : "{Mod betöltési hiba}\n\nKritikus problémák léptek fel a modok betöltésekor! A játék nem működhet megfelelően, vagy összeomolhat! Frissítse vagy tiltsa le az alábbi modokat:\n\n",
 	"vcmi.server.errors.disconnected" : "{Hálózati hiba}\n\nA kapcsolat a játékszerverrel megszakadt!",
 	"vcmi.server.errors.playerLeft" : "{Játékos kilépett}\n\n%s játékos kilépett a játékból!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/italian.json

@@ -221,7 +221,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Mappa o campagna non valida}\n\nImpossibile avviare la partita! La mappa o la campagna selezionata potrebbe essere non valida o corrotta. Motivo:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{File dati mancanti}\n\nI file dati delle campagne non sono stati trovati! Potresti avere file dati incompleti o corrotti di Heroes 3. Reinstalla i dati del gioco.",
 	"vcmi.client.errors.modLoadingFailure" : "{Errore di caricamento delle mod}\n\nSono stati rilevati problemi critici durante il caricamento delle mod! Il gioco potrebbe non funzionare correttamente o bloccarsi! Aggiorna o disattiva le seguenti mod:\n\n",
 	"vcmi.server.errors.disconnected" : "{Errore di rete}\n\nLa connessione al server di gioco è stata persa!",
 	"vcmi.server.errors.playerLeft" : "{Giocatore disconnesso}\n\nIl giocatore %s si è disconnesso dalla partita!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/polish.json

@@ -221,7 +221,6 @@
 	"vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć?",
 
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
 	"vcmi.server.errors.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!",
 	"vcmi.server.errors.playerLeft" : "{Rozłączenie z graczem}\n\n%s opuścił rozgrywkę!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",

+ 0 - 1
Mods/vcmi/Content/config/portuguese.json

@@ -220,7 +220,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Mapa ou campanha inválido}\n\nFalha ao iniciar o jogo! O mapa ou campanha selecionado pode ser inválido ou corrompido. Motivo:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Arquivos de dados ausentes}\n\nOs arquivos de dados das campanhas não foram encontrados! Você pode estar usando arquivos de dados incompletos ou corrompidos do Heroes 3. Por favor, reinstale os dados do jogo.",
 	"vcmi.client.errors.modLoadingFailure" : "{Falha ao carregar mod}\n\nProblemas críticos encontrados ao carregar mods! O jogo pode não funcionar corretamente ou travar! Por favor, atualize ou desative os seguintes mods:\n\n",
 	"vcmi.server.errors.disconnected" : "{Erro de Rede}\n\nA conexão com o servidor do jogo foi perdida!",
 	"vcmi.server.errors.playerLeft" : "{Jogador Saiu}\n\nO jogador %s desconectou-se do jogo!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/russian.json

@@ -218,7 +218,6 @@
 	"vcmi.lobby.pvp.randomTownVs.help" : "Написать в чате два случайных города",
 	"vcmi.lobby.pvp.versus" : "против",
 	"vcmi.client.errors.invalidMap" : "{Недопустимая карта или кампания}\n\nНе удалось запустить игру! Возможно, выбранная карта или кампания недействительны или повреждены. Причина:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Отсутствуют файлы данных}\n\nФайлы данных кампаний не найдены! Возможно, вы используете неполные или поврежденные файлы данных Heroes 3. Пожалуйста, переустановите игровые данные.",
 	"vcmi.client.errors.modLoadingFailure" : "{Ошибка загрузки модов}\n\nПри загрузке модов обнаружены серьезные проблемы! Игра может работать некорректно или вылетать! Пожалуйста, обновите или отключите следующие моды:\n\n",
 	"vcmi.server.errors.disconnected" : "{Сетевая ошибка}\n\nПодключение к игровому серверу потеряно!",
 	"vcmi.server.errors.playerLeft" : "{Игрок вышел}\n\n%s игрок отключился от игры!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/spanish.json

@@ -72,7 +72,6 @@
 	"vcmi.lobby.sortDate" : "Ordena los mapas por la fecha de modificación",
 
 	"vcmi.client.errors.invalidMap" : "{Mapa o Campaña invalido}\n\n¡No se pudo iniciar el juego! El mapa o la campaña seleccionados pueden no ser válidos o estar dañados. Motivo:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Archivos de datos faltantes}\n\n¡No se encontraron los archivos de datos de las campañas! Quizás estés utilizando archivos de datos incompletos o dañados de Heroes 3. Por favor, reinstala los datos del juego.",
 	"vcmi.server.errors.existingProcess" : "Otro servidor VCMI está en ejecución. Por favor, termínalo antes de comenzar un nuevo juego.",
 	"vcmi.server.errors.modsToEnable"    : "{Se requieren los siguientes mods}",
 	"vcmi.server.errors.modsToDisable"   : "{Deben desactivarse los siguientes mods}",

+ 0 - 1
Mods/vcmi/Content/config/swedish.json

@@ -223,7 +223,6 @@
 	"vcmi.lobby.pvp.versus"              : "vs.",
 
 	"vcmi.client.errors.invalidMap"       : "{Ogiltig karta eller kampanj}\n\nMisslyckades med att starta spelet! Vald karta eller kampanj kan vara ogiltig eller skadad. Orsak:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Saknade datafiler}\n\nDatafiler för kampanjen hittades inte! Du kanske använder ofullständiga eller skadade Heroes 3-datafiler. Vänligen installera om speldata.",
 	"vcmi.client.errors.modLoadingFailure": "{Moddladdningsfel}\n\nKritiska fel upptäcktes vid laddning av moddar! Spelet kanske inte fungerar korrekt eller kraschar! Vänligen uppdatera eller inaktivera följande moddar:\n\n",
 	"vcmi.server.errors.disconnected"     : "{Nätverksfel}\n\nAnslutningen till spelservern har förlorats!",
 	"vcmi.server.errors.playerLeft"       : "{Spelare har lämnat}\n\n%s-spelare har kopplat bort sig från spelet!", //%s -> spelarens färg

+ 0 - 1
Mods/vcmi/Content/config/ukrainian.json

@@ -226,7 +226,6 @@
 	"vcmi.lobby.pvp.versus" : "проти",
 
 	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.client.errors.modLoadingFailure" : "{Помилка завантаження модифікації}\n\nВиявлено критичні проблеми при завантаженні модифікацій! Гра може працювати некоректно або аварійно завершитись! Будь ласка, оновіть або вимкніть наступні моди:\n\n",
 	"vcmi.server.errors.disconnected" : "{Помилка мережі}\n\nВтрачено зв'язок з сервером гри!",
 	"vcmi.server.errors.playerLeft" : "{Гравець покинув гру}\n\n%s гравець від'єднався від гри!", //%s -> player color

+ 0 - 1
Mods/vcmi/Content/config/vietnamese.json

@@ -219,7 +219,6 @@
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Bản đồ hoặc chiến dịch không hợp lệ}\n\nKhông thể bắt đầu trò chơi! Bản đồ hoặc chiến dịch đã chọn có thể không hợp lệ hoặc bị lỗi như sau:\n%s",
-	"vcmi.client.errors.missingCampaigns" : "{Thiếu tệp tin dữ liệu}\n\nKhông tìm thấy tệp tin dữ liệu của chiến dịch! Có thể bạn đang sử dụng các tệp tin dữ liệu Heroes 3 bị thiếu hoặc bị lỗi. Hãy thử cài đặt lại trò chơi.",
 	"vcmi.client.errors.modLoadingFailure" : "{Lỗi tải mod}\n\nĐã phát hiện ra lỗi nghiêm trọng khi tải mod! Trò chơi có thể không hoạt động chính xác hoặc bị văng! Hãy cập nhật hoặc tắt các mod sau:\n\n",
 	"vcmi.server.errors.disconnected" : "{Mạng bị lỗi}\n\nĐã mất kết nối tới máy chủ trò chơi!",
 	"vcmi.server.errors.playerLeft" : "{Người chơi}\n\n%s đã ngắt kết nối khỏi trò chơi!", //%s -> màu của người chơi

+ 4 - 0
client/gui/Shortcut.h

@@ -65,6 +65,10 @@ enum class EShortcut
 	MAIN_MENU_CAMPAIGN_ROE,
 	MAIN_MENU_CAMPAIGN_AB,
 	MAIN_MENU_CAMPAIGN_CUSTOM,
+	MAIN_MENU_CAMPAIGN_CHR,
+	MAIN_MENU_CAMPAIGN_HOTA,
+	MAIN_MENU_CAMPAIGN_WOG,
+	MAIN_MENU_CAMPAIGN_VCMI,
 
 	MAIN_MENU_HOTSEAT,
 	MAIN_MENU_LOBBY,

+ 4 - 0
client/gui/ShortcutHandler.cpp

@@ -150,6 +150,10 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"mainMenuCampaignRoe",      EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
 		{"mainMenuCampaignAb",       EShortcut::MAIN_MENU_CAMPAIGN_AB     },
 		{"mainMenuCampaignCustom",   EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
+		{"mainMenuCampaignChr",      EShortcut::MAIN_MENU_CAMPAIGN_CHR    },
+		{"mainMenuCampaignHota",     EShortcut::MAIN_MENU_CAMPAIGN_HOTA   },
+		{"mainMenuCampaignWog",      EShortcut::MAIN_MENU_CAMPAIGN_WOG    },
+		{"mainMenuCampaignVCMI",     EShortcut::MAIN_MENU_CAMPAIGN_VCMI   },
 		{"mainMenuLobby",            EShortcut::MAIN_MENU_LOBBY           },
 		{"lobbyBeginStandardGame",   EShortcut::LOBBY_BEGIN_STANDARD_GAME },
 		{"lobbyBeginCampaign",       EShortcut::LOBBY_BEGIN_CAMPAIGN      },

+ 124 - 10
client/mainmenu/CCampaignScreen.cpp

@@ -47,27 +47,83 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name)
 	: CWindowObject(BORDERED), campaignSet(name)
 {
 	OBJECT_CONSTRUCTION;
+	
+	const auto& campaigns = config[name]["items"].Vector();
 
-	for(const JsonNode & node : config[name]["images"].Vector())
+	// Define mapping of background name -> campaigns per page
+	const std::unordered_map<std::string, int> campaignsPerPageMap = {
+		{"CampaignBackground4", 4},
+		{"CampaignBackground5", 5},
+		{"CampaignBackground6", 6},
+		{"CampaignBackground7", 7},
+		{"CAMPBACK", 7},
+		{"CAMPBKX2", 7},
+		{"CampaignBackground8", 8}
+	};
+
+	// Process images and check if name is in the map
+	for (const JsonNode& node : config[name]["images"].Vector())
+	{
 		images.push_back(CMainMenu::createPicture(node));
 
-	if(!images.empty())
+		std::string imageName = node["name"].String();
+		auto it = campaignsPerPageMap.find(imageName);
+		if (it != campaignsPerPageMap.end())
+		{
+			campaignsPerPage = it->second;
+		}
+	}
+
+	if (!images.empty())
 	{
 		images[0]->center(); // move background to center
 		moveTo(images[0]->pos.topLeft()); // move everything else to center
 		images[0]->moveTo(pos.topLeft()); // restore moved twice background
 		pos = images[0]->pos; // fix height\width of this window
 	}
+	
+	for (const auto& node : campaigns)
+	{
+		auto button = std::make_shared<CCampaignButton>(node, config, campaignSet);
+		button->enable();
+		campButtons.push_back(button);
+	}
+
+	maxPages = (campaigns.size() + campaignsPerPage - 1) / campaignsPerPage;
+	
+	if (!config[name]["nextbutton"].isNull())
+	{
+		buttonNext = std::make_shared<CButton>(
+			Point(config[name]["nextbutton"]["x"].Integer(), config[name]["nextbutton"]["y"].Integer()),
+			AnimationPath::fromJson(config[name]["nextbutton"]["name"]),
+			std::make_pair("", ""),
+			[this, name]() { switchPage(1); }
+		);
+		buttonNext->setHoverable(true);
+		buttonNext->disable();
+	}
 
-	if(!config[name]["exitbutton"].isNull())
+	if (!config[name]["backbutton"].isNull())
+	{
+		buttonPrev = std::make_shared<CButton>(
+			Point(config[name]["backbutton"]["x"].Integer(), config[name]["backbutton"]["y"].Integer()),
+			AnimationPath::fromJson(config[name]["backbutton"]["name"]),
+			std::make_pair("", ""),
+			[this, name]() { switchPage(-1); }
+		);
+		buttonPrev->setHoverable(true);
+		buttonPrev->disable();
+	}
+
+	page = std::make_shared<CLabel>(10, 570, FONT_MEDIUM, ETextAlignment::BOTTOMLEFT, Colors::YELLOW, "");
+
+	if (!config[name]["exitbutton"].isNull())
 	{
 		buttonBack = createExitButton(config[name]["exitbutton"]);
 		buttonBack->setHoverable(true);
 	}
 
-	for(const JsonNode & node : config[name]["items"].Vector())
-		if(CResourceHandler::get()->existsResource(ResourcePath(node["file"].String(), EResType::CAMPAIGN)))
-			campButtons.push_back(std::make_shared<CCampaignButton>(node, config, campaignSet));
+	updateCampaignButtons(config);
 }
 
 void CCampaignScreen::activate()
@@ -101,11 +157,18 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
 
 	status = CCampaignScreen::ENABLED;
 
-	auto header = CampaignHandler::getHeader(campFile);
-	hoverText = header->getNameTranslated();
+	if(CResourceHandler::get()->existsResource(ResourcePath(campFile, EResType::CAMPAIGN)))
+	{
+		auto header = CampaignHandler::getHeader(campFile);
+		hoverText = header->getNameTranslated();
 
-	if(persistentStorage["completedCampaigns"][header->getFilename()].Bool())
-		status = CCampaignScreen::COMPLETED;
+		if (persistentStorage["completedCampaigns"][header->getFilename()].Bool())
+			status = CCampaignScreen::COMPLETED;
+	}
+	else
+	{
+		status = CCampaignScreen::DISABLED;
+	}
 
 	for(const JsonNode & node : parentConfig[campaignSet]["items"].Vector())
 	{
@@ -154,3 +217,54 @@ void CCampaignScreen::CCampaignButton::hover(bool on)
 			hoverLabel->setText(" ");
 	}
 }
+
+void CCampaignScreen::switchPage(int delta)
+{
+	currentPage += delta;
+	currentPage = std::clamp(currentPage, 0, maxPages - 1);
+
+	const auto& campaignConfig = CMainMenuConfig::get().getCampaigns();
+
+	updateCampaignButtons(campaignConfig);
+}
+
+void CCampaignScreen::updateCampaignButtons(const JsonNode & parentConfig)
+{
+	const auto& campaigns = parentConfig[campaignSet]["items"].Vector();
+
+	int minId = (currentPage * campaignsPerPage) + 1;
+	int maxId = minId + campaignsPerPage - 1;
+
+	for(size_t i = 0; i < campButtons.size(); ++i)
+	{
+		int campaignId = campaigns[i]["id"].Integer();
+
+		if(campaignId >= minId && campaignId <= maxId)
+			campButtons[i]->enable();
+		else
+			campButtons[i]->disable();
+
+		if(!CResourceHandler::get()->existsResource(ResourcePath(campaigns[i]["file"].String(), EResType::CAMPAIGN)))
+		{
+			campButtons[i]->disable();
+			logGlobal->warn("Campaign %s doesn't exist", campaigns[i]["file"].String());
+		}
+	}
+
+	if(buttonNext && buttonPrev)
+	{
+		page->setText(std::to_string(currentPage + 1) + "/" + std::to_string(maxPages));
+
+		if (maxId < campaigns.size())
+			buttonNext->enable();
+		else
+			buttonNext->disable();
+
+		if (currentPage > 0)
+			buttonPrev->enable();
+		else
+			buttonPrev->disable();
+	}
+
+	redraw();
+}

+ 10 - 0
client/mainmenu/CCampaignScreen.h

@@ -56,9 +56,19 @@ private:
 	std::vector<std::shared_ptr<CCampaignButton>> campButtons;
 	std::vector<std::shared_ptr<CPicture>> images;
 	std::shared_ptr<CButton> buttonBack;
+	std::shared_ptr<CButton> buttonNext;
+	std::shared_ptr<CButton> buttonPrev;
+	std::shared_ptr<CLabel> page;
 
 	std::shared_ptr<CButton> createExitButton(const JsonNode & button);
 
+	int campaignsPerPage = 8;
+	int currentPage = 0;
+	int maxPages = 0;
+
+	void switchPage(int delta);
+	void updateCampaignButtons(const JsonNode& parentConfig);
+
 public:
 	CCampaignScreen(const JsonNode & config, std::string campaignSet);
 

+ 0 - 16
client/mainmenu/CMainMenu.cpp

@@ -425,22 +425,6 @@ void CMainMenu::openCampaignScreen(std::string name)
 		return;
 	}
 
-	bool campaignsFound = true;
-	for (auto const & entry : config[name]["items"].Vector())
-	{
-		ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN);
-		if(entry["optional"].Bool())
-			continue;
-		if(!CResourceHandler::get()->existsResource(resourceID))
-			campaignsFound = false;
-	}
-
-	if (!campaignsFound)
-	{
-		CInfoWindow::showInfoDialog(LIBRARY->generaltexth->translate("vcmi.client.errors.missingCampaigns"), std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
-		return;
-	}
-
 	ENGINE->windows().createAndPushWindow<CCampaignScreen>(config, name);
 }
 

+ 88 - 23
client/render/AssetGenerator.cpp

@@ -43,7 +43,11 @@ void AssetGenerator::initialize()
 	imageFiles[ImagePath::builtin("combatUnitNumberWindowPositive.png")] = [this](){ return createCombatUnitNumberWindow(0.2f, 1.0f, 0.2f);};
 	imageFiles[ImagePath::builtin("combatUnitNumberWindowNegative.png")] = [this](){ return createCombatUnitNumberWindow(1.0f, 0.2f, 0.2f);};
 
-	imageFiles[ImagePath::builtin("CampaignBackground8.png")] = [this](){ return createCampaignBackground();};
+	imageFiles[ImagePath::builtin("CampaignBackground4.png")] = [this]() { return createCampaignBackground(4); };
+	imageFiles[ImagePath::builtin("CampaignBackground5.png")] = [this]() { return createCampaignBackground(5); };
+	imageFiles[ImagePath::builtin("CampaignBackground6.png")] = [this]() { return createCampaignBackground(6); };
+	imageFiles[ImagePath::builtin("CampaignBackground7.png")] = [this]() { return createCampaignBackground(7); };
+	imageFiles[ImagePath::builtin("CampaignBackground8.png")] = [this]() { return createCampaignBackground(8); };
 
 	for (PlayerColor color(0); color < PlayerColor::PLAYER_LIMIT; ++color)
 		imageFiles[ImagePath::builtin("DialogBoxBackground_" + color.toString())] = [this, color](){ return createPlayerColoredBackground(color);};
@@ -202,41 +206,102 @@ AssetGenerator::CanvasPtr AssetGenerator::createCombatUnitNumberWindow(float mul
 	return image;
 }
 
-AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground() const
+AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground(int selection) const
 {
-	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
 
+	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
 	std::shared_ptr<IImage> img = ENGINE->renderHandler().loadImage(locator);
+
 	auto image = ENGINE->renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
-
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
 
-	// left image
-	canvas.draw(img, Point(220, 73), Rect(290, 73, 141, 115));
-	canvas.draw(img, Point(37, 70), Rect(87, 70, 207, 120));
+	// BigBlock section
+	auto bigBlock = ENGINE->renderHandler().createImage(Point(248, 114), CanvasScalingPolicy::IGNORE);
+	Rect bigBlockRegion(292, 74, 248, 114);
+	Canvas croppedBigBlock = bigBlock->getCanvas();
+	croppedBigBlock.draw(img, Point(0, 0), bigBlockRegion);
+	bigBlock->scaleTo(Point(200, 114), EScalingAlgorithm::NEAREST);
+
+	// SmallBlock section
+	auto smallBlock = ENGINE->renderHandler().createImage(Point(248, 114), CanvasScalingPolicy::IGNORE);
+	Canvas croppedSmallBlock = smallBlock->getCanvas();
+	croppedSmallBlock.draw(img, Point(0, 0), bigBlockRegion);
+	smallBlock->scaleTo(Point(134, 114), EScalingAlgorithm::NEAREST);
+
+	// Tripple block section
+	auto trippleBlock = ENGINE->renderHandler().createImage(Point(72, 116), CanvasScalingPolicy::IGNORE);
+	Rect trippleBlockSection(512, 246, 72, 116);
+	Canvas croppedTrippleBlock = trippleBlock->getCanvas();
+	croppedTrippleBlock.draw(img, Point(0, 0), trippleBlockSection);
+	trippleBlock->scaleTo(Point(70, 114), EScalingAlgorithm::NEAREST);
+
+
+	// First campaigns line
+	if (selection > 7)
+	{
+		// Rebuild 1. campaigns line from 2 to 3 fields
+		canvas.draw(bigBlock, Point(40, 72));
+		canvas.draw(trippleBlock, Point(240, 73));
+		canvas.draw(bigBlock, Point(310, 72));
+		canvas.draw(trippleBlock, Point(510, 72));
+		canvas.draw(bigBlock, Point(580, 72));
+		canvas.draw(trippleBlock, Point(780, 72));
+	} 
+	else
+	{
+		// Empty 1 + 2. field
+		canvas.draw(bigBlock, Point(90, 72));
+		canvas.draw(bigBlock, Point(540, 72));
+	}
 
-	// right image
-	canvas.draw(img, Point(513, 67), Rect(463, 67, 71, 126));
-	canvas.draw(img, Point(586, 71), Rect(536, 71, 207, 117));
+	// Second campaigns line
+	// 3. Field
+	canvas.draw(bigBlock, Point(43, 245));
 
-	// middle image
-	canvas.draw(img, Point(306, 68), Rect(86, 68, 209, 122));
+	if (selection == 4)
+	{
+		// Disabled 4. field
+		canvas.draw(trippleBlock, Point(310, 245));
+		canvas.draw(smallBlock, Point(380, 245));
+	}
+	else
+	{
+		// Empty 4. field
+		canvas.draw(bigBlock, Point(314, 244));
+	}
+	
+	// 5. Field
+	canvas.draw(bigBlock, Point(586, 246));
 
-	// disabled fields
-	canvas.draw(img, Point(40, 72), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(310, 72), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(590, 72), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(43, 245), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(313, 244), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(586, 246), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(34, 417), Rect(313, 74, 197, 114));
-	canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114));
+	// Third campaigns line
+	// 6. Field
+	if (selection >= 6)
+	{
+		canvas.draw(bigBlock, Point(32, 417));
+	}
+	else
+	{
+		canvas.draw(trippleBlock, Point(30, 417));
+		canvas.draw(smallBlock, Point(100, 417));
+	}
 
-	// skull
 	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"), EImageBlitMode::OPAQUE);
 	std::shared_ptr<IImage> imgSkull = ENGINE->renderHandler().loadImage(locatorSkull);
-	canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19));
+
+	if (selection >= 7)
+	{
+		// Only skull part
+		canvas.draw(bigBlock, Point(404, 417));
+		canvas.draw(imgSkull, Point(563, 512), Rect(178, 108, 43, 19));
+	}
+	else
+	{
+		// Original disabled field with skull and stone for 8. field
+		imgSkull->scaleTo(Point(238, 150), EScalingAlgorithm::NEAREST);
+		canvas.draw(imgSkull, Point(385, 400));
+	}
+
 
 	return image;
 }

+ 1 - 1
client/render/AssetGenerator.h

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

+ 7 - 8
config/campaignSets.json

@@ -18,9 +18,8 @@
 	{
 		"images" :
 		[
-			{"x": 0,   "y": 0,   "name":"CAMPBACK"},
-			{"x": 34,  "y": 417, "name":"CAMP1FWX"},//one campaign have special inactive image
-			{"x": 385, "y": 401, "name":"CAMPNOSC"},//and the last one is not present
+			{"x": 0,   "y": 0,   "name":"CampaignBackground6"},
+			{"x": 34,  "y": 417, "name":"CAMP1FWX"}				// 6. campaign have special inactive image
 		],
 		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
 		"items":
@@ -54,14 +53,14 @@
 		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
 		"items":
 		[
-			{ "id": 1, "x":40,  "y":72,  "file":"Maps/Chronicles/Hc1_Main", "image":"CampaignHc1Image", "video":"", "requires": [], "optional": true },
-			{ "id": 2, "x":310, "y":72,  "file":"Maps/Chronicles/Hc2_Main", "image":"CampaignHc2Image", "video":"", "requires": [], "optional": true },
-			{ "id": 3, "x":590, "y":72,  "file":"Maps/Chronicles/Hc3_Main", "image":"CampaignHc3Image", "video":"", "requires": [], "optional": true },
+			{ "id": 1, "x":41,  "y":72,  "file":"Maps/Chronicles/Hc1_Main", "image":"CampaignHc1Image", "video":"", "requires": [], "optional": true },
+			{ "id": 2, "x":312, "y":72,  "file":"Maps/Chronicles/Hc2_Main", "image":"CampaignHc2Image", "video":"", "requires": [], "optional": true },
+			{ "id": 3, "x":581, "y":72,  "file":"Maps/Chronicles/Hc3_Main", "image":"CampaignHc3Image", "video":"", "requires": [], "optional": true },
 			{ "id": 4, "x":43,  "y":245, "file":"Maps/Chronicles/Hc4_Main", "image":"CampaignHc4Image", "video":"", "requires": [], "optional": true },
 			{ "id": 5, "x":313, "y":244, "file":"Maps/Chronicles/Hc5_Main", "image":"CampaignHc5Image", "video":"", "requires": [], "optional": true },
-			{ "id": 6, "x":586, "y":244, "file":"Maps/Chronicles/Hc6_Main", "image":"CampaignHc6Image", "video":"", "requires": [], "optional": true },
+			{ "id": 6, "x":585, "y":244, "file":"Maps/Chronicles/Hc6_Main", "image":"CampaignHc6Image", "video":"", "requires": [], "optional": true },
 			{ "id": 7, "x":34,  "y":413, "file":"Maps/Chronicles/Hc7_Main", "image":"CampaignHc7Image", "video":"", "requires": [], "optional": true },
-			{ "id": 8, "x":404, "y":414, "file":"Maps/Chronicles/Hc8_Main", "image":"CampaignHc8Image", "video":"", "requires": [], "optional": true }
+			{ "id": 8, "x":404, "y":415, "file":"Maps/Chronicles/Hc8_Main", "image":"CampaignHc8Image", "video":"", "requires": [], "optional": true }
 		]
 	}
 }

+ 4 - 0
config/shortcutsConfig.json

@@ -165,6 +165,10 @@
 		"mainMenuCampaignCustom":   "C",
 		"mainMenuCampaignRoe":      "R",
 		"mainMenuCampaignSod":      "S",
+		"mainMenuCampaignChr":      "T",
+		"mainMenuCampaignHota":     "H",
+		"mainMenuCampaignWog":      "W",
+		"mainMenuCampaignVCMI":     "V",
 		"mainMenuCredits":          "C",
 		"mainMenuHighScores":       "H",
 		"mainMenuHostGame":         "C",