From 6d9385b8bd045be4d355dcc207b4f746f2ffce6c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:31:17 +0200 Subject: [PATCH 01/25] outro for RoE --- client/CServerHandler.cpp | 12 +++++++++++- client/NetPacksLobbyClient.cpp | 15 ++++++++++++--- client/lobby/CBonusSelection.cpp | 19 +++++++------------ client/lobby/CBonusSelection.h | 7 ++++--- config/campaignOverrides.json | 19 +++++++++++-------- docs/modders/Campaign_Format.md | 3 ++- lib/campaign/CampaignHandler.cpp | 3 ++- lib/campaign/CampaignState.cpp | 15 +++++++++++---- lib/campaign/CampaignState.h | 10 +++++++--- lib/serializer/ESerializationVersion.h | 3 ++- 10 files changed, 69 insertions(+), 37 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 744c51f2b..4c11c21f0 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -21,7 +21,9 @@ #include "globalLobby/GlobalLobbyClient.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" +#include "lobby/CBonusSelection.h" #include "windows/InfoWindows.h" +#include "media/CMusicHandler.h" #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" @@ -704,7 +706,15 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); + if(!ourCampaign->getOutroVideo().empty()) + { + CCS->musich->stopMusic(); + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), [campaignScoreCalculator, statistic](){ + GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); + }); + } + else + GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); } }; diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 5d81fbdef..b9b54296e 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -31,6 +31,7 @@ #include "gui/WindowHandler.h" #include "widgets/Buttons.h" #include "widgets/TextControls.h" +#include "media/CMusicHandler.h" #include "../lib/CConfigHandler.h" #include "../lib/texts/CGeneralTextHandler.h" @@ -203,11 +204,19 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) { - lobby->bonusSel = std::make_shared(); + auto bonusSel = std::make_shared(); + lobby->bonusSel = bonusSel; if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty()) - GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getIntroVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getIntroVideoRim(), lobby->bonusSel); + { + CCS->musich->stopMusic(); + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), [bonusSel](){ + if(!CSH->si->campState->getMusic().empty()) + CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); + GH.windows().pushWindow(bonusSel); + }); + } else - GH.windows().pushWindow(lobby->bonusSel); + GH.windows().pushWindow(bonusSel); } if(lobby->bonusSel) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index e6de278b1..c43532ace 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -59,8 +59,8 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CampaignIntroVideo::CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr bonusSel) - : CWindowObject(BORDERED), bonusSel(bonusSel) +CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, std::function closeCb) + : CWindowObject(BORDERED), closeCb(closeCb) { OBJECT_CONSTRUCTION; @@ -70,26 +70,21 @@ CampaignIntroVideo::CampaignIntroVideo(VideoPath video, ImagePath rim, std::shar videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(); }); setBackground(rim); - - CCS->musich->stopMusic(); } -void CampaignIntroVideo::exit() +void CampaignRimVideo::exit() { close(); - - if (!CSH->si->campState->getMusic().empty()) - CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); - - GH.windows().pushWindow(bonusSel); + if(closeCb) + closeCb(); } -void CampaignIntroVideo::clickPressed(const Point & cursorPosition) +void CampaignRimVideo::clickPressed(const Point & cursorPosition) { exit(); } -void CampaignIntroVideo::keyPressed(EShortcut key) +void CampaignRimVideo::keyPressed(EShortcut key) { exit(); } diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index e6b486390..176429f8d 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -33,14 +33,15 @@ class VideoWidgetOnce; class CBonusSelection; -class CampaignIntroVideo : public CWindowObject +class CampaignRimVideo : public CWindowObject { std::shared_ptr videoPlayer; - std::shared_ptr bonusSel; + + std::function closeCb; void exit(); public: - CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr bonusSel); + CampaignRimVideo(VideoPath video, ImagePath rim, std::function closeCb); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; diff --git a/config/campaignOverrides.json b/config/campaignOverrides.json index a14f4fa2f..d7d46ae0f 100644 --- a/config/campaignOverrides.json +++ b/config/campaignOverrides.json @@ -1,4 +1,7 @@ { + "DATA/GOOD3" : { // RoE - "Song for the Father" + "outroVideo": "Endgame" + }, "MAPS/HC1_MAIN" : { // Heroes Chronicles 1 "regions": { @@ -29,7 +32,7 @@ { "voiceProlog": "chronicles_1/H3X2BBF", "voiceEpilog": "chronicles_1/N1C_D" } ], "loadingBackground": "chronicles_1/LoadBar", - "introVideoRim": "chronicles_1/INTRORIM", + "videoRim": "chronicles_1/INTRORIM", "introVideo": "chronicles_1/Intro" }, "MAPS/HC2_MAIN" : { // Heroes Chronicles 2 @@ -62,7 +65,7 @@ { "voiceProlog": "chronicles_2/G1A", "voiceEpilog": "chronicles_2/S1C" } ], "loadingBackground": "chronicles_2/LoadBar", - "introVideoRim": "chronicles_2/INTRORIM", + "videoRim": "chronicles_2/INTRORIM", "introVideo": "chronicles_2/Intro" }, "MAPS/HC3_MAIN" : { // Heroes Chronicles 3 @@ -95,7 +98,7 @@ { "voiceProlog": "chronicles_3/G3B", "voiceEpilog": "chronicles_3/ABVOFL2" } ], "loadingBackground": "chronicles_3/LoadBar", - "introVideoRim": "chronicles_3/INTRORIM", + "videoRim": "chronicles_3/INTRORIM", "introVideo": "chronicles_3/Intro" }, "MAPS/HC4_MAIN" : { // Heroes Chronicles 4 @@ -128,7 +131,7 @@ { "voiceProlog": "chronicles_4/H3X2NBD", "voiceEpilog": "chronicles_4/S1C" } ], "loadingBackground": "chronicles_4/LoadBar", - "introVideoRim": "chronicles_4/INTRORIM", + "videoRim": "chronicles_4/INTRORIM", "introVideo": "chronicles_4/Intro" }, "MAPS/HC5_MAIN" : { // Heroes Chronicles 5 @@ -155,7 +158,7 @@ { "voiceProlog": "chronicles_5/H3X2UAH", "voiceEpilog": "chronicles_5/N1C_D" } ], "loadingBackground": "chronicles_5/LoadBar", - "introVideoRim": "chronicles_5/INTRORIM", + "videoRim": "chronicles_5/INTRORIM", "introVideo": "chronicles_5/Intro" }, "MAPS/HC6_MAIN" : { // Heroes Chronicles 6 @@ -182,7 +185,7 @@ { "voiceProlog": "chronicles_6/ABVOAB5", "voiceEpilog": "chronicles_6/ABVODB2" } ], "loadingBackground": "chronicles_6/LoadBar", - "introVideoRim": "chronicles_6/INTRORIM", + "videoRim": "chronicles_6/INTRORIM", "introVideo": "chronicles_6/Intro" }, "MAPS/HC7_MAIN" : { // Heroes Chronicles 7 @@ -215,7 +218,7 @@ { "voiceProlog": "chronicles_7/ABVOFW4", "voiceEpilog": "chronicles_7/ABVOAB1" } ], "loadingBackground": "chronicles_7/LoadBar", - "introVideoRim": "chronicles_7/INTRORIM", + "videoRim": "chronicles_7/INTRORIM", "introVideo": "chronicles_7/Intro5" }, "MAPS/HC8_MAIN" : { // Heroes Chronicles 8 @@ -248,7 +251,7 @@ { "voiceProlog": "chronicles_8/H3X2ELE", "voiceEpilog": "chronicles_8/ABVOAB7" } ], "loadingBackground": "chronicles_8/LoadBar", - "introVideoRim": "chronicles_8/INTRORIM", + "videoRim": "chronicles_8/INTRORIM", "introVideo": "chronicles_8/Intro6" } } diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 46cc7b2de..8a319f5e8 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -54,7 +54,8 @@ In header are parameters describing campaign properties - `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start - `"loadingBackground"` is for setting a different loading screen background - `"introVideo"` is for defining an optional intro video -- `"introVideoRim"` is for the Rim around the optional video (default is INTRORIM) +- `"introVideo"` is for defining an optional outro video +- `"videoRim"` is for the Rim around the optional video (default is INTRORIM) ## Scenario description diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 5c7110e00..563437cb0 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -169,8 +169,9 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.modName = modName; ret.encoding = encoding; ret.loadingBackground = ImagePath::fromJson(reader["loadingBackground"]); - ret.introVideoRim = ImagePath::fromJson(reader["introVideoRim"]); + ret.videoRim = ImagePath::fromJson(reader["videoRim"]); ret.introVideo = VideoPath::fromJson(reader["introVideo"]); + ret.outroVideo = VideoPath::fromJson(reader["outroVideo"]); } CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index e1e33c758..d4e5aaf98 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -210,9 +210,9 @@ ImagePath CampaignHeader::getLoadingBackground() const return loadingBackground; } -ImagePath CampaignHeader::getIntroVideoRim() const +ImagePath CampaignHeader::getVideoRim() const { - return introVideoRim; + return videoRim; } VideoPath CampaignHeader::getIntroVideo() const @@ -220,6 +220,11 @@ VideoPath CampaignHeader::getIntroVideo() const return introVideo; } +VideoPath CampaignHeader::getOutroVideo() const +{ + return outroVideo; +} + const CampaignRegions & CampaignHeader::getRegions() const { return campaignRegions; @@ -490,10 +495,12 @@ void Campaign::overrideCampaign() loadLegacyData(CampaignRegions::fromJson(entry.second["regions"]), entry.second["scenarioCount"].Integer()); if(!entry.second["loadingBackground"].isNull()) loadingBackground = ImagePath::builtin(entry.second["loadingBackground"].String()); - if(!entry.second["introVideoRim"].isNull()) - introVideoRim = ImagePath::builtin(entry.second["introVideoRim"].String()); + if(!entry.second["videoRim"].isNull()) + videoRim = ImagePath::builtin(entry.second["videoRim"].String()); if(!entry.second["introVideo"].isNull()) introVideo = VideoPath::builtin(entry.second["introVideo"].String()); + if(!entry.second["outroVideo"].isNull()) + outroVideo = VideoPath::builtin(entry.second["outroVideo"].String()); } } diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 303e7428e..67e027354 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -98,8 +98,9 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable std::string modName; std::string encoding; ImagePath loadingBackground; - ImagePath introVideoRim; + ImagePath videoRim; VideoPath introVideo; + VideoPath outroVideo; int numberOfScenarios = 0; bool difficultyChosenByPlayer = false; @@ -124,8 +125,9 @@ public: std::string getEncoding() const; AudioPath getMusic() const; ImagePath getLoadingBackground() const; - ImagePath getIntroVideoRim() const; + ImagePath getVideoRim() const; VideoPath getIntroVideo() const; + VideoPath getOutroVideo() const; const CampaignRegions & getRegions() const; TextContainerRegistrable & getTexts(); @@ -153,9 +155,11 @@ public: if (h.version >= Handler::Version::CHRONICLES_SUPPORT) { h & loadingBackground; - h & introVideoRim; + h & videoRim; h & introVideo; } + if (h.version >= Handler::Version::CAMPAIGN_OUTRO_SUPPORT) + h & outroVideo; } }; diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 5c72c10fa..8a5dc8fdb 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -57,6 +57,7 @@ enum class ESerializationVersion : int32_t PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization CHRONICLES_SUPPORT, // 860 - support for heroes chronicles + CAMPAIGN_OUTRO_SUPPORT, // 861 - support for campaign outro video - CURRENT = CHRONICLES_SUPPORT + CURRENT = CAMPAIGN_OUTRO_SUPPORT }; From 0df13040ec3043b698147059550dcd8d1c064799 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:32:44 +0200 Subject: [PATCH 02/25] doc fix --- docs/modders/Campaign_Format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 8a319f5e8..4711361ea 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -54,7 +54,7 @@ In header are parameters describing campaign properties - `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start - `"loadingBackground"` is for setting a different loading screen background - `"introVideo"` is for defining an optional intro video -- `"introVideo"` is for defining an optional outro video +- `"outroVideo"` is for defining an optional outro video - `"videoRim"` is for the Rim around the optional video (default is INTRORIM) ## Scenario description From 45625f0355c3a12bdc3b3abdd466adf88aca12e1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:57:53 +0200 Subject: [PATCH 03/25] added AB intro; play only when resource exists --- client/CServerHandler.cpp | 4 +++- client/NetPacksLobbyClient.cpp | 3 ++- config/campaignOverrides.json | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 4c11c21f0..4e05ce505 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -24,6 +24,8 @@ #include "lobby/CBonusSelection.h" #include "windows/InfoWindows.h" #include "media/CMusicHandler.h" +#include "media/IVideoPlayer.h" + #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" @@ -706,7 +708,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - if(!ourCampaign->getOutroVideo().empty()) + if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), false)) { CCS->musich->stopMusic(); GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), [campaignScoreCalculator, statistic](){ diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index b9b54296e..f280b4d44 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -32,6 +32,7 @@ #include "widgets/Buttons.h" #include "widgets/TextControls.h" #include "media/CMusicHandler.h" +#include "media/IVideoPlayer.h" #include "../lib/CConfigHandler.h" #include "../lib/texts/CGeneralTextHandler.h" @@ -206,7 +207,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & { auto bonusSel = std::make_shared(); lobby->bonusSel = bonusSel; - if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty()) + if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), false)) { CCS->musich->stopMusic(); GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), [bonusSel](){ diff --git a/config/campaignOverrides.json b/config/campaignOverrides.json index d7d46ae0f..28bbbfca6 100644 --- a/config/campaignOverrides.json +++ b/config/campaignOverrides.json @@ -2,6 +2,10 @@ "DATA/GOOD3" : { // RoE - "Song for the Father" "outroVideo": "Endgame" }, + "DATA/AB" : { // AB Intro + "introVideo": "H3X1intr", + "videoRim": "IntroRm2" + }, "MAPS/HC1_MAIN" : { // Heroes Chronicles 1 "regions": { From f1de265568d33c0b4b102bc506b355481ff61e96 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:07:24 +0200 Subject: [PATCH 04/25] roe video in line as ab --- clientapp/EntryPoint.cpp | 2 -- config/campaignOverrides.json | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/clientapp/EntryPoint.cpp b/clientapp/EntryPoint.cpp index e32303f07..3ba0ff27c 100644 --- a/clientapp/EntryPoint.cpp +++ b/clientapp/EntryPoint.cpp @@ -410,8 +410,6 @@ void playIntro() if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) return; - - CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); } static void mainLoop() diff --git a/config/campaignOverrides.json b/config/campaignOverrides.json index 28bbbfca6..ce667ba85 100644 --- a/config/campaignOverrides.json +++ b/config/campaignOverrides.json @@ -1,8 +1,11 @@ { + "DATA/GOOD1" : { // RoE - "Long Live the Queen" + "introVideo": "H3INTRO" + }, "DATA/GOOD3" : { // RoE - "Song for the Father" "outroVideo": "Endgame" }, - "DATA/AB" : { // AB Intro + "DATA/AB" : { // AB - "Armageddon's Blade" "introVideo": "H3X1intr", "videoRim": "IntroRm2" }, From c5b598b277c5b7dd801a092b92823fc72418a150 Mon Sep 17 00:00:00 2001 From: kdmcser Date: Sun, 8 Sep 2024 01:05:03 +0800 Subject: [PATCH 05/25] fix Ballista Bank Mod auto combat crash --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 70c881d23..6f3bcf0ab 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -290,7 +290,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass( continue; auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); - auto turnsToKill = unit->getAvailableHealth() / dmg; + auto turnsToKill = unit->getAvailableHealth() / vstd::amax(dmg, 1); vstd::amin(turnsToKill, 100); From b84d653a998f07fae3c61a7eb138de05481631e2 Mon Sep 17 00:00:00 2001 From: kdmcser Date: Mon, 9 Sep 2024 19:03:56 +0800 Subject: [PATCH 06/25] make TimesStackLevelUpdater support commander --- lib/bonuses/Updaters.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/bonuses/Updaters.cpp b/lib/bonuses/Updaters.cpp index 9a40a2976..064818a58 100644 --- a/lib/bonuses/Updaters.cpp +++ b/lib/bonuses/Updaters.cpp @@ -145,7 +145,7 @@ JsonNode ArmyMovementUpdater::toJsonNode() const } std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const { - if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE) + if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE || context.getNodeType() == CBonusSystemNode::COMMANDER) { int level = dynamic_cast(context).getLevel(); auto newBonus = std::make_shared(*b); @@ -155,8 +155,7 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha else if(context.getNodeType() == CBonusSystemNode::STACK_BATTLE) { const auto & stack = dynamic_cast(context); - //only update if stack doesn't have an instance (summons, war machines) - //otherwise we'd end up multiplying twice + //update if stack doesn't have an instance (summons, war machines) if(stack.base == nullptr) { int level = stack.unitType()->getLevel(); @@ -164,6 +163,14 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha newBonus->val *= level; return newBonus; } + // If these are not handled here, the final outcome may potentially be incorrect. + else + { + int level = dynamic_cast(stack.base)->getLevel(); + auto newBonus = std::make_shared(*b); + newBonus->val *= level; + return newBonus; + } } return b; } From 7e1eddf530d2c57f56bdff12f1542213b23eff10 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:08:57 +0200 Subject: [PATCH 07/25] Revert "roe video in line as ab" This reverts commit f1de265568d33c0b4b102bc506b355481ff61e96. --- clientapp/EntryPoint.cpp | 2 ++ config/campaignOverrides.json | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/clientapp/EntryPoint.cpp b/clientapp/EntryPoint.cpp index 3ba0ff27c..e32303f07 100644 --- a/clientapp/EntryPoint.cpp +++ b/clientapp/EntryPoint.cpp @@ -410,6 +410,8 @@ void playIntro() if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) return; + + CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); } static void mainLoop() diff --git a/config/campaignOverrides.json b/config/campaignOverrides.json index ce667ba85..28bbbfca6 100644 --- a/config/campaignOverrides.json +++ b/config/campaignOverrides.json @@ -1,11 +1,8 @@ { - "DATA/GOOD1" : { // RoE - "Long Live the Queen" - "introVideo": "H3INTRO" - }, "DATA/GOOD3" : { // RoE - "Song for the Father" "outroVideo": "Endgame" }, - "DATA/AB" : { // AB - "Armageddon's Blade" + "DATA/AB" : { // AB Intro "introVideo": "H3X1intr", "videoRim": "IntroRm2" }, From 1fdbaf943f593fd42d44fc3cd1629e6ee540b74d Mon Sep 17 00:00:00 2001 From: Fabrice TIERCELIN Date: Sun, 8 Sep 2024 10:02:27 +0200 Subject: [PATCH 08/25] Update the French translation --- .../Entities_Format/Creature_Format.md | 2 + docs/modders/Entities_Format/Creature_Help.md | 154 ++ .../modders/Entities_Format/Faction_Format.md | 4 +- docs/modders/Entities_Format/Faction_Help.md | 92 + docs/modders/Readme.md | 6 +- docs/modders/Translations.md | 24 +- launcher/translation/french.ts | 107 +- launcher/translation/swedish.ts | 1514 +++++++++++++++++ mapeditor/translation/french.ts | 126 +- 9 files changed, 1902 insertions(+), 127 deletions(-) create mode 100644 docs/modders/Entities_Format/Creature_Help.md create mode 100644 docs/modders/Entities_Format/Faction_Help.md create mode 100644 launcher/translation/swedish.ts diff --git a/docs/modders/Entities_Format/Creature_Format.md b/docs/modders/Entities_Format/Creature_Format.md index ecbb2b78e..252a5e140 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -1,5 +1,7 @@ # Creature Format +This page tells you what you need to do to make your creature work. For help, tips and advices, read the [creature help](Creature_Help.md). + ## Required data In order to make functional creature you also need: diff --git a/docs/modders/Entities_Format/Creature_Help.md b/docs/modders/Entities_Format/Creature_Help.md new file mode 100644 index 000000000..d5ca1a590 --- /dev/null +++ b/docs/modders/Entities_Format/Creature_Help.md @@ -0,0 +1,154 @@ +# Creature Help + +This page helps you to create a creature (i.e. a unit that fights in a battle) for a creature mod or inside a bigger mod like a [faction mod](Faction_Help.md). + +## Utilities + +You need to download the two utilities [`DefPreview`](https://sourceforge.net/projects/grayface-misc/files/DefPreview-1.2.1/) and [`H3DefTool`](https://sourceforge.net/projects/grayface-misc/files/H3DefTool-3.4.2/) from the internet: +- `DefPreview` converts a `.def` file to `.bmp` images +- `H3DefTool` converts `.bmp` images to a `.def` file + +But you can create a configuration that directly reads your image files. Most of the existing mods are coded with `.def` files but direct images are recommended. + +## Make a playable creature + +First of all, retrieve an existing creature and be sure you can clone it and make it work independently without any new content. If it already fails, don't waste your time to draw the new animation. It should work first. + +## Battle render + +The sun is always at zenith, so the shadow is always behind. The reason is that the creature render may be mirrored. There was no strong rules in the original game but usually, the shadow is twice less higher than the creature. + +We don't know the right elevation angle for the view. + +### 3D render +You can render your creature using a 3D software like _Blender_. You can start with those free-licenced rigged 3D models: +- [Fantasy-bandit](https://www.cgtrader.com/free-3d-models/character/man/fantasy-bandit) +- [Monster-4](https://www.cgtrader.com/free-3d-models/character/fantasy-character/monster-4-f5757b92-dc9c-4f5e-ad0d-593203d14fe2) +- [Crypt-fiend-modular-character](https://www.cgtrader.com/free-3d-models/character/fantasy-character/crypt-fiend-modular-character-demo-scene) +- [Solus-the-knight](https://www.cgtrader.com/free-3d-models/character/man/solus-the-knight-low-poly-character) +- [Ancient-earth-golem](https://www.cgtrader.com/free-3d-models/character/fantasy-character/ancient-earth-golem-v2) +- [Shadow-golem-elemental](https://www.cgtrader.com/free-3d-models/character/fantasy-character/shadow-golem-elemental) +- [Earth-golem-elemental](https://www.cgtrader.com/free-3d-models/character/fantasy-character/earth-golem-elemental) +- [Kong-2021-rig](https://www.cgtrader.com/free-3d-models/character/sci-fi-character/kong-2021-rig) +- [Shani](https://www.cgtrader.com/free-3d-models/character/woman/shani-3d-character) + +You can also create your 3D model from a single image: +- _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d +- _Unique3D_: https://huggingface.co/spaces/abreza/Unique3D + +To use it in _Blender_, create a `.blend` project and import the file. To render the texture: +1. Add a _Principled BSDF_ material to the object +1. Create a _Color Attribute_ in the _Shader Editor_ view +1. Link the Color output of the _Color Attribute_ to the _Base color_ input of the _Principled BSDF_ + +You can improve details by cropping the source image on a detail and generate a model for this detail. Once both imported in _Blender_, melt them together. + +Render the images without background by selecting png RVBA and disabling background (_Film_ -> _Filter_ -> _Transparent_). It avoids the creatures to have an ugly dark border. Then, to correctly separate the creature from the cyan area, in _GIMP_, apply the threeshold on the transparency by clicking on _Layer_ -> _Transparency_ -> _Alpha threeshold_. + +The global FPS of the game is 10 f/s but you can render at a higher level and configure it in the `.json` files. We are not in the 1990's. + +### IA render + +You can also use an AI like _Flux_ to generate the main creature representation: https://huggingface.co/spaces/multimodalart/FLUX.1-merged + +Then you can add random animations for idle states with _SVD_: https://huggingface.co/spaces/xi0v/Stable-Video-Diffusion-Img2Vid + +Most of the time, the creatures do not move more than one pixel in an idle animation. The reason may be to avoid too much animation on screen and make the transition with the other animations always seamless. Use poses with _ControlNet_ or _OpenPose_. For specific animations, I recommend to use _Cinemo_ because it adds a description prompt but the resolution is smaller: https://huggingface.co/spaces/maxin-cn/Cinemo + +Make animations seamless from one to another. To do this, you can draw the first and the last images with a prompt with _ToonCrafter_: https://huggingface.co/spaces/ChristianHappy/tooncrafter + +Most of the time, you need to increase the resolution or the quality of your template image, so use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR + +## Battle sound effect + +To create the audio effects, I recommend to use _Tango 2_: https://huggingface.co/spaces/declare-lab/tango2 + +The quality is better than _Stable Audio_. + +## Map render + +We don't know the right elevation angle for the view but 45ยฐ elevation seems to be a good choice. For the sunlight direction, I would say 45ยฐ elevation and 45ยฐ azimut. + +The map creatures are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). If you are creating a creature and its updated version, most of the time, the both creatures are not oriented to the same side on the map. I think that the animation on the map is usually the _Mouse Over_ animation on battle. + +You can see that the view angle is higher than on a battle. To change the angle from a battle sprite, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live + +You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD + +If you have a 3D software, you can get better quality by converting your image into 3D model and then render it from another angle using _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d + +Follow this comment to retrieve the color: https://huggingface.co/stabilityai/TripoSR/discussions/1#65e8a8e5e214f37d85dad366 + +### Shadow render + +There are no strong rules in the original game about the angle of the shadows on the map. Different buildings have inconsistent shadows. To draw the shadow, I recommend the following technique: + +Let's consider that the object is a vertical cone: +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | + +Locate the top and its projection to the ground: +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฅ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | ๐ŸŸฅ | โ€โฌ› | โ€โฌ› | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | + +Then draw a rectangle triangle on the left: +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | + +The square top is the projection of the shadow of the top of the cone: +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | + +Then you can draw the rest of the shadow: +| | | | | | | | | | | +|---|---|---|---|---|---|---|---|---|---| +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | ๐ŸŸช | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐Ÿ’Ÿ | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | โ€โฌ› | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | โ€โฌ› | โ€โฌ› | โ€โฌ› | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | +| ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | ๐ŸŸฆ | diff --git a/docs/modders/Entities_Format/Faction_Format.md b/docs/modders/Entities_Format/Faction_Format.md index 81178e22f..739086331 100644 --- a/docs/modders/Entities_Format/Faction_Format.md +++ b/docs/modders/Entities_Format/Faction_Format.md @@ -1,8 +1,10 @@ # Faction Format +This page tells you what you need to do to make your faction work. For help, tips and advices, read the [faction help](Faction_Help.md). + ## Required data -In order to make functional town you also need: +In order to make functional town, you also need: ### Images diff --git a/docs/modders/Entities_Format/Faction_Help.md b/docs/modders/Entities_Format/Faction_Help.md new file mode 100644 index 000000000..c141a8673 --- /dev/null +++ b/docs/modders/Entities_Format/Faction_Help.md @@ -0,0 +1,92 @@ +# Faction Help + +This page helps you to create from scratch a VCMI mod that adds a new faction. The faction mod structure is described [here](Faction_Format.md). + +## Questioning the faction creation +Before creating a faction, be aware that creating a faction mod is lots of work. You can start [creating creatures](Creature_Help.md) in a creature mod that can be converted into a faction mod after. This way, you are sure to release something. The smallest contribution is a hero portrait that you can suggest on an existing mod. You can also restore the former version of the [Ruins faction](https://github.com/vcmi-mods/ruins-town/tree/1bea30a1d915770e2fd0f95d158030815ff462cd). You would only have to remake the similar parts to the new version. + +## Make a playable faction mod +Before creating your content, retrieve the content of an existing faction mod like [Highlands town](https://github.com/vcmi-mods/highlands-town). To download the project, click on the _Code_ button and click on _Download ZIP_. The first thing to do is to change all the faction identifiers in the files following the [faction format](Faction_Format.md) and manage to play with the faction and the original without any conflict. To play to a faction, you have to add all the files in your _Mods_ folder. When it works, you will be able to modify the content step by step. + +Keep in mind that the most important part of a faction mod, above the animations, the graphisms and the musics, is the concept because if you have to change it, you have to change everything else. All the remaining content can be improved by the community. + +## Town screen +### Background +Beware to direct all the shadows to the same direction. The easiest way to create the background is to use a text-to-image AI. The free most powerful AI at the moment is _Flux_ available here: https://huggingface.co/spaces/multimodalart/FLUX.1-merged + +In the _Advanced Options_, set the width to 800px and set the height to 374px. + +### Buildings +To render a building upon the background, I recommend to use an inpainting AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting + +The idea is to select the area where you want to add the building. As a prompt, describe the new building. The advantage is a perfect match between the background and the building. Keep in mind that to correctly integrate a building image, it must contain the image of the background on its edges. It simulates the semi-transparency. + +You can also animate the building or the background using _Stable Video Diffusion_: https://huggingface.co/spaces/multimodalart/stable-video-diffusion + +## Map dwellings +You may want to get the same render as in the town, so you have to change the angle and the shadows. If you handle a 3D model software, you can start with _Stable Fast 3D_: https://huggingface.co/spaces/stabilityai/stable-fast-3d + +The map dwellings are not rendered on the map with vanishing points but in isometric. You can [get an orthogonal render in Blender](https://blender.stackexchange.com/a/135384/2768). + +Without 3D, you can use _Zero 1-to-3_: https://huggingface.co/spaces/cvlab/zero123-live + +You can get higher resolution using this Video AI that can control the motion of the camera: https://huggingface.co/spaces/TencentARC/MotionCtrl_SVD + +The buildings on the map are more satured than on town screen. If you have to reduce the size of an image, do not use interpolation (LANCZOS, Bilinear...) to get more details, not a blurred image. If you need to increase the resolution or the quality of your template image, use _SUPIR_: https://huggingface.co/spaces/Fabrice-TIERCELIN/SUPIR + +## Map buildings +The AIs badly understand the sun direction and the perspective angles. To generate the buildings on the adventure map: + +1. Open the HOMM3 map editor +1. Put items all around a big empty area +1. Make a screenshot +1. Go on an AI like _BRIA Inpaint_: https://huggingface.co/spaces/briaai/BRIA-2.3-Inpainting +1. Inpaint the (big) empty middle with the brush +1. Use a prompt like: `A dark house, at the center of the image, map, isometric, parallel perspective, sunlight from the bottom right` + +## Music +Here are unused available themes: + +* [Synthetic Horizon](https://github.com/Fabrice-TIERCELIN/forge/raw/theme/content/music/factions/theme.ogg) +1. Prompt: `Dystopy, Cinematic classical, Science fiction, 160 bpm, Best quality, Futuristic` +1. Initially created for: _Forge town_ + +* [Quantum Overture](https://github.com/Fabrice-TIERCELIN/asylum-town/raw/theme/asylum-town/content/Music/factions/AsylumTown.ogg) +1. Prompt: `Clef shifting, Fantasy, Mystical, Overworldly, Cinematic classical` +1. Initially created for: _Asylum town_ + +* [Warrior s March](https://github.com/vcmi-mods/ruins-town/assets/20668759/964f27de-6feb-4ef6-9d25-455f52938cef) +1. Prompt: `Powerful percussions, Drums, Battle Anthem, Rythm, Warrior, 160 bpm, Celtic, New age, Instrumental, Accoustic, Medieval` +1. Initially created for: _Ruins town_ + +* [Clan of Echoes](https://github.com/Fabrice-TIERCELIN/ruins-town/raw/theme/ruins-town/content/music/ruins.ogg) +1. Prompt: `new age, medieval, celtic, warrior, battle, soundtrack, accoustic, drums, rythm` +1. Initially created for: _Ruins town_ + +* [Enchanted Reverie](https://github.com/Fabrice-TIERCELIN/grove/raw/theme/Grove/content/Music/factions/GroveTown.ogg) +1. Prompt: `Classical music, Soundtrack, Score, Instrumental, 160 bpm, ((((fantasy)))), mystic` +1. Initially created for: _Grove town_ + +* [World Discovery](https://github.com/vcmi-mods/asylum-town/assets/20668759/34438523-8a44-44ca-b493-127501b474a6) +1. Prompt: `Clef shifting, fantasy, mystical, overworldly, Cinematic classical` +1. Initially created for: _Asylum town_ + +* [Enchanted Ballad](https://github.com/vcmi-mods/fairy-town/assets/20668759/619e6e33-d940-4899-8c76-9c1e8d3d20aa) +1. Prompt: `Females vocalize, Cinematic classical, Harp, Fairy tale, Princess, 160 bpm` +1. Initially created for: _Fairy town_ + +* [Baroque Resurgence](https://github.com/Fabrice-TIERCELIN/courtyard_proposal/raw/theme/Courtyard/Content/music/factions/courtyard/CourtTown.ogg) +1. Prompt: `Baroque, Instrumental, 160 bpm, Cinematic classical, Best quality` +1. Initially created for: _Courtyard town_ + +* [Harvest Parade](https://github.com/Fabrice-TIERCELIN/greenhouse-town/raw/theme/Greenhouse/content/Music/town.ogg) +1. Prompt: `Marching band, Best quality, Happy, Vegetables` +1. Initially created for: _Green town_ + +Those themes have been generated using _[Udio](https://udio.com)_. + +## Screenshots +Most of the time, the first screenshot is the townscreen because it's the most specific content. + +## Recycle +Some mods contain neutral heroes or creatures. You can integrate them in your faction mod. Don't forget to remove the content from the original mod. \ No newline at end of file diff --git a/docs/modders/Readme.md b/docs/modders/Readme.md index 1bf8c80ce..5fba25113 100644 --- a/docs/modders/Readme.md +++ b/docs/modders/Readme.md @@ -56,8 +56,10 @@ Random Map Generator: Game Entities: - [Artifact](Entities_Format/Artifact_Format.md) -- [Creature](Entities_Format/Creature_Format.md) -- [Faction](Entities_Format/Faction_Format.md) +- [Creature Requirement](Entities_Format/Creature_Format.md) +- [Creature Help](Entities_Format/Creature_Help.md) +- [Faction Requirement](Entities_Format/Faction_Format.md) +- [Faction Help](Entities_Format/Faction_Help.md) - [Hero Class](Entities_Format/Hero_Class_Format.md) - [Hero Type](Entities_Format/Hero_Type_Format.md) - [Spell](Entities_Format/Spell_Format.md) diff --git a/docs/modders/Translations.md b/docs/modders/Translations.md index 3915986c5..bc3aa42e9 100644 --- a/docs/modders/Translations.md +++ b/docs/modders/Translations.md @@ -35,10 +35,16 @@ VCMI allows translating game data into languages other than English. In order to - Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation (delete sound and video folders) - Rename mod to indicate your language, preferred form is "(language)-translation" - Update mod.json to match your mod -- Translate all texts strings from game.json, campaigns.json and maps.json +- Translate all texts strings from `game.json`, `campaigns.json` and `maps.json` - Replace images in data and sprites with translated ones (or delete it if you don't want to translate them) - If unicode characters needed for language: Create a submod with a free font like here: https://github.com/vcmi-mods/vietnamese-translation/tree/vcmi-1.4/vietnamese-translation/mods/VietnameseTrueTypeFonts +If you can't produce some content on your own (like the images or the sounds): +- Create a `README.md` file at the root of the mod +- Write into the file the translations and the detailled location + +This way, a contributor that is not a native speaker can do it for you in the future. + If you have already existing Heroes III translation you can: - Install VCMI and select your localized Heroes III data files for VCMI data files @@ -75,7 +81,7 @@ After this, you can set language in Launcher to your language and start game. Al VCMI Launcher and Map Editor use translation system provided by Qt framework so it requires slightly different approach than in-game translations: - Install Qt Linguist. You can find find standalone version here: https://download.qt.io/linguist_releases/ -- Open `/launcher/translation/` directory, copy english.ts file and rename it to your language +- Open `/launcher/translation/` directory, copy `english.ts` file and rename it to your language - Launch Qt Linguist, select Open and navigate to your copied file - Select any untranslated string, enter translation in field below, and click "Done and Next" (Ctrl+Return) to navigate to next untranslated string - Once translation has been finished, save resulting file. @@ -129,7 +135,7 @@ After that, start Launcher, switch to Help tab and open "log files directory". Y If your mod also contains maps or campaigns that you want to translate, then use '/translate maps' command instead. ### Translating mod information -In order to display information in Launcher in language selected by user add following block into your mod.json: +In order to display information in Launcher in language selected by user add following block into your `mod.json`: ``` "" : { "name" : "", @@ -140,7 +146,7 @@ In order to display information in Launcher in language selected by user add fol ] }, ``` -However, normally you don't need to use block for English. Instead, English text should remain in root section of your mod.json file, to be used when game can not find translated version. +However, normally you don't need to use block for English. Instead, English text should remain in root section of your `mod.json` file, to be used when game can not find translated version. ### Translating in-game strings @@ -152,8 +158,8 @@ Use any text editor (Notepad++ is recommended for Windows) and translate all str ### Adding new languages In order to add new language it needs to be added in multiple locations in source code: -- Generate new .ts files for launcher and map editor, either by running `lupdate` with name of new .ts or by copying english.ts and editing language tag in the header. -- Add new language into lib/Languages.h entry. This will trigger static_assert's in places that needs an update in code +- Generate new .ts files for launcher and map editor, either by running `lupdate` with name of new .ts or by copying `english.ts` and editing language tag in the header. +- Add new language into `lib/Languages.h` entry. This will trigger static_assert's in places that needs an update in code - Add new language into json schemas validation list - settings schema and mod schema - Add new language into mod json format - in order to allow translation into new language @@ -161,14 +167,14 @@ Also, make full search for a name of an existing language to ensure that there a ### Updating translation of Launcher and Map Editor to include new strings -At the moment, build system will generate binary translation files (.qs) that can be opened by Qt. +At the moment, build system will generate binary translation files (`.qs`) that can be opened by Qt. However, any new or changed lines will not be added into existing .ts files. -In order to update .ts files manually, open command line shell in `mapeditor` or `launcher` source directories and execute command +In order to update `.ts` files manually, open command line shell in `mapeditor` or `launcher` source directories and execute command ``` lupdate -no-obsolete * -ts translation/*.ts ``` -This will remove any no longer existing lines from translation and add any new lines for all translations. If you want to keep old lines, remove `-no-obsolete` key from the command +This will remove any no longer existing lines from translation and add any new lines for all translations. If you want to keep old lines, remove `-no-obsolete` key from the command. There *may* be a way to do the same via QtCreator UI or via CMake, if you find one feel free to update this information. ### Updating translation of Launcher and Map Editor using new .ts file from translators diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index c1ddec902..e3524de8b 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -74,7 +74,7 @@ Configuration files directory - + Dossier de fichiers de configuration @@ -234,7 +234,7 @@ Reload repositories - + Recharger les dossiers @@ -452,7 +452,7 @@ Replace config file? - Remplacer le fichier de configuration? + Remplacer le fichier de configuration ? @@ -462,7 +462,7 @@ Downloading %1. %p% (%v MB out of %m MB) finished - + Tรฉlรฉchargement %1. %p% (%v Mo sur %m Mo) terminรฉ @@ -656,162 +656,162 @@ Installer les tรฉchargements rรฉussis? Online Lobby port - + Port de la salle d'attente en ligne Autocombat AI in battles - + IA de combat automatique dans les batailles Sticks Sensitivity - + Sensibilitรฉ au batons Automatic (Linear) - + Automatique (Linรฉaire) Haptic Feedback - + Retour Tactile Software Cursor - + Curseur Logiciel Automatic - + Automatique None - + Aucun xBRZ x2 - + xBRZ x2 xBRZ x3 - + xBRZ x3 xBRZ x4 - + xBRZ x4 Online Lobby address - + Adresse de la salle d'attente en ligne Upscaling Filter - + Filtre d'Agrandissement Use Relative Pointer Mode - + Utiliser le Mode de Pointeur Relatif Nearest - + Le plus Proche Linear - + Linรฉaire Input - Touchscreen - + Entrรฉe - ร‰cran tactile Network - + Rรฉseau Downscaling Filter - + Filtre de Rรฉtrรฉcissement Show Tutorial again - + Remontrer le Didacticiel Reset - + Rรฉinitialiser Audio - + Audio Relative Pointer Speed - + Vitesse de Pointeur Relatif Music Volume - + Volume de la Musique Ignore SSL errors - + Ignorer les erreurs SSL Input - Mouse - + Entrรฉe - Sourie Long Touch Duration - + Durรฉe de Touche Prolongรฉe % - + % Controller Click Tolerance - + Tolรฉrance au Clic de Contrรดleur Touch Tap Tolerance - + Tolรฉrance ร  la Frappe de Touche Input - Controller - + Entrรฉe - Contrรดleur Sound Volume - + Volume du Son @@ -913,12 +913,12 @@ Mode exclusif plein รฉcran - le jeu couvrira l"intรฉgralitรฉ de votre รฉcra Mouse Click Tolerance - + Tolรฉrance au Clic de Sourie Sticks Acceleration - + Accelรฉration de Bรขton @@ -1059,12 +1059,12 @@ Mode exclusif plein รฉcran - le jeu couvrira l"intรฉgralitรฉ de votre รฉcra Use offline installer from gog.com - + Utiliser l'installeur hors ligne depuis gog.com You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page - + Vous pouvez copier manuellement les dossiers de Maps, Data et Mp3 depuis le dossier de jeu d'origine vers le dossier data VCMI que vous pouvez voir en haut de cette page @@ -1074,22 +1074,22 @@ Mode exclusif plein รฉcran - le jeu couvrira l"intรฉgralitรฉ de votre รฉcra Manual Installation - + Installation Manuelle Installing... %p% - + Installation... %p% If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. - + Si vous avez dรฉjร  les fichiers Heroes III sur votre appareil, vous pouvez sรฉlectionner ce dossier et VCMI copiera automatiquement les donnรฉes existantes. Copy existing files - + Copier les fichiers existants @@ -1100,7 +1100,8 @@ Mode exclusif plein รฉcran - le jeu couvrira l"intรฉgralitรฉ de votre รฉcra If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. Offline installer consists of two parts, .exe and .bin. Make sure you download both of them. - + Si vous possรฉdez Heroes III sur gog.com, vous pouvez tรฉlรฉcharger le programme d'installation hors ligne de sauvegarde depuis gog.com, et VCMI importera les donnรฉes de Heroes III ร  l'aide du programme d'installation hors ligne. +Le programme d'installation hors ligne se compose de deux parties, .exe et .bin. Assurez-vous de les tรฉlรฉcharger tous les deux. @@ -1246,7 +1247,7 @@ Heroesยฎ of Might and Magicยฎ III HD n"est actuellement pas pris en charge File cannot opened - + Le fichier ne peut pas รชtre ouvert @@ -1291,23 +1292,24 @@ Veuillez selectionner un dossier ou les donnรฉes de Heroes III sont prรฉsentes.< You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! - + Vous avez fourni le programme d'installation de GOG Galaxyย ! Ce fichier ne contient pas le jeu. Veuillez tรฉlรฉcharger le programme d'installation de sauvegarde hors ligne du jeuย ! Stream error while extracting files! error reason: - + Erreur de flux lors de l'extraction des fichiersย ! +Raison de l'erreurย : Not a supported Inno Setup installer! - + Programme dโ€™installation Inno Setup non pris en chargeย ! Extracting error! - + Erreur d'extraction ! @@ -1476,13 +1478,14 @@ Veuillez sรฉlectionner un dossier contenant les donnรฉes de Heroes III: Complete Error starting executable - + Erreur lors du dรฉmarrage de l'exรฉcutable Failed to start %1 Reason: %2 - + ร‰chec de dรฉmarrage %1 +Raisonย : %2 diff --git a/launcher/translation/swedish.ts b/launcher/translation/swedish.ts new file mode 100644 index 000000000..d265539e3 --- /dev/null +++ b/launcher/translation/swedish.ts @@ -0,0 +1,1514 @@ + + + + + AboutProjectView + + + VCMI on Discord + VCMI pรฅ Discord + + + + Have a question? Found a bug? Want to help? Join us! + Har du nรฅgot som รคr svรฅr att fรถrstรฅ? Har du hittat ett fel eller ett annat problem? Behรถver du hjรคlp? Fรถrena oss i samma grupp! + + + + VCMI on Github + VCMI pรฅ Github + + + + Our Community + Vรฅr gemenskap + + + + Build Information + Konstruktionsinformation + + + + User data directory + Anvรคndardatamapp + + + + + + + Open + ร–ppna + + + + Check for updates + Sรถk efter uppdateringar + + + + Game version + Spelversion + + + + Log files directory + Loggmapp + + + + Data Directories + Datafil + + + + Game data directory + Speldatamapp + + + + Operating System + Operativsystem + + + + Configuration files directory + Mapp fรถr konfigurationsfiler + + + + Project homepage + Projektets hemsida + + + + Report a bug + Rapportera ett fel + + + + CModListModel + + + Translation + ร–versรคttning + + + + Town + Stad + + + + Test + Testa + + + + Templates + Modeller + + + + Spells + Trollformler + + + + Music + Musik + + + + Maps + Kartor + + + + Sounds + Ljud + + + + Skills + Fรคrdigheter + + + + + Other + ร–vrigt + + + + Objects + Objekt + + + + + Mechanics + Mekanik + + + + + Interface + Grรคnssnitt + + + + Heroes + Hjรคlte + + + + + Graphical + Grafik + + + + Expansion + Tillรคgg + + + + Creatures + Varelser + + + + Compatibility + Kompatibilitet + + + + Artifacts + Artefakter + + + + AI + AI + + + + CModListView + + + Filter + Filter + + + + All mods + Alla mods + + + + Downloadable + Nedladdningsbar + + + + Installed + Installerad + + + + Updatable + Uppdaterbar + + + + Active + Tillgรฅngar + + + + Inactive + Inaktiv + + + + Reload repositories + Ladda om mappar + + + + + Description + Beskrivning + + + + Changelog + Dagbok + + + + Screenshots + Skรคrmutskrifter + + + + %p% (%v KB out of %m KB) + %p% (%v KB av %m KB) + + + + Install from file + Installera frรฅn fil + + + + Uninstall + Avinstallera + + + + Enable + Aktivera + + + + Disable + Inaktivera + + + + Update + Uppdatera + + + + Install + Installera + + + + Abort + Ge upp + + + + Mod name + Modnamn + + + + Installed version + Installerad version + + + + Latest version + Senaste version + + + + Size + Storlek + + + + Download size + Nedladdningsstorlek + + + + Authors + Fรถrfattare + + + + License + Licens + + + + Contact + Kontakt + + + + Compatibility + Kompatibilitet + + + + + Required VCMI version + Obligatorisk version av VCMI + + + + Supported VCMI version + Version av VCMI som stรถds + + + + please upgrade mod + vรคnligen uppdatera mod + + + + + mods repository index + Mod Repository Index + + + + or newer + eller nyare + + + + Supported VCMI versions + Versionerna av VCMI som stรถds + + + + Languages + Sprรฅk + + + + Required mods + Obligatoriska mods + + + + Conflicting mods + Modskonflikt + + + + This mod can not be installed or enabled because the following dependencies are not present + Denna mod kan inte installeras eller aktiveras eftersom fรถljande beroenden inte finns + + + + This mod can not be enabled because the following mods are incompatible with it + Denna mod kan inte installeras eller aktiveras eftersom fรถljande beroenden รคr inkompatibla med den + + + + This mod cannot be disabled because it is required by the following mods + Denna mod kan inte inaktiveras eftersom den krรคvs fรถr fรถljande beroenden + + + + This mod cannot be uninstalled or updated because it is required by the following mods + Denna mod kan inte avinstalleras eller uppdateras eftersom den krรคvs fรถr fรถljande beroenden + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod + Denna undermod kan inte installeras eller uppdateras separat frรฅn modermoden + + + + Notes + Anteckningar + + + + All supported files + Alla filer som stรถds + + + + Maps + Kartor + + + + Campaigns + Kampanjer + + + + Configs + Konfigurationer + + + + Mods + Mods + + + + Select files (configs, mods, maps, campaigns) to install... + Vรคlj filer att installera (konfigurationer, mods, kartor, kampanjer)... + + + + Replace config file? + Byta ut konfigurationsfilen? + + + + Do you want to replace %1? + Vill du ersรคtta %1? + + + + Downloading %1. %p% (%v MB out of %m MB) finished + Ladda ner %1. %p% (%v MB av %m MB) slutfรถrt + + + + Download failed + Nedladdning misslyckades + + + + Unable to download all files. + +Encountered errors: + + + Det gรฅr inte att ladda ner alla filer. + +Fel pรฅtrรคffat: + + + + + + + +Install successfully downloaded? + + +Installera lyckade nedladdningar? + + + + Installing mod %1 + Installera mod %1 + + + + Operation failed + ร…tgรคrden misslyckades + + + + Encountered errors: + + Fel pรฅtrรคffade: + + + + + screenshots + skรคrmdumpar + + + + Screenshot %1 + Skriv ut skรคrm %1 + + + + Mod is incompatible + Denna mod รคr inkompatibel + + + + CModManager + + + Can not install submod + Det gรฅr inte att installera undermod + + + + Mod is already installed + Mod รคr redan installerad + + + + Can not uninstall submod + Det gรฅr inte att avinstallera submod + + + + Mod is not installed + Mod รคr inte installerad + + + + Mod is already enabled + Mod redan aktiverad + + + + + Mod must be installed first + Lรคget mรฅste installeras fรถrst + + + + Mod is not compatible, please update VCMI and checkout latest mod revisions + Mod inte kompatibel, uppdatera VCMI och kontrollera den senaste versionen av moden + + + + Required mod %1 is missing + Obligatorisk mod %1 saknas + + + + Required mod %1 is not enabled + Obligatorisk mod %1 รคr inte aktiverad + + + + + This mod conflicts with %1 + Denna mod รคr i konflikt med %1 + + + + Mod is already disabled + Mod redan inaktiverad + + + + This mod is needed to run %1 + Modden krรคvs fรถr att kรถra %1 + + + + Mod archive is missing + Modarkiv saknas + + + + Mod with such name is already installed + En mod med samma namn รคr redan installerad + + + + Mod archive is invalid or corrupted + Modarkivet รคr ogiltigt eller korrupt + + + + Failed to extract mod data + Det gick inte att extrahera data frรฅn mod + + + + Data with this mod was not found + Data fรถr denna mod hittades inte + + + + Mod is located in protected directory, please remove it manually: + + Modden รคr placerad i en skyddad mapp, vรคnligen radera den manuellt: + + + + + CSettingsView + + + Off + Inaktiverad + + + + Artificial Intelligence + Artificiell intelligens + + + + On + Aktiverad + + + + Enemy AI in battles + Fiendens AI i strider + + + + Default repository + Standardfรถrrรฅd + + + + VSync + Vertikal synkronisering + + + + Online Lobby port + Port av vรคntrummet online + + + + Autocombat AI in battles + Automatisk strids-AI i strider + + + + Sticks Sensitivity + Kรคnslighet fรถr sticks + + + + Automatic (Linear) + Automatisk (linjรคr) + + + + Haptic Feedback + Tryck pรฅ Tillbaka + + + + Software Cursor + Programvarumarkรถr + + + + Automatic + Automatisk + + + + None + Ingen + + + + xBRZ x2 + xBRZ x2 + + + + xBRZ x3 + xBRZ x3 + + + + xBRZ x4 + xBRZ x4 + + + + Online Lobby address + Vรคntrumsadress online + + + + Upscaling Filter + Fรถrstoringsfilter + + + + Use Relative Pointer Mode + Anvรคnda relativt pekarlรคge + + + + Nearest + Nรคrmast + + + + Linear + Linjรคr + + + + Input - Touchscreen + Enter - Pekskรคrm + + + + Network + Nรคtverk + + + + Downscaling Filter + Skrympfilter + + + + Show Tutorial again + Titta om sjรคlvstudien + + + + Reset + ร…terstรคll + + + + Audio + Ljud + + + + Relative Pointer Speed + Relativ pekarhastighet + + + + Music Volume + Musikvolym + + + + Ignore SSL errors + Ignorera SSL-fel + + + + Input - Mouse + Enter - Smile + + + + Long Touch Duration + Utรถkad berรถringslรคngd + + + + % + % + + + + Controller Click Tolerance + Styrenhet Klicka pรฅ Tolerans + + + + Touch Tap Tolerance + Tanslagstolerans + + + + Input - Controller + Ingรฅng - Styrenhet + + + + Sound Volume + Ljudvolym + + + + Select display mode for game + +Windowed - game will run inside a window that covers part of your screen + +Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. + +Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. + Vรคlj visningslรคge fรถr spelet + +Windowed - spelet kommer att kรถras i ett fรถnster som tรคcker en del av din skรคrm + +Kantlรถst fรถnsterlรคge - spelet kรถrs i ett fรถnster som helt tรคcker din skรคrm, med samma upplรถsning som din skรคrm. + +Exklusivt helskรคrmslรคge - spelet kommer att tรคcka hela skรคrmen och anvรคnda den valda upplรถsningen. + + + + Windowed + Fรถnster + + + + Borderless fullscreen + Fรถnster utan kant + + + + Exclusive fullscreen + Exklusiv helskรคrm + + + + Reserved screen area + Reserverat skรคrmomrรฅde + + + + Neutral AI in battles + Neutral AI i strider + + + + Autosave limit (0 = off) + Grรคns โ€‹โ€‹fรถr automatisk lagring (0 = inaktiverad) + + + + Adventure Map Enemies + ร„ventyrskartfiender + + + + Autosave prefix + Autospara prefix + + + + empty = map name prefix + tomt = kortnamnsprefix + + + + Interface Scaling + Grรคnssnittsskalning + + + + Framerate Limit + Grรคns โ€‹โ€‹fรถr bildhastighet + + + + Renderer + Renderingsmotor + + + + Heroes III Translation + Heroes III รถversรคttning + + + + Adventure Map Allies + Adventure Map Allies + + + + Additional repository + Ytterligare insรคttning + + + + Check on startup + Kontrollera vid start + + + + Mouse Click Tolerance + Musklickstolerans + + + + Sticks Acceleration + Personalacceleration + + + + Refresh now + Uppdatera nu + + + + Fullscreen + Helskรคrm + + + + General + Allmรคnt + + + + VCMI Language + VCMI-sprรฅk + + + + Resolution + Upplรถsning + + + + Autosave + Autospara + + + + Display index + Visa index + + + + Network port + Nรคtverksport + + + + Video + Video + + + + Show intro + Visa intro + + + + Active + Aktiv + + + + Disabled + Inaktiverad + + + + Enable + Aktiverad + + + + Not Installed + Inte installerat + + + + Install + Installera + + + + File size + + + %1 B + %1 B + + + + %1 KiB + %1 KiB + + + + %1 MiB + %1 MiB + + + + %1 GiB + %1 GiB + + + + %1 TiB + %1 TiB + + + + FirstLaunchView + + + Language + Sprรฅk + + + + Heroes III Data + Heroes III-data + + + + Mods Preset + Mod-fรถrinstรคllningar + + + + Select your language + Vรคlj ditt sprรฅk + + + + Have a question? Found a bug? Want to help? Join us! + Har du en frรฅga? Hittade du en bugg? Behรถver du hjรคlp? Gรฅ med oss! + + + + Locate Heroes III data files + Hitta Heroes III-datafiler + + + + Use offline installer from gog.com + Anvรคnd offlineinstallationsprogrammet frรฅn gog.com + + + + You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page + Du kan manuellt kopiera mappar Maps, Data och Mp3 frรฅn den ursprungliga spelmappen till VCMI-datamappen som du kan se รถverst pรฅ den hรคr sidan + + + + Install gog.com files + Installera filer frรฅn GOG.com + + + + Manual Installation + Manuell installation + + + + Installing... %p% + Facilitet... %p% + + + + If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically. + Om du redan har Heroes III-filerna pรฅ din enhet kan du vรคlja den hรคr mappen och VCMI kommer automatiskt att kopiera befintliga data. + + + + Copy existing files + Kopiera befintliga filer + + + + Your Heroes III data files have been successfully found. + Dina Heroes III-datafiler har hittats. + + + + If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. +Offline installer consists of two parts, .exe and .bin. Make sure you download both of them. + Om du รคger Heroes III frรฅn gog.com kan du ladda ner backup offlineinstallationsprogrammet frรฅn gog.com, och VCMI kommer att importera Heroes III-data med offlineinstallationsprogrammet. +Offlineinstallationsprogrammet bestรฅr av tvรฅ delar, .exe och .bin. Se till att ladda ner bรฅda. + + + + Install a translation of Heroes III in your preferred language + Installera en รถversรคttning av Heroes III pรฅ ditt fรถredragna sprรฅk + + + + Finish + Slutfรถr + + + + VCMI on Github + VCMI pรฅ Github + + + + VCMI on Discord + VCMI pรฅ Discord + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps that need to be completed. + +Please keep in mind that in order to use VCMI you must own the original data files for Heroesยฎ of Might and Magicยฎ III: Complete or The Shadow of Death. + +Heroesยฎ of Might and Magicยฎ III HD is currently not supported! + Tack fรถr att du installerade VCMI! + +Innan du kan bรถrja spela finns det nรฅgra steg kvar att slutfรถra. + +Tรคnk pรฅ att fรถr att anvรคnda VCMI mรฅste du ha originaldatafilerna fรถr Heroesยฎ of Might and Magicยฎ III: Complete eller The Shadow of Death. + +Heroesยฎ of Might and Magicยฎ III HD stรถds fรถr nรคrvarande inte! + + + + + Next + Nรคsta + + + + Search again + Sรถk igen + + + + Heroes III data files + Heroes III-datafiler + + + + Copy existing data + Kopiera befintliga data + + + + + Back + Retur + + + + Install VCMI Mod Preset + Installera VCMI Mod Preset + + + + Horn of the Abyss + Avgrundens horn + + + + Heroes III Translation + Heroes III รถversรคttning + + + + Interface Improvements + Utvecklingsgrรคnssnitt + + + + In The Wake of Gods + In The Wake of Gods + + + + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher + Valfritt kan du installera ytterligare mods antingen nu eller nรคr som helst senare med hjรคlp av VCMI Launcher + + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Installera moden som ger olika grรคnssnittsfรถrbรคttringar, sรฅsom ett bรคttre grรคnssnitt fรถr slumpmรคssiga kartor och valbara รฅtgรคrder i strider + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + Installera en kompatibel version av "Horn of the Abyss", en fantillverkad Heroes III-expansion som stรถds av VCMI-teamet + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + Installera en kompatibel version av "In The Wake of Gods", en fan-made Heroes III-expansion + + + + Heroes III installation found! + Heroes III-installationen hittades! + + + + Copy data to VCMI folder? + Kopiera datamappen till VCMI-mappen? + + + + Select %1 file... + param is file extension + Vรคlj filen %1... + + + + You have to select %1 file! + param is file extension + Du har valt filen %1! + + + + GOG file (*.*) + GOG-fil (*.*) + + + + File selection + Filval + + + + File cannot opened + Filen kan inte รถppnas + + + + Invalid file selected + Ogiltig vald fil + + + + GOG installer + GOG installationsprogram + + + + GOG data + GOG-data + + + + No Heroes III data! + Inga Heroes III-data! + + + + Selected files do not contain Heroes III data! + De valda filerna innehรฅller inte Heroes III-data! + + + + + + + Heroes III data not found! + Heroes III-data hittades inte! + + + + Failed to detect valid Heroes III data in chosen directory. +Please select directory with installed Heroes III data. + Det gรฅr inte att upptรคcka giltiga Heroes III-data i den valda katalogen, +Vรคlj en mapp dรคr Heroes III-data finns. + + + + You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer! + Du har tillhandahรฅllit installationsprogrammet fรถr GOG Galaxy! Den hรคr filen innehรฅller inte spelet. Ladda ner installationsprogrammet fรถr spelet offline! + + + + Stream error while extracting files! +error reason: + Strรถmfel vid extrahering av filer! +Orsak till fel: + + + + Not a supported Inno Setup installer! + Inno Setup-installationsprogram stรถds inte! + + + + Extracting error! + Extraktionsfel! + + + + Heroes III: HD Edition files are not supported by VCMI. +Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Heroes III HD Edition-filer stรถds inte av VCMI. +Vรคlj en mapp som innehรฅller data frรฅn Heroes III: Complete Edition eller Heroes III: Shadow of Death. + + + + Unknown or unsupported Heroes III version found. +Please select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death. + Okรคnd eller ostรถdd version av Heroes III. +Vรคlj en mapp som innehรฅller data frรฅn Heroes III: Complete Edition eller Heroes III: Shadow of Death. + + + + ImageViewer + + + Image Viewer + Bildvisare + + + + Language + + + Czech + Tjeckiska + + + + Chinese + kinesiska + + + + English + Engelska + + + + Finnish + finska + + + + French + Franska + + + + German + Tyska + + + + Hungarian + Ungerska + + + + Italian + Italienska + + + + Korean + Koreanska + + + + Polish + Polska + + + + Portuguese + Portugisiska + + + + Russian + ryska + + + + Spanish + Spanska + + + + Swedish + Svenska + + + + Turkish + Turkiska + + + + Ukrainian + Ukrainska + + + + Vietnamese + Vietnamesiska + + + + Auto (%1) + Auto (%1) + + + + MainWindow + + + VCMI Launcher + VCMI Launcher + + + + Mods + Mods + + + + Settings + Parametrar + + + + Help + Hjรคlp + + + + Map Editor + Kartredigerare + + + + Start game + Starta ett spel + + + + ModFields + + + Name + Namn + + + + Type + Typ + + + + QObject + + + Error starting executable + Fel vid start av kรถrbar fil + + + + Failed to start %1 +Reason: %2 + Startfel %1 +Orsak: %2 + + + + UpdateDialog + + + You have the latest version + Du har den senaste versionen + + + + Close + Stรคng + + + + Check for updates on startup + Sรถk efter uppdateringar vid start + + + + Network error + Nรคtverksfel + + + + Cannot read JSON from url or incorrect JSON data + Det gรฅr inte att lรคsa JSON-data frรฅn URL eller fel JSON-data + + + diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 41d810f61..1fa1b653e 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -67,22 +67,22 @@ Author - + Auteur Author contact (e.g. email) - + Contact de l'auteur (e.g. email) Map Creation Time - + Temps de Crรฉation de la carte Map Version - + Version de la carte @@ -149,37 +149,37 @@ Spells - Sorts + Sorts Customize spells - + Personnaliser les sorts Level 1 - + Niveau 1 Level 2 - + Niveau 2 Level 3 - + Niveau 3 Level 4 - + Niveau 4 Level 5 - + Niveau 5 @@ -581,12 +581,12 @@ Unsaved changes will be lost, are you sure? - + Les modifications non sauvegardรฉes seront perdues. รŠtes-vous sรปr ? Open map - Ouvrir la carte + Ouvrir une carte @@ -947,17 +947,17 @@ Can't place object - Impossible de placer l'objet + Impossible de placer l'objet There can only be one grail object on the map. - + Il ne peut y avoir qu'un objet Graal sur la carte. Hero %1 cannot be created as NEUTRAL. - + Le hรฉro %1 ne peut pas รชtre crรฉรฉ en tant que NEUTRE. @@ -1473,37 +1473,37 @@ Build all - + Construire tout Demolish all - + Dรฉtruire tout Enable all - + Autoriser tout Disable all - + Interdire tout Type - Type + Type Enabled - + Autorisรฉยทe Built - + Construitยทe @@ -1511,77 +1511,77 @@ Town event - + ร‰vรจnement de ville General - Gรฉnรฉral + Gรฉnรฉral Event name - Nom de l'รฉvรจnement + Nom de l'รฉvรจnement Type event message text - Taper le message d'รฉvรจnement + Taper le message d'รฉvรจnement Day of first occurrence - Jour de la premiรจre occurrence + Jour de la premiรจre occurrence Repeat after (0 = no repeat) - Rรฉcurrence (0 = pas de rรฉcurrence) + Rรฉoรฉter aprรจs (0 = pas de rรฉpรฉtition) Affected players - Joueurs affectรฉs + Joueurs affectรฉs affects human - afttecte les joueurs + afttecte les joueurs affects AI - affecte l'ordinateur + affecte l'ordinateur Resources - Resources + Ressources Buildings - Bรขtiments + Bรขtiments Creatures - Crรฉatures + Crรฉatures OK - + OK Creature level %1 / Creature level %1 Upgrade - + Crรฉature niveau %1 / Crรฉature niveau %1 Augmentรฉ Day %1 - %2 - + Jour %1 - %2 @@ -1589,32 +1589,32 @@ Town events - + ร‰vรจnements de ville Timed events - Evenements timรฉs + ร‰vรจnements temporels Add - Ajouter + Ajouter Remove - Supprimer + Supprimer Day %1 - %2 - + Jour %1 - %2 New event - Nouvel รฉvรจnement + Nouvel รฉvรจnement @@ -1622,17 +1622,17 @@ Spells - Sorts + Sorts Customize spells - + Personnaliser les sorts Level 1 - + Niveau 1 @@ -1641,7 +1641,7 @@ Spell that may appear in mage guild - + Sort qui peut apparaitre dans la Guilde des Mages @@ -1649,28 +1649,28 @@ - Spell that must appear in mage guild - + Sort qui doit apparaitre dans la Guilde des Mages + Level 2 - + Niveau 2 Level 3 - + Niveau 3 Level 4 - + Niveau 4 Level 5 - + Niveau 5 @@ -1762,17 +1762,17 @@ Spell scroll % 1 doesn't have instance assigned and must be removed - + Le dรฉfilement de sort %1 n'a pas d'instance assignรฉe et doit รชtre enlevรฉ Artifact % 1 is prohibited by map settings - + L'artรฉfact %1 est interdit par la configuration de la carte Player %1 has no towns and heroes assigned - + Le joueur %1 n'a pas de ville ni de hรฉro assignรฉ @@ -1963,32 +1963,32 @@ S (36x36) - + S (36x36) M (72x72) - + M (72x72) L (108x108) - + L (108x108) H (180x180) - + H (180x180) XH (216x216) - + XH (216x216) G (252x252) - + G (252x252) From 58cdbccf21814b4ffbee56ebe906bc5ab3f476a8 Mon Sep 17 00:00:00 2001 From: kdmcser Date: Tue, 10 Sep 2024 15:29:11 +0800 Subject: [PATCH 09/25] Update AI/BattleAI/BattleExchangeVariant.cpp Co-authored-by: Ivan Savenko --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 6f3bcf0ab..666279f1c 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -290,7 +290,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass( continue; auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); - auto turnsToKill = unit->getAvailableHealth() / vstd::amax(dmg, 1); + auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, 1); vstd::amin(turnsToKill, 100); From bb759dc2b8422e5e985afbacfaff90ad11e943f9 Mon Sep 17 00:00:00 2001 From: kdmcser Date: Wed, 11 Sep 2024 01:43:30 +0800 Subject: [PATCH 10/25] fix compile error --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 666279f1c..5a091fe5f 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -290,7 +290,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass( continue; auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); - auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, 1); + auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, 1LL); vstd::amin(turnsToKill, 100); From 312403962b1d3632f1cbb618bcc29cc1d56b0921 Mon Sep 17 00:00:00 2001 From: kdmcser Date: Wed, 11 Sep 2024 11:39:03 +0800 Subject: [PATCH 11/25] Update BattleExchangeVariant.cpp fix.compile error --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 5a091fe5f..c731e1b3e 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -290,7 +290,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass( continue; auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); - auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, 1LL); + auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, (int64_t)1); vstd::amin(turnsToKill, 100); From 785036836c5408681d2161c3e1ec77505a6b8829 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 30 Aug 2024 14:21:44 +0000 Subject: [PATCH 12/25] Rewardable objects may now define guards. Converted Crypt to rewardable. --- AI/VCAI/FuzzyHelper.cpp | 2 +- config/objects/creatureBanks.json | 76 ++++---- .../CRewardableConstructor.cpp | 23 +-- lib/mapObjects/CBank.cpp | 19 -- lib/mapObjects/CRewardableObject.cpp | 164 ++++++++++++++---- lib/mapObjects/CRewardableObject.h | 11 ++ lib/mapObjects/CreatureBank.cpp | 0 lib/mapObjects/CreatureBank.h | 0 lib/networkPacks/NetPacksLib.cpp | 1 + lib/rewardable/Configuration.h | 3 +- lib/rewardable/Info.cpp | 12 ++ lib/rewardable/Reward.h | 3 + 12 files changed, 209 insertions(+), 105 deletions(-) create mode 100644 lib/mapObjects/CreatureBank.cpp create mode 100644 lib/mapObjects/CreatureBank.h diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index 776c3b98f..332610ac6 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -302,6 +302,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) return iat.army.getStrength(); } case Obj::MONSTER: + case Obj::CRYPT: { //TODO!!!!!!!! const CGCreature * cre = dynamic_cast(obj); @@ -319,7 +320,6 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) const CArmedInstance * a = dynamic_cast(obj); return a->getArmyStrength(); } - case Obj::CRYPT: //crypt case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index ffb7a9c79..fd8786c53 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -833,7 +833,7 @@ }, "crypt" : { "index" :84, - "handler": "bank", + "handler": "configurable", "base" : { "sounds" : { "ambient" : ["LOOPDEAD"], @@ -843,16 +843,27 @@ "types" : { "crypt" : { "index" : 0, - "resetDuration" : 0, "name" : "Crypt", "aiValue" : 1500, + "rmg" : { "value" : 1000, "rarity" : 100 }, - "levels": [ + + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 119, // Do you want to search the graves? + "onVisited" : [ { - "chance": 30, + "message" : 120, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ] + } + ], + + "rewards": [ + { + "appearChance" : { "min" : 0, "max" : 30 }, "guards": [ { "amount": 10, "type": "skeleton" }, { "amount": 10, "type": "walkingDead" }, @@ -860,17 +871,14 @@ { "amount": 10, "type": "skeleton" }, { "amount": 10, "type": "skeleton" } ], - "combat_value": 75, - "reward" : { - "value": 1500, - "resources": - { - "gold" : 1500 - } + "message" : 121, // you search the graves and find something + "resources": + { + "gold" : 1500 } }, { - "chance": 30, + "appearChance" : { "min" : 30, "max" : 60 }, "guards": [ { "amount": 13, "type": "skeleton" }, { "amount": 10, "type": "walkingDead" }, @@ -878,50 +886,42 @@ { "amount": 10, "type": "walkingDead" }, { "amount": 12, "type": "skeleton" } ], - "combat_value": 94, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 2000 - } + "message" : 121, // you search the graves and find something + "resources": + { + "gold" : 2000 } }, { - "chance": 30, + "appearChance" : { "min" : 60, "max" : 90 }, "guards": [ { "amount": 20, "type": "skeleton" }, { "amount": 20, "type": "walkingDead" }, { "amount": 10, "type": "wight" }, { "amount": 5, "type": "vampire" } ], - "combat_value": 169, - "reward" : { - "value": 3500, - "resources": - { - "gold" : 2500 - }, - "artifacts": [ { "class" : "TREASURE" } ] + "message" : 121, // you search the graves and find something + "resources": + { + "gold" : 2500 + }, + "artifacts": [ { "class" : "TREASURE" } ] } }, { - "chance": 10, + "appearChance" : { "min" : 90, "max" : 100 }, "guards": [ { "amount": 20, "type": "skeleton" }, { "amount": 20, "type": "walkingDead" }, { "amount": 10, "type": "wight" }, { "amount": 10, "type": "vampire" } ], - "combat_value": 225, - "reward" : { - "value": 6000, - "resources": - { - "gold" : 5000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "message" : 121, // you search the graves and find something + "resources": + { + "gold" : 5000 + }, + "artifacts": [ { "class" : "TREASURE" } ] } ] } diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 90da5925b..3fbcba73d 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -62,17 +62,20 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RNG & rng) const { - if(auto * rewardableObject = dynamic_cast(object)) - { - rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID); + auto * rewardableObject = dynamic_cast(object); - if (rewardableObject->configuration.info.empty()) - { - if (objectInfo.getParameters()["rewards"].isNull()) - logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey()); - else - logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey()); - } + if (!rewardableObject) + throw std::runtime_error("Object " + std::to_string(object->getObjGroupIndex()) + ", " + std::to_string(object->getObjTypeIndex()) + " is not a rewardable object!" ); + + rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID); + rewardableObject->initializeGuards(); + + if (rewardableObject->configuration.info.empty()) + { + if (objectInfo.getParameters()["rewards"].isNull()) + logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey()); + else + logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey()); } } diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 28d8264f7..beed5a4ae 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -15,7 +15,6 @@ #include #include "../texts/CGeneralTextHandler.h" -#include "../CSoundBase.h" #include "../IGameSettings.h" #include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -156,23 +155,18 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const case Obj::DRAGON_UTOPIA: banktext = 47; break; - case Obj::CRYPT: - banktext = 119; - break; case Obj::SHIPWRECK: banktext = 122; break; case Obj::PYRAMID: banktext = 105; break; - case Obj::CREATURE_BANK: default: banktext = 32; break; } BlockingDialog bd(true, false); bd.player = h->getOwner(); - bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); bd.components = getPopupComponents(h->getOwner()); if (banktext == 32) @@ -196,17 +190,12 @@ void CBank::doVisit(const CGHeroInstance * hero) const case Obj::DERELICT_SHIP: textID = 43; break; - case Obj::CRYPT: - textID = 121; - break; case Obj::SHIPWRECK: textID = 124; break; case Obj::PYRAMID: textID = 106; break; - case Obj::CREATURE_BANK: - case Obj::DRAGON_UTOPIA: default: textID = 34; break; @@ -218,7 +207,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const { case Obj::SHIPWRECK: case Obj::DERELICT_SHIP: - case Obj::CRYPT: { GiveBonus gbonus; gbonus.id = hero->id; @@ -237,14 +225,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const textID = 42; gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.101"); break; - case Obj::CRYPT: - textID = 120; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.98"); - break; } cb->giveHeroBonus(&gbonus); iw.components.emplace_back(ComponentType::MORALE, -1); - iw.soundID = soundBase::invalid; break; } case Obj::PYRAMID: @@ -258,8 +241,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const iw.components.emplace_back(ComponentType::LUCK, -2); break; } - case Obj::CREATURE_BANK: - case Obj::DRAGON_UTOPIA: default: iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty iw.text.replaceRawString(getObjectName()); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index e0ebe2d8f..5f5203740 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -10,16 +10,19 @@ #include "StdInc.h" #include "CRewardableObject.h" -#include "../gameState/CGameState.h" -#include "../texts/CGeneralTextHandler.h" + #include "../CPlayerState.h" +#include "../GameSettings.h" #include "../IGameCallback.h" +#include "../gameState/CGameState.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" #include "../mapObjects/CGHeroInstance.h" #include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../serializer/JsonSerializeFormat.h" +#include "../texts/CGeneralTextHandler.h" #include @@ -91,7 +94,42 @@ std::vector CRewardableObject::loadComponents(const CGHeroInstance * return result; } +bool CRewardableObject::guardedPotentially() const +{ + for (auto const & visitInfo : configuration.info) + if (!visitInfo.reward.guards.empty()) + return true; + + return false; +} + +bool CRewardableObject::guardedPresently() const +{ + return stacksCount() > 0; +} + void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const +{ + if (guardedPresently()) + { + auto guardedIndexes = getAvailableRewards(h, Rewardable::EEventType::EVENT_GUARDED); + auto guardedReward = configuration.info.at(guardedIndexes.at(0)); + + // ask player to confirm attack + BlockingDialog bd(true, false); + bd.player = h->getOwner(); + bd.text = guardedReward.message; + bd.components = getPopupComponents(h->getOwner()); + + cb->showBlockingDialog(&bd); + } + else + { + doHeroVisit(h); + } +} + +void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const { if(!wasVisitedBefore(h)) { @@ -181,39 +219,54 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero); } -void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const +void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { - if(answer == 0) + if (result.winner == BattleSide::ATTACKER) { - 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 + doHeroVisit(hero); } +} - if(answer > 0 && answer-1 < configuration.info.size()) +void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const +{ + if(guardedPresently()) { - auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - markAsVisited(hero); - grantReward(list[answer - 1], hero); + if (answer) + cb->startBattleI(hero, this, true); } else { - throw std::runtime_error("Unhandled choice"); + 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()) + { + auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + markAsVisited(hero); + grantReward(list[answer - 1], hero); + } + else + { + throw std::runtime_error("Unhandled choice"); + } } } @@ -368,19 +421,41 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (!configuration.showScoutedPreview) return {}; - auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info.empty()) + if (guardedPresently()) { - // Object has valid config, but current hero has no rewards that he can receive. - // Usually this happens if hero has already visited this object -> show reward using context without any hero - // since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level - return loadComponents(nullptr, {0}); + if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + return {}; + + std::map guardsAmounts; + std::vector result; + + for (auto const & slot : Slots()) + if (slot.second) + guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); + + for (auto const & guard : guardsAmounts) + { + Component comp(ComponentType::CREATURE, guard.first, guard.second); + result.push_back(comp); + } + return result; } + else + { + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() && !configuration.info.empty()) + { + // Object has valid config, but current hero has no rewards that he can receive. + // Usually this happens if hero has already visited this object -> show reward using context without any hero + // since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level + return loadComponents(nullptr, {0}); + } - if (rewardIndices.empty()) - return {}; + if (rewardIndices.empty()) + return {}; - return loadComponents(hero, rewardIndices); + return loadComponents(hero, rewardIndices); + } } std::vector CRewardableObject::getPopupComponents(PlayerColor player) const @@ -440,4 +515,21 @@ void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeStruct("rewardable", static_cast(*this)); } +void CRewardableObject::initializeGuards() +{ + clearSlots(); + + for (auto const & visitInfo : configuration.info) + { + for (auto const & guard : visitInfo.reward.guards) + { + auto slotID = getFreeSlot(); + if (!slotID.validSlot()) + return; + + putStack(slotID, new CStackInstance(guard.getId(), guard.getCount())); + } + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 5e424db2a..7aed36cce 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -45,7 +45,14 @@ protected: std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const; std::vector getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const; + void doHeroVisit(const CGHeroInstance *h) const; + + /// Returns true if this object might have guards present, whether they were cleared or not + bool guardedPotentially() const; + /// Returns true if this object is currently guarded + bool guardedPresently() const; public: + /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; bool wasVisited(const CGHeroInstance * h) const override; @@ -56,6 +63,8 @@ public: /// gives reward to player or ask for choice in case of multiple rewards void onHeroVisit(const CGHeroInstance *h) const override; + void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; + ///possibly resets object state void newTurn(vstd::RNG & rand) const override; @@ -66,6 +75,8 @@ public: void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override; void initObj(vstd::RNG & rand) override; + + void initializeGuards(); void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; diff --git a/lib/mapObjects/CreatureBank.cpp b/lib/mapObjects/CreatureBank.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mapObjects/CreatureBank.h b/lib/mapObjects/CreatureBank.h new file mode 100644 index 000000000..e69de29bb diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index f9956a00b..6e2be8e19 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2434,6 +2434,7 @@ void SetRewardableConfiguration::applyGs(CGameState *gs) auto * rewardablePtr = dynamic_cast(objectPtr); assert(rewardablePtr); rewardablePtr->configuration = configuration; + rewardablePtr->initializeGuards(); } else { diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index fd8f8230d..a6237f4ca 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -44,7 +44,8 @@ enum class EEventType EVENT_INVALID = 0, EVENT_FIRST_VISIT, EVENT_ALREADY_VISITED, - EVENT_NOT_AVAILABLE + EVENT_NOT_AVAILABLE, + EVENT_GUARDED }; constexpr std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"}; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index d1ebba30c..9805f82e1 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -174,6 +174,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, vstd: reward.removeObject = source["removeObject"].Bool(); reward.bonuses = randomizer.loadBonuses(source["bonuses"]); + reward.guards = randomizer.loadCreatures(source["guards"], rng, variables); + reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables); reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables); @@ -378,6 +380,16 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd: object.info.push_back(onEmpty); } + if (!parameters["onGuardedMessage"].isNull()) + { + Rewardable::VisitInfo onGuarded; + onGuarded.visitType = Rewardable::EEventType::EVENT_GUARDED; + onGuarded.message = loadMessage(parameters["onGuardedMessage"], TextIdentifier(objectTextID, "onGuarded")); + replaceTextPlaceholders(onGuarded.message, object.variables); + + object.info.push_back(onGuarded); + } + configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]); object.canRefuse = parameters["canRefuse"].Bool(); diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index a7410a241..1f279a61d 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -82,6 +82,9 @@ struct DLL_LINKAGE Reward final /// fixed value, in form of percentage from max si32 movePercentage; + /// Guards that must be defeated in order to access this reward, empty if not guarded + std::vector guards; + /// list of bonuses, e.g. morale/luck std::vector bonuses; From cb5df096c122c1d28d22f05daf110a57b0ad987e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 30 Aug 2024 15:24:13 +0000 Subject: [PATCH 13/25] Added per-team tracking of scouted state of an object --- lib/CPlayerState.h | 5 +++ lib/mapObjects/CBank.cpp | 2 +- lib/mapObjects/CRewardableObject.cpp | 51 +++++++++----------------- lib/networkPacks/NetPacksLib.cpp | 35 ++++++++---------- lib/networkPacks/PacksForClient.h | 10 ++--- lib/serializer/ESerializationVersion.h | 3 +- 6 files changed, 47 insertions(+), 59 deletions(-) diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index f62b27bef..3da93f139 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -153,6 +153,8 @@ public: //TODO: boost::array, bool if possible boost::multi_array fogOfWarMap; //[z][x][y] true - visible, false - hidden + std::set scoutedObjects; + TeamState(); template void serialize(Handler &h) @@ -173,6 +175,9 @@ public: h & fogOfWarMap; h & static_cast(*this); + + if (h.version >= Handler::Version::TEAM_STATE_SCOUTED_OBJECTS) + h & scoutedObjects; } }; diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index beed5a4ae..8ec1baa9c 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -137,7 +137,7 @@ bool CBank::wasVisited (PlayerColor player) const void CBank::onHeroVisit(const CGHeroInstance * h) const { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); cb->sendAndApply(&cov); if(!bankConfig && (ID.toEnum() == Obj::CREATURE_BANK || ID.toEnum() == Obj::DRAGON_UTOPIA)) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 5f5203740..674edf70b 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -108,24 +108,30 @@ bool CRewardableObject::guardedPresently() const return stacksCount() > 0; } -void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const +void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const { + if(!wasScouted(hero->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_SCOUTED, id, hero->id); + cb->sendAndApply(&cov); + } + if (guardedPresently()) { - auto guardedIndexes = getAvailableRewards(h, Rewardable::EEventType::EVENT_GUARDED); + auto guardedIndexes = getAvailableRewards(hero, Rewardable::EEventType::EVENT_GUARDED); auto guardedReward = configuration.info.at(guardedIndexes.at(0)); // ask player to confirm attack BlockingDialog bd(true, false); - bd.player = h->getOwner(); + bd.player = hero->getOwner(); bd.text = guardedReward.message; - bd.components = getPopupComponents(h->getOwner()); + bd.components = getPopupComponents(hero->getOwner()); cb->showBlockingDialog(&bd); } else { - doHeroVisit(h); + doHeroVisit(hero); } } @@ -192,7 +198,7 @@ void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); cb->sendAndApply(&cov); } } @@ -202,7 +208,7 @@ void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const if (!wasVisited(h->getOwner())) { - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); cb->sendAndApply(&cov); } @@ -236,27 +242,6 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int3 } else { - 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()) { auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); @@ -274,7 +259,7 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const { cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); - ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_HERO, id, hero->id); cb->sendAndApply(&cov); } @@ -330,7 +315,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const bool CRewardableObject::wasScouted(PlayerColor player) const { - return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); + return vstd::contains(cb->getPlayerTeam(player)->scoutedObjects, ObjectInstanceID(id)); } bool CRewardableObject::wasVisited(const CGHeroInstance * h) const @@ -418,9 +403,6 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (!wasScouted(player)) return {}; - if (!configuration.showScoutedPreview) - return {}; - if (guardedPresently()) { if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) @@ -442,6 +424,9 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla } else { + if (!configuration.showScoutedPreview) + return {}; + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); if (rewardIndices.empty() && !configuration.info.empty()) { diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 6e2be8e19..bccbbef61 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1035,32 +1035,32 @@ void ChangeObjPos::applyGs(CGameState *gs) void ChangeObjectVisitors::applyGs(CGameState *gs) { switch (mode) { - case VISITOR_ADD: + case VISITOR_ADD_HERO: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); gs->getHero(hero)->visitedObjects.insert(object); gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object); break; - case VISITOR_ADD_TEAM: - { - TeamState *ts = gs->getPlayerTeam(gs->getHero(hero)->tempOwner); - for(const auto & color : ts->players) - { - gs->getPlayerState(color)->visitedObjects.insert(object); - } - } + case VISITOR_ADD_PLAYER: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); + for(const auto & color : gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->players) + gs->getPlayerState(color)->visitedObjects.insert(object); + break; case VISITOR_CLEAR: + // remove visit info from all heroes, including those that are not present on map for (CGHeroInstance * hero : gs->map->allHeroes) - { if (hero) - { - hero->visitedObjects.erase(object); // remove visit info from all heroes, including those that are not present on map - } - } + hero->visitedObjects.erase(object); for(auto &elem : gs->players) - { elem.second.visitedObjects.erase(object); - } + + for(auto &elem : gs->teams) + elem.second.scoutedObjects.erase(object); + + break; + case VISITOR_SCOUTED: + gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object); break; case VISITOR_GLOBAL: @@ -1069,9 +1069,6 @@ void ChangeObjectVisitors::applyGs(CGameState *gs) gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID}); break; } - case VISITOR_REMOVE: - gs->getHero(hero)->visitedObjects.erase(object); - break; } } diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 145bdd19e..5a81ec806 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1216,11 +1216,11 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient { enum VisitMode { - VISITOR_ADD, // mark hero as one that have visited this object - VISITOR_ADD_TEAM, // mark team as one that have visited this object - VISITOR_GLOBAL, // mark player as one that have visited object of this type - VISITOR_REMOVE, // unmark visitor, reversed to ADD - VISITOR_CLEAR // clear all visitors from this object (object reset) + VISITOR_ADD_HERO, // mark hero as one that have visited this object + VISITOR_ADD_PLAYER, // mark player as one that have visited this object instance + VISITOR_GLOBAL, // mark player as one that have visited object of this type + VISITOR_SCOUTED, // marks targeted team as having scouted this object + VISITOR_CLEAR, // clear all visitors from this object (object reset) }; VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum ObjectInstanceID object; diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index a2f9ebe97..3a835ef1e 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -59,6 +59,7 @@ enum class ESerializationVersion : int32_t CHRONICLES_SUPPORT, // 860 - support for heroes chronicles PER_MAP_GAME_SETTINGS, // 861 - game settings are now stored per-map CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video + REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects - CURRENT = CAMPAIGN_OUTRO_SUPPORT + CURRENT = REWARDABLE_BANKS }; From 503b87561e8bff15081a26c2897c1fd0b9c494a8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 30 Aug 2024 16:24:58 +0000 Subject: [PATCH 14/25] Converted all h3 banks to rewardable, remove most of hardcoded checks --- AI/Nullkiller/Engine/FuzzyEngines.cpp | 6 - AI/Nullkiller/Engine/FuzzyHelper.cpp | 24 +- AI/Nullkiller/Engine/FuzzyHelper.h | 8 - AI/VCAI/AIUtility.cpp | 1 - AI/VCAI/FuzzyEngines.cpp | 6 - AI/VCAI/FuzzyHelper.cpp | 27 +- AI/VCAI/FuzzyHelper.h | 8 - AI/VCAI/VCAI.cpp | 2 - config/objects/creatureBanks.json | 1288 ++++++++--------- config/schemas/rewardable.json | 6 + .../CommonConstructors.h | 1 - lib/mapObjects/CBank.cpp | 122 +- lib/mapObjects/CRewardableObject.h | 7 - lib/mapObjects/MapObjects.h | 1 - lib/mapping/MapFormatH3M.cpp | 2 +- 15 files changed, 633 insertions(+), 876 deletions(-) diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index a7fb1b26c..7659f1352 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -208,12 +208,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c enemyFlyers->setValue(enemyStructure.flyers); enemySpeed->setValue(enemyStructure.maxSpeed); - bool bank = dynamic_cast(enemy); - if(bank) - bankPresent->setValue(1); - else - bankPresent->setValue(0); - const CGTownInstance * fort = dynamic_cast(enemy); if(fort) castleWalls->setValue(fort->fortLevel()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index ed7abb7c1..9188ed66f 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -20,25 +20,6 @@ namespace NKAI { -ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) -{ - //this one is not fuzzy anymore, just calculate weighted average - - auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); - - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - - ui64 totalStrength = 0; - ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards(bank->cb)) - { - totalStrength += config.second.totalStrength * config.first; - totalChance += config.first; - } - return totalStrength / std::max(totalChance, 1); //avoid division by zero - -} - ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards) { auto cb = ai->cb.get(); @@ -171,14 +152,11 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship + case Obj::PYRAMID: { const CArmedInstance * a = dynamic_cast(obj); return a->getArmyStrength(); } - case Obj::PYRAMID: - { - return estimateBankDanger(dynamic_cast(obj)); - } default: return 0; } diff --git a/AI/Nullkiller/Engine/FuzzyHelper.h b/AI/Nullkiller/Engine/FuzzyHelper.h index b203916ad..455da61e6 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.h +++ b/AI/Nullkiller/Engine/FuzzyHelper.h @@ -10,12 +10,6 @@ #pragma once #include "FuzzyEngines.h" -VCMI_LIB_NAMESPACE_BEGIN - -class CBank; - -VCMI_LIB_NAMESPACE_END - namespace NKAI { @@ -30,8 +24,6 @@ private: public: FuzzyHelper(const Nullkiller * ai): ai(ai) {} - ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class? - ui64 evaluateDanger(const CGObjectInstance * obj); ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true); }; diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index ac68fa71b..a7c4c2f7a 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -16,7 +16,6 @@ #include "../../lib/UnlockGuard.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/mapObjects/CBank.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapping/CMapDefines.h" diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 95c3dc4a9..05db1b15a 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c enemyFlyers->setValue(enemyStructure.flyers); enemySpeed->setValue(enemyStructure.maxSpeed); - bool bank = dynamic_cast(enemy); - if(bank) - bankPresent->setValue(1); - else - bankPresent->setValue(0); - const CGTownInstance * fort = dynamic_cast(enemy); if(fort) castleWalls->setValue(fort->fortLevel()); diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index 332610ac6..ce51b2f7d 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -16,7 +16,6 @@ #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h" -#include "../../lib/mapObjects/CBank.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGDwelling.h" #include "../../lib/gameState/InfoAboutArmy.h" @@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) return result; } -ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) -{ - //this one is not fuzzy anymore, just calculate weighted average - - auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); - - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - - ui64 totalStrength = 0; - ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards(bank->cb)) - { - totalStrength += config.second.totalStrength * config.first; - totalChance += config.first; - } - return totalStrength / std::max(totalChance, 1); //avoid division by zero - -} - float FuzzyHelper::evaluate(Goals::VisitTile & g) { if(g.parent) @@ -302,7 +282,6 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) return iat.army.getStrength(); } case Obj::MONSTER: - case Obj::CRYPT: { //TODO!!!!!!!! const CGCreature * cre = dynamic_cast(obj); @@ -320,12 +299,16 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) const CArmedInstance * a = dynamic_cast(obj); return a->getArmyStrength(); } + case Obj::CRYPT: case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); + { + const CArmedInstance * a = dynamic_cast(obj); + return a->getArmyStrength(); + } default: return 0; } diff --git a/AI/VCAI/FuzzyHelper.h b/AI/VCAI/FuzzyHelper.h index 7542e7f70..dc0709a67 100644 --- a/AI/VCAI/FuzzyHelper.h +++ b/AI/VCAI/FuzzyHelper.h @@ -10,12 +10,6 @@ #pragma once #include "FuzzyEngines.h" -VCMI_LIB_NAMESPACE_BEGIN - -class CBank; - -VCMI_LIB_NAMESPACE_END - class DLL_EXPORT FuzzyHelper { public: @@ -42,8 +36,6 @@ public: float evaluate(Goals::AbstractGoal & g); void setPriority(Goals::TSubgoal & g); - ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class? - Goals::TSubgoal chooseSolution(Goals::TGoalVec vec); //std::shared_ptr chooseSolution (std::vector> & vec); diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 4da3ce9ef..3757c56d3 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -2750,8 +2750,6 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) if(dynamic_cast(obj)) return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; switch(obj->ID) { diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index fd8786c53..f05744014 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -1,7 +1,7 @@ { "creatureBank" : { "index" :16, - "handler": "bank", + "handler" : "configurable", "lastReservedIndex" : 6, "base" : { "sounds" : { @@ -21,106 +21,97 @@ "value" : 3000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 4, "type": "cyclop" }, - { "amount": 4, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" } ], - "combat_value": 506, - "reward" : { - "value": 10000, - "resources": - { - "wood" : 4, - "mercury" : 4, - "ore" : 4, - "sulfur" : 4, - "crystal" : 4, - "gems" : 4 - } + "resources" : + { + "wood" : 4, + "mercury" : 4, + "ore" : 4, + "sulfur" : 4, + "crystal" : 4, + "gems" : 4 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 6, "type": "cyclop" }, - { "amount": 6, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" } ], - "combat_value": 760, - "reward" : { - "value": 15000, - "resources": - { - "wood" : 6, - "mercury" : 6, - "ore" : 6, - "sulfur" : 6, - "crystal" : 6, - "gems" : 6 - } + "resources" : + { + "wood" : 6, + "mercury" : 6, + "ore" : 6, + "sulfur" : 6, + "crystal" : 6, + "gems" : 6 } }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 8, "type": "cyclop" }, - { "amount": 8, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" } ], - "combat_value": 1013, - "reward" : { - "value": 20000, - "resources": - { - "wood" : 8, - "mercury" : 8, - "ore" : 8, - "sulfur" : 8, - "crystal" : 8, - "gems" : 8 - } + "resources" : + { + "wood" : 8, + "mercury" : 8, + "ore" : 8, + "sulfur" : 8, + "crystal" : 8, + "gems" : 8 } }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop", "upgradeChance": 50 }, - { "amount": 10, "type": "cyclop" }, - { "amount": 10, "type": "cyclop" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" } ], - "combat_value": 1266, - "reward" : { - "value": 25000, - "resources": - { - "wood" : 10, - "mercury" : 10, - "ore" : 10, - "sulfur" : 10, - "crystal" : 10, - "gems" : 10 - } + "resources" : + { + "wood" : 10, + "mercury" : 10, + "ore" : 10, + "sulfur" : 10, + "crystal" : 10, + "gems" : 10 } } ] }, "dwarvenTreasury" : { "index" : 1, - "resetDuration" : 0, "name" : "Dwarven Treasury", "aiValue" : 2000, "sounds" : { @@ -130,88 +121,79 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 10, "type": "dwarf" }, - { "amount": 10, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "dwarf" }, + { "amount" : 10, "type" : "dwarf" } ], - "combat_value": 194, - "reward" : { - "value": 3500, - "resources": - { - "crystal" : 2, - "gold" : 2500 - } + "resources" : + { + "crystal" : 2, + "gold" : 2500 } }, { - "chance": 30, - "guards": [ - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 15, "type": "dwarf" }, - { "amount": 15, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 15, "type" : "dwarf" }, + { "amount" : 15, "type" : "dwarf" } ], - "combat_value": 291, - "reward" : { - "value": 5500, - "resources": - { - "crystal" : 3, - "gold" : 4000 - } + "resources" : + { + "crystal" : 3, + "gold" : 4000 } }, { - "chance": 30, - "guards": [ - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 20, "type": "dwarf" }, - { "amount": 20, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "dwarf" }, + { "amount" : 20, "type" : "dwarf" } ], - "combat_value": 388, - "reward" : { - "value": 7500, - "resources": - { - "crystal" : 5, - "gold" : 5000 - } + "resources" : + { + "crystal" : 5, + "gold" : 5000 } }, { - "chance": 10, - "guards": [ - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf", "upgradeChance": 50 }, - { "amount": 30, "type": "dwarf" }, - { "amount": 30, "type": "dwarf" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "dwarf" }, + { "amount" : 30, "type" : "dwarf" } ], - "combat_value": 582, - "reward" : { - "value": 12500, - "resources": - { - "crystal" : 10, - "gold" : 7500 - } + "resources" : + { + "crystal" : 10, + "gold" : 7500 } } ] }, "griffinConservatory" : { "index" : 2, - "resetDuration" : 0, "name" : "Griffin Conservatory", "aiValue" : 9000, "sounds" : { @@ -221,72 +203,63 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin", "upgradeChance": 50 }, - { "amount": 10, "type": "griffin" }, - { "amount": 10, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "griffin" }, + { "amount" : 10, "type" : "griffin" } ], - "combat_value": 351, - "reward" : { - "value": 3000, - "creatures": [ { "amount": 1, "type": "angel" } ] - } + "creatures" : [ { "amount" : 1, "type" : "angel" } ] }, { - "chance": 30, - "guards": [ - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin", "upgradeChance": 50 }, - { "amount": 20, "type": "griffin" }, - { "amount": 20, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "griffin" }, + { "amount" : 20, "type" : "griffin" } ], - "combat_value": 702, - "reward" : { - "value": 6000, - "creatures": [ { "amount": 2, "type": "angel" } ] - } + "creatures" : [ { "amount" : 2, "type" : "angel" } ] }, { - "chance": 30, - "guards": [ - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin", "upgradeChance": 50 }, - { "amount": 30, "type": "griffin" }, - { "amount": 30, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "griffin" }, + { "amount" : 30, "type" : "griffin" } ], - "combat_value": 1053, - "reward" : { - "value": 9000, - "creatures": [ { "amount": 3, "type": "angel" } ] - } + "creatures" : [ { "amount" : 3, "type" : "angel" } ] }, { - "chance": 10, - "guards": [ - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin", "upgradeChance": 50 }, - { "amount": 40, "type": "griffin" }, - { "amount": 40, "type": "griffin" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin", "upgradeChance" : 50 }, + { "amount" : 40, "type" : "griffin" }, + { "amount" : 40, "type" : "griffin" } ], - "combat_value": 1404, - "reward" : { - "value": 12000, - "creatures": [ { "amount": 4, "type": "angel" } ] - } + "creatures" : [ { "amount" : 4, "type" : "angel" } ] } ] }, "inpCache" : { "index" : 3, - "resetDuration" : 0, "name" : "Imp Cache", "aiValue" : 1500, "sounds" : { @@ -296,87 +269,78 @@ "value" : 5000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp", "upgradeChance": 50 }, - { "amount": 20, "type": "imp" }, - { "amount": 20, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 20, "type" : "imp" }, + { "amount" : 20, "type" : "imp" } ], - "combat_value": 100, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 1000 - } + "resources" : + { + "gold" : 1000 } }, { - "chance": 30, - "guards": [ - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp", "upgradeChance": 50 }, - { "amount": 30, "type": "imp" }, - { "amount": 30, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 30, "type" : "imp" }, + { "amount" : 30, "type" : "imp" } ], - "combat_value": 150, - "reward" : { - "value": 3000, - "resources": - { - "mercury" : 3, - "gold" : 1500 - } + "resources" : + { + "mercury" : 3, + "gold" : 1500 } }, { - "chance": 30, - "guards": [ - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp", "upgradeChance": 50 }, - { "amount": 40, "type": "imp" }, - { "amount": 40, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 40, "type" : "imp" }, + { "amount" : 40, "type" : "imp" } ], - "combat_value": 200, - "reward" : { - "value": 4000, - "resources": - { - "mercury" : 4, - "gold" : 2000 - } + "resources" : + { + "mercury" : 4, + "gold" : 2000 } }, { - "chance": 10, - "guards": [ - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp", "upgradeChance": 50 }, - { "amount": 60, "type": "imp" }, - { "amount": 60, "type": "imp" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp", "upgradeChance" : 50 }, + { "amount" : 60, "type" : "imp" }, + { "amount" : 60, "type" : "imp" } ], - "combat_value": 300, - "reward" : { - "value": 6000, - "resources": - { - "mercury" : 6, - "gold" : 3000 - } + "resources" : + { + "mercury" : 6, + "gold" : 3000 } } ] }, "medusaStore" : { "index" : 4, - "resetDuration" : 0, "name" : "Medusa Stores", "aiValue" : 1500, "sounds" : { @@ -386,88 +350,79 @@ "value" : 1500, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa", "upgradeChance": 50 }, - { "amount": 4, "type": "medusa" }, - { "amount": 4, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "medusa" }, + { "amount" : 4, "type" : "medusa" } ], - "combat_value": 207, - "reward" : { - "value": 4500, - "resources": - { - "sulfur" : 5, - "gold" : 2000 - } + "resources" : + { + "sulfur" : 5, + "gold" : 2000 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa", "upgradeChance": 50 }, - { "amount": 6, "type": "medusa" }, - { "amount": 6, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "medusa" }, + { "amount" : 6, "type" : "medusa" } ], - "combat_value": 310, - "reward" : { - "value": 6000, - "resources": - { - "sulfur" : 6, - "gold" : 3000 - } + "resources" : + { + "sulfur" : 6, + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa", "upgradeChance": 50 }, - { "amount": 8, "type": "medusa" }, - { "amount": 8, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "medusa" }, + { "amount" : 8, "type" : "medusa" } ], - "combat_value": 414, - "reward" : { - "value": 8000, - "resources": - { - "sulfur" : 8, - "gold" : 4000 - } + "resources" : + { + "sulfur" : 8, + "gold" : 4000 } }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa", "upgradeChance": 50 }, - { "amount": 10, "type": "medusa" }, - { "amount": 10, "type": "medusa" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "medusa" }, + { "amount" : 10, "type" : "medusa" } ], - "combat_value": 517, - "reward" : { - "value": 10000, - "resources": - { - "sulfur" : 10, - "gold" : 5000 - } + "resources" : + { + "sulfur" : 10, + "gold" : 5000 } } ] }, "nagaBank" : { "index" : 5, - "resetDuration" : 0, "name" : "Naga Bank", "aiValue" : 3000, "sounds" : { @@ -477,88 +432,79 @@ "value" : 3000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga", "upgradeChance": 50 }, - { "amount": 2, "type": "naga" }, - { "amount": 2, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 2, "type" : "naga" }, + { "amount" : 2, "type" : "naga" } ], - "combat_value": 403, - "reward" : { - "value": 8000, - "resources": - { - "gems" : 8, - "gold" : 4000 - } + "resources" : + { + "gems" : 8, + "gold" : 4000 } }, { - "chance": 30, - "guards": [ - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga", "upgradeChance": 50 }, - { "amount": 3, "type": "naga" }, - { "amount": 3, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 3, "type" : "naga" }, + { "amount" : 3, "type" : "naga" } ], - "combat_value": 605, - "reward" : { - "value": 12000, - "resources": - { - "gems" : 12, - "gold" : 6000 - } + "resources" : + { + "gems" : 12, + "gold" : 6000 } }, { - "chance": 30, - "guards": [ - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga", "upgradeChance": 50 }, - { "amount": 4, "type": "naga" }, - { "amount": 4, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "naga" }, + { "amount" : 4, "type" : "naga" } ], - "combat_value": 806, - "reward" : { - "value": 16000, - "resources": - { - "gems" : 16, - "gold" : 8000 - } + "resources" : + { + "gems" : 16, + "gold" : 8000 } }, { - "chance": 10, - "guards": [ - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga", "upgradeChance": 50 }, - { "amount": 6, "type": "naga" }, - { "amount": 6, "type": "naga" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "naga" }, + { "amount" : 6, "type" : "naga" } ], - "combat_value": 1210, - "reward" : { - "value": 24000, - "resources": - { - "gems" : 24, - "gold" : 12000 - } + "resources" : + { + "gems" : 24, + "gold" : 12000 } } ] }, "dragonFlyHive" : { "index" : 6, - "resetDuration" : 0, "name" : "Dragon Fly Hive", "aiValue" : 9000, "sounds" : { @@ -568,66 +514,58 @@ "value" : 9000, "rarity" : 100 }, - "levels": [ + "onGuardedMessage" : 32, + "onVisitedMessage" : 33, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" }, - { "amount": 6, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" }, + { "amount" : 6, "type" : "fireDragonFly" } ], - "combat_value": 154, - "reward" : { - "value": 3200, - "creatures": [ { "amount": 4, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 4, "type" : "wyvern" } ] }, { - "chance": 30, - "guards": [ - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" }, - { "amount": 9, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" }, + { "amount" : 9, "type" : "fireDragonFly" } ], - "combat_value": 230, - "reward" : { - "value": 4800, - "creatures": [ { "amount": 6, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 6, "type" : "wyvern" } ] }, { - "chance": 30, - "guards": [ - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" }, - { "amount": 12, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" }, + { "amount" : 12, "type" : "fireDragonFly" } ], - "combat_value": 307, - "reward" : { - "value": 6400, - "creatures": [ { "amount": 8, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 8, "type" : "wyvern" } ] }, { - "chance": 10, - "guards": [ - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" }, - { "amount": 18, "type": "fireDragonFly" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" }, + { "amount" : 18, "type" : "fireDragonFly" } ], - "combat_value": 461, - "reward" : { - "value": 9600, - "creatures": [ { "amount": 12, "type": "wyvern" } ] - } + "creatures" : [ { "amount" : 12, "type" : "wyvern" } ] } ] } @@ -635,7 +573,7 @@ }, "shipwreck" : { "index" :85, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["ROGUE"] @@ -644,7 +582,7 @@ "types" : { "shipwreck" : { "index" : 0, - "resetDuration" : 0, + "blockedVisitable" : true, "coastVisitable" : true, "name" : "Shipwreck", @@ -653,80 +591,77 @@ "value" : 2000, "rarity" : 100 }, - "levels": [ + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 122, + "onVisited" : [ { - "chance": 30, - "guards": [ - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" }, - { "amount": 2, "type": "wight" } + "message" : 123, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ] + } + ], + "rewards" : [ + { + "message" : 124, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" }, + { "amount" : 2, "type" : "wight" } ], - "combat_value": 31, - "reward" : { - "value": 2000, - "resources": - { - "gold" : 2000 - } + "resources" : + { + "gold" : 2000 } }, { - "chance": 30, - "guards": [ - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" }, - { "amount": 3, "type": "wight" } + "message" : 124, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" }, + { "amount" : 3, "type" : "wight" } ], - "combat_value": 46, - "reward" : { - "value": 3000, - "resources": - { - "gold" : 3000 - } + "resources" : + { + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" }, - { "amount": 5, "type": "wight" } + "message" : 124, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 5, "type" : "wight" } ], - "combat_value": 77, - "reward" : { - "value": 5000, - "resources": - { - "gold" : 4000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : + { + "gold" : 4000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 10, - "guards": [ - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "wight" } + "message" : 124, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "wight" } ], - "combat_value": 154, - "reward" : { - "value": 7000, - "resources": - { - "gold" : 5000 - }, - "artifacts": [ { "class" : "MINOR" } ] - } + "resources" : + { + "gold" : 5000 + }, + "artifacts" : [ { "class" : "MINOR" } ] } ] } @@ -734,7 +669,7 @@ }, "derelictShip" : { "index" :24, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["ROGUE"] @@ -743,7 +678,7 @@ "types" : { "derelictShip" : { "index" : 0, - "resetDuration" : 0, + "blockedVisitable" : true, "name" : "Derelict Ship", "aiValue" : 4000, @@ -751,81 +686,75 @@ "value" : 4000, "rarity" : 20 }, - "levels": [ + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 41, + "onVisited" : [ { - "chance": 30, - "guards": [ - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" }, - { "amount": 4, "type": "waterElemental" } + "message" : 42, // Such a despicable act reduces your army's morale. + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 101 } ] + } + ], + "rewards" : [ + { + "message" : 43, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" }, + { "amount" : 4, "type" : "waterElemental" } ], - "combat_value": 138, - "reward" : { - "value": 3000, - "resources": - { - "gold" : 3000 - } + "resources" : + { + "gold" : 3000 } }, { - "chance": 30, - "guards": [ - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" }, - { "amount": 6, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" }, + { "amount" : 6, "type" : "waterElemental" } ], - "combat_value": 207, - "reward" : { - "value": 4000, - "resources": - { - "gold" : 3000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : { + "gold" : 3000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" }, - { "amount": 8, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" }, + { "amount" : 8, "type" : "waterElemental" } ], - "combat_value": 276, - "reward" : { - "value": 5000, - "resources": - { - "gold" : 4000 - }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "resources" : { + "gold" : 4000 + }, + "artifacts" : [ { "class" : "TREASURE" } ] }, { - "chance": 10, - "guards": [ - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" }, - { "amount": 12, "type": "waterElemental" } + "message" : 43, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" }, + { "amount" : 12, "type" : "waterElemental" } ], - "combat_value": 414, - "reward" : { - "value": 8000, - "resources": - { - "gold" : 6000 - }, - "artifacts": [ { "class" : "MINOR" } ] - } + "resources" : { + "gold" : 6000 + }, + "artifacts" : [ { "class" : "MINOR" } ] } ] } @@ -833,7 +762,7 @@ }, "crypt" : { "index" :84, - "handler": "configurable", + "handler" : "configurable", "base" : { "sounds" : { "ambient" : ["LOOPDEAD"], @@ -860,68 +789,66 @@ "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ] } ], - - "rewards": [ + "rewards" : [ { "appearChance" : { "min" : 0, "max" : 30 }, - "guards": [ - { "amount": 10, "type": "skeleton" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 10, "type": "skeleton" }, - { "amount": 10, "type": "skeleton" } + "guards" : [ + { "amount" : 10, "type" : "skeleton" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 10, "type" : "skeleton" }, + { "amount" : 10, "type" : "skeleton" } ], "message" : 121, // you search the graves and find something - "resources": + "resources" : { "gold" : 1500 } }, { "appearChance" : { "min" : 30, "max" : 60 }, - "guards": [ - { "amount": 13, "type": "skeleton" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 5, "type": "wight" }, - { "amount": 10, "type": "walkingDead" }, - { "amount": 12, "type": "skeleton" } + "guards" : [ + { "amount" : 13, "type" : "skeleton" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 5, "type" : "wight" }, + { "amount" : 10, "type" : "walkingDead" }, + { "amount" : 12, "type" : "skeleton" } ], "message" : 121, // you search the graves and find something - "resources": + "resources" : { "gold" : 2000 } }, { "appearChance" : { "min" : 60, "max" : 90 }, - "guards": [ - { "amount": 20, "type": "skeleton" }, - { "amount": 20, "type": "walkingDead" }, - { "amount": 10, "type": "wight" }, - { "amount": 5, "type": "vampire" } + "guards" : [ + { "amount" : 20, "type" : "skeleton" }, + { "amount" : 20, "type" : "walkingDead" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 5, "type" : "vampire" } ], "message" : 121, // you search the graves and find something - "resources": + "resources" : { "gold" : 2500 }, - "artifacts": [ { "class" : "TREASURE" } ] - } + "artifacts" : [ { "class" : "TREASURE" } ] }, { "appearChance" : { "min" : 90, "max" : 100 }, - "guards": [ - { "amount": 20, "type": "skeleton" }, - { "amount": 20, "type": "walkingDead" }, - { "amount": 10, "type": "wight" }, - { "amount": 10, "type": "vampire" } + "guards" : [ + { "amount" : 20, "type" : "skeleton" }, + { "amount" : 20, "type" : "walkingDead" }, + { "amount" : 10, "type" : "wight" }, + { "amount" : 10, "type" : "vampire" } ], "message" : 121, // you search the graves and find something - "resources": + "resources" : { "gold" : 5000 }, - "artifacts": [ { "class" : "TREASURE" } ] + "artifacts" : [ { "class" : "TREASURE" } ] } ] } @@ -929,7 +856,7 @@ }, "dragonUtopia" : { "index" :25, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "ambient" : ["LOOPDRAG"], @@ -939,105 +866,98 @@ "types" : { "dragonUtopia" : { "index" : 0, - "resetDuration" : 0, + "name" : "Dragon Utopia", "aiValue" : 11000, "rmg" : { "value" : 10000, "rarity" : 100 }, - "levels": [ + + "visitMode" : "once", + "selectMode" : "selectFirst", + "onGuardedMessage" : 47, + "onVisitedMessage" : 33, + "rewards" : [ { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 5, "type": "redDragon" }, - { "amount": 2, "type": "goldDragon" }, - { "amount": 1, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 5, "type" : "redDragon" }, + { "amount" : 2, "type" : "goldDragon" }, + { "amount" : 1, "type" : "blackDragon" } ], - "combat_value": 769, - "reward" : { - "value": 38000, - "resources": - { - "gold" : 20000 - }, - "artifacts": [ - { "class" : "TREASURE" }, - { "class" : "MINOR" }, - { "class" : "MAJOR" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 20000 + }, + "artifacts" : [ + { "class" : "TREASURE" }, + { "class" : "MINOR" }, + { "class" : "MAJOR" }, + { "class" : "RELIC" } + ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 6, "type": "redDragon" }, - { "amount": 3, "type": "goldDragon" }, - { "amount": 2, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 6, "type" : "redDragon" }, + { "amount" : 3, "type" : "goldDragon" }, + { "amount" : 2, "type" : "blackDragon" } ], - "combat_value": 209, - "reward" : { - "value": 57000, - "resources": - { - "gold" : 30000 - }, - "artifacts": [ - { "class" : "MINOR" }, - { "class" : "MAJOR" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 30000 + }, + "artifacts" : [ + { "class" : "MINOR" }, + { "class" : "MAJOR" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] }, { - "chance": 30, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 6, "type": "redDragon" }, - { "amount": 4, "type": "goldDragon" }, - { "amount": 3, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 6, "type" : "redDragon" }, + { "amount" : 4, "type" : "goldDragon" }, + { "amount" : 3, "type" : "blackDragon" } ], - "combat_value": 556, - "reward" : { - "value": 75000, - "resources": - { - "gold" : 40000 - }, - "artifacts": [ - { "class" : "MAJOR" }, - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 40000 + }, + "artifacts" : [ + { "class" : "MAJOR" }, + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] }, { - "chance": 10, - "guards": [ - { "amount": 8, "type": "greenDragon" }, - { "amount": 7, "type": "redDragon" }, - { "amount": 6, "type": "goldDragon" }, - { "amount": 5, "type": "blackDragon" } + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 8, "type" : "greenDragon" }, + { "amount" : 7, "type" : "redDragon" }, + { "amount" : 6, "type" : "goldDragon" }, + { "amount" : 5, "type" : "blackDragon" } ], - "combat_value": 343, - "reward" : { - "value": 90000, - "resources": - { - "gold" : 50000 - }, - "artifacts": [ - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" }, - { "class" : "RELIC" } - ] - } + "resources" : + { + "gold" : 50000 + }, + "artifacts" : [ + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" }, + { "class" : "RELIC" } + ] } ] } @@ -1045,7 +965,7 @@ }, "pyramid" : { "index" :63, - "handler": "bank", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["MYSTERY"] @@ -1054,26 +974,32 @@ "types" : { "pyramid" : { "index" : 0, - "resetDuration" : 0, + "name" : "Pyramid", "aiValue" : 8000, "rmg" : { "value" : 5000, "rarity" : 20 }, - "levels": [ + + "onGuardedMessage" : 105, + "visitMode" : "once", + "selectMode" : "selectFirst", + "onVisited" : [ { - "chance": 100, - "guards": [ - { "amount": 40, "type": "goldGolem" }, - { "amount": 10, "type": "diamondGolem" }, - { "amount": 10, "type": "diamondGolem" } + "message" : 107, + "bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ] + } + ], + "rewards" : [ + { + "message" : 106, + "guards" : [ + { "amount" : 40, "type" : "goldGolem" }, + { "amount" : 10, "type" : "diamondGolem" }, + { "amount" : 10, "type" : "diamondGolem" } ], - "combat_value": 786, - "reward" : { - "value": 15000, - "spells" : [ { "level" : 5 } ] - } + "spells" : [ { "level" : 5 } ] } ] } diff --git a/config/schemas/rewardable.json b/config/schemas/rewardable.json index bc76b743f..eaa54aff4 100644 --- a/config/schemas/rewardable.json +++ b/config/schemas/rewardable.json @@ -98,6 +98,9 @@ "max" : { "type" : "number", "exclusiveMinimum" : 0, "maximum" : 100 } } }, + + "guards" : {}, //TODO + "limiter" : { "$ref" : "#/definitions/limiter" }, "message" : { "$ref" : "#/definitions/message" }, "description" : { "$ref" : "#/definitions/message" }, @@ -256,6 +259,9 @@ }, }, + "onGuardedMessage" : { + "$ref" : "#/definitions/message" + }, "onSelectMessage" : { "$ref" : "#/definitions/message" }, diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 2b16f3112..d7ce61f5e 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -26,7 +26,6 @@ class CGHeroInstance; class CGMarket; class CHeroClass; class CGCreature; -class CBank; class CGBoat; class CFaction; class CStackBasicDescriptor; diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 8ec1baa9c..d0f371195 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -140,119 +140,28 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id); cb->sendAndApply(&cov); - if(!bankConfig && (ID.toEnum() == Obj::CREATURE_BANK || ID.toEnum() == Obj::DRAGON_UTOPIA)) - { - blockingDialogAnswered(h, 1); - return; - } - - int banktext = 0; - switch (ID.toEnum()) - { - case Obj::DERELICT_SHIP: - banktext = 41; - break; - case Obj::DRAGON_UTOPIA: - banktext = 47; - break; - case Obj::SHIPWRECK: - banktext = 122; - break; - case Obj::PYRAMID: - banktext = 105; - break; - default: - banktext = 32; - break; - } BlockingDialog bd(true, false); bd.player = h->getOwner(); - bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); + bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32); bd.components = getPopupComponents(h->getOwner()); - if (banktext == 32) - bd.text.replaceRawString(getObjectName()); - + bd.text.replaceRawString(getObjectName()); cb->showBlockingDialog(this, &bd); } void CBank::doVisit(const CGHeroInstance * hero) const { - int textID = -1; InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.player = hero->getOwner(); MetaString loot; - if (bankConfig) + if (!bankConfig) { - switch (ID.toEnum()) - { - case Obj::DERELICT_SHIP: - textID = 43; - break; - case Obj::SHIPWRECK: - textID = 124; - break; - case Obj::PYRAMID: - textID = 106; - break; - default: - textID = 34; - break; - } - } - else - { - switch (ID.toEnum()) - { - case Obj::SHIPWRECK: - case Obj::DERELICT_SHIP: - { - GiveBonus gbonus; - gbonus.id = hero->id; - gbonus.bonus.duration = BonusDuration::ONE_BATTLE; - gbonus.bonus.source = BonusSource::OBJECT_TYPE; - gbonus.bonus.sid = BonusSourceID(ID); - gbonus.bonus.type = BonusType::MORALE; - gbonus.bonus.val = -1; - switch (ID.toEnum()) - { - case Obj::SHIPWRECK: - textID = 123; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.99"); - break; - case Obj::DERELICT_SHIP: - textID = 42; - gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.101"); - break; - } - cb->giveHeroBonus(&gbonus); - iw.components.emplace_back(ComponentType::MORALE, -1); - break; - } - case Obj::PYRAMID: - { - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id)); - gb.bonus.description = MetaString::createFromTextID("core.arraytxt.70"); - gb.id = hero->id; - cb->giveHeroBonus(&gb); - textID = 107; - iw.components.emplace_back(ComponentType::LUCK, -2); - break; - } - default: - iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty - iw.text.replaceRawString(getObjectName()); - } - if(textID != -1) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); - } + iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty + iw.text.replaceRawString(getObjectName()); cb->showInfoDialog(&iw); } - //grant resources if (bankConfig) { @@ -278,17 +187,15 @@ void CBank::doVisit(const CGHeroInstance * hero) const //display loot if (!iw.components.empty()) { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); - if (textID == 34) + iw.text.appendLocalString(EMetaText::ADVOB_TXT, 34); + const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b) { - const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b) - { - return a.type->getFightValue() < b.type->getFightValue(); - })->type; + return a.type->getFightValue() < b.type->getFightValue(); + })->type; + + iw.text.replaceNamePlural(strongest->getId()); + iw.text.replaceRawString(loot.buildList()); - iw.text.replaceNamePlural(strongest->getId()); - iw.text.replaceRawString(loot.buildList()); - } cb->showInfoDialog(&iw); } @@ -301,10 +208,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const std::set spells; bool noWisdom = false; - if(textID == 106) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); //pyramid - } + for(const SpellID & spellId : bankConfig->spells) { const auto * spell = spellId.toEntity(VLC); diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 7aed36cce..99bbef46d 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -100,13 +100,6 @@ public: }; //TODO: - -// MAX -// class DLL_LINKAGE CBank : public CArmedInstance -// class DLL_LINKAGE CGPyramid : public CBank - -// EXTRA -// class DLL_LINKAGE CTownBonus : public CGTownBuilding // class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards // class DLL_LINKAGE CGKeymasterTent : public CGKeys // class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject diff --git a/lib/mapObjects/MapObjects.h b/lib/mapObjects/MapObjects.h index 01e38b4d8..7a54592ae 100644 --- a/lib/mapObjects/MapObjects.h +++ b/lib/mapObjects/MapObjects.h @@ -14,7 +14,6 @@ #include "CObjectHandler.h" #include "CArmedInstance.h" -#include "CBank.h" #include "CGDwelling.h" #include "CGHeroInstance.h" #include "CGMarket.h" diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 543711e1b..32e0057ac 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1459,7 +1459,7 @@ CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::sha CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) { if(objectTemplate->subid == 0) - return new CBank(map->cb); + return readGeneric(mapPosition, objectTemplate); return new CGObjectInstance(map->cb); } From 31dff8f8da99974223613c4a268ff0cc9706c614 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 31 Aug 2024 15:28:58 +0000 Subject: [PATCH 15/25] Add coastVisitable property to rewardables, update schemas --- config/schemas/rewardable.json | 10 ++++++---- lib/CPlayerState.h | 2 +- lib/mapObjects/CRewardableObject.cpp | 5 +++++ lib/mapObjects/CRewardableObject.h | 2 ++ lib/rewardable/Configuration.cpp | 1 + lib/rewardable/Configuration.h | 6 ++++++ lib/rewardable/Info.cpp | 1 + 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/config/schemas/rewardable.json b/config/schemas/rewardable.json index eaa54aff4..150349a92 100644 --- a/config/schemas/rewardable.json +++ b/config/schemas/rewardable.json @@ -99,7 +99,7 @@ } }, - "guards" : {}, //TODO + "guards" : { "$ref" : "#/definitions/identifierWithValueList" }, "limiter" : { "$ref" : "#/definitions/limiter" }, "message" : { "$ref" : "#/definitions/message" }, @@ -293,14 +293,16 @@ "type" : "boolean" }, + "coastVisitable": { + "type" : "boolean" + }, + "visitMode": { "enum" : [ "unlimited", "once", "hero", "bonus", "limiter", "player" ], "type" : "string" }, - "visitLimiter": { - "type" : "object" - }, + "visitLimiter": { "$ref" : "#/definitions/limiter" }, "selectMode": { "enum" : [ "selectFirst", "selectPlayer", "selectRandom", "selectAll" ], diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 3da93f139..96a0ba790 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -176,7 +176,7 @@ public: h & fogOfWarMap; h & static_cast(*this); - if (h.version >= Handler::Version::TEAM_STATE_SCOUTED_OBJECTS) + if (h.version >= Handler::Version::REWARDABLE_BANKS) h & scoutedObjects; } diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 674edf70b..3a26738f5 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -517,4 +517,9 @@ void CRewardableObject::initializeGuards() } } +bool CRewardableObject::isCoastVisitable() const +{ + return configuration.coastVisitable; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 99bbef46d..39a658ad2 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -76,6 +76,8 @@ public: void initObj(vstd::RNG & rand) override; + bool isCoastVisitable() const override; + void initializeGuards(); void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 9eb95649a..e866d627d 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -103,6 +103,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) handler.serializeStruct("resetParameters", resetParameters); handler.serializeBool("canRefuse", canRefuse); handler.serializeBool("showScoutedPreview", showScoutedPreview); + handler.serializeBool("coastVisitable", coastVisitable); handler.serializeInt("infoWindowType", infoWindowType); } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index a6237f4ca..d4ed53db0 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -162,6 +162,8 @@ struct DLL_LINKAGE Configuration /// if true - right-clicking object will show preview of object rewards bool showScoutedPreview = false; + bool coastVisitable = false; + /// if true - object info will shown in infobox (like resource pickup) EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO; @@ -190,6 +192,10 @@ struct DLL_LINKAGE Configuration h & canRefuse; h & showScoutedPreview; h & infoWindowType; + if (h.version >= Handler::Version::BANK_UNIT_PLACEMENT) + h & coastVisitable; + else + coastVisitable = false; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 9805f82e1..abf8fc56c 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -394,6 +394,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd: object.canRefuse = parameters["canRefuse"].Bool(); object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); + object.coastVisitable = parameters["coastVisitable"].Bool(); if(parameters["showInInfobox"].isNull()) object.infoWindowType = EInfoWindowMode::AUTO; From c55e07dabccd0a052693d66ae3592adde59c5c2c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 31 Aug 2024 15:36:40 +0000 Subject: [PATCH 16/25] Fix typo in schema, fix ballista yard --- config/buildingsLibrary.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/buildingsLibrary.json b/config/buildingsLibrary.json index 49eacb269..2bcb3b598 100644 --- a/config/buildingsLibrary.json +++ b/config/buildingsLibrary.json @@ -251,7 +251,7 @@ // Section 4 - buildings that now have dedicated mechanics "ballistaYard": { - "blacksmith" : "ballista" + "warMachine" : "ballista" }, "thievesGuild" : { From 39a2c29c97aeba396de47f9c1dfe7d10861cf7db Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 31 Aug 2024 21:04:32 +0000 Subject: [PATCH 17/25] Removed creatureBank flag from battle, battles now receive BattleLayout struct that defines how units are placed in combat --- AI/BattleAI/StackWithBonuses.cpp | 6 +- AI/BattleAI/StackWithBonuses.h | 2 +- client/Client.h | 5 +- config/battleStartpos.json | 87 ---------------- config/gameConfig.json | 73 ++++++++++++- config/objects/creatureBanks.json | 12 +++ config/schemas/rewardable.json | 4 + lib/CMakeLists.txt | 2 + lib/GameSettings.cpp | 1 + lib/IGameCallback.h | 6 +- lib/IGameSettings.h | 1 + lib/battle/BattleInfo.cpp | 147 ++++++++++----------------- lib/battle/BattleInfo.h | 8 +- lib/battle/BattleLayout.cpp | 81 +++++++++++++++ lib/battle/BattleLayout.h | 39 +++++++ lib/battle/IBattleState.h | 3 +- lib/mapObjects/CBank.cpp | 2 +- lib/mapObjects/CGCreature.cpp | 2 +- lib/mapObjects/CGDwelling.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CGTownInstance.cpp | 3 +- lib/mapObjects/CRewardableObject.cpp | 10 +- lib/mapObjects/MiscObjects.cpp | 8 +- lib/rewardable/Configuration.h | 7 +- lib/rewardable/Info.cpp | 1 + server/CGameHandler.cpp | 13 +-- server/CGameHandler.h | 5 +- server/battles/BattleProcessor.cpp | 35 +++---- server/battles/BattleProcessor.h | 11 +- server/queries/BattleQueries.cpp | 5 +- test/game/CGameStateTest.cpp | 4 +- test/mock/mock_IGameCallback.h | 5 +- test/mock/mock_battle_IBattleState.h | 3 +- 34 files changed, 341 insertions(+), 258 deletions(-) delete mode 100644 config/battleStartpos.json create mode 100644 lib/battle/BattleLayout.cpp create mode 100644 lib/battle/BattleLayout.h diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 9ded2518f..fbecaf541 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -12,6 +12,7 @@ #include +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CStack.h" #include "../../lib/ScriptHandler.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const return int3(-1, -1, -1); } -bool HypotheticBattle::isCreatureBank() const +BattleLayout HypotheticBattle::getLayout() const { - // TODO - return false; + return subject->getBattle()->getLayout(); } int64_t HypotheticBattle::getTreeVersion() const diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 3d249d428..f5b4fac23 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -160,7 +160,7 @@ public: int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; std::vector getUsedSpells(BattleSide side) const override; int3 getLocation() const override; - bool isCreatureBank() const override; + BattleLayout getLayout() const override; int64_t getTreeVersion() const; diff --git a/client/Client.h b/client/Client.h index 600528188..c5b763a49 100644 --- a/client/Client.h +++ b/client/Client.h @@ -193,9 +193,8 @@ public: void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, const BattleLayout & layout, const CGTownInstance * town) override {}; //use hero=nullptr for no hero + void startBattle(const CArmedInstance * army1, const CArmedInstance * army2) override {}; //if any of armies is hero, hero will be used bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; void giveHeroBonus(GiveBonus * bonus) override {}; void setMovePoints(SetMovePoints * smp) override {}; diff --git a/config/battleStartpos.json b/config/battleStartpos.json deleted file mode 100644 index 5921cc071..000000000 --- a/config/battleStartpos.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "battle_positions": - [ - { - "name" : "attackerLoose", // loose formation, attacker - "levels": [ - [ 86 ], - [ 35, 137 ], - [ 35, 86, 137 ], - [ 1, 69, 103, 171 ], - [ 1, 35, 86, 137, 171 ], - [ 1, 35, 69, 103, 137, 171 ], - [ 1, 35, 69, 86, 103, 137, 171 ] - ] - }, - - { - "name" : "defenderLoose", // loose formation, defender - "levels": [ - [ 100 ], - [ 49, 151 ], - [ 49, 100, 151 ], - [ 15, 83, 117, 185 ], - [ 15, 49, 100, 151, 185 ], - [ 15, 49, 83, 117, 151, 185 ], - [ 15, 49, 83, 100, 117, 151, 185 ] - ] - }, - - { - "name" : "attackerTight", // tight formation, attacker - "levels": [ - [ 86 ], - [ 69, 103 ], - [ 69, 86, 103 ], - [ 35, 69, 103, 137 ], - [ 35, 69, 86, 103, 137 ], - [ 1, 35, 69, 103, 137, 171 ], - [ 1, 35, 69, 86, 103, 137, 171 ] - ] - }, - - { - "name" : "defenderTight", // tight formation, defender - "levels": [ - [ 100 ], - [ 83, 117 ], - [ 83, 100, 117 ], - [ 49, 83, 117, 151 ], - [ 49, 83, 100, 117, 151 ], - [ 15, 49, 83, 117, 151, 185 ], - [ 15, 49, 83, 100, 117, 151, 185 ] - ] - }, - - { - "name" : "attackerCreBank", // creature bank, attacker - "levels": [ - [ 57 ], - [ 57, 61 ], - [ 57, 61, 90 ], - [ 57, 61, 90, 93 ], - [ 57, 61, 90, 93, 96 ], - [ 57, 61, 90, 93, 96, 125 ], - [ 57, 61, 90, 93, 96, 125, 129 ] - ] - }, - - { - "name" : "defenderCreBank", // creature bank, defender - "levels": [ - [ 15 ], - [ 15, 185 ], - [ 15, 185, 172 ], - [ 15, 185, 172, 2 ], - [ 15, 185, 172, 2, 100 ], - [ 15, 185, 172, 2, 100, 86 ], - [ 15, 185, 172, 2, 100, 86, 8 ] - ] - } - ], - "commanderPositions": - { - "field" : [88, 98], //attacker/defender - "creBank" : [95, 8] //not expecting defendig hero at bank, but hell knows - } -} diff --git a/config/gameConfig.json b/config/gameConfig.json index 802c92f02..5ceae7dcc 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -342,8 +342,77 @@ // limit of damage reduction that can be achieved by overpowering defense points "defensePointDamageFactorCap": 0.7, // If set to true, double-wide creatures will trigger obstacle effect when moving one tile forward or backwards - "oneHexTriggersObstacles": false - }, + "oneHexTriggersObstacles": false, + + // Positions of units on start of the combat + // If battle does not defines specific configuration, 'default' configuration will be used + // Configuration must define either 'attackerUnits' list of 7 elements or both 'attackerUnitsLoose' and 'attackerUnitsTight' lists of 7 elements, 1..7 elements each + // Similarly, for defender configuration must have either 'defenderUnits' or both 'defenderUnitsLoose' and 'defenderUnitsTight' + "layouts" : { + "default" : { + "tacticsAllowed" : true, + "obstaclesAllowed" : true, + "attackerCommander" : 88, + "defenderCommander" : 98, + "attackerWarMachines" : [ 52, 18, 154, 120 ], + "defenderWarMachines" : [ 66, 32, 168, 134 ], + "attackerUnitsLoose": [ + [ 86 ], + [ 35, 137 ], + [ 35, 86, 137 ], + [ 1, 69, 103, 171 ], + [ 1, 35, 86, 137, 171 ], + [ 1, 35, 69, 103, 137, 171 ], + [ 1, 35, 69, 86, 103, 137, 171 ] + ], + "defenderUnitsLoose": [ + [ 100 ], + [ 49, 151 ], + [ 49, 100, 151 ], + [ 15, 83, 117, 185 ], + [ 15, 49, 100, 151, 185 ], + [ 15, 49, 83, 117, 151, 185 ], + [ 15, 49, 83, 100, 117, 151, 185 ] + ], + "attackerUnitsTight": [ + [ 86 ], + [ 69, 103 ], + [ 69, 86, 103 ], + [ 35, 69, 103, 137 ], + [ 35, 69, 86, 103, 137 ], + [ 1, 35, 69, 103, 137, 171 ], + [ 1, 35, 69, 86, 103, 137, 171 ] + ], + "defenderUnitsTight": [ + [ 100 ], + [ 83, 117 ], + [ 83, 100, 117 ], + [ 49, 83, 117, 151 ], + [ 49, 83, 100, 117, 151 ], + [ 15, 49, 83, 117, 151, 185 ], + [ 15, 49, 83, 100, 117, 151, 185 ] + ] + }, + // Configuration for creature banks with single-tile enemies + "creatureBankNarrow" : { + "tacticsAllowed" : false, + "obstaclesAllowed" : false, + "attackerCommander" : 95, + "defenderCommander" : 8, + "attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ], + "defenderUnits": [ 15, 185, 172, 2, 100, 87, 8 ] + }, + // Configuration for creature banks with double-wide enemies + "creatureBankWide" : { + "tacticsAllowed" : false, + "obstaclesAllowed" : false, + "attackerCommander" : 95, + "defenderCommander" : 8, + "attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ], + "defenderUnits": [ 15, 185, 171, 1, 100, 86, 8 ] + } + } + }, "creatures": { diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index f05744014..b1d84cebb 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -25,6 +25,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "message" : 34, @@ -125,6 +126,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "message" : 34, @@ -207,6 +209,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", "rewards" : [ { "message" : 34, @@ -273,6 +276,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "message" : 34, @@ -354,6 +358,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", "rewards" : [ { "message" : 34, @@ -436,6 +441,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankWide", "rewards" : [ { "message" : 34, @@ -518,6 +524,7 @@ "onVisitedMessage" : 33, "visitMode" : "once", "selectMode" : "selectFirst", + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "message" : 34, @@ -600,6 +607,7 @@ "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ] } ], + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "message" : 124, @@ -695,6 +703,7 @@ "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 101 } ] } ], + "guardsLayout" : "creatureBankWide", "rewards" : [ { "message" : 43, @@ -789,6 +798,7 @@ "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ] } ], + "guardsLayout" : "creatureBankNarrow", "rewards" : [ { "appearChance" : { "min" : 0, "max" : 30 }, @@ -878,6 +888,7 @@ "selectMode" : "selectFirst", "onGuardedMessage" : 47, "onVisitedMessage" : 33, + "guardsLayout" : "creatureBankWide", "rewards" : [ { "message" : 34, @@ -991,6 +1002,7 @@ "bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ] } ], + "guardsLayout" : "default", "rewards" : [ { "message" : 106, diff --git a/config/schemas/rewardable.json b/config/schemas/rewardable.json index 150349a92..afa4119e0 100644 --- a/config/schemas/rewardable.json +++ b/config/schemas/rewardable.json @@ -302,6 +302,10 @@ "type" : "string" }, + "guardsLayout": { + "type" : "string" + }, + "visitLimiter": { "$ref" : "#/definitions/limiter" }, "selectMode": { diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 002b83951..d691daaa3 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -48,6 +48,7 @@ set(lib_MAIN_SRCS battle/BattleAttackInfo.cpp battle/BattleHex.cpp battle/BattleInfo.cpp + battle/BattleLayout.cpp battle/BattleProxy.cpp battle/BattleStateInfoForRetreat.cpp battle/CBattleInfoCallback.cpp @@ -404,6 +405,7 @@ set(lib_MAIN_HEADERS battle/BattleAttackInfo.h battle/BattleHex.h battle/BattleInfo.h + battle/BattleLayout.h battle/BattleSide.h battle/BattleStateInfoForRetreat.h battle/BattleProxy.h diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 3e2e2b8ef..4a34df857 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -47,6 +47,7 @@ const std::vector GameSettings::settingProperties = {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap" }, {EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" }, {EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" }, + {EGameSettings::COMBAT_LAYOUTS, "combat", "layouts" }, {EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" }, {EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" }, {EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" }, diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index a53691f4e..a9cc7b655 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -28,6 +28,7 @@ struct TeleportDialog; struct StackLocation; struct ArtifactLocation; struct BankConfig; +struct BattleLayout; class CCreatureSet; class CStackBasicDescriptor; class CGCreature; @@ -128,9 +129,8 @@ public: virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0; virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; - virtual void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr)=0; //use hero=nullptr for no hero - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false)=0; //if any of armies is hero, hero will be used - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)=0; //use hero=nullptr for no hero + virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle virtual bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveMove, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; virtual void giveHeroBonus(GiveBonus * bonus)=0; diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 85ffeefc0..c32f71785 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -25,6 +25,7 @@ enum class EGameSettings COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, COMBAT_GOOD_LUCK_DICE, COMBAT_GOOD_MORALE_DICE, + COMBAT_LAYOUTS, CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, CREATURES_DAILY_STACK_EXPERIENCE, diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index c098f5298..1a8a89e8f 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -9,6 +9,8 @@ */ #include "StdInc.h" #include "BattleInfo.h" + +#include "BattleLayout.h" #include "CObstacleInstance.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" @@ -74,22 +76,6 @@ void BattleInfo::localInit() exportBonuses(); } -namespace CGH -{ - static void readBattlePositions(const JsonNode &node, std::vector< std::vector > & dest) - { - for(const JsonNode &level : node.Vector()) - { - std::vector pom; - for(const JsonNode &value : level.Vector()) - { - pom.push_back(static_cast(value.Float())); - } - - dest.push_back(pom); - } - } -} //RNG that works like H3 one struct RandGen @@ -173,22 +159,20 @@ struct RangeGenerator std::function myRand; }; -BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, bool creatureBank, const CGTownInstance * town) +BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance * town) { CMP_stack cmpst; - auto * curB = new BattleInfo(); + auto * curB = new BattleInfo(layout); for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE}) curB->sides[i].init(heroes[i], armies[i]); - std::vector & stacks = (curB->stacks); curB->tile = tile; curB->battlefieldType = battlefieldType; curB->round = -2; curB->activeStack = -1; - curB->creatureBank = creatureBank; curB->replayAllowed = false; if(town) @@ -225,7 +209,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } //randomize obstacles - if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank + if (layout.obstaclesAllowed) { RandGen r{}; auto ourRand = [&](){ return r.rand(); }; @@ -321,63 +305,40 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } } - //reading battleStartpos - add creatures AFTER random obstacles are generated - //TODO: parse once to some structure - BattleSideArray>> looseFormations; - BattleSideArray>> tightFormations; - BattleSideArray>> creBankFormations; - BattleSideArray commanderField; - BattleSideArray commanderBank; - const JsonNode config(JsonPath::builtin("config/battleStartpos.json")); - const JsonVector &positions = config["battle_positions"].Vector(); - - CGH::readBattlePositions(positions[0]["levels"], looseFormations[BattleSide::ATTACKER]); - CGH::readBattlePositions(positions[1]["levels"], looseFormations[BattleSide::DEFENDER]); - CGH::readBattlePositions(positions[2]["levels"], tightFormations[BattleSide::ATTACKER]); - CGH::readBattlePositions(positions[3]["levels"], tightFormations[BattleSide::DEFENDER]); - CGH::readBattlePositions(positions[4]["levels"], creBankFormations[BattleSide::ATTACKER]); - CGH::readBattlePositions(positions[5]["levels"], creBankFormations[BattleSide::DEFENDER]); - - commanderField[BattleSide::ATTACKER] = config["commanderPositions"]["field"][0].Integer(); - commanderField[BattleSide::DEFENDER] = config["commanderPositions"]["field"][1].Integer(); - - commanderBank[BattleSide::ATTACKER] = config["commanderPositions"]["creBank"][0].Integer(); - commanderBank[BattleSide::DEFENDER] = config["commanderPositions"]["creBank"][1].Integer(); - //adding war machines - if(!creatureBank) + //Checks if hero has artifact and create appropriate stack + auto handleWarMachine = [&](BattleSide side, const ArtifactPosition & artslot, BattleHex hex) { - //Checks if hero has artifact and create appropriate stack - auto handleWarMachine = [&](BattleSide side, const ArtifactPosition & artslot, BattleHex hex) + const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + + if(nullptr != warMachineArt && hex.isValid()) { - const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + CreatureID cre = warMachineArt->artType->getWarMachine(); - if(nullptr != warMachineArt) - { - CreatureID cre = warMachineArt->artType->getWarMachine(); - - if(cre != CreatureID::NONE) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); - } - }; - - if(heroes[BattleSide::ATTACKER]) - { - - handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, 52); - handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, 18); - handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, 154); - if(town && town->fortificationsLevel().wallsHealth > 0) - handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, 120); + if(cre != CreatureID::NONE) + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); } + }; - if(heroes[BattleSide::DEFENDER]) - { - if(!town) //defending hero shouldn't receive ballista (bug #551) - handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH1, 66); - handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH2, 32); - handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH3, 168); - } + if(heroes[BattleSide::ATTACKER]) + { + auto warMachineHexes = layout.warMachines.at(BattleSide::ATTACKER); + + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, warMachineHexes.at(0)); + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, warMachineHexes.at(1)); + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, warMachineHexes.at(2)); + if(town && town->fortificationsLevel().wallsHealth > 0) + handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, warMachineHexes.at(3)); + } + + if(heroes[BattleSide::DEFENDER]) + { + auto warMachineHexes = layout.warMachines.at(BattleSide::DEFENDER); + + if(!town) //defending hero shouldn't receive ballista (bug #551) + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH1, warMachineHexes.at(0)); + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH2, warMachineHexes.at(1)); + handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH3, warMachineHexes.at(2)); } //war machines added @@ -390,20 +351,13 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const int k = 0; //stack serial for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++) { - std::vector *formationVector = nullptr; - if(armies[side]->formation == EArmyFormation::TIGHT ) - formationVector = &tightFormations[side][formationNo]; - else - formationVector = &looseFormations[side][formationNo]; + const BattleHex & pos = layout.units.at(side).at(k); - if(creatureBank) - formationVector = &creBankFormations[side][formationNo]; + //if(creatureBank && i->second->type->isDoubleWide()) + // pos += side == BattleSide::RIGHT_SIDE ? BattleHex::LEFT : BattleHex::RIGHT; - BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0); - if(creatureBank && i->second->type->isDoubleWide()) - pos += side == BattleSide::RIGHT_SIDE ? BattleHex::LEFT : BattleHex::RIGHT; - - curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); + if (pos.isValid()) + curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); } } @@ -412,9 +366,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const { if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive) { - curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]); + curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i)); } - } if (curB->town) @@ -453,8 +406,6 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const ////////////////////////////////////////////////////////////////////////// //tactics - bool isTacticsAllowed = !creatureBank; //no tactics in creature banks - BattleSideArray battleRepositionHex = {}; BattleSideArray battleRepositionHexBlock = {}; for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER}) @@ -475,7 +426,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const double tactics will be implemented. */ - if(isTacticsAllowed) + if(layout.tacticsAllowed) { if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); @@ -523,11 +474,18 @@ CStack * BattleInfo::getStack(int stackID, bool onlyAlive) return const_cast(battleGetStackByID(stackID, onlyAlive)); } +BattleInfo::BattleInfo(const BattleLayout & layout): + BattleInfo() +{ + *this->layout = layout; +} + BattleInfo::BattleInfo(): round(-1), activeStack(-1), town(nullptr), tile(-1,-1,-1), + layout(std::make_unique()), battlefieldType(BattleField::NONE), tacticsSide(BattleSide::NONE), tacticDistance(0) @@ -535,6 +493,11 @@ BattleInfo::BattleInfo(): setNodeType(BATTLE); } +BattleLayout BattleInfo::getLayout() const +{ + return *layout; +} + BattleID BattleInfo::getBattleID() const { return battleID; @@ -679,12 +642,6 @@ int3 BattleInfo::getLocation() const return tile; } -bool BattleInfo::isCreatureBank() const -{ - return creatureBank; -} - - std::vector BattleInfo::getUsedSpells(BattleSide side) const { return getSide(side).usedSpellsHistory; diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index a95c154b5..3c3dd4502 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -22,10 +22,12 @@ class CStack; class CStackInstance; class CStackBasicDescriptor; class BattleField; +struct BattleLayout; class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState { BattleSideArray sides; //sides[0] - attacker, sides[1] - defender + std::unique_ptr layout; public: BattleID battleID = BattleID(0); @@ -33,7 +35,6 @@ public: si32 activeStack; const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) int3 tile; //for background and bonuses - bool creatureBank; //auxiliary field, do not serialize bool replayAllowed; std::vector stacks; std::vector > obstacles; @@ -65,6 +66,7 @@ public: } ////////////////////////////////////////////////////////////////////////// + BattleInfo(const BattleLayout & layout); BattleInfo(); virtual ~BattleInfo(); @@ -108,7 +110,7 @@ public: int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int3 getLocation() const override; - bool isCreatureBank() const override; + BattleLayout getLayout() const override; std::vector getUsedSpells(BattleSide side) const override; @@ -152,7 +154,7 @@ public: const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player void localInit(); - static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, bool creatureBank, const CGTownInstance * town); + static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance * town); BattleSide whatSide(const PlayerColor & player) const; diff --git a/lib/battle/BattleLayout.cpp b/lib/battle/BattleLayout.cpp new file mode 100644 index 000000000..272b14aa7 --- /dev/null +++ b/lib/battle/BattleLayout.cpp @@ -0,0 +1,81 @@ +/* + * BattleLayout.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 "BattleLayout.h" + +#include "../GameSettings.h" +#include "../IGameCallback.h" +#include "../VCMI_Lib.h" +#include "../json/JsonNode.h" +#include "../mapObjects/CArmedInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleLayout BattleLayout::createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender) +{ + return createLayout(cb, "default", attacker, defender); +} + +BattleLayout BattleLayout::createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender) +{ + const auto & loadHex = [](const JsonNode & node) + { + if (node.isNull()) + return BattleHex(); + else + return BattleHex(node.Integer()); + }; + + const auto & loadUnits = [](const JsonNode & node) + { + UnitsArrayType::value_type result; + for (size_t i = 0; i < GameConstants::ARMY_SIZE; ++i) + { + if (!node[i].isNull()) + result[i] = BattleHex(node[i].Integer()); + } + return result; + }; + + const JsonNode & configRoot = cb->getSettings().getValue(EGameSettings::COMBAT_LAYOUTS); + const JsonNode & config = configRoot[layoutName]; + + BattleLayout result; + + result.commanders[BattleSide::ATTACKER] = loadHex(config["attackerCommander"]); + result.commanders[BattleSide::DEFENDER] = loadHex(config["defenderCommander"]); + + for (size_t i = 0; i < 4; ++i) + result.warMachines[BattleSide::ATTACKER][i] = loadHex(config["attackerWarMachines"][i]); + + for (size_t i = 0; i < 4; ++i) + result.warMachines[BattleSide::DEFENDER][i] = loadHex(config["attackerWarMachines"][i]); + + if (attacker->formation == EArmyFormation::LOOSE && !config["attackerUnitsLoose"].isNull()) + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsLoose"][attacker->stacksCount()]); + else if (attacker->formation == EArmyFormation::TIGHT && !config["attackerUnitsTight"].isNull()) + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsTight"][attacker->stacksCount()]); + else + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnits"]); + + if (attacker->formation == EArmyFormation::LOOSE && !config["defenderUnitsLoose"].isNull()) + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsLoose"][attacker->stacksCount()]); + else if (attacker->formation == EArmyFormation::TIGHT && !config["defenderUnitsTight"].isNull()) + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsTight"][attacker->stacksCount()]); + else + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnits"]); + + result.obstaclesAllowed = config["obstaclesAllowed"].Bool(); + result.tacticsAllowed = config["tacticsAllowed"].Bool(); + + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleLayout.h b/lib/battle/BattleLayout.h new file mode 100644 index 000000000..a6a6948de --- /dev/null +++ b/lib/battle/BattleLayout.h @@ -0,0 +1,39 @@ +/* + * BattleLayout.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 "BattleHex.h" +#include "BattleSide.h" +#include "../constants/NumericConstants.h" +#include "../constants/Enumerations.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArmedInstance; +class IGameCallback; + +struct DLL_EXPORT BattleLayout +{ + using UnitsArrayType = BattleSideArray>; + using MachinesArrayType = BattleSideArray>; + using CommanderArrayType = BattleSideArray; + + UnitsArrayType units; + MachinesArrayType warMachines; + CommanderArrayType commanders; + + bool tacticsAllowed = false; + bool obstaclesAllowed = false; + + static BattleLayout createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender); + static BattleLayout createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 6c866d449..a2ae55576 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class ObstacleChanges; class UnitChanges; struct Bonus; +struct BattleLayout; class JsonNode; class JsonSerializeFormat; class BattleField; @@ -72,7 +73,7 @@ public: virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; virtual int3 getLocation() const = 0; - virtual bool isCreatureBank() const = 0; + virtual BattleLayout getLayout() const = 0; }; class DLL_LINKAGE IBattleState : public IBattleInfo diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index d0f371195..ac65f7fa8 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -283,7 +283,7 @@ void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) c if (answer) { if (bankConfig) // not looted bank - cb->startBattleI(hero, this, !regularUnitPlacement); + cb->startBattle(hero, this); else doVisit(hero); } diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index f6ae1eaac..4d2495c82 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -465,7 +465,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const } } - cb->startBattleI(h, this); + cb->startBattle(h, this); } diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index f376ca98e..1ec3e492c 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -505,7 +505,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answ if(stacksCount() > 0 && relations == PlayerRelations::ENEMIES) //guards present { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } else if(answer) { diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index afe95892a..35782bfdb 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -512,7 +512,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if(visitedTown) //we're in town visitedTown->onHeroVisit(h); //town will handle attacking else - cb->startBattleI(h, this); + cb->startBattle(h, this); } } else if(ID == Obj::PRISON) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index eaf4d8473..c75f04dab 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -193,7 +193,7 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t an if(stacksCount() > 0) //if pandora's box is protected by army { hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL); - cb->startBattleI(hero, this); //grants things after battle + cb->startBattle(hero, this); //grants things after battle } else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { @@ -332,7 +332,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const else iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16); cb->showInfoDialog(&iw); - cb->startBattleI(h, this); + cb->startBattle(h, this); } else { diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 078e2ce0c..2c040da6b 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -15,6 +15,7 @@ #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" #include "../battle/IBattleInfoCallback.h" +#include "../battle/BattleLayout.h" #include "../CConfigHandler.h" #include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" @@ -321,7 +322,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const const_cast(defendingHero)->inTownGarrison = false; //hack to return visitor from garrison after battle } - cb->startBattlePrimary(h, defendingArmy, getSightCenter(), h, defendingHero, false, (isBattleOutside ? nullptr : this)); + cb->startBattle(h, defendingArmy, getSightCenter(), h, defendingHero, BattleLayout::createDefaultLayout(cb, h, defendingArmy), (isBattleOutside ? nullptr : this)); } else { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 3a26738f5..461a0d661 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -14,6 +14,7 @@ #include "../CPlayerState.h" #include "../GameSettings.h" #include "../IGameCallback.h" +#include "../battle/BattleLayout.h" #include "../gameState/CGameState.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -127,7 +128,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const bd.text = guardedReward.message; bd.components = getPopupComponents(hero->getOwner()); - cb->showBlockingDialog(&bd); + cb->showBlockingDialog(this, &bd); } else { @@ -238,7 +239,10 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int3 if(guardedPresently()) { if (answer) - cb->startBattleI(hero, this, true); + { + auto layout = BattleLayout::createLayout(cb, configuration.guardsLayout, hero, this); + cb->startBattle(hero, this, visitablePos(), hero, nullptr, layout, nullptr); + } } else { @@ -405,7 +409,7 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (guardedPresently()) { - if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + if (!cb->getSettings().getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) return {}; std::map guardsAmounts; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ca0714235..ab5fc3d50 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -219,7 +219,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) @@ -352,7 +352,7 @@ void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult & void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGResource::serializeJsonOptions(JsonSerializeFormat & handler) @@ -919,7 +919,7 @@ void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult & void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const { if(answer) - cb->startBattleI(hero, this); + cb->startBattle(hero, this); } void CGArtifact::afterAddToMap(CMap * map) @@ -999,7 +999,7 @@ void CGGarrison::onHeroVisit (const CGHeroInstance *h) const auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); if (relations == PlayerRelations::ENEMIES && stacksCount() > 0) { //TODO: Find a way to apply magic garrison effects in battle. - cb->startBattleI(h, this); + cb->startBattle(h, this); return; } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index d4ed53db0..e9e9f832f 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -156,6 +156,8 @@ struct DLL_LINKAGE Configuration /// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter" Rewardable::Limiter visitLimiter; + std::string guardsLayout; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; @@ -192,8 +194,11 @@ struct DLL_LINKAGE Configuration h & canRefuse; h & showScoutedPreview; h & infoWindowType; - if (h.version >= Handler::Version::BANK_UNIT_PLACEMENT) + if (h.version >= Handler::Version::REWARDABLE_BANKS) + { h & coastVisitable; + h & guardsLayout; + } else coastVisitable = false; } diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index abf8fc56c..bc01b41c1 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -394,6 +394,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd: object.canRefuse = parameters["canRefuse"].Bool(); object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); + object.guardsLayout = parameters["guardsLayout"].String(); object.coastVisitable = parameters["coastVisitable"].Bool(); if(parameters["showInInfobox"].isNull()) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 13cf68c43..4b93943b1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4114,17 +4114,12 @@ void CGameHandler::newObject(CGObjectInstance * object, PlayerColor initiator) sendAndApply(&no); } -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) +void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { - battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); + battles->startBattle(army1, army2, tile, hero1, hero2, layout, town); } -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2 ) { - battles->startBattleI(army1, army2, tile, creatureBank); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank ) -{ - battles->startBattleI(army1, army2, creatureBank); + battles->startBattle(army1, army2); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 2a12c6a9b..b7df98e05 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -149,9 +149,8 @@ public: void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override; //use hero=nullptr for no hero + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; void giveHeroBonus(GiveBonus * bonus) override; void setMovePoints(SetMovePoints * smp) override; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 2b9a8e103..57d0e32d1 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -23,6 +23,7 @@ #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/entities/building/TownFortifications.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" @@ -52,9 +53,8 @@ void BattleProcessor::engageIntoBattle(PlayerColor player) gameHandler->sendAndApply(&pb); } -void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) +void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { auto battle = gameHandler->gameState()->getBattle(battleID); @@ -90,12 +90,11 @@ void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArm bc.battleID = battleID; gameHandler->sendAndApply(&bc); - startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); + startBattle(army1, army2, tile, hero1, hero2, layout, town); } -void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) +void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) { assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); @@ -103,7 +102,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm BattleSideArray armies{army1, army2}; BattleSideArrayheroes{hero1, hero2}; - auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + auto battleID = setupBattle(tile, armies, heroes, layout, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces const auto * battle = gameHandler->gameState()->getBattle(battleID); assert(battle); @@ -144,20 +143,16 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm flowProcessor->onBattleStarted(*battle); } -void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) +void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2) { - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); + startBattle(army1, army2, army2->visitablePos(), + army1->ID == Obj::HERO ? dynamic_cast(army1) : nullptr, + army2->ID == Obj::HERO ? dynamic_cast(army2) : nullptr, + BattleLayout::createDefaultLayout(gameHandler, army1, army2), + nullptr); } -void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} - -BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, bool creatureBank, const CGTownInstance *town) +BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance *town) { const auto & t = *gameHandler->getTile(tile); TerrainId terrain = t.terType->getId(); @@ -170,7 +165,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArraygameState()->nextBattleID; engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color); diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index c21dd6a66..480627b21 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -20,6 +20,7 @@ class BattleAction; class int3; class CBattleInfoCallback; struct BattleResult; +struct BattleLayout; class BattleID; VCMI_LIB_NAMESPACE_END @@ -45,7 +46,7 @@ class BattleProcessor : boost::noncopyable void engageIntoBattle(PlayerColor player); bool checkBattleStateChanges(const CBattleInfoCallback & battle); - BattleID setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, bool creatureBank, const CGTownInstance *town); + BattleID setupBattle(int3 tile, BattleSideArray armies, BattleSideArray heroes, const BattleLayout & layout, const CGTownInstance *town); bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); @@ -56,13 +57,11 @@ public: ~BattleProcessor(); /// Starts battle with specified parameters - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); - /// Starts battle between two armies (which can also be heroes) at specified tile - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town); /// Starts battle between two armies (which can also be heroes) at position of 2nd object - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2); /// Restart ongoing battle and end previous battle - void restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); + void restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town); /// Processing of incoming battle action netpack bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba); diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 80ac9029c..30b06db91 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -17,6 +17,7 @@ #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/SideInBattle.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CPlayerState.h" #include "../../lib/mapObjects/CGObjectInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -95,14 +96,14 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) assert(answer); if(*answer == 1) { - gh->battles->restartBattlePrimary( + gh->battles->restartBattle( bi->getBattleID(), bi->getSideArmy(BattleSide::ATTACKER), bi->getSideArmy(BattleSide::DEFENDER), bi->getLocation(), bi->getSideHero(BattleSide::ATTACKER), bi->getSideHero(BattleSide::DEFENDER), - bi->isCreatureBank(), + bi->getLayout(), bi->getDefendedTown() ); } diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index a897732dc..be71314fc 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -24,6 +24,7 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/CStack.h" #include "../../lib/filesystem/ResourcePath.h" @@ -197,10 +198,11 @@ public: auto terrain = t.terType->getId(); BattleField terType(0); + BattleLayout layout = BattleLayout::createDefaultLayout(gameState->callback, attacker, defender); //send info about battles - BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, false, nullptr); + BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, layout, nullptr); BattleStart bs; bs.info = battle; diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index c1b094366..1f2456a2f 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -79,9 +79,8 @@ public: void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {} void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {} void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {} - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override {} //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override {} //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override {} //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override {} //use hero=nullptr for no hero + void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override {} bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;} bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;} void giveHeroBonus(GiveBonus * bonus) override {} diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index 0893fd6cc..02389dc42 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -11,6 +11,7 @@ #pragma once #include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/BattleLayout.h" #include "../../lib/int3.h" class BattleStateMock : public IBattleState @@ -37,7 +38,7 @@ public: MOCK_CONST_METHOD3(getActualDamage, int64_t(const DamageRange &, int32_t, vstd::RNG &)); MOCK_CONST_METHOD0(getBattleID, BattleID()); MOCK_CONST_METHOD0(getLocation, int3()); - MOCK_CONST_METHOD0(isCreatureBank, bool()); + MOCK_CONST_METHOD0(getLayout, BattleLayout()); MOCK_CONST_METHOD1(getUsedSpells, std::vector(BattleSide)); MOCK_METHOD0(nextRound, void()); From a19a55b4aec439d20d9a414c2c98b22f852bd789 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 2 Sep 2024 14:47:37 +0000 Subject: [PATCH 18/25] Add checking handler type for validness, deprecate 'bank' handler --- config/schemas/object.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/schemas/object.json b/config/schemas/object.json index a055d279b..f74dffce3 100644 --- a/config/schemas/object.json +++ b/config/schemas/object.json @@ -17,7 +17,13 @@ "type" : "number" }, "handler" : { - "type" : "string" + "type" : "string", + "enum" : [ + "configurable", "dwelling", "hero", "town", "boat", "market", "hillFort", "shipyard", "monster", "resource", "static", "randomArtifact", + "randomHero", "randomResource", "randomTown", "randomMonster", "randomDwelling", "generic", "artifact", "borderGate", "borderGuard", "denOfThieves", + "event", "garrison", "heroPlaceholder", "keymaster", "lighthouse", "magi", "mine", "obelisk", "pandora", "prison", "questGuard", "seerHut", "sign", + "siren", "monolith", "subterraneanGate", "whirlpool", "terrain" + ] }, "base" : { "type" : "object" From 525ae72f8e805c79664a818d665259d8d2ddf11f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 2 Sep 2024 15:35:02 +0000 Subject: [PATCH 19/25] Update documentation --- docs/modders/Map_Object_Format.md | 68 +++++++++++-- docs/modders/Map_Objects/Creature_Bank.md | 113 +++++++++++++++++++++- docs/modders/Map_Objects/Rewardable.md | 32 +++++- 3 files changed, 199 insertions(+), 14 deletions(-) diff --git a/docs/modders/Map_Object_Format.md b/docs/modders/Map_Object_Format.md index 887af1965..22785ac55 100644 --- a/docs/modders/Map_Object_Format.md +++ b/docs/modders/Map_Object_Format.md @@ -14,14 +14,6 @@ Full object consists from 3 parts: generated by the game. When new object is created its starting appearance will be copied from template -## Object types - -- [Rewardable](Map_Objects/Rewardable.md) - Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...) -- [Creature Bank](Map_Objects/Creature_Bank.md) - Object that grants award on defeating guardians -- [Dwelling](Map_Objects/Dwelling.md) - Object that allows recruitments of units outside of towns -- [Market](Map_Objects/Market.md) - Trading resources, artifacts, creatures and such -- [Boat](Map_Objects/Boat.md) - Object to move across different terrains, such as water - ## Object group format ``` javascript @@ -33,7 +25,8 @@ Full object consists from 3 parts: // human readable name, localized "name": "My cool object", - //defines C++/script class name that handles behavior of this object + // defines C++ class name that handles behavior of this object + // see Object Types section below for possible values "handler" : "mine", // default values, will be merged with each type during loading @@ -46,6 +39,61 @@ Full object consists from 3 parts: } ``` +## Object types + +### Moddable types +These are object types that are available for modding and have configurable properties + +- `configurable` - see [Rewardable](Map_Objects/Rewardable.md). Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...) +- `bank` - see [Creature Bank](Map_Objects/Creature_Bank.md). Object that grants award on defeating guardians. Deprectated in favor of [Rewardable](Map_Objects/Rewardable.md) +- `dwelling` - see [Dwelling](Map_Objects/Dwelling.md). Object that allows recruitments of units outside of towns +- `market` - see [Market](Map_Objects/Market.md). Trading resources, artifacts, creatures and such +- `boat` - see [Boat](Map_Objects/Boat.md). Object to move across different terrains, such as water +- `hillFort` - TODO: documentation. See config files in vcmi installation for reference +- `shipyard` - TODO: documentation. See config files in vcmi installation for reference +- `terrain` - Defines terrain overlays such as magic grounds. TODO: documentation. See config files in vcmi installation for reference + +### Common types +These are types that don't have configurable properties, however it is possible to add additional map templates for this objects, for use in editor or in random maps generator + +- `static` - Defines unpassable static map obstacles that can be used by RMG +- `generic` - Defines empty object type that provides no functionality. Note that unlike `static`, objects of this type are never used by RMG +- `borderGate` +- `borderGuard` +- `lighthouse` +- `magi` +- `mine` +- `obelisk` +- `subterraneanGate` +- `whirlpool` +- `resource` +- `denOfThieves` +- `garrison` +- `keymaster` +- `pandora` +- `prison` +- `questGuard` +- `seerHut` +- `sign` +- `siren` +- `monolith` + +### Internal types +These are internal types that are generally not available for modding and are handled by vcmi internally. + +- `hero` +- `town` +- `monster` +- `randomArtifact` +- `randomHero` +- `randomResource` +- `randomTown` +- `randomMonster` +- `randomDwelling` +- `artifact` +- `event` +- `heroPlaceholder` + ## Object type format ``` javascript @@ -151,4 +199,4 @@ Full object consists from 3 parts: "zIndex": 0 } } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Creature_Bank.md b/docs/modders/Map_Objects/Creature_Bank.md index 36a3b6a3d..e5389846b 100644 --- a/docs/modders/Map_Objects/Creature_Bank.md +++ b/docs/modders/Map_Objects/Creature_Bank.md @@ -3,7 +3,116 @@ Reward types for clearing creature bank are limited to resources, creatures, artifacts and spell. Format of rewards is same as in [Rewardable Objects](Rewardable.md) -``` javascript +Deprecated in 1.6. Please use [Rewardable Objects](Rewardable.md) instead. See Conversion from 1.5 format section below for help with migration + +### Example +This example defines a rewardable object with functionality similar of H3 creature bank. +See [Rewardable Objects](Rewardable.md) for detailed documentation of these properties. +```jsonc +{ + "name" : "Cyclops Stockpile", + + // Generic message to ask player whether he wants to attack a creature bank, can be replaced with custom string + "onGuardedMessage" : 32, + + // Generic message to inform player that bank was already cleared + "onVisitedMessage" : 33, + + // As an alternative to a generic message you can define 'reward' + // that will be granted for visiting already cleared bank, such as morale debuff + "onVisited" : [ + { + "message" : 123, // "Such a despicable act reduces your army's morale." + "bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ] + } + ], + "visitMode" : "once", // Banks never reset + // Defines layout of guards. To emulate H3 logic, + // use 'creatureBankNarrow' if guardian units are narrow (1-tile units) + // or, 'creatureBankWide' if defenders are double-hex units + "guardsLayout" : "creatureBankNarrow", + "rewards" : [ + { + "message" : 34, + "appearChance" : { "min" : 0, "max" : 30 }, + "guards" : [ + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 4, "type" : "cyclop" }, + { "amount" : 4, "type" : "cyclop" } + ], + "resources" : { + "gold" : 4000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 30, "max" : 60 }, + "guards" : [ + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 6, "type" : "cyclop" }, + { "amount" : 6, "type" : "cyclop" } + ], + "resources" : { + "gold" : 6000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 60, "max" : 90 }, + "guards" : [ + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 8, "type" : "cyclop" }, + { "amount" : 8, "type" : "cyclop" } + ], + "resources" : { + "gold" : 8000 + } + }, + { + "message" : 34, + "appearChance" : { "min" : 90, "max" : 100 }, + "guards" : [ + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop", "upgradeChance" : 50 }, + { "amount" : 10, "type" : "cyclop" }, + { "amount" : 10, "type" : "cyclop" } + ], + "resources" : { + "gold" : 10000 + } + } + ] +}, +``` + +### Conversion from 1.5 format +This is a list of changes that needs to be done to bank config to migrate it to 1.6 system. See [Rewardable Objects](Rewardable.md) documentation for description of new fields + +- If your object type has defined `handler`, change its value from `bank` to `configurable` + +- If your object has non-zero `resetDuration`, replace with `resetParameters` entry + +- For each possible level, replace `chance` with `appearChance` entry + +- If you have `combat_value` or `field` entries inside 'reward' - remove them. These fields are unused in both 1.5 and in 1.6 + +- Rename `levels` entry to `rewards` + +- Add property `"visitMode" : "once"` +- Add property `"onGuardedMessage" : 119`, optionally - replace with custom message for object visit +- Add property `"onVisitedMessage" : 33`, optionally - custom message or morale debuff +- Add property `"message" : 34`, to every level of your reward, optionally - replace with custom message + +### Old format (1.5 or earlier) + +``` jsonc { /// If true, battle setup will be like normal - Attacking player on the left, enemy on the right "regularUnitPlacement" : true, @@ -63,4 +172,4 @@ Format of rewards is same as in [Rewardable Objects](Rewardable.md) ] } -``` \ No newline at end of file +``` diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 48426cf00..d7b1761d3 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -117,6 +117,9 @@ Rewardable object is defined similarly to other objects, with key difference bei // Message that will be shown if there are multiple selectable awards to choose from "onSelectMessage" : "", +// Message that will be shown if object has undefeated guards +"onGuardedMessage" : "", + // Message that will be shown if this object has been already visited before "onVisitedMessage" : "{Warehouse of Crystal}\r\n\r\nThe owner of the storage is apologising: 'I am sorry Milord, no crystal here. Please, return next week!'", @@ -125,10 +128,22 @@ Rewardable object is defined similarly to other objects, with key difference bei "onVisited" : [ ] +// Layout of units in the battle (only used if guards are present) +// Predefined values: +// "default" - attacker is on the left, defender is on the right, war machine, tactics, and battlefield obstacles are present +// "creatureBankNarrow" - emulates H3 logic for banks with narrow (1-tile wide) units +// "creatureBankWide" - emulates H3 logic for banks with wide units that take 2 hexes +// Additionally, it is possible to define new layouts, see "layouts" field in (vcmi install)/config/gameConfig.json file +"guardsLayout" : "default" + // if true, then player can refuse from reward and don't select anything // Note that in this case object will not become "visited" and can still be revisited later "canRefuse": true, +// If set to true, then this object can be visited from land when placed next to a coast. +// NOTE: make sure that object also has "blockedVisitable" set to true. Othervice, behavior is undefined +"coastVisitable" : true + // Controls when object state will be reset, allowing potential revisits. See Reset Parameters definition section "resetParameters" : { } @@ -479,8 +494,6 @@ Keep in mind, that all randomization is performed on map load and on object rese ], ``` -canLearnSpells - ### Creatures - Can be used as limiter - Can be used as reward, to give new creatures to a hero @@ -496,6 +509,21 @@ canLearnSpells ], ``` +### Guards +- When used in a reward, these creatures will be added to guards of the objects +- Hero must defeat all guards before being able to receive rewards +- Guards are only reset when object rewards are reset +- Requires `guardsLayout` property to be set in main part of object configuration +- It is possible to add up to 7 slots of creatures +- Guards of the same creature type will never merge or rearrange their stacks +```jsonc +"guards" : [ + { "type" : "archer", "amount" : 20 }, + { "type" : "archer", "amount" : 20, "upgradeChance" : 30 }, + { "type" : "archer", "amount" : 20 } +], +``` + ### Creatures Change - Can NOT be used as limiter - Can be used as reward, to replace creatures in hero army. It is possible to use this parameter both for upgrades of creatures as well as for changing them into completely unrelated creature, e.g. similar to Skeleton Transformer From 2ee4e4234805cfcbecba87ceba153662fedfa5ee Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 2 Sep 2024 19:15:12 +0000 Subject: [PATCH 20/25] Implement placeholders for creature banks messages --- config/objects/creatureBanks.json | 2 +- lib/battle/BattleInfo.cpp | 2 +- lib/mapObjects/CRewardableObject.cpp | 5 +++ lib/rewardable/Info.cpp | 53 ++++++++++++++++++++++++---- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index b1d84cebb..cd566f19e 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -477,7 +477,7 @@ }, { "message" : 34, - "appearChance" : { "min" : 30, "max" : 60 }, + "appearChance" : { "min" : 60, "max" : 90 }, "guards" : [ { "amount" : 4, "type" : "naga" }, { "amount" : 4, "type" : "naga" }, diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 1a8a89e8f..a0fe489da 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -481,11 +481,11 @@ BattleInfo::BattleInfo(const BattleLayout & layout): } BattleInfo::BattleInfo(): + layout(std::make_unique()), round(-1), activeStack(-1), town(nullptr), tile(-1,-1,-1), - layout(std::make_unique()), battlefieldType(BattleField::NONE), tacticsSide(BattleSide::NONE), tacticDistance(0) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 461a0d661..19576e054 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -508,6 +508,11 @@ void CRewardableObject::initializeGuards() { clearSlots(); + // Workaround for default creature banks strings that has placeholder for object name + // TODO: find better location for this code + for (auto & visitInfo : configuration.info) + visitInfo.message.replaceRawString(getObjectName()); + for (auto const & visitInfo : configuration.info) { for (auto const & guard : visitInfo.reward.guards) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index bc01b41c1..7c067ba7e 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -266,14 +266,55 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const { - for (const auto & artifact : info.reward.artifacts ) - target.replaceName(artifact); + if (!info.reward.guards.empty()) + { + CreatureID strongest = info.reward.guards.at(0).getId(); - for (const auto & spell : info.reward.spells ) - target.replaceName(spell); + for (const auto & guard : info.reward.guards ) + { + if (strongest.toEntity(VLC)->getFightValue() < guard.getId().toEntity(VLC)->getFightValue()) + strongest = guard.getId(); + } + target.replaceNamePlural(strongest); // FIXME: use singular if only 1 such unit is in guards - for (const auto & secondary : info.reward.secondary ) - target.replaceName(secondary.first); + MetaString loot; + + for (GameResID it : GameResID::ALL_RESOURCES()) + { + if (info.reward.resources[it] != 0) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(info.reward.resources[it]); + loot.replaceName(it); + } + } + + for (const auto & artifact : info.reward.artifacts ) + { + loot.appendRawString("%s"); + loot.replaceName(artifact); + } + + for (const auto & spell : info.reward.spells ) + { + target.replaceName(spell); + } + + for (const auto & secondary : info.reward.secondary ) + target.replaceName(secondary.first); + } + else + { + + for (const auto & artifact : info.reward.artifacts ) + target.replaceName(artifact); + + for (const auto & spell : info.reward.spells ) + target.replaceName(spell); + + for (const auto & secondary : info.reward.secondary ) + target.replaceName(secondary.first); + } replaceTextPlaceholders(target, variables); } From ca13fe04df7cc4eeed3d7f244ac542d4db7ad453 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 11 Sep 2024 09:54:54 +0000 Subject: [PATCH 21/25] Use alphabetic ordering for game settings --- lib/GameSettings.cpp | 34 ++++++++++++++++---------------- lib/IGameSettings.h | 46 ++++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 4a34df857..56c60055e 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -37,6 +37,7 @@ GameSettings::GameSettings() = default; GameSettings::~GameSettings() = default; const std::vector GameSettings::settingProperties = { + {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" }, @@ -54,28 +55,39 @@ const std::vector GameSettings::settingProperties = {EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" }, {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, + {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, + {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, + {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"}, + {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"}, + {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, + {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, + {EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" }, {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, - {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, - {EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" }, - {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, - {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, {EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" }, {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, + {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, + {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, {EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, - {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, + {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, + {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, + {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, + {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, {EGameSettings::TEXTS_FACTION, "textData", "faction" }, @@ -86,18 +98,6 @@ const std::vector GameSettings::settingProperties = {EGameSettings::TEXTS_ROAD, "textData", "road" }, {EGameSettings::TEXTS_SPELL, "textData", "spell" }, {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, - {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, - {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, - {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, - {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, - {EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"}, - {EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" }, - {EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" }, - {EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" }, - {EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"}, {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, }; diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index c32f71785..049fdef23 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -15,6 +15,7 @@ class JsonNode; enum class EGameSettings { + BANKS_SHOW_GUARDS_COMPOSITION, BONUSES_GLOBAL, BONUSES_PER_HERO, COMBAT_ATTACK_POINT_DAMAGE_FACTOR, @@ -26,26 +27,45 @@ enum class EGameSettings COMBAT_GOOD_LUCK_DICE, COMBAT_GOOD_MORALE_DICE, COMBAT_LAYOUTS, + COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, CREATURES_DAILY_STACK_EXPERIENCE, CREATURES_WEEKLY_GROWTH_CAP, CREATURES_WEEKLY_GROWTH_PERCENT, + DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, + DIMENSION_DOOR_FAILURE_SPENDS_POINTS, + DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, + DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, + DIMENSION_DOOR_TRIGGERS_GUARDS, DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, DWELLINGS_ACCUMULATE_WHEN_OWNED, DWELLINGS_MERGE_ON_RECRUIT, + HEROES_BACKPACK_CAP, + HEROES_MINIMAL_PRIMARY_SKILLS, HEROES_PER_PLAYER_ON_MAP_CAP, HEROES_PER_PLAYER_TOTAL_CAP, HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, HEROES_STARTING_STACKS_CHANCES, - HEROES_BACKPACK_CAP, HEROES_TAVERN_INVITE, - HEROES_MINIMAL_PRIMARY_SKILLS, + MAP_FORMAT_ARMAGEDDONS_BLADE, + MAP_FORMAT_CHRONICLES, + MAP_FORMAT_HORN_OF_THE_ABYSS, + MAP_FORMAT_IN_THE_WAKE_OF_GODS, + MAP_FORMAT_JSON_VCMI, + MAP_FORMAT_RESTORATION_OF_ERATHIA, + MAP_FORMAT_SHADOW_OF_DEATH, MARKETS_BLACK_MARKET_RESTOCK_PERIOD, - BANKS_SHOW_GUARDS_COMPOSITION, MODULE_COMMANDERS, MODULE_STACK_ARTIFACT, MODULE_STACK_EXPERIENCE, + PATHFINDER_IGNORE_GUARDS, + PATHFINDER_ORIGINAL_FLY_RULES, + PATHFINDER_USE_BOAT, + PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, + PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, + PATHFINDER_USE_MONOLITH_TWO_WAY, + PATHFINDER_USE_WHIRLPOOL, TEXTS_ARTIFACT, TEXTS_CREATURE, TEXTS_FACTION, @@ -56,28 +76,8 @@ enum class EGameSettings TEXTS_ROAD, TEXTS_SPELL, TEXTS_TERRAIN, - MAP_FORMAT_RESTORATION_OF_ERATHIA, - MAP_FORMAT_ARMAGEDDONS_BLADE, - MAP_FORMAT_SHADOW_OF_DEATH, - MAP_FORMAT_CHRONICLES, - MAP_FORMAT_HORN_OF_THE_ABYSS, - MAP_FORMAT_JSON_VCMI, - MAP_FORMAT_IN_THE_WAKE_OF_GODS, - PATHFINDER_USE_BOAT, - PATHFINDER_IGNORE_GUARDS, - PATHFINDER_USE_MONOLITH_TWO_WAY, - PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, - PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, - PATHFINDER_USE_WHIRLPOOL, - PATHFINDER_ORIGINAL_FLY_RULES, TOWNS_BUILDINGS_PER_TURN_CAP, TOWNS_STARTING_DWELLING_CHANCES, - COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, - DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, - DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, - DIMENSION_DOOR_FAILURE_SPENDS_POINTS, - DIMENSION_DOOR_TRIGGERS_GUARDS, - DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, OPTIONS_COUNT, OPTIONS_BEGIN = BONUSES_GLOBAL From d20d9bd96b21b3b91a00f349adc987fb3af865ee Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 11 Sep 2024 10:42:44 +0000 Subject: [PATCH 22/25] Fix generation of placeholders for banks --- config/gameConfig.json | 1 + config/objects/creatureBanks.json | 43 ----------------- config/objects/pyramid.json | 76 +++++++++++++++++++++++++++++++ lib/battle/BattleLayout.cpp | 8 ++-- lib/rewardable/Info.cpp | 17 +++++-- 5 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 config/objects/pyramid.json diff --git a/config/gameConfig.json b/config/gameConfig.json index 5ceae7dcc..c6994e855 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -57,6 +57,7 @@ "config/objects/magicWell.json", "config/objects/moddables.json", "config/objects/observatory.json", + "config/objects/pyramid.json", "config/objects/rewardableBonusing.json", "config/objects/rewardableOncePerHero.json", "config/objects/rewardableOncePerWeek.json", diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index cd566f19e..7b320c767 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -973,48 +973,5 @@ ] } } - }, - "pyramid" : { - "index" :63, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["MYSTERY"] - } - }, - "types" : { - "pyramid" : { - "index" : 0, - - "name" : "Pyramid", - "aiValue" : 8000, - "rmg" : { - "value" : 5000, - "rarity" : 20 - }, - - "onGuardedMessage" : 105, - "visitMode" : "once", - "selectMode" : "selectFirst", - "onVisited" : [ - { - "message" : 107, - "bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ] - } - ], - "guardsLayout" : "default", - "rewards" : [ - { - "message" : 106, - "guards" : [ - { "amount" : 40, "type" : "goldGolem" }, - { "amount" : 10, "type" : "diamondGolem" }, - { "amount" : 10, "type" : "diamondGolem" } - ], - "spells" : [ { "level" : 5 } ] - } - ] - } - } } } diff --git a/config/objects/pyramid.json b/config/objects/pyramid.json new file mode 100644 index 000000000..a52782029 --- /dev/null +++ b/config/objects/pyramid.json @@ -0,0 +1,76 @@ +{ + "pyramid" : { + "index" :63, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["MYSTERY"] + } + }, + "types" : { + "pyramid" : { + "index" : 0, + + "name" : "Pyramid", + "aiValue" : 8000, + "rmg" : { + "value" : 5000, + "rarity" : 20 + }, + + "variables" : { + "spell" : { + "gainedSpell" : { + "level": 5 + } + } + }, + + "onGuardedMessage" : 105, + "visitMode" : "once", + "selectMode" : "selectFirst", + "onVisited" : [ + { + "message" : 107, + "bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ] + } + ], + "guardsLayout" : "default", + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "message" : [ 106, "%s." ] // Upon defeating monsters, you learn new spell + "guards" : [ + { "amount" : 40, "type" : "goldGolem" }, + { "amount" : 10, "type" : "diamondGolem" }, + { "amount" : 10, "type" : "diamondGolem" } + ] + } + ], + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 106, "%s.", 108 ] // No Wisdom + }, + { + "message" : [ 106, "%s.", 109 ] // No spellbook + } + ] + + } + } + } +} \ No newline at end of file diff --git a/lib/battle/BattleLayout.cpp b/lib/battle/BattleLayout.cpp index 272b14aa7..e1f711277 100644 --- a/lib/battle/BattleLayout.cpp +++ b/lib/battle/BattleLayout.cpp @@ -59,16 +59,16 @@ BattleLayout BattleLayout::createLayout(IGameCallback * cb, const std::string & result.warMachines[BattleSide::DEFENDER][i] = loadHex(config["attackerWarMachines"][i]); if (attacker->formation == EArmyFormation::LOOSE && !config["attackerUnitsLoose"].isNull()) - result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsLoose"][attacker->stacksCount()]); + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsLoose"][attacker->stacksCount() - 1]); else if (attacker->formation == EArmyFormation::TIGHT && !config["attackerUnitsTight"].isNull()) - result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsTight"][attacker->stacksCount()]); + result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsTight"][attacker->stacksCount() - 1]); else result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnits"]); if (attacker->formation == EArmyFormation::LOOSE && !config["defenderUnitsLoose"].isNull()) - result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsLoose"][attacker->stacksCount()]); + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsLoose"][attacker->stacksCount() - 1]); else if (attacker->formation == EArmyFormation::TIGHT && !config["defenderUnitsTight"].isNull()) - result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsTight"][attacker->stacksCount()]); + result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsTight"][attacker->stacksCount() - 1]); else result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnits"]); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 7c067ba7e..a12d0a0fc 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -268,6 +268,8 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab { if (!info.reward.guards.empty()) { + replaceTextPlaceholders(target, variables); + CreatureID strongest = info.reward.guards.at(0).getId(); for (const auto & guard : info.reward.guards ) @@ -297,15 +299,20 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab for (const auto & spell : info.reward.spells ) { - target.replaceName(spell); + loot.appendRawString("%s"); + loot.replaceName(spell); } for (const auto & secondary : info.reward.secondary ) - target.replaceName(secondary.first); + { + loot.appendRawString("%s"); + loot.replaceName(secondary.first); + } + + target.replaceRawString(loot.buildList()); } else { - for (const auto & artifact : info.reward.artifacts ) target.replaceName(artifact); @@ -314,9 +321,9 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab for (const auto & secondary : info.reward.secondary ) target.replaceName(secondary.first); - } - replaceTextPlaceholders(target, variables); + replaceTextPlaceholders(target, variables); + } } void Rewardable::Info::configureRewards( From 84c82a807d9a368b7cdbec927723a4f549220fbd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 11 Sep 2024 12:40:16 +0000 Subject: [PATCH 23/25] Fix texts in shipwreck --- config/objects/creatureBanks.json | 8 ++++---- config/objects/pyramid.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index 7b320c767..8624e8e4d 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -610,7 +610,7 @@ "guardsLayout" : "creatureBankNarrow", "rewards" : [ { - "message" : 124, + "message" : 34, "appearChance" : { "min" : 0, "max" : 30 }, "guards" : [ { "amount" : 2, "type" : "wight" }, @@ -625,7 +625,7 @@ } }, { - "message" : 124, + "message" : 34, "appearChance" : { "min" : 30, "max" : 60 }, "guards" : [ { "amount" : 3, "type" : "wight" }, @@ -640,7 +640,7 @@ } }, { - "message" : 124, + "message" : 34, "appearChance" : { "min" : 60, "max" : 90 }, "guards" : [ { "amount" : 5, "type" : "wight" }, @@ -656,7 +656,7 @@ "artifacts" : [ { "class" : "TREASURE" } ] }, { - "message" : 124, + "message" : 34, "appearChance" : { "min" : 90, "max" : 100 }, "guards" : [ { "amount" : 10, "type" : "wight" }, diff --git a/config/objects/pyramid.json b/config/objects/pyramid.json index a52782029..cc8b06a93 100644 --- a/config/objects/pyramid.json +++ b/config/objects/pyramid.json @@ -46,7 +46,7 @@ "spells" : [ "@gainedSpell" ], - "message" : [ 106, "%s." ] // Upon defeating monsters, you learn new spell + "message" : [ 106, "%s." ], // Upon defeating monsters, you learn new spell "guards" : [ { "amount" : 40, "type" : "goldGolem" }, { "amount" : 10, "type" : "diamondGolem" }, From 29f393e0241fa32665375391fd20365af2f9281d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 11 Sep 2024 14:24:27 +0000 Subject: [PATCH 24/25] Removed hardcoded checks for map objects in favor of analyzing rewardable object content --- AI/Nullkiller/Engine/FuzzyHelper.cpp | 23 ++-------- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 53 ---------------------- AI/VCAI/FuzzyHelper.cpp | 36 +++------------ 3 files changed, 12 insertions(+), 100 deletions(-) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 9188ed66f..3907d5df3 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -139,27 +139,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) return 0; [[fallthrough]]; } - case Obj::MONSTER: - case Obj::GARRISON: - case Obj::GARRISON2: - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR4: - case Obj::MINE: - case Obj::ABANDONED_MINE: - case Obj::PANDORAS_BOX: - case Obj::CRYPT: //crypt - case Obj::CREATURE_BANK: //crebank - case Obj::DRAGON_UTOPIA: - case Obj::SHIPWRECK: //shipwreck - case Obj::DERELICT_SHIP: //derelict ship - case Obj::PYRAMID: + default: { const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); + if (a) + return a->getArmyStrength(); + else + return 0; } - default: - return 0; } } - } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 54f5b89b1..2d5839040 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -118,25 +118,6 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); } -TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero) -{ - //Fixme: unused variable hero - - auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); - CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - auto resources = bankInfo->getPossibleResourcesReward(); - TResources result = TResources(); - int sum = 0; - - for(auto & reward : resources) - { - result += reward.data * reward.chance; - sum += reward.chance; - } - - return sum > 1 ? result / sum : result; -} - int32_t getResourcesGoldReward(const TResources & res) { int32_t result = 0; @@ -303,22 +284,13 @@ uint64_t RewardEvaluator::getArmyReward( { case Obj::HILL_FORT: return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; - case Obj::CREATURE_BANK: - return getCreatureBankArmyReward(target, hero); case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: return getDwellingArmyValue(ai->cb.get(), target, checkGold); - case Obj::CRYPT: - case Obj::SHIPWRECK: - case Obj::SHIPWRECK_SURVIVOR: - case Obj::WARRIORS_TOMB: - return 1000; case Obj::ARTIFACT: return evaluateArtifactArmyValue(dynamic_cast(target)->storedArtifact->artType); - case Obj::DRAGON_UTOPIA: - return 10000; case Obj::HERO: return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() @@ -553,13 +525,6 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons return getResourceRequirementStrength(res); } - case Obj::CREATURE_BANK: - { - auto resourceReward = getCreatureBankResources(target, nullptr); - - return getResourceRequirementStrength(resourceReward); - } - case Obj::TOWN: { if(ai->buildAnalyzer->getDevelopmentInfo().empty()) @@ -670,8 +635,6 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; - case Obj::PYRAMID: - return 6.0f; case Obj::HERO: return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES ? enemyHeroEliminationSkillRewardRatio * dynamic_cast(target)->level @@ -778,22 +741,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG auto * mine = dynamic_cast(target); return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); } - case Obj::MYSTICAL_GARDEN: - case Obj::WINDMILL: - return 100; - case Obj::CAMPFIRE: - return 800; - case Obj::WAGON: - return 100; - case Obj::CREATURE_BANK: - return getResourcesGoldReward(getCreatureBankResources(target, hero)); - case Obj::CRYPT: - case Obj::DERELICT_SHIP: - return 3000; - case Obj::DRAGON_UTOPIA: - return 10000; - case Obj::SEA_CHEST: - return 1500; case Obj::PANDORAS_BOX: return 2500; case Obj::PRISON: diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index ce51b2f7d..6293851e4 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -281,35 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) cb->getTownInfo(obj, iat); return iat.army.getStrength(); } - case Obj::MONSTER: - { - //TODO!!!!!!!! - const CGCreature * cre = dynamic_cast(obj); - return cre->getArmyStrength(); - } - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR4: - { - const CGDwelling * d = dynamic_cast(obj); - return d->getArmyStrength(); - } - case Obj::MINE: - case Obj::ABANDONED_MINE: - { - const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); - } - case Obj::CRYPT: - case Obj::CREATURE_BANK: //crebank - case Obj::DRAGON_UTOPIA: - case Obj::SHIPWRECK: //shipwreck - case Obj::DERELICT_SHIP: //derelict ship - case Obj::PYRAMID: - { - const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); - } default: - return 0; + { + const CArmedInstance * a = dynamic_cast(obj); + if (a) + return a->getArmyStrength(); + else + return 0; + } } } From 45c8553ecbb8fcf65463c3dda99cc76a4af49a1a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 11 Sep 2024 16:32:43 +0000 Subject: [PATCH 25/25] Remove old code --- lib/battle/BattleInfo.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index a0fe489da..401a656d2 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -353,9 +353,6 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const { const BattleHex & pos = layout.units.at(side).at(k); - //if(creatureBank && i->second->type->isDoubleWide()) - // pos += side == BattleSide::RIGHT_SIDE ? BattleHex::LEFT : BattleHex::RIGHT; - if (pos.isValid()) curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); }