From 45aeba74c362c59075c95a2e6e0fa1444183ded3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:43:25 +0100 Subject: [PATCH 01/22] tab support for battle mode --- Mods/vcmi/Content/config/english.json | 1 + Mods/vcmi/Content/config/german.json | 1 + client/NetPacksLobbyClient.cpp | 9 ++--- client/lobby/BattleOnlyMode.cpp | 55 +++++++++------------------ client/lobby/BattleOnlyMode.h | 22 +++++------ client/lobby/CLobbyScreen.cpp | 10 +++++ client/lobby/CSelectionBase.h | 2 + client/lobby/SelectionTab.cpp | 7 ++-- client/widgets/TextControls.cpp | 13 ++++--- 9 files changed, 55 insertions(+), 65 deletions(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 2aa81d5a6..10934c7e9 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -139,6 +139,7 @@ "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", "vcmi.lobby.battleOnlyMode" : "Battle only mode", + "vcmi.lobby.battleOnlyModeSubTitle" : "Select heroes and army for simple battle without adventure map", "vcmi.lobby.battleOnlyModeBattlefield" : "Battlefield", "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Select Battlefield", "vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index c05a712f8..4d8dc528e 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -139,6 +139,7 @@ "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", "vcmi.lobby.battleOnlyMode" : "Nur Kämpfen Modus", + "vcmi.lobby.battleOnlyModeSubTitle" : "Wähle Helden und eine Armee für einen einfachen Kampf ohne Abenteuerkarte", "vcmi.lobby.battleOnlyModeBattlefield" : "Schlachtfeld", "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Schlachtfeld auswählen", "vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen", diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index f286af001..444e6e4e4 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -114,9 +114,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack if(!lobby || !handler.isGuest()) return; - if(auto topWindow = ENGINE->windows().topWindow()) - topWindow->close(); - switch(pack.action) { case LobbyGuiAction::NO_TAB: @@ -138,7 +135,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack lobby->toggleTab(lobby->tabExtraOptions); break; case LobbyGuiAction::BATTLE_MODE: - BattleOnlyMode::openBattleWindow(); + lobby->toggleTab(lobby->tabBattleOnlyMode); break; } } @@ -242,6 +239,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyShowMessage(LobbyShowMessage & void ApplyOnLobbyScreenNetPackVisitor::visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) { - if(auto topWindow = ENGINE->windows().topWindow()) - topWindow->applyStartInfo(pack.startInfo); + if(lobby->tabBattleOnlyMode) + lobby->tabBattleOnlyMode->applyStartInfo(pack.startInfo); } diff --git a/client/lobby/BattleOnlyMode.cpp b/client/lobby/BattleOnlyMode.cpp index d9b71da35..f158fdbdf 100644 --- a/client/lobby/BattleOnlyMode.cpp +++ b/client/lobby/BattleOnlyMode.cpp @@ -56,39 +56,21 @@ #include "../../lib/texts/TextOperations.h" #include "../../lib/filesystem/Filesystem.h" -void BattleOnlyMode::openBattleWindow() -{ - GAME->server().sendGuiAction(LobbyGuiAction::BATTLE_MODE); - ENGINE->windows().createAndPushWindow(); -} - -BattleOnlyModeWindow::BattleOnlyModeWindow() - : CWindowObject(BORDERED) - , startInfo(std::make_shared()) +BattleOnlyModeTab::BattleOnlyModeTab() + : startInfo(std::make_shared()) , disabledColor(GAME->server().isHost() ? Colors::WHITE : Colors::ORANGE) { OBJECT_CONSTRUCTION; - pos.w = 519; - pos.h = 238; - - updateShadow(); - center(); - init(); - backgroundTexture = std::make_shared(Rect(0, 0, pos.w, pos.h)); - backgroundTexture->setPlayerColor(PlayerColor(1)); - buttonOk = std::make_shared(Point(191, 203), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT); + backgroundImage = std::make_shared(ImagePath::builtin("AdventureOptionsBackgroundClear"), 0, 6); + buttonOk = std::make_shared(Point(148, 430), AnimationPath::builtin("CBBEGIB"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT); buttonOk->block(true); - buttonAbort = std::make_shared(Point(265, 203), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ - GAME->server().sendGuiAction(LobbyGuiAction::NO_TAB); - close(); - }, EShortcut::GLOBAL_CANCEL); - buttonAbort->block(true); - title = std::make_shared(260, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")); + title = std::make_shared(220, 35, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")); + subTitle = std::make_shared(Rect(55, 40, 333, 40), FONT_SMALL, ETextAlignment::BOTTOMCENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSubTitle")); - battlefieldSelector = std::make_shared(Point(29, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ + battlefieldSelector = std::make_shared(Point(120, 370), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ std::vector texts; std::vector> images; @@ -137,7 +119,7 @@ BattleOnlyModeWindow::BattleOnlyModeWindow() }, (startInfo->selectedTerrain ? static_cast(*startInfo->selectedTerrain) : static_cast(*startInfo->selectedTown + terrains.size())), images, true, true); }); battlefieldSelector->block(GAME->server().isGuest()); - buttonReset = std::make_shared(Point(289, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ + buttonReset = std::make_shared(Point(120, 400), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ if(GAME->server().isHost()) { startInfo->selectedTerrain = TerrainId::DIRT; @@ -155,15 +137,15 @@ BattleOnlyModeWindow::BattleOnlyModeWindow() }); buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE); - heroSelector1 = std::make_shared(0, *this, Point(0, 40)); - heroSelector2 = std::make_shared(1, *this, Point(260, 40)); + heroSelector1 = std::make_shared(0, *this, Point(91, 90)); + heroSelector2 = std::make_shared(1, *this, Point(91, 225)); heroSelector1->setInputEnabled(GAME->server().isHost()); onChange(); } -void BattleOnlyModeWindow::init() +void BattleOnlyModeTab::init() { map = std::make_unique(nullptr); map->version = EMapFormat::VCMI; @@ -177,12 +159,12 @@ void BattleOnlyModeWindow::init() cb = std::make_unique(map.get()); } -void BattleOnlyModeWindow::onChange() +void BattleOnlyModeTab::onChange() { GAME->server().setBattleOnlyModeStartInfo(startInfo); } -void BattleOnlyModeWindow::update() +void BattleOnlyModeTab::update() { setTerrainButtonText(); setOkButtonEnabled(); @@ -194,25 +176,24 @@ void BattleOnlyModeWindow::update() redraw(); } -void BattleOnlyModeWindow::applyStartInfo(std::shared_ptr si) +void BattleOnlyModeTab::applyStartInfo(std::shared_ptr si) { startInfo = si; update(); } -void BattleOnlyModeWindow::setTerrainButtonText() +void BattleOnlyModeTab::setTerrainButtonText() { battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ": " + (startInfo->selectedTerrain ? (*startInfo->selectedTerrain).toEntity(LIBRARY)->getNameTranslated() : (*startInfo->selectedTown).toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor); } -void BattleOnlyModeWindow::setOkButtonEnabled() +void BattleOnlyModeTab::setOkButtonEnabled() { bool army2Empty = std::all_of(startInfo->selectedArmy[1].begin(), startInfo->selectedArmy[1].end(), [](const auto x) { return x.getId() == CreatureID::NONE; }); bool canStart = (startInfo->selectedTerrain || startInfo->selectedTown); canStart &= (startInfo->selectedHero[0] && ((startInfo->selectedHero[1]) || (startInfo->selectedTown && !army2Empty))); buttonOk->block(!canStart || GAME->server().isGuest()); - buttonAbort->block(GAME->server().isGuest()); } std::shared_ptr drawBlackBox(Point size, std::string text, ColorRGBA color) @@ -224,7 +205,7 @@ std::shared_ptr drawBlackBox(Point size, std::string text, ColorRGBA col return image; } -BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& p, Point position) +BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& p, Point position) : parent(p) , id(id) { @@ -440,7 +421,7 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() } } -void BattleOnlyModeWindow::startBattle() +void BattleOnlyModeTab::startBattle() { auto rng = &CRandomGenerator::getDefault(); diff --git a/client/lobby/BattleOnlyMode.h b/client/lobby/BattleOnlyMode.h index eac3f08ef..1b33e79a6 100644 --- a/client/lobby/BattleOnlyMode.h +++ b/client/lobby/BattleOnlyMode.h @@ -11,6 +11,7 @@ #include "../windows/CWindowObject.h" #include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/mapping/CMap.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,22 +26,17 @@ class FilledTexturePlayerColored; class CButton; class CPicture; class CLabel; -class BattleOnlyModeWindow; +class CMultiLineLabel; +class BattleOnlyModeTab; class CAnimImage; class GraphicalPrimitiveCanvas; class CTextInput; class TransparentFilledRectangle; -class BattleOnlyMode -{ -public: - static void openBattleWindow(); -}; - class BattleOnlyModeHeroSelector : public CIntObject { private: - BattleOnlyModeWindow& parent; + BattleOnlyModeTab& parent; std::shared_ptr backgroundImage; std::shared_ptr heroImage; @@ -57,10 +53,10 @@ public: void setHeroIcon(); void setCreatureIcons(); - BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& parent, Point position); + BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& parent, Point position); }; -class BattleOnlyModeWindow : public CWindowObject +class BattleOnlyModeTab : public CIntObject { friend class BattleOnlyModeHeroSelector; private: @@ -68,10 +64,10 @@ private: std::unique_ptr map; std::shared_ptr cb; - std::shared_ptr backgroundTexture; + std::shared_ptr backgroundImage; std::shared_ptr buttonOk; - std::shared_ptr buttonAbort; std::shared_ptr title; + std::shared_ptr subTitle; std::shared_ptr battlefieldSelector; std::shared_ptr buttonReset; @@ -87,6 +83,6 @@ private: void setOkButtonEnabled(); void startBattle(); public: - BattleOnlyModeWindow(); + BattleOnlyModeTab(); void applyStartInfo(std::shared_ptr si); }; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 78941a833..b9faf88b2 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -16,6 +16,7 @@ #include "OptionsTab.h" #include "RandomMapTab.h" #include "SelectionTab.h" +#include "BattleOnlyMode.h" #include "../CServerHandler.h" #include "../GameEngine.h" @@ -146,6 +147,8 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) GAME->server().sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS); else if(tab == tabExtraOptions) GAME->server().sendGuiAction(LobbyGuiAction::OPEN_EXTRA_OPTIONS); + else if(tab == tabBattleOnlyMode) + GAME->server().sendGuiAction(LobbyGuiAction::BATTLE_MODE); CSelectionBase::toggleTab(tab); } @@ -239,6 +242,13 @@ void CLobbyScreen::toggleChat() void CLobbyScreen::updateAfterStateChange() { + OBJECT_CONSTRUCTION; + if(!tabBattleOnlyMode) + { + tabBattleOnlyMode = std::make_shared(); + tabBattleOnlyMode->setEnabled(false); + } + if(GAME->server().isHost() && screenType == ESelectionScreen::newGame) { bool isMultiplayer = GAME->server().loadMode == ELoadMode::MULTI; diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index bb9b0f373..388845165 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -29,6 +29,7 @@ class OptionsTab; class TurnOptionsTab; class ExtraOptionsTab; class SelectionTab; +class BattleOnlyModeTab; class InfoCard; class CChatBox; class PvPBox; @@ -78,6 +79,7 @@ public: std::shared_ptr tabTurnOptions; std::shared_ptr tabExtraOptions; std::shared_ptr tabRand; + std::shared_ptr tabBattleOnlyMode; std::shared_ptr curTab; CSelectionBase(ESelectionScreen type); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index ddd8afb34..adf45aa50 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -244,9 +244,10 @@ SelectionTab::SelectionTab(ESelectionScreen Type) if(tabType == ESelectionScreen::newGame) { - buttonBattleOnlyMode = std::make_shared(Point(23, 18), AnimationPath::builtin("lobby/battleButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")), [tabTitle, tabTitleDelete](){ - BattleOnlyMode::openBattleWindow(); - }); + buttonBattleOnlyMode = std::make_shared(Point(23, 18), AnimationPath::builtin("lobby/battleButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")), [this](){ + auto lobby = static_cast(parent); + lobby->toggleTab(lobby->tabBattleOnlyMode); + }, EShortcut::LOBBY_ADDITIONAL_OPTIONS); } if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 245e02a1c..df5542728 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -351,12 +351,13 @@ Rect CMultiLineLabel::getTextLocation() switch(alignment) { - case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSizeComputed); - case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSizeComputed); - case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSizeComputed); - case ETextAlignment::CENTERLEFT: return Rect(pos.topLeft() + Point(0, textOffset.y / 2), textSizeComputed); - case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed); - case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSizeComputed); + case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSizeComputed); + case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSizeComputed); + case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSizeComputed); + case ETextAlignment::CENTERLEFT: return Rect(pos.topLeft() + Point(0, textOffset.y / 2), textSizeComputed); + case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed); + case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSizeComputed); + case ETextAlignment::BOTTOMCENTER: return Rect(pos.topLeft() + Point(textOffset.x / 2, textOffset.y), textSizeComputed); } assert(0); return Rect(); From 408e83eb9a0abbd25fd7467608d70f1c250f0c6b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:27:14 +0100 Subject: [PATCH 02/22] rename file --- client/CMakeLists.txt | 4 ++-- client/NetPacksLobbyClient.cpp | 2 +- client/lobby/{BattleOnlyMode.cpp => BattleOnlyModeTab.cpp} | 4 ++-- client/lobby/{BattleOnlyMode.h => BattleOnlyModeTab.h} | 2 +- client/lobby/CLobbyScreen.cpp | 2 +- client/lobby/SelectionTab.cpp | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename client/lobby/{BattleOnlyMode.cpp => BattleOnlyModeTab.cpp} (99%) rename client/lobby/{BattleOnlyMode.h => BattleOnlyModeTab.h} (98%) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 25b009a59..95b93ea5d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -52,7 +52,7 @@ set(vcmiclientcommon_SRCS gui/ShortcutHandler.cpp gui/WindowHandler.cpp - lobby/BattleOnlyMode.cpp + lobby/BattleOnlyModeTab.cpp lobby/CBonusSelection.cpp lobby/CCampaignInfoScreen.cpp lobby/CLobbyScreen.cpp @@ -263,7 +263,7 @@ set(vcmiclientcommon_HEADERS gui/TextAlignment.h gui/WindowHandler.h - lobby/BattleOnlyMode.h + lobby/BattleOnlyModeTab.h lobby/CBonusSelection.h lobby/CCampaignInfoScreen.h lobby/CLobbyScreen.h diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 444e6e4e4..ef5b49ce9 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -19,7 +19,7 @@ #include "lobby/ExtraOptionsTab.h" #include "lobby/SelectionTab.h" #include "lobby/CBonusSelection.h" -#include "lobby/BattleOnlyMode.h" +#include "lobby/BattleOnlyModeTab.h" #include "globalLobby/GlobalLobbyWindow.h" #include "globalLobby/GlobalLobbyServerSetup.h" #include "globalLobby/GlobalLobbyClient.h" diff --git a/client/lobby/BattleOnlyMode.cpp b/client/lobby/BattleOnlyModeTab.cpp similarity index 99% rename from client/lobby/BattleOnlyMode.cpp rename to client/lobby/BattleOnlyModeTab.cpp index f158fdbdf..dc5b771e6 100644 --- a/client/lobby/BattleOnlyMode.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -1,5 +1,5 @@ /* - * BattleOnlyMode.cpp, part of VCMI engine + * BattleOnlyModeTab.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,7 +9,7 @@ */ #include "StdInc.h" -#include "BattleOnlyMode.h" +#include "BattleOnlyModeTab.h" #include "../CServerHandler.h" #include "../GameEngine.h" diff --git a/client/lobby/BattleOnlyMode.h b/client/lobby/BattleOnlyModeTab.h similarity index 98% rename from client/lobby/BattleOnlyMode.h rename to client/lobby/BattleOnlyModeTab.h index 1b33e79a6..bf7d621d0 100644 --- a/client/lobby/BattleOnlyMode.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -1,5 +1,5 @@ /* - * BattleOnlyMode.h, part of VCMI engine + * BattleOnlyModeTab.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index b9faf88b2..bac09568a 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -16,7 +16,7 @@ #include "OptionsTab.h" #include "RandomMapTab.h" #include "SelectionTab.h" -#include "BattleOnlyMode.h" +#include "BattleOnlyModeTab.h" #include "../CServerHandler.h" #include "../GameEngine.h" diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index adf45aa50..517988622 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -12,7 +12,7 @@ #include "SelectionTab.h" #include "CSelectionBase.h" #include "CLobbyScreen.h" -#include "BattleOnlyMode.h" +#include "BattleOnlyModeTab.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" From 59936017fb6bf3d8df4755d4808ede8b46c4d076 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:43:18 +0100 Subject: [PATCH 03/22] shortcut --- Mods/vcmi/Content/config/english.json | 1 + Mods/vcmi/Content/config/german.json | 1 + client/gui/Shortcut.h | 1 + client/gui/ShortcutHandler.cpp | 1 + client/lobby/SelectionTab.cpp | 2 +- config/keyBindingsConfig.json | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 10934c7e9..37cfdb8b3 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -437,6 +437,7 @@ "vcmi.keyBindings.keyBinding.lobbyToggleChat": "Lobby toggle chat", "vcmi.keyBindings.keyBinding.lobbyTurnOptions": "Lobby turn options", "vcmi.keyBindings.keyBinding.lobbyCampaignSets": "Lobby campaign sets", + "vcmi.keyBindings.keyBinding.lobbyBattleMode": "Lobby battle mode", "vcmi.keyBindings.keyBinding.mainMenuBack": "Main menu back", "vcmi.keyBindings.keyBinding.mainMenuCampaign": "Main menu campaign", "vcmi.keyBindings.keyBinding.mainMenuCampaignAb": "Main menu campaign ab", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index 4d8dc528e..edcec5c94 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -445,6 +445,7 @@ "vcmi.keyBindings.keyBinding.lobbyToggleChat": "Lobby Chat umschalten", "vcmi.keyBindings.keyBinding.lobbyTurnOptions": "Lobby Zugoptionen", "vcmi.keyBindings.keyBinding.lobbyCampaignSets": "Lobby Kampagnensätze", + "vcmi.keyBindings.keyBinding.lobbyBattleMode": "Lobby Kampfmodus", "vcmi.keyBindings.keyBinding.mainMenuBack": "Hauptmenü zurück", "vcmi.keyBindings.keyBinding.mainMenuCampaign": "Hauptmenü Kampagne", "vcmi.keyBindings.keyBinding.mainMenuCampaignAb": "Hauptmenü Kampagne Ab", diff --git a/client/gui/Shortcut.h b/client/gui/Shortcut.h index 858309055..905fcc2fd 100644 --- a/client/gui/Shortcut.h +++ b/client/gui/Shortcut.h @@ -98,6 +98,7 @@ enum class EShortcut LOBBY_RANDOM_TOWN_VS, LOBBY_HANDICAP, LOBBY_CAMPAIGN_SETS, + LOBBY_BATTLE_MODE, MAPS_SIZE_S, MAPS_SIZE_M, diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index b81096cdd..2870a2f19 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -162,6 +162,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"lobbyToggleChat", EShortcut::LOBBY_TOGGLE_CHAT }, {"lobbyAdditionalOptions", EShortcut::LOBBY_ADDITIONAL_OPTIONS }, {"lobbySelectScenario", EShortcut::LOBBY_SELECT_SCENARIO }, + {"lobbyBattleMode", EShortcut::LOBBY_BATTLE_MODE }, {"gameEndTurn", EShortcut::ADVENTURE_END_TURN }, // compatibility ID - extra's use this string {"adventureEndTurn", EShortcut::ADVENTURE_END_TURN }, {"adventureLoadGame", EShortcut::ADVENTURE_LOAD_GAME }, diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 517988622..a0416842b 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -247,7 +247,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) buttonBattleOnlyMode = std::make_shared(Point(23, 18), AnimationPath::builtin("lobby/battleButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")), [this](){ auto lobby = static_cast(parent); lobby->toggleTab(lobby->tabBattleOnlyMode); - }, EShortcut::LOBBY_ADDITIONAL_OPTIONS); + }, EShortcut::LOBBY_BATTLE_MODE); } if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame) diff --git a/config/keyBindingsConfig.json b/config/keyBindingsConfig.json index 8759dc023..89fc60bed 100644 --- a/config/keyBindingsConfig.json +++ b/config/keyBindingsConfig.json @@ -155,6 +155,7 @@ "lobbyToggleChat": "C", "lobbyTurnOptions": "T", "lobbyCampaignSets": "G", + "lobbyBattleMode": "X", "mainMenuBack": [ "B", "Escape" ], "mainMenuCampaign": "C", "mainMenuCampaignAb": "A", From 43c66f50863bc4985176ac66809d7745fd4621e8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 6 Nov 2025 23:16:57 +0100 Subject: [PATCH 04/22] disable text --- client/lobby/CLobbyScreen.cpp | 12 ++++++++++++ client/lobby/CSelectionBase.cpp | 19 +++++++++++++++++++ client/lobby/CSelectionBase.h | 1 + 3 files changed, 32 insertions(+) diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index bac09568a..776c23a87 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -149,6 +149,18 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) GAME->server().sendGuiAction(LobbyGuiAction::OPEN_EXTRA_OPTIONS); else if(tab == tabBattleOnlyMode) GAME->server().sendGuiAction(LobbyGuiAction::BATTLE_MODE); + + if(tab == tabBattleOnlyMode) + { + buttonStart->block(true); + card->clearSelection(); + } + else + { + buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest()); + card->changeSelection(); + } + CSelectionBase::toggleTab(tab); } diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 25e75bf32..68e4703ff 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -264,6 +264,7 @@ void InfoCard::changeSelection() labelLossConditionText->setText(header->defeatMessage.toString()); flagbox->recreate(); labelDifficulty->setText(LIBRARY->generaltexth->arraytxt[142 + vstd::to_underlying(mapInfo->mapHeader->difficulty)]); + iconDifficulty->activate(); iconDifficulty->setSelected(SEL->getCurrentDifficulty()); if(SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::saveGame) for(auto & button : iconDifficulty->buttons) @@ -294,6 +295,24 @@ void InfoCard::changeSelection() } } +void InfoCard::clearSelection() +{ + labelSaveDate->setText(""); + mapName->setText(""); + mapDescription->setText(""); + + if(SEL->screenType == ESelectionScreen::campaignList) + return; + + labelMapSize->setText(""); + + labelVictoryConditionText->setText(""); + labelLossConditionText->setText(""); + iconDifficulty->deactivate(); + labelDifficulty->setText(""); + labelDifficultyPercent->setText(""); +} + void InfoCard::toggleChat() { setChat(!showChat); diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 388845165..5b0d5be02 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -129,6 +129,7 @@ public: InfoCard(); void disableLabelRedraws(); void changeSelection(); + void clearSelection(); void toggleChat(); void setChat(bool activateChat); }; From a5cbf878bdbf773cdb190e19efe8cc2b731b0377 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 7 Nov 2025 00:07:32 +0100 Subject: [PATCH 05/22] reuse begin button --- client/lobby/BattleOnlyModeTab.cpp | 9 ++++----- client/lobby/BattleOnlyModeTab.h | 5 ++--- client/lobby/CLobbyScreen.cpp | 18 ++++++++++++++---- client/lobby/CLobbyScreen.h | 1 + 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index dc5b771e6..673aaafa2 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "BattleOnlyModeTab.h" +#include "CLobbyScreen.h" #include "../CServerHandler.h" #include "../GameEngine.h" @@ -65,8 +66,6 @@ BattleOnlyModeTab::BattleOnlyModeTab() init(); backgroundImage = std::make_shared(ImagePath::builtin("AdventureOptionsBackgroundClear"), 0, 6); - buttonOk = std::make_shared(Point(148, 430), AnimationPath::builtin("CBBEGIB"), CButton::tooltip(), [this](){ startBattle(); }, EShortcut::GLOBAL_ACCEPT); - buttonOk->block(true); title = std::make_shared(220, 35, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")); subTitle = std::make_shared(Rect(55, 40, 333, 40), FONT_SMALL, ETextAlignment::BOTTOMCENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSubTitle")); @@ -167,7 +166,7 @@ void BattleOnlyModeTab::onChange() void BattleOnlyModeTab::update() { setTerrainButtonText(); - setOkButtonEnabled(); + setStartButtonEnabled(); heroSelector1->setHeroIcon(); heroSelector1->setCreatureIcons(); @@ -187,13 +186,13 @@ void BattleOnlyModeTab::setTerrainButtonText() battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ": " + (startInfo->selectedTerrain ? (*startInfo->selectedTerrain).toEntity(LIBRARY)->getNameTranslated() : (*startInfo->selectedTown).toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor); } -void BattleOnlyModeTab::setOkButtonEnabled() +void BattleOnlyModeTab::setStartButtonEnabled() { bool army2Empty = std::all_of(startInfo->selectedArmy[1].begin(), startInfo->selectedArmy[1].end(), [](const auto x) { return x.getId() == CreatureID::NONE; }); bool canStart = (startInfo->selectedTerrain || startInfo->selectedTown); canStart &= (startInfo->selectedHero[0] && ((startInfo->selectedHero[1]) || (startInfo->selectedTown && !army2Empty))); - buttonOk->block(!canStart || GAME->server().isGuest()); + (static_cast(parent))->buttonStart->block(!canStart || GAME->server().isGuest()); } std::shared_ptr drawBlackBox(Point size, std::string text, ColorRGBA color) diff --git a/client/lobby/BattleOnlyModeTab.h b/client/lobby/BattleOnlyModeTab.h index bf7d621d0..402ad16bc 100644 --- a/client/lobby/BattleOnlyModeTab.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -65,7 +65,6 @@ private: std::shared_ptr cb; std::shared_ptr backgroundImage; - std::shared_ptr buttonOk; std::shared_ptr title; std::shared_ptr subTitle; @@ -80,9 +79,9 @@ private: void onChange(); void update(); void setTerrainButtonText(); - void setOkButtonEnabled(); - void startBattle(); public: BattleOnlyModeTab(); void applyStartInfo(std::shared_ptr si); + void startBattle(); + void setStartButtonEnabled(); }; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 776c23a87..307f6345b 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -88,7 +88,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, &GAME->server(), _1)); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_STANDARD_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::start, this, false), EShortcut::LOBBY_BEGIN_STANDARD_GAME); initLobby(); break; } @@ -97,13 +97,13 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) tabOpt = std::make_shared(); tabTurnOptions = std::make_shared(); tabExtraOptions = std::make_shared(); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), LIBRARY->generaltexth->zelp[103], std::bind(&CLobbyScreen::start, this, false), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; } case ESelectionScreen::campaignList: tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, &GAME->server(), _1, nullptr); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_CAMPAIGN); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::start, this, true), EShortcut::LOBBY_BEGIN_CAMPAIGN); break; } @@ -152,7 +152,7 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) if(tab == tabBattleOnlyMode) { - buttonStart->block(true); + tabBattleOnlyMode->setStartButtonEnabled(); card->clearSelection(); } else @@ -164,6 +164,16 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) CSelectionBase::toggleTab(tab); } +void CLobbyScreen::start(bool campaign) +{ + if(curTab == tabBattleOnlyMode) + tabBattleOnlyMode->startBattle(); + else if(campaign) + startCampaign(); + else + startScenario(false); +} + void CLobbyScreen::startCampaign() { if(!GAME->server().mi) diff --git a/client/lobby/CLobbyScreen.h b/client/lobby/CLobbyScreen.h index d8377b5d8..90d75d44f 100644 --- a/client/lobby/CLobbyScreen.h +++ b/client/lobby/CLobbyScreen.h @@ -23,6 +23,7 @@ public: CLobbyScreen(ESelectionScreen type, bool hideScreen = false); ~CLobbyScreen(); void toggleTab(std::shared_ptr tab) final; + void start(bool campaign); void startCampaign(); void startScenario(bool allowOnlyAI = false); void toggleMode(bool host); From 0c4a0ad1f1c44ae140620d8c9c59c3c669efa2c2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:34:18 +0100 Subject: [PATCH 06/22] add possiblity to add shortcut for battle mode in main menu --- client/CServerHandler.cpp | 1 + client/CServerHandler.h | 1 + client/lobby/CLobbyScreen.cpp | 10 ++++++++-- client/mainmenu/CMainMenu.cpp | 21 ++++++++++++--------- client/mainmenu/CMainMenu.h | 2 +- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 0011c22b8..eae23dc09 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -107,6 +107,7 @@ CServerHandler::CServerHandler() , screenType(ESelectionScreen::unknown) , serverMode(EServerMode::NONE) , loadMode(ELoadMode::NONE) + , battleMode(false) , client(nullptr) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 38e23e697..e0c42b057 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -140,6 +140,7 @@ public: ESelectionScreen screenType; // To create lobby UI only after server is setup EServerMode serverMode; ELoadMode loadMode; // For saves filtering in SelectionTab + bool battleMode; //////////////////// std::unique_ptr th; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 307f6345b..6e320288c 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -269,6 +269,9 @@ void CLobbyScreen::updateAfterStateChange() { tabBattleOnlyMode = std::make_shared(); tabBattleOnlyMode->setEnabled(false); + + if(GAME->server().battleMode) + toggleTab(tabBattleOnlyMode); } if(GAME->server().isHost() && screenType == ESelectionScreen::newGame) @@ -291,9 +294,12 @@ void CLobbyScreen::updateAfterStateChange() tabExtraOptions->recreate(); } - buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest()); + if(curTab && curTab != tabBattleOnlyMode) + { + buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest()); + card->changeSelection(); + } - card->changeSelection(); if (card->iconDifficulty) { if (screenType == ESelectionScreen::loadGame) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index fdf2aa717..47d17320a 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -144,7 +144,7 @@ static std::function genCommand(CMenuScreen * menu, std::vector commandType = {"to", "campaigns", "start", "load", "exit", "highscores"}; - static const std::vector gameType = {"single", "multi", "campaign", "tutorial"}; + static const std::vector gameType = {"single", "multi", "campaign", "tutorial", "battle"}; std::list commands; boost::split(commands, string, boost::is_any_of("\t ")); @@ -174,13 +174,15 @@ static std::function genCommand(CMenuScreen * menu, std::vectorwindows().createAndPushWindow(ESelectionScreen::newGame); }; case 2: - return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE); }; + return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE, false); }; case 3: return []() { CMainMenu::startTutorial(); }; + case 4: + return []() { CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE, true); }; } break; } @@ -189,13 +191,13 @@ static std::function genCommand(CMenuScreen * menu, std::vectorwindows().createAndPushWindow(ESelectionScreen::loadGame); }; case 2: - return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN); }; + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN, false); }; case 3: - return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL); }; + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL, false); }; } } @@ -402,10 +404,11 @@ void CMainMenu::makeActiveInterface() menu->switchToTab(menu->getActiveTab()); } -void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode) +void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode) { GAME->server().resetStateForLobby(screenType == ESelectionScreen::newGame ? EStartMode::NEW_GAME : EStartMode::LOAD_GAME, screenType, EServerMode::LOCAL, names); GAME->server().loadMode = loadMode; + GAME->server().battleMode = battleMode; ENGINE->windows().createAndPushWindow(host); } @@ -448,7 +451,7 @@ void CMainMenu::startTutorial() auto mapInfo = std::make_shared(); mapInfo->mapInit(tutorialMap.getName()); - CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE); + CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE, false); GAME->server().startMapAfterConnection(mapInfo); } @@ -615,7 +618,7 @@ void CMultiPlayers::enterSelectionScreen() playerName->clear(); } - CMainMenu::openLobby(screenType, host, playerNames, loadMode); + CMainMenu::openLobby(screenType, host, playerNames, loadMode, false); } CSimpleJoinScreen::CSimpleJoinScreen(bool host) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index a8ddb29ed..c33550d40 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -154,7 +154,7 @@ public: void activate() override; void onScreenResize() override; void makeActiveInterface(); - static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode); + static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode); static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); From 0c9f5261a0503d1c0e26fff0316f53e91cc865e4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:57:18 +0100 Subject: [PATCH 07/22] bugfixes --- client/lobby/BattleOnlyModeTab.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 673aaafa2..186f51257 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -30,6 +30,7 @@ #include "../windows/GUIClasses.h" #include "../windows/CHeroOverview.h" #include "../windows/CCreatureWindow.h" +#include "../eventsSDL/InputHandler.h" #include "../../lib/GameLibrary.h" #include "../../lib/gameState/CGameState.h" @@ -73,12 +74,10 @@ BattleOnlyModeTab::BattleOnlyModeTab() std::vector texts; std::vector> images; - auto & terrains = LIBRARY->terrainTypeHandler->objects; + std::vector> terrains; + std::copy_if(LIBRARY->terrainTypeHandler->objects.begin(), LIBRARY->terrainTypeHandler->objects.end(), std::back_inserter(terrains), [](auto terrain) { return terrain->isPassable(); }); for (const auto & terrain : terrains) { - if(!terrain->isPassable()) - continue; - texts.push_back(terrain->getNameTranslated()); const auto & patterns = LIBRARY->terviewh->getTerrainViewPatterns(terrain->getId()); @@ -274,6 +273,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() } heroImage->addLClickCallback([this](){ + ENGINE->input().hapticFeedback(); auto allowedSet = LIBRARY->heroh->getDefaultAllowed(); std::vector heroes(allowedSet.begin(), allowedSet.end()); std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) { @@ -357,6 +357,7 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() } creatureImage[i]->addLClickCallback([this, i](){ + ENGINE->input().hapticFeedback(); auto allowedSet = LIBRARY->creh->getDefaultAllowed(); std::vector creatures(allowedSet.begin(), allowedSet.end()); std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) { From cfc043846839c362f6f928f5ad5601ffcac6bf0b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:05:57 +0100 Subject: [PATCH 08/22] implement skill, artifact, warmachine, spellbook --- .../Content/Sprites/lobby/checkboxSmall.json | 8 + .../Sprites/lobby/checkboxSmallOff.png | Bin 0 -> 736 bytes .../Content/Sprites/lobby/checkboxSmallOn.png | Bin 0 -> 564 bytes Mods/vcmi/Content/config/english.json | 2 + Mods/vcmi/Content/config/german.json | 2 + client/lobby/BattleOnlyModeTab.cpp | 321 +++++++++++++++++- client/lobby/BattleOnlyModeTab.h | 11 + client/render/AssetGenerator.cpp | 15 +- client/renderSDL/ScalableImage.cpp | 2 +- client/widgets/CTextInput.cpp | 11 +- client/widgets/Images.cpp | 4 + lib/StartInfo.cpp | 4 + lib/StartInfo.h | 13 +- 13 files changed, 380 insertions(+), 13 deletions(-) create mode 100644 Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json create mode 100644 Mods/vcmi/Content/Sprites/lobby/checkboxSmallOff.png create mode 100644 Mods/vcmi/Content/Sprites/lobby/checkboxSmallOn.png diff --git a/Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json b/Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json new file mode 100644 index 000000000..f71c7f736 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/lobby/checkboxSmall.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "checkboxSmallOff.png"}, + { "frame" : 1, "file" : "checkboxSmallOn.png"} + ] +} diff --git a/Mods/vcmi/Content/Sprites/lobby/checkboxSmallOff.png b/Mods/vcmi/Content/Sprites/lobby/checkboxSmallOff.png new file mode 100644 index 0000000000000000000000000000000000000000..6a422b5d484d930e555398ce4b632fbbd9ce7405 GIT binary patch literal 736 zcmV<60w4W}P);%MMd}$2pM9L7DYu-kxDA4FJZ`pSfr(xv?$${+fuW#&3|b7b9-=B<_DNO8&na1GD_%8=_?8jG7;mJ@#As1nxhiZ6g>rK8ZDn1ewP z03swwhI?YxXLn3z>5p}dtRL-dd)bBj?8F+mT#9(w_Obo=Idhq`d?b|vycw8?XzBXw zo16b5Qj((;nHdRvz0VG3tOx0gZMDBWi!T5&wedzk9|B1F$>20Jab)*bGpRr=g{> z`0VPQH-qJ5B@Kei1kht~0hm`d``6&i6W$X%-bL(AFFh6)7i;?f9s|{YbGxDddf+x-1(rf?4aM~9lm7zCbTqlD SAMgDD0000=R&P)j={EFz|^>2=)UE zzDYC!H`J;^?Bg0b$AFUnuk1;>LN>s|>KZlFtOIcxAogjRGgmDvI~fG-RNf|G`eodF zC*!7^8g9?++qF^xVZZ>WsL^k^uzo)4y4UZHsC@$y?|}|ruA-n)xi747HT>-3SKtVc z^6ses^7Dz+i2Oa^EaMGBV&_E{%F7-7^8E@BDSq%*Bc*U3!M1rP-`_hrbkKX?A98bl z2O*yV)Ge#gbX~iB_`s?)G8!-_C_!LF^4kyh)a5}eJNXE>;CU%#VUM(1sp(9cf4Q%v zbxWg>&ScF(opcn$c7W$E$JD-oiAb^BQWOBud~GH+mfkxv>tuAF%AOrf9g3oKdR{^g zA^^c+xr;LU;ixI&ru0s%|B!6mTrYuc7WSnae|*0oYI~eqJ_V3-3-pAeSa$MpIWLPc z`MPb>)Np$?cy6m~Y1Zd}4OIn{uoid=>;|+-27UsqRM1j=TKNS40000(220, 35, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")); subTitle = std::make_shared(Rect(55, 40, 333, 40), FONT_SMALL, ETextAlignment::BOTTOMCENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSubTitle")); - battlefieldSelector = std::make_shared(Point(120, 370), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ + battlefieldSelector = std::make_shared(Point(57, 552), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ std::vector texts; std::vector> images; @@ -117,26 +122,36 @@ BattleOnlyModeTab::BattleOnlyModeTab() }, (startInfo->selectedTerrain ? static_cast(*startInfo->selectedTerrain) : static_cast(*startInfo->selectedTown + terrains.size())), images, true, true); }); battlefieldSelector->block(GAME->server().isGuest()); - buttonReset = std::make_shared(Point(120, 400), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ + buttonReset = std::make_shared(Point(259, 552), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](){ if(GAME->server().isHost()) { startInfo->selectedTerrain = TerrainId::DIRT; startInfo->selectedTown = std::nullopt; startInfo->selectedHero[0] = std::nullopt; startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); + startInfo->secSkillLevel[0].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); + startInfo->artifacts[0].clear(); + startInfo->spellBook[0] = true; + startInfo->warMachines[0] = false; for(size_t i=0; iselectedArmyInput.at(i)->disable(); + for(size_t i=0; i<8; i++) + heroSelector1->selectedSecSkillInput.at(i)->disable(); } startInfo->selectedHero[1] = std::nullopt; startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); - for(size_t i=0; iselectedArmyInput.at(i)->disable(); + startInfo->secSkillLevel[1].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); + startInfo->artifacts[1].clear(); + startInfo->spellBook[1] = true; + startInfo->warMachines[1] = false; + for(size_t i=0; i<8; i++) + heroSelector2->selectedSecSkillInput.at(i)->disable(); onChange(); }); buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE); - heroSelector1 = std::make_shared(0, *this, Point(91, 90)); - heroSelector2 = std::make_shared(1, *this, Point(91, 225)); + heroSelector1 = std::make_shared(0, *this, Point(55, 88)); + heroSelector2 = std::make_shared(1, *this, Point(55, 307)); heroSelector1->setInputEnabled(GAME->server().isHost()); @@ -155,6 +170,7 @@ void BattleOnlyModeTab::init() map->name = MetaString::createFromTextID("vcmi.lobby.battleOnlyMode"); cb = std::make_unique(map.get()); + map->cb = cb.get(); } void BattleOnlyModeTab::onChange() @@ -169,8 +185,16 @@ void BattleOnlyModeTab::update() heroSelector1->setHeroIcon(); heroSelector1->setCreatureIcons(); + heroSelector1->setSecSkillIcons(); + heroSelector1->setArtifactIcons(); + heroSelector1->spellBook->setSelectedSilent(startInfo->spellBook[0]); + heroSelector1->warMachines->setSelectedSilent(startInfo->warMachines[0]); heroSelector2->setHeroIcon(); heroSelector2->setCreatureIcons(); + heroSelector2->setSecSkillIcons(); + heroSelector2->setArtifactIcons(); + heroSelector2->spellBook->setSelectedSilent(startInfo->spellBook[1]); + heroSelector2->warMachines->setSelectedSilent(startInfo->warMachines[1]); redraw(); } @@ -249,8 +273,55 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab }); } + for(size_t i=0; i<8; i++) + { + bool isLeft = (i % 2 == 0); + int line = (i / 2); + Point textPos(261 + (isLeft ? 0 : 36), 41 + line * 54); + + selectedSecSkillInput.push_back(std::make_shared(Rect(textPos, Point(32, 16)), EFonts::FONT_SMALL, ETextAlignment::CENTER, false)); + selectedSecSkillInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor); + selectedSecSkillInput.back()->setFilterNumber(0, 3); + selectedSecSkillInput.back()->setText("3"); + selectedSecSkillInput.back()->setCallback([this, i, id](const std::string & text){ + if(parent.startInfo->secSkillLevel[id][i].second != MasteryLevel::NONE) + { + parent.startInfo->secSkillLevel[id][i].second = static_cast(std::stoi(text)); + parent.onChange(); + selectedSecSkillInput[i]->enable(); + } + else + selectedSecSkillInput[i]->disable(); + }); + } + secSkillImage.resize(8); + + artifactImage.resize(14); + + auto tmpIcon = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), ArtifactID(ArtifactID::SPELLBOOK).toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST); + addIcon.push_back(std::make_shared(tmpIcon, Point(220, 32))); + spellBook = std::make_shared(Point(235, 31), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){ + parent.startInfo->spellBook[id] = enabled; + parent.onChange(); + redraw(); + }); + spellBook->setSelectedSilent(parent.startInfo->spellBook[id]); + + tmpIcon = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), ArtifactID(ArtifactID::BALLISTA).toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST); + addIcon.push_back(std::make_shared(tmpIcon, Point(220, 56))); + warMachines = std::make_shared(Point(235, 55), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){ + parent.startInfo->warMachines[id] = enabled; + parent.onChange(); + redraw(); + }); + warMachines->setSelectedSilent(parent.startInfo->warMachines[id]); + setHeroIcon(); setCreatureIcons(); + setSecSkillIcons(); + setArtifactIcons(); } void BattleOnlyModeHeroSelector::setHeroIcon() @@ -273,7 +344,6 @@ void BattleOnlyModeHeroSelector::setHeroIcon() } heroImage->addLClickCallback([this](){ - ENGINE->input().hapticFeedback(); auto allowedSet = LIBRARY->heroh->getDefaultAllowed(); std::vector heroes(allowedSet.begin(), allowedSet.end()); std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) { @@ -357,7 +427,6 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() } creatureImage[i]->addLClickCallback([this, i](){ - ENGINE->input().hapticFeedback(); auto allowedSet = LIBRARY->creh->getDefaultAllowed(); std::vector creatures(allowedSet.begin(), allowedSet.end()); std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) { @@ -421,6 +490,209 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() } } +void BattleOnlyModeHeroSelector::setSecSkillIcons() +{ + OBJECT_CONSTRUCTION; + + for(int i = 0; i < secSkillImage.size(); i++) + { + bool isLeft = (i % 2 == 0); + int line = (i / 2); + Point imgPos(261 + (isLeft ? 0 : 36), 7 + line * 54); + auto skillInfo = parent.startInfo->secSkillLevel[id][i]; + if(skillInfo.second == MasteryLevel::NONE) + { + secSkillImage[i] = std::make_shared(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), imgPos); + selectedSecSkillInput[i]->disable(); + } + else + { + secSkillImage[i] = std::make_shared(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"), EImageBlitMode::COLORKEY)->getImage(skillInfo.first.toSkill()->getIconIndex(skillInfo.second - 1)), imgPos); + selectedSecSkillInput[i]->setText(std::to_string(skillInfo.second)); + selectedSecSkillInput[i]->enable(); + } + + secSkillImage[i]->addLClickCallback([this, i](){ + auto allowedSet = LIBRARY->skillh->getDefaultAllowed(); + std::vector skills(allowedSet.begin(), allowedSet.end()); + skills.erase( // remove already added skills from selection + std::remove_if( + skills.begin(), + skills.end(), + [this, i](auto & skill) { + return std::any_of( + parent.startInfo->secSkillLevel[id].begin(), parent.startInfo->secSkillLevel[id].end(), + [&skill](auto & s) { return s.first == skill; } + ) && parent.startInfo->secSkillLevel[id][i].first != skill; + } + ), + skills.end() + ); + std::sort(skills.begin(), skills.end(), [](auto a, auto b) { + auto skillA = a.toSkill(); + auto skillB = b.toSkill(); + return skillA->getNameTranslated() < skillB->getNameTranslated(); + }); + + int selectedIndex = parent.startInfo->secSkillLevel[id][i].second == MasteryLevel::NONE ? 0 : (1 + std::distance(skills.begin(), std::find_if(skills.begin(), skills.end(), [this, i](auto skillID) { + return skillID == parent.startInfo->secSkillLevel[id][i].first; + }))); + + std::vector texts; + std::vector> images; + // Add "no skill" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & c : skills) + { + texts.push_back(c.toSkill()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SECSK32"), c.toSkill()->getIconIndex(0), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), [this, skills, i](int index){ + if(index == 0) + { + parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); + parent.onChange(); + return; + } + index--; + + auto skill = skills.at(index).toSkill(); + parent.startInfo->secSkillLevel[id][i] = std::make_pair(skill->getId(), MasteryLevel::EXPERT); + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [skills](int index) { + if(index == 0) + return; + index--; + + auto skillId = skills.at(index); + std::shared_ptr comp = std::make_shared(ComponentType::SEC_SKILL, skillId, MasteryLevel::EXPERT); + CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(MasteryLevel::EXPERT), CInfoWindow::TCompsInfo(1, comp)); + }; + ENGINE->windows().pushWindow(window); + }); + + secSkillImage[i]->addRClickCallback([this, i](){ + auto skillId = parent.startInfo->secSkillLevel[id][i].first; + auto skillLevel = parent.startInfo->secSkillLevel[id][i].second; + + if(skillLevel == MasteryLevel::NONE) + return; + + std::shared_ptr comp = std::make_shared(ComponentType::SEC_SKILL, skillId, skillLevel); + CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(skillLevel), CInfoWindow::TCompsInfo(1, comp)); + }); + } +} + + +void BattleOnlyModeHeroSelector::setArtifactIcons() +{ + OBJECT_CONSTRUCTION; + + std::vector artPos = { + ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, ArtifactPosition::NECK, ArtifactPosition::RIGHT_HAND, ArtifactPosition::LEFT_HAND, ArtifactPosition::TORSO, ArtifactPosition::FEET, + ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING, ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 + }; + + for(int i = 0; i < artifactImage.size(); i++) + { + int xPos = i % 7; + int yPos = i / 7; + Point imgPos(6 + xPos * 36, 137 + yPos * 36); + auto artifactId = parent.startInfo->artifacts[id][artPos[i]]; + if(artifactId == ArtifactID::NONE) + artifactImage[i] = std::make_shared(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), imgPos); + else + { + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), artifactId.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(32, 32), EScalingAlgorithm::NEAREST); + artifactImage[i] = std::make_shared(image, imgPos); + } + + artifactImage[i]->addLClickCallback([this, i, artPos, artifactId](){ + auto allowedSet = LIBRARY->arth->getDefaultAllowed(); + std::vector artifacts(allowedSet.begin(), allowedSet.end()); + artifacts.erase( // remove already added and not for that slot allowed artifacts from selection + std::remove_if( + artifacts.begin(), + artifacts.end(), + [this, i, artPos](auto & artifact) { + auto possibleSlots = artifact.toArtifact()->getPossibleSlots(); + std::vector allowedSlots; + if(possibleSlots.find(ArtBearer::HERO) != possibleSlots.end() && !possibleSlots.at(ArtBearer::HERO).empty()) + allowedSlots = possibleSlots.at(ArtBearer::HERO); + + return (std::any_of(parent.startInfo->artifacts[id].begin(), parent.startInfo->artifacts[id].end(), [&artifact](auto & a) {return a.second == artifact;}) + && parent.startInfo->artifacts[id][artPos[i]] != artifact) + || !std::any_of(allowedSlots.begin(), allowedSlots.end(),[i, artPos](auto & p){ return p == artPos[i]; }); + } + ), + artifacts.end() + ); + std::sort(artifacts.begin(), artifacts.end(), [](auto a, auto b) { + auto artifactA = a.toArtifact(); + auto artifactB = b.toArtifact(); + return artifactA->getNameTranslated() < artifactB->getNameTranslated(); + }); + + int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [this, i, artifactId](auto artID) { + return artID == artifactId; + }))); + + std::vector texts; + std::vector> images; + // Add "no artifact" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & a : artifacts) + { + texts.push_back(a.toArtifact()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), a.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), [this, artifacts, i, artPos](int index){ + if(index == 0) + { + parent.startInfo->artifacts[id][artPos[i]] = ArtifactID::NONE; + parent.onChange(); + return; + } + index--; + + auto artifact = artifacts.at(index); + parent.startInfo->artifacts[id][artPos[i]] = artifact; + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [artifacts](int index) { + if(index == 0) + return; + index--; + + auto artifactId = artifacts.at(index); + std::shared_ptr comp = std::make_shared(ComponentType::ARTIFACT, artifactId); + CRClickPopup::createAndPush(artifactId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp)); + }; + ENGINE->windows().pushWindow(window); + }); + + artifactImage[i]->addRClickCallback([this, i, artPos](){ + auto artId = parent.startInfo->artifacts[id][artPos[i]]; + if(artId == ArtifactID::NONE) + return; + + std::shared_ptr comp = std::make_shared(ComponentType::ARTIFACT, artId); + CRClickPopup::createAndPush(artId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp)); + }); + } +} + void BattleOnlyModeTab::startBattle() { auto rng = &CRandomGenerator::getDefault(); @@ -452,6 +724,37 @@ void BattleOnlyModeTab::startBattle() for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++) if(startInfo->selectedArmy[sel][slot].getId() != CreatureID::NONE) obj->setCreature(SlotID(slot), startInfo->selectedArmy[sel][slot].getId(), startInfo->selectedArmy[sel][slot].getCount()); + + // give spellbook + if(!obj->getArt(ArtifactPosition::SPELLBOOK) && startInfo->spellBook[sel]) + obj->putArtifact(ArtifactPosition::SPELLBOOK, map->createArtifact(ArtifactID::SPELLBOOK)); + else if(obj->getArt(ArtifactPosition::SPELLBOOK) && !startInfo->spellBook[sel]) + obj->removeArtifact(ArtifactPosition::SPELLBOOK); + + if(startInfo->warMachines[sel]) + { + obj->putArtifact(ArtifactPosition::MACH1, map->createArtifact(ArtifactID::BALLISTA)); + obj->putArtifact(ArtifactPosition::MACH2, map->createArtifact(ArtifactID::AMMO_CART)); + obj->putArtifact(ArtifactPosition::MACH3, map->createArtifact(ArtifactID::FIRST_AID_TENT)); + } + + // give all spells (TODO: make selectable) + for(const auto & spell : LIBRARY->spellh->getDefaultAllowed()) + obj->addSpellToSpellbook(spell); + + for(auto & artifact : startInfo->artifacts[sel]) + if(artifact.second != ArtifactID::NONE) + obj->putArtifact(artifact.first, map->createArtifact(artifact.second)); + + bool skillSetted = std::any_of(startInfo->secSkillLevel[sel].begin(), startInfo->secSkillLevel[sel].end(),[](const auto& p){ return p.second != MasteryLevel::NONE; }); + if(skillSetted) + { + for(const auto & skill : LIBRARY->skillh->objects) // reset all standard skills + obj->setSecSkillLevel(SecondarySkill(skill->getId()), MasteryLevel::NONE, ChangeValueMode::ABSOLUTE); + for(int skillSlot = 0; skillSlot < 8; skillSlot++) + obj->setSecSkillLevel(startInfo->secSkillLevel[sel][skillSlot].first, startInfo->secSkillLevel[sel][skillSlot].second, ChangeValueMode::ABSOLUTE); + } + map->getEditManager()->insertObject(obj); }; diff --git a/client/lobby/BattleOnlyModeTab.h b/client/lobby/BattleOnlyModeTab.h index 402ad16bc..aec6f15e8 100644 --- a/client/lobby/BattleOnlyModeTab.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -32,6 +32,7 @@ class CAnimImage; class GraphicalPrimitiveCanvas; class CTextInput; class TransparentFilledRectangle; +class CToggleButton; class BattleOnlyModeHeroSelector : public CIntObject { @@ -42,6 +43,10 @@ private: std::shared_ptr heroImage; std::shared_ptr heroLabel; std::vector> creatureImage; + std::vector> secSkillImage; + std::vector> artifactImage; + + std::vector> addIcon; int id; public: @@ -50,9 +55,15 @@ public: std::vector> primSkillsInput; std::vector> selectedArmyInput; + std::vector> selectedSecSkillInput; + + std::shared_ptr spellBook; + std::shared_ptr warMachines; void setHeroIcon(); void setCreatureIcons(); + void setSecSkillIcons(); + void setArtifactIcons(); BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& parent, Point position); }; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 5b9f9fe28..abdc3ba6f 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -954,11 +954,24 @@ AssetGenerator::CanvasPtr AssetGenerator::createHeroSlotsColored(PlayerColor bac static const std::array filters = getColorFilters(); img->adjustPalette(filters[backColor.getNum()], 0); - auto image = ENGINE->renderHandler().createImage(Point(260, 150), CanvasScalingPolicy::IGNORE); + auto image = ENGINE->renderHandler().createImage(Point(327, 216), CanvasScalingPolicy::IGNORE); Canvas canvas = image->getCanvas(); canvas.draw(img, Point(0, 0), Rect(3, 4, 253, 107)); for(int i = 0; i<7; i++) canvas.draw(img, Point(1 + i * 36, 108), Rect(76, 57, 35, 17)); + // sec skill + for(int x = 0; x<2; x++) + for(int y = 0; y<4; y++) + { + canvas.draw(img, Point(255 + x * 36, y * (36 + 18)), Rect(3, 75, 36, 36)); + canvas.draw(img, Point(256 + x * 36, 37 + y * (36 + 18)), Rect(76, 57, 35, 17)); + } + + // artifacts + for(int x = 0; x<7; x++) + for(int y = 0; y<2; y++) + canvas.draw(img, Point(x * 36, 130 + y * 36), Rect(3, 75, 36, 36)); + return image; } diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp index b490994a7..102aca4a9 100644 --- a/client/renderSDL/ScalableImage.cpp +++ b/client/renderSDL/ScalableImage.cpp @@ -354,7 +354,7 @@ Rect ScalableImageInstance::contentRect() const Point ScalableImageInstance::dimensions() const { if (scaledImage) - return scaledImage->dimensions() / ENGINE->screenHandler().getScalingFactor(); + return scaledImage->dimensions(); return image->dimensions(); } diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 0320e925c..7d9813c7f 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -381,7 +381,16 @@ void CTextInput::numberFilter(std::string & text, const std::string & oldText, i if (value < minValue) text = metricDigits ? TextOperations::formatMetric(minValue, metricDigits) : std::to_string(minValue); else if (value > maxValue) - text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue); + { + if(text.length() == 2 && oldText.length() == 1) + { + // special case: if max val < 10 you cannot erase number to add another -> in this case just use last typed number + int newNum = std::stoi(text.substr(text.size() - 1)); + text = std::to_string(newNum < maxValue ? newNum : maxValue); + } + else + text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue); + } } void CTextInput::activate() diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 68f282aaa..bc10eac04 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -19,6 +19,7 @@ #include "../render/Canvas.h" #include "../render/ColorFilter.h" #include "../render/Colors.h" +#include "../eventsSDL/InputHandler.h" #include "../battle/BattleInterface.h" @@ -150,7 +151,10 @@ void CPicture::addRClickCallback(const std::function & callback) void CPicture::clickPressed(const Point & cursorPosition) { if(lCallback) + { + ENGINE->input().hapticFeedback(); lCallback(); + } } void CPicture::showPopupWindow(const Point & cursorPosition) diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 50181bfe0..d95e52288 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -248,6 +248,10 @@ BattleOnlyModeStartInfo::BattleOnlyModeStartInfo() for(auto & element : primSkillLevel) for(size_t i=0; i selectedTown; std::array, 2> selectedHero; - std::array, 2> selectedArmy; + std::array, 2> selectedArmy; std::array, 2> primSkillLevel; + std::array, 8>, 2> secSkillLevel; + + std::array, 2> artifacts; + + std::array warMachines; + + std::array spellBook; BattleOnlyModeStartInfo(); @@ -260,6 +267,10 @@ public: h & selectedHero; h & selectedArmy; h & primSkillLevel; + h & secSkillLevel; + h & artifacts; + h & warMachines; + h & spellBook; } }; From 5511c751d0c9d278c65383a128c97a9c401a6939 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:11:56 +0100 Subject: [PATCH 09/22] fix --- client/lobby/BattleOnlyModeTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 4745c2375..78bef1eb4 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -640,7 +640,7 @@ void BattleOnlyModeHeroSelector::setArtifactIcons() return artifactA->getNameTranslated() < artifactB->getNameTranslated(); }); - int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [this, i, artifactId](auto artID) { + int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [artifactId](auto artID) { return artID == artifactId; }))); From 1a3e3d1d9e97bd878d6ffe00a425063cac579fa5 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:49:59 +0100 Subject: [PATCH 10/22] small layout adjustment --- client/lobby/BattleOnlyModeTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 78bef1eb4..283539e81 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -150,8 +150,8 @@ BattleOnlyModeTab::BattleOnlyModeTab() }); buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE); - heroSelector1 = std::make_shared(0, *this, Point(55, 88)); - heroSelector2 = std::make_shared(1, *this, Point(55, 307)); + heroSelector1 = std::make_shared(0, *this, Point(55, 90)); + heroSelector2 = std::make_shared(1, *this, Point(55, 320)); heroSelector1->setInputEnabled(GAME->server().isHost()); From 7d09289e5baf7c1efc2a637191cdf47300c57ccc Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:23:12 +0100 Subject: [PATCH 11/22] adjust select descriptions --- Mods/vcmi/Content/config/english.json | 19 ++++++++++++++- Mods/vcmi/Content/config/german.json | 19 ++++++++++++++- client/lobby/BattleOnlyModeTab.cpp | 33 +++++++++++++++++++++++---- client/lobby/BattleOnlyModeTab.h | 2 ++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 742be71ce..12c7550db 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -146,7 +146,24 @@ "vcmi.lobby.battleOnlyModeCreatureSelect" : "Select Creature", "vcmi.lobby.battleOnlyModeSecSkillSelect" : "Select Secondary Skill", "vcmi.lobby.battleOnlyModeArtifactSelect" : "Select Artifact", - "vcmi.lobby.battleOnlyModeSelect" : "Select", + "vcmi.lobby.battleOnlyModeSelectHero" : "Select\nHero", + "vcmi.lobby.battleOnlyModeSelectUnit" : "Unit\n%d", + "vcmi.lobby.battleOnlyModeSelectSkill" : "Skill\n%d", + "vcmi.lobby.battleOnlyModeSelectArtifact" : "Art.\n%s", + "vcmi.lobby.battleOnlyModeSelectArtifact.0" : "Head", + "vcmi.lobby.battleOnlyModeSelectArtifact.1" : "Should.", + "vcmi.lobby.battleOnlyModeSelectArtifact.2" : "Neck", + "vcmi.lobby.battleOnlyModeSelectArtifact.3" : "R.Hand", + "vcmi.lobby.battleOnlyModeSelectArtifact.4" : "L.Hand", + "vcmi.lobby.battleOnlyModeSelectArtifact.5" : "Torso", + "vcmi.lobby.battleOnlyModeSelectArtifact.6" : "R.Ring", + "vcmi.lobby.battleOnlyModeSelectArtifact.7" : "L.Ring", + "vcmi.lobby.battleOnlyModeSelectArtifact.8" : "Feet", + "vcmi.lobby.battleOnlyModeSelectArtifact.9" : "Misc 1", + "vcmi.lobby.battleOnlyModeSelectArtifact.10" : "Misc 2", + "vcmi.lobby.battleOnlyModeSelectArtifact.11" : "Misc 3", + "vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4", + "vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5", "vcmi.lobby.battleOnlyModeReset" : "Reset", "vcmi.lobby.templatesSelect.hover" : "Templates", "vcmi.lobby.templatesSelect.help" : "Search and select template", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index d5a5a8fb7..5580d1863 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -146,7 +146,24 @@ "vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen", "vcmi.lobby.battleOnlyModeSecSkillSelect" : "Sekundären Skill auswählen", "vcmi.lobby.battleOnlyModeArtifactSelect" : "Artifakt auswählen", - "vcmi.lobby.battleOnlyModeSelect" : "Wählen", + "vcmi.lobby.battleOnlyModeSelectHero" : "Wähle\nHelden", + "vcmi.lobby.battleOnlyModeSelectUnit" : "Einh.\n%d", + "vcmi.lobby.battleOnlyModeSelectSkill" : "Skill\n%d", + "vcmi.lobby.battleOnlyModeSelectArtifact" : "Art.\n%s", + "vcmi.lobby.battleOnlyModeSelectArtifact.0" : "Kopf", + "vcmi.lobby.battleOnlyModeSelectArtifact.1" : "Schult.", + "vcmi.lobby.battleOnlyModeSelectArtifact.2" : "Nacken", + "vcmi.lobby.battleOnlyModeSelectArtifact.3" : "R.Hand", + "vcmi.lobby.battleOnlyModeSelectArtifact.4" : "L.Hand", + "vcmi.lobby.battleOnlyModeSelectArtifact.5" : "Torso", + "vcmi.lobby.battleOnlyModeSelectArtifact.6" : "R.Ring", + "vcmi.lobby.battleOnlyModeSelectArtifact.7" : "L.Ring", + "vcmi.lobby.battleOnlyModeSelectArtifact.8" : "Fuß", + "vcmi.lobby.battleOnlyModeSelectArtifact.9" : "Misc 1", + "vcmi.lobby.battleOnlyModeSelectArtifact.10" : "Misc 2", + "vcmi.lobby.battleOnlyModeSelectArtifact.11" : "Misc 3", + "vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4", + "vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5", "vcmi.lobby.battleOnlyModeReset" : "Zurücksetzen", "vcmi.lobby.templatesSelect.hover" : "Templates", "vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus", diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 283539e81..b64efb981 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -20,6 +20,7 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/CanvasImage.h" +#include "../render/IFont.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/Buttons.h" @@ -66,6 +67,8 @@ BattleOnlyModeTab::BattleOnlyModeTab() : startInfo(std::make_shared()) , disabledColor(GAME->server().isHost() ? Colors::WHITE : Colors::ORANGE) + , boxColor(ColorRGBA(128, 128, 128)) + , disabledBoxColor(GAME->server().isHost() ? boxColor : ColorRGBA(116, 92, 16)) { OBJECT_CONSTRUCTION; @@ -223,7 +226,16 @@ std::shared_ptr drawBlackBox(Point size, std::string text, ColorRGBA col auto image = ENGINE->renderHandler().createImage(size, CanvasScalingPolicy::AUTO); Canvas canvas = image->getCanvas(); canvas.drawColor(Rect(0, 0, size.x, size.y), Colors::BLACK); - canvas.drawText(Point(size.x / 2, size.y / 2), FONT_TINY, color, ETextAlignment::CENTER, text); + + std::vector lines; + boost::split(lines, text, boost::is_any_of("\n")); + int lineH = ENGINE->renderHandler().loadFont(FONT_TINY)->getLineHeight(); + int totalH = lines.size() * lineH; + int startY = (size.y - totalH) / 2 + lineH / 2; + + for (size_t i = 0; i < lines.size(); ++i) + canvas.drawText(Point(size.x / 2, startY + i * lineH), FONT_TINY, color, ETextAlignment::CENTER, lines[i]); + return image; } @@ -330,7 +342,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() if(!parent.startInfo->selectedHero[id]) { - heroImage = std::make_shared(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), Point(6, 7)); + heroImage = std::make_shared(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelectHero"), id == 1 ? parent.boxColor : parent.disabledBoxColor), Point(6, 7)); heroLabel = std::make_shared(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, LIBRARY->generaltexth->translate("core.genrltxt.507")); for(size_t i=0; isetText("0"); @@ -414,7 +426,10 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() { if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE) { - creatureImage[i] = std::make_shared(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), Point(6 + i * 36, 78)); + MetaString str; + str.appendTextID("vcmi.lobby.battleOnlyModeSelectUnit"); + str.replaceNumber(i + 1); + creatureImage[i] = std::make_shared(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), Point(6 + i * 36, 78)); selectedArmyInput[i]->disable(); } else @@ -502,7 +517,10 @@ void BattleOnlyModeHeroSelector::setSecSkillIcons() auto skillInfo = parent.startInfo->secSkillLevel[id][i]; if(skillInfo.second == MasteryLevel::NONE) { - secSkillImage[i] = std::make_shared(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), imgPos); + MetaString str; + str.appendTextID("vcmi.lobby.battleOnlyModeSelectSkill"); + str.replaceNumber(i + 1); + secSkillImage[i] = std::make_shared(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), imgPos); selectedSecSkillInput[i]->disable(); } else @@ -606,7 +624,12 @@ void BattleOnlyModeHeroSelector::setArtifactIcons() Point imgPos(6 + xPos * 36, 137 + yPos * 36); auto artifactId = parent.startInfo->artifacts[id][artPos[i]]; if(artifactId == ArtifactID::NONE) - artifactImage[i] = std::make_shared(drawBlackBox(Point(32, 32), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelect"), id == 1 ? Colors::WHITE : parent.disabledColor), imgPos); + { + MetaString str; + str.appendTextID("vcmi.lobby.battleOnlyModeSelectArtifact"); + str.replaceTextID("vcmi.lobby.battleOnlyModeSelectArtifact." + std::to_string(artPos[i])); + artifactImage[i] = std::make_shared(drawBlackBox(Point(32, 32), str.toString(), id == 1 ? parent.boxColor : parent.disabledBoxColor), imgPos); + } else { auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), artifactId.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); diff --git a/client/lobby/BattleOnlyModeTab.h b/client/lobby/BattleOnlyModeTab.h index aec6f15e8..f8dc3680f 100644 --- a/client/lobby/BattleOnlyModeTab.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -85,6 +85,8 @@ private: std::shared_ptr heroSelector2; ColorRGBA disabledColor; + ColorRGBA boxColor; + ColorRGBA disabledBoxColor; void init(); void onChange(); From e15b776e1884853bb9ecde8ad3e9ff4e0e16fa11 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:10:22 +0100 Subject: [PATCH 12/22] add spell managment --- .../Content/Sprites/lobby/removeChannel.png | Bin 0 -> 126 bytes Mods/vcmi/Content/config/english.json | 4 + Mods/vcmi/Content/config/german.json | 4 + client/lobby/BattleOnlyModeTab.cpp | 94 +++++++++++++++++- client/lobby/BattleOnlyModeTab.h | 1 + lib/StartInfo.h | 2 + 6 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 Mods/vcmi/Content/Sprites/lobby/removeChannel.png diff --git a/Mods/vcmi/Content/Sprites/lobby/removeChannel.png b/Mods/vcmi/Content/Sprites/lobby/removeChannel.png new file mode 100644 index 0000000000000000000000000000000000000000..d8310f49f549c31522f2ed8f6e4807a65fbc3aed GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fY)RhkE)4%caKYZ?lYt_}o-U3d z7QM*{60D08UN9e9etq@-;M)P2XT#jovZT3K|7WHe9KK*YL(nVOmdX8~h6Eo2!|a)C VoXM;gW&*V{c)I$ztaD0e0svQNC5-?8 literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 12c7550db..c6b04a4b1 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -165,6 +165,10 @@ "vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4", "vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5", "vcmi.lobby.battleOnlyModeReset" : "Reset", + "vcmi.lobby.battleOnlySpellSelect" : "Add spells to spellbook", + "vcmi.lobby.battleOnlySpellSelectCurrent" : "Add or remove spells from spellbook\n\nCurrent spells:", + "vcmi.lobby.battleOnlySpellAdd" : "Add spell", + "vcmi.lobby.battleOnlySpellRemove" : "Remove spell", "vcmi.lobby.templatesSelect.hover" : "Templates", "vcmi.lobby.templatesSelect.help" : "Search and select template", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index 5580d1863..21bd54dd1 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -165,6 +165,10 @@ "vcmi.lobby.battleOnlyModeSelectArtifact.12" : "Misc 4", "vcmi.lobby.battleOnlyModeSelectArtifact.18" : "Misc 5", "vcmi.lobby.battleOnlyModeReset" : "Zurücksetzen", + "vcmi.lobby.battleOnlySpellSelect" : "Zauber zum Zauberbuch hinzufügen", + "vcmi.lobby.battleOnlySpellSelectCurrent" : "Zauber zum Zauberbuch hinzufügen oder entfernen\n\nAktuelle Zauber:", + "vcmi.lobby.battleOnlySpellAdd" : "Zauber hinzufügen", + "vcmi.lobby.battleOnlySpellRemove" : "Zauber entfernen", "vcmi.lobby.templatesSelect.hover" : "Templates", "vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus", diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index b64efb981..1c5126832 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -59,6 +59,7 @@ #include "../../lib/mapping/CMapService.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/SpellSchoolHandler.h" #include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/MetaString.h" #include "../../lib/texts/TextOperations.h" @@ -313,6 +314,12 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab auto tmpIcon = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), ArtifactID(ArtifactID::SPELLBOOK).toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST); addIcon.push_back(std::make_shared(tmpIcon, Point(220, 32))); + addIcon.back()->addLClickCallback([this](){ manageSpells(); }); + addIcon.back()->addRClickCallback([this](){ + //std::shared_ptr comp = std::make_shared(ComponentType::SPELL, list[index]); + //CRClickPopup::createAndPush("", CInfoWindow::TCompsInfo(1, comp)); + }); + spellBook = std::make_shared(Point(235, 31), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){ parent.startInfo->spellBook[id] = enabled; parent.onChange(); @@ -336,6 +343,90 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab setArtifactIcons(); } +void BattleOnlyModeHeroSelector::manageSpells() +{ + std::vector> resComps; + for(auto & spellId : parent.startInfo->spells[id]) + resComps.push_back(std::make_shared(ComponentType::SPELL, spellId)); + + std::vector>> pom; + for(int i = 0; i < 3; i++) + pom.emplace_back(AnimationPath::builtin("settingsWindow/button80"), nullptr); + + auto allowedSet = LIBRARY->spellh->getDefaultAllowed(); + std::vector allSpells(allowedSet.begin(), allowedSet.end()); + std::sort(allSpells.begin(), allSpells.end(), [](auto a, auto b) { + auto A = a.toSpell(); + auto B = b.toSpell(); + if(A->getLevel() < B->getLevel()) + return true; + if(A->getLevel() > B->getLevel()) + return false; + for (const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects()) + { + if(A->schools.count(schoolId) && !B->schools.count(schoolId)) + return true; + if(!A->schools.count(schoolId) && B->schools.count(schoolId)) + return false; + } + return TextOperations::compareLocalizedStrings(A->getNameTranslated(), B->getNameTranslated()); + }); + + std::vector toAdd; + std::vector toRemove; + for (const auto& spell : allSpells) + { + bool inCurrent = std::find(parent.startInfo->spells[id].begin(), parent.startInfo->spells[id].end(), spell) != parent.startInfo->spells[id].end(); + if (inCurrent) + toRemove.push_back(spell); + else + toAdd.push_back(spell); + } + + auto openList = [this](std::vector list, bool add){ + std::vector texts; + std::vector> images; + for (const auto & s : list) + { + texts.push_back(s.toSpell()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Spells"), s.toSpell()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + + std::string title = LIBRARY->generaltexth->translate(add ? "vcmi.lobby.battleOnlySpellAdd" : "vcmi.lobby.battleOnlySpellRemove"); + auto window = std::make_shared(texts, nullptr, title, title, [this, list, add](int index){ + auto & v = parent.startInfo->spells[id]; + if(add) + v.push_back(list[index]); + else + v.erase(std::remove(v.begin(), v.end(), list[index]), v.end()); + + parent.onChange(); + manageSpells(); + }, 0, images, true, true); + window->onPopup = [list](int index) { + std::shared_ptr comp = std::make_shared(ComponentType::SPELL, list[index]); + CRClickPopup::createAndPush(list[index].toSpell()->getDescriptionTranslated(0), CInfoWindow::TCompsInfo(1, comp)); + }; + ENGINE->windows().pushWindow(window); + }; + + auto temp = std::make_shared(LIBRARY->generaltexth->translate(parent.startInfo->spells[id].size() ? "vcmi.lobby.battleOnlySpellSelectCurrent" : "vcmi.lobby.battleOnlySpellSelect"), PlayerColor(0), resComps, pom); + temp->buttons[0]->setOverlay(std::make_shared(ImagePath::builtin("lobby/addChannel"))); + temp->buttons[0]->addCallback([openList, toAdd](){ openList(toAdd, true); }); + temp->buttons[0]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlySpellAdd")); }); + temp->buttons[1]->setOverlay(std::make_shared(ImagePath::builtin("lobby/removeChannel"))); + temp->buttons[1]->addCallback([openList, toRemove](){ openList(toRemove, false); }); + temp->buttons[1]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlySpellRemove")); }); + temp->buttons[1]->setEnabled(parent.startInfo->spells[id].size()); + temp->buttons[2]->setOverlay(std::make_shared(ImagePath::builtin("spellResearch/close"))); + temp->buttons[2]->addPopupCallback([](){ CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("core.genrltxt.600")); }); + + ENGINE->windows().pushWindow(temp); +} + void BattleOnlyModeHeroSelector::setHeroIcon() { OBJECT_CONSTRUCTION; @@ -761,8 +852,7 @@ void BattleOnlyModeTab::startBattle() obj->putArtifact(ArtifactPosition::MACH3, map->createArtifact(ArtifactID::FIRST_AID_TENT)); } - // give all spells (TODO: make selectable) - for(const auto & spell : LIBRARY->spellh->getDefaultAllowed()) + for(const auto & spell : startInfo->spells[sel]) obj->addSpellToSpellbook(spell); for(auto & artifact : startInfo->artifacts[sel]) diff --git a/client/lobby/BattleOnlyModeTab.h b/client/lobby/BattleOnlyModeTab.h index f8dc3680f..e72c747ea 100644 --- a/client/lobby/BattleOnlyModeTab.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -64,6 +64,7 @@ public: void setCreatureIcons(); void setSecSkillIcons(); void setArtifactIcons(); + void manageSpells(); BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab& parent, Point position); }; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 8733fb6bc..c587578f2 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -253,6 +253,7 @@ public: std::array, 8>, 2> secSkillLevel; std::array, 2> artifacts; + std::array, 2> spells; std::array warMachines; @@ -271,6 +272,7 @@ public: h & artifacts; h & warMachines; h & spellBook; + h & spells; } }; From f42a56edc8d6bf819a0a32f870fbde83f01ab739 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:46:56 +0100 Subject: [PATCH 13/22] spellbook fixes --- client/lobby/BattleOnlyModeTab.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 1c5126832..c787dc7a2 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -137,6 +137,7 @@ BattleOnlyModeTab::BattleOnlyModeTab() startInfo->artifacts[0].clear(); startInfo->spellBook[0] = true; startInfo->warMachines[0] = false; + startInfo->spells[0].clear(); for(size_t i=0; iselectedArmyInput.at(i)->disable(); for(size_t i=0; i<8; i++) @@ -148,6 +149,7 @@ BattleOnlyModeTab::BattleOnlyModeTab() startInfo->artifacts[1].clear(); startInfo->spellBook[1] = true; startInfo->warMachines[1] = false; + startInfo->spells[1].clear(); for(size_t i=0; i<8; i++) heroSelector2->selectedSecSkillInput.at(i)->disable(); onChange(); @@ -315,9 +317,11 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab tmpIcon->scaleTo(Point(16, 16), EScalingAlgorithm::NEAREST); addIcon.push_back(std::make_shared(tmpIcon, Point(220, 32))); addIcon.back()->addLClickCallback([this](){ manageSpells(); }); - addIcon.back()->addRClickCallback([this](){ - //std::shared_ptr comp = std::make_shared(ComponentType::SPELL, list[index]); - //CRClickPopup::createAndPush("", CInfoWindow::TCompsInfo(1, comp)); + addIcon.back()->addRClickCallback([this, id](){ + std::vector> comps; + for(auto & spell : parent.startInfo->spells[id]) + comps.push_back(std::make_shared(ComponentType::SPELL, spell, std::nullopt, CComponent::ESize::large)); + CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("artifact.core.spellBook.name"), comps); }); spellBook = std::make_shared(Point(235, 31), AnimationPath::builtin("lobby/checkboxSmall"), CButton::tooltip(), [this, id](bool enabled){ @@ -347,7 +351,7 @@ void BattleOnlyModeHeroSelector::manageSpells() { std::vector> resComps; for(auto & spellId : parent.startInfo->spells[id]) - resComps.push_back(std::make_shared(ComponentType::SPELL, spellId)); + resComps.push_back(std::make_shared(ComponentType::SPELL, spellId, std::nullopt, CComponent::ESize::large)); std::vector>> pom; for(int i = 0; i < 3; i++) @@ -355,6 +359,9 @@ void BattleOnlyModeHeroSelector::manageSpells() auto allowedSet = LIBRARY->spellh->getDefaultAllowed(); std::vector allSpells(allowedSet.begin(), allowedSet.end()); + allSpells.erase(std::remove_if(allSpells.begin(), allSpells.end(), [](const SpellID& spell) { + return !spell.toSpell()->isCombat(); + }), allSpells.end()); std::sort(allSpells.begin(), allSpells.end(), [](auto a, auto b) { auto A = a.toSpell(); auto B = b.toSpell(); @@ -390,8 +397,8 @@ void BattleOnlyModeHeroSelector::manageSpells() { texts.push_back(s.toSpell()->getNameTranslated()); - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Spells"), s.toSpell()->getIconIndex(), 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SpellInt"), s.toSpell()->getIconIndex() + 1, 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); images.push_back(image); } From 34af1433e6f65aca5009db5dcd4ba626319f24f5 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 05:55:44 +0100 Subject: [PATCH 14/22] remove optional and add serializer --- client/lobby/BattleOnlyModeTab.cpp | 59 +++++++++++++++++----------- lib/StartInfo.cpp | 62 +++++++++++++++++++++++++++++- lib/StartInfo.h | 8 ++-- 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index c787dc7a2..61ded6179 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -34,6 +34,7 @@ #include "../windows/CCreatureWindow.h" #include "../windows/InfoWindows.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/GameLibrary.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/PacksForLobby.h" @@ -64,6 +65,8 @@ #include "../../lib/texts/MetaString.h" #include "../../lib/texts/TextOperations.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/serializer/JsonSerializer.h" +#include "../../lib/serializer/JsonDeserializer.h" BattleOnlyModeTab::BattleOnlyModeTab() : startInfo(std::make_shared()) @@ -73,6 +76,10 @@ BattleOnlyModeTab::BattleOnlyModeTab() { OBJECT_CONSTRUCTION; + //JsonNode node = persistentStorage["lobby"]["battleModeSettings"]; + //JsonDeserializer handler(nullptr, node); + //startInfo->serializeJson(handler); + init(); backgroundImage = std::make_shared(ImagePath::builtin("AdventureOptionsBackgroundClear"), 0, 6); @@ -113,25 +120,25 @@ BattleOnlyModeTab::BattleOnlyModeTab() if(terrains.size() > index) { startInfo->selectedTerrain = terrains[index]->getId(); - startInfo->selectedTown = std::nullopt; + startInfo->selectedTown = FactionID::ANY; } else { - startInfo->selectedTerrain = std::nullopt; + startInfo->selectedTerrain = TerrainId::NONE; auto it = std::next(factions.begin(), index - terrains.size()); if (it != factions.end()) startInfo->selectedTown = *it; } onChange(); - }, (startInfo->selectedTerrain ? static_cast(*startInfo->selectedTerrain) : static_cast(*startInfo->selectedTown + terrains.size())), images, true, true); + }, (startInfo->selectedTerrain != TerrainId::NONE ? static_cast(startInfo->selectedTerrain) : static_cast(startInfo->selectedTown + terrains.size())), images, true, true); }); battlefieldSelector->block(GAME->server().isGuest()); buttonReset = std::make_shared(Point(259, 552), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](){ if(GAME->server().isHost()) { startInfo->selectedTerrain = TerrainId::DIRT; - startInfo->selectedTown = std::nullopt; - startInfo->selectedHero[0] = std::nullopt; + startInfo->selectedTown = FactionID::ANY; + startInfo->selectedHero[0] = HeroTypeID::NONE; startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); startInfo->secSkillLevel[0].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); startInfo->artifacts[0].clear(); @@ -143,7 +150,7 @@ BattleOnlyModeTab::BattleOnlyModeTab() for(size_t i=0; i<8; i++) heroSelector1->selectedSecSkillInput.at(i)->disable(); } - startInfo->selectedHero[1] = std::nullopt; + startInfo->selectedHero[1] = HeroTypeID::NONE; startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); startInfo->secSkillLevel[1].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); startInfo->artifacts[1].clear(); @@ -202,6 +209,12 @@ void BattleOnlyModeTab::update() heroSelector2->spellBook->setSelectedSilent(startInfo->spellBook[1]); heroSelector2->warMachines->setSelectedSilent(startInfo->warMachines[1]); redraw(); + + JsonNode node; + JsonSerializer handler(nullptr, node); + startInfo->serializeJson(handler); + Settings storage = persistentStorage.write["lobby"]["battleModeSettings"]; + storage->Struct() = node.Struct(); } void BattleOnlyModeTab::applyStartInfo(std::shared_ptr si) @@ -212,15 +225,15 @@ void BattleOnlyModeTab::applyStartInfo(std::shared_ptr void BattleOnlyModeTab::setTerrainButtonText() { - battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ": " + (startInfo->selectedTerrain ? (*startInfo->selectedTerrain).toEntity(LIBRARY)->getNameTranslated() : (*startInfo->selectedTown).toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor); + battlefieldSelector->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield") + ": " + (startInfo->selectedTerrain != TerrainId::NONE ? startInfo->selectedTerrain.toEntity(LIBRARY)->getNameTranslated() : startInfo->selectedTown.toEntity(LIBRARY)->getNameTranslated()), EFonts::FONT_SMALL, disabledColor); } void BattleOnlyModeTab::setStartButtonEnabled() { bool army2Empty = std::all_of(startInfo->selectedArmy[1].begin(), startInfo->selectedArmy[1].end(), [](const auto x) { return x.getId() == CreatureID::NONE; }); - bool canStart = (startInfo->selectedTerrain || startInfo->selectedTown); - canStart &= (startInfo->selectedHero[0] && ((startInfo->selectedHero[1]) || (startInfo->selectedTown && !army2Empty))); + bool canStart = (startInfo->selectedTerrain != TerrainId::NONE || startInfo->selectedTown != FactionID::ANY); + canStart &= (startInfo->selectedHero[0] != HeroTypeID::NONE && ((startInfo->selectedHero[1] != HeroTypeID::NONE) || (startInfo->selectedTown != FactionID::ANY && !army2Empty))); (static_cast(parent))->buttonStart->block(!canStart || GAME->server().isGuest()); } @@ -438,7 +451,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() { OBJECT_CONSTRUCTION; - if(!parent.startInfo->selectedHero[id]) + if(parent.startInfo->selectedHero[id] == HeroTypeID::NONE) { heroImage = std::make_shared(drawBlackBox(Point(58, 64), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSelectHero"), id == 1 ? parent.boxColor : parent.disabledBoxColor), Point(6, 7)); heroLabel = std::make_shared(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, LIBRARY->generaltexth->translate("core.genrltxt.507")); @@ -447,8 +460,8 @@ void BattleOnlyModeHeroSelector::setHeroIcon() } else { - heroImage = std::make_shared(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("PortraitsLarge"), EImageBlitMode::COLORKEY)->getImage((*parent.startInfo->selectedHero[id]).toHeroType()->imageIndex), Point(6, 7)); - heroLabel = std::make_shared(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, (*parent.startInfo->selectedHero[id]).toHeroType()->getNameTranslated()); + heroImage = std::make_shared(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("PortraitsLarge"), EImageBlitMode::COLORKEY)->getImage(parent.startInfo->selectedHero[id].toHeroType()->imageIndex), Point(6, 7)); + heroLabel = std::make_shared(160, 16, FONT_SMALL, ETextAlignment::CENTER, id == 1 ? Colors::WHITE : parent.disabledColor, parent.startInfo->selectedHero[id].toHeroType()->getNameTranslated()); for(size_t i=0; isetText(std::to_string(parent.startInfo->primSkillLevel[id][i])); } @@ -466,8 +479,8 @@ void BattleOnlyModeHeroSelector::setHeroIcon() return heroA->getNameTranslated() < heroB->getNameTranslated(); }); - int selectedIndex = !parent.startInfo->selectedHero[id] ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) { - return heroID == (*parent.startInfo->selectedHero[id]); + int selectedIndex = parent.startInfo->selectedHero[id] == HeroTypeID::NONE ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) { + return heroID == parent.startInfo->selectedHero[id]; }))); std::vector texts; @@ -486,7 +499,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){ if(index == 0) { - parent.startInfo->selectedHero[id] = std::nullopt; + parent.startInfo->selectedHero[id] = HeroTypeID::NONE; parent.onChange(); return; } @@ -509,10 +522,10 @@ void BattleOnlyModeHeroSelector::setHeroIcon() }); heroImage->addRClickCallback([this](){ - if(!parent.startInfo->selectedHero[id]) + if(parent.startInfo->selectedHero[id] == HeroTypeID::NONE) return; - ENGINE->windows().createAndPushWindow(parent.startInfo->selectedHero[id]->toHeroType()->getId()); + ENGINE->windows().createAndPushWindow(parent.startInfo->selectedHero[id].toHeroType()->getId()); }); } @@ -822,7 +835,7 @@ void BattleOnlyModeTab::startBattle() map->getEditManager()->clearTerrain(rng); map->getEditManager()->getTerrainSelection().selectAll(); - map->getEditManager()->drawTerrain(!startInfo->selectedTerrain ? TerrainId::DIRT : *startInfo->selectedTerrain, 0, rng); + map->getEditManager()->drawTerrain(startInfo->selectedTerrain == TerrainId::NONE ? TerrainId::DIRT : startInfo->selectedTerrain, 0, rng); map->players[0].canComputerPlay = true; map->players[0].canHumanPlay = true; @@ -832,10 +845,10 @@ void BattleOnlyModeTab::startBattle() auto addHero = [&, this](int sel, PlayerColor color, const int3 & position) { - auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, (*startInfo->selectedHero[sel]).toHeroType()->heroClass->getId()); + auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, startInfo->selectedHero[sel].toHeroType()->heroClass->getId()); auto templates = factory->getTemplates(); auto obj = std::dynamic_pointer_cast(factory->create(cb.get(), templates.front())); - obj->setHeroType(*startInfo->selectedHero[sel]); + obj->setHeroType(startInfo->selectedHero[sel]); obj->setOwner(color); obj->pos = position; @@ -879,11 +892,11 @@ void BattleOnlyModeTab::startBattle() }; addHero(0, PlayerColor(0), int3(5, 6, 0)); - if(!startInfo->selectedTown) + if(startInfo->selectedTown == FactionID::ANY) addHero(1, PlayerColor(1), int3(5, 5, 0)); else { - auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::TOWN, *startInfo->selectedTown); + auto factory = LIBRARY->objtypeh->getHandlerFor(Obj::TOWN, startInfo->selectedTown); auto templates = factory->getTemplates(); auto obj = factory->create(cb.get(), templates.front()); auto townObj = std::dynamic_pointer_cast(obj); @@ -891,7 +904,7 @@ void BattleOnlyModeTab::startBattle() obj->pos = int3(5, 5, 0); for (const auto & building : townObj->getTown()->getAllBuildings()) townObj->addBuilding(building); - if(!startInfo->selectedHero[1]) + if(startInfo->selectedHero[1] == HeroTypeID::NONE) { for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++) if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE) diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index d95e52288..abab23688 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -21,6 +21,7 @@ #include "mapping/CMapHeader.h" #include "mapping/CMapService.h" #include "modding/ModIncompatibility.h" +#include "serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -243,7 +244,7 @@ TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color) BattleOnlyModeStartInfo::BattleOnlyModeStartInfo() : selectedTerrain(TerrainId::DIRT) - , selectedTown(std::nullopt) + , selectedTown(FactionID::ANY) { for(auto & element : primSkillLevel) for(size_t i=0; iserializeId("selectedHero", selectedHero[i]); + { + auto selectedArmyArray = s->enterArray("selectedArmy"); + selectedArmyArray.resize(GameConstants::ARMY_SIZE, JsonNode::JsonType::DATA_VECTOR); + for(int j = 0; j < GameConstants::ARMY_SIZE; j++) + { + JsonArraySerializer inner = selectedArmyArray.enterArray(j); + selectedArmy[i][j].serializeJson(handler); + } + } + { + auto primSkillLevelArray = s->enterArray("primSkillLevel"); + primSkillLevelArray.resize(4, JsonNode::JsonType::DATA_VECTOR); + for(int j = 0; j < 4; j++) + primSkillLevelArray.serializeInt(j, primSkillLevel[i][j]); + } + { + auto secSkillLevelArray = s->enterArray("secSkillLevel"); + secSkillLevelArray.resize(8, JsonNode::JsonType::DATA_VECTOR); + for(int j = 0; j < 8; j++) + { + JsonArraySerializer inner = secSkillLevelArray.enterArray(j); + inner->serializeId("skill", secSkillLevel[i][j].first); + inner->serializeEnum("masteryLevel", secSkillLevel[i][j].second, MasteryLevel::NONE, {"none", "basic", "advanced", "expert"}); + } + } + if(handler.saving) + { + auto reversed = vstd::reverseMap(artifacts[i]); + std::map tmp; + for (auto& [id, pos] : reversed) + tmp[id] = static_cast(pos); + tmp.erase(ArtifactID::NONE); + s->serializeIdMap("artifacts", tmp); + } + else + { + std::map tmp; + s->serializeIdMap("artifacts", tmp); + std::map converted; + for (auto &[id, pos] : tmp) + converted[id] = ArtifactPosition(pos); + artifacts[i] = vstd::reverseMap(converted); + } + s->serializeIdArray("spells", spells[i]); + s->serializeBool("warMachines", warMachines[i]); + s->serializeBool("spellBook", spellBook[i]); + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/StartInfo.h b/lib/StartInfo.h index c587578f2..502104aa1 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -243,10 +243,10 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState class DLL_LINKAGE BattleOnlyModeStartInfo : public Serializeable { public: - std::optional selectedTerrain; - std::optional selectedTown; + TerrainId selectedTerrain; + FactionID selectedTown; - std::array, 2> selectedHero; + std::array selectedHero; std::array, 2> selectedArmy; std::array, 2> primSkillLevel; @@ -261,6 +261,8 @@ public: BattleOnlyModeStartInfo(); + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h) { h & selectedTerrain; From 07621f795c30cc0197a4aa1d39b880d009114e79 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 06:41:06 +0100 Subject: [PATCH 15/22] fix serialisation --- client/lobby/BattleOnlyModeTab.cpp | 19 +++++++++++++++---- lib/StartInfo.cpp | 8 +++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 61ded6179..32148abaf 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -76,9 +76,20 @@ BattleOnlyModeTab::BattleOnlyModeTab() { OBJECT_CONSTRUCTION; - //JsonNode node = persistentStorage["lobby"]["battleModeSettings"]; - //JsonDeserializer handler(nullptr, node); - //startInfo->serializeJson(handler); + try + { + JsonNode node = persistentStorage["battleModeSettings"]; + if(!node.isNull()) + { + node.setModScope(ModScope::scopeGame()); + JsonDeserializer handler(nullptr, node); + startInfo->serializeJson(handler); + } + } + catch(std::exception & e) + { + logGlobal->error("Error loading saved battleModeSettings, received exception: %s", e.what()); + } init(); @@ -213,7 +224,7 @@ void BattleOnlyModeTab::update() JsonNode node; JsonSerializer handler(nullptr, node); startInfo->serializeJson(handler); - Settings storage = persistentStorage.write["lobby"]["battleModeSettings"]; + Settings storage = persistentStorage.write["battleModeSettings"]; storage->Struct() = node.Struct(); } diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index abab23688..4e23d3cbb 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -259,6 +259,9 @@ void BattleOnlyModeStartInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeId("selectedTerrain", selectedTerrain); handler.serializeId("selectedTown", selectedTown); + if(!handler.saving && selectedTown == FactionID::NONE) + selectedTown = FactionID::ANY; + auto slots = handler.enterArray("slots"); slots.resize(2); for(int i = 0; i < 2; i++) @@ -305,10 +308,13 @@ void BattleOnlyModeStartInfo::serializeJson(JsonSerializeFormat & handler) s->serializeIdMap("artifacts", tmp); std::map converted; for (auto &[id, pos] : tmp) - converted[id] = ArtifactPosition(pos); + if(id != ArtifactID::NONE) + converted[id] = ArtifactPosition(pos); artifacts[i] = vstd::reverseMap(converted); } s->serializeIdArray("spells", spells[i]); + if(!handler.saving) + spells[i].erase(std::remove(spells[i].begin(), spells[i].end(), SpellID::NONE), spells[i].end()); s->serializeBool("warMachines", warMachines[i]); s->serializeBool("spellBook", spellBook[i]); } From 8582fd70436c4d22617a149527ac7037fa24450d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:40:25 +0100 Subject: [PATCH 16/22] set skill to initial after changing hero --- client/lobby/BattleOnlyModeTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 32148abaf..4c333fce0 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -519,7 +519,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() parent.startInfo->selectedHero[id] = heroes[index]; for(size_t i=0; iprimSkillLevel[id][i] = 0; + parent.startInfo->primSkillLevel[id][i] = heroes[index].toHeroType()->heroClass->primarySkillInitial[i]; parent.onChange(); }, selectedIndex, images, true, true); window->onPopup = [heroes](int index) { From 11fa9144a499d4bbec0de35333a7061d2e0a6c96 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:58:31 +0100 Subject: [PATCH 17/22] after hero change set secSkill and spellBook to default for hero --- client/lobby/BattleOnlyModeTab.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 4c333fce0..a93da638d 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -520,6 +520,15 @@ void BattleOnlyModeHeroSelector::setHeroIcon() for(size_t i=0; iprimSkillLevel[id][i] = heroes[index].toHeroType()->heroClass->primarySkillInitial[i]; + + for(size_t i=0; i<8; i++) + if(heroes[index].toHeroType()->secSkillsInit.size() > i) + parent.startInfo->secSkillLevel[id][i] = std::make_pair(heroes[index].toHeroType()->secSkillsInit[i].first, MasteryLevel::Type(heroes[index].toHeroType()->secSkillsInit[i].second)); + else + parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); + + parent.startInfo->spellBook[id] = heroes[index].toHeroType()->haveSpellBook; + parent.onChange(); }, selectedIndex, images, true, true); window->onPopup = [heroes](int index) { @@ -890,14 +899,10 @@ void BattleOnlyModeTab::startBattle() if(artifact.second != ArtifactID::NONE) obj->putArtifact(artifact.first, map->createArtifact(artifact.second)); - bool skillSetted = std::any_of(startInfo->secSkillLevel[sel].begin(), startInfo->secSkillLevel[sel].end(),[](const auto& p){ return p.second != MasteryLevel::NONE; }); - if(skillSetted) - { - for(const auto & skill : LIBRARY->skillh->objects) // reset all standard skills - obj->setSecSkillLevel(SecondarySkill(skill->getId()), MasteryLevel::NONE, ChangeValueMode::ABSOLUTE); - for(int skillSlot = 0; skillSlot < 8; skillSlot++) - obj->setSecSkillLevel(startInfo->secSkillLevel[sel][skillSlot].first, startInfo->secSkillLevel[sel][skillSlot].second, ChangeValueMode::ABSOLUTE); - } + for(const auto & skill : LIBRARY->skillh->objects) // reset all standard skills + obj->setSecSkillLevel(SecondarySkill(skill->getId()), MasteryLevel::NONE, ChangeValueMode::ABSOLUTE); + for(int skillSlot = 0; skillSlot < 8; skillSlot++) + obj->setSecSkillLevel(startInfo->secSkillLevel[sel][skillSlot].first, startInfo->secSkillLevel[sel][skillSlot].second, ChangeValueMode::ABSOLUTE); map->getEditManager()->insertObject(obj); }; From dab6a11736e454c0ce447b635dddcdb84244e240 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:29:26 +0100 Subject: [PATCH 18/22] neutral always on bottom of list --- client/lobby/BattleOnlyModeTab.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index a93da638d..f2d857277 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -578,6 +578,8 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) { auto creatureA = a.toCreature(); auto creatureB = b.toCreature(); + if ((creatureA->getFactionID() == FactionID::NEUTRAL) != (creatureB->getFactionID() == FactionID::NEUTRAL)) + return creatureA->getFactionID() != FactionID::NEUTRAL; if(creatureA->getFactionID() != creatureB->getFactionID()) return creatureA->getFactionID() < creatureB->getFactionID(); if(creatureA->getLevel() != creatureB->getLevel()) From 4612087f1e112debf5dc2e4550866dd84ef564d1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:11:42 +0100 Subject: [PATCH 19/22] code review --- Mods/vcmi/Content/config/english.json | 2 +- Mods/vcmi/Content/config/german.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index c6b04a4b1..10f474e1b 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -139,7 +139,7 @@ "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", "vcmi.lobby.battleOnlyMode" : "Battle only mode", - "vcmi.lobby.battleOnlyModeSubTitle" : "Select heroes and army for simple battle without adventure map", + "vcmi.lobby.battleOnlyModeSubTitle" : "Select heroes, army, skills, artifact and battleground for simple battle without adventure map", "vcmi.lobby.battleOnlyModeBattlefield" : "Battlefield", "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Select Battlefield", "vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index 21bd54dd1..4acbc5ad1 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -139,13 +139,13 @@ "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", "vcmi.lobby.battleOnlyMode" : "Nur Kämpfen Modus", - "vcmi.lobby.battleOnlyModeSubTitle" : "Wähle Helden und eine Armee für einen einfachen Kampf ohne Abenteuerkarte", + "vcmi.lobby.battleOnlyModeSubTitle" : "Wähle Helden, Armeen, Skills, Artefakte und ein Schlachtfeld für einen einfachen Kampf ohne Abenteuerkarte", "vcmi.lobby.battleOnlyModeBattlefield" : "Schlachtfeld", "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Schlachtfeld auswählen", "vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen", "vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen", "vcmi.lobby.battleOnlyModeSecSkillSelect" : "Sekundären Skill auswählen", - "vcmi.lobby.battleOnlyModeArtifactSelect" : "Artifakt auswählen", + "vcmi.lobby.battleOnlyModeArtifactSelect" : "Artefakt auswählen", "vcmi.lobby.battleOnlyModeSelectHero" : "Wähle\nHelden", "vcmi.lobby.battleOnlyModeSelectUnit" : "Einh.\n%d", "vcmi.lobby.battleOnlyModeSelectSkill" : "Skill\n%d", From 8fcd5be977068f44aa049a13af3759e55bbb02db Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:40:36 +0100 Subject: [PATCH 20/22] allow to type 0 into textbox for unit amount (that we can input numbers with 1 digit) --- client/lobby/BattleOnlyModeTab.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index f2d857277..6193661bb 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -298,7 +298,7 @@ BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeTab { selectedArmyInput.push_back(std::make_shared(Rect(5 + i * 36, 113, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false)); selectedArmyInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor); - selectedArmyInput.back()->setFilterNumber(1, 10000000, 3); + selectedArmyInput.back()->setFilterNumber(0, 10000000, 3); selectedArmyInput.back()->setText("1"); selectedArmyInput.back()->setCallback([this, i, id](const std::string & text){ if(parent.startInfo->selectedArmy[id][i].getId() != CreatureID::NONE) @@ -878,7 +878,7 @@ void BattleOnlyModeTab::startBattle() obj->pushPrimSkill(PrimarySkill(i), startInfo->primSkillLevel[sel][i]); obj->clearSlots(); for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++) - if(startInfo->selectedArmy[sel][slot].getId() != CreatureID::NONE) + if(startInfo->selectedArmy[sel][slot].getId() != CreatureID::NONE && startInfo->selectedArmy[sel][slot].getCount() > 0) obj->setCreature(SlotID(slot), startInfo->selectedArmy[sel][slot].getId(), startInfo->selectedArmy[sel][slot].getCount()); // give spellbook @@ -925,7 +925,7 @@ void BattleOnlyModeTab::startBattle() if(startInfo->selectedHero[1] == HeroTypeID::NONE) { for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++) - if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE) + if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE && startInfo->selectedArmy[1][slot].getCount() > 0) townObj->getArmy()->setCreature(SlotID(slot), startInfo->selectedArmy[1][slot].getId(), startInfo->selectedArmy[1][slot].getCount()); } else From b2f7af4afba525144dcd66cfcb3cdb985792d9a3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:45:40 +0100 Subject: [PATCH 21/22] changed case for consistency --- Mods/vcmi/Content/config/english.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 10f474e1b..16381b008 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -138,7 +138,7 @@ "vcmi.lobby.deleteFile" : "Do you want to delete following file?", "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", - "vcmi.lobby.battleOnlyMode" : "Battle only mode", + "vcmi.lobby.battleOnlyMode" : "Battle Only Mode", "vcmi.lobby.battleOnlyModeSubTitle" : "Select heroes, army, skills, artifact and battleground for simple battle without adventure map", "vcmi.lobby.battleOnlyModeBattlefield" : "Battlefield", "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Select Battlefield", From 54bc5c96df46de4dae9740b580562e07f5771ec8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:59:46 +0100 Subject: [PATCH 22/22] code review: big lambdas to function & simplify sort --- client/lobby/BattleOnlyModeTab.cpp | 685 +++++++++++++++-------------- client/lobby/BattleOnlyModeTab.h | 7 + 2 files changed, 361 insertions(+), 331 deletions(-) diff --git a/client/lobby/BattleOnlyModeTab.cpp b/client/lobby/BattleOnlyModeTab.cpp index 6193661bb..7aa28d78b 100644 --- a/client/lobby/BattleOnlyModeTab.cpp +++ b/client/lobby/BattleOnlyModeTab.cpp @@ -97,81 +97,9 @@ BattleOnlyModeTab::BattleOnlyModeTab() title = std::make_shared(220, 35, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode")); subTitle = std::make_shared(Rect(55, 40, 333, 40), FONT_SMALL, ETextAlignment::BOTTOMCENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSubTitle")); - battlefieldSelector = std::make_shared(Point(57, 552), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ - std::vector texts; - std::vector> images; - - std::vector> terrains; - std::copy_if(LIBRARY->terrainTypeHandler->objects.begin(), LIBRARY->terrainTypeHandler->objects.end(), std::back_inserter(terrains), [](auto terrain) { return terrain->isPassable(); }); - for (const auto & terrain : terrains) - { - texts.push_back(terrain->getNameTranslated()); - - const auto & patterns = LIBRARY->terviewh->getTerrainViewPatterns(terrain->getId()); - TerrainViewPattern pattern; - for(auto & p : patterns) - if(p[0].id == "n1") - pattern = p[0]; - auto image = ENGINE->renderHandler().loadImage(terrain->tilesFilename, pattern.mapping[0].first, 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - - auto factions = LIBRARY->townh->getDefaultAllowed(); - for (const auto & faction : factions) - { - texts.push_back(faction.toFaction()->getNameTranslated()); - - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("ITPA"), faction.toFaction()->town->clientInfo.icons[true][false] + 2, 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - - ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefieldSelect"), [this, terrains, factions](int index){ - if(terrains.size() > index) - { - startInfo->selectedTerrain = terrains[index]->getId(); - startInfo->selectedTown = FactionID::ANY; - } - else - { - startInfo->selectedTerrain = TerrainId::NONE; - auto it = std::next(factions.begin(), index - terrains.size()); - if (it != factions.end()) - startInfo->selectedTown = *it; - } - onChange(); - }, (startInfo->selectedTerrain != TerrainId::NONE ? static_cast(startInfo->selectedTerrain) : static_cast(startInfo->selectedTown + terrains.size())), images, true, true); - }); + battlefieldSelector = std::make_shared(Point(57, 552), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ selectTerrain(); }); battlefieldSelector->block(GAME->server().isGuest()); - buttonReset = std::make_shared(Point(259, 552), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](){ - if(GAME->server().isHost()) - { - startInfo->selectedTerrain = TerrainId::DIRT; - startInfo->selectedTown = FactionID::ANY; - startInfo->selectedHero[0] = HeroTypeID::NONE; - startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); - startInfo->secSkillLevel[0].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); - startInfo->artifacts[0].clear(); - startInfo->spellBook[0] = true; - startInfo->warMachines[0] = false; - startInfo->spells[0].clear(); - for(size_t i=0; iselectedArmyInput.at(i)->disable(); - for(size_t i=0; i<8; i++) - heroSelector1->selectedSecSkillInput.at(i)->disable(); - } - startInfo->selectedHero[1] = HeroTypeID::NONE; - startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); - startInfo->secSkillLevel[1].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); - startInfo->artifacts[1].clear(); - startInfo->spellBook[1] = true; - startInfo->warMachines[1] = false; - startInfo->spells[1].clear(); - for(size_t i=0; i<8; i++) - heroSelector2->selectedSecSkillInput.at(i)->disable(); - onChange(); - }); + buttonReset = std::make_shared(Point(259, 552), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](){ reset(); }); buttonReset->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeReset"), EFonts::FONT_SMALL, Colors::WHITE); heroSelector1 = std::make_shared(0, *this, Point(55, 90)); @@ -182,6 +110,84 @@ BattleOnlyModeTab::BattleOnlyModeTab() onChange(); } +void BattleOnlyModeTab::reset() +{ + if(GAME->server().isHost()) + { + startInfo->selectedTerrain = TerrainId::DIRT; + startInfo->selectedTown = FactionID::ANY; + startInfo->selectedHero[0] = HeroTypeID::NONE; + startInfo->selectedArmy[0].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); + startInfo->secSkillLevel[0].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); + startInfo->artifacts[0].clear(); + startInfo->spellBook[0] = true; + startInfo->warMachines[0] = false; + startInfo->spells[0].clear(); + for(size_t i=0; iselectedArmyInput.at(i)->disable(); + for(size_t i=0; i<8; i++) + heroSelector1->selectedSecSkillInput.at(i)->disable(); + } + startInfo->selectedHero[1] = HeroTypeID::NONE; + startInfo->selectedArmy[1].fill(CStackBasicDescriptor(CreatureID::NONE, 1)); + startInfo->secSkillLevel[1].fill(std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE)); + startInfo->artifacts[1].clear(); + startInfo->spellBook[1] = true; + startInfo->warMachines[1] = false; + startInfo->spells[1].clear(); + for(size_t i=0; i<8; i++) + heroSelector2->selectedSecSkillInput.at(i)->disable(); + onChange(); +} + +void BattleOnlyModeTab::selectTerrain() +{ + std::vector texts; + std::vector> images; + + std::vector> terrains; + std::copy_if(LIBRARY->terrainTypeHandler->objects.begin(), LIBRARY->terrainTypeHandler->objects.end(), std::back_inserter(terrains), [](auto terrain) { return terrain->isPassable(); }); + for (const auto & terrain : terrains) + { + texts.push_back(terrain->getNameTranslated()); + + const auto & patterns = LIBRARY->terviewh->getTerrainViewPatterns(terrain->getId()); + TerrainViewPattern pattern; + for(auto & p : patterns) + if(p[0].id == "n1") + pattern = p[0]; + auto image = ENGINE->renderHandler().loadImage(terrain->tilesFilename, pattern.mapping[0].first, 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + + auto factions = LIBRARY->townh->getDefaultAllowed(); + for (const auto & faction : factions) + { + texts.push_back(faction.toFaction()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("ITPA"), faction.toFaction()->town->clientInfo.icons[true][false] + 2, 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + + ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefield"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeBattlefieldSelect"), [this, terrains, factions](int index){ + if(terrains.size() > index) + { + startInfo->selectedTerrain = terrains[index]->getId(); + startInfo->selectedTown = FactionID::ANY; + } + else + { + startInfo->selectedTerrain = TerrainId::NONE; + auto it = std::next(factions.begin(), index - terrains.size()); + if (it != factions.end()) + startInfo->selectedTown = *it; + } + onChange(); + }, (startInfo->selectedTerrain != TerrainId::NONE ? static_cast(startInfo->selectedTerrain) : static_cast(startInfo->selectedTown + terrains.size())), images, true, true); +} + void BattleOnlyModeTab::init() { map = std::make_unique(nullptr); @@ -389,10 +395,8 @@ void BattleOnlyModeHeroSelector::manageSpells() std::sort(allSpells.begin(), allSpells.end(), [](auto a, auto b) { auto A = a.toSpell(); auto B = b.toSpell(); - if(A->getLevel() < B->getLevel()) - return true; - if(A->getLevel() > B->getLevel()) - return false; + if(A->getLevel() != B->getLevel()) + return A->getLevel() < B->getLevel(); for (const auto schoolId : LIBRARY->spellSchoolHandler->getAllObjects()) { if(A->schools.count(schoolId) && !B->schools.count(schoolId)) @@ -458,6 +462,71 @@ void BattleOnlyModeHeroSelector::manageSpells() ENGINE->windows().pushWindow(temp); } +void BattleOnlyModeHeroSelector::selectHero() +{ + auto allowedSet = LIBRARY->heroh->getDefaultAllowed(); + std::vector heroes(allowedSet.begin(), allowedSet.end()); + std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) { + auto heroA = a.toHeroType(); + auto heroB = b.toHeroType(); + if(heroA->heroClass->faction != heroB->heroClass->faction) + return heroA->heroClass->faction < heroB->heroClass->faction; + if(heroA->heroClass->getId() != heroB->heroClass->getId()) + return heroA->heroClass->getId() < heroB->heroClass->getId(); + return heroA->getNameTranslated() < heroB->getNameTranslated(); + }); + + int selectedIndex = parent.startInfo->selectedHero[id] == HeroTypeID::NONE ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) { + return heroID == parent.startInfo->selectedHero[id]; + }))); + + std::vector texts; + std::vector> images; + // Add "no hero" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & h : heroes) + { + texts.push_back(h.toHeroType()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("PortraitsSmall"), h.toHeroType()->imageIndex, 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){ + if(index == 0) + { + parent.startInfo->selectedHero[id] = HeroTypeID::NONE; + parent.onChange(); + return; + } + index--; + + parent.startInfo->selectedHero[id] = heroes[index]; + + for(size_t i=0; iprimSkillLevel[id][i] = heroes[index].toHeroType()->heroClass->primarySkillInitial[i]; + + for(size_t i=0; i<8; i++) + if(heroes[index].toHeroType()->secSkillsInit.size() > i) + parent.startInfo->secSkillLevel[id][i] = std::make_pair(heroes[index].toHeroType()->secSkillsInit[i].first, MasteryLevel::Type(heroes[index].toHeroType()->secSkillsInit[i].second)); + else + parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); + + parent.startInfo->spellBook[id] = heroes[index].toHeroType()->haveSpellBook; + + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [heroes](int index) { + if(index == 0) + return; + index--; + + ENGINE->windows().createAndPushWindow(heroes.at(index)); + }; + ENGINE->windows().pushWindow(window); +} + void BattleOnlyModeHeroSelector::setHeroIcon() { OBJECT_CONSTRUCTION; @@ -477,69 +546,7 @@ void BattleOnlyModeHeroSelector::setHeroIcon() primSkillsInput[i]->setText(std::to_string(parent.startInfo->primSkillLevel[id][i])); } - heroImage->addLClickCallback([this](){ - auto allowedSet = LIBRARY->heroh->getDefaultAllowed(); - std::vector heroes(allowedSet.begin(), allowedSet.end()); - std::sort(heroes.begin(), heroes.end(), [](auto a, auto b) { - auto heroA = a.toHeroType(); - auto heroB = b.toHeroType(); - if(heroA->heroClass->faction != heroB->heroClass->faction) - return heroA->heroClass->faction < heroB->heroClass->faction; - if(heroA->heroClass->getId() != heroB->heroClass->getId()) - return heroA->heroClass->getId() < heroB->heroClass->getId(); - return heroA->getNameTranslated() < heroB->getNameTranslated(); - }); - - int selectedIndex = parent.startInfo->selectedHero[id] == HeroTypeID::NONE ? 0 : (1 + std::distance(heroes.begin(), std::find_if(heroes.begin(), heroes.end(), [this](auto heroID) { - return heroID == parent.startInfo->selectedHero[id]; - }))); - - std::vector texts; - std::vector> images; - // Add "no hero" option - texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); - images.push_back(nullptr); - for (const auto & h : heroes) - { - texts.push_back(h.toHeroType()->getNameTranslated()); - - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("PortraitsSmall"), h.toHeroType()->imageIndex, 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeHeroSelect"), [this, heroes](int index){ - if(index == 0) - { - parent.startInfo->selectedHero[id] = HeroTypeID::NONE; - parent.onChange(); - return; - } - index--; - - parent.startInfo->selectedHero[id] = heroes[index]; - - for(size_t i=0; iprimSkillLevel[id][i] = heroes[index].toHeroType()->heroClass->primarySkillInitial[i]; - - for(size_t i=0; i<8; i++) - if(heroes[index].toHeroType()->secSkillsInit.size() > i) - parent.startInfo->secSkillLevel[id][i] = std::make_pair(heroes[index].toHeroType()->secSkillsInit[i].first, MasteryLevel::Type(heroes[index].toHeroType()->secSkillsInit[i].second)); - else - parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); - - parent.startInfo->spellBook[id] = heroes[index].toHeroType()->haveSpellBook; - - parent.onChange(); - }, selectedIndex, images, true, true); - window->onPopup = [heroes](int index) { - if(index == 0) - return; - index--; - - ENGINE->windows().createAndPushWindow(heroes.at(index)); - }; - ENGINE->windows().pushWindow(window); - }); + heroImage->addLClickCallback([this](){ selectHero(); }); heroImage->addRClickCallback([this](){ if(parent.startInfo->selectedHero[id] == HeroTypeID::NONE) @@ -549,6 +556,64 @@ void BattleOnlyModeHeroSelector::setHeroIcon() }); } +void BattleOnlyModeHeroSelector::selectCreature(int slot) +{ + auto allowedSet = LIBRARY->creh->getDefaultAllowed(); + std::vector creatures(allowedSet.begin(), allowedSet.end()); + std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) { + auto creatureA = a.toCreature(); + auto creatureB = b.toCreature(); + if ((creatureA->getFactionID() == FactionID::NEUTRAL) != (creatureB->getFactionID() == FactionID::NEUTRAL)) + return creatureA->getFactionID() != FactionID::NEUTRAL; + if(creatureA->getFactionID() != creatureB->getFactionID()) + return creatureA->getFactionID() < creatureB->getFactionID(); + if(creatureA->getLevel() != creatureB->getLevel()) + return creatureA->getLevel() < creatureB->getLevel(); + if(creatureA->upgrades.size() != creatureB->upgrades.size()) + return creatureA->upgrades.size() > creatureB->upgrades.size(); + return creatureA->getNameSingularTranslated() < creatureB->getNameSingularTranslated(); + }); + + int selectedIndex = parent.startInfo->selectedArmy[id][slot].getId() == CreatureID::NONE ? 0 : (1 + std::distance(creatures.begin(), std::find_if(creatures.begin(), creatures.end(), [this, slot](auto creatureID) { + return creatureID == parent.startInfo->selectedArmy[id][slot].getId(); + }))); + + std::vector texts; + std::vector> images; + // Add "no creature" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & c : creatures) + { + texts.push_back(c.toCreature()->getNameSingularTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("CPRSMALL"), c.toCreature()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), [this, creatures, slot](int index){ + if(index == 0) + { + parent.startInfo->selectedArmy[id][slot] = CStackBasicDescriptor(CreatureID::NONE, 1); + parent.onChange(); + return; + } + index--; + + auto creature = creatures.at(index).toCreature(); + parent.startInfo->selectedArmy[id][slot] = CStackBasicDescriptor(creature->getId(), 100); + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [creatures](int index) { + if(index == 0) + return; + index--; + + ENGINE->windows().createAndPushWindow(creatures.at(index).toCreature(), true); + }; + ENGINE->windows().pushWindow(window); +} + void BattleOnlyModeHeroSelector::setCreatureIcons() { OBJECT_CONSTRUCTION; @@ -572,62 +637,7 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() selectedArmyInput[i]->enable(); } - creatureImage[i]->addLClickCallback([this, i](){ - auto allowedSet = LIBRARY->creh->getDefaultAllowed(); - std::vector creatures(allowedSet.begin(), allowedSet.end()); - std::sort(creatures.begin(), creatures.end(), [](auto a, auto b) { - auto creatureA = a.toCreature(); - auto creatureB = b.toCreature(); - if ((creatureA->getFactionID() == FactionID::NEUTRAL) != (creatureB->getFactionID() == FactionID::NEUTRAL)) - return creatureA->getFactionID() != FactionID::NEUTRAL; - if(creatureA->getFactionID() != creatureB->getFactionID()) - return creatureA->getFactionID() < creatureB->getFactionID(); - if(creatureA->getLevel() != creatureB->getLevel()) - return creatureA->getLevel() < creatureB->getLevel(); - if(creatureA->upgrades.size() != creatureB->upgrades.size()) - return creatureA->upgrades.size() > creatureB->upgrades.size(); - return creatureA->getNameSingularTranslated() < creatureB->getNameSingularTranslated(); - }); - - int selectedIndex = parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE ? 0 : (1 + std::distance(creatures.begin(), std::find_if(creatures.begin(), creatures.end(), [this, i](auto creatureID) { - return creatureID == parent.startInfo->selectedArmy[id][i].getId(); - }))); - - std::vector texts; - std::vector> images; - // Add "no creature" option - texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); - images.push_back(nullptr); - for (const auto & c : creatures) - { - texts.push_back(c.toCreature()->getNameSingularTranslated()); - - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("CPRSMALL"), c.toCreature()->getIconIndex(), 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeCreatureSelect"), [this, creatures, i](int index){ - if(index == 0) - { - parent.startInfo->selectedArmy[id][i] = CStackBasicDescriptor(CreatureID::NONE, 1); - parent.onChange(); - return; - } - index--; - - auto creature = creatures.at(index).toCreature(); - parent.startInfo->selectedArmy[id][i] = CStackBasicDescriptor(creature->getId(), 100); - parent.onChange(); - }, selectedIndex, images, true, true); - window->onPopup = [creatures](int index) { - if(index == 0) - return; - index--; - - ENGINE->windows().createAndPushWindow(creatures.at(index).toCreature(), true); - }; - ENGINE->windows().pushWindow(window); - }); + creatureImage[i]->addLClickCallback([this, i](){ selectCreature(i); }); creatureImage[i]->addRClickCallback([this, i](){ if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE) @@ -638,6 +648,71 @@ void BattleOnlyModeHeroSelector::setCreatureIcons() } } +void BattleOnlyModeHeroSelector::selectSecSkill(int slot) +{ + auto allowedSet = LIBRARY->skillh->getDefaultAllowed(); + std::vector skills(allowedSet.begin(), allowedSet.end()); + skills.erase( // remove already added skills from selection + std::remove_if( + skills.begin(), + skills.end(), + [this, slot](auto & skill) { + return std::any_of( + parent.startInfo->secSkillLevel[id].begin(), parent.startInfo->secSkillLevel[id].end(), + [&skill](auto & s) { return s.first == skill; } + ) && parent.startInfo->secSkillLevel[id][slot].first != skill; + } + ), + skills.end() + ); + std::sort(skills.begin(), skills.end(), [](auto a, auto b) { + auto skillA = a.toSkill(); + auto skillB = b.toSkill(); + return skillA->getNameTranslated() < skillB->getNameTranslated(); + }); + + int selectedIndex = parent.startInfo->secSkillLevel[id][slot].second == MasteryLevel::NONE ? 0 : (1 + std::distance(skills.begin(), std::find_if(skills.begin(), skills.end(), [this, slot](auto skillID) { + return skillID == parent.startInfo->secSkillLevel[id][slot].first; + }))); + + std::vector texts; + std::vector> images; + // Add "no skill" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & c : skills) + { + texts.push_back(c.toSkill()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SECSK32"), c.toSkill()->getIconIndex(0), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), [this, skills, slot](int index){ + if(index == 0) + { + parent.startInfo->secSkillLevel[id][slot] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); + parent.onChange(); + return; + } + index--; + + auto skill = skills.at(index).toSkill(); + parent.startInfo->secSkillLevel[id][slot] = std::make_pair(skill->getId(), MasteryLevel::EXPERT); + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [skills](int index) { + if(index == 0) + return; + index--; + + auto skillId = skills.at(index); + std::shared_ptr comp = std::make_shared(ComponentType::SEC_SKILL, skillId, MasteryLevel::EXPERT); + CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(MasteryLevel::EXPERT), CInfoWindow::TCompsInfo(1, comp)); + }; + ENGINE->windows().pushWindow(window); +} + void BattleOnlyModeHeroSelector::setSecSkillIcons() { OBJECT_CONSTRUCTION; @@ -663,69 +738,7 @@ void BattleOnlyModeHeroSelector::setSecSkillIcons() selectedSecSkillInput[i]->enable(); } - secSkillImage[i]->addLClickCallback([this, i](){ - auto allowedSet = LIBRARY->skillh->getDefaultAllowed(); - std::vector skills(allowedSet.begin(), allowedSet.end()); - skills.erase( // remove already added skills from selection - std::remove_if( - skills.begin(), - skills.end(), - [this, i](auto & skill) { - return std::any_of( - parent.startInfo->secSkillLevel[id].begin(), parent.startInfo->secSkillLevel[id].end(), - [&skill](auto & s) { return s.first == skill; } - ) && parent.startInfo->secSkillLevel[id][i].first != skill; - } - ), - skills.end() - ); - std::sort(skills.begin(), skills.end(), [](auto a, auto b) { - auto skillA = a.toSkill(); - auto skillB = b.toSkill(); - return skillA->getNameTranslated() < skillB->getNameTranslated(); - }); - - int selectedIndex = parent.startInfo->secSkillLevel[id][i].second == MasteryLevel::NONE ? 0 : (1 + std::distance(skills.begin(), std::find_if(skills.begin(), skills.end(), [this, i](auto skillID) { - return skillID == parent.startInfo->secSkillLevel[id][i].first; - }))); - - std::vector texts; - std::vector> images; - // Add "no skill" option - texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); - images.push_back(nullptr); - for (const auto & c : skills) - { - texts.push_back(c.toSkill()->getNameTranslated()); - - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("SECSK32"), c.toSkill()->getIconIndex(0), 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeSecSkillSelect"), [this, skills, i](int index){ - if(index == 0) - { - parent.startInfo->secSkillLevel[id][i] = std::make_pair(SecondarySkill::NONE, MasteryLevel::NONE); - parent.onChange(); - return; - } - index--; - - auto skill = skills.at(index).toSkill(); - parent.startInfo->secSkillLevel[id][i] = std::make_pair(skill->getId(), MasteryLevel::EXPERT); - parent.onChange(); - }, selectedIndex, images, true, true); - window->onPopup = [skills](int index) { - if(index == 0) - return; - index--; - - auto skillId = skills.at(index); - std::shared_ptr comp = std::make_shared(ComponentType::SEC_SKILL, skillId, MasteryLevel::EXPERT); - CRClickPopup::createAndPush(skillId.toSkill()->getDescriptionTranslated(MasteryLevel::EXPERT), CInfoWindow::TCompsInfo(1, comp)); - }; - ENGINE->windows().pushWindow(window); - }); + secSkillImage[i]->addLClickCallback([this, i](){ selectSecSkill(i); }); secSkillImage[i]->addRClickCallback([this, i](){ auto skillId = parent.startInfo->secSkillLevel[id][i].first; @@ -740,15 +753,91 @@ void BattleOnlyModeHeroSelector::setSecSkillIcons() } } +std::vector getArtPos() +{ + std::vector artPos = { + ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, ArtifactPosition::NECK, ArtifactPosition::RIGHT_HAND, ArtifactPosition::LEFT_HAND, ArtifactPosition::TORSO, ArtifactPosition::FEET, + ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING, ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 + }; + return artPos; +} + +void BattleOnlyModeHeroSelector::selectArtifact(int slot, ArtifactID artifactId) +{ + auto artPos = getArtPos(); + + auto allowedSet = LIBRARY->arth->getDefaultAllowed(); + std::vector artifacts(allowedSet.begin(), allowedSet.end()); + artifacts.erase( // remove already added and not for that slot allowed artifacts from selection + std::remove_if( + artifacts.begin(), + artifacts.end(), + [this, slot, artPos](auto & artifact) { + auto possibleSlots = artifact.toArtifact()->getPossibleSlots(); + std::vector allowedSlots; + if(possibleSlots.find(ArtBearer::HERO) != possibleSlots.end() && !possibleSlots.at(ArtBearer::HERO).empty()) + allowedSlots = possibleSlots.at(ArtBearer::HERO); + + return (std::any_of(parent.startInfo->artifacts[id].begin(), parent.startInfo->artifacts[id].end(), [&artifact](auto & a) {return a.second == artifact;}) + && parent.startInfo->artifacts[id][artPos[slot]] != artifact) + || !std::any_of(allowedSlots.begin(), allowedSlots.end(), [slot, artPos](auto & p){ return p == artPos[slot]; }); + } + ), + artifacts.end() + ); + std::sort(artifacts.begin(), artifacts.end(), [](auto a, auto b) { + auto artifactA = a.toArtifact(); + auto artifactB = b.toArtifact(); + return artifactA->getNameTranslated() < artifactB->getNameTranslated(); + }); + + int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [artifactId](auto artID) { + return artID == artifactId; + }))); + + std::vector texts; + std::vector> images; + // Add "no artifact" option + texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); + images.push_back(nullptr); + for (const auto & a : artifacts) + { + texts.push_back(a.toArtifact()->getNameTranslated()); + + auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), a.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); + image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); + images.push_back(image); + } + auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), [this, artifacts, slot, artPos](int index){ + if(index == 0) + { + parent.startInfo->artifacts[id][artPos[slot]] = ArtifactID::NONE; + parent.onChange(); + return; + } + index--; + + auto artifact = artifacts.at(index); + parent.startInfo->artifacts[id][artPos[slot]] = artifact; + parent.onChange(); + }, selectedIndex, images, true, true); + window->onPopup = [artifacts](int index) { + if(index == 0) + return; + index--; + + auto artifactId = artifacts.at(index); + std::shared_ptr comp = std::make_shared(ComponentType::ARTIFACT, artifactId); + CRClickPopup::createAndPush(artifactId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp)); + }; + ENGINE->windows().pushWindow(window); +} void BattleOnlyModeHeroSelector::setArtifactIcons() { OBJECT_CONSTRUCTION; - std::vector artPos = { - ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, ArtifactPosition::NECK, ArtifactPosition::RIGHT_HAND, ArtifactPosition::LEFT_HAND, ArtifactPosition::TORSO, ArtifactPosition::FEET, - ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING, ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 - }; + auto artPos = getArtPos(); for(int i = 0; i < artifactImage.size(); i++) { @@ -770,73 +859,7 @@ void BattleOnlyModeHeroSelector::setArtifactIcons() artifactImage[i] = std::make_shared(image, imgPos); } - artifactImage[i]->addLClickCallback([this, i, artPos, artifactId](){ - auto allowedSet = LIBRARY->arth->getDefaultAllowed(); - std::vector artifacts(allowedSet.begin(), allowedSet.end()); - artifacts.erase( // remove already added and not for that slot allowed artifacts from selection - std::remove_if( - artifacts.begin(), - artifacts.end(), - [this, i, artPos](auto & artifact) { - auto possibleSlots = artifact.toArtifact()->getPossibleSlots(); - std::vector allowedSlots; - if(possibleSlots.find(ArtBearer::HERO) != possibleSlots.end() && !possibleSlots.at(ArtBearer::HERO).empty()) - allowedSlots = possibleSlots.at(ArtBearer::HERO); - - return (std::any_of(parent.startInfo->artifacts[id].begin(), parent.startInfo->artifacts[id].end(), [&artifact](auto & a) {return a.second == artifact;}) - && parent.startInfo->artifacts[id][artPos[i]] != artifact) - || !std::any_of(allowedSlots.begin(), allowedSlots.end(),[i, artPos](auto & p){ return p == artPos[i]; }); - } - ), - artifacts.end() - ); - std::sort(artifacts.begin(), artifacts.end(), [](auto a, auto b) { - auto artifactA = a.toArtifact(); - auto artifactB = b.toArtifact(); - return artifactA->getNameTranslated() < artifactB->getNameTranslated(); - }); - - int selectedIndex = artifactId == ArtifactID::NONE ? 0 : (1 + std::distance(artifacts.begin(), std::find_if(artifacts.begin(), artifacts.end(), [artifactId](auto artID) { - return artID == artifactId; - }))); - - std::vector texts; - std::vector> images; - // Add "no artifact" option - texts.push_back(LIBRARY->generaltexth->translate("core.genrltxt.507")); - images.push_back(nullptr); - for (const auto & a : artifacts) - { - texts.push_back(a.toArtifact()->getNameTranslated()); - - auto image = ENGINE->renderHandler().loadImage(AnimationPath::builtin("Artifact"), a.toArtifact()->getIconIndex(), 0, EImageBlitMode::OPAQUE); - image->scaleTo(Point(23, 23), EScalingAlgorithm::NEAREST); - images.push_back(image); - } - auto window = std::make_shared(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyModeArtifactSelect"), [this, artifacts, i, artPos](int index){ - if(index == 0) - { - parent.startInfo->artifacts[id][artPos[i]] = ArtifactID::NONE; - parent.onChange(); - return; - } - index--; - - auto artifact = artifacts.at(index); - parent.startInfo->artifacts[id][artPos[i]] = artifact; - parent.onChange(); - }, selectedIndex, images, true, true); - window->onPopup = [artifacts](int index) { - if(index == 0) - return; - index--; - - auto artifactId = artifacts.at(index); - std::shared_ptr comp = std::make_shared(ComponentType::ARTIFACT, artifactId); - CRClickPopup::createAndPush(artifactId.toArtifact()->getDescriptionTranslated(), CInfoWindow::TCompsInfo(1, comp)); - }; - ENGINE->windows().pushWindow(window); - }); + artifactImage[i]->addLClickCallback([this, i, artifactId](){ selectArtifact(i, artifactId); }); artifactImage[i]->addRClickCallback([this, i, artPos](){ auto artId = parent.startInfo->artifacts[id][artPos[i]]; diff --git a/client/lobby/BattleOnlyModeTab.h b/client/lobby/BattleOnlyModeTab.h index e72c747ea..e68060649 100644 --- a/client/lobby/BattleOnlyModeTab.h +++ b/client/lobby/BattleOnlyModeTab.h @@ -48,6 +48,11 @@ private: std::vector> addIcon; + void selectHero(); + void selectCreature(int slot); + void selectSecSkill(int slot); + void selectArtifact(int slot, ArtifactID artifactId); + int id; public: std::vector> primSkills; @@ -93,6 +98,8 @@ private: void onChange(); void update(); void setTerrainButtonText(); + void selectTerrain(); + void reset(); public: BattleOnlyModeTab(); void applyStartInfo(std::shared_ptr si);