diff --git a/Mods/vcmi/Content/Sprites/lobby/battle-normal.png b/Mods/vcmi/Content/Sprites/lobby/battle-normal.png new file mode 100644 index 000000000..ad6e918e5 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/lobby/battle-normal.png differ diff --git a/Mods/vcmi/Content/Sprites/lobby/battle-pressed.png b/Mods/vcmi/Content/Sprites/lobby/battle-pressed.png new file mode 100644 index 000000000..f52afa391 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/lobby/battle-pressed.png differ diff --git a/Mods/vcmi/Content/Sprites/lobby/battleButton.json b/Mods/vcmi/Content/Sprites/lobby/battleButton.json new file mode 100644 index 000000000..c198750c7 --- /dev/null +++ b/Mods/vcmi/Content/Sprites/lobby/battleButton.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "battle-normal.png"}, + { "frame" : 1, "file" : "battle-pressed.png"} + ] +} diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index da2b1d01d..2aa81d5a6 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -138,6 +138,13 @@ "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.battleOnlyModeBattlefield" : "Battlefield", + "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Select Battlefield", + "vcmi.lobby.battleOnlyModeHeroSelect" : "Select Hero", + "vcmi.lobby.battleOnlyModeCreatureSelect" : "Select Creature", + "vcmi.lobby.battleOnlyModeSelect" : "Select", + "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 ed0a74cf3..c05a712f8 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -138,6 +138,13 @@ "vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?", "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.battleOnlyModeBattlefield" : "Schlachtfeld", + "vcmi.lobby.battleOnlyModeBattlefieldSelect" : "Schlachtfeld auswählen", + "vcmi.lobby.battleOnlyModeHeroSelect" : "Helden auswählen", + "vcmi.lobby.battleOnlyModeCreatureSelect" : "Kreatur auswählen", + "vcmi.lobby.battleOnlyModeSelect" : "Wählen", + "vcmi.lobby.battleOnlyModeReset" : "Zurücksetzen", "vcmi.lobby.templatesSelect.hover" : "Templates", "vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus", diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 165326f5f..25b009a59 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -52,6 +52,7 @@ set(vcmiclientcommon_SRCS gui/ShortcutHandler.cpp gui/WindowHandler.cpp + lobby/BattleOnlyMode.cpp lobby/CBonusSelection.cpp lobby/CCampaignInfoScreen.cpp lobby/CLobbyScreen.cpp @@ -262,6 +263,7 @@ set(vcmiclientcommon_HEADERS gui/TextAlignment.h gui/WindowHandler.h + lobby/BattleOnlyMode.h lobby/CBonusSelection.h lobby/CCampaignInfoScreen.h lobby/CLobbyScreen.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index acff46b6c..c0ff4943f 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -97,6 +97,7 @@ #include "../lib/mapObjects/MiscObjects.h" #include "../lib/mapObjects/ObjectTemplate.h" +#include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapHeader.h" #include "../lib/networkPacks/PacksForClient.h" @@ -658,7 +659,7 @@ void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet { EVENT_HANDLER_CALLED_BY_CLIENT; - bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); + bool useQuickCombat = settings["adventure"]["quickCombat"].Bool() || GAME->map().getMap()->battleOnly; bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); if ((replayAllowed && useQuickCombat) || forceQuickCombat) @@ -1482,7 +1483,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) { if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE) { - if(GAME->server().howManyPlayerInterfaces() > 1 && GAME->interface() != this && GAME->interface()->makingTurn == false) + if(GAME->server().howManyPlayerInterfaces() > 1 && GAME->interface() != this && GAME->interface()->makingTurn == false && !GAME->map().getMap()->battleOnly) { //one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode) GAME->setInterfaceInstance(this); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index cb80f843d..0011c22b8 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -432,6 +432,13 @@ void CServerHandler::setCampaignBonus(int bonusId) const sendLobbyPack(lscb); } +void CServerHandler::setBattleOnlyModeStartInfo(std::shared_ptr startInfo) const +{ + LobbySetBattleOnlyModeStartInfo lsbomsui; + lsbomsui.startInfo = startInfo; + sendLobbyPack(lsbomsui); +} + void CServerHandler::setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts) const { LobbySetMap lsm; diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 4cfdd52a1..38e23e697 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -77,6 +77,7 @@ public: virtual void setCampaignState(std::shared_ptr newCampaign) = 0; virtual void setCampaignMap(CampaignScenarioID mapId) const = 0; virtual void setCampaignBonus(int bonusId) const = 0; + virtual void setBattleOnlyModeStartInfo(std::shared_ptr startInfo) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0; @@ -186,6 +187,7 @@ public: void setCampaignState(std::shared_ptr newCampaign) override; void setCampaignMap(CampaignScenarioID mapId) const override; void setCampaignBonus(int bonusId) const override; + void setBattleOnlyModeStartInfo(std::shared_ptr startInfo) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; void setPlayerName(PlayerColor color, const std::string & name) const override; diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index b3996cce7..2639b44ca 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -59,4 +59,5 @@ public: void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; void visitLobbyUpdateState(LobbyUpdateState & pack) override; void visitLobbyShowMessage(LobbyShowMessage & pack) override; + void visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) override; }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 5e74691f0..af8243e98 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -15,6 +15,7 @@ #include "windows/GUIClasses.h" #include "windows/CCastleInterface.h" #include "mapView/mapHandler.h" +#include "mainmenu/CMainMenu.h" #include "adventureMap/AdventureMapInterface.h" #include "adventureMap/CInGameConsole.h" #include "battle/BattleInterface.h" @@ -400,7 +401,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.gameInfo().getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory(); bool lastHumanEndsGame = GAME->server().howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.gameInfo().getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool(); - if(lastHumanEndsGame || localHumanWinsGame) + if(lastHumanEndsGame || localHumanWinsGame || pack.silentEnd) { assert(adventureInt); if(adventureInt) @@ -409,7 +410,13 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) adventureInt.reset(); } - GAME->server().showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic); + if(!pack.silentEnd) + GAME->server().showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic); + else + { + GAME->server().endGameplay(); + GAME->mainmenu()->menu->switchToTab("main"); + } } // In auto testing pack.mode we always close client if red pack.player won or lose diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index f81006125..f286af001 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -19,6 +19,7 @@ #include "lobby/ExtraOptionsTab.h" #include "lobby/SelectionTab.h" #include "lobby/CBonusSelection.h" +#include "lobby/BattleOnlyMode.h" #include "globalLobby/GlobalLobbyWindow.h" #include "globalLobby/GlobalLobbyServerSetup.h" #include "globalLobby/GlobalLobbyClient.h" @@ -113,6 +114,9 @@ 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: @@ -133,6 +137,9 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack case LobbyGuiAction::OPEN_EXTRA_OPTIONS: lobby->toggleTab(lobby->tabExtraOptions); break; + case LobbyGuiAction::BATTLE_MODE: + BattleOnlyMode::openBattleWindow(); + break; } } @@ -232,3 +239,9 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyShowMessage(LobbyShowMessage & lobby->buttonStart->block(false); handler.showServerError(pack.message.toString()); } + +void ApplyOnLobbyScreenNetPackVisitor::visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) +{ + if(auto topWindow = ENGINE->windows().topWindow()) + topWindow->applyStartInfo(pack.startInfo); +} diff --git a/client/battle/BattleResultWindow.cpp b/client/battle/BattleResultWindow.cpp index 2f79ba894..f5a49589c 100644 --- a/client/battle/BattleResultWindow.cpp +++ b/client/battle/BattleResultWindow.cpp @@ -12,6 +12,9 @@ #include "BattleWindow.h" +#include "../GameInstance.h" +#include "../Client.h" +#include "../CServerHandler.h" #include "../CPlayerInterface.h" #include "../GameEngine.h" #include "../gui/Shortcut.h" @@ -23,6 +26,7 @@ #include "../widgets/VideoWidget.h" #include "../../lib/CStack.h" +#include "../../lib/CPlayerState.h" #include "../../lib/ConditionalWait.h" #include "../../lib/GameLibrary.h" #include "../../lib/StartInfo.h" @@ -45,7 +49,14 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [this](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); exit->setBorderColor(Colors::METALLIC_GOLD); - if(allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay) + auto battle = owner.cb->getBattle(br.battleID); + const auto * attackerPlayer = GAME->server().client->gameInfo().getPlayerState(battle->sideToPlayer(BattleSide::ATTACKER)); + const auto * defenderPlayer = GAME->server().client->gameInfo().getPlayerState(battle->sideToPlayer(BattleSide::DEFENDER)); + bool isAttackerHuman = attackerPlayer && attackerPlayer->isHuman(); + bool isDefenderHuman = defenderPlayer && defenderPlayer->isHuman(); + bool onlyOnePlayerHuman = isAttackerHuman != isDefenderHuman; + + if((allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay) && onlyOnePlayerHuman) { repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [this](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); repeat->setBorderColor(Colors::METALLIC_GOLD); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 57aee279d..5c2a990d2 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -51,6 +51,7 @@ #include "../../lib/entities/artifact/CArtHandler.h" #include "../../lib/filesystem/ResourcePath.h" #include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/texts/CGeneralTextHandler.h" @@ -851,6 +852,8 @@ void BattleWindow::endWithAutocombat() void BattleWindow::showAll(Canvas & to) { + if(owner.curInt->cb->getMapHeader()->battleOnly) + to.fillTexture(ENGINE->renderHandler().loadImage(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE)); CIntObject::showAll(to); if (ENGINE->screenDimensions().x != 800 || ENGINE->screenDimensions().y !=600) diff --git a/client/lobby/BattleOnlyMode.cpp b/client/lobby/BattleOnlyMode.cpp new file mode 100644 index 000000000..d9b71da35 --- /dev/null +++ b/client/lobby/BattleOnlyMode.cpp @@ -0,0 +1,517 @@ +/* + * BattleOnlyMode.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "BattleOnlyMode.h" + +#include "../CServerHandler.h" +#include "../GameEngine.h" +#include "../GameInstance.h" + +#include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/CanvasImage.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/TextControls.h" +#include "../widgets/CTextInput.h" +#include "../widgets/Images.h" +#include "../windows/GUIClasses.h" +#include "../windows/CHeroOverview.h" +#include "../windows/CCreatureWindow.h" + +#include "../../lib/GameLibrary.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/networkPacks/PacksForLobby.h" +#include "../../lib/StartInfo.h" +#include "../../lib/VCMIDirs.h" +#include "../../lib/CRandomGenerator.h" +#include "../../lib/callback/EditorCallback.h" +#include "../../lib/entities/hero/CHero.h" +#include "../../lib/entities/hero/CHeroClass.h" +#include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/entities/faction/CTown.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" +#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/mapping/CMapEditManager.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/mapping/MapFormat.h" +#include "../../lib/texts/CGeneralTextHandler.h" +#include "../../lib/texts/MetaString.h" +#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()) + , 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); + 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")); + + battlefieldSelector = std::make_shared(Point(29, 174), AnimationPath::builtin("GSPButtonClear"), CButton::tooltip(), [this](){ + std::vector texts; + std::vector> images; + + auto & terrains = LIBRARY->terrainTypeHandler->objects; + for (const auto & terrain : terrains) + { + if(!terrain->isPassable()) + continue; + + 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 = std::nullopt; + } + else + { + startInfo->selectedTerrain = std::nullopt; + 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); + }); + battlefieldSelector->block(GAME->server().isGuest()); + buttonReset = std::make_shared(Point(289, 174), AnimationPath::builtin("GSPButtonClear"), 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)); + for(size_t i=0; iselectedArmyInput.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(); + onChange(); + }); + 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->setInputEnabled(GAME->server().isHost()); + + onChange(); +} + +void BattleOnlyModeWindow::init() +{ + map = std::make_unique(nullptr); + map->version = EMapFormat::VCMI; + map->creationDateTime = std::time(nullptr); + map->width = 10; + map->height = 10; + map->mapLevels = 1; + map->battleOnly = true; + map->name = MetaString::createFromTextID("vcmi.lobby.battleOnlyMode"); + + cb = std::make_unique(map.get()); +} + +void BattleOnlyModeWindow::onChange() +{ + GAME->server().setBattleOnlyModeStartInfo(startInfo); +} + +void BattleOnlyModeWindow::update() +{ + setTerrainButtonText(); + setOkButtonEnabled(); + + heroSelector1->setHeroIcon(); + heroSelector1->setCreatureIcons(); + heroSelector2->setHeroIcon(); + heroSelector2->setCreatureIcons(); + redraw(); +} + +void BattleOnlyModeWindow::applyStartInfo(std::shared_ptr si) +{ + startInfo = si; + update(); +} + +void BattleOnlyModeWindow::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() +{ + 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) +{ + 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); + return image; +} + +BattleOnlyModeHeroSelector::BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& p, Point position) +: parent(p) +, id(id) +{ + OBJECT_CONSTRUCTION; + + pos.x += position.x; + pos.y += position.y; + + backgroundImage = std::make_shared(ImagePath::builtin("heroSlotsBlue"), Point(3, 4)); + + for(size_t i=0; i(AnimationPath::builtin("PSKIL32"), i, 0, 78 + i * 36, 26); + primSkills.push_back(image); + primSkillsBorder.push_back(std::make_shared(Rect(78 + i * 36, 26, 32, 32))); + primSkillsBorder.back()->addRectangle(Point(0, 0), Point(32, 32), ColorRGBA(44, 108, 255)); + primSkillsInput.push_back(std::make_shared(Rect(78 + i * 36, 58, 32, 16), EFonts::FONT_SMALL, ETextAlignment::CENTER, false)); + primSkillsInput.back()->setColor(id == 1 ? Colors::WHITE : parent.disabledColor); + primSkillsInput.back()->setFilterNumber(0, 100); + primSkillsInput.back()->setText("0"); + primSkillsInput.back()->setCallback([this, i, id](const std::string & text){ + parent.startInfo->primSkillLevel[id][i] = std::stoi(primSkillsInput[i]->getText()); + parent.onChange(); + }); + } + + creatureImage.resize(GameConstants::ARMY_SIZE); + for(size_t i=0; i(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()->setText("1"); + selectedArmyInput.back()->setCallback([this, i, id](const std::string & text){ + if(parent.startInfo->selectedArmy[id][i].getId() != CreatureID::NONE) + { + parent.startInfo->selectedArmy[id][i].setCount(TextOperations::parseMetric(text)); + parent.onChange(); + selectedArmyInput[i]->enable(); + } + else + selectedArmyInput[i]->disable(); + }); + } + + setHeroIcon(); + setCreatureIcons(); +} + +void BattleOnlyModeHeroSelector::setHeroIcon() +{ + OBJECT_CONSTRUCTION; + + 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)); + 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"); + } + 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()); + for(size_t i=0; isetText(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] ? 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] = std::nullopt; + parent.onChange(); + return; + } + index--; + + parent.startInfo->selectedHero[id] = heroes[index]; + + for(size_t i=0; iprimSkillLevel[id][i] = 0; + 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->addRClickCallback([this](){ + if(!parent.startInfo->selectedHero[id]) + return; + + ENGINE->windows().createAndPushWindow(parent.startInfo->selectedHero[id]->toHeroType()->getId()); + }); +} + +void BattleOnlyModeHeroSelector::setCreatureIcons() +{ + OBJECT_CONSTRUCTION; + + for(int i = 0; i < creatureImage.size(); i++) + { + 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)); + selectedArmyInput[i]->disable(); + } + else + { + auto unit = parent.startInfo->selectedArmy[id][i]; + auto creatureID = unit.getId(); + creatureImage[i] = std::make_shared(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"), EImageBlitMode::COLORKEY)->getImage(LIBRARY->creh->objects.at(creatureID)->getIconIndex()), Point(6 + i * 36, 78)); + selectedArmyInput[i]->setText(TextOperations::formatMetric(unit.getCount(), 3)); + 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() != 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]->addRClickCallback([this, i](){ + if(parent.startInfo->selectedArmy[id][i].getId() == CreatureID::NONE) + return; + + ENGINE->windows().createAndPushWindow(LIBRARY->creh->objects.at(parent.startInfo->selectedArmy[id][i].getId()).get(), true); + }); + } +} + +void BattleOnlyModeWindow::startBattle() +{ + auto rng = &CRandomGenerator::getDefault(); + + map->initTerrain(); + map->getEditManager()->clearTerrain(rng); + + map->getEditManager()->getTerrainSelection().selectAll(); + map->getEditManager()->drawTerrain(!startInfo->selectedTerrain ? TerrainId::DIRT : *startInfo->selectedTerrain, 0, rng); + + map->players[0].canComputerPlay = true; + map->players[0].canHumanPlay = true; + map->players[1] = map->players[0]; + + auto knownHeroes = LIBRARY->objtypeh->knownSubObjects(Obj::HERO); + + auto addHero = [&, this](int sel, PlayerColor color, const int3 & position) + { + 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->setOwner(color); + obj->pos = position; + for(size_t i=0; ipushPrimSkill(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) + obj->setCreature(SlotID(slot), startInfo->selectedArmy[sel][slot].getId(), startInfo->selectedArmy[sel][slot].getCount()); + map->getEditManager()->insertObject(obj); + }; + + addHero(0, PlayerColor(0), int3(5, 6, 0)); + if(!startInfo->selectedTown) + addHero(1, PlayerColor(1), int3(5, 5, 0)); + else + { + 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); + obj->setOwner(PlayerColor(1)); + obj->pos = int3(5, 5, 0); + for (const auto & building : townObj->getTown()->getAllBuildings()) + townObj->addBuilding(building); + if(!startInfo->selectedHero[1]) + { + for(int slot = 0; slot < GameConstants::ARMY_SIZE; slot++) + if(startInfo->selectedArmy[1][slot].getId() != CreatureID::NONE) + townObj->getArmy()->setCreature(SlotID(slot), startInfo->selectedArmy[1][slot].getId(), startInfo->selectedArmy[1][slot].getCount()); + } + else + addHero(1, PlayerColor(1), int3(5, 5, 0)); + + map->getEditManager()->insertObject(townObj); + } + + auto path = VCMIDirs::get().userDataPath() / "Maps"; + boost::filesystem::create_directories(path); + const std::string fileName = "BattleOnlyMode.vmap"; + const auto fullPath = path / fileName; + CMapService mapService; + mapService.saveMap(map, fullPath); + CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) { return true; }); + + auto mapInfo = std::make_shared(); + mapInfo->mapInit("Maps/BattleOnlyMode"); + GAME->server().setMapInfo(mapInfo); + ExtraOptionsInfo extraOptions; + extraOptions.unlimitedReplay = true; + GAME->server().setExtraOptionsInfo(extraOptions); + GAME->server().sendStartGame(); +} diff --git a/client/lobby/BattleOnlyMode.h b/client/lobby/BattleOnlyMode.h new file mode 100644 index 000000000..eac3f08ef --- /dev/null +++ b/client/lobby/BattleOnlyMode.h @@ -0,0 +1,92 @@ +/* + * BattleOnlyMode.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" +#include "../../lib/constants/EntityIdentifiers.h" + + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CCreatureSet; +class CMap; +class EditorCallback; +class BattleOnlyModeStartInfo; +VCMI_LIB_NAMESPACE_END + +class FilledTexturePlayerColored; +class CButton; +class CPicture; +class CLabel; +class BattleOnlyModeWindow; +class CAnimImage; +class GraphicalPrimitiveCanvas; +class CTextInput; +class TransparentFilledRectangle; + +class BattleOnlyMode +{ +public: + static void openBattleWindow(); +}; + +class BattleOnlyModeHeroSelector : public CIntObject +{ +private: + BattleOnlyModeWindow& parent; + + std::shared_ptr backgroundImage; + std::shared_ptr heroImage; + std::shared_ptr heroLabel; + std::vector> creatureImage; + + int id; +public: + std::vector> primSkills; + std::vector> primSkillsBorder; + std::vector> primSkillsInput; + + std::vector> selectedArmyInput; + + void setHeroIcon(); + void setCreatureIcons(); + BattleOnlyModeHeroSelector(int id, BattleOnlyModeWindow& parent, Point position); +}; + +class BattleOnlyModeWindow : public CWindowObject +{ + friend class BattleOnlyModeHeroSelector; +private: + std::shared_ptr startInfo; + std::unique_ptr map; + std::shared_ptr cb; + + std::shared_ptr backgroundTexture; + std::shared_ptr buttonOk; + std::shared_ptr buttonAbort; + std::shared_ptr title; + + std::shared_ptr battlefieldSelector; + std::shared_ptr buttonReset; + std::shared_ptr heroSelector1; + std::shared_ptr heroSelector2; + + ColorRGBA disabledColor; + + void init(); + void onChange(); + void update(); + void setTerrainButtonText(); + void setOkButtonEnabled(); + void startBattle(); +public: + BattleOnlyModeWindow(); + void applyStartInfo(std::shared_ptr si); +}; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 8e9f2d45c..6ed53ab08 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -172,14 +172,26 @@ RandomMapTab::RandomMapTab(): { std::vector texts; texts.push_back(readText(variables["randomTemplate"])); - for(auto & t : getTemplates()) - texts.push_back(t->getName()); + + auto selectedTemplate = mapGenOptions->getMapTemplate(); + const auto& templates = getTemplates(); + for(int i = 0; i < templates.size(); i++) + { + if(selectedTemplate) + { + if(templates[i]->getId() == selectedTemplate->getId()) + templateIndex = i + 1; + } + else + templateIndex = 0; + + texts.push_back(templates[i]->getName()); + } ENGINE->windows().popWindows(1); ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.hover"), LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.help"), [this](int index){ widget("templateList")->setItem(index); - templateIndex = index; - }, templateIndex, std::vector>(), true); + }, templateIndex, std::vector>(), true, true); }); } diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a35922ba0..ddd8afb34 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -12,6 +12,7 @@ #include "SelectionTab.h" #include "CSelectionBase.h" #include "CLobbyScreen.h" +#include "BattleOnlyMode.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" @@ -241,6 +242,13 @@ SelectionTab::SelectionTab(ESelectionScreen Type) sortByDate->setOverlay(std::make_shared(ImagePath::builtin("lobby/selectionTabSortDate"))); buttonsSortBy.push_back(sortByDate); + 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(); + }); + } + if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame) { buttonDeleteMode = std::make_shared(Point(367, 18), AnimationPath::builtin("lobby/deleteButton"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.deleteMode")), [this, tabTitle, tabTitleDelete](){ @@ -315,6 +323,8 @@ void SelectionTab::toggleMode() { if(slider) slider->block(true); + if(buttonBattleOnlyMode) + buttonBattleOnlyMode->block(true); } else { @@ -325,6 +335,7 @@ void SelectionTab::toggleMode() inputName->disable(); auto files = getFiles("Maps/", EResType::MAP); files.erase(ResourcePath("Maps/Tutorial.tut", EResType::MAP)); + files.erase(ResourcePath("Maps/BattleOnlyMode.vmap", EResType::MAP)); parseMaps(files); break; } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 1718173a6..de3768323 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -128,6 +128,8 @@ private: std::shared_ptr buttonDeleteMode; bool deleteMode; + std::shared_ptr buttonBattleOnlyMode; + bool enableUiEnhancements; std::shared_ptr buttonCampaignSet; diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 7eddde407..5b9f9fe28 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -93,6 +93,14 @@ void AssetGenerator::initialize() animationFiles[AnimationPath::builtin("SPRITES/GSPButtonClear")] = createGSPButtonClear(); + for (PlayerColor color(-1); color < PlayerColor::PLAYER_LIMIT; ++color) + { + std::string name = "TownPortalBackgroundBlue" + (color == -1 ? "" : "-" + color.toString()); + imageFiles[ImagePath::builtin(name)] = [this, color](){ return createGateListColored(std::max(PlayerColor(0), color), PlayerColor(1)); }; + } + + imageFiles[ImagePath::builtin("heroSlotsBlue.png")] = [this](){ return createHeroSlotsColored(PlayerColor(1));}; + createPaletteShiftedSprites(); } @@ -129,6 +137,22 @@ void AssetGenerator::addAnimationFile(const AnimationPath & path, AnimationLayou animationFiles[path] = anim; } +auto getColorFilters() +{ + auto filterSettings = LIBRARY->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"]; + static const std::array filters = { + ColorFilter::genRangeShifter( filterSettings["red" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["blue" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["tan" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["green" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["orange"].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["purple"].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["teal" ].convertTo>() ), + ColorFilter::genRangeShifter( filterSettings["pink" ].convertTo>() ) + }; + return filters; +} + AssetGenerator::CanvasPtr AssetGenerator::createAdventureOptionsCleanBackground() const { auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE); @@ -208,17 +232,7 @@ AssetGenerator::CanvasPtr AssetGenerator::createPlayerColoredBackground(const Pl std::shared_ptr texture = ENGINE->renderHandler().loadImage(locator); // transform to make color of brown DIBOX.PCX texture match color of specified player - auto filterSettings = LIBRARY->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"]; - static const std::array filters = { - ColorFilter::genRangeShifter( filterSettings["red" ].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["blue" ].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["tan" ].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["green" ].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["orange"].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["purple"].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["teal" ].convertTo>() ), - ColorFilter::genRangeShifter( filterSettings["pink" ].convertTo>() ) - }; + static const std::array filters = getColorFilters(); assert(player.isValidPlayer()); if (!player.isValidPlayer()) @@ -884,3 +898,67 @@ AssetGenerator::AnimationLayoutMap AssetGenerator::createGSPButtonClear() return layout; } + +AssetGenerator::CanvasPtr AssetGenerator::createGateListColored(PlayerColor color, PlayerColor backColor) const +{ + auto locator = ImageLocator(ImagePath::builtin("TpGate"), EImageBlitMode::COLORKEY); + std::shared_ptr img = ENGINE->renderHandler().loadImage(locator); + img->playerColored(color); + std::shared_ptr imgColored = ENGINE->renderHandler().loadImage(locator); + static const std::array filters = getColorFilters(); + imgColored->adjustPalette(filters[backColor.getNum()], 0); + + auto image = ENGINE->renderHandler().createImage(img->dimensions(), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); + + canvas.draw(imgColored, Point(0, 0)); + + std::vector keepOriginalRects = { + Rect(0, 0, 14, 393), + Rect(293, 0, 13, 393), + Rect(0, 393, 8, 76), + Rect(299, 393, 6, 76), + Rect(0, 0, 306, 16), + Rect(0, 383, 306, 10), + Rect(0, 441, 306, 2), + Rect(0, 462, 306, 7), + // Edges + Rect(14, 15, 2, 5), + Rect(16, 15, 3, 2), + Rect(16, 17, 1, 1), + Rect(14, 379, 3, 4), + Rect(16, 381, 2, 2), + Rect(16, 380, 1, 1), + Rect(289, 16, 2, 2), + Rect(291, 16, 2, 4), + Rect(289, 381, 2, 2), + Rect(291, 379, 2, 4) + }; + for(auto & rect : keepOriginalRects) + canvas.draw(img, Point(rect.x, rect.y), rect); + + std::vector blackRect = { + Rect(14, 401, 66, 32), + Rect(227, 401, 66, 32) + }; + for(auto & rect : blackRect) + canvas.drawBorder(rect, Colors::BLACK); + + return image; +} + +AssetGenerator::CanvasPtr AssetGenerator::createHeroSlotsColored(PlayerColor backColor) const +{ + auto locator = ImageLocator(AnimationPath::builtin("OVSLOT"), 4, 0, EImageBlitMode::COLORKEY); + std::shared_ptr img = ENGINE->renderHandler().loadImage(locator); + static const std::array filters = getColorFilters(); + img->adjustPalette(filters[backColor.getNum()], 0); + + auto image = ENGINE->renderHandler().createImage(Point(260, 150), 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)); + + return image; +} diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h index d0a95cc9a..96e9cbc54 100644 --- a/client/render/AssetGenerator.h +++ b/client/render/AssetGenerator.h @@ -66,6 +66,8 @@ private: CanvasPtr createCreatureInfoPanelElement(CreatureInfoPanelElement element) const; CanvasPtr createQuestWindow() const; AnimationLayoutMap createGSPButtonClear(); + CanvasPtr createGateListColored(PlayerColor color, PlayerColor backColor) const; + CanvasPtr createHeroSlotsColored(PlayerColor backColor) const; void createPaletteShiftedSprites(); void generatePaletteShiftedAnimation(const AnimationPath & source, const std::vector & animation); diff --git a/client/render/CanvasImage.cpp b/client/render/CanvasImage.cpp index bfb718adb..88327f524 100644 --- a/client/render/CanvasImage.cpp +++ b/client/render/CanvasImage.cpp @@ -65,6 +65,8 @@ Rect CanvasImage::contentRect() const Point CanvasImage::dimensions() const { + if (scalingPolicy != CanvasScalingPolicy::IGNORE) + return Point(surface->w, surface->h) / ENGINE->screenHandler().getScalingFactor(); return {surface->w, surface->h}; } diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 70ab3c343..0320e925c 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -200,9 +200,9 @@ void CTextInput::setFilterFilename() onTextFiltering = std::bind(&CTextInput::filenameFilter, _1, _2); } -void CTextInput::setFilterNumber(int minValue, int maxValue) +void CTextInput::setFilterNumber(int minValue, int maxValue, int metricDigits) { - onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue); + onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue, metricDigits); } std::string CTextInput::getVisibleText() const @@ -256,6 +256,10 @@ void CTextInput::keyPressed(EShortcut key) if(redrawNeeded) { + std::string oldText = currentText; + if(onTextFiltering) + onTextFiltering(currentText, oldText); + updateLabel(); if(onTextEdited) onTextEdited(currentText); @@ -295,9 +299,9 @@ void CTextInput::textInputted(const std::string & enteredText) if(onTextFiltering) onTextFiltering(currentText, oldText); + updateLabel(); if(currentText != oldText) { - updateLabel(); if(onTextEdited) onTextEdited(currentText); } @@ -321,40 +325,63 @@ void CTextInput::filenameFilter(std::string & text, const std::string &oldText) text.erase(pos, 1); } -void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue) +std::optional getMetricSuffix(const std::string& text) +{ + const std::string suffixes = "kKmMgGtTpPeE"; + std::vector found; + + // Collect all suffixes in the string + for (char c : text) { + if (suffixes.find(c) != std::string::npos) { + // Normalize: 'k' lowercase, others uppercase + found.push_back((c == 'k' || c == 'K') ? 'k' : static_cast(std::toupper(c))); + } + } + + if (found.empty()) return std::nullopt; // No suffix + if (found.size() == 1) return found[0]; // Single suffix + // More than one suffix + bool allSame = std::all_of(found.begin(), found.end(), [&](char c){ return c == found[0]; }); + if (allSame) return std::nullopt; // Multiple but identical → nullopt + return found.back(); // Multiple different → last suffix +} + + +void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue, int metricDigits) { assert(minValue < maxValue); - if(text.empty()) + bool isNegative = std::count_if(text.begin(), text.end(), [](char c){ return c == '-'; }) == 1 && minValue < 0; + auto suffix = getMetricSuffix(text); + if(metricDigits == 0) + suffix = std::nullopt; + + // Remove all non-digit characters + text.erase(std::remove_if(text.begin(), text.end(), [](char c){ return !isdigit(c); }), text.end()); + + // Remove leading zeros + size_t firstNonZero = text.find_first_not_of('0'); + if (firstNonZero > 0) + text.erase(0, firstNonZero); + + if (text.empty()) text = "0"; - size_t pos = 0; - if(text[0] == '-') //allow '-' sign as first symbol only - pos++; + // Add negative sign + text = (isNegative ? "-" : "") + text; - while(pos < text.size()) - { - if(text[pos] < '0' || text[pos] > '9') - { - text = oldText; - return; //new text is not number. - } - pos++; - } - try - { - int value = boost::lexical_cast(text); - if(value < minValue) - text = std::to_string(minValue); - else if(value > maxValue) - text = std::to_string(maxValue); - } - catch(boost::bad_lexical_cast &) - { - //Should never happen. Unless I missed some cases - logGlobal->warn("Warning: failed to convert %s to number!", text); - text = oldText; - } + // Restore suffix if it exists + if (suffix) + text += *suffix; + + // Clamp value + int value = TextOperations::parseMetric(text); + if (metricDigits) + text = (isNegative && value == 0 ? "-" : "") + TextOperations::formatMetric(value, metricDigits); + 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); } void CTextInput::activate() diff --git a/client/widgets/CTextInput.h b/client/widgets/CTextInput.h index 9edbdde3d..9e8bca271 100644 --- a/client/widgets/CTextInput.h +++ b/client/widgets/CTextInput.h @@ -66,7 +66,7 @@ protected: static void filenameFilter(std::string & text, const std::string & oldText); //Filter that will allow only input of numbers in range min-max (min-max are allowed) //min-max should be set via something like std::bind - static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue); + static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue, int metricDigits); std::string getVisibleText() const; void createLabel(bool giveFocusToInput); @@ -99,7 +99,7 @@ public: /// Enables filtering entered text that ensures that text is valid filename (existing or not) void setFilterFilename(); /// Enable filtering entered text that ensures that text is valid number in provided range [min, max] - void setFilterNumber(int minValue, int maxValue); + void setFilterNumber(int minValue, int maxValue, int metricDigits=0); void setFont(EFonts Font); void setColor(const ColorRGBA & Color); diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index ccb750d89..68f282aaa 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -46,7 +46,7 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) pos.w = bg->width(); pos.h = bg->height(); - addUsedEvents(SHOW_POPUP); + addUsedEvents(LCLICK | SHOW_POPUP); } CPicture::CPicture( const ImagePath &bmpname, int x, int y ) @@ -75,7 +75,7 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position, EImageBli pos.w = pos.h = 0; } - addUsedEvents(SHOW_POPUP); + addUsedEvents(LCLICK | SHOW_POPUP); } CPicture::CPicture( const ImagePath & bmpname, const Point & position ) @@ -89,7 +89,7 @@ CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y) pos.w = srcRect->w; pos.h = srcRect->h; - addUsedEvents(SHOW_POPUP); + addUsedEvents(LCLICK | SHOW_POPUP); } CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, int y) @@ -99,7 +99,7 @@ CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, in pos.w = srcRect->w; pos.h = srcRect->h; - addUsedEvents(SHOW_POPUP); + addUsedEvents(LCLICK | SHOW_POPUP); } void CPicture::show(Canvas & to) @@ -137,11 +137,22 @@ void CPicture::setPlayerColor(PlayerColor player) bg->playerColored(player); } +void CPicture::addLClickCallback(const std::function & callback) +{ + lCallback = callback; +} + void CPicture::addRClickCallback(const std::function & callback) { rCallback = callback; } +void CPicture::clickPressed(const Point & cursorPosition) +{ + if(lCallback) + lCallback(); +} + void CPicture::showPopupWindow(const Point & cursorPosition) { if(rCallback) diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 6f4a9c950..c0c37ee95 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -27,6 +27,7 @@ enum class EImageBlitMode : uint8_t; class CPicture : public CIntObject { std::shared_ptr bg; + std::function lCallback; std::function rCallback; public: @@ -60,10 +61,12 @@ public: void scaleTo(Point size); void setPlayerColor(PlayerColor player); + void addLClickCallback(const std::function & callback); void addRClickCallback(const std::function & callback); void show(Canvas & to) override; void showAll(Canvas & to) override; + void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; }; diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index c851fde55..3432b3b89 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -623,13 +623,15 @@ void MoraleLuckBox::set(const AFactionMember * node) else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE)) { auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE)); - text += "\n" + noMorale->Description(GAME->interface()->cb.get()); + if(GAME->interface()) + text += "\n" + noMorale->Description(GAME->interface()->cb.get()); component.value = 0; } else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK)) { auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK)); - text += "\n" + noLuck->Description(GAME->interface()->cb.get()); + if(GAME->interface()) + text += "\n" + noLuck->Description(GAME->interface()->cb.get()); component.value = 0; } else @@ -637,7 +639,7 @@ void MoraleLuckBox::set(const AFactionMember * node) std::string addInfo = ""; for(auto & bonus : * modifierList) { - if(bonus->val) { + if(GAME->interface() && bonus->val) { const std::string& description = bonus->Description(GAME->interface()->cb.get()); //arraytxt already contains \n if (description.size() && description[0] != '\n') diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index c58959402..89ba786cf 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -952,8 +952,8 @@ void CStackWindow::initSections() { OBJECT_CONSTRUCTION; - bool showArt = GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; - bool showExp = (GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode; + bool showArt = GAME->interface() && GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; + bool showExp = ((GAME->interface() && GAME->interface()->cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) || info->commander != nullptr) && info->stackNode; mainSection = std::make_shared(this, pos.h, showExp, showArt); diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index e0665ed28..071f7a4bf 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -86,7 +86,9 @@ std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, b return nullptr; auto image = std::make_shared(imageName, Point(0,0), EImageBlitMode::OPAQUE); - if(playerColored && GAME->interface()) + if(!GAME->interface()) + image->setPlayerColor(PlayerColor(1)); // in main menu we use blue + else if(playerColored) image->setPlayerColor(GAME->interface()->playerID); return image; } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 224ea5766..c9f8665ce 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1535,8 +1535,9 @@ CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::st index(_id) { OBJECT_CONSTRUCTION; - if(parent->images.size() > index) - icon = std::make_shared(parent->images[index], Point(1, 1)); + auto imgIndex = parent->itemsVisible[index].first; + if(parent->images.size() > index && parent->images[imgIndex]) + icon = std::make_shared(parent->images[imgIndex], Point(1, 1)); border = std::make_shared(ImagePath::builtin("TPGATES")); pos = border->pos; @@ -1577,12 +1578,13 @@ void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) void CObjectListWindow::CItem::showPopupWindow(const Point & cursorPosition) { + int where = parent->itemsVisible[index].first; if(parent->onPopup) - parent->onPopup(index); + parent->onPopup(where); } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled, bool blue) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin(blue ? "TownPortalBackgroundBlue" : "TPGATE")), onSelect(Callback), selected(initialSelection), images(images) @@ -1601,12 +1603,12 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share } itemsVisible = items; - init(titleWidget_, _title, _descr, searchBoxEnabled); + init(titleWidget_, _title, _descr, searchBoxEnabled, blue); list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images, bool searchBoxEnabled, bool blue) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin(blue ? "TownPortalBackgroundBlue" : "TPGATE")), onSelect(Callback), selected(initialSelection), images(images) @@ -1625,17 +1627,17 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, st } itemsVisible = items; - init(titleWidget_, _title, _descr, searchBoxEnabled); + init(titleWidget_, _title, _descr, searchBoxEnabled, blue); list->scrollTo(std::min(static_cast(initialSelection + 4), static_cast(items.size() - 1))); // 4 is for centering (list have 9 elements) } -void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled) +void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled, bool blue) { titleWidget = titleWidget_; title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); - exit = std::make_shared( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); + exit = std::make_shared( Point(228, 402), AnimationPath::builtin(blue ? "MuBcanc" : "ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); if(titleWidget) { @@ -1644,10 +1646,10 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri titleWidget->pos.y = 75 + pos.y - titleWidget->pos.h/2; } list = std::make_shared(std::bind(&CObjectListWindow::genItem, this, _1), - Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1, Rect(262, -32, 256, 256) ); + Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1 + (blue ? 4 : 0), Rect(262, -32, 256, 256) ); list->setRedrawParent(true); - ok = std::make_shared(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); + ok = std::make_shared(Point(15, 402), AnimationPath::builtin(blue ? "MuBchck" : "IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); ok->block(!list->size()); if(!searchBoxEnabled) @@ -1655,8 +1657,8 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri Rect r(50, 90, pos.w - 100, 16); const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); - const ColorRGBA borderColor = ColorRGBA(128, 100, 75); - const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); + const ColorRGBA borderColor = blue ? ColorRGBA(75, 84, 128) : ColorRGBA(128, 100, 75); + const ColorRGBA grayedColor = blue ? ColorRGBA(105, 127, 159) : ColorRGBA(158, 130, 105); searchBoxRectangle = std::make_shared(r.resize(1), rectangleColor, borderColor); searchBoxDescription = std::make_shared(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, LIBRARY->generaltexth->translate("vcmi.spellBook.search")); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 6a59a8c1b..72d805ded 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -205,7 +205,7 @@ class CObjectListWindow : public CWindowObject std::vector< std::pair > items; //all items present in list std::vector< std::pair > itemsVisible; //visible items present in list - void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled); + void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled, bool blue); void trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const; // trim item's text to fit within window's width void itemsSearchCallback(const std::string & text); void exitPressed(); @@ -219,8 +219,8 @@ public: /// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item /// Image can be nullptr ///item names will be taken from map objects - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false); - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false, bool blue = false); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}, bool searchBoxEnabled = false, bool blue = false); std::shared_ptr genItem(size_t index); void elementSelected();//call callback and close this window diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index bcc272dcf..50181bfe0 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -241,4 +241,13 @@ TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color) return TeamID::NO_TEAM; } +BattleOnlyModeStartInfo::BattleOnlyModeStartInfo() + : selectedTerrain(TerrainId::DIRT) + , selectedTown(std::nullopt) +{ + for(auto & element : primSkillLevel) + for(size_t i=0; i selectedTerrain; + std::optional selectedTown; + + std::array, 2> selectedHero; + std::array, 2> selectedArmy; + + std::array, 2> primSkillLevel; + + BattleOnlyModeStartInfo(); + + template void serialize(Handler &h) + { + h & selectedTerrain; + h & selectedTown; + h & selectedHero; + h & selectedArmy; + h & primSkillLevel; + } +}; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index cfb0ec938..4aa76445c 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1345,28 +1345,6 @@ void CGHeroInstance::restoreBonusSystem(CGameState & gs) } } -void CGHeroInstance::attachToBonusSystem(CGameState & gs) -{ - CArmedInstance::attachToBonusSystem(gs); - if (boardedBoat.hasValue()) - { - auto boat = gs.getObjInstance(boardedBoat); - if (boat) - attachTo(dynamic_cast(*boat)); - } -} - -void CGHeroInstance::detachFromBonusSystem(CGameState & gs) -{ - CArmedInstance::detachFromBonusSystem(gs); - if (boardedBoat.hasValue()) - { - auto boat = gs.getObjInstance(boardedBoat); - if (boat) - detachFrom(dynamic_cast(*boat)); - } -} - CBonusSystemNode & CGHeroInstance::whereShouldBeAttached(CGameState & gs) { if(visitedTown.hasValue()) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index f2f63be95..361636f71 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -311,8 +311,6 @@ public: void afterAddToMap(CMap * map) override; void afterRemoveFromMap(CMap * map) override; - void attachToBonusSystem(CGameState & gs) override; - void detachFromBonusSystem(CGameState & gs) override; void restoreBonusSystem(CGameState & gs) override; void updateFrom(const JsonNode & data) override; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 7fc258be3..f26f6b3ed 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -129,6 +129,7 @@ CMapHeader::CMapHeader() , defeatIconIndex(0) , howManyTeams(0) , areAnyPlayers(false) + , battleOnly(false) { setupEvents(); allowedHeroes = LIBRARY->heroh->getDefaultAllowed(); diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index dbb8c0c71..c86ffc142 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -272,6 +272,8 @@ public: bool areAnyPlayers; /// Unused. True if there are any playable players on the map. + bool battleOnly; /// Battle only mode + /// "main quests" of the map that describe victory and loss conditions std::vector triggeredEvents; @@ -316,6 +318,8 @@ public: h & levelLimit; h & areAnyPlayers; + if (h.version >= Handler::Version::BATTLE_ONLY) + h & battleOnly; h & players; h & howManyTeams; h & allowedHeroes; diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index 70de36ad1..800740067 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -120,14 +120,14 @@ void CTerrainSelection::setSelection(const std::vector & vec) void CTerrainSelection::selectAll() { - selectRange(MapRect(int3(0, 0, 0), getMap()->width, getMap()->height)); - selectRange(MapRect(int3(0, 0, 1), getMap()->width, getMap()->height)); + for(int i = 0; i < getMap()->mapLevels; i++) + selectRange(MapRect(int3(0, 0, i), getMap()->width, getMap()->height)); } void CTerrainSelection::clearSelection() { - deselectRange(MapRect(int3(0, 0, 0), getMap()->width, getMap()->height)); - deselectRange(MapRect(int3(0, 0, 1), getMap()->width, getMap()->height)); + for(int i = 0; i < getMap()->mapLevels; i++) + deselectRange(MapRect(int3(0, 0, i), getMap()->width, getMap()->height)); } CObjectSelection::CObjectSelection(CMap * map) : CMapSelection(map) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 36f7717c2..dae8d9a0e 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -870,6 +870,8 @@ void CMapLoaderJson::readHeader(const bool complete) readTeams(handler); //TODO: check mods + mapHeader->battleOnly = header["battleOnly"].Bool(); + if(complete) readOptions(handler); @@ -1225,6 +1227,8 @@ void CMapSaverJson::writeHeader() writeTeams(handler); + header["battleOnly"].Bool() = mapHeader->battleOnly; + writeOptions(handler); writeTranslations(); diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index d1221172f..88cbde6b1 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -170,6 +170,7 @@ public: virtual void visitLobbySetCampaign(LobbySetCampaign & pack) {} virtual void visitLobbySetCampaignMap(LobbySetCampaignMap & pack) {} virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) {} + virtual void visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) {} virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {} virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {} virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {} diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index c7df14a35..43cd343a1 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -773,6 +773,11 @@ void LobbySetCampaignBonus::visitTyped(ICPackVisitor & visitor) visitor.visitLobbySetCampaignBonus(*this); } +void LobbySetBattleOnlyModeStartInfo::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetBattleOnlyModeStartInfo(*this); +} + void LobbyChangePlayerOption::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbyChangePlayerOption(*this); diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index cd7608954..9c3fb54d6 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -524,6 +524,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient PlayerColor player; EVictoryLossCheckResult victoryLossCheckResult; StatisticDataSet statistic; + bool silentEnd = false; void visitTyped(ICPackVisitor & visitor) override; @@ -532,6 +533,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient h & player; h & victoryLossCheckResult; h & statistic; + h & silentEnd; } }; diff --git a/lib/networkPacks/PacksForLobby.h b/lib/networkPacks/PacksForLobby.h index 549499fdc..0878ff7d6 100644 --- a/lib/networkPacks/PacksForLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -90,7 +90,7 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate { enum EAction : ui8 { - NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS, OPEN_TURN_OPTIONS, OPEN_EXTRA_OPTIONS + NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS, OPEN_TURN_OPTIONS, OPEN_EXTRA_OPTIONS, BATTLE_MODE } action = NONE; @@ -230,6 +230,18 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer } }; +struct DLL_LINKAGE LobbySetBattleOnlyModeStartInfo : public CLobbyPackToPropagate +{ + std::shared_ptr startInfo; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) + { + h & startInfo; + } +}; + struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer { enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS, TOWN_ID, HERO_ID, BONUS_ID}; diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index e3f1520db..a2c534e3e 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -51,8 +51,9 @@ enum class ESerializationVersion : int32_t MORE_MAP_LAYERS, // more map layers CONFIGURABLE_RESOURCES, // configurable resources CUSTOM_NAMES, // custom names + BATTLE_ONLY, // battle only mode - CURRENT = CUSTOM_NAMES, + CURRENT = BATTLE_ONLY, }; static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!"); diff --git a/lib/serializer/RegisterTypes.h b/lib/serializer/RegisterTypes.h index 9116a902d..be48a67c1 100644 --- a/lib/serializer/RegisterTypes.h +++ b/lib/serializer/RegisterTypes.h @@ -293,6 +293,7 @@ void registerTypes(Serializer &s) s.template registerType(251); s.template registerType(252); s.template registerType(253); + s.template registerType(254); } VCMI_LIB_NAMESPACE_END diff --git a/lib/texts/TextOperations.h b/lib/texts/TextOperations.h index bae9c5873..710352b82 100644 --- a/lib/texts/TextOperations.h +++ b/lib/texts/TextOperations.h @@ -9,6 +9,8 @@ */ #pragma once +#include + VCMI_LIB_NAMESPACE_BEGIN /// Namespace that provides utilities for unicode support (UTF-8) @@ -54,6 +56,9 @@ namespace TextOperations template inline std::string formatMetric(Arithmetic number, int maxDigits); + template + inline Arithmetic parseMetric(const std::string &text); + /// replaces all symbols that normally need escaping with appropriate escape sequences std::string escapeString(std::string input); @@ -116,4 +121,66 @@ inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits return std::to_string(number) + *iter; } +template +inline Arithmetic TextOperations::parseMetric(const std::string &text) +{ + if (text.empty()) + return 0; + + // Trim whitespace + std::string trimmed = text; + trimmed.erase(trimmed.begin(), std::find_if(trimmed.begin(), trimmed.end(), [](unsigned char ch){ return !std::isspace(ch); })); + trimmed.erase(std::find_if(trimmed.rbegin(), trimmed.rend(), [](unsigned char ch){ return !std::isspace(ch); }).base(), trimmed.end()); + + // Check if last character is a metric suffix + char last = trimmed.back(); + int power = 0; // number of *1000 multiplications + + switch (std::toupper(last)) + { + case 'K': power = 1; break; + case 'M': power = 2; break; + case 'G': power = 3; break; + case 'T': power = 4; break; + case 'P': power = 5; break; + case 'E': power = 6; break; + default: power = 0; break; // no suffix + } + + std::string numberPart = trimmed; + if (power > 0) + numberPart.pop_back(); + + // Remove any non-digit or minus sign (same spirit as your numberFilter) + numberPart.erase(std::remove_if(numberPart.begin(), numberPart.end(), [](char c) + { + return !(std::isdigit(static_cast(c)) || c == '-'); + }), numberPart.end()); + + if (numberPart.empty() || (numberPart == "-")) + return 0; + + try + { + Arithmetic value = std::stoll(numberPart); + + for (int i = 0; i < power; ++i) + { + // Multiply by 1000, check for overflow if desired + if (value > std::numeric_limits::max() / 1000) + return std::numeric_limits::max(); + if (value < std::numeric_limits::min() / 1000) + return std::numeric_limits::min(); + + value *= static_cast(1000); + } + + return value; + } + catch (std::invalid_argument &) + { + return 0; + } +} + VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 34a827dd9..22de7cd40 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3491,6 +3491,19 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) if(!p || p->status != EPlayerStatus::INGAME) return; + if(gameState().getMap().battleOnly) + { + for(const auto & playerIt : gameState().players) + { + PlayerEndsGame peg; + peg.player = playerIt.first; + peg.silentEnd = true; + sendAndApply(peg); + } + gameServer().setState(EServerState::SHUTDOWN); + return; + } + auto victoryLossCheckResult = gameState().checkForVictoryAndLoss(player); if (victoryLossCheckResult.victory() || victoryLossCheckResult.loss()) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 8bf463da3..6ab7c168b 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -1021,7 +1021,7 @@ void CVCMIServer::multiplayerWelcomeMessage() if(pi.second.isControlledByHuman()) humanPlayer++; - if(humanPlayer < 2) // Singleplayer + if(humanPlayer < 2 || mi->mapHeader->battleOnly) // Singleplayer or Battle only mode return; gh->playerMessages->broadcastSystemMessage(MetaString::createFromTextID("vcmi.broadcast.command")); diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 51a04aa97..e2721eb21 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -47,6 +47,7 @@ public: void visitLobbyGuiAction(LobbyGuiAction & pack) override; void visitLobbyPvPAction(LobbyPvPAction & pack) override; void visitLobbyDelete(LobbyDelete & pack) override; + void visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) override; }; class ApplyOnServerAfterAnnounceNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 15b0cd352..5411a2e4d 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -385,12 +385,16 @@ void ApplyOnServerNetPackVisitor::visitLobbyPvPAction(LobbyPvPAction & pack) result = true; } - void ClientPermissionsCheckerNetPackVisitor::visitLobbyDelete(LobbyDelete & pack) { result = srv.isClientHost(connection->connectionID); } +void ClientPermissionsCheckerNetPackVisitor::visitLobbySetBattleOnlyModeStartInfo(LobbySetBattleOnlyModeStartInfo & pack) +{ + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbyDelete(LobbyDelete & pack) { if(pack.type == LobbyDelete::EType::SAVEGAME || pack.type == LobbyDelete::EType::RANDOMMAP) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 050216e63..9fedd7057 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -26,6 +26,7 @@ #include "../../lib/entities/artifact/CArtifact.h" #include "../../lib/entities/artifact/CArtifactFittingSet.h" #include "../../lib/gameState/CGameState.h" +#include "../../lib/mapping/CMap.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/spells/CSpellHandler.h" diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 04ddfb683..6b9f0efce 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -19,6 +19,8 @@ #include "../../lib/CPlayerState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/mapObjects/CGObjectInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/pathfinder/CPathfinder.h" #include "../../lib/pathfinder/PathfinderOptions.h" @@ -364,6 +366,18 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) void TurnOrderProcessor::onGameStarted() { + if(gameHandler->gameInfo().getMapHeader()->battleOnly) + { + auto towns = gameHandler->gameState().getMap().getObjects(); + auto heroes = gameHandler->gameState().getMap().getObjects(); + if(!towns.size() && heroes.size() == 2) + gameHandler->startBattle(heroes.at(0), heroes.at(1)); + else + towns.at(0)->onHeroVisit(*gameHandler, heroes.at(0)); + + return; + } + if (actingPlayers.empty()) blockedContacts = computeContactStatus();