Bläddra i källkod

Merge pull request #4327 from Laserlicht/handicap

Handicap / resource transfer
Ivan Savenko 1 år sedan
förälder
incheckning
257fb8c70c

+ 1 - 1
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -317,7 +317,7 @@ void BuildAnalyzer::updateDailyIncome()
 
 		if(mine)
 		{
-			dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
+			dailyIncome[mine->producedResource.getNum()] += mine->getProducedQuantity();
 		}
 	}
 

+ 5 - 1
Mods/vcmi/config/vcmi/english.json

@@ -73,7 +73,11 @@
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
 	"vcmi.lobby.backToLobby" : "Return to lobby",
 	"vcmi.lobby.author" : "Author",
-	
+	"vcmi.lobby.handicap" : "Handicap",
+	"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
+	"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
+	"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
+		
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Username:",
 	"vcmi.lobby.login.connecting" : "Connecting...",

+ 4 - 0
Mods/vcmi/config/vcmi/german.json

@@ -73,6 +73,10 @@
 	"vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum",
 	"vcmi.lobby.backToLobby" : "Zur Lobby zurückkehren",
 	"vcmi.lobby.author" : "Author",
+	"vcmi.lobby.handicap" : "Handicap",
+	"vcmi.lobby.handicap.resource" : "Gibt den Spielern entsprechende Ressourcen zum Start zusätzlich zu den normalen Startressourcen. Negative Werte sind erlaubt, werden aber insgesamt auf 0 begrenzt (der Spieler beginnt nie mit negativen Ressourcen).",
+	"vcmi.lobby.handicap.income" : "Verändert die verschiedenen Einkommen des Spielers um den Prozentsatz. Wird aufgerundet.",
+	"vcmi.lobby.handicap.growth" : "Verändert die Wachstumsrate der Kreaturen in den Städten, die der Spieler besitzt. Wird aufgerundet.",
 	
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Benutzername:",

+ 8 - 0
client/CServerHandler.cpp

@@ -492,6 +492,14 @@ void CServerHandler::setPlayerName(PlayerColor color, const std::string & name)
 	sendLobbyPack(lspn);
 }
 
+void CServerHandler::setPlayerHandicap(PlayerColor color, Handicap handicap) const
+{
+	LobbySetPlayerHandicap lsph;
+	lsph.color = color;
+	lsph.handicap = handicap;
+	sendLobbyPack(lsph);
+}
+
 void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const
 {
 	LobbyChangePlayerOption lcpo;

+ 2 - 0
client/CServerHandler.h

@@ -82,6 +82,7 @@ public:
 	virtual void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const = 0;
 	virtual void setPlayer(PlayerColor color) const = 0;
 	virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0;
+	virtual void setPlayerHandicap(PlayerColor color, Handicap handicap) const = 0;
 	virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0;
 	virtual void setDifficulty(int to) const = 0;
 	virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0;
@@ -191,6 +192,7 @@ public:
 	void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const override;
 	void setPlayer(PlayerColor color) const override;
 	void setPlayerName(PlayerColor color, const std::string & name) const override;
+	void setPlayerHandicap(PlayerColor color, Handicap handicap) const override;
 	void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override;
 	void setDifficulty(int to) const override;
 	void setTurnTimerInfo(const TurnTimerInfo &) const override;

+ 1 - 0
client/gui/Shortcut.h

@@ -91,6 +91,7 @@ enum class EShortcut
 	LOBBY_FLIP_COIN,
 	LOBBY_RANDOM_TOWN,
 	LOBBY_RANDOM_TOWN_VS,
+	LOBBY_HANDICAP,
 
 	MAPS_SIZE_S,
 	MAPS_SIZE_M,

+ 1 - 0
client/gui/ShortcutHandler.cpp

@@ -288,6 +288,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"lobbyFlipCoin",            EShortcut::LOBBY_FLIP_COIN           },
 		{"lobbyRandomTown",          EShortcut::LOBBY_RANDOM_TOWN         },
 		{"lobbyRandomTownVs",        EShortcut::LOBBY_RANDOM_TOWN_VS      },
+		{"lobbyHandicap",            EShortcut::LOBBY_HANDICAP            },
 		{"mapsSizeS",                EShortcut::MAPS_SIZE_S               },
 		{"mapsSizeM",                EShortcut::MAPS_SIZE_M               },
 		{"mapsSizeL",                EShortcut::MAPS_SIZE_L               },

+ 7 - 0
client/lobby/CSelectionBase.cpp

@@ -437,6 +437,13 @@ PvPBox::PvPBox(const Rect & rect)
 		CSH->sendLobbyPack(lpa);
 	}, EShortcut::LOBBY_RANDOM_TOWN_VS);
 	buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE);
+
+	buttonHandicap = std::make_shared<CButton>(Point(190, 81), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.handicap")), [](){
+		if(!CSH->isHost())
+			return;
+		GH.windows().createAndPushWindow<OptionsTab::HandicapWindow>();
+	}, EShortcut::LOBBY_HANDICAP);
+	buttonHandicap->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.handicap"), EFonts::FONT_SMALL, Colors::WHITE);
 }
 
 TownSelector::TownSelector(const Point & loc)

+ 1 - 0
client/lobby/CSelectionBase.h

@@ -153,6 +153,7 @@ class PvPBox : public CIntObject
 	std::shared_ptr<CButton> buttonFlipCoin;
 	std::shared_ptr<CButton> buttonRandomTown;
 	std::shared_ptr<CButton> buttonRandomTownVs;
+	std::shared_ptr<CButton> buttonHandicap;
 public:
 	PvPBox(const Rect & rect);
 };

+ 155 - 0
client/lobby/OptionsTab.cpp

@@ -29,6 +29,7 @@
 #include "../widgets/ObjectLists.h"
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
 #include "../windows/CHeroOverview.h"
@@ -793,6 +794,119 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition)
 	setElement(elem, false);
 }
 
+OptionsTab::HandicapWindow::HandicapWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	addUsedEvents(LCLICK);
+
+	pos = Rect(0, 0, 660, 100 + SEL->getStartInfo()->playerInfos.size() * 30);
+
+	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), pos);
+	backgroundTexture->setPlayerColor(PlayerColor(1));
+
+	labels.push_back(std::make_shared<CLabel>(pos.w / 2 + 8, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.handicap")));
+
+	enum Columns : int32_t
+	{
+		INCOME = 1000,
+		GROWTH = 2000,
+	};
+	auto columns = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS, Columns::INCOME, Columns::GROWTH};
+
+	int i = 0;
+	for(auto & pInfo : SEL->getStartInfo()->playerInfos)
+	{
+		PlayerColor player = pInfo.first;
+		anim.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, 7, 57 + i * 30));
+		for(int j = 0; j < columns.size(); j++)
+		{
+			bool isIncome = int(columns[j]) == Columns::INCOME;
+			bool isGrowth = int(columns[j]) == Columns::GROWTH;
+			EGameResID resource = columns[j];
+
+			const PlayerSettings &ps = SEL->getStartInfo()->getIthPlayersSettings(player);
+
+			int xPos = 30 + j * 70;
+			xPos += j > 0 ? 10 : 0; // Gold field is larger
+
+			if(i == 0)
+			{
+				if(isIncome)
+					labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32")));
+				else if(isGrowth)
+					labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194")));
+				else
+					anim.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SMALRES"), GameResID(resource), 0, 15 + xPos + (j == 0 ? 10 : 0), 35));
+			}
+
+			auto area = Rect(xPos, 60 + i * 30, j == 0 ? 60 : 50, 16);
+			textinputbackgrounds.push_back(std::make_shared<TransparentFilledRectangle>(area.resize(3), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)));
+			textinputs[player][resource] = std::make_shared<CTextInput>(area, FONT_SMALL, ETextAlignment::CENTERLEFT, true);
+			textinputs[player][resource]->setText(std::to_string(isIncome ? ps.handicap.percentIncome : (isGrowth ? ps.handicap.percentGrowth : ps.handicap.startBonus[resource])));
+			textinputs[player][resource]->setCallback([this, player, resource, isIncome, isGrowth](const std::string & s){
+				// text input processing: add/remove sign when pressing "-"; remove non digits; cut length; fill empty field with 0
+				std::string tmp = s;
+				bool negative = std::count_if( s.begin(), s.end(), []( char c ){ return c == '-'; }) == 1 && !isIncome && !isGrowth;
+				tmp.erase(std::remove_if(tmp.begin(), tmp.end(), [](char c) { return !isdigit(c); }), tmp.end());
+				int maxLength = isIncome || isGrowth ? 3 : (resource == EGameResID::GOLD ? 6 : 5);
+				tmp = tmp.substr(0, maxLength);
+				textinputs[player][resource]->setText(tmp.length() == 0 ? "0" : (negative ? "-" : "") + std::to_string(stoi(tmp)));
+			});
+			textinputs[player][resource]->setPopupCallback([isIncome, isGrowth](){
+				// Help for the textinputs
+				if(isIncome)
+					CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.income"));
+				else if(isGrowth)
+					CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.growth"));
+				else
+					CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.lobby.handicap.resource"));
+			});
+			if(isIncome || isGrowth)
+				labels.push_back(std::make_shared<CLabel>(area.topRight().x, area.center().y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, "%"));
+		}
+		i++;
+	}
+	
+	buttons.push_back(std::make_shared<CButton>(Point(pos.w / 2 - 32, 60 + SEL->getStartInfo()->playerInfos.size() * 30), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){
+		for (const auto& player : textinputs)
+		{
+			TResources resources = TResources();
+			int income = 100;
+			int growth = 100;
+			for (const auto& resource : player.second)
+			{
+				bool isIncome = int(resource.first) == Columns::INCOME;
+				bool isGrowth = int(resource.first) == Columns::GROWTH;
+				if(isIncome)
+					income = std::stoi(resource.second->getText());
+				else if(isGrowth)
+					growth = std::stoi(resource.second->getText());
+				else
+					resources[resource.first] = std::stoi(resource.second->getText());
+			}
+			CSH->setPlayerHandicap(player.first, Handicap{resources, income, growth});
+		}
+	    		
+		close();
+	}, EShortcut::GLOBAL_RETURN));
+
+	updateShadow();
+	center();
+}
+
+bool OptionsTab::HandicapWindow::receiveEvent(const Point & position, int eventType) const
+{
+	return true;  // capture click also outside of window
+}
+
+void OptionsTab::HandicapWindow::clickReleased(const Point & cursorPosition)
+{
+	if(!pos.isInside(cursorPosition)) // make it possible to close window by touching/clicking outside of window
+		close();
+}
+
 OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type)
 	: Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL)
 	, CPlayerSettingsHelper(playerSettings, type)
@@ -923,6 +1037,47 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 	}
 	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
 
+	auto hasHandicap = [this](){ return s->handicap.startBonus.empty() && s->handicap.percentIncome == 100 && s->handicap.percentGrowth == 100; };
+	std::string labelHandicapText = hasHandicap() ? CGI->generaltexth->arraytxt[210] : MetaString::createFromTextID("vcmi.lobby.handicap").toString();
+	labelHandicap = std::make_shared<CMultiLineLabel>(Rect(57, 24, 47, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText);
+	handicap = std::make_shared<LRClickableArea>(Rect(56, 24, 49, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), [](){
+		if(!CSH->isHost())
+			return;
+		
+		GH.windows().createAndPushWindow<HandicapWindow>();
+	}, [this, hasHandicap](){
+		if(hasHandicap())
+			CRClickPopup::createAndPush(MetaString::createFromTextID("core.help.124.help").toString());
+		else
+		{
+			auto str = MetaString::createFromTextID("vcmi.lobby.handicap");
+			str.appendRawString(":\n");
+			for(auto & res : EGameResID::ALL_RESOURCES())
+				if(s->handicap.startBonus[res] != 0)
+				{
+					str.appendRawString("\n");
+					str.appendName(res);
+					str.appendRawString(": ");
+					str.appendRawString(std::to_string(s->handicap.startBonus[res]));
+				}
+			if(s->handicap.percentIncome != 100)
+			{
+				str.appendRawString("\n");
+				str.appendTextID("core.jktext.32");
+				str.appendRawString(": ");
+				str.appendRawString(std::to_string(s->handicap.percentIncome) + "%");
+			}
+			if(s->handicap.percentGrowth != 100)
+			{
+				str.appendRawString("\n");
+				str.appendTextID("core.genrltxt.194");
+				str.appendRawString(": ");
+				str.appendRawString(std::to_string(s->handicap.percentGrowth) + "%");
+			}
+			CRClickPopup::createAndPush(str.toString());
+		}
+	});
+
 	if(SEL->screenType == ESelectionScreen::newGame)
 	{
 		buttonTownLeft   = std::make_shared<CButton>(Point(107, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color));

+ 20 - 0
client/lobby/OptionsTab.h

@@ -27,8 +27,10 @@ class CComponentBox;
 class CTextBox;
 class CButton;
 class CSlider;
+class LRClickableArea;
 
 class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
 
 /// The options tab which is shown at the map selection phase.
 class OptionsTab : public OptionsTabBase
@@ -51,6 +53,22 @@ public:
 		BONUS
 	};
 
+	class HandicapWindow : public CWindowObject
+	{
+		std::shared_ptr<FilledTexturePlayerColored> backgroundTexture;
+
+		std::vector<std::shared_ptr<CLabel>> labels;
+		std::vector<std::shared_ptr<CAnimImage>> anim;
+		std::vector<std::shared_ptr<TransparentFilledRectangle>> textinputbackgrounds;
+		std::map<PlayerColor, std::map<EGameResID, std::shared_ptr<CTextInput>>> textinputs;
+		std::vector<std::shared_ptr<CButton>> buttons;
+
+		bool receiveEvent(const Point & position, int eventType) const override;
+		void clickReleased(const Point & cursorPosition) override;
+	public:
+		HandicapWindow();
+	};
+
 private:
 	
 	struct CPlayerSettingsHelper
@@ -192,6 +210,8 @@ private:
 		std::shared_ptr<SelectedBox> town;
 		std::shared_ptr<SelectedBox> hero;
 		std::shared_ptr<SelectedBox> bonus;
+		std::shared_ptr<LRClickableArea> handicap;
+		std::shared_ptr<CMultiLineLabel> labelHandicap;
 		enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay;
 
 		PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab);

+ 12 - 1
client/widgets/CTextInput.cpp

@@ -30,7 +30,7 @@ CTextInput::CTextInput(const Rect & Pos)
 	pos.h = Pos.h;
 	pos.w = Pos.w;
 
-	addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT);
+	addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT);
 }
 
 void CTextInput::createLabel(bool giveFocusToInput)
@@ -106,6 +106,11 @@ void CTextInput::setCallback(const TextEditedCallback & cb)
 	onTextEdited = cb;
 }
 
+void CTextInput::setPopupCallback(const std::function<void()> & cb)
+{
+	callbackPopup = cb;
+}
+
 void CTextInput::setFilterFilename()
 {
 	assert(!onTextFiltering);
@@ -122,6 +127,12 @@ std::string CTextInput::getVisibleText() const
 	return hasFocus() ? currentText + composedText + "_" : currentText;
 }
 
+void CTextInput::showPopupWindow(const Point & cursorPosition)
+{
+	if(callbackPopup)
+		callbackPopup();
+}
+
 void CTextInput::clickPressed(const Point & cursorPosition)
 {
 	// attempt to give focus unconditionally, even if we already have it

+ 6 - 0
client/widgets/CTextInput.h

@@ -14,6 +14,7 @@
 #include "../render/EFont.h"
 
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/FunctionList.h"
 
 class CLabel;
 class IImage;
@@ -58,6 +59,7 @@ class CTextInput final : public CFocusable
 
 	TextEditedCallback onTextEdited;
 	TextFilterCallback onTextFiltering;
+	CFunctionList<void()> callbackPopup;
 
 	//Filter that will block all characters not allowed in filenames
 	static void filenameFilter(std::string & text, const std::string & oldText);
@@ -74,6 +76,7 @@ class CTextInput final : public CFocusable
 	void textEdited(const std::string & enteredText) final;
 	void onFocusGot() final;
 	void onFocusLost() final;
+	void showPopupWindow(const Point & cursorPosition) final;
 
 	CTextInput(const Rect & Pos);
 public:
@@ -89,6 +92,9 @@ public:
 	/// Set callback that will be called whenever player enters new text
 	void setCallback(const TextEditedCallback & cb);
 
+	/// Set callback when player want to open popup
+	void setPopupCallback(const std::function<void()> & cb);
+
 	/// Enables filtering entered text that ensures that text is valid filename (existing or not)
 	void setFilterFilename();
 	/// Enable filtering entered text that ensures that text is valid number in provided range [min, max]

+ 6 - 4
client/windows/CKingdomInterface.cpp

@@ -37,6 +37,7 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/GameSettings.h"
 #include "../../lib/CSkillHandler.h"
+#include "../../lib/StartInfo.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
@@ -586,15 +587,16 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 			minesCount[mine->producedResource]++;
 
 			if (mine->producedResource == EGameResID::GOLD)
-				totalIncome += mine->producedQuantity;
+				totalIncome += mine->getProducedQuantity();
 		}
 	}
 
 	//Heroes can produce gold as well - skill, specialty or arts
 	std::vector<const CGHeroInstance*> heroes = LOCPLINT->cb->getHeroesInfo(true);
+	auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
 	for(auto & hero : heroes)
 	{
-		totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD))));
+		totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))) * playerSettings->handicap.percentIncome / 100;
 	}
 
 	//Add town income of all towns
@@ -605,8 +607,8 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 	}
 
 	//if player has some modded boosts we want to show that as well
-	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD)));
-	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size();
+	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * playerSettings->handicap.percentIncome / 100;
+	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size() * playerSettings->handicap.percentIncome / 100;
 
 	for(int i=0; i<7; i++)
 	{

+ 2 - 1
config/shortcutsConfig.json

@@ -150,10 +150,11 @@
 		"lobbyRandomMap":           "R",
 		"lobbyRandomTown":          "T",
 		"lobbyRandomTownVs":        "V",
+		"lobbyHandicap":            "H",
 		"lobbyReplayVideo":         "R",
 		"lobbySaveGame":            [ "S", "Return", "Keypad Enter"],
 		"lobbySelectScenario":      "S",
-		"lobbyToggleChat":          "H",
+		"lobbyToggleChat":          "C",
 		"lobbyTurnOptions":         "T",
 		"mainMenuBack":             [ "B", "Escape" ],
 		"mainMenuCampaign":         "C",

+ 1 - 1
lib/StartInfo.cpp

@@ -25,7 +25,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 PlayerSettings::PlayerSettings()
-	: bonus(PlayerStartingBonus::RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false)
+	: bonus(PlayerStartingBonus::RANDOM), color(0), compOnly(false)
 {
 }
 

+ 25 - 3
lib/StartInfo.h

@@ -16,6 +16,7 @@
 #include "ExtraOptionsInfo.h"
 #include "campaign/CampaignConstants.h"
 #include "serializer/Serializeable.h"
+#include "ResourceSet.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -65,6 +66,20 @@ enum class PlayerStartingBonus : int8_t
 	RESOURCE =  2
 };
 
+struct DLL_LINKAGE Handicap {
+	TResources startBonus = TResources();
+	int percentIncome = 100;
+	int percentGrowth = 100;
+
+	template <typename Handler>
+	void serialize(Handler &h)
+	{
+		h & startBonus;
+		h & percentIncome;
+		h & percentGrowth;
+	}
+};
+
 /// Struct which describes the name, the color, the starting bonus of a player
 struct DLL_LINKAGE PlayerSettings
 {
@@ -77,8 +92,8 @@ struct DLL_LINKAGE PlayerSettings
 
 	std::string heroNameTextId;
 	PlayerColor color; //from 0 -
-	enum EHandicap {NO_HANDICAP, MILD, SEVERE};
-	EHandicap handicap;//0-no, 1-mild, 2-severe
+
+	Handicap handicap;
 
 	std::string name;
 	std::set<ui8> connectedPlayerIDs; //Empty - AI, or connectrd player ids
@@ -92,7 +107,14 @@ struct DLL_LINKAGE PlayerSettings
 		h & heroNameTextId;
 		h & bonus;
 		h & color;
-		h & handicap;
+		if (h.version >= Handler::Version::PLAYER_HANDICAP)
+			h & handicap;
+		else
+		{
+			enum EHandicap {NO_HANDICAP, MILD, SEVERE};
+			EHandicap handicapLegacy = NO_HANDICAP;
+			h & handicapLegacy;
+		}
 		h & name;
 		h & connectedPlayerIDs;
 		h & compOnly;

+ 9 - 5
lib/gameState/CGameState.cpp

@@ -387,10 +387,14 @@ void CGameState::initDifficulty()
 	const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]);
 	const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]);
 	
-	auto setDifficulty = [](PlayerState & state, const JsonNode & json)
+	auto setDifficulty = [this](PlayerState & state, const JsonNode & json)
 	{
 		//set starting resources
 		state.resources = TResources(json["resources"]);
+
+		//handicap
+		const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(state.color);
+		state.resources += ps.handicap.startBonus;
 		
 		//set global bonuses
 		for(auto & jsonBonus : json["globalBonuses"].Vector())
@@ -1614,7 +1618,7 @@ struct statsHLP
 	}
 
 	// get total gold income
-	static int getIncome(const PlayerState * ps)
+	static int getIncome(const PlayerState * ps, int percentIncome)
 	{
 		int totalIncome = 0;
 		const CGObjectInstance * heroOrTown = nullptr;
@@ -1622,7 +1626,7 @@ struct statsHLP
 		//Heroes can produce gold as well - skill, specialty or arts
 		for(const auto & h : ps->heroes)
 		{
-			totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD))));
+			totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
 
 			if(!heroOrTown)
 				heroOrTown = h;
@@ -1657,7 +1661,7 @@ struct statsHLP
 				assert(mine);
 
 				if (mine->producedResource == EGameResID::GOLD)
-					totalIncome += mine->producedQuantity;
+					totalIncome += mine->getProducedQuantity();
 			}
 		}
 
@@ -1747,7 +1751,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 	}
 	if(level >= 5) //income
 	{
-		FILL_FIELD(income, statsHLP::getIncome(&g->second))
+		FILL_FIELD(income, statsHLP::getIncome(&g->second, scenarioOps->getIthPlayersSettings(g->second.color).handicap.percentIncome))
 	}
 	if(level >= 2) //best hero's stats
 	{

+ 15 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -137,6 +137,14 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 	const int base = creature->getGrowth();
 	int castleBonus = 0;
 
+	if(tempOwner.isValidPlayer())
+	{
+		auto * playerSettings = cb->getPlayerSettings(tempOwner);
+		ret.handicapPercentage = playerSettings->handicap.percentGrowth;
+	}
+	else
+		ret.handicapPercentage = 100;
+
 	ret.entries.emplace_back(VLC->generaltexth->allTexts[590], base); // \n\nBasic growth %d"
 
 	if (hasBuilt(BuildingID::CASTLE))
@@ -215,6 +223,11 @@ TResources CGTownInstance::dailyIncome() const
 			ret += p.second->produce;
 		}
 	}
+
+	auto playerSettings = cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner());
+	for(TResources::nziterator it(ret); it.valid(); it++)
+		// always round up income - we don't want to always produce zero if handicap in use
+		ret[it->resType] = (ret[it->resType] * playerSettings.handicap.percentIncome + 99) / 100;
 	return ret;
 }
 
@@ -1257,7 +1270,8 @@ int GrowthInfo::totalGrowth() const
 	for(const Entry &entry : entries)
 		ret += entry.count;
 
-	return ret;
+	// always round up income - we don't want buildings to always produce zero if handicap in use
+	return (ret * handicapPercentage + 99) / 100;
 }
 
 void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const

+ 1 - 0
lib/mapObjects/CGTownInstance.h

@@ -41,6 +41,7 @@ struct DLL_LINKAGE GrowthInfo
 
 	std::vector<Entry> entries;
 	int totalGrowth() const;
+	int handicapPercentage;
 };
 
 class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader

+ 10 - 2
lib/mapObjects/MiscObjects.cpp

@@ -23,6 +23,7 @@
 #include "../gameState/CGameState.h"
 #include "../mapping/CMap.h"
 #include "../CPlayerState.h"
+#include "../StartInfo.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -103,7 +104,7 @@ void CGMine::newTurn(vstd::RNG & rand) const
 	if (tempOwner == PlayerColor::NEUTRAL)
 		return;
 
-	cb->giveResource(tempOwner, producedResource, producedQuantity);
+	cb->giveResource(tempOwner, producedResource, getProducedQuantity());
 }
 
 void CGMine::initObj(vstd::RNG & rand)
@@ -177,7 +178,7 @@ void CGMine::flagMine(const PlayerColor & player) const
 	iw.type = EInfoWindowMode::AUTO;
 	iw.text.appendTextID(TextIdentifier("core.mineevnt", producedResource.getNum()).get()); //not use subID, abandoned mines uses default mine texts
 	iw.player = player;
-	iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity);
+	iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, getProducedQuantity());
 	cb->showInfoDialog(&iw);
 }
 
@@ -195,6 +196,13 @@ ui32 CGMine::defaultResProduction() const
 	}
 }
 
+ui32 CGMine::getProducedQuantity() const
+{
+	auto * playerSettings = cb->getPlayerSettings(getOwner());
+	// always round up income - we don't want mines to always produce zero if handicap in use
+	return (producedQuantity * playerSettings->handicap.percentIncome + 99) / 100;
+}
+
 void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
 {
 	if(result.winner == 0) //attacker won

+ 1 - 0
lib/mapObjects/MiscObjects.h

@@ -181,6 +181,7 @@ public:
 		h & abandonedMineResources;
 	}
 	ui32 defaultResProduction() const;
+	ui32 getProducedQuantity() const;
 
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;

+ 1 - 0
lib/networkPacks/NetPackVisitor.h

@@ -168,6 +168,7 @@ public:
 	virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {}
 	virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {}
 	virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {}
+	virtual void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) {}
 	virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {}
 	virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {}
 	virtual void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) {}

+ 5 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -790,6 +790,11 @@ void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor)
 	visitor.visitLobbySetPlayerName(*this);
 }
 
+void LobbySetPlayerHandicap::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitLobbySetPlayerHandicap(*this);
+}
+
 void LobbySetSimturns::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitLobbySetSimturns(*this);

+ 14 - 0
lib/networkPacks/PacksForLobby.h

@@ -285,6 +285,20 @@ struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer
 	}
 };
 
+struct DLL_LINKAGE LobbySetPlayerHandicap : public CLobbyPackToServer
+{
+	PlayerColor color = PlayerColor::CANNOT_DETERMINE;
+	Handicap handicap = Handicap();
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler &h)
+	{
+		h & color;
+		h & handicap;
+	}
+};
+
 struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer
 {
 	SimturnsInfo simturnsInfo;

+ 1 - 0
lib/registerTypes/RegisterTypesLobbyPacks.h

@@ -55,6 +55,7 @@ void registerTypesLobbyPacks(Serializer &s)
 	s.template registerType<CLobbyPackToServer, LobbySetCampaignBonus>();
 	s.template registerType<CLobbyPackToServer, LobbySetPlayer>();
 	s.template registerType<CLobbyPackToServer, LobbySetPlayerName>();
+	s.template registerType<CLobbyPackToServer, LobbySetPlayerHandicap>();
 	s.template registerType<CLobbyPackToServer, LobbySetTurnTime>();
 	s.template registerType<CLobbyPackToServer, LobbySetSimturns>();
 	s.template registerType<CLobbyPackToServer, LobbySetDifficulty>();

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -58,6 +58,7 @@ enum class ESerializationVersion : int32_t
 	MAP_FORMAT_ADDITIONAL_INFOS, // 848 - serialize new infos in map format
 	REMOVE_LIB_RNG, // 849 - removed random number generators from library classes
 	HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign
+  PLAYER_HANDICAP, // 851 - player handicap selection at game start
 
-	CURRENT = HIGHSCORE_PARAMETERS
+	CURRENT = PLAYER_HANDICAP
 };

+ 5 - 0
lib/texts/MetaString.cpp

@@ -367,6 +367,11 @@ void MetaString::appendName(const CreatureID & id, TQuantity count)
 		appendNamePlural(id);
 }
 
+void MetaString::appendName(const GameResID& id)
+{
+	appendTextID(TextIdentifier("core.restypes", id.getNum()).get());
+}
+
 void MetaString::appendNameSingular(const CreatureID & id)
 {
 	appendTextID(id.toEntity(VLC)->getNameSingularTextID());

+ 1 - 0
lib/texts/MetaString.h

@@ -80,6 +80,7 @@ public:
 	void appendName(const SpellID& id);
 	void appendName(const PlayerColor& id);
 	void appendName(const CreatureID & id, TQuantity count);
+	void appendName(const GameResID& id);
 	void appendNameSingular(const CreatureID & id);
 	void appendNamePlural(const CreatureID & id);
 	void appendEOL();

+ 6 - 4
server/CGameHandler.cpp

@@ -760,6 +760,8 @@ void CGameHandler::onNewTurn()
 			continue;
 
 		assert(elem.first.isValidPlayer());//illegal player number!
+			
+		auto playerSettings = gameState()->scenarioOps->getIthPlayersSettings(elem.first);
 
 		std::pair<PlayerColor, si32> playerGold(elem.first, elem.second.resources[EGameResID::GOLD]);
 		hadGold.insert(playerGold);
@@ -773,8 +775,8 @@ void CGameHandler::onNewTurn()
 		{
 			for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
 			{
-				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k));
-				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size();
+				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
+				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size() * playerSettings.handicap.percentIncome / 100;
 			}
 
 			if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison
@@ -806,7 +808,7 @@ void CGameHandler::onNewTurn()
 					}
 				}
 				if(hasCrystalGenCreature)
-					n.res[elem.first][EGameResID::CRYSTAL] += 3;
+					n.res[elem.first][EGameResID::CRYSTAL] += 3 * playerSettings.handicap.percentIncome / 100;
 			}
 		}
 
@@ -828,7 +830,7 @@ void CGameHandler::onNewTurn()
 			{
 				for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
 				{
-					n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
+					n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
 				}
 			}
 		}

+ 87 - 2
server/CVCMIServer.cpp

@@ -624,8 +624,6 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr<CMapInfo> mapInfo,
 				pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
 				pset.heroPortrait = pinfo.mainCustomHeroPortrait;
 			}
-
-			pset.handicap = PlayerSettings::NO_HANDICAP;
 		}
 
 		if(mi->isRandomMap && mapGenOpts)
@@ -765,6 +763,60 @@ void CVCMIServer::setPlayerName(PlayerColor color, std::string name)
 	setPlayerConnectedId(player, nameID);
 }
 
+void CVCMIServer::setPlayerHandicap(PlayerColor color, Handicap handicap)
+{
+	if(color == PlayerColor::CANNOT_DETERMINE)
+		return;
+
+	si->playerInfos[color].handicap = handicap;
+
+	int humanPlayer = 0;
+	for (const auto & pi : si->playerInfos)
+		if(pi.second.isControlledByHuman())
+			humanPlayer++;
+
+	if(humanPlayer < 2) // Singleplayer
+		return;
+
+	MetaString str;
+	str.appendTextID("vcmi.lobby.handicap");
+	str.appendRawString(" ");
+	str.appendName(color);
+	str.appendRawString(":");
+
+	if(handicap.startBonus.empty() && handicap.percentIncome == 100 && handicap.percentGrowth == 100)
+	{
+		str.appendRawString(" ");
+		str.appendTextID("core.genrltxt.523");
+		announceTxt(str);
+		return;
+	}
+
+	for(auto & res : EGameResID::ALL_RESOURCES())
+		if(handicap.startBonus[res] != 0)
+		{
+			str.appendRawString(" ");
+			str.appendName(res);
+			str.appendRawString(":");
+			str.appendRawString(std::to_string(handicap.startBonus[res]));
+		}
+	if(handicap.percentIncome != 100)
+	{
+		str.appendRawString(" ");
+		str.appendTextID("core.jktext.32");
+		str.appendRawString(":");
+		str.appendRawString(std::to_string(handicap.percentIncome) + "%");
+	}
+	if(handicap.percentGrowth != 100)
+	{
+		str.appendRawString(" ");
+		str.appendTextID("core.genrltxt.194");
+		str.appendRawString(":");
+		str.appendRawString(std::to_string(handicap.percentGrowth) + "%");
+	}
+	announceTxt(str);
+}
+
 void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
 {
 	PlayerSettings & s = si->playerInfos[player];
@@ -1011,6 +1063,39 @@ void CVCMIServer::multiplayerWelcomeMessage()
 
 	gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands");
 
+	for (const auto & pi : si->playerInfos)
+		if(!pi.second.handicap.startBonus.empty() || pi.second.handicap.percentIncome != 100 || pi.second.handicap.percentGrowth != 100)
+		{
+			MetaString str;
+			str.appendTextID("vcmi.lobby.handicap");
+			str.appendRawString(" ");
+			str.appendName(pi.first);
+			str.appendRawString(":");
+			for(auto & res : EGameResID::ALL_RESOURCES())
+				if(pi.second.handicap.startBonus[res] != 0)
+				{
+					str.appendRawString(" ");
+					str.appendName(res);
+					str.appendRawString(":");
+					str.appendRawString(std::to_string(pi.second.handicap.startBonus[res]));
+				}
+			if(pi.second.handicap.percentIncome != 100)
+			{
+				str.appendRawString(" ");
+				str.appendTextID("core.jktext.32");
+				str.appendRawString(":");
+				str.appendRawString(std::to_string(pi.second.handicap.percentIncome) + "%");
+			}
+			if(pi.second.handicap.percentGrowth != 100)
+			{
+				str.appendRawString(" ");
+				str.appendTextID("core.genrltxt.194");
+				str.appendRawString(":");
+				str.appendRawString(std::to_string(pi.second.handicap.percentGrowth) + "%");
+			}
+			gh->playerMessages->broadcastSystemMessage(str);
+		}
+
 	std::vector<std::string> optionIds;
 	if(si->extraOptionsInfo.cheatsAllowed)
 		optionIds.emplace_back("vcmi.optionsTab.cheatAllowed.hover");

+ 1 - 0
server/CVCMIServer.h

@@ -117,6 +117,7 @@ public:
 	// Work with LobbyInfo
 	void setPlayer(PlayerColor clickedColor);
 	void setPlayerName(PlayerColor player, std::string name);
+	void setPlayerHandicap(PlayerColor player, Handicap handicap);
 	void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1
 	void optionSetHero(PlayerColor player, HeroTypeID id);
 	HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction);

+ 1 - 0
server/LobbyNetPackVisitors.h

@@ -89,6 +89,7 @@ public:
 	void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override;
 	void visitLobbySetPlayer(LobbySetPlayer & pack) override;
 	void visitLobbySetPlayerName(LobbySetPlayerName & pack) override;
+	void visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack) override;
 	void visitLobbySetTurnTime(LobbySetTurnTime & pack) override;
 	void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override;
 	void visitLobbySetSimturns(LobbySetSimturns & pack) override;

+ 6 - 0
server/NetPacksLobbyServer.cpp

@@ -346,6 +346,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayerName(LobbySetPlayerName & p
 	result = true;
 }
 
+void ApplyOnServerNetPackVisitor::visitLobbySetPlayerHandicap(LobbySetPlayerHandicap & pack)
+{
+	srv.setPlayerHandicap(pack.color, pack.handicap);
+	result = true;
+}
+
 void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack)
 {
 	srv.si->simturnsInfo = pack.simturnsInfo;

+ 0 - 2
test/game/CGameStateTest.cpp

@@ -175,8 +175,6 @@ public:
 				pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
 				pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait);
 			}
-
-			pset.handicap = PlayerSettings::NO_HANDICAP;
 		}