diff --git a/.gitmodules b/.gitmodules index b484aea8e..d6b206fe1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "test/googletest"] path = test/googletest url = https://github.com/google/googletest - branch = v1.13.x + branch = v1.15.x [submodule "AI/FuzzyLite"] path = AI/FuzzyLite url = https://github.com/fuzzylite/fuzzylite.git diff --git a/ChangeLog.md b/ChangeLog.md index d6d66a6ec..8f2ccba9c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -31,6 +31,35 @@ * Added support for multiple music tracks for terrains on adventure map * Fixed several cases where vcmi will report errors in json without specifying filename of invalid file +# 1.5.5 -> 1.5.6 + +### Stability +* Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred +* Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army +* Fixed possible crash on application shutdown due to incorrect destruction order of UI entities + +### Multiplayer +* Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss. +* Incompatible mods are now placed before compatible mods when joining lobby room. +* Fixed text overflow in online lobby interface +* Fixed jittering simultaneous turns slider after moving it twice over short period +* Fixed non-functioning slider in invite to game room dialog + +### Interface +* Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild. +* Game now correctly uses melee damage calculation when forcing a melee attack with a shooter. +* Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups + +### Map Objects +* Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box +* Spells that cannot be learned are now displayed with gray text in the name of the spell. +* Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward +* Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit + +### Modding +* Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus +* Added compatibility check when loading maps with old names for boats + # 1.5.4 -> 1.5.5 * Fixed crash when advancing to the next scenario in campaigns when the hero not transferring has a combination artefact that can be transferred to the next scenario. diff --git a/Global.h b/Global.h index 57c4466ef..6fa656a17 100644 --- a/Global.h +++ b/Global.h @@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); # define STRONG_INLINE inline #endif +// Required for building boost::stacktrace on macOS. +// See https://github.com/boostorg/stacktrace/issues/88 +#if defined(VCMI_APPLE) +#define _GNU_SOURCE +#endif + #define _USE_MATH_DEFINES #include @@ -700,6 +706,33 @@ namespace vstd return a + (b - a) * f; } + /// Divides dividend by divisor and rounds result up + /// For use with integer-only arithmetic + template + Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result to nearest + /// For use with integer-only arithmetic + template + Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor / 2 - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result down + /// For use with integer-only arithmetic + template + Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return dividend / divisor; + } + template bool isAlmostZero(const Floating & value) { diff --git a/Mods/vcmi/config/vcmi/portuguese.json b/Mods/vcmi/config/vcmi/portuguese.json index de4b67adf..5e1da5196 100644 --- a/Mods/vcmi/config/vcmi/portuguese.json +++ b/Mods/vcmi/config/vcmi/portuguese.json @@ -72,6 +72,11 @@ "vcmi.lobby.noUnderground" : "sem subterrâneo", "vcmi.lobby.sortDate" : "Classifica mapas por data de alteração", "vcmi.lobby.backToLobby" : "Voltar para a sala de espera", + "vcmi.lobby.author" : "Autor", + "vcmi.lobby.handicap" : "Desvant.", + "vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).", + "vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.", + "vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.", "vcmi.lobby.login.title" : "Sala de Espera Online do VCMI", "vcmi.lobby.login.username" : "Nome de usuário:", diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ffbbbab0c..121d764d0 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -171,40 +171,39 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: adventureInt.reset(new AdventureMapInterface()); } +void CPlayerInterface::closeAllDialogs() +{ + // remove all active dialogs that do not expect query answer + for (;;) + { + auto adventureWindow = GH.windows().topWindow(); + auto infoWindow = GH.windows().topWindow(); + + if(adventureWindow != nullptr) + break; + + if(infoWindow && infoWindow->ID != QueryID::NONE) + break; + + if (infoWindow) + infoWindow->close(); + else + GH.windows().popWindows(1); + } + + if(castleInt) + castleInt->close(); + + castleInt = nullptr; +} + void CPlayerInterface::playerEndsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; if (player == playerID) { makingTurn = false; - - // remove all active dialogs that do not expect query answer - for (;;) - { - auto adventureWindow = GH.windows().topWindow(); - auto infoWindow = GH.windows().topWindow(); - - if(adventureWindow != nullptr) - break; - - if(infoWindow && infoWindow->ID != QueryID::NONE) - break; - - if (infoWindow) - infoWindow->close(); - else - GH.windows().popWindows(1); - } - - if(castleInt) - castleInt->close(); - - castleInt = nullptr; - - // remove all pending dialogs that do not expect query answer - vstd::erase_if(dialogs, [](const std::shared_ptr & window){ - return window->ID == QueryID::NONE; - }); + closeAllDialogs(); } } @@ -286,6 +285,7 @@ void CPlayerInterface::gamePause(bool pause) void CPlayerInterface::yourTurn(QueryID queryID) { + closeAllDialogs(); CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); EVENT_HANDLER_CALLED_BY_CLIENT; @@ -1477,7 +1477,7 @@ void CPlayerInterface::update() return; //if there are any waiting dialogs, show them - if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->isBusy()) + if (makingTurn && !dialogs.empty() && !showingDialog->isBusy()) { showingDialog->setBusy(); GH.windows().pushWindow(dialogs.front()); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 3fce1bd58..c5d682621 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -197,6 +197,7 @@ public: // public interface for use by client via LOCPLINT access void performAutosave(); void gamePause(bool pause); void endNetwork(); + void closeAllDialogs(); ///returns true if all events are processed internally bool capturedAllEvents(); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 58b94a8be..3e1553df3 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -35,6 +35,7 @@ #include "../lib/TurnTimerInfo.h" #include "../lib/VCMIDirs.h" #include "../lib/campaign/CampaignState.h" +#include "../lib/gameState/HighScore.h" #include "../lib/CPlayerState.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapObjects/CGTownInstance.h" @@ -672,39 +673,9 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta setState(EClientState::GAMEPLAY); } -HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory) -{ - const auto * gs = client->gameState(); - const auto * playerState = gs->getPlayerState(player); - - HighScoreParameter param; - param.difficulty = gs->getStartInfo()->difficulty; - param.day = gs->getDate(); - param.townAmount = gs->howManyTowns(player); - param.usedCheat = gs->getPlayerState(player)->cheated; - param.hasGrail = false; - for(const CGHeroInstance * h : playerState->heroes) - if(h->hasArt(ArtifactID::GRAIL)) - param.hasGrail = true; - for(const CGTownInstance * t : playerState->towns) - if(t->builtBuildings.count(BuildingID::GRAIL)) - param.hasGrail = true; - param.allEnemiesDefeated = true; - for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer) - { - auto ps = gs->getPlayerState(otherPlayer, false); - if(ps && otherPlayer != player && !ps->checkVanquished()) - param.allEnemiesDefeated = false; - } - param.scenarioName = gs->getMapHeader()->name.toString(); - param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name; - - return param; -} - void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory) { - HighScoreParameter param = prepareHighScores(player, victory); + HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory); if(victory && client->gameState()->getStartInfo()->campState) { diff --git a/client/CServerHandler.h b/client/CServerHandler.h index e96496af6..ace71bd68 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -40,8 +40,6 @@ class GlobalLobbyClient; class GameChatHandler; class IServerRunner; -class HighScoreCalculation; - enum class ESelectionScreen : ui8; enum class ELoadMode : ui8; @@ -128,8 +126,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor bool isServerLocal() const; - HighScoreParameter prepareHighScores(PlayerColor player, bool victory); - public: /// High-level connection overlay that is capable of (de)serializing network data std::shared_ptr logicConnection; diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index ef0a1be11..647ca18f7 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -33,6 +33,7 @@ #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" +#include "../render/IScreenHandler.h" #include "../CMT.h" #include "../PlayerLocalState.h" #include "../CPlayerInterface.h" @@ -232,7 +233,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) bool cursorInScrollArea = scrollDelta != Point(0,0); bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; - bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); + bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool() || !GH.screenHandler().hasFocus(); if (!scrollingWasActive && scrollingBlocked) { @@ -375,7 +376,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma mapAudio->onEnemyTurnStarted(); widget->getMinimap()->setAIRadar(!isHuman); widget->getInfoBar()->startEnemyTurn(playerID); - setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); + setState(isHuman ? EAdventureState::MAKING_TURN : EAdventureState::AI_PLAYER_TURN); } void AdventureMapInterface::setState(EAdventureState state) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index d0e28ba19..2de96b126 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -532,7 +532,7 @@ bool AdventureMapShortcuts::optionCanVisitObject() auto * hero = LOCPLINT->localState->getCurrentHero(); auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos()); - assert(vstd::contains(objects,hero)); + //assert(vstd::contains(objects,hero)); return objects.size() > 1; // there is object other than our hero } @@ -577,16 +577,15 @@ bool AdventureMapShortcuts::optionInWorldView() bool AdventureMapShortcuts::optionSidePanelActive() { -return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; +return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW; } bool AdventureMapShortcuts::optionMapScrollingActive() { - return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; + return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW; } bool AdventureMapShortcuts::optionMapViewActive() { - return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL - || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; + return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL; } diff --git a/client/adventureMap/AdventureState.h b/client/adventureMap/AdventureState.h index 32bfc2e2d..6fb2ec6e3 100644 --- a/client/adventureMap/AdventureState.h +++ b/client/adventureMap/AdventureState.h @@ -15,7 +15,6 @@ enum class EAdventureState HOTSEAT_WAIT, MAKING_TURN, AI_PLAYER_TURN, - OTHER_HUMAN_PLAYER_TURN, CASTING_SPELL, WORLD_VIEW }; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 2a5fceb34..b182cb400 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -499,9 +499,12 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { + const auto * attacker = owner.stacksController->getActiveStack(); BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0; DamageEstimation retaliation; - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation); + BattleAttackInfo attackInfo(attacker, targetStack, distance, false ); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount(); @@ -514,7 +517,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle const auto * shooter = owner.stacksController->getActiveStack(); DamageEstimation retaliation; - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation); + BattleAttackInfo attackInfo(shooter, targetStack, 0, true ); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index d10956326..4d43dd452 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -433,6 +433,50 @@ QuickSpellPanel::QuickSpellPanel(BattleInterface & owner) create(); } +std::vector> QuickSpellPanel::getSpells() +{ + std::vector spellIds; + std::vector spellIdsFromSetting; + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + { + std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String(); + SpellID id; + try + { + id = SpellID::decode(spellIdentifier); + } + catch(const IdentifierResolutionException& e) + { + id = SpellID::NONE; + } + spellIds.push_back(id); + spellIdsFromSetting.push_back(id != SpellID::NONE); + } + + // autofill empty slots with spells if possible + auto hero = owner.getBattle()->battleGetMyHero(); + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + { + if(spellIds[i] != SpellID::NONE) + continue; + + for(const auto & availableSpellID : CGI->spellh->getDefaultAllowed()) + { + const auto * availableSpell = availableSpellID.toSpell(); + if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId())) + { + spellIds[i] = availableSpell->getId(); + break; + } + } + } + + std::vector> ret; + for(int i = 0; i < QUICKSPELL_SLOTS; i++) + ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i])); + return ret; +} + void QuickSpellPanel::create() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -447,18 +491,11 @@ void QuickSpellPanel::create() if(!hero) return; - for(int i = 0; i < 12; i++) { - std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String(); - + auto spells = getSpells(); + for(int i = 0; i < QUICKSPELL_SLOTS; i++) { SpellID id; - try - { - id = SpellID::decode(spellIdentifier); - } - catch(const IdentifierResolutionException& e) - { - id = SpellID::NONE; - } + bool fromSettings; + std::tie(id, fromSettings) = spells[i]; auto button = std::make_shared(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){ if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero)) @@ -466,16 +503,19 @@ void QuickSpellPanel::create() owner.castThisSpell(id); } }); - button->setOverlay(std::make_shared(AnimationPath::builtin("spellint"), !spellIdentifier.empty() ? id.num + 1 : 0)); + button->setOverlay(std::make_shared(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0)); button->addPopupCallback([this, i, hero](){ GH.input().hapticFeedback(); GH.windows().createAndPushWindow(hero, owner.curInt.get(), true, [this, i](SpellID spell){ Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)]; - configID->String() = spell.toSpell()->identifier; + configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier; create(); }); }); + if(fromSettings) + buttonsIsAutoGenerated.push_back(std::make_shared(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE)); + if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero)) { buttonsDisabled.push_back(std::make_shared(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172))); diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index a1a16121a..2af8c6bbd 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -155,17 +155,22 @@ private: std::shared_ptr background; std::shared_ptr rect; std::vector> buttons; + std::vector> buttonsIsAutoGenerated; std::vector> buttonsDisabled; std::vector> labels; BattleInterface & owner; public: + int QUICKSPELL_SLOTS = 12; + bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag QuickSpellPanel(BattleInterface & owner); void create(); + std::vector> getSpells(); + void show(Canvas & to) override; void inputModeChanged(InputMode modi) override; }; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 9bc8806cd..cba351e3a 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -218,7 +218,9 @@ void BattleWindow::showStickyQuickSpellWindow() Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"]; showStickyQuickSpellWindow->Bool() = true; - if(GH.screenDimensions().x >= 1050 && owner.getBattle()->battleGetMyHero()->hasSpellbook()) + auto hero = owner.getBattle()->battleGetMyHero(); + + if(GH.screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook()) { quickSpellWindow->enable(); quickSpellWindow->isEnabled = true; @@ -273,16 +275,13 @@ std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & void BattleWindow::useSpellIfPossible(int slot) { - std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(slot)].String(); SpellID id; - try - { - id = SpellID::decode(spellIdentifier); - } - catch(const IdentifierResolutionException& e) - { + bool fromSettings; + std::tie(id, fromSettings) = quickSpellWindow->getSpells()[slot]; + + if(id == SpellID::NONE) return; - } + if(id.hasValue() && owner.getBattle()->battleGetMyHero() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, owner.getBattle()->battleGetMyHero())) { owner.castThisSpell(id); diff --git a/client/globalLobby/GlobalLobbyInviteWindow.cpp b/client/globalLobby/GlobalLobbyInviteWindow.cpp index f282f9902..d954f3268 100644 --- a/client/globalLobby/GlobalLobbyInviteWindow.cpp +++ b/client/globalLobby/GlobalLobbyInviteWindow.cpp @@ -94,7 +94,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow() }; listBackground = std::make_shared(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1); - accountList = std::make_shared(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320)); + accountList = std::make_shared(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, CSH->getGlobalLobby().getActiveAccounts().size(), 0, 1 | 4, Rect(200, 0, 320, 320)); buttonClose = std::make_shared(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN ); diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp index 6e866443c..ce32268e3 100644 --- a/client/globalLobby/GlobalLobbyLoginWindow.cpp +++ b/client/globalLobby/GlobalLobbyLoginWindow.cpp @@ -43,7 +43,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title")); labelUsernameTitle = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username")); - labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString()); + labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265); backgroundUsername = std::make_shared(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); inputUsername = std::make_shared(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true); buttonLogin = std::make_shared(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT); diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index 08d4bb475..cfeb07fae 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -37,7 +37,7 @@ GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount pos.w = 130; pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } @@ -56,9 +56,14 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName, 190); labelVersion = std::make_shared(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version); - labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); + auto statusColor = Colors::RED; + if(modInfo.status == ModVerificationStatus::FULL_MATCH) + statusColor = ColorRGBA(128, 128, 128); + else if(modInfo.status == ModVerificationStatus::VERSION_MISMATCH) + statusColor = Colors::YELLOW; + labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, statusColor, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); } static std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector & modVerificationList) @@ -134,6 +139,13 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s modVerificationList.push_back(modInfo); } + std::sort(modVerificationList.begin(), modVerificationList.end(), [](const GlobalLobbyRoomModInfo &a, const GlobalLobbyRoomModInfo &b) + { + if(a.status == b.status) + return a.modName < b.modName; + + return a.status < b.status; + }); MetaString subtitleText; subtitleText.appendTextID("vcmi.lobby.preview.subtitle"); @@ -142,7 +154,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString()); - labelSubtitle = std::make_shared( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString()); + labelSubtitle = std::make_shared( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400); labelVersionTitle = std::make_shared( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString()); labelVersionValue = std::make_shared( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion); diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index a8a57ac7f..75f8f789b 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -207,7 +207,7 @@ GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const : GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } @@ -238,8 +238,8 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa else backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName); - labelDescription = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName, 180); + labelDescription = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description, 160); labelRoomSize = std::make_shared(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString()); labelRoomStatus = std::make_shared(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString()); iconRoomSize = std::make_shared(ImagePath::builtin("lobby/iconPlayer"), Point(214, 5)); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 675130e46..f5dd29eab 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -19,6 +19,7 @@ #include "../eventsSDL/InputHandler.h" #include "../CGameInfo.h" +#include "../adventureMap/AdventureMapInterface.h" #include "../render/Colors.h" #include "../render/Graphics.h" #include "../render/IFont.h" @@ -145,7 +146,13 @@ CGuiHandler::CGuiHandler() { } -CGuiHandler::~CGuiHandler() = default; +CGuiHandler::~CGuiHandler() +{ + // enforce deletion order on shutdown + // all UI elements including adventure map must be destroyed before Gui Handler + // proper solution would be removal of adventureInt global + adventureInt.reset(); +} ShortcutHandler & CGuiHandler::shortcuts() { diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 3f136151e..cd768b033 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -258,6 +258,15 @@ void CIntObject::redraw() } } +void CIntObject::moveChildForeground(const CIntObject * childToMove) +{ + for(auto child = children.begin(); child != children.end(); child++) + if(*child == childToMove && child != children.end()) + { + std::rotate(child, child + 1, children.end()); + } +} + bool CIntObject::receiveEvent(const Point & position, int eventType) const { return pos.isInside(position); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index a2afaea54..3a8760c4c 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -102,6 +102,8 @@ public: void showAll(Canvas & to) override; //request complete redraw of this object void redraw() override; + // Move child object to foreground + void moveChildForeground(const CIntObject * childToMove); /// returns true if this element is a popup window /// called only for windows diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 7b1db756f..c290cac81 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -345,7 +345,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildLabel(const JsonNode & auto color = readColor(config["color"]); auto text = readText(config["text"]); auto position = readPosition(config["position"]); - return std::make_shared(position.x, position.y, font, alignment, color, text); + auto maxWidth = config["maxWidth"].Integer(); + return std::make_shared(position.x, position.y, font, alignment, color, text, maxWidth); } std::shared_ptr InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 8b924f23c..ad148b7a2 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -340,10 +340,10 @@ void OptionsTabBase::recreate(bool campaign) //Simultaneous turns if(auto turnSlider = widget("simturnsDurationMin")) - turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns, false); if(auto turnSlider = widget("simturnsDurationMax")) - turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns, false); if(auto w = widget("labelSimturnsDurationValueMin")) w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); @@ -388,7 +388,7 @@ void OptionsTabBase::recreate(bool campaign) auto & tpreset = variables["timerPresets"].Vector()[idx]; if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) { - turnSlider->scrollTo(idx); + turnSlider->scrollTo(idx, false); if(auto w = widget("labelTurnDurationValue")) w->setText(CGI->generaltexth->turnDurations[idx]); } diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index fdf59d2a9..141ed7416 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -34,74 +34,6 @@ #include "../../lib/constants/EntityIdentifiers.h" #include "../../lib/gameState/HighScore.h" -auto HighScoreCalculation::calculate() -{ - struct Result - { - int basic = 0; - int total = 0; - int sumDays = 0; - bool cheater = false; - }; - - Result firstResult; - Result summary; - const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; - for(auto & param : parameters) - { - double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); - firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; - summary.basic += firstResult.basic * 5.0 / parameters.size(); - summary.total += firstResult.total * 5.0 / parameters.size(); - summary.sumDays += firstResult.sumDays; - summary.cheater |= firstResult.cheater; - } - - if(parameters.size() == 1) - return firstResult; - - return summary; -} - -struct HighScoreCreature -{ - CreatureID creature; - int min; - int max; -}; - -static std::vector getHighscoreCreaturesList() -{ - JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); - - std::vector ret; - - for(auto & json : configCreatures["creatures"].Vector()) - { - HighScoreCreature entry; - entry.creature = CreatureID::decode(json["creature"].String()); - entry.max = json["max"].isNull() ? std::numeric_limits::max() : json["max"].Integer(); - entry.min = json["min"].isNull() ? std::numeric_limits::min() : json["min"].Integer(); - - ret.push_back(entry); - } - - return ret; -} - -CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) -{ - static const std::vector creatures = getHighscoreCreaturesList(); - - int divide = campaign ? 5 : 1; - - for(auto & creature : creatures) - if(points / divide <= creature.max && points / divide >= creature.min) - return creature.creature; - - throw std::runtime_error("Unable to find creature for score " + std::to_string(points)); -} - CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) { diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 3a963a88e..93f79e42d 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -21,16 +21,6 @@ class CFilledTexture; class TransparentFilledRectangle; -class HighScoreCalculation -{ -public: - std::vector parameters; - bool isCampaign = false; - - auto calculate(); - static CreatureID getCreatureForPoints(int points, bool campaign); -}; - class CHighScoreScreen : public CWindowObject { public: diff --git a/client/widgets/CArtPlace.cpp b/client/widgets/CArtPlace.cpp index d4d57d785..bd0a39cc3 100644 --- a/client/widgets/CArtPlace.cpp +++ b/client/widgets/CArtPlace.cpp @@ -90,6 +90,7 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); image->disable(); + moveSelectionForeground(); } const CArtifactInstance * CArtPlace::getArt() const diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 7c28c8828..2bd0e2d77 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -293,7 +293,10 @@ std::string CComponent::getSubtitle() const return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); case ComponentType::SPELL_SCROLL: case ComponentType::SPELL: - return CGI->spells()->getById(data.subType.as())->getNameTranslated(); + if (data.value < 0) + return "{#A9A9A9|" + CGI->spells()->getById(data.subType.as())->getNameTranslated() + "}"; + else + return CGI->spells()->getById(data.subType.as())->getNameTranslated(); case ComponentType::NONE: case ComponentType::MORALE: case ComponentType::LUCK: diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index fcaefaa86..403dba07d 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -714,3 +714,8 @@ void SelectableSlot::setSelectionWidth(int width) selection = std::make_shared( selection->pos - pos.topLeft(), Colors::TRANSPARENCY, Colors::YELLOW, width); selectSlot(selected); } + +void SelectableSlot::moveSelectionForeground() +{ + moveChildForeground(selection.get()); +} diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 36a741755..926d54f32 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -261,4 +261,5 @@ public: void selectSlot(bool on); bool isSelected() const; void setSelectionWidth(int width); + void moveSelectionForeground(); }; diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 2c866ff47..e51896cdc 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -70,7 +70,7 @@ int CSlider::getValue() const return value; } -void CSlider::setValue(int to) +void CSlider::setValue(int to, bool callCallbacks) { scrollTo(value); } @@ -113,7 +113,7 @@ void CSlider::updateSliderPos() } } -void CSlider::scrollTo(int to) +void CSlider::scrollTo(int to, bool callCallbacks) { vstd::amax(to, 0); vstd::amin(to, positions); @@ -125,7 +125,8 @@ void CSlider::scrollTo(int to) updateSliderPos(); - moved(getValue()); + if (callCallbacks) + moved(getValue()); } void CSlider::clickPressed(const Point & cursorPosition) @@ -321,7 +322,7 @@ int SliderNonlinear::getValue() const return scaledValues.at(CSlider::getValue()); } -void SliderNonlinear::setValue(int to) +void SliderNonlinear::setValue(int to, bool callCallbacks) { size_t nearest = 0; @@ -334,5 +335,5 @@ void SliderNonlinear::setValue(int to) nearest = i; } - scrollTo(nearest); + scrollTo(nearest, callCallbacks); } diff --git a/client/widgets/Slider.h b/client/widgets/Slider.h index 88a580187..6cbba6823 100644 --- a/client/widgets/Slider.h +++ b/client/widgets/Slider.h @@ -52,14 +52,14 @@ public: void clearScrollBounds(); /// Value modifiers - void scrollTo(int value); + void scrollTo(int value, bool callCallbacks = true); void scrollBy(int amount) override; void scrollToMin(); void scrollToMax(); /// Amount modifier void setAmount(int to); - virtual void setValue(int to); + virtual void setValue(int to, bool callCallbacks = true); /// Accessors int getAmount() const; @@ -95,7 +95,7 @@ class SliderNonlinear : public CSlider using CSlider::setAmount; // make private public: - void setValue(int to) override; + void setValue(int to, bool callCallbacks) override; int getValue() const override; SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 6c562e191..ad2f4e4bb 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -29,6 +29,7 @@ #include "../widgets/CComponent.h" #include "../widgets/CTextInput.h" #include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" #include "../adventureMap/AdventureMapInterface.h" #include "../render/IRenderHandler.h" #include "../render/IImage.h" @@ -130,9 +131,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); + Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16); if(settings["general"]["enableUiEnhancements"].Bool()) { - Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16); const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); const ColorRGBA borderColor = ColorRGBA(128, 100, 75); const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); @@ -143,6 +144,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m searchBox->setCallback(std::bind(&CSpellWindow::searchInput, this)); } + if(onSpellSelect) + { + Point boxPos = r.bottomLeft() + Point(-2, 5); + showAllSpells = std::make_shared(boxPos, AnimationPath::builtin("sysopchk.def"), CButton::tooltip(CGI->generaltexth->translate("core.help.458.hover"), CGI->generaltexth->translate("core.help.458.hover")), [this](bool state){ searchInput(); }); + showAllSpellsDescription = std::make_shared(boxPos.x + 40, boxPos.y + 12, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, CGI->generaltexth->translate("core.help.458.hover")); + } + processSpells(); //numbers of spell pages computed @@ -288,7 +296,7 @@ void CSpellWindow::processSpells() if(onSpellSelect) { - if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound) + if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound && (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get()))) mySpells.push_back(spell.get()); continue; } @@ -359,6 +367,9 @@ void CSpellWindow::fexitb() (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage; + if(onSpellSelect) + onSpellSelect(SpellID::NONE); + close(); } @@ -605,7 +616,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) if(owner->onSpellSelect) { owner->onSpellSelect(mySpell->id); - owner->fexitb(); + owner->close(); return; } diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 73dee4e4c..3119860ca 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -27,6 +27,7 @@ class CPlayerInterface; class CSpellWindow; class CTextInput; class TransparentFilledRectangle; +class CToggleButton; /// The spell window class CSpellWindow : public CWindowObject @@ -82,6 +83,9 @@ class CSpellWindow : public CWindowObject std::shared_ptr searchBoxRectangle; std::shared_ptr searchBoxDescription; + std::shared_ptr showAllSpells; + std::shared_ptr showAllSpellsDescription; + bool isBigSpellbook; int spellsPerPage; int offL; diff --git a/config/objects/moddables.json b/config/objects/moddables.json index b97606eb9..422fe3532 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -155,18 +155,21 @@ "types" : { "boatNecropolis" : { "index" : 0, + "compatibilityIdentifiers" : [ "evil" ], "actualAnimation" : "AB01_.def", "overlayAnimation" : "ABM01_.def", "flagAnimations" : ["ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K"] }, "boatCastle" : { "index" : 1, + "compatibilityIdentifiers" : [ "good" ], "actualAnimation" : "AB02_.def", "overlayAnimation" : "ABM02_.def", "flagAnimations" : ["ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K"] }, "boatFortress" : { "index" : 2, + "compatibilityIdentifiers" : [ "neutral" ], "actualAnimation" : "AB03_.def", "overlayAnimation" : "ABM03_.def", "flagAnimations" : ["ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K"] diff --git a/config/schemas/template.json b/config/schemas/template.json index a1a353a6d..564210632 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -99,7 +99,7 @@ "type": { "type" : "string", - "enum" : ["wide", "fictive", "repulsive"] + "enum" : ["wide", "fictive", "repulsive", "forcePortal"] } } }, diff --git a/config/widgets/lobbyWindow.json b/config/widgets/lobbyWindow.json index 529d26ce5..118a60317 100644 --- a/config/widgets/lobbyWindow.json +++ b/config/widgets/lobbyWindow.json @@ -43,7 +43,8 @@ { "name" : "accountNameLabel", "type": "labelTitleMain", - "position": {"x": 15, "y": 10} + "position": {"x": 15, "y": 10}, + "maxWidth": 230 }, { diff --git a/debian/changelog b/debian/changelog index 5998a8c0a..8e7da0a56 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,12 @@ vcmi (1.6.0) jammy; urgency=medium -- Ivan Savenko Fri, 30 Aug 2024 12:00:00 +0200 +vcmi (1.5.6) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Sun, 4 Aug 2024 12:00:00 +0200 + vcmi (1.5.5) jammy; urgency=medium * New upstream release diff --git a/docs/Readme.md b/docs/Readme.md index 8ceb7a55d..074f07bf6 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,7 +1,7 @@ [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.4) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.5) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) # VCMI Project diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index 7a1ea901a..883b249b1 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -463,7 +463,9 @@ Configurable object has following structure: `"text"`: [text](#text), -`"position"`: [position](#position) +`"position"`: [position](#position), + +`"maxWidth"`: int` optional, trim longer text #### [VCMI-1.4] Multi-line label diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index c133b80d8..bf43cb106 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -38,7 +38,7 @@ { "a" : "zoneA", "b" : "zoneB", "guard" : 5000, "road" : "false" }, { "a" : "zoneA", "b" : "zoneC", "guard" : 5000, "road" : "random" }, { "a" : "zoneB", "b" : "zoneC", "type" : "wide" } - //"type" can be "guarded" (default), "wide", "fictive" or "repulsive" + //"type" can be "guarded" (default), "wide", "fictive", "repulsive" or "forcePortal" //"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual - //they do not create actual path, but only attract or repulse zones, respectively ] diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 4ab10bb28..effc34c70 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -91,6 +91,7 @@ vcmilauncher.desktop + diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 05ea6f428..8f1b0096e 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -13,6 +13,8 @@ #include "CThreadHelper.h" +#include + VCMI_LIB_NAMESPACE_BEGIN std::mutex CConsoleHandler::smx; @@ -142,6 +144,30 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo) MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); } +LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) +{ + logGlobal->error("Disaster happened."); + + PEXCEPTION_RECORD einfo = exception->ExceptionRecord; + logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); + + if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) + { + logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); + } + const DWORD threadId = ::GetCurrentThreadId(); + logGlobal->error("Thread ID: %d", threadId); + + //exception info to be placed in the dump + MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; + + createMemoryDump(&meinfo); + + return EXCEPTION_EXECUTE_HANDLER; +} + +#endif + [[noreturn]] static void onTerminate() { logGlobal->error("Disaster happened."); @@ -166,37 +192,20 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo) logGlobal->error("Reason: unknown exception!"); } + logGlobal->error("Call stack information:"); + std::stringstream stream; + stream << boost::stacktrace::stacktrace(); + logGlobal->error("%s", stream.str()); + +#ifdef VCMI_WINDOWS const DWORD threadId = ::GetCurrentThreadId(); logGlobal->error("Thread ID: %d", threadId); createMemoryDump(nullptr); +#endif std::abort(); } -LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) -{ - logGlobal->error("Disaster happened."); - - PEXCEPTION_RECORD einfo = exception->ExceptionRecord; - logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); - - if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) - { - logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); - } - const DWORD threadId = ::GetCurrentThreadId(); - logGlobal->error("Thread ID: %d", threadId); - - //exception info to be placed in the dump - MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; - - createMemoryDump(&meinfo); - - return EXCEPTION_EXECUTE_HANDLER; -} -#endif - - void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) { TColor colorCode; @@ -289,11 +298,14 @@ CConsoleHandler::CConsoleHandler(): defErrColor = csbi.wAttributes; #ifndef _DEBUG SetUnhandledExceptionFilter(onUnhandledException); - std::set_terminate(onTerminate); #endif #else defColor = "\x1b[0m"; #endif + +#ifndef _DEBUG + std::set_terminate(onTerminate); +#endif } CConsoleHandler::~CConsoleHandler() { diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 6c462a53b..e598a4389 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -99,9 +99,11 @@ set(lib_MAIN_SRCS gameState/CGameState.cpp gameState/CGameStateCampaign.cpp + gameState/HighScore.cpp gameState/InfoAboutArmy.cpp gameState/RumorState.cpp gameState/TavernHeroesPool.cpp + gameState/GameStatistics.cpp mapObjectConstructors/AObjectTypeHandler.cpp mapObjectConstructors/CBankInstanceConstructor.cpp @@ -468,6 +470,7 @@ set(lib_MAIN_HEADERS gameState/RumorState.h gameState/SThievesGuildInfo.h gameState/TavernHeroesPool.h + gameState/GameStatistics.h gameState/TavernSlot.h gameState/QuestInfo.h diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 8e046a81d..2c246e7c8 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -755,15 +755,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * { RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int getMovementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0; - return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg); + int movementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0; + return battleEstimateDamage(attacker, defender, movementRange, retaliationDmg); } -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementRange, DamageEstimation * retaliationDmg) const { RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting); + const BattleAttackInfo bai(attacker, defender, movementRange, shooting); return battleEstimateDamage(bai, retaliationDmg); } diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 99bce45e6..0bcc88ab0 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -145,7 +145,8 @@ int DamageCalculator::getActorAttackIgnored() const if(multAttackReductionPercent > 0) { - int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + int reduction = vstd::divideAndRound( getActorAttackBase() * multAttackReductionPercent, 100); return -std::min(reduction, getActorAttackBase()); } return 0; diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 5e3002391..e1b9f83b2 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -32,18 +32,14 @@ public: int32_t getNum() const { int32_t result; - std::visit([&result] (const auto& v) { result = v.getNum(); }, value); - return result; } std::string toString() const { std::string result; - std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value); - return result; } @@ -58,6 +54,13 @@ public: return IdentifierType(); } + bool hasValue() const + { + bool result = false; + std::visit([&result] (const auto& v) { result = v.hasValue(); }, value); + return result; + } + template void serialize(Handler &h) { h & value; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7b0599d9b..696d51308 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -875,7 +875,7 @@ void CGameState::initTowns() } //init spells vti->spells.resize(GameConstants::SPELL_LEVELS); - + vti->possibleSpells -= SpellID::PRESET; for(ui32 z=0; zobligatorySpells.size();z++) { const auto * s = vti->obligatorySpells[z].toSpell(); @@ -1538,137 +1538,6 @@ bool CGameState::checkForStandardLoss(const PlayerColor & player) const return pState.checkVanquished(); } -struct statsHLP -{ - using TStat = std::pair; - //converts [] to vec[place] -> platers - static std::vector< std::vector< PlayerColor > > getRank( std::vector stats ) - { - std::sort(stats.begin(), stats.end(), statsHLP()); - - //put first element - std::vector< std::vector > ret; - std::vector tmp; - tmp.push_back( stats[0].first ); - ret.push_back( tmp ); - - //the rest of elements - for(int g=1; gpush_back( stats[g].first ); - } - else - { - //create next occupied rank - std::vector tmp; - tmp.push_back(stats[g].first); - ret.push_back(tmp); - } - } - - return ret; - } - - bool operator()(const TStat & a, const TStat & b) const - { - return a.second > b.second; - } - - static const CGHeroInstance * findBestHero(CGameState * gs, const PlayerColor & color) - { - std::vector > &h = gs->players[color].heroes; - if(h.empty()) - return nullptr; - //best hero will be that with highest exp - int best = 0; - for(int b=1; bexp > h[best]->exp) - { - best = b; - } - } - return h[best]; - } - - //calculates total number of artifacts that belong to given player - static int getNumberOfArts(const PlayerState * ps) - { - int ret = 0; - for(auto h : ps->heroes) - { - ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size(); - } - return ret; - } - - // get total strength of player army - static si64 getArmyStrength(const PlayerState * ps) - { - si64 str = 0; - - for(auto h : ps->heroes) - { - if(!h->inTownGarrison) //original h3 behavior - str += h->getArmyStrength(); - } - return str; - } - - // get total gold income - static int getIncome(const PlayerState * ps, int percentIncome) - { - int totalIncome = 0; - const CGObjectInstance * heroOrTown = nullptr; - - //Heroes can produce gold as well - skill, specialty or arts - for(const auto & h : ps->heroes) - { - totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100; - - if(!heroOrTown) - heroOrTown = h; - } - - //Add town income of all towns - for(const auto & t : ps->towns) - { - totalIncome += t->dailyIncome()[EGameResID::GOLD]; - - if(!heroOrTown) - heroOrTown = t; - } - - /// FIXME: Dirty dirty hack - /// Stats helper need some access to gamestate. - std::vector ownedObjects; - for(const CGObjectInstance * obj : heroOrTown->cb->gameState()->map->objects) - { - if(obj && obj->tempOwner == ps->color) - ownedObjects.push_back(obj); - } - /// This is code from CPlayerSpecificInfoCallback::getMyObjects - /// I'm really need to find out about callback interface design... - - for(const auto * object : ownedObjects) - { - //Mines - if ( object->ID == Obj::MINE ) - { - const auto * mine = dynamic_cast(object); - assert(mine); - - if (mine->producedResource == EGameResID::GOLD) - totalIncome += mine->getProducedQuantity(); - } - } - - return totalIncome; - } -}; - void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { auto playerInactive = [&](const PlayerColor & color) @@ -1688,7 +1557,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) stat.second = VAL_GETTER; \ stats.push_back(stat); \ } \ - tgi.FIELD = statsHLP::getRank(stats); \ + tgi.FIELD = Statistic::getRank(stats); \ } for(auto & elem : players) @@ -1710,7 +1579,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { if(playerInactive(player.second.color)) continue; - const CGHeroInstance * best = statsHLP::findBestHero(this, player.second.color); + const CGHeroInstance * best = Statistic::findBestHero(this, player.second.color); InfoAboutHero iah; iah.initFromHero(best, (level >= 2) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC); iah.army.clear(); @@ -1731,27 +1600,19 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) } if(level >= 3) //obelisks found { - auto getObeliskVisited = [&](const TeamID & t) - { - if(map->obelisksVisited.count(t)) - return map->obelisksVisited[t]; - else - return ui8(0); - }; - - FILL_FIELD(obelisks, getObeliskVisited(gs->getPlayerTeam(g->second.color)->id)) + FILL_FIELD(obelisks, Statistic::getObeliskVisited(gs, gs->getPlayerTeam(g->second.color)->id)) } if(level >= 4) //artifacts { - FILL_FIELD(artifacts, statsHLP::getNumberOfArts(&g->second)) + FILL_FIELD(artifacts, Statistic::getNumberOfArts(&g->second)) } if(level >= 4) //army strength { - FILL_FIELD(army, statsHLP::getArmyStrength(&g->second)) + FILL_FIELD(army, Statistic::getArmyStrength(&g->second)) } if(level >= 5) //income { - FILL_FIELD(income, statsHLP::getIncome(&g->second, scenarioOps->getIthPlayersSettings(g->second.color).handicap.percentIncome)) + FILL_FIELD(income, Statistic::getIncome(gs, &g->second)) } if(level >= 2) //best hero's stats { diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index f7814fde9..c63f5cc52 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -15,6 +15,7 @@ #include "../ConstTransitivePtr.h" #include "RumorState.h" +#include "GameStatistics.h" namespace boost { @@ -90,6 +91,8 @@ public: CBonusSystemNode globalEffects; RumorState currentRumor; + StatisticDataSet statistic; + static boost::shared_mutex mutex; void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; @@ -167,6 +170,8 @@ public: h & currentRumor; h & campaign; h & allocatedArtifacts; + if (h.version >= Handler::Version::STATISTICS) + h & statistic; BONUS_TREE_DESERIALIZATION_FIX } diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 241324b8a..b5a30ceff 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -133,13 +133,15 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr if(!art) return false; - bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); + ArtifactLocation al(hero.hero->id, artifactPosition); - if (takeable) + bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); + bool locked = hero.hero->getSlot(al.slot)->locked; + + if (!locked && takeable) hero.transferrableArtifacts.push_back(artifactPosition); - ArtifactLocation al(hero.hero->id, artifactPosition); - if(!takeable && !hero.hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719 + if (!locked && !takeable) { hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot); return true; diff --git a/lib/gameState/GameStatistics.cpp b/lib/gameState/GameStatistics.cpp new file mode 100644 index 000000000..5daf0e075 --- /dev/null +++ b/lib/gameState/GameStatistics.cpp @@ -0,0 +1,370 @@ +/* + * GameStatistics.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 "GameStatistics.h" +#include "../CPlayerState.h" +#include "../constants/StringConstants.h" +#include "CGameState.h" +#include "TerrainHandler.h" +#include "CHeroHandler.h" +#include "StartInfo.h" +#include "HighScore.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/CGObjectInstance.h" +#include "../mapObjects/MiscObjects.h" +#include "../mapping/CMap.h" +#include "../entities/building/CBuilding.h" + + +VCMI_LIB_NAMESPACE_BEGIN + +void StatisticDataSet::add(StatisticDataSetEntry entry) +{ + data.push_back(entry); +} + +StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs) +{ + StatisticDataSetEntry data; + + HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false); + HighScoreCalculation scenarioHighScores; + scenarioHighScores.parameters.push_back(param); + scenarioHighScores.isCampaign = false; + + data.map = gs->map->name.toString(); + data.timestamp = std::time(0); + data.day = gs->getDate(Date::DAY); + data.player = ps->color; + data.team = ps->team; + data.isHuman = ps->isHuman(); + data.status = ps->status; + data.resources = ps->resources; + data.numberHeroes = ps->heroes.size(); + data.numberTowns = gs->howManyTowns(ps->color); + data.numberArtifacts = Statistic::getNumberOfArts(ps); + data.numberDwellings = gs->getPlayerState(ps->color)->dwellings.size(); + data.armyStrength = Statistic::getArmyStrength(ps, true); + data.totalExperience = Statistic::getTotalExperience(ps); + data.income = Statistic::getIncome(gs, ps); + data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color); + data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team); + data.townBuiltRatio = Statistic::getTownBuiltRatio(ps); + data.hasGrail = param.hasGrail; + data.numMines = Statistic::getNumMines(gs, ps); + data.score = scenarioHighScores.calculate().total; + data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0; + data.numBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesNeutral : 0; + data.numBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesPlayer : 0; + data.numWinBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0; + data.numWinBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0; + data.numHeroSurrendered = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroSurrendered : 0; + data.numHeroEscaped = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroEscaped : 0; + data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources(); + data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources(); + data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources(); + data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0; + + return data; +} + +std::string StatisticDataSet::toCsv() +{ + std::stringstream ss; + + auto resources = std::vector{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; + + ss << "Map" << ";"; + ss << "Timestamp" << ";"; + ss << "Day" << ";"; + ss << "Player" << ";"; + ss << "Team" << ";"; + ss << "IsHuman" << ";"; + ss << "Status" << ";"; + ss << "NumberHeroes" << ";"; + ss << "NumberTowns" << ";"; + ss << "NumberArtifacts" << ";"; + ss << "NumberDwellings" << ";"; + ss << "ArmyStrength" << ";"; + ss << "TotalExperience" << ";"; + ss << "Income" << ";"; + ss << "MapExploredRatio" << ";"; + ss << "ObeliskVisitedRatio" << ";"; + ss << "TownBuiltRatio" << ";"; + ss << "HasGrail" << ";"; + ss << "Score" << ";"; + ss << "MaxHeroLevel" << ";"; + ss << "NumBattlesNeutral" << ";"; + ss << "NumBattlesPlayer" << ";"; + ss << "NumWinBattlesNeutral" << ";"; + ss << "NumWinBattlesPlayer" << ";"; + ss << "NumHeroSurrendered" << ";"; + ss << "NumHeroEscaped" << ";"; + ss << "MovementPointsUsed"; + for(auto & resource : resources) + ss << ";" << GameConstants::RESOURCE_NAMES[resource]; + for(auto & resource : resources) + ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "Mines"; + for(auto & resource : resources) + ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy"; + for(auto & resource : resources) + ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings"; + for(auto & resource : resources) + ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume"; + ss << "\r\n"; + + for(auto & entry : data) + { + ss << entry.map << ";"; + ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << ";"; + ss << entry.day << ";"; + ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << ";"; + ss << entry.team.getNum() << ";"; + ss << entry.isHuman << ";"; + ss << (int)entry.status << ";"; + ss << entry.numberHeroes << ";"; + ss << entry.numberTowns << ";"; + ss << entry.numberArtifacts << ";"; + ss << entry.numberDwellings << ";"; + ss << entry.armyStrength << ";"; + ss << entry.totalExperience << ";"; + ss << entry.income << ";"; + ss << entry.mapExploredRatio << ";"; + ss << entry.obeliskVisitedRatio << ";"; + ss << entry.townBuiltRatio << ";"; + ss << entry.hasGrail << ";"; + ss << entry.score << ";"; + ss << entry.maxHeroLevel << ";"; + ss << entry.numBattlesNeutral << ";"; + ss << entry.numBattlesPlayer << ";"; + ss << entry.numWinBattlesNeutral << ";"; + ss << entry.numWinBattlesPlayer << ";"; + ss << entry.numHeroSurrendered << ";"; + ss << entry.numHeroEscaped << ";"; + ss << entry.movementPointsUsed; + for(auto & resource : resources) + ss << ";" << entry.resources[resource]; + for(auto & resource : resources) + ss << ";" << entry.numMines[resource]; + for(auto & resource : resources) + ss << ";" << entry.spentResourcesForArmy[resource]; + for(auto & resource : resources) + ss << ";" << entry.spentResourcesForBuildings[resource]; + for(auto & resource : resources) + ss << ";" << entry.tradeVolume[resource]; + ss << "\r\n"; + } + + return ss.str(); +} + +std::vector Statistic::getMines(const CGameState * gs, const PlayerState * ps) +{ + std::vector tmp; + + /// FIXME: Dirty dirty hack + /// Stats helper need some access to gamestate. + std::vector ownedObjects; + for(const CGObjectInstance * obj : gs->map->objects) + { + if(obj && obj->tempOwner == ps->color) + ownedObjects.push_back(obj); + } + /// This is code from CPlayerSpecificInfoCallback::getMyObjects + /// I'm really need to find out about callback interface design... + + for(const auto * object : ownedObjects) + { + //Mines + if ( object->ID == Obj::MINE ) + { + const auto * mine = dynamic_cast(object); + assert(mine); + + tmp.push_back(mine); + } + } + + return tmp; +} + +//calculates total number of artifacts that belong to given player +int Statistic::getNumberOfArts(const PlayerState * ps) +{ + int ret = 0; + for(auto h : ps->heroes) + { + ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size(); + } + return ret; +} + +// get total strength of player army +si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison) +{ + si64 str = 0; + + for(auto h : ps->heroes) + { + if(!h->inTownGarrison || withTownGarrison) //original h3 behavior + str += h->getArmyStrength(); + } + return str; +} + +// get total experience of all heroes +si64 Statistic::getTotalExperience(const PlayerState * ps) +{ + si64 tmp = 0; + + for(auto h : ps->heroes) + tmp += h->exp; + + return tmp; +} + +// get total gold income +int Statistic::getIncome(const CGameState * gs, const PlayerState * ps) +{ + int percentIncome = gs->getStartInfo()->getIthPlayersSettings(ps->color).handicap.percentIncome; + int totalIncome = 0; + + //Heroes can produce gold as well - skill, specialty or arts + for(const auto & h : ps->heroes) + totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100; + + //Add town income of all towns + for(const auto & t : ps->towns) + totalIncome += t->dailyIncome()[EGameResID::GOLD]; + + for(const CGMine * mine : getMines(gs, ps)) + if(mine->producedResource == EGameResID::GOLD) + totalIncome += mine->getProducedQuantity(); + + return totalIncome; +} + +float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player) +{ + float visible = 0.0; + float numTiles = 0.0; + + for(int layer = 0; layer < (gs->map->twoLevel ? 2 : 1); layer++) + for(int y = 0; y < gs->map->height; ++y) + for(int x = 0; x < gs->map->width; ++x) + { + TerrainTile tile = gs->map->getTile(int3(x, y, layer)); + + if(tile.blocked && (!tile.visitable)) + continue; + + if(gs->isVisible(int3(x, y, layer), player)) + visible++; + numTiles++; + } + + return visible / numTiles; +} + +const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color) +{ + auto &h = gs->players.at(color).heroes; + if(h.empty()) + return nullptr; + //best hero will be that with highest exp + int best = 0; + for(int b=1; bexp > h[best]->exp) + { + best = b; + } + } + return h[best]; +} + +std::vector> Statistic::getRank(std::vector> stats) +{ + std::sort(stats.begin(), stats.end(), [](const std::pair & a, const std::pair & b) { return a.second > b.second; }); + + //put first element + std::vector< std::vector > ret; + std::vector tmp; + tmp.push_back( stats[0].first ); + ret.push_back( tmp ); + + //the rest of elements + for(int g=1; gpush_back( stats[g].first ); + } + else + { + //create next occupied rank + std::vector tmp; + tmp.push_back(stats[g].first); + ret.push_back(tmp); + } + } + + return ret; +} + +int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t) +{ + if(gs->map->obelisksVisited.count(t)) + return gs->map->obelisksVisited.at(t); + else + return 0; +} + +float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t) +{ + if(!gs->map->obeliskCount) + return 0; + return (float)getObeliskVisited(gs, t) / (float)gs->map->obeliskCount; +} + +std::map Statistic::getNumMines(const CGameState * gs, const PlayerState * ps) +{ + std::map tmp; + + for(auto & res : EGameResID::ALL_RESOURCES()) + tmp[res] = 0; + + for(const CGMine * mine : getMines(gs, ps)) + tmp[mine->producedResource]++; + + return tmp; +} + +float Statistic::getTownBuiltRatio(const PlayerState * ps) +{ + float built = 0.0; + float total = 0.0; + + for(const auto & t : ps->towns) + { + built += t->builtBuildings.size(); + for(const auto & b : t->town->buildings) + if(!t->forbiddenBuildings.count(b.first)) + total += 1; + } + + if(total < 1) + return 0; + + return built / total; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/GameStatistics.h b/lib/gameState/GameStatistics.h new file mode 100644 index 000000000..daec71e4a --- /dev/null +++ b/lib/gameState/GameStatistics.h @@ -0,0 +1,156 @@ +/* + * GameSTatistics.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 "../GameConstants.h" +#include "../ResourceSet.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct PlayerState; +class CGameState; +class CGHeroInstance; +class CGMine; + +struct DLL_LINKAGE StatisticDataSetEntry +{ + std::string map; + time_t timestamp; + int day; + PlayerColor player; + TeamID team; + bool isHuman; + EPlayerStatus status; + TResources resources; + int numberHeroes; + int numberTowns; + int numberArtifacts; + int numberDwellings; + si64 armyStrength; + si64 totalExperience; + int income; + float mapExploredRatio; + float obeliskVisitedRatio; + float townBuiltRatio; + bool hasGrail; + std::map numMines; + int score; + int maxHeroLevel; + int numBattlesNeutral; + int numBattlesPlayer; + int numWinBattlesNeutral; + int numWinBattlesPlayer; + int numHeroSurrendered; + int numHeroEscaped; + TResources spentResourcesForArmy; + TResources spentResourcesForBuildings; + TResources tradeVolume; + si64 movementPointsUsed; + + template void serialize(Handler &h) + { + h & map; + h & timestamp; + h & day; + h & player; + h & team; + h & isHuman; + h & status; + h & resources; + h & numberHeroes; + h & numberTowns; + h & numberArtifacts; + h & numberDwellings; + h & armyStrength; + h & totalExperience; + h & income; + h & mapExploredRatio; + h & obeliskVisitedRatio; + h & townBuiltRatio; + h & hasGrail; + h & numMines; + h & score; + h & maxHeroLevel; + h & numBattlesNeutral; + h & numBattlesPlayer; + h & numWinBattlesNeutral; + h & numWinBattlesPlayer; + h & numHeroSurrendered; + h & numHeroEscaped; + h & spentResourcesForArmy; + h & spentResourcesForBuildings; + h & tradeVolume; + h & movementPointsUsed; + } +}; + +class DLL_LINKAGE StatisticDataSet +{ + std::vector data; + +public: + void add(StatisticDataSetEntry entry); + static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs); + std::string toCsv(); + + struct PlayerAccumulatedValueStorage // holds some actual values needed for stats + { + int numBattlesNeutral; + int numBattlesPlayer; + int numWinBattlesNeutral; + int numWinBattlesPlayer; + int numHeroSurrendered; + int numHeroEscaped; + TResources spentResourcesForArmy; + TResources spentResourcesForBuildings; + TResources tradeVolume; + si64 movementPointsUsed; + + template void serialize(Handler &h) + { + h & numBattlesNeutral; + h & numBattlesPlayer; + h & numWinBattlesNeutral; + h & numWinBattlesPlayer; + h & numHeroSurrendered; + h & numHeroEscaped; + h & spentResourcesForArmy; + h & spentResourcesForBuildings; + h & tradeVolume; + h & movementPointsUsed; + } + }; + std::map accumulatedValues; + + template void serialize(Handler &h) + { + h & data; + h & accumulatedValues; + } +}; + +class DLL_LINKAGE Statistic +{ + static std::vector getMines(const CGameState * gs, const PlayerState * ps); +public: + static int getNumberOfArts(const PlayerState * ps); + static si64 getArmyStrength(const PlayerState * ps, bool withTownGarrison = false); + static si64 getTotalExperience(const PlayerState * ps); + static int getIncome(const CGameState * gs, const PlayerState * ps); + static float getMapExploredRatio(const CGameState * gs, PlayerColor player); + static const CGHeroInstance * findBestHero(const CGameState * gs, const PlayerColor & color); + static std::vector> getRank(std::vector> stats); + static int getObeliskVisited(const CGameState * gs, const TeamID & t); + static float getObeliskVisitedRatio(const CGameState * gs, const TeamID & t); + static std::map getNumMines(const CGameState * gs, const PlayerState * ps); + static float getTownBuiltRatio(const PlayerState * ps); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/HighScore.cpp b/lib/gameState/HighScore.cpp new file mode 100644 index 000000000..51759629e --- /dev/null +++ b/lib/gameState/HighScore.cpp @@ -0,0 +1,111 @@ +/* + * HighScore.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 "HighScore.h" +#include "../CPlayerState.h" +#include "../constants/StringConstants.h" +#include "CGameState.h" +#include "StartInfo.h" +#include "../mapping/CMapHeader.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +HighScoreParameter HighScore::prepareHighScores(const CGameState * gs, PlayerColor player, bool victory) +{ + const auto * playerState = gs->getPlayerState(player); + + HighScoreParameter param; + param.difficulty = gs->getStartInfo()->difficulty; + param.day = gs->getDate(); + param.townAmount = gs->howManyTowns(player); + param.usedCheat = gs->getPlayerState(player)->cheated; + param.hasGrail = false; + for(const CGHeroInstance * h : playerState->heroes) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + for(const CGTownInstance * t : playerState->towns) + if(t->builtBuildings.count(BuildingID::GRAIL)) + param.hasGrail = true; + param.allEnemiesDefeated = true; + for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer) + { + auto ps = gs->getPlayerState(otherPlayer, false); + if(ps && otherPlayer != player && !ps->checkVanquished()) + param.allEnemiesDefeated = false; + } + param.scenarioName = gs->getMapHeader()->name.toString(); + param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name; + + return param; +} + +HighScoreCalculation::Result HighScoreCalculation::calculate() +{ + Result firstResult; + Result summary; + const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; + for(auto & param : parameters) + { + double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); + firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; + summary.basic += firstResult.basic * 5.0 / parameters.size(); + summary.total += firstResult.total * 5.0 / parameters.size(); + summary.sumDays += firstResult.sumDays; + summary.cheater |= firstResult.cheater; + } + + if(parameters.size() == 1) + return firstResult; + + return summary; +} + +struct HighScoreCreature +{ + CreatureID creature; + int min; + int max; +}; + +static std::vector getHighscoreCreaturesList() +{ + JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + + std::vector ret; + + for(auto & json : configCreatures["creatures"].Vector()) + { + HighScoreCreature entry; + entry.creature = CreatureID::decode(json["creature"].String()); + entry.max = json["max"].isNull() ? std::numeric_limits::max() : json["max"].Integer(); + entry.min = json["min"].isNull() ? std::numeric_limits::min() : json["min"].Integer(); + + ret.push_back(entry); + } + + return ret; +} + +CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) +{ + static const std::vector creatures = getHighscoreCreaturesList(); + + int divide = campaign ? 5 : 1; + + for(auto & creature : creatures) + if(points / divide <= creature.max && points / divide >= creature.min) + return creature.creature; + + throw std::runtime_error("Unable to find creature for score " + std::to_string(points)); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/HighScore.h b/lib/gameState/HighScore.h index 74e2dcd76..b031ed1b4 100644 --- a/lib/gameState/HighScore.h +++ b/lib/gameState/HighScore.h @@ -9,8 +9,12 @@ */ #pragma once +#include "../GameConstants.h" + VCMI_LIB_NAMESPACE_BEGIN +class CGameState; + class DLL_LINKAGE HighScoreParameter { public: @@ -37,5 +41,28 @@ public: h & playerName; } }; +class DLL_LINKAGE HighScore +{ +public: + static HighScoreParameter prepareHighScores(const CGameState * gs, PlayerColor player, bool victory); +}; + +class DLL_LINKAGE HighScoreCalculation +{ +public: + struct Result + { + int basic = 0; + int total = 0; + int sumDays = 0; + bool cheater = false; + }; + + std::vector parameters; + bool isCampaign = false; + + Result calculate(); + static CreatureID getCreatureForPoints(int points, bool campaign); +}; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 8ac6fec76..552237825 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -393,7 +393,7 @@ void CBank::battleFinished(const CGHeroInstance *hero, const BattleResult &resul } } -void CBank::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if (answer) { diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index d2fe226f1..b61563421 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -40,7 +40,7 @@ public: bool isCoastVisitable() const override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::vector getPopupComponents(PlayerColor player) const override; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 94ad2ad84..efba48004 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -523,7 +523,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & } } -void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { auto action = takenAction(hero); if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 588e26d0f..2b88d367d 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -48,7 +48,7 @@ public: void pickRandomObject(vstd::RNG & rand) override; void newTurn(vstd::RNG & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; CreatureID getCreature() const; //stack formation depends on position, diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 0b4b6b500..37a717960 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -348,15 +348,19 @@ void CGDwelling::newTurn(vstd::RNG & rand) const std::vector CGDwelling::getPopupComponents(PlayerColor player) const { - if (getOwner() != player) - return {}; + bool visitedByOwner = getOwner() == player; std::vector result; if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty()) { for (auto const & creature : creatures.front().second) - result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + { + if (visitedByOwner) + result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + else + result.emplace_back(ComponentType::CREATURE, creature); + } } if (ID == Obj::CREATURE_GENERATOR4) @@ -364,7 +368,12 @@ std::vector CGDwelling::getPopupComponents(PlayerColor player) const for (auto const & creatureLevel : creatures) { if (!creatureLevel.second.empty()) - result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + { + if (visitedByOwner) + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + else + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back()); + } } } return result; @@ -426,7 +435,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const if(count) //there are available creatures { - if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED)) + if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_MERGE_ON_RECRUIT)) { SlotID testSlot = h->getSlotFor(crid); if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero @@ -507,7 +516,7 @@ void CGDwelling::battleFinished(const CGHeroInstance *hero, const BattleResult & } } -void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { auto relations = cb->getPlayerRelations(getOwner(), hero->getOwner()); if(stacksCount() > 0 && relations == PlayerRelations::ENEMIES) //guards present diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index 4afa3907e..feb4b9bc5 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -54,7 +54,7 @@ private: void newTurn(vstd::RNG & rand) const override; void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::vector getPopupComponents(PlayerColor player) const override; void updateGuards() const; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index cc29e0bfc..a4941ea37 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1704,6 +1704,16 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) setHeroTypeName(typeName); } + if(!handler.saving) + { + if(!appearance) + { + // crossoverDeserialize + type = getHeroType().toHeroType(); + appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); + } + } + CArmedInstance::serializeJsonOptions(handler); { @@ -1719,13 +1729,6 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!handler.saving) { - if(!appearance) - { - // crossoverDeserialize - type = getHeroType().toHeroType(); - appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); - } - patrol.patrolling = (rawPatrolRadius > NO_PATROLING); patrol.initialPos = visitablePos(); patrol.patrolRadius = (rawPatrolRadius > NO_PATROLING) ? rawPatrolRadius : 0; diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index faa49360b..c9ede1335 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -186,7 +186,7 @@ void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult } } -void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) { diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index a1e5e8394..7277a792d 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -26,7 +26,7 @@ public: void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; template void serialize(Handler &h) { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 367641c1f..3d84843ea 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -385,7 +385,7 @@ void CTownRewardableBuilding::heroLevelUpDone(const CGHeroInstance *hero) const grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero); } -void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer == 0) return; // player refused diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h index 9ddf4e616..b8617d784 100644 --- a/lib/mapObjects/CGTownBuilding.h +++ b/lib/mapObjects/CGTownBuilding.h @@ -133,7 +133,7 @@ public: void initObj(vstd::RNG & rand) override; /// applies player selection of reward - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * town, vstd::RNG & rand); CTownRewardableBuilding(IGameCallback *cb); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index baec8bfbf..04feb9d09 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -227,7 +227,7 @@ TResources CGTownInstance::dailyIncome() const auto playerSettings = cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()); for(TResources::nziterator it(ret); it.valid(); it++) // always round up income - we don't want to always produce zero if handicap in use - ret[it->resType] = (ret[it->resType] * playerSettings.handicap.percentIncome + 99) / 100; + ret[it->resType] = vstd::divideAndCeil(ret[it->resType] * playerSettings.handicap.percentIncome, 100); return ret; } @@ -281,7 +281,7 @@ void CGTownInstance::setOwner(const PlayerColor & player) const cb->setOwner(this, player); } -void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { for (auto building : bonusingBuildings) building->blockingDialogAnswered(hero, answer); @@ -1229,6 +1229,12 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeIdArray( "possibleSpells", possibleSpells); handler.serializeIdArray( "obligatorySpells", obligatorySpells); } + + { + auto eventsHandler = handler.enterArray("events"); + eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR); + eventsHandler.serializeStruct(events); + } } FactionID CGTownInstance::getFaction() const @@ -1279,7 +1285,7 @@ int GrowthInfo::totalGrowth() const ret += entry.count; // always round up income - we don't want buildings to always produce zero if handicap in use - return (ret * handicapPercentage + 99) / 100; + return vstd::divideAndCeil(ret * handicapPercentage, 100); } void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index ce771acf2..d105c3a87 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -65,7 +65,7 @@ public: std::vector bonusingBuildings; std::vector possibleSpells, obligatorySpells; std::vector > spells; //spells[level] -> vector of spells, first will be available in guild - std::list events; + std::vector events; std::pair bonusValue;//var to store town bonuses (rampart = resources from mystic pond, factory = save debts); ////////////////////////////////////////////////////////////////////////// @@ -223,7 +223,7 @@ public: protected: void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; private: FactionID randomizeFaction(vstd::RNG & rand); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 98179cd05..ca06e4d49 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -660,7 +660,7 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const return dynamic_cast(o); } -void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { CRewardableObject::blockingDialogAnswered(hero, answer); if(answer) @@ -865,7 +865,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const } } -void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if (answer) cb->removeObject(this, hero->getOwner()); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 281a087ba..407c46134 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -150,7 +150,7 @@ public: std::vector getPopupComponents(const CGHeroInstance * hero) const override; void newTurn(vstd::RNG & rand) const override; void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; virtual void init(vstd::RNG & rand); @@ -229,7 +229,7 @@ public: void initObj(vstd::RNG & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; void getRolloverText (MetaString &text, bool onHover) const; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 416fe93fa..4fedb7f09 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -181,10 +181,29 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero); } -void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer == 0) + { + switch (configuration.visitMode) + { + case Rewardable::VISIT_UNLIMITED: + case Rewardable::VISIT_BONUS: + case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: + { + // workaround for object with refusable reward not getting marked as visited + // TODO: better solution that would also work for player-visitable objects + if (!wasScouted(hero->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id); + cb->sendAndApply(&cov); + } + } + } + return; // player refused + } if(answer > 0 && answer-1 < configuration.info.size()) { diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index a3fb7c2e2..9e1735a5f 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -63,7 +63,7 @@ public: void heroLevelUpDone(const CGHeroInstance *hero) const override; /// applies player selection of reward - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void initObj(vstd::RNG & rand) override; diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index b3101155d..11d9e294c 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -68,7 +68,7 @@ void IObjectInterface::preInit() void IObjectInterface::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const {} -void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const {} void IObjectInterface::garrisonDialogClosed(const CGHeroInstance *hero) const diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 82a798e72..c528dbc37 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -61,7 +61,7 @@ public: //Called when queries created DURING HERO VISIT are resolved //First parameter is always hero that visited object and triggered the query virtual void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const; - virtual void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const; + virtual void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const; virtual void garrisonDialogClosed(const CGHeroInstance *hero) const; virtual void heroLevelUpDone(const CGHeroInstance *hero) const; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index b0eff7592..95059c339 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -200,7 +200,7 @@ ui32 CGMine::getProducedQuantity() const { auto * playerSettings = cb->getPlayerSettings(getOwner()); // always round up income - we don't want mines to always produce zero if handicap in use - return (producedQuantity * playerSettings->handicap.percentIncome + 99) / 100; + return vstd::divideAndCeil(producedQuantity * playerSettings->handicap.percentIncome, 100); } void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const @@ -215,7 +215,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu } } -void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) cb->startBattleI(hero, this); @@ -348,7 +348,7 @@ void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult & collectRes(hero->getOwner()); } -void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) cb->startBattleI(hero, this); @@ -915,7 +915,7 @@ void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult & pick(hero); } -void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const +void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) cb->startBattleI(hero, this); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 4f15210fc..542894a2f 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -91,7 +91,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::string getObjectName() const override; std::string getPopupText(PlayerColor player) const override; @@ -132,7 +132,7 @@ public: void initObj(vstd::RNG & rand) override; void pickRandomObject(vstd::RNG & rand) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; std::string getHoverText(PlayerColor player) const override; void collectRes(const PlayerColor & player) const; @@ -163,7 +163,7 @@ private: void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void flagMine(const PlayerColor & player) const; void newTurn(vstd::RNG & rand) const override; diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 8ef2de96e..48f3c0b6f 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -561,7 +561,7 @@ struct DLL_LINKAGE UpdateMapEvents : public CPackForClient struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient { ObjectInstanceID town; - std::list events; + std::vector events; void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 0bdd5fb5d..c52501a6b 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -115,8 +115,10 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - if (!h || h->canLearnSpell(entry.toEntity(VLC), true)) - comps.emplace_back(ComponentType::SPELL, entry); + { + bool learnable = !h || h->canLearnSpell(entry.toEntity(VLC), true); + comps.emplace_back(ComponentType::SPELL, entry, learnable ? 0 : -1); + } for(const auto & entry : creatures) comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index e5fb4836b..f678b47d3 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -463,7 +463,8 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler) "guarded", "fictive", "repulsive", - "wide" + "wide", + "forcePortal" }; static const std::vector roadOptions = diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 1d0382ad1..ba5e49f9b 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -75,7 +75,8 @@ enum class EConnectionType GUARDED = 0, //default FICTIVE, REPULSIVE, - WIDE + WIDE, + FORCE_PORTAL }; enum class ERoadOption diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 94719a983..29b1acfc6 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -80,12 +80,21 @@ void CZonePlacer::findPathsBetweenZones() for (auto & connection : connectedZoneIds) { - if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { //Do not consider virtual connections for graph distance - continue; + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; } auto neighbor = connection.getOtherZoneId(current); + + if (current == neighbor) + { + //Do not consider self-connections + continue; + } + if (!visited[neighbor]) { visited[neighbor] = true; @@ -552,8 +561,16 @@ void CZonePlacer::attractConnectedZones(TZoneMap & zones, TForceVector & forces, for (const auto & connection : zone.second->getConnections()) { - if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { + //Do not consider virtual connections for graph distance + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; + } + if (connection.getZoneA() == connection.getZoneB()) + { + //Do not consider self-connections continue; } @@ -710,11 +727,19 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista std::set connectedZones; for (const auto& connection : firstZone->getConnections()) { - //FIXME: Should we also exclude fictive connections? - if (connection.getConnectionType() != rmg::EConnectionType::REPULSIVE) + switch (connection.getConnectionType()) { - connectedZones.insert(connection.getOtherZoneId(firstZone->getId())); + //Do not consider virtual connections for graph distance + case rmg::EConnectionType::REPULSIVE: + case rmg::EConnectionType::FORCE_PORTAL: + continue; } + if (connection.getZoneA() == connection.getZoneB()) + { + //Do not consider self-connections + continue; + } + connectedZones.insert(connection.getOtherZoneId(firstZone->getId())); } auto level = firstZone->getCenter().z; diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index d6bc3c2c5..79c4ff438 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -55,6 +55,17 @@ void ConnectionsPlacer::process() { for (auto& c : dConnections) { + if (c.getZoneA() == c.getZoneB()) + { + // Zone can always be connected to itself, but only by monolith pair + RecursiveLock lock(externalAccessMutex); + if (!vstd::contains(dCompleted, c)) + { + placeMonolithConnection(c); + continue; + } + } + auto otherZone = map.getZones().at(c.getZoneB()); auto* cp = otherZone->getModificator(); @@ -74,6 +85,11 @@ void ConnectionsPlacer::process() } }; + diningPhilosophers([this](const rmg::ZoneConnection& c) + { + forcePortalConnection(c); + }); + diningPhilosophers([this](const rmg::ZoneConnection& c) { selfSideDirectConnection(c); @@ -115,6 +131,15 @@ void ConnectionsPlacer::otherSideConnection(const rmg::ZoneConnection & connecti dCompleted.push_back(connection); } +void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connection) +{ + // This should always succeed + if (connection.getConnectionType() == rmg::EConnectionType::FORCE_PORTAL) + { + placeMonolithConnection(connection); + } +} + void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & connection) { bool success = false; @@ -410,23 +435,30 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c //4. place monoliths/portals if(!success) { - auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex()); - auto * teleport1 = factory->create(map.mapInstance->cb, nullptr); - auto * teleport2 = factory->create(map.mapInstance->cb, nullptr); - - RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad); - RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad); - zone.getModificator()->addRequiredObject(obj1); - otherZone->getModificator()->addRequiredObject(obj2); - - assert(otherZone->getModificator()); - otherZone->getModificator()->otherSideConnection(connection); - - success = true; + placeMonolithConnection(connection); } +} + +void ConnectionsPlacer::placeMonolithConnection(const rmg::ZoneConnection & connection) +{ + auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA()); + auto & otherZone = map.getZones().at(otherZoneId); + + bool allowRoad = shouldGenerateRoad(connection); + + auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex()); + auto * teleport1 = factory->create(map.mapInstance->cb, nullptr); + auto * teleport2 = factory->create(map.mapInstance->cb, nullptr); + + RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad); + RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad); + zone.getModificator()->addRequiredObject(obj1); + otherZone->getModificator()->addRequiredObject(obj2); + + dCompleted.push_back(connection); - if(success) - dCompleted.push_back(connection); + assert(otherZone->getModificator()); + otherZone->getModificator()->otherSideConnection(connection); } void ConnectionsPlacer::collectNeighbourZones() diff --git a/lib/rmg/modificators/ConnectionsPlacer.h b/lib/rmg/modificators/ConnectionsPlacer.h index 0350d6d92..ad31609b1 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.h +++ b/lib/rmg/modificators/ConnectionsPlacer.h @@ -23,7 +23,8 @@ public: void init() override; void addConnection(const rmg::ZoneConnection& connection); - + void placeMonolithConnection(const rmg::ZoneConnection& connection); + void forcePortalConnection(const rmg::ZoneConnection & connection); void selfSideDirectConnection(const rmg::ZoneConnection & connection); void selfSideIndirectConnection(const rmg::ZoneConnection & connection); void otherSideConnection(const rmg::ZoneConnection & connection); diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 853bcde7e..a243b4373 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -59,6 +59,7 @@ enum class ESerializationVersion : int32_t REMOVE_LIB_RNG, // 849 - removed random number generators from library classes HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign PLAYER_HANDICAP, // 851 - player handicap selection at game start + STATISTICS, // 852 - removed random number generators from library classes - CURRENT = PLAYER_HANDICAP + CURRENT = STATISTICS }; diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index cd0ca7b35..4fc4679e6 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -29,6 +29,9 @@ set(editor_SRCS validator.cpp inspector/inspector.cpp inspector/townbuildingswidget.cpp + inspector/towneventdialog.cpp + inspector/towneventswidget.cpp + inspector/townspellswidget.cpp inspector/armywidget.cpp inspector/messagewidget.cpp inspector/rewardswidget.cpp @@ -70,6 +73,9 @@ set(editor_HEADERS validator.h inspector/inspector.h inspector/townbuildingswidget.h + inspector/towneventdialog.h + inspector/towneventswidget.h + inspector/townspellswidget.h inspector/armywidget.h inspector/messagewidget.h inspector/rewardswidget.h @@ -79,6 +85,7 @@ set(editor_HEADERS inspector/PickObjectDelegate.h inspector/portraitwidget.h resourceExtractor/ResourceConverter.h + mapeditorroles.h ) set(editor_FORMS @@ -98,6 +105,9 @@ set(editor_FORMS playerparams.ui validator.ui inspector/townbuildingswidget.ui + inspector/towneventdialog.ui + inspector/towneventswidget.ui + inspector/townspellswidget.ui inspector/armywidget.ui inspector/messagewidget.ui inspector/rewardswidget.ui diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index e878f06fa..971c6676d 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -21,6 +21,8 @@ #include "../lib/constants/StringConstants.h" #include "townbuildingswidget.h" +#include "towneventswidget.h" +#include "townspellswidget.h" #include "armywidget.h" #include "messagewidget.h" #include "rewardswidget.h" @@ -342,6 +344,8 @@ void Inspector::updateProperties(CGTownInstance * o) auto * delegate = new TownBuildingsDelegate(*o); addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false); + addProperty("Spells", PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false); + addProperty("Events", PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false); } void Inspector::updateProperties(CGArtifact * o) diff --git a/mapeditor/inspector/townbuildingswidget.cpp b/mapeditor/inspector/townbuildingswidget.cpp index f14af11a6..775c735f9 100644 --- a/mapeditor/inspector/townbuildingswidget.cpp +++ b/mapeditor/inspector/townbuildingswidget.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "townbuildingswidget.h" #include "ui_townbuildingswidget.h" +#include "mapeditorroles.h" #include "../lib/entities/building/CBuilding.h" #include "../lib/entities/faction/CTownHandler.h" #include "../lib/texts/CGeneralTextHandler.h" @@ -68,6 +69,56 @@ std::string defaultBuildingIdConversion(BuildingID bId) } } +QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model) +{ + QStandardItem * parent = nullptr; + std::vector stack(1); + do + { + auto pindex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(pindex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, 0, pindex); + if (building->upgrade.getNum() == model.itemFromIndex(index)->data(MapEditorRoles::BuildingIDRole).toInt()) + { + parent = model.itemFromIndex(index); + break; + } + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!parent && !stack.empty()); + return parent; +} + +QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState) +{ + QVariantList result; + std::vector stack(1); + do + { + auto pindex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(pindex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, modelColumn, pindex); + auto * item = model.itemFromIndex(index); + if(item && item->checkState() == checkState) + result.push_back(item->data(MapEditorRoles::BuildingIDRole)); + index = model.index(i, 0, pindex); + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!stack.empty()); + + return result; +} + + + TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) : town(t), QDialog(parent), @@ -76,8 +127,8 @@ TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) : ui->setupUi(this); ui->treeView->setModel(&model); //ui->treeView->setColumnCount(3); - model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built")); - + model.setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Enabled") << tr("Built")); + connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); //setAttribute(Qt::WA_DeleteOnClose); } @@ -96,7 +147,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s return nullptr; } - QString name = tr(building->getNameTranslated().c_str()); + QString name = QString::fromStdString(building->getNameTranslated()); if(name.isEmpty()) name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); @@ -104,17 +155,17 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s QList checks; checks << new QStandardItem(name); - checks.back()->setData(bId, Qt::UserRole); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); checks << new QStandardItem; checks.back()->setCheckable(true); checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked); - checks.back()->setData(bId, Qt::UserRole); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); checks << new QStandardItem; checks.back()->setCheckable(true); checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked); - checks.back()->setData(bId, Qt::UserRole); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); if(building->getBase() == buildingId) { @@ -122,25 +173,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s } else { - QStandardItem * parent = nullptr; - std::vector stack; - stack.push_back(QModelIndex()); - while(!parent && !stack.empty()) - { - auto pindex = stack.back(); - stack.pop_back(); - for(int i = 0; i < model.rowCount(pindex); ++i) - { - QModelIndex index = model.index(i, 0, pindex); - if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) - { - parent = model.itemFromIndex(index); - break; - } - if(model.hasChildren(index)) - stack.push_back(index); - } - } + QStandardItem * parent = getBuildingParentFromTreeModel(building, model); if(!parent) parent = addBuilding(ctown, building->upgrade.getNum(), remaining); @@ -172,36 +205,23 @@ void TownBuildingsWidget::addBuildings(const CTown & ctown) std::set TownBuildingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState) { + auto buildingVariants = getBuildingVariantsFromModel(model, modelColumn, checkState); std::set result; - std::vector stack; - stack.push_back(QModelIndex()); - while(!stack.empty()) + for (const auto & buildingId : buildingVariants) { - auto pindex = stack.back(); - stack.pop_back(); - for(int i = 0; i < model.rowCount(pindex); ++i) - { - QModelIndex index = model.index(i, modelColumn, pindex); - if(auto * item = model.itemFromIndex(index)) - if(item->checkState() == checkState) - result.emplace(item->data(Qt::UserRole).toInt()); - index = model.index(i, 0, pindex); //children are linked to first column of the model - if(model.hasChildren(index)) - stack.push_back(index); - } + result.insert(buildingId.toInt()); } - return result; } std::set TownBuildingsWidget::getForbiddenBuildings() { - return getBuildingsFromModel(1, Qt::Unchecked); + return getBuildingsFromModel(Column::ENABLED, Qt::Unchecked); } std::set TownBuildingsWidget::getBuiltBuildings() { - return getBuildingsFromModel(2, Qt::Checked); + return getBuildingsFromModel(Column::BUILT, Qt::Checked); } void TownBuildingsWidget::on_treeView_expanded(const QModelIndex &index) @@ -214,6 +234,87 @@ void TownBuildingsWidget::on_treeView_collapsed(const QModelIndex &index) ui->treeView->resizeColumnToContents(0); } +void TownBuildingsWidget::on_buildAll_clicked() +{ + setAllRowsColumnCheckState(Column::BUILT, Qt::Checked); +} + +void TownBuildingsWidget::on_demolishAll_clicked() +{ + setAllRowsColumnCheckState(Column::BUILT, Qt::Unchecked); +} + +void TownBuildingsWidget::on_enableAll_clicked() +{ + setAllRowsColumnCheckState(Column::ENABLED, Qt::Checked); +} + +void TownBuildingsWidget::on_disableAll_clicked() +{ + setAllRowsColumnCheckState(Column::ENABLED, Qt::Unchecked); +} + + +void TownBuildingsWidget::setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState) { + auto sibling = item->model()->sibling(item->row(), column, item->index()); + model.itemFromIndex(sibling)->setCheckState(checkState); +} + +void TownBuildingsWidget::setAllRowsColumnCheckState(Column column, Qt::CheckState checkState) +{ + std::vector stack(1); + do + { + auto parentIndex = stack.back(); + stack.pop_back(); + auto rowCount = model.rowCount(parentIndex); + for (int i = 0; i < rowCount; ++i) + { + QModelIndex index = model.index(i, column, parentIndex); + if (auto* item = model.itemFromIndex(index)) + item->setCheckState(checkState); + index = model.index(i, 0, parentIndex); + if (model.hasChildren(index)) + stack.push_back(index); + } + } while(!stack.empty()); +} + +void TownBuildingsWidget::onItemChanged(const QStandardItem * item) { + disconnect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); + auto rowFirstColumnIndex = item->model()->sibling(item->row(), Column::TYPE, item->index()); + QStandardItem * nextRow = model.itemFromIndex(rowFirstColumnIndex); + if (item->checkState() == Qt::Checked) { + while (nextRow) { + setRowColumnCheckState(nextRow, Column(item->column()), Qt::Checked); + if (item->column() == Column::BUILT) { + setRowColumnCheckState(nextRow, Column::ENABLED, Qt::Checked); + } + nextRow = nextRow->parent(); + + } + } + else if (item->checkState() == Qt::Unchecked) { + std::vector stack; + stack.push_back(nextRow); + do + { + nextRow = stack.back(); + stack.pop_back(); + setRowColumnCheckState(nextRow, Column(item->column()), Qt::Unchecked); + if (item->column() == Column::ENABLED) { + setRowColumnCheckState(nextRow, Column::BUILT, Qt::Unchecked); + } + if (nextRow->hasChildren()) { + for (int i = 0; i < nextRow->rowCount(); ++i) { + stack.push_back(nextRow->child(i, Column::TYPE)); + } + } + + } while(!stack.empty()); + } + connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged); +} TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate() { diff --git a/mapeditor/inspector/townbuildingswidget.h b/mapeditor/inspector/townbuildingswidget.h index 90629f8be..2b3cf1dd2 100644 --- a/mapeditor/inspector/townbuildingswidget.h +++ b/mapeditor/inspector/townbuildingswidget.h @@ -19,6 +19,10 @@ class TownBuildingsWidget; std::string defaultBuildingIdConversion(BuildingID bId); +QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model); + +QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState); + class TownBuildingsWidget : public QDialog { Q_OBJECT @@ -26,9 +30,13 @@ class TownBuildingsWidget : public QDialog QStandardItem * addBuilding(const CTown & ctown, int bId, std::set & remaining); public: + enum Column + { + TYPE, ENABLED, BUILT + }; explicit TownBuildingsWidget(CGTownInstance &, QWidget *parent = nullptr); ~TownBuildingsWidget(); - + void addBuildings(const CTown & ctown); std::set getForbiddenBuildings(); std::set getBuiltBuildings(); @@ -38,9 +46,21 @@ private slots: void on_treeView_collapsed(const QModelIndex &index); + void on_buildAll_clicked(); + + void on_demolishAll_clicked(); + + void on_enableAll_clicked(); + + void on_disableAll_clicked(); + + void onItemChanged(const QStandardItem * item); + private: std::set getBuildingsFromModel(int modelColumn, Qt::CheckState checkState); - + void setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState); + void setAllRowsColumnCheckState(Column column, Qt::CheckState checkState); + Ui::TownBuildingsWidget *ui; CGTownInstance & town; mutable QStandardItemModel model; diff --git a/mapeditor/inspector/townbuildingswidget.ui b/mapeditor/inspector/townbuildingswidget.ui index 86bbaf54e..472093960 100644 --- a/mapeditor/inspector/townbuildingswidget.ui +++ b/mapeditor/inspector/townbuildingswidget.ui @@ -9,7 +9,7 @@ 0 0 - 480 + 580 280 @@ -21,7 +21,7 @@ - 480 + 580 280 @@ -45,6 +45,38 @@ + + + + + + Build all + + + + + + + Demolish all + + + + + + + Enable all + + + + + + + Disable all + + + + + diff --git a/mapeditor/inspector/towneventdialog.cpp b/mapeditor/inspector/towneventdialog.cpp new file mode 100644 index 000000000..845f774c6 --- /dev/null +++ b/mapeditor/inspector/towneventdialog.cpp @@ -0,0 +1,289 @@ +/* + * towneventdialog.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 "townbuildingswidget.h" +#include "towneventdialog.h" +#include "ui_towneventdialog.h" +#include "mapeditorroles.h" +#include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/faction/CTownHandler.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +static const int FIRST_DAY_FOR_EVENT = 1; +static const int LAST_DAY_FOR_EVENT = 999; +static const int MAXIMUM_EVENT_REPEAT_AFTER = 999; + +static const int MAXIMUM_GOLD_CHANGE = 999999; +static const int MAXIMUM_RESOURCE_CHANGE = 999; +static const int GOLD_STEP = 100; +static const int RESOURCE_STEP = 1; + +static const int MAXIMUM_CREATURES_CHANGE = 999999; + +TownEventDialog::TownEventDialog(CGTownInstance & t, QListWidgetItem * item, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownEventDialog), + town(t), + townEventListItem(item) +{ + ui->setupUi(this); + + ui->buildingsTree->setModel(&buildingsModel); + + params = townEventListItem->data(MapEditorRoles::TownEventRole).toMap(); + ui->eventFirstOccurrence->setMinimum(FIRST_DAY_FOR_EVENT); + ui->eventFirstOccurrence->setMaximum(LAST_DAY_FOR_EVENT); + ui->eventRepeatAfter->setMaximum(MAXIMUM_EVENT_REPEAT_AFTER); + ui->eventNameText->setText(params.value("name").toString()); + ui->eventMessageText->setPlainText(params.value("message").toString()); + ui->eventAffectsCpu->setChecked(params.value("computerAffected").toBool()); + ui->eventAffectsHuman->setChecked(params.value("humanAffected").toBool()); + ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt()+1); + ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt()); + + initPlayers(); + initResources(); + initBuildings(); + initCreatures(); +} + +TownEventDialog::~TownEventDialog() +{ + delete ui; +} + +void TownEventDialog::initPlayers() +{ + for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + bool isAffected = (1 << i) & params.value("players").toInt(); + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); + item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i)); + item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); + ui->playersAffected->addItem(item); + } +} + +void TownEventDialog::initResources() +{ + ui->resourcesTable->setRowCount(GameConstants::RESOURCE_QUANTITY); + auto resourcesMap = params.value("resources").toMap(); + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); + auto * item = new QTableWidgetItem(); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setText(name); + ui->resourcesTable->setItem(i, 0, item); + + int val = resourcesMap.value(name).toInt(); + auto * edit = new QSpinBox(ui->resourcesTable); + edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE); + edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE); + edit->setSingleStep(i == GameResID::GOLD ? GOLD_STEP : RESOURCE_STEP); + edit->setValue(val); + + ui->resourcesTable->setCellWidget(i, 1, edit); + } +} + +void TownEventDialog::initBuildings() +{ + auto * ctown = town.town; + if (!ctown) + ctown = VLC->townh->randomTown; + if (!ctown) + throw std::runtime_error("No Town defined for type selected"); + auto allBuildings = ctown->getAllBuildings(); + while (!allBuildings.empty()) + { + addBuilding(*ctown, *allBuildings.begin(), allBuildings); + } + ui->buildingsTree->resizeColumnToContents(0); + + connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); +} + +QStandardItem * TownEventDialog::addBuilding(const CTown& ctown, BuildingID buildingId, std::set& remaining) +{ + auto bId = buildingId.num; + const CBuilding * building = ctown.buildings.at(buildingId); + + QString name = QString::fromStdString(building->getNameTranslated()); + + if (name.isEmpty()) + name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); + + QList checks; + + checks << new QStandardItem(name); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(params["buildings"].toList().contains(bId) ? Qt::Checked : Qt::Unchecked); + checks.back()->setData(bId, MapEditorRoles::BuildingIDRole); + + if (building->getBase() == buildingId) + { + buildingsModel.appendRow(checks); + } + else + { + QStandardItem * parent = getBuildingParentFromTreeModel(building, buildingsModel); + + if (!parent) + parent = addBuilding(ctown, building->upgrade.getNum(), remaining); + + parent->appendRow(checks); + } + + remaining.erase(bId); + return checks.front(); +} + +void TownEventDialog::initCreatures() +{ + auto creatures = params.value("creatures").toList(); + auto * ctown = town.town; + for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i) + { + QString creatureNames; + if (!ctown) + { + creatureNames.append(tr("Creature level %1 / Creature level %1 Upgrade").arg(i + 1)); + } + else + { + auto creaturesOnLevel = ctown->creatures.at(i); + for (auto& creature : creaturesOnLevel) + { + auto cre = VLC->creatures()->getById(creature); + auto creatureName = QString::fromStdString(cre->getNameSingularTranslated()); + creatureNames.append(creatureNames.isEmpty() ? creatureName : " / " + creatureName); + } + } + auto * item = new QTableWidgetItem(); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setText(creatureNames); + ui->creaturesTable->setItem(i, 0, item); + + auto creatureNumber = creatures.size() > i ? creatures.at(i).toInt() : 0; + auto * edit = new QSpinBox(ui->creaturesTable); + edit->setValue(creatureNumber); + edit->setMaximum(MAXIMUM_CREATURES_CHANGE); + ui->creaturesTable->setCellWidget(i, 1, edit); + + } + ui->creaturesTable->resizeColumnToContents(0); +} + +void TownEventDialog::on_TownEventDialog_finished(int result) +{ + QVariantMap descriptor; + descriptor["name"] = ui->eventNameText->text(); + descriptor["message"] = ui->eventMessageText->toPlainText(); + descriptor["humanAffected"] = QVariant::fromValue(ui->eventAffectsHuman->isChecked()); + descriptor["computerAffected"] = QVariant::fromValue(ui->eventAffectsCpu->isChecked()); + descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value()-1); + descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); + descriptor["players"] = playersToVariant(); + descriptor["resources"] = resourcesToVariant(); + descriptor["buildings"] = buildingsToVariant(); + descriptor["creatures"] = creaturesToVariant(); + + townEventListItem->setData(MapEditorRoles::TownEventRole, descriptor); + auto itemText = tr("Day %1 - %2").arg(ui->eventFirstOccurrence->value(), 3).arg(ui->eventNameText->text()); + townEventListItem->setText(itemText); +} + +QVariant TownEventDialog::playersToVariant() +{ + int players = 0; + for (int i = 0; i < ui->playersAffected->count(); ++i) + { + auto * item = ui->playersAffected->item(i); + if (item->checkState() == Qt::Checked) + players |= 1 << i; + } + return QVariant::fromValue(players); +} + +QVariantMap TownEventDialog::resourcesToVariant() +{ + auto res = params.value("resources").toMap(); + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto * itemType = ui->resourcesTable->item(i, 0); + auto * itemQty = static_cast (ui->resourcesTable->cellWidget(i, 1)); + + res[itemType->text()] = QVariant::fromValue(itemQty->value()); + } + return res; +} + +QVariantList TownEventDialog::buildingsToVariant() +{ + return getBuildingVariantsFromModel(buildingsModel, 1, Qt::Checked); +} + +QVariantList TownEventDialog::creaturesToVariant() +{ + QVariantList creaturesList; + for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i) + { + auto * item = static_cast(ui->creaturesTable->cellWidget(i, 1)); + creaturesList.push_back(item->value()); + } + return creaturesList; +} + +void TownEventDialog::on_okButton_clicked() +{ + close(); +} + +void TownEventDialog::setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState) { + auto sibling = item->model()->sibling(item->row(), column, item->index()); + buildingsModel.itemFromIndex(sibling)->setCheckState(checkState); +} + +void TownEventDialog::onItemChanged(const QStandardItem * item) +{ + disconnect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); + auto rowFirstColumnIndex = item->model()->sibling(item->row(), 0, item->index()); + QStandardItem * nextRow = buildingsModel.itemFromIndex(rowFirstColumnIndex); + if (item->checkState() == Qt::Checked) { + while (nextRow) { + setRowColumnCheckState(nextRow,item->column(), Qt::Checked); + nextRow = nextRow->parent(); + + } + } + else if (item->checkState() == Qt::Unchecked) { + std::vector stack; + stack.push_back(nextRow); + do + { + nextRow = stack.back(); + stack.pop_back(); + setRowColumnCheckState(nextRow, item->column(), Qt::Unchecked); + if (nextRow->hasChildren()) { + for (int i = 0; i < nextRow->rowCount(); ++i) { + stack.push_back(nextRow->child(i, 0)); + } + } + + } while(!stack.empty()); + } + connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged); +} diff --git a/mapeditor/inspector/towneventdialog.h b/mapeditor/inspector/towneventdialog.h new file mode 100644 index 000000000..635939a36 --- /dev/null +++ b/mapeditor/inspector/towneventdialog.h @@ -0,0 +1,53 @@ +/* + * towneventdialog.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 "../StdInc.h" +#include +#include "../lib/mapObjects/CGTownInstance.h" + +namespace Ui { + class TownEventDialog; +} + +class TownEventDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TownEventDialog(CGTownInstance & town, QListWidgetItem * item, QWidget * parent); + ~TownEventDialog(); + + +private slots: + void onItemChanged(const QStandardItem * item); + void on_TownEventDialog_finished(int result); + void on_okButton_clicked(); + void setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState); + +private: + void initPlayers(); + void initResources(); + void initBuildings(); + void initCreatures(); + + QVariant playersToVariant(); + QVariantMap resourcesToVariant(); + QVariantList buildingsToVariant(); + QVariantList creaturesToVariant(); + + QStandardItem * addBuilding(const CTown & ctown, BuildingID bId, std::set & remaining); + + Ui::TownEventDialog * ui; + CGTownInstance & town; + QListWidgetItem * townEventListItem; + QMap params; + QStandardItemModel buildingsModel; +}; diff --git a/mapeditor/inspector/towneventdialog.ui b/mapeditor/inspector/towneventdialog.ui new file mode 100644 index 000000000..9d698afc2 --- /dev/null +++ b/mapeditor/inspector/towneventdialog.ui @@ -0,0 +1,266 @@ + + + TownEventDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 693 + 525 + + + + + 0 + 0 + + + + Town event + + + + 0 + + + 3 + + + 3 + + + + + 0 + + + + General + + + + + 9 + 9 + 511 + 351 + + + + + + + Event name + + + + + + + Type event message text + + + + + + + + + 10 + 370 + 511 + 61 + + + + + + + + + Day of first occurrence + + + + + + + + + + + + + + Repeat after (0 = no repeat) + + + + + + + + + + + + + + 529 + 9 + 141 + 421 + + + + + + + Affected players + + + + + + + + 0 + 0 + + + + + 200 + 16777215 + + + + + + + + affects human + + + + + + + + + affects AI + + + + + + + + + + + Resources + + + + + 10 + 10 + 661 + 421 + + + + + 0 + 0 + + + + 2 + + + false + + + false + + + + + + + + Buildings + + + + + 10 + 10 + 661 + 421 + + + + QAbstractItemView::NoEditTriggers + + + false + + + + + + Creatures + + + + + 10 + 10 + 661 + 421 + + + + 7 + + + 2 + + + false + + + false + + + + + + + + + + + + + + + + + + OK + + + + + + + + diff --git a/mapeditor/inspector/towneventswidget.cpp b/mapeditor/inspector/towneventswidget.cpp new file mode 100644 index 000000000..f963a2ae3 --- /dev/null +++ b/mapeditor/inspector/towneventswidget.cpp @@ -0,0 +1,177 @@ +/* + * towneventswidget.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 "towneventswidget.h" +#include "ui_towneventswidget.h" +#include "towneventdialog.h" +#include "mapeditorroles.h" +#include "mapsettings/eventsettings.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +TownEventsWidget::TownEventsWidget(CGTownInstance & town, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownEventsWidget), + town(town) +{ + ui->setupUi(this); +} + +TownEventsWidget::~TownEventsWidget() +{ + delete ui; +} + +QVariant toVariant(const std::set & buildings) +{ + QVariantList result; + for (auto b : buildings) + result.push_back(QVariant::fromValue(b.num)); + return result; +} + +QVariant toVariant(const std::vector & creatures) +{ + QVariantList result; + for (auto c : creatures) + result.push_back(QVariant::fromValue(c)); + return result; +} + +std::set buildingsFromVariant(const QVariant& v) +{ + std::set result; + for (const auto & r : v.toList()) { + result.insert(BuildingID(r.toInt())); + } + return result; +} + +std::vector creaturesFromVariant(const QVariant& v) +{ + std::vector result; + for (const auto & r : v.toList()) { + result.push_back(r.toInt()); + } + return result; +} + +QVariant toVariant(const CCastleEvent& event) +{ + QVariantMap result; + result["name"] = QString::fromStdString(event.name); + result["message"] = QString::fromStdString(event.message.toString()); + result["players"] = QVariant::fromValue(event.players); + result["humanAffected"] = QVariant::fromValue(event.humanAffected); + result["computerAffected"] = QVariant::fromValue(event.computerAffected); + result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence); + result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence); + result["resources"] = toVariant(event.resources); + result["buildings"] = toVariant(event.buildings); + result["creatures"] = toVariant(event.creatures); + + return QVariant(result); +} + +CCastleEvent eventFromVariant(CMapHeader& map, const CGTownInstance& town, const QVariant& variant) +{ + CCastleEvent result; + auto v = variant.toMap(); + result.name = v.value("name").toString().toStdString(); + result.message.appendTextID(mapRegisterLocalizedString("map", map, TextIdentifier("town", town.instanceName, "event", result.name, "message"), v.value("message").toString().toStdString())); + result.players = v.value("players").toInt(); + result.humanAffected = v.value("humanAffected").toInt(); + result.computerAffected = v.value("computerAffected").toInt(); + result.firstOccurrence = v.value("firstOccurrence").toInt(); + result.nextOccurrence = v.value("nextOccurrence").toInt(); + result.resources = resourcesFromVariant(v.value("resources")); + result.buildings = buildingsFromVariant(v.value("buildings")); + result.creatures = creaturesFromVariant(v.value("creatures")); + return result; +} + +void TownEventsWidget::obtainData() +{ + for (const auto & event : town.events) + { + auto eventName = QString::fromStdString(event.name); + auto itemText = tr("Day %1 - %2").arg(event.firstOccurrence+1, 3).arg(eventName); + + auto * item = new QListWidgetItem(itemText); + item->setData(MapEditorRoles::TownEventRole, toVariant(event)); + ui->eventsList->addItem(item); + } +} + +void TownEventsWidget::commitChanges(MapController& controller) +{ + town.events.clear(); + for (int i = 0; i < ui->eventsList->count(); ++i) + { + const auto * item = ui->eventsList->item(i); + town.events.push_back(eventFromVariant(*controller.map(), town, item->data(MapEditorRoles::TownEventRole))); + } +} + +void TownEventsWidget::on_timedEventAdd_clicked() +{ + CCastleEvent event; + event.name = tr("New event").toStdString(); + auto* item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(MapEditorRoles::TownEventRole, toVariant(event)); + ui->eventsList->addItem(item); + on_eventsList_itemActivated(item); +} + +void TownEventsWidget::on_timedEventRemove_clicked() +{ + delete ui->eventsList->takeItem(ui->eventsList->currentRow()); +} + +void TownEventsWidget::on_eventsList_itemActivated(QListWidgetItem* item) +{ + TownEventDialog dlg{ town, item, parentWidget() }; + dlg.exec(); +} + + +TownEventsDelegate::TownEventsDelegate(CGTownInstance & town, MapController & c) : QStyledItemDelegate(), town(town), controller(c) +{ +} + +QWidget* TownEventsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new TownEventsWidget(town, parent); +} + +void TownEventsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownEventsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(controller); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/towneventswidget.h b/mapeditor/inspector/towneventswidget.h new file mode 100644 index 000000000..7bc85f6ca --- /dev/null +++ b/mapeditor/inspector/towneventswidget.h @@ -0,0 +1,58 @@ +/* + * towneventswidget.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 "../StdInc.h" +#include +#include "../lib/mapping/CMapDefines.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../mapcontroller.h" + +namespace Ui { + class TownEventsWidget; +} + +class TownEventsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit TownEventsWidget(CGTownInstance &, QWidget * parent = nullptr); + ~TownEventsWidget(); + + void obtainData(); + void commitChanges(MapController & controller); +private slots: + void on_timedEventAdd_clicked(); + void on_timedEventRemove_clicked(); + void on_eventsList_itemActivated(QListWidgetItem * item); + +private: + + Ui::TownEventsWidget * ui; + CGTownInstance & town; +}; + +class TownEventsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + TownEventsDelegate(CGTownInstance &, MapController &); + + QWidget* createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGTownInstance & town; + MapController & controller; +}; diff --git a/mapeditor/inspector/towneventswidget.ui b/mapeditor/inspector/towneventswidget.ui new file mode 100644 index 000000000..cecc34149 --- /dev/null +++ b/mapeditor/inspector/towneventswidget.ui @@ -0,0 +1,93 @@ + + + TownEventsWidget + + + Qt::ApplicationModal + + + + 0 + 0 + 691 + 462 + + + + + 0 + 0 + + + + + 400 + 400 + + + + Town events + + + + + + + + Timed events + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + Remove + + + + + + + + + true + + + + + + + + diff --git a/mapeditor/inspector/townspellswidget.cpp b/mapeditor/inspector/townspellswidget.cpp new file mode 100644 index 000000000..abfd4def2 --- /dev/null +++ b/mapeditor/inspector/townspellswidget.cpp @@ -0,0 +1,166 @@ +/* + * townspellswidget.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 "townspellswidget.h" +#include "ui_townspellswidget.h" +#include "inspector.h" +#include "mapeditorroles.h" +#include "../../lib/constants/StringConstants.h" +#include "../../lib/spells/CSpellHandler.h" + +TownSpellsWidget::TownSpellsWidget(CGTownInstance & town, QWidget * parent) : + QDialog(parent), + ui(new Ui::TownSpellsWidget), + town(town) +{ + ui->setupUi(this); + + possibleSpellLists = { ui->possibleSpellList1, ui->possibleSpellList2, ui->possibleSpellList3, ui->possibleSpellList4, ui->possibleSpellList5 }; + requiredSpellLists = { ui->requiredSpellList1, ui->requiredSpellList2, ui->requiredSpellList3, ui->requiredSpellList4, ui->requiredSpellList5 }; + + std::array mageGuilds = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5}; + for (int i = 0; i < mageGuilds.size(); i++) + { + ui->tabWidget->setTabEnabled(i, vstd::contains(town.getTown()->buildings, mageGuilds[i])); + } +} + +TownSpellsWidget::~TownSpellsWidget() +{ + delete ui; +} + + +void TownSpellsWidget::obtainData() +{ + initSpellLists(); + if (vstd::contains(town.possibleSpells, SpellID::PRESET)) { + ui->customizeSpells->setChecked(true); + } + else + { + ui->customizeSpells->setChecked(false); + ui->tabWidget->setEnabled(false); + } +} + +void TownSpellsWidget::resetSpells() +{ + town.possibleSpells.clear(); + town.obligatorySpells.clear(); + for (auto spellID : VLC->spellh->getDefaultAllowed()) + town.possibleSpells.push_back(spellID); +} + +void TownSpellsWidget::initSpellLists() +{ + auto spells = VLC->spellh->getDefaultAllowed(); + for (int i = 0; i < GameConstants::SPELL_LEVELS; i++) + { + std::vector spellsByLevel; + auto getSpellsByLevel = [i](auto spellID) { + return spellID.toEntity(VLC)->getLevel() == i + 1; + }; + vstd::copy_if(spells, std::back_inserter(spellsByLevel), getSpellsByLevel); + possibleSpellLists[i]->clear(); + requiredSpellLists[i]->clear(); + for (auto spellID : spellsByLevel) + { + auto spell = spellID.toEntity(VLC); + auto * possibleItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated())); + possibleItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex())); + possibleItem->setFlags(possibleItem->flags() | Qt::ItemIsUserCheckable); + possibleItem->setCheckState(vstd::contains(town.possibleSpells, spell->getId()) ? Qt::Checked : Qt::Unchecked); + possibleSpellLists[i]->addItem(possibleItem); + + auto * requiredItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated())); + requiredItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex())); + requiredItem->setFlags(requiredItem->flags() | Qt::ItemIsUserCheckable); + requiredItem->setCheckState(vstd::contains(town.obligatorySpells, spell->getId()) ? Qt::Checked : Qt::Unchecked); + requiredSpellLists[i]->addItem(requiredItem); + } + } +} + +void TownSpellsWidget::commitChanges() +{ + if (!ui->tabWidget->isEnabled()) + { + resetSpells(); + return; + } + + auto updateTownSpellList = [](auto uiSpellLists, auto & townSpellList) { + for (const QListWidget * spellList : uiSpellLists) + { + for (int i = 0; i < spellList->count(); ++i) + { + const auto * item = spellList->item(i); + if (item->checkState() == Qt::Checked) + { + townSpellList.push_back(item->data(MapEditorRoles::SpellIDRole).toInt()); + } + } + } + }; + + town.possibleSpells.clear(); + town.obligatorySpells.clear(); + town.possibleSpells.push_back(SpellID::PRESET); + updateTownSpellList(possibleSpellLists, town.possibleSpells); + updateTownSpellList(requiredSpellLists, town.obligatorySpells); +} + +void TownSpellsWidget::on_customizeSpells_toggled(bool checked) +{ + if (checked) + { + town.possibleSpells.push_back(SpellID::PRESET); + } + else + { + resetSpells(); + } + ui->tabWidget->setEnabled(checked); + initSpellLists(); +} + +TownSpellsDelegate::TownSpellsDelegate(CGTownInstance & town) : QStyledItemDelegate(), town(town) +{ +} + +QWidget * TownSpellsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + return new TownSpellsWidget(town, parent); +} + +void TownSpellsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownSpellsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + if (auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/townspellswidget.h b/mapeditor/inspector/townspellswidget.h new file mode 100644 index 000000000..1548c25f7 --- /dev/null +++ b/mapeditor/inspector/townspellswidget.h @@ -0,0 +1,60 @@ +/* + * townspellswidget.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 +#include "../../lib/mapObjects/CGTownInstance.h" + +namespace Ui { + class TownSpellsWidget; +} + + +class TownSpellsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit TownSpellsWidget(CGTownInstance &, QWidget * parent = nullptr); + ~TownSpellsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void on_customizeSpells_toggled(bool checked); + +private: + Ui::TownSpellsWidget * ui; + + CGTownInstance & town; + + std::array possibleSpellLists; + std::array requiredSpellLists; + + void resetSpells(); + void initSpellLists(); +}; + +class TownSpellsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + TownSpellsDelegate(CGTownInstance&); + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex& index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private: + CGTownInstance& town; +}; diff --git a/mapeditor/inspector/townspellswidget.ui b/mapeditor/inspector/townspellswidget.ui new file mode 100644 index 000000000..40156178b --- /dev/null +++ b/mapeditor/inspector/townspellswidget.ui @@ -0,0 +1,304 @@ + + + TownSpellsWidget + + + Qt::NonModal + + + + 0 + 0 + 600 + 480 + + + + + 0 + 0 + + + + + 600 + 480 + + + + Spells + + + Qt::LeftToRight + + + true + + + + 10 + + + 5 + + + + + Customize spells + + + false + + + + + + + + 0 + 0 + + + + 0 + + + true + + + + + 0 + 0 + + + + Level 1 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 2 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 3 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 4 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + 0 + 0 + + + + Level 5 + + + + 12 + + + 12 + + + 12 + + + + + + + Spell that may appear in mage guild + + + + + + + Spell that must appear in mage guild + + + + + + + + + + + + + + + + + + + + diff --git a/mapeditor/mapeditorroles.h b/mapeditor/mapeditorroles.h new file mode 100644 index 000000000..3564ad598 --- /dev/null +++ b/mapeditor/mapeditorroles.h @@ -0,0 +1,20 @@ +/* + * mapeditorroles.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 "StdInc.h" + +enum MapEditorRoles +{ + TownEventRole = Qt::UserRole + 1, + PlayerIDRole, + BuildingIDRole, + SpellIDRole +}; diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h index ef29f0308..991e09037 100644 --- a/mapeditor/mapsettings/eventsettings.h +++ b/mapeditor/mapsettings/eventsettings.h @@ -15,6 +15,9 @@ namespace Ui { class EventSettings; } +QVariant toVariant(const TResources & resources); +TResources resourcesFromVariant(const QVariant & v); + class EventSettings : public AbstractSettings { Q_OBJECT diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts index 10de74c60..55b329e4f 100644 --- a/mapeditor/translation/chinese.ts +++ b/mapeditor/translation/chinese.ts @@ -715,7 +715,7 @@ MapView - + Can't place object 无法放置物体 @@ -889,38 +889,38 @@ 高级 - + Compliant 屈服的 - + Friendly 友善的 - + Aggressive 好斗的 - + Hostile 有敌意的 - + Savage 野蛮的 - - + + neutral 中立 - + UNFLAGGABLE 没有旗帜 @@ -1435,6 +1435,208 @@ Buildings 建筑 + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + 类型 + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + 通用 + + + + Event name + 事件名 + + + + Type event message text + 输入事件信息文本 + + + + Day of first occurrence + 首次发生天数 + + + + Repeat after (0 = no repeat) + 重复周期 (0 = 不重复) + + + + Affected players + 生效玩家 + + + + affects human + 人类玩家生效 + + + + affects AI + AI玩家生效 + + + + Resources + 资源 + + + + Buildings + 建筑 + + + + Creatures + 生物 + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + 计时事件 + + + + Add + 添加 + + + + Remove + 移除 + + + + Day %1 - %2 + + + + + New event + 新事件 + + + + TownSpellsWidget + + + Spells + 魔法 + + + + Customize spells + 自定义魔法 + + + + Level 1 + 1级 + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + 2级 + + + + Level 3 + 3级 + + + + Level 4 + 4级 + + + + Level 5 + 5级 + Translations @@ -1698,18 +1900,6 @@ Width 宽度 - - S (36x36) - 小(36x36) - - - M (72x72) - 中(72x72) - - - L (108x108) - 大(108x108) - XL (144x144) diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index c5ea0c7c5..95c43ba55 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -715,7 +715,7 @@ MapView - + Can't place object Nelze umístit objekt @@ -889,38 +889,38 @@ Expert - + Compliant Ochotná - + Friendly Přátelská - + Aggressive Agresivní - + Hostile Nepřátelská - + Savage Brutální - - + + neutral neutrální - + UNFLAGGABLE NEOZNAČITELNÝ @@ -1435,6 +1435,208 @@ Buildings Budovy + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Druh + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + + + + + Event name + Název události + + + + Type event message text + Zadejte text zprávy události + + + + Day of first occurrence + Den prvního výskytu + + + + Repeat after (0 = no repeat) + Opakovat po (0 = bez opak.) + + + + Affected players + Ovlivnění hráči + + + + affects human + ovlivňuje lidi + + + + affects AI + ovlivňuje AI + + + + Resources + Zdroje + + + + Buildings + Budovy + + + + Creatures + Jednotky + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + Načasované události + + + + Add + Přidat + + + + Remove + Odebrat + + + + Day %1 - %2 + + + + + New event + Nová událost + + + + TownSpellsWidget + + + Spells + Kouzla + + + + Customize spells + Přizpůsobit kouzla + + + + Level 1 + Úroveň 1 + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + Úroveň 2 + + + + Level 3 + Úroveň 3 + + + + Level 4 + Úroveň 4 + + + + Level 5 + Úroveň 5 + Translations @@ -1698,18 +1900,6 @@ Width Šířka - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 33f48c1ce..115f79aed 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -715,7 +715,7 @@ MapView - + Can't place object @@ -889,38 +889,38 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + neutral - + UNFLAGGABLE @@ -1435,6 +1435,208 @@ Buildings + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 3e165b41b..5e725a3bd 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -560,8 +560,8 @@ - Unsaved changes will be lost, are you sur? - Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? + Unsaved changes will be lost, are you sure? + @@ -715,7 +715,7 @@ MapView - + Can't place object Impossible de placer l'objet @@ -889,38 +889,38 @@ Expert - + Compliant Compérhensif - + Friendly Amical - + Aggressive Aggressif - + Hostile Hostile - + Savage Sauvage - - + + neutral neutre - + UNFLAGGABLE INCLASSABLE @@ -1435,6 +1435,208 @@ Buildings Bâtiments + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Type + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Général + + + + Event name + Nom de l'évènement + + + + Type event message text + Taper le message d'évènement + + + + Day of first occurrence + Jour de la première occurrence + + + + Repeat after (0 = no repeat) + Récurrence (0 = pas de récurrence) + + + + Affected players + Joueurs affectés + + + + affects human + afttecte les joueurs + + + + affects AI + affecte l'ordinateur + + + + Resources + Resources + + + + Buildings + Bâtiments + + + + Creatures + Créatures + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + Evenements timés + + + + Add + Ajouter + + + + Remove + Supprimer + + + + Day %1 - %2 + + + + + New event + Nouvel évènement + + + + TownSpellsWidget + + + Spells + Sorts + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1698,18 +1900,6 @@ Width Largeur - - S (36x36) - Petite (36x36) - - - M (72x72) - Moyenne (72x72) - - - L (108x108) - Grande (108x108) - XL (144x144) diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 21ca1043e..1d5d186be 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -715,7 +715,7 @@ MapView - + Can't place object Objekt kann nicht platziert werden @@ -889,38 +889,38 @@ Experte - + Compliant Konform - + Friendly Freundlich - + Aggressive Aggressiv - + Hostile Feindlich - + Savage Wild - - + + neutral neutral - + UNFLAGGABLE UNFLAGGBAR @@ -1435,6 +1435,208 @@ Buildings Gebäude + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Typ + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Allgemein + + + + Event name + Name des Ereignisses + + + + Type event message text + Ereignistext eingeben + + + + Day of first occurrence + Tag des ersten Auftretens + + + + Repeat after (0 = no repeat) + Wiederholung nach (0 = keine Wiederholung) + + + + Affected players + Betroffene Spieler + + + + affects human + beeinflusst Menschen + + + + affects AI + beeinflusst KI + + + + Resources + Ressourcen + + + + Buildings + Gebäude + + + + Creatures + Kreaturen + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + Zeitlich begrenzte Ereignisse + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Day %1 - %2 + + + + + New event + Neues Ereignis + + + + TownSpellsWidget + + + Spells + Zaubersprüche + + + + Customize spells + Zaubersprüche anpassen + + + + Level 1 + Level 1 + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + Level 2 + + + + Level 3 + Level 3 + + + + Level 4 + Level 4 + + + + Level 5 + Level 5 + Translations @@ -1698,18 +1900,6 @@ Width Breite - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index faf60b6cb..9b57bcf7a 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -715,7 +715,7 @@ MapView - + Can't place object Nie można umieścić obiektu @@ -889,38 +889,38 @@ Ekspert - + Compliant Przyjazny - + Friendly Przychylny - + Aggressive Agresywny - + Hostile Wrogi - + Savage Nienawistny - - + + neutral neutralny - + UNFLAGGABLE NIEOFLAGOWYWALNY @@ -1435,6 +1435,208 @@ Buildings Budynki + + + Build all + Zbuduj wsyzstkie + + + + Demolish all + Zburz wszystkie + + + + Enable all + Włącz wszystkie + + + + Disable all + Wyłącz wszystkie + + + + Type + Typ + + + + Enabled + Włączony + + + + Built + Zbudowany + + + + TownEventDialog + + + Town event + Zdarzenie miasta + + + + General + Ogólne + + + + Event name + Nazwa zdarzenia + + + + Type event message text + Wpisz treść komunikatu zdarzenia + + + + Day of first occurrence + Dzień pierwszego wystąpienia + + + + Repeat after (0 = no repeat) + Powtórz po... (0 = nigdy) + + + + Affected players + Dotyczy graczy + + + + affects human + dotyczy graczy ludzkich + + + + affects AI + dotyczy graczy AI + + + + Resources + Zasoby + + + + Buildings + Budynki + + + + Creatures + Stworzenia + + + + OK + OK + + + + Creature level %1 / Creature level %1 Upgrade + Stworzenie poziomu %1 / Ulepszone stworzenie poziomu %1 + + + + Day %1 - %2 + Dzień %1 - %2 + + + + TownEventsWidget + + + Town events + Zdarzenia miasta + + + + Timed events + Zdarzenia czasowe + + + + Add + Dodaj + + + + Remove + Usuń + + + + Day %1 - %2 + Dzień %1 - %2 + + + + New event + Nowe zdarzenie + + + + TownSpellsWidget + + + Spells + Zaklęcia + + + + Customize spells + Własne zaklęcia + + + + Level 1 + Poziom 1 + + + + + + + + Spell that may appear in mage guild + Zaklecia, które mogą pojawić się w gildii magów + + + + + + + + Spell that must appear in mage guild + Zaklecia, które muszą pojawić się w gildii magów + + + + Level 2 + Poziom 2 + + + + Level 3 + Poziom 3 + + + + Level 4 + Poziom 4 + + + + Level 5 + Poziom 5 + Translations @@ -1698,18 +1900,6 @@ Width Szerokość - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) diff --git a/mapeditor/translation/portuguese.ts b/mapeditor/translation/portuguese.ts index e6ca56dd1..a5069bb28 100644 --- a/mapeditor/translation/portuguese.ts +++ b/mapeditor/translation/portuguese.ts @@ -715,7 +715,7 @@ MapView - + Can't place object Não é possível colocar objeto @@ -889,38 +889,38 @@ Experiente - + Compliant Conformista - + Friendly Amigável - + Aggressive Agressivo - + Hostile Hostil - + Savage Selvagem - - + + neutral neutro - + UNFLAGGABLE NÃO TEM BANDEIRA @@ -1435,6 +1435,208 @@ Buildings Estruturas + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Tipo + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Geral + + + + Event name + Nome do evento + + + + Type event message text + Introduza o texto da mensagem do evento + + + + Day of first occurrence + Dia da primeira ocorrência + + + + Repeat after (0 = no repeat) + Repetir após (0 = não repetir) + + + + Affected players + Jogadores afetados + + + + affects human + afeta humano + + + + affects AI + afeta IA + + + + Resources + Recursos + + + + Buildings + Estruturas + + + + Creatures + Criaturas + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + Eventos Temporizados + + + + Add + Adicionar + + + + Remove + Remover + + + + Day %1 - %2 + + + + + New event + Novo Evento + + + + TownSpellsWidget + + + Spells + Feitiços + + + + Customize spells + Personalizar feitiços + + + + Level 1 + Nível 1 + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + Nível 2 + + + + Level 3 + Nível 3 + + + + Level 4 + Nível 4 + + + + Level 5 + Nível 5 + Translations @@ -1698,18 +1900,6 @@ Width Largura - - S (36x36) - P (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - G (108x108) - XL (144x144) diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index b32fe31d8..db0e4ee99 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -715,7 +715,7 @@ MapView - + Can't place object @@ -889,38 +889,38 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + neutral - + UNFLAGGABLE @@ -1435,6 +1435,208 @@ Buildings Постройки + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Тип + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Общее + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Постройки + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Заклинания + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1698,18 +1900,6 @@ Width Ширина - - S (36x36) - Мал. (36x36) - - - M (72x72) - Ср. (72x72) - - - L (108x108) - Бол. (108x108) - XL (144x144) diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 17ad2c24b..ae0b04a25 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -715,7 +715,7 @@ MapView - + Can't place object @@ -889,38 +889,38 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + neutral - + UNFLAGGABLE @@ -1435,6 +1435,208 @@ Buildings Edificios + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Tipo + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + General + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Edificios + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Hechizos + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1698,18 +1900,6 @@ Width Ancho - - S (36x36) - S (36x36) - - - M (72x72) - M (72x72) - - - L (108x108) - L (108x108) - XL (144x144) diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index f280967f8..7ffd2aa0b 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -715,7 +715,7 @@ MapView - + Can't place object @@ -889,38 +889,38 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + neutral - + UNFLAGGABLE @@ -1435,6 +1435,208 @@ Buildings Будівлі + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Тип + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Загальний + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Будівлі + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Закляття + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1698,18 +1900,6 @@ Width Ширина - - S (36x36) - М (36x36) - - - M (72x72) - С (72x72) - - - L (108x108) - В (108x108) - XL (144x144) diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 32f286fae..4d1b78974 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -715,7 +715,7 @@ MapView - + Can't place object Không thể đặt vật thể @@ -889,38 +889,38 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + neutral - + UNFLAGGABLE @@ -1435,6 +1435,208 @@ Buildings Công trình + + + Build all + + + + + Demolish all + + + + + Enable all + + + + + Disable all + + + + + Type + Loại + + + + Enabled + + + + + Built + + + + + TownEventDialog + + + Town event + + + + + General + Chung + + + + Event name + + + + + Type event message text + + + + + Day of first occurrence + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + affects human + + + + + affects AI + + + + + Resources + + + + + Buildings + Công trình + + + + Creatures + + + + + OK + + + + + Creature level %1 / Creature level %1 Upgrade + + + + + Day %1 - %2 + + + + + TownEventsWidget + + + Town events + + + + + Timed events + + + + + Add + + + + + Remove + + + + + Day %1 - %2 + + + + + New event + + + + + TownSpellsWidget + + + Spells + Phép + + + + Customize spells + + + + + Level 1 + + + + + + + + + Spell that may appear in mage guild + + + + + + + + + Spell that must appear in mage guild + + + + + Level 2 + + + + + Level 3 + + + + + Level 4 + + + + + Level 5 + + Translations @@ -1698,18 +1900,6 @@ Width Rộng - - S (36x36) - Nhỏ (36x36) - - - M (72x72) - Vừa (72x72) - - - L (108x108) - Lớn (108x108) - XL (144x144) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index ddffc86b6..3b1b138a9 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -669,6 +669,19 @@ void CGameHandler::onPlayerTurnEnded(PlayerColor which) heroPool->onNewWeek(which); } +void CGameHandler::addStatistics() +{ + for (auto & elem : gs->players) + { + if (elem.first == PlayerColor::NEUTRAL || !elem.first.isValidPlayer()) + continue; + + auto data = StatisticDataSet::createEntry(&elem.second, gs); + + gameState()->statistic.add(data); + } +} + void CGameHandler::onNewTurn() { logGlobal->trace("Turn %d", gs->day+1); @@ -694,6 +707,15 @@ void CGameHandler::onNewTurn() } } + for (auto & player : gs->players) + { + if (player.second.status != EPlayerStatus::INGAME) + continue; + + if (player.second.heroes.empty() && player.second.towns.empty()) + throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString()); + } + if (newWeek && !firstTurn) { n.specialWeek = NewTurn::NORMAL; @@ -1004,6 +1026,8 @@ void CGameHandler::onNewTurn() } synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that + + addStatistics(); } void CGameHandler::start(bool resume) @@ -1336,6 +1360,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme turnTimerHandler->setEndTurnAllowed(h->getOwner(), !movingOntoWater && !movingOntoObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); + gs->statistic.accumulatedValues[asker].movementPointsUsed += tmh.movePoints; return true; } } @@ -2448,7 +2473,10 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, //Take cost if(!force) + { giveResources(t->tempOwner, -requestedBuilding->resources); + gs->statistic.accumulatedValues[t->tempOwner].spentResourcesForBuildings += requestedBuilding->resources; + } //We know what has been built, apply changes. Do this as final step to properly update town window sendAndApply(&ns); @@ -2569,7 +2597,9 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } //recruit - giveResources(army->tempOwner, -(c->getFullRecruitCost() * cram)); + TResources cost = (c->getFullRecruitCost() * cram); + giveResources(army->tempOwner, -cost); + gs->statistic.accumulatedValues[army->tempOwner].spentResourcesForArmy += cost; SetAvailableCreatures sac; sac.tid = objid; @@ -2622,6 +2652,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI //take resources giveResources(player, -totalCost); + gs->statistic.accumulatedValues[player].spentResourcesForArmy += totalCost; //upgrade creature changeStackType(StackLocation(obj, pos), upgID.toCreature()); @@ -3246,6 +3277,9 @@ bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, Play giveResource(player, toSell, -b1 * amountToBoy); giveResource(player, toBuy, b2 * amountToBoy); + gs->statistic.accumulatedValues[player].tradeVolume[toSell] += -b1 * amountToBoy; + gs->statistic.accumulatedValues[player].tradeVolume[toBuy] += b2 * amountToBoy; + return true; } @@ -3434,7 +3468,7 @@ void CGameHandler::handleTimeEvents() void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) { - town->events.sort(evntCmp); + std::sort(town->events.begin(), town->events.end(), evntCmp); while(town->events.size() && town->events.front().firstOccurrence == gs->day) { PlayerColor player = town->tempOwner; @@ -3495,7 +3529,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (ev.nextOccurrence) { - town->events.pop_front(); + town->events.erase(town->events.begin()); ev.firstOccurrence += ev.nextOccurrence; auto it = town->events.begin(); @@ -3505,7 +3539,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) } else { - town->events.pop_front(); + town->events.erase(town->events.begin()); } } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 91e1b2d15..d250f6b03 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -227,6 +227,7 @@ public: void onPlayerTurnStarted(PlayerColor which); void onPlayerTurnEnded(PlayerColor which); void onNewTurn(); + void addStatistics(); void handleTimeEvents(); void handleTownEvents(CGTownInstance *town, NewTurn &n); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 4a8e9b4f0..945ee960c 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -496,7 +496,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con else destStack = battle.battleGetUnitByPos(target.at(0).hexValue); - if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == BonusSubtypeID()) + if(stack == nullptr || destStack == nullptr || !healerAbility || !healerAbility->subtype.hasValue()) { gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); } @@ -973,7 +973,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const } std::shared_ptr bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? + if(bonus && ranged && bonus->subtype.hasValue()) //TODO: make it work in melee? { //this is need for displaying hit animation bat.flags |= BattleAttack::SPELL_LIKE; @@ -1267,7 +1267,7 @@ void BattleActionProcessor::handleDeathStare(const CBattleInfoCallback & battle, vstd::amin(chanceToKill, 1); //cap at 100% int killedCreatures = gameHandler->getRandomGenerator().nextBinomialInt(attacker->getCount(), chanceToKill); - int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100; + int maxToKill = vstd::divideAndCeil(attacker->getCount() * singleCreatureKillChancePercent, 100); vstd::amin(killedCreatures, maxToKill); killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 122ec9a7b..46d5f3954 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -447,16 +447,16 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) addArtifactToTransfer(packCommander, artSlot.first, artSlot.second.getArt()); sendArtifacts(packCommander); } - } - auto armyObj = battle.battleGetArmyObject(battle.otherSide(battleResult->winner)); - for(const auto & armySlot : armyObj->stacks) - { - BulkMoveArtifacts packsArmy(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false); - packsArmy.srcArtHolder = armyObj->id; - packsArmy.srcCreature = armySlot.first; - for(const auto & artSlot : armySlot.second->artifactsWorn) - addArtifactToTransfer(packsArmy, artSlot.first, armySlot.second->getArt(artSlot.first)); - sendArtifacts(packsArmy); + auto armyObj = battle.battleGetArmyObject(battle.otherSide(battleResult->winner)); + for(const auto & armySlot : armyObj->stacks) + { + BulkMoveArtifacts packsArmy(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false); + packsArmy.srcArtHolder = armyObj->id; + packsArmy.srcCreature = armySlot.first; + for(const auto & artSlot : armySlot.second->artifactsWorn) + addArtifactToTransfer(packsArmy, artSlot.first, armySlot.second->getArt(artSlot.first)); + sendArtifacts(packsArmy); + } } // Display loot if(!arts.empty()) @@ -497,6 +497,22 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) gameHandler->sendAndApply(&ro); } + // add statistic + if(battle.sideToPlayer(0) == PlayerColor::NEUTRAL || battle.sideToPlayer(1) == PlayerColor::NEUTRAL) + { + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(0)].numBattlesNeutral++; + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(1)].numBattlesNeutral++; + if(!finishingBattle->isDraw()) + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesNeutral++; + } + else + { + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(0)].numBattlesPlayer++; + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(1)].numBattlesPlayer++; + if(!finishingBattle->isDraw()) + gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesPlayer++; + } + BattleResultAccepted raccepted; raccepted.battleID = battle.getBattle()->getBattleID(); raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(BattleSide::ATTACKER)); @@ -556,10 +572,16 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const gameHandler->checkVictoryLossConditions(playerColors); if (result.result == EBattleResult::SURRENDER) + { + gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroSurrendered++; gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + } if (result.result == EBattleResult::ESCAPE) + { + gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroEscaped++; gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + } if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 4ed115559..6ff2b93f5 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -29,6 +29,7 @@ #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/StackLocation.h" #include "../../lib/serializer/Connection.h" +#include "../lib/VCMIDirs.h" PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler) :gameHandler(gameHandler) @@ -133,12 +134,30 @@ void PlayerMessageProcessor::commandCheaters(PlayerColor player, const std::vect broadcastSystemMessage("No cheaters registered!"); } +void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vector & words) +{ + bool isHost = gameHandler->gameLobby()->isPlayerHost(player); + if(!isHost) + return; + + const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic"; + boost::filesystem::create_directories(outPath); + + const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv"); + std::ofstream file(filePath.c_str()); + std::string csv = gameHandler->gameState()->statistic.toCsv(); + file << csv; + + broadcastSystemMessage("Statistic files can be found in " + outPath.string() + " directory\n"); +} + void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector & words) { broadcastSystemMessage("Available commands to host:"); broadcastSystemMessage("'!exit' - immediately ends current game"); broadcastSystemMessage("'!kick ' - kick specified player from the game"); broadcastSystemMessage("'!save ' - save game under specified filename"); + broadcastSystemMessage("'!statistic' - save game statistics as csv file"); broadcastSystemMessage("Available commands to all players:"); broadcastSystemMessage("'!help' - display this help"); broadcastSystemMessage("'!cheaters' - list players that entered cheat command during game"); @@ -319,6 +338,8 @@ void PlayerMessageProcessor::handleCommand(PlayerColor player, const std::string commandSave(player, words); if(words[0] == "!cheaters") commandCheaters(player, words); + if(words[0] == "!statistic") + commandStatistic(player, words); } void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero) diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index 810c25188..8671817e8 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -62,6 +62,7 @@ class PlayerMessageProcessor void commandKick(PlayerColor player, const std::vector & words); void commandSave(PlayerColor player, const std::vector & words); void commandCheaters(PlayerColor player, const std::vector & words); + void commandStatistic(PlayerColor player, const std::vector & words); void commandHelp(PlayerColor player, const std::vector & words); void commandVote(PlayerColor player, const std::vector & words); diff --git a/test/googletest b/test/googletest index b796f7d44..b514bdc89 160000 --- a/test/googletest +++ b/test/googletest @@ -1 +1 @@ -Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1 +Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59