From 60afbbe20f1802b3e75c25fb94011c0a00b54572 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:10:14 +0200 Subject: [PATCH] Necromancy & Eagle eye infowindows --- client/ArtifactsUIController.cpp | 16 ---- client/ArtifactsUIController.h | 3 - client/CMakeLists.txt | 2 + client/NetPacksClient.cpp | 27 +++++-- client/UIHelper.cpp | 93 ++++++++++++++++++++++++ client/UIHelper.h | 35 +++++++++ client/windows/CWindowWithArtifacts.cpp | 1 - lib/mapObjects/CGHeroInstance.cpp | 28 +------ lib/mapObjects/CGHeroInstance.h | 1 - lib/networkPacks/NetPacksLib.cpp | 2 + lib/networkPacks/PacksForClientBattle.h | 8 +- server/battles/BattleResultProcessor.cpp | 76 ++++--------------- server/battles/BattleResultProcessor.h | 1 - 13 files changed, 175 insertions(+), 118 deletions(-) create mode 100644 client/UIHelper.cpp create mode 100644 client/UIHelper.h diff --git a/client/ArtifactsUIController.cpp b/client/ArtifactsUIController.cpp index e7708293a..3d15f1495 100644 --- a/client/ArtifactsUIController.cpp +++ b/client/ArtifactsUIController.cpp @@ -167,19 +167,3 @@ void ArtifactsUIController::artifactDisassembled() for(const auto & artWin : ENGINE->windows().findWindows()) artWin->update(); } - -std::vector ArtifactsUIController::getMovedComponents(const CArtifactSet & artSet, const std::vector & movedPack) const -{ - std::vector components; - for(const auto & artMoveInfo : movedPack) - { - const auto art = artSet.getArt(artMoveInfo.dstPos); - assert(art); - - if(art->isScroll()) - components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); - else - components.emplace_back(ComponentType::ARTIFACT, art->getTypeId()); - } - return components; -} diff --git a/client/ArtifactsUIController.h b/client/ArtifactsUIController.h index 87c509b2f..122f66e04 100644 --- a/client/ArtifactsUIController.h +++ b/client/ArtifactsUIController.h @@ -14,9 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -class CArtifactSet; class CGHeroInstance; -struct Component; VCMI_LIB_NAMESPACE_END @@ -38,5 +36,4 @@ public: void bulkArtMovementStart(size_t totalNumOfArts, size_t possibleAssemblyNumOfArts); void artifactAssembled(); void artifactDisassembled(); - std::vector getMovedComponents(const CArtifactSet & artSet, const std::vector & movedPack) const; }; diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 566db8f25..afc94eb49 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -191,6 +191,7 @@ set(vcmiclientcommon_SRCS NetPacksClient.cpp NetPacksLobbyClient.cpp ServerRunner.cpp + UIHelper.cpp ) set(vcmiclientcommon_HEADERS @@ -410,6 +411,7 @@ set(vcmiclientcommon_HEADERS LobbyClientNetPackVisitors.h ServerRunner.h resource.h + UIHelper.h ) if(APPLE_IOS) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index e36cb88d9..53d04c5fd 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -26,6 +26,7 @@ #include "CMT.h" #include "GameChatHandler.h" #include "CServerHandler.h" +#include "UIHelper.h" #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" @@ -851,22 +852,34 @@ void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) { - InfoWindow win; - win.player = pack.victor; - win.text.appendLocalString(EMetaText::GENERAL_TXT, 30); + if(!pack.learnedSpells.spells.empty()) + { + const auto hero = GAME->interface()->cb->getHero(pack.learnedSpells.hid); + assert(hero); + callInterfaceIfPresent(cl, pack.victor, &CGameInterface::showInfoDialog, EInfoWindowMode::MODAL, + UIHelper::getEagleEyeInfoWindowText(*hero, pack.learnedSpells.spells), UIHelper::getSpellsComponents(pack.learnedSpells.spells), soundBase::soundID(0)); + } + const auto artSet = GAME->interface()->cb->getArtSet(ArtifactLocation(pack.artifacts.front().dstArtHolder)); assert(artSet); + std::vector artComponents; for(const auto & artPack : pack.artifacts) { - auto packComponents = GAME->interface()->artifactController->getMovedComponents(*artSet, artPack.artsPack0); - win.components.insert(win.components.end(), std::make_move_iterator(packComponents.begin()), std::make_move_iterator(packComponents.end())); + auto packComponents = UIHelper::getArtifactsComponents(*artSet, artPack.artsPack0); + artComponents.insert(artComponents.end(), std::make_move_iterator(packComponents.begin()), std::make_move_iterator(packComponents.end())); } - if(!win.components.empty()) - visitInfoWindow(win); + if(!artComponents.empty()) + callInterfaceIfPresent(cl, pack.victor, &CGameInterface::showInfoDialog, EInfoWindowMode::MODAL, UIHelper::getArtifactsInfoWindowText(), + artComponents, soundBase::soundID(0)); for(auto & artPack : pack.artifacts) visitBulkMoveArtifacts(artPack); + if(pack.raisedStack.getCreature()) + callInterfaceIfPresent(cl, pack.victor, &CGameInterface::showInfoDialog, EInfoWindowMode::AUTO, + UIHelper::getNecromancyInfoWindowText(pack.raisedStack), std::vector{Component(ComponentType::CREATURE, pack.raisedStack.getId(), + pack.raisedStack.count)}, UIHelper::getNecromancyInfoWindowSound()); + callInterfaceIfPresent(cl, pack.victor, &IGameEventsReceiver::battleResultsApplied); callInterfaceIfPresent(cl, pack.loser, &IGameEventsReceiver::battleResultsApplied); callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); diff --git a/client/UIHelper.cpp b/client/UIHelper.cpp new file mode 100644 index 000000000..5e29aa843 --- /dev/null +++ b/client/UIHelper.cpp @@ -0,0 +1,93 @@ +/* + * UIHelper.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 "UIHelper.h" + +#include "widgets/CComponent.h" + +#include "../lib/CArtHandler.h" +#include "../lib/CArtifactInstance.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/networkPacks/ArtifactLocation.h" +#include "../lib/CRandomGenerator.h" +#include "../lib/CCreatureSet.h" + +std::vector UIHelper::getArtifactsComponents(const CArtifactSet & artSet, const std::vector & movedPack) +{ + std::vector components; + for(const auto & artMoveInfo : movedPack) + { + const auto art = artSet.getArt(artMoveInfo.dstPos); + assert(art); + + if(art->isScroll()) + components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); + else + components.emplace_back(ComponentType::ARTIFACT, art->getTypeId()); + } + return components; +} + +std::vector UIHelper::getSpellsComponents(const std::set & spells) +{ + std::vector components; + for(const auto & spell : spells) + components.emplace_back(ComponentType::SPELL, spell); + return components; +} + +soundBase::soundID UIHelper::getNecromancyInfoWindowSound() +{ + return soundBase::soundID(soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6)); +} + +std::string UIHelper::getNecromancyInfoWindowText(const CStackBasicDescriptor & stack) +{ + MetaString text; + if(stack.count > 1) // Practicing the dark arts of necromancy, ... (plural) + { + text.appendLocalString(EMetaText::GENERAL_TXT, 145); + text.replaceNumber(stack.count); + } + else // Practicing the dark arts of necromancy, ... (singular) + { + text.appendLocalString(EMetaText::GENERAL_TXT, 146); + } + text.replaceName(stack); + return text.toString(); +} + +std::string UIHelper::getArtifactsInfoWindowText() +{ + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 30); + return text.toString(); +} + +std::string UIHelper::getEagleEyeInfoWindowText(const CGHeroInstance & hero, const std::set & spells) +{ + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 221); // Through eagle-eyed observation, %s is able to learn %s + text.replaceRawString(hero.getNameTranslated()); + + auto curSpell = spells.begin(); + text.replaceName(*curSpell++); + for(int i = 1; i < spells.size(); i++, curSpell++) + { + if(i + 1 == spells.size()) + text.appendLocalString(EMetaText::GENERAL_TXT, 141); // " and " + else + text.appendRawString(", "); + text.appendName(*curSpell); + } + text.appendRawString("."); + return text.toString(); +} diff --git a/client/UIHelper.h b/client/UIHelper.h new file mode 100644 index 000000000..d46923f61 --- /dev/null +++ b/client/UIHelper.h @@ -0,0 +1,35 @@ +/* + * UIHelper.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 "../lib/CSoundBase.h" +#include "../lib/texts/MetaString.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct MoveArtifactInfo; +struct Component; +class CArtifactSet; +class CGHeroInstance; +class CStackBasicDescriptor; + +VCMI_LIB_NAMESPACE_END + +namespace UIHelper +{ + std::vector getArtifactsComponents(const CArtifactSet & artSet, const std::vector & movedPack); + std::vector getSpellsComponents(const std::set & spells); + soundBase::soundID getNecromancyInfoWindowSound(); + std::string getNecromancyInfoWindowText(const CStackBasicDescriptor & stack); + std::string getArtifactsInfoWindowText(); + std::string getEagleEyeInfoWindowText(const CGHeroInstance & hero, const std::set & spells); +} diff --git a/client/windows/CWindowWithArtifacts.cpp b/client/windows/CWindowWithArtifacts.cpp index 3c206c43a..6f680f378 100644 --- a/client/windows/CWindowWithArtifacts.cpp +++ b/client/windows/CWindowWithArtifacts.cpp @@ -12,7 +12,6 @@ #include "CHeroWindow.h" #include "CSpellWindow.h" -#include "CExchangeWindow.h" #include "CHeroBackpackWindow.h" #include "../GameEngine.h" diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 3a3575738..5a92e37a9 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -968,7 +968,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b double necromancySkill = valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE) / 100.0; const ui8 necromancyLevel = valOfBonuses(BonusType::IMPROVED_NECROMANCY); vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all... - const std::map casualties {}; //= battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)]; + const std::map &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)]; // figure out what to raise - pick strongest creature meeting requirements CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode int requiredCasualtyLevel = 1; @@ -1037,32 +1037,6 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b return CStackBasicDescriptor(); } -/** - * Show the necromancy dialog with information about units raised. - * @param raisedStack Pair where the first element represents ID of the raised creature - * and the second element the amount. - */ -void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.soundID = soundBase::pickup01 + rand.nextInt(6); - iw.player = tempOwner; - iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count); - - if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural) - { - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 145); - iw.text.replaceNumber(raisedStack.count); - } - else // Practicing the dark arts of necromancy, ... (singular) - { - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146); - } - iw.text.replaceName(raisedStack); - - cb->showInfoDialog(&iw); -} /* int3 CGHeroInstance::getSightCenter() const { diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 59c431671..cc06977b5 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -241,7 +241,6 @@ public: int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const; - void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; EDiggingStatus diggingStatus() const; ////////////////////////////////////////////////////////////////////////// diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index e9ef5d5be..ec276ab9e 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2306,6 +2306,8 @@ void BattleUnitsChanged::applyBattle(IBattleState * battleState) void BattleResultsApplied::applyGs(CGameState * gs) { + learnedSpells.applyGs(gs); + for(auto & artPack : artifacts) artPack.applyGs(gs); diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index 667b020d6..41a24bcdb 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -96,8 +96,8 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient struct HeroBattleResults { HeroBattleResults() - : armyId(ObjectInstanceID::NONE) - , heroId(ObjectInstanceID::NONE) + : heroId(ObjectInstanceID::NONE) + , armyId(ObjectInstanceID::NONE) , exp(0) {} ObjectInstanceID heroId; @@ -426,7 +426,9 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient BattleID battleID = BattleID::NONE; PlayerColor victor; PlayerColor loser; + ChangeSpells learnedSpells; std::vector artifacts; + CStackBasicDescriptor raisedStack; void visitTyped(ICPackVisitor & visitor) override; void applyGs(CGameState *gs) override; @@ -435,7 +437,9 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient h & battleID; h & victor; h & loser; + h & learnedSpells; h & artifacts; + h & raisedStack; assert(battleID != BattleID::NONE); } }; diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index ec244f37e..97eb6f317 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -21,17 +21,12 @@ #include "../../lib/CStack.h" #include "../../lib/CPlayerState.h" #include "../../lib/IGameSettings.h" -#include "../../lib/battle/CBattleInfoCallback.h" -#include "../../lib/battle/IBattleState.h" #include "../../lib/battle/SideInBattle.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForClientBattle.h" -#include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/spells/CSpellHandler.h" -#include - #include BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner, CGameHandler * newGameHandler) @@ -374,7 +369,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) raccepted.winnerSide = finishingBattle->winnerSide; gameHandler->sendAndApply(raccepted); - //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query + //--> continuation (battleFinalize) occurs after level-up gameHandler->queries are handled or on removing query } void BattleResultProcessor::battleFinalize(const BattleID & battleID, const BattleResult & result) @@ -399,9 +394,9 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt // Still, it looks like a hole. const auto battle = std::find_if(gameHandler->gameState()->currentBattles.begin(), gameHandler->gameState()->currentBattles.end(), - [battleID](const auto & battle) + [battleID](const auto & desiredBattle) { - return battle->battleID == battleID; + return desiredBattle->battleID == battleID; }); assert(battle != gameHandler->gameState()->currentBattles.end()); @@ -412,58 +407,22 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt // Eagle Eye handling if(!finishingBattle->isDraw() && winnerHero) { - ChangeSpells spells; - if(auto eagleEyeLevel = winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT)) { - auto eagleEyeChance = winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); + resultsApplied.learnedSpells.learn = 1; + resultsApplied.learnedSpells.hid = finishingBattle->winnerId; for(const auto & spellId : (*battle)->getUsedSpells(CBattleInfoEssentials::otherSide(result.winner))) { - auto spell = spellId.toEntity(LIBRARY->spells()); + const auto spell = spellId.toEntity(LIBRARY->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !winnerHero->spellbookContainsSpell(spell->getId()) - && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) + && gameHandler->getRandomGenerator().nextInt(99) < winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE)) { - spells.spells.insert(spell->getId()); + resultsApplied.learnedSpells.spells.insert(spell->getId()); } } } - - if(!spells.spells.empty()) - { - spells.learn = 1; - spells.hid = finishingBattle->winnerId; - - InfoWindow iw; - iw.player = winnerHero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.replaceRawString(winnerHero->getNameTranslated()); - - std::ostringstream names; - for(int i = 0; i < spells.spells.size(); i++) - { - names << "%s"; - if(i < spells.spells.size() - 2) - names << ", "; - else if(i < spells.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.replaceRawString(names.str()); - - auto it = spells.spells.begin(); - for(int i = 0; i < spells.spells.size(); i++, it++) - { - iw.text.replaceName(*it); - if(i == spells.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(ComponentType::SPELL, *it); - } - gameHandler->sendAndApply(iw); - gameHandler->sendAndApply(spells); - } } // Artifacts handling @@ -519,16 +478,14 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt } } - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = winnerHero ? winnerHero->calculateNecromancy(result) : CStackBasicDescriptor(); - // Give raised units to winner and show dialog, if any were raised, - // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.getCreature() ? winnerHero->getSlotFor(raisedStack.getCreature()) : SlotID(); - - if (necroSlot != SlotID() && !finishingBattle->isDraw()) + // Necromancy handling + // Give raised units to winner, if any were raised, units will be given after casualties are taken + if(winnerHero) { - winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); - gameHandler->addToSlot(StackLocation(finishingBattle->winnerId, necroSlot), raisedStack.getCreature(), raisedStack.count); + resultsApplied.raisedStack = winnerHero->calculateNecromancy(result); + const SlotID necroSlot = resultsApplied.raisedStack.getCreature() ? winnerHero->getSlotFor(resultsApplied.raisedStack.getCreature()) : SlotID(); + if(necroSlot != SlotID() && !finishingBattle->isDraw()) + gameHandler->addToSlot(StackLocation(finishingBattle->winnerId, necroSlot), resultsApplied.raisedStack.getCreature(), resultsApplied.raisedStack.count); } resultsApplied.battleID = battleID; @@ -537,8 +494,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt gameHandler->sendAndApply(resultsApplied); //handle victory/loss of engaged players - std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; - gameHandler->checkVictoryLossConditions(playerColors); + gameHandler->checkVictoryLossConditions({finishingBattle->loser, finishingBattle->victor}); if (result.result == EBattleResult::SURRENDER) { diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index d09929024..b5f1e6e2c 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -17,7 +17,6 @@ VCMI_LIB_NAMESPACE_BEGIN struct SideInBattle; struct BattleResult; -struct BulkMoveArtifacts; class CBattleInfoCallback; class CGHeroInstance; class CArmedInstance;