1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-28 23:06:24 +02:00

Merge pull request #4327 from Laserlicht/handicap

Handicap / resource transfer
This commit is contained in:
Ivan Savenko 2024-08-01 23:11:22 +03:00 committed by GitHub
commit 257fb8c70c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 423 additions and 29 deletions

View File

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

View File

@ -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...",

View File

@ -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:",

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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 },

View File

@ -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)

View File

@ -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);
};

View File

@ -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));

View File

@ -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);

View File

@ -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

View File

@ -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]

View File

@ -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++)
{

View File

@ -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",

View File

@ -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)
{
}

View File

@ -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;

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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) {}

View File

@ -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);

View File

@ -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;

View File

@ -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>();

View File

@ -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
};

View File

@ -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());

View File

@ -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();

View File

@ -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;
}
}
}

View File

@ -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");

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

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