Răsfoiți Sursa

Merge pull request #4444 from Laserlicht/extended_statistic

Statistics
Ivan Savenko 1 an în urmă
părinte
comite
2a84627fd2

+ 32 - 0
Mods/vcmi/config/vcmi/english.json

@@ -162,6 +162,38 @@
 	"vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Town Screen",
 
+	"vcmi.statisticWindow.statistics" : "Statistics",
+	"vcmi.statisticWindow.tsvCopy" : "Data to clipboard",
+	"vcmi.statisticWindow.selectView" : "Select view",
+	"vcmi.statisticWindow.value" : "Value",
+	"vcmi.statisticWindow.title.overview" : "Overview",
+	"vcmi.statisticWindow.title.resources" : "Resources",
+	"vcmi.statisticWindow.title.income" : "Income",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "No. of heroes",
+	"vcmi.statisticWindow.title.numberOfTowns" : "No. of towns",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "No. of artifacts",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "No. of dwellings",
+	"vcmi.statisticWindow.title.numberOfMines" : "No. of mines",
+	"vcmi.statisticWindow.title.armyStrength" : "Army strength",
+	"vcmi.statisticWindow.title.experience" : "Experience",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Army costs",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Building costs",
+	"vcmi.statisticWindow.title.mapExplored" : "Map explore ratio",
+	"vcmi.statisticWindow.param.playerName" : "Player name",
+	"vcmi.statisticWindow.param.daysSurvived" : "Days survived",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Max hero level",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Win ratio (vs. hero)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Win ratio (vs. neutral)",
+	"vcmi.statisticWindow.param.battlesHero" : "Battles (vs. hero)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Battles (vs. neutral)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Max total army strength",
+	"vcmi.statisticWindow.param.tradeVolume" : "Trade volume",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk visited",
+	"vcmi.statisticWindow.icon.townCaptured" : "Town captured",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Strongest hero of opponent defeated",
+	"vcmi.statisticWindow.icon.grailFound" : "Grail found",
+	"vcmi.statisticWindow.icon.defeated" : "Defeated",
+
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Fullscreen (exclusive)",

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

@@ -162,6 +162,38 @@
 	"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
 
+	"vcmi.statisticWindow.statistics" : "Statistik",
+	"vcmi.statisticWindow.tsvCopy" : "Daten in Zwischenabl.",
+	"vcmi.statisticWindow.selectView" : "Ansicht wählen",
+	"vcmi.statisticWindow.value" : "Wert",
+	"vcmi.statisticWindow.title.overview" : "Überblick",
+	"vcmi.statisticWindow.title.resources" : "Ressourcen",
+	"vcmi.statisticWindow.title.income" : "Einkommen",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "Nr. der Helden",
+	"vcmi.statisticWindow.title.numberOfTowns" : "Nr. der Städte",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "Nr. der Artefakte",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "Nr. der Behausungen",
+	"vcmi.statisticWindow.title.numberOfMines" : "Nr. der Minen",
+	"vcmi.statisticWindow.title.armyStrength" : "Armeestärke",
+	"vcmi.statisticWindow.title.experience" : "Erfahrung",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Armeekosten",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Gebäudekosten",
+	"vcmi.statisticWindow.title.mapExplored" : "Maperkundungsrate",
+	"vcmi.statisticWindow.param.playerName" : "Spielername",
+	"vcmi.statisticWindow.param.daysSurvived" : "Tage überlebt",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Max Heldenlevel",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Sieg Verh. (Helden)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Sieg Verh. (Neutral)",
+	"vcmi.statisticWindow.param.battlesHero" : "Kämpfe (Helden)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Kämpfe (Neutral)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Max Gesamt-Armeestärke",
+	"vcmi.statisticWindow.param.tradeVolume" : "Handelsvol.",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk besucht",
+	"vcmi.statisticWindow.icon.townCaptured" : "Stadt erobert",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Stärksten Helden eines Gegners besiegt",
+	"vcmi.statisticWindow.icon.grailFound" : "Gral gefunden",
+	"vcmi.statisticWindow.icon.defeated" : "Besiegt",
+
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Vollbild (exklusiv)",

+ 2 - 0
client/CMakeLists.txt

@@ -65,6 +65,7 @@ set(client_SRCS
 	mainmenu/CPrologEpilogVideo.cpp
 	mainmenu/CreditsScreen.cpp
 	mainmenu/CHighScoreScreen.cpp
+	mainmenu/CStatisticScreen.cpp
 
 	mapView/MapRenderer.cpp
 	mapView/MapRendererContext.cpp
@@ -260,6 +261,7 @@ set(client_HEADERS
 	mainmenu/CPrologEpilogVideo.h
 	mainmenu/CreditsScreen.h
 	mainmenu/CHighScoreScreen.h
+	mainmenu/CStatisticScreen.h
 
 	mapView/IMapRendererContext.h
 	mapView/IMapRendererObserver.h

+ 6 - 6
client/CServerHandler.cpp

@@ -673,13 +673,13 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 	setState(EClientState::GAMEPLAY);
 }
 
-void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory)
+void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic)
 {
 	HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory);
 
 	if(victory && client->gameState()->getStartInfo()->campState)
 	{
-		startCampaignScenario(param, client->gameState()->getStartInfo()->campState);
+		startCampaignScenario(param, client->gameState()->getStartInfo()->campState, statistic);
 	}
 	else
 	{
@@ -689,7 +689,7 @@ void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victo
 
 		endGameplay();
 		CMM->menu->switchToTab("main");
-		GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores);
+		GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores, statistic);
 	}
 }
 
@@ -722,7 +722,7 @@ void CServerHandler::restartGameplay()
 	logicConnection->enterLobbyConnectionMode();
 }
 
-void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs)
+void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic)
 {
 	std::shared_ptr<CampaignState> ourCampaign = cs;
 
@@ -738,7 +738,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 	endGameplay();
 
 	auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
-	auto finisher = [ourCampaign, campaignScoreCalculator]()
+	auto finisher = [ourCampaign, campaignScoreCalculator, statistic]()
 	{
 		if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
 		{
@@ -754,7 +754,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 		else
 		{
 			CMM->openCampaignScreen(ourCampaign->campaignSet);
-			GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator);
+			GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator, statistic);
 		}
 	};
 

+ 3 - 2
client/CServerHandler.h

@@ -13,6 +13,7 @@
 
 #include "../lib/network/NetworkInterface.h"
 #include "../lib/StartInfo.h"
+#include "../lib/gameState/GameStatistics.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -204,11 +205,11 @@ public:
 	void debugStartTest(std::string filename, bool save = false);
 
 	void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
-	void showHighScoresAndEndGameplay(PlayerColor player, bool victory);
+	void showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic);
 	void endNetwork();
 	void endGameplay();
 	void restartGameplay();
-	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {});
+	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic);
 	void showServerError(const std::string & txt) const;
 
 	// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle

+ 1 - 1
client/NetPacksClient.cpp

@@ -420,7 +420,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 			adventureInt.reset();
 		}
 
-		CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory());
+		CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic);
 	}
 
 	// In auto testing pack.mode we always close client if red pack.player won or lose

+ 6 - 0
client/eventsSDL/InputHandler.cpp

@@ -32,6 +32,7 @@
 
 #include <SDL_events.h>
 #include <SDL_timer.h>
+#include <SDL_clipboard.h>
 
 InputHandler::InputHandler()
 	: enableMouse(settings["input"]["enableMouse"].Bool())
@@ -142,6 +143,11 @@ InputMode InputHandler::getCurrentInputMode()
 	return currentInputMode;
 }
 
+void InputHandler::copyToClipBoard(const std::string & text)
+{
+	SDL_SetClipboardText(text.c_str());
+}
+
 std::vector<SDL_Event> InputHandler::acquireEvents()
 {
 	boost::unique_lock<boost::mutex> lock(eventsMutex);

+ 2 - 0
client/eventsSDL/InputHandler.h

@@ -103,4 +103,6 @@ public:
 	bool isKeyboardShiftDown() const;
 
 	InputMode getCurrentInputMode();
+
+	void copyToClipBoard(const std::string & text);
 };

+ 9 - 4
client/gui/CIntObject.cpp

@@ -156,12 +156,17 @@ void CIntObject::setRedrawParent(bool on)
 }
 
 void CIntObject::fitToScreen(int borderWidth, bool propagate)
+{
+	fitToRect(Rect(Point(0, 0), GH.screenDimensions()), borderWidth, propagate);
+}
+
+void CIntObject::fitToRect(Rect rect, int borderWidth, bool propagate)
 {
 	Point newPos = pos.topLeft();
-	vstd::amax(newPos.x, borderWidth);
-	vstd::amax(newPos.y, borderWidth);
-	vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w);
-	vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h);
+	vstd::amax(newPos.x, rect.x + borderWidth);
+	vstd::amax(newPos.y, rect.y + borderWidth);
+	vstd::amin(newPos.x, rect.x + rect.w - borderWidth - pos.w);
+	vstd::amin(newPos.y, rect.y + rect.h - borderWidth - pos.h);
 	if (newPos != pos.topLeft())
 		moveTo(newPos, propagate);
 }

+ 1 - 0
client/gui/CIntObject.h

@@ -122,6 +122,7 @@ public:
 	const Rect & center(const Point &p, bool propagate = true);  //moves object so that point p will be in its center
 	const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position
 	void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen
+	void fitToRect(Rect rect, int borderWidth, bool propagate = true); //moves window to fit into rect
 	void moveBy(const Point &p, bool propagate = true);
 	void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner)
 

+ 1 - 0
client/gui/Shortcut.h

@@ -74,6 +74,7 @@ enum class EShortcut
 	HIGH_SCORES_CAMPAIGNS,
 	HIGH_SCORES_SCENARIOS,
 	HIGH_SCORES_RESET,
+	HIGH_SCORES_STATISTICS,
 
 	// Game lobby / scenario selection
 	LOBBY_BEGIN_STANDARD_GAME, // b

+ 1 - 0
client/gui/ShortcutHandler.cpp

@@ -290,6 +290,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"highScoresCampaigns",      EShortcut::HIGH_SCORES_CAMPAIGNS     },
 		{"highScoresScenarios",      EShortcut::HIGH_SCORES_SCENARIOS     },
 		{"highScoresReset",          EShortcut::HIGH_SCORES_RESET         },
+		{"highScoresStatistics",     EShortcut::HIGH_SCORES_STATISTICS    },
 		{"lobbyReplayVideo",         EShortcut::LOBBY_REPLAY_VIDEO        },
 		{"lobbyExtraOptions",        EShortcut::LOBBY_EXTRA_OPTIONS       },
 		{"lobbyTurnOptions",         EShortcut::LOBBY_TURN_OPTIONS        },

+ 15 - 2
client/mainmenu/CHighScoreScreen.cpp

@@ -11,6 +11,7 @@
 #include "StdInc.h"
 
 #include "CHighScoreScreen.h"
+#include "CStatisticScreen.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
@@ -33,6 +34,7 @@
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 #include "../../lib/gameState/HighScore.h"
+#include "../../lib/gameState/GameStatistics.h"
 
 CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
 	: CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted)
@@ -170,8 +172,8 @@ void CHighScoreScreen::buttonExitClick()
 	close();
 }
 
-CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc)
-	: CWindowObject(BORDERED), won(won), calc(calc)
+CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic)
+	: CWindowObject(BORDERED), won(won), calc(calc), stat(statistic)
 {
 	addUsedEvents(LCLICK | KEYBOARD);
 
@@ -204,6 +206,12 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 		videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();});
 		CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true);
 	}
+
+	if (settings["general"]["enableUiEnhancements"].Bool())
+	{
+		statisticButton = std::make_shared<CButton>(Point(726, 10), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.statisticWindow.statistics")), [this](){ GH.windows().createAndPushWindow<CStatisticScreen>(stat); }, EShortcut::HIGH_SCORES_STATISTICS);
+		texts.push_back(std::make_shared<CLabel>(716, 25, EFonts::FONT_HIGH_SCORE, ETextAlignment::CENTERRIGHT, Colors::WHITE, CGI->generaltexth->translate("vcmi.statisticWindow.statistics") + ":"));
+	}
 }
 
 int CHighScoreInputScreen::addEntry(std::string text) {
@@ -253,6 +261,9 @@ void CHighScoreInputScreen::show(Canvas & to)
 
 void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
 {
+	if(statisticButton->pos.isInside(cursorPosition))
+		return;
+
 	OBJECT_CONSTRUCTION;
 
 	if(!won)
@@ -280,6 +291,8 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
 
 void CHighScoreInputScreen::keyPressed(EShortcut key)
 {
+	if(key == EShortcut::HIGH_SCORES_STATISTICS) // ignore shortcut for skipping video with key
+		return;
 	clickPressed(Point());
 }
 

+ 6 - 2
client/mainmenu/CHighScoreScreen.h

@@ -10,6 +10,7 @@
 #pragma once
 #include "../windows/CWindowObject.h"
 #include "../../lib/gameState/HighScore.h"
+#include "../../lib/gameState/GameStatistics.h"
 
 class CButton;
 class CLabel;
@@ -70,16 +71,19 @@ public:
 
 class CHighScoreInputScreen : public CWindowObject
 {
-	std::vector<std::shared_ptr<CMultiLineLabel>> texts;
+	std::vector<std::shared_ptr<CLabel>> texts;
 	std::shared_ptr<CHighScoreInput> input;
 	std::shared_ptr<TransparentFilledRectangle> background;
 	std::shared_ptr<VideoWidgetBase> videoPlayer;
 	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
+	std::shared_ptr<CButton> statisticButton;
+
 	bool won;
 	HighScoreCalculation calc;
+	StatisticDataSet stat;
 public:
-	CHighScoreInputScreen(bool won, HighScoreCalculation calc);
+	CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic);
 
 	int addEntry(std::string text);
 

+ 521 - 0
client/mainmenu/CStatisticScreen.cpp

@@ -0,0 +1,521 @@
+/*
+ * CStatisticScreen.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 "CStatisticScreen.h"
+#include "../CGameInfo.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/Shortcut.h"
+
+#include "../render/Graphics.h"
+#include "../render/IImage.h"
+#include "../render/IRenderHandler.h"
+
+#include "../widgets/ComboBox.h"
+#include "../widgets/Images.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/Buttons.h"
+#include "../windows/InfoWindows.h"
+#include "../widgets/Slider.h"
+
+#include "../../lib/gameState/GameStatistics.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/TextOperations.h"
+
+#include <vstd/DateUtils.h>
+
+std::string CStatisticScreen::getDay(int d)
+{
+	return std::to_string(CGameState::getDate(d, Date::MONTH)) + "/" + std::to_string(CGameState::getDate(d, Date::WEEK)) + "/" + std::to_string(CGameState::getDate(d, Date::DAY_OF_WEEK));
+}
+
+CStatisticScreen::CStatisticScreen(const StatisticDataSet & stat)
+	: CWindowObject(BORDERED), statistic(stat)
+{
+	OBJECT_CONSTRUCTION;
+	pos = center(Rect(0, 0, 800, 600));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground->setPlayerColor(PlayerColor(1));
+
+	contentArea = Rect(10, 40, 780, 510);
+	layout.emplace_back(std::make_shared<CLabel>(400, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.statisticWindow.statistics")));
+	layout.emplace_back(std::make_shared<TransparentFilledRectangle>(contentArea, ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 80, 128, 255), 1));
+	layout.emplace_back(std::make_shared<CButton>(Point(725, 558), AnimationPath::builtin("MUBCHCK"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT));
+
+	buttonSelect = std::make_shared<CToggleButton>(Point(10, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ onSelectButton(); });
+	buttonSelect->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.selectView"), EFonts::FONT_SMALL, Colors::YELLOW);
+
+	buttonCsvSave = std::make_shared<CToggleButton>(Point(150, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ GH.input().copyToClipBoard(statistic.toCsv("\t"));	});
+	buttonCsvSave->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.tsvCopy"), EFonts::FONT_SMALL, Colors::YELLOW);
+
+	mainContent = getContent(OVERVIEW, EGameResID::NONE);
+}
+
+void CStatisticScreen::onSelectButton()
+{
+	std::vector<std::string> texts;
+	for(auto & val : contentInfo)
+		texts.emplace_back(CGI->generaltexth->translate(std::get<0>(val.second)));
+	GH.windows().createAndPushWindow<StatisticSelector>(texts, [this](int selectedIndex)
+	{
+		OBJECT_CONSTRUCTION;
+		if(!std::get<1>(contentInfo[static_cast<Content>(selectedIndex)]))
+			mainContent = getContent(static_cast<Content>(selectedIndex), EGameResID::NONE);
+		else
+		{
+			auto content = static_cast<Content>(selectedIndex);
+			auto possibleRes = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
+			std::vector<std::string> resourceText;
+			for(const auto & res : possibleRes)
+				resourceText.emplace_back(CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()));
+			
+			GH.windows().createAndPushWindow<StatisticSelector>(resourceText, [this, content, possibleRes](int index)
+			{
+				OBJECT_CONSTRUCTION;
+				mainContent = getContent(content, possibleRes[index]);
+			});
+		}
+	});
+}
+
+TData CStatisticScreen::extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const
+{
+	auto tmpData = stat.data;
+	std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; });
+
+	PlayerColor tmpColor = PlayerColor::NEUTRAL;
+	std::vector<float> tmpColorSet;
+	TData plotData;
+	EPlayerStatus statusLastRound = EPlayerStatus::INGAME;
+	for(const auto & val : tmpData)
+	{
+		if(tmpColor != val.player)
+		{
+			if(tmpColorSet.size())
+			{
+				plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector<float>(tmpColorSet)});
+				tmpColorSet.clear();
+			}
+
+			tmpColor = val.player;
+		}
+		if(val.status == EPlayerStatus::INGAME || (statusLastRound == EPlayerStatus::INGAME && val.status == EPlayerStatus::LOSER))
+			tmpColorSet.emplace_back(selector(val));
+		statusLastRound = val.status; //to keep at least one dataset after loose
+	}
+	if(tmpColorSet.size())
+		plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector<float>(tmpColorSet)});
+
+	return plotData;
+}
+
+TIcons CStatisticScreen::extractIcons() const
+{
+	TIcons icons;
+
+	auto tmpData = statistic.data;
+	std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; });
+
+	auto imageTown = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 3, 0, EImageBlitMode::COLORKEY);
+	imageTown->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
+	auto imageBattle = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 5, 0, EImageBlitMode::COLORKEY);
+	imageBattle->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
+	auto imageDefeated = GH.renderHandler().loadImage(AnimationPath::builtin("tpthchk"), 1, 0, EImageBlitMode::COLORKEY);
+	imageDefeated->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
+	auto imageGrail = GH.renderHandler().loadImage(AnimationPath::builtin("vwsymbol"), 2, 0, EImageBlitMode::COLORKEY);
+	imageGrail->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
+
+	std::map<PlayerColor, bool> foundDefeated;
+	std::map<PlayerColor, bool> foundGrail;
+
+	for(const auto & val : tmpData)
+	{
+		if(val.eventCapturedTown)
+			icons.push_back({ graphics->playerColors[val.player], val.day, imageTown, CGI->generaltexth->translate("vcmi.statisticWindow.icon.townCaptured") });
+		if(val.eventDefeatedStrongestHero)
+			icons.push_back({ graphics->playerColors[val.player], val.day, imageBattle, CGI->generaltexth->translate("vcmi.statisticWindow.icon.strongestHeroDefeated") });
+		if(val.status == EPlayerStatus::LOSER && !foundDefeated[val.player])
+		{
+			foundDefeated[val.player] = true;
+			icons.push_back({ graphics->playerColors[val.player], val.day, imageDefeated, CGI->generaltexth->translate("vcmi.statisticWindow.icon.defeated") });
+		}
+		if(val.hasGrail && !foundGrail[val.player])
+		{
+			foundGrail[val.player] = true;
+			icons.push_back({ graphics->playerColors[val.player], val.day, imageGrail, CGI->generaltexth->translate("vcmi.statisticWindow.icon.grailFound") });
+		}
+	}
+
+	return icons;
+}
+
+std::shared_ptr<CIntObject> CStatisticScreen::getContent(Content c, EGameResID res)
+{
+	TData plotData;
+	TIcons icons = extractIcons();
+
+	switch (c)
+	{
+	case OVERVIEW:
+		return std::make_shared<OverviewPanel>(contentArea.resize(-15), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), statistic);
+	
+	case CHART_RESOURCES:
+		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.resources[res]; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+	
+	case CHART_INCOME:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.income; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_NUMBER_OF_HEROES:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberHeroes; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_NUMBER_OF_TOWNS:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberTowns; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_NUMBER_OF_ARTIFACTS:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberArtifacts; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_NUMBER_OF_DWELLINGS:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberDwellings; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_NUMBER_OF_MINES:
+		plotData = extractData(statistic, [res](StatisticDataSetEntry val) -> float { return val.numMines[res]; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+	
+	case CHART_ARMY_STRENGTH:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.armyStrength; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_EXPERIENCE:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.totalExperience; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
+	
+	case CHART_RESOURCES_SPENT_ARMY:
+		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForArmy[res]; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+	
+	case CHART_RESOURCES_SPENT_BUILDINGS:
+		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForBuildings[res]; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+	
+	case CHART_MAP_EXPLORED:
+		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.mapExploredRatio; });
+		return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 1);
+	}
+
+	return nullptr;
+}
+
+StatisticSelector::StatisticSelector(const std::vector<std::string> & texts, const std::function<void(int selectedIndex)> & cb)
+	: CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), texts(texts), cb(cb)
+{
+	OBJECT_CONSTRUCTION;
+	pos = center(Rect(0, 0, 128 + 16, std::min(static_cast<int>(texts.size()), LINES) * 40));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground->setPlayerColor(PlayerColor(1));
+
+	slider = std::make_shared<CSlider>(Point(pos.w - 16, 0), pos.h, [this](int to){ update(to); redraw(); }, LINES, texts.size(), 0, Orientation::VERTICAL, CSlider::BLUE);
+	slider->setPanningStep(40);
+	slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, pos.h));
+
+	update(0);
+}
+
+void StatisticSelector::update(int to)
+{
+	OBJECT_CONSTRUCTION;
+	buttons.clear();
+	for(int i = to; i < LINES + to; i++)
+	{
+		if(i>=texts.size())
+			continue;
+
+		auto button = std::make_shared<CToggleButton>(Point(0, 10 + (i - to) * 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this, i](bool on){ close(); cb(i); });
+		button->setTextOverlay(texts[i], EFonts::FONT_SMALL, Colors::WHITE);
+		buttons.emplace_back(button);
+	}
+}
+
+OverviewPanel::OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat)
+	: CIntObject(), data(stat)
+{
+	OBJECT_CONSTRUCTION;
+
+	pos = position + pos.topLeft();
+
+	layout.emplace_back(std::make_shared<CLabel>(pos.w / 2, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title));
+
+	canvas = std::make_shared<GraphicalPrimitiveCanvas>(Rect(0, Y_OFFS, pos.w - 16, pos.h - Y_OFFS));
+
+	dataExtract = {
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.playerName"), [this](PlayerColor color){
+				return playerDataFilter(color).front().playerName;
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.daysSurvived"), [this](PlayerColor color){
+				return CStatisticScreen::getDay(playerDataFilter(color).size());
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.maxHeroLevel"), [this](PlayerColor color){
+				int maxLevel = 0;
+				for(const auto & val : playerDataFilter(color))
+					if(maxLevel < val.maxHeroLevel)
+						maxLevel = val.maxHeroLevel;
+				return std::to_string(maxLevel);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioHero"), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				if(!val.numBattlesPlayer)
+					return std::string("");
+				float tmp = (static_cast<float>(val.numWinBattlesPlayer) / static_cast<float>(val.numBattlesPlayer)) * 100;
+				return std::to_string(static_cast<int>(tmp)) + " %";
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioNeutral"), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				if(!val.numWinBattlesNeutral)
+					return std::string("");
+				float tmp = (static_cast<float>(val.numWinBattlesNeutral) / static_cast<float>(val.numBattlesNeutral)) * 100;
+				return std::to_string(static_cast<int>(tmp)) + " %";
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesHero"), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.numBattlesPlayer);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesNeutral"), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.numBattlesNeutral);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.obeliskVisited"), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(static_cast<int>(val.obeliskVisitedRatio * 100)) + " %";
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.maxArmyStrength"), [this](PlayerColor color){
+				int maxArmyStrength = 0;
+				for(const auto & val : playerDataFilter(color))
+					if(maxArmyStrength < val.armyStrength)
+						maxArmyStrength = val.armyStrength;
+				return TextOperations::formatMetric(maxArmyStrength, 6);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GOLD).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::GOLD]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::WOOD).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::WOOD]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::MERCURY).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::MERCURY]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::ORE).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::ORE]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::SULFUR).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::SULFUR]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::CRYSTAL).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::CRYSTAL]);
+			}
+		},
+		{
+			CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GEMS).get()), [this](PlayerColor color){
+				auto val = playerDataFilter(color).back();
+				return std::to_string(val.tradeVolume[EGameResID::GEMS]);
+			}
+		},
+	};
+
+	int usedLines = dataExtract.size();
+
+	slider = std::make_shared<CSlider>(Point(pos.w - 16, Y_OFFS), pos.h - Y_OFFS, [this](int to){ update(to); setRedrawParent(true); redraw(); }, LINES - 1, usedLines, 0, Orientation::VERTICAL, CSlider::BLUE);
+	slider->setPanningStep(canvas->pos.h / LINES);
+	slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, canvas->pos.h));
+
+	fieldSize = Point(canvas->pos.w / (graphics->playerColors.size() + 2), canvas->pos.h / LINES);
+	for(int x = 0; x < graphics->playerColors.size() + 1; x++)
+		for(int y = 0; y < LINES; y++)
+		{
+			int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x;
+			int yStart = y * fieldSize.y;
+			if(x == 0 || y == 0)
+				canvas->addBox(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(0, 0, 0, 100));
+			canvas->addRectangle(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(127, 127, 127, 255));
+		}
+
+	update(0);
+}
+
+std::vector<StatisticDataSetEntry> OverviewPanel::playerDataFilter(PlayerColor color)
+{
+	std::vector<StatisticDataSetEntry> tmpData;
+	std::copy_if(data.data.begin(), data.data.end(), std::back_inserter(tmpData), [color](const StatisticDataSetEntry & e){ return e.player == color; });
+	return tmpData;
+}
+
+void OverviewPanel::update(int to)
+{
+	OBJECT_CONSTRUCTION;
+
+	content.clear();
+	for(int y = to; y < LINES - 1 + to; y++)
+	{
+		if(y >= dataExtract.size())
+			continue;
+
+		for(int x = 0; x < PlayerColor::PLAYER_LIMIT_I + 1; x++)
+		{
+			if(y == to && x < PlayerColor::PLAYER_LIMIT_I)
+				content.emplace_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ITGFLAGS"), x, 0, 180 + x * fieldSize.x, 35));
+			int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x + (x == 0 ? fieldSize.x : (fieldSize.x / 2));
+			int yStart = Y_OFFS + (y + 1 - to) * fieldSize.y + (fieldSize.y / 2);
+			PlayerColor tmpColor(x - 1);
+			if(playerDataFilter(tmpColor).size() || x == 0)
+				content.emplace_back(std::make_shared<CLabel>(xStart, yStart, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, (x == 0 ? dataExtract[y].first : dataExtract[y].second(tmpColor)), x == 0 ? (fieldSize.x * 2) : fieldSize.x));
+		}
+	}
+}
+
+LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY)
+	: CIntObject(), maxVal(0), maxDay(0)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | MOVE);
+
+	pos = position + pos.topLeft();
+
+	layout.emplace_back(std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title));
+
+	chartArea = pos.resize(-50);
+	chartArea.moveTo(Point(50, 50));
+
+	canvas = std::make_shared<GraphicalPrimitiveCanvas>(Rect(0, 0, pos.w, pos.h));
+
+	statusBar = CGStatusBar::create(0, 0, ImagePath::builtin("radialMenu/statusBar"));
+	static_cast<std::shared_ptr<CIntObject>>(statusBar)->setEnabled(false);
+
+	// additional calculations
+	bool skipMaxValCalc = maxY > 0;
+	maxVal = maxY;
+	for(const auto & line : data)
+	{
+		for(auto & val : line.second)
+			if(maxVal < val && !skipMaxValCalc)
+				maxVal = val;
+		if(maxDay < line.second.size())
+			maxDay = line.second.size();
+	}
+
+	// draw
+	for(const auto & line : data)
+	{
+		Point lastPoint(-1, -1);
+		for(int i = 0; i < line.second.size(); i++)
+		{
+			float x = (static_cast<float>(chartArea.w) / static_cast<float>(maxDay - 1)) * static_cast<float>(i);
+			float y = static_cast<float>(chartArea.h) - (static_cast<float>(chartArea.h) / maxVal) * line.second[i];
+			Point p = Point(x, y) + chartArea.topLeft();
+
+			if(lastPoint.x != -1)
+				canvas->addLine(lastPoint, p, line.first);
+			
+			// icons
+			for(auto & icon : icons)
+				if(std::get<0>(icon) == line.first && std::get<1>(icon) == i + 1) // color && day
+				{
+					pictures.emplace_back(std::make_shared<CPicture>(std::get<2>(icon), Point(x - (CHART_ICON_SIZE / 2), y - (CHART_ICON_SIZE / 2)) + chartArea.topLeft()));
+					pictures.back()->addRClickCallback([icon](){ CRClickPopup::createAndPush(std::get<3>(icon)); });
+				}
+
+			lastPoint = p;
+		}
+	}
+
+	// Axis
+	canvas->addLine(chartArea.topLeft() + Point(0, -10), chartArea.topLeft() + Point(0, chartArea.h + 10), Colors::WHITE);
+	canvas->addLine(chartArea.topLeft() + Point(-10, chartArea.h), chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h), Colors::WHITE);
+
+	Point p = chartArea.topLeft() + Point(-5, chartArea.h + 10);
+	layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, "0"));
+	p = chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h + 10);
+	layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CStatisticScreen::getDay(maxDay)));
+	p = chartArea.topLeft() + Point(-5, -10);
+	layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, std::to_string(static_cast<int>(maxVal))));
+	p = chartArea.bottomLeft() + Point(chartArea.w / 2, + 20);
+	layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.64")));
+}
+
+void LineChart::updateStatusBar(const Point & cursorPosition)
+{
+	statusBar->moveTo(cursorPosition + Point(-statusBar->pos.w / 2, 20));
+	statusBar->fitToRect(pos, 10);
+	Rect r(pos.x + chartArea.x, pos.y + chartArea.y, chartArea.w, chartArea.h);
+	statusBar->setEnabled(r.isInside(cursorPosition));
+	if(r.isInside(cursorPosition))
+	{
+		float x = (static_cast<float>(maxDay) / static_cast<float>(chartArea.w)) * (static_cast<float>(cursorPosition.x) - static_cast<float>(r.x)) + 1.0f;
+		float y = maxVal - (maxVal / static_cast<float>(chartArea.h)) * (static_cast<float>(cursorPosition.y) - static_cast<float>(r.y));
+		statusBar->write(CGI->generaltexth->translate("core.genrltxt.64") + ": " + CStatisticScreen::getDay(x) + "   " + CGI->generaltexth->translate("vcmi.statisticWindow.value") + ": " + (static_cast<int>(y) > 0 ? std::to_string(static_cast<int>(y)) : std::to_string(y)));
+	}
+	setRedrawParent(true);
+	redraw();
+}
+
+void LineChart::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance)
+{
+	updateStatusBar(cursorPosition);
+}
+
+void LineChart::clickPressed(const Point & cursorPosition)
+{
+	updateStatusBar(cursorPosition);
+}

+ 134 - 0
client/mainmenu/CStatisticScreen.h

@@ -0,0 +1,134 @@
+/*
+ * CStatisticScreen.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 "../windows/CWindowObject.h"
+#include "../../lib/gameState/GameStatistics.h"
+
+class FilledTexturePlayerColored;
+class CToggleButton;
+class GraphicalPrimitiveCanvas;
+class LineChart;
+class CGStatusBar;
+class ComboBox;
+class CSlider;
+class IImage;
+class CPicture;
+
+using TData = std::vector<std::pair<ColorRGBA, std::vector<float>>>;
+using TIcons = std::vector<std::tuple<ColorRGBA, int, std::shared_ptr<IImage>, std::string>>; // Color, Day, Image, Helptext
+
+const int CHART_ICON_SIZE = 32;
+
+class CStatisticScreen : public CWindowObject
+{
+	enum Content {
+		OVERVIEW,
+		CHART_RESOURCES,
+		CHART_INCOME,
+		CHART_NUMBER_OF_HEROES,
+		CHART_NUMBER_OF_TOWNS,
+		CHART_NUMBER_OF_ARTIFACTS,
+		CHART_NUMBER_OF_DWELLINGS,
+		CHART_NUMBER_OF_MINES,
+		CHART_ARMY_STRENGTH,
+		CHART_EXPERIENCE,
+		CHART_RESOURCES_SPENT_ARMY,
+		CHART_RESOURCES_SPENT_BUILDINGS,
+		CHART_MAP_EXPLORED,
+	};
+	std::map<Content, std::tuple<std::string, bool>> contentInfo = { // tuple: textid, resource selection needed
+		{ OVERVIEW,                        { "vcmi.statisticWindow.title.overview",                false } },
+		{ CHART_RESOURCES,                 { "vcmi.statisticWindow.title.resources",               true  } },
+		{ CHART_INCOME,                    { "vcmi.statisticWindow.title.income",                  false } },
+		{ CHART_NUMBER_OF_HEROES,          { "vcmi.statisticWindow.title.numberOfHeroes",          false } },
+		{ CHART_NUMBER_OF_TOWNS,           { "vcmi.statisticWindow.title.numberOfTowns",           false } },
+		{ CHART_NUMBER_OF_ARTIFACTS,       { "vcmi.statisticWindow.title.numberOfArtifacts",       false } },
+		{ CHART_NUMBER_OF_DWELLINGS,       { "vcmi.statisticWindow.title.numberOfDwellings",       false } },
+		{ CHART_NUMBER_OF_MINES,           { "vcmi.statisticWindow.title.numberOfMines",           true  } },
+		{ CHART_ARMY_STRENGTH,             { "vcmi.statisticWindow.title.armyStrength",            false } },
+		{ CHART_EXPERIENCE,                { "vcmi.statisticWindow.title.experience",              false } },
+		{ CHART_RESOURCES_SPENT_ARMY,      { "vcmi.statisticWindow.title.resourcesSpentArmy",      true  } },
+		{ CHART_RESOURCES_SPENT_BUILDINGS, { "vcmi.statisticWindow.title.resourcesSpentBuildings", true  } },
+		{ CHART_MAP_EXPLORED,              { "vcmi.statisticWindow.title.mapExplored",             false } },
+	};
+
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::vector<std::shared_ptr<CIntObject>> layout;
+	std::shared_ptr<CToggleButton> buttonCsvSave;
+	std::shared_ptr<CToggleButton> buttonSelect;
+	StatisticDataSet statistic;
+	std::shared_ptr<CIntObject> mainContent;
+	Rect contentArea;
+
+	using ExtractFunctor = std::function<float(StatisticDataSetEntry val)>;
+	TData extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const;
+	TIcons extractIcons() const;
+	std::shared_ptr<CIntObject> getContent(Content c, EGameResID res);
+	void onSelectButton();
+public:
+	CStatisticScreen(const StatisticDataSet & stat);
+	static std::string getDay(int day);
+};
+
+class StatisticSelector : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::vector<std::shared_ptr<CToggleButton>> buttons;
+	std::shared_ptr<CSlider> slider;
+
+	const int LINES = 10;
+
+	std::vector<std::string> texts;
+	std::function<void(int selectedIndex)> cb;
+
+	void update(int to);
+public:
+	StatisticSelector(const std::vector<std::string> & texts, const std::function<void(int selectedIndex)> & cb);
+};
+
+class OverviewPanel : public CIntObject
+{
+	std::shared_ptr<GraphicalPrimitiveCanvas> canvas;
+	std::vector<std::shared_ptr<CIntObject>> layout;
+	std::vector<std::shared_ptr<CIntObject>> content;
+	std::shared_ptr<CSlider> slider;
+
+	Point fieldSize;
+	StatisticDataSet data;
+
+	std::vector<std::pair<std::string, std::function<std::string(PlayerColor color)>>> dataExtract;
+
+	const int LINES = 15;
+	const int Y_OFFS = 30;
+
+	std::vector<StatisticDataSetEntry> playerDataFilter(PlayerColor color);
+	void update(int to);
+public:
+	OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat);
+};
+
+class LineChart : public CIntObject
+{
+	std::shared_ptr<GraphicalPrimitiveCanvas> canvas;
+	std::vector<std::shared_ptr<CIntObject>> layout;
+	std::shared_ptr<CGStatusBar> statusBar;
+	std::vector<std::shared_ptr<CPicture>> pictures;
+
+	Rect chartArea;
+	float maxVal;
+	int maxDay;
+
+	void updateStatusBar(const Point & cursorPosition);
+public:
+	LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY);
+
+	void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
+	void clickPressed(const Point & cursorPosition) override;
+};

+ 19 - 0
client/widgets/Images.cpp

@@ -39,6 +39,8 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Point & position)
 	pos += position;
 	pos.w = bg->width();
 	pos.h = bg->height();
+
+	addUsedEvents(SHOW_POPUP);
 }
 
 CPicture::CPicture( const ImagePath &bmpname, int x, int y )
@@ -66,6 +68,8 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position )
 	{
 		pos.w = pos.h = 0;
 	}
+
+	addUsedEvents(SHOW_POPUP);
 }
 
 CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
@@ -74,6 +78,8 @@ CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
 	srcRect = SrcRect;
 	pos.w = srcRect->w;
 	pos.h = srcRect->h;
+
+	addUsedEvents(SHOW_POPUP);
 }
 
 CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, int y)
@@ -82,6 +88,8 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, in
 	srcRect = SrcRect;
 	pos.w = srcRect->w;
 	pos.h = srcRect->h;
+
+	addUsedEvents(SHOW_POPUP);
 }
 
 void CPicture::show(Canvas & to)
@@ -119,6 +127,17 @@ void CPicture::setPlayerColor(PlayerColor player)
 	bg->playerColored(player);
 }
 
+void CPicture::addRClickCallback(const std::function<void()> & callback)
+{
+	rCallback = callback;
+}
+
+void CPicture::showPopupWindow(const Point & cursorPosition)
+{
+	if(rCallback)
+		rCallback();
+}
+
 CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position)
 	: CIntObject(0, position.topLeft())
 	, texture(GH.renderHandler().loadImage(imageName, EImageBlitMode::COLORKEY))

+ 4 - 0
client/widgets/Images.h

@@ -26,6 +26,7 @@ class IImage;
 class CPicture : public CIntObject
 {
 	std::shared_ptr<IImage> bg;
+	std::function<void()> rCallback;
 
 public:
 	/// if set, only specified section of internal image will be rendered
@@ -57,8 +58,11 @@ public:
 	void scaleTo(Point size);
 	void setPlayerColor(PlayerColor player);
 
+	void addRClickCallback(const std::function<void()> & callback);
+
 	void show(Canvas & to) override;
 	void showAll(Canvas & to) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 };
 
 /// area filled with specific texture

+ 1 - 1
client/widgets/TextControls.cpp

@@ -330,6 +330,7 @@ Rect CMultiLineLabel::getTextLocation()
 	case ETextAlignment::TOPLEFT:     return Rect(pos.topLeft(), textSize);
 	case ETextAlignment::TOPCENTER:   return Rect(pos.topLeft(), textSize);
 	case ETextAlignment::CENTER:      return Rect(pos.topLeft() + textOffset / 2, textSize);
+	case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSize);
 	case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize);
 	}
 	assert(0);
@@ -543,7 +544,6 @@ void CGStatusBar::activate()
 
 void CGStatusBar::deactivate()
 {
-	assert(GH.statusbar().get() == this);
 	GH.setStatusbar(nullptr);
 
 	if (enteringText)

+ 1 - 0
config/shortcutsConfig.json

@@ -137,6 +137,7 @@
 		"heroToggleTactics":        "B",
 		"highScoresCampaigns":      "C",
 		"highScoresReset":          "R",
+		"highScoresStatistics":     ".",
 		"highScoresScenarios":      "S",
 		"kingdomHeroesTab":         "H",
 		"kingdomTownsTab":          "T",

+ 5 - 0
lib/Color.h

@@ -57,6 +57,11 @@ public:
 		h & b;
 		h & a;
 	}
+
+	bool operator==(ColorRGBA const& rhs) const
+	{
+		return r == rhs.r && g == rhs.g && b == rhs.b && a == rhs.a;
+	}
 };
 
 VCMI_LIB_NAMESPACE_END

+ 11 - 6
lib/gameState/CGameState.cpp

@@ -129,26 +129,26 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner)
 	throw std::runtime_error("Can not allocate hero. All heroes are already used.");
 }
 
-int CGameState::getDate(Date mode) const
+int CGameState::getDate(int d, Date mode)
 {
 	int temp;
 	switch (mode)
 	{
 	case Date::DAY:
-		return day;
+		return d;
 	case Date::DAY_OF_WEEK: //day of week
-		temp = (day)%7; // 1 - Monday, 7 - Sunday
+		temp = (d)%7; // 1 - Monday, 7 - Sunday
 		return temp ? temp : 7;
 	case Date::WEEK:  //current week
-		temp = ((day-1)/7)+1;
+		temp = ((d-1)/7)+1;
 		if (!(temp%4))
 			return 4;
 		else
 			return (temp%4);
 	case Date::MONTH: //current month
-		return ((day-1)/28)+1;
+		return ((d-1)/28)+1;
 	case Date::DAY_OF_MONTH: //day of month
-		temp = (day)%28;
+		temp = (d)%28;
 		if (temp)
 			return temp;
 		else return 28;
@@ -156,6 +156,11 @@ int CGameState::getDate(Date mode) const
 	return 0;
 }
 
+int CGameState::getDate(Date mode) const
+{
+	return getDate(day, mode);
+}
+
 CGameState::CGameState()
 {
 	gs = this;

+ 1 - 0
lib/gameState/CGameState.h

@@ -138,6 +138,7 @@ public:
 	bool isVisible(int3 pos, const std::optional<PlayerColor> & player) const override;
 	bool isVisible(const CGObjectInstance * obj, const std::optional<PlayerColor> & player) const override;
 
+	static int getDate(int day, Date mode);
 	int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
 
 	// ----- getters, setters -----

+ 86 - 63
lib/gameState/GameStatistics.cpp

@@ -11,6 +11,7 @@
 #include "GameStatistics.h"
 #include "../CPlayerState.h"
 #include "../constants/StringConstants.h"
+#include "../VCMIDirs.h"
 #include "CGameState.h"
 #include "TerrainHandler.h"
 #include "CHeroHandler.h"
@@ -44,6 +45,7 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
 	data.timestamp = std::time(nullptr);
 	data.day = gs->getDate(Date::DAY);
 	data.player = ps->color;
+	data.playerName = gs->getStartInfo()->playerInfos.at(ps->color).name;
 	data.team = ps->team;
 	data.isHuman = ps->isHuman();
 	data.status = ps->status;
@@ -71,101 +73,122 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
 	data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
 	data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
 	data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources();
+	data.eventCapturedTown = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
+	data.eventDefeatedStrongestHero = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
 	data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0;
 
 	return data;
 }
 
-std::string StatisticDataSet::toCsv()
+std::string StatisticDataSet::toCsv(std::string sep)
 {
 	std::stringstream ss;
 
 	auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
 
-	ss << "Map" << ";";
-	ss << "Timestamp" << ";";
-	ss << "Day" << ";";
-	ss << "Player" << ";";
-	ss << "Team" << ";";
-	ss << "IsHuman" << ";";
-	ss << "Status" << ";";
-	ss << "NumberHeroes" << ";";
-	ss << "NumberTowns" << ";";
-	ss << "NumberArtifacts" << ";";
-	ss << "NumberDwellings" << ";";
-	ss << "ArmyStrength" << ";";
-	ss << "TotalExperience" << ";";
-	ss << "Income" << ";";
-	ss << "MapExploredRatio" << ";";
-	ss << "ObeliskVisitedRatio" << ";";
-	ss << "TownBuiltRatio" << ";";
-	ss << "HasGrail" << ";";
-	ss << "Score" << ";";
-	ss << "MaxHeroLevel" << ";";
-	ss << "NumBattlesNeutral" << ";";
-	ss << "NumBattlesPlayer" << ";";
-	ss << "NumWinBattlesNeutral" << ";";
-	ss << "NumWinBattlesPlayer" << ";";
-	ss << "NumHeroSurrendered" << ";";
-	ss << "NumHeroEscaped" << ";";
+	ss << "Map" << sep;
+	ss << "Timestamp" << sep;
+	ss << "Day" << sep;
+	ss << "Player" << sep;
+	ss << "PlayerName" << sep;
+	ss << "Team" << sep;
+	ss << "IsHuman" << sep;
+	ss << "Status" << sep;
+	ss << "NumberHeroes" << sep;
+	ss << "NumberTowns" << sep;
+	ss << "NumberArtifacts" << sep;
+	ss << "NumberDwellings" << sep;
+	ss << "ArmyStrength" << sep;
+	ss << "TotalExperience" << sep;
+	ss << "Income" << sep;
+	ss << "MapExploredRatio" << sep;
+	ss << "ObeliskVisitedRatio" << sep;
+	ss << "TownBuiltRatio" << sep;
+	ss << "HasGrail" << sep;
+	ss << "Score" << sep;
+	ss << "MaxHeroLevel" << sep;
+	ss << "NumBattlesNeutral" << sep;
+	ss << "NumBattlesPlayer" << sep;
+	ss << "NumWinBattlesNeutral" << sep;
+	ss << "NumWinBattlesPlayer" << sep;
+	ss << "NumHeroSurrendered" << sep;
+	ss << "NumHeroEscaped" << sep;
+	ss << "EventCapturedTown" << sep;
+	ss << "EventDefeatedStrongestHero" << sep;
 	ss << "MovementPointsUsed";
 	for(auto & resource : resources)
-		ss << ";" << GameConstants::RESOURCE_NAMES[resource];
+		ss << sep << GameConstants::RESOURCE_NAMES[resource];
 	for(auto & resource : resources)
-		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "Mines";
+		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "Mines";
 	for(auto & resource : resources)
-		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
+		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
 	for(auto & resource : resources)
-		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
+		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
 	for(auto & resource : resources)
-		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
+		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
 	ss << "\r\n";
 
 	for(auto & entry : data)
 	{
-		ss << entry.map << ";";
-		ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << ";";
-		ss << entry.day << ";";
-		ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << ";";
-		ss << entry.team.getNum() << ";";
-		ss << entry.isHuman << ";";
-		ss << static_cast<int>(entry.status) << ";";
-		ss << entry.numberHeroes << ";";
-		ss << entry.numberTowns <<  ";";
-		ss << entry.numberArtifacts << ";";
-		ss << entry.numberDwellings << ";";
-		ss << entry.armyStrength << ";";
-		ss << entry.totalExperience << ";";
-		ss << entry.income << ";";
-		ss << entry.mapExploredRatio << ";";
-		ss << entry.obeliskVisitedRatio << ";";
-		ss << entry.townBuiltRatio << ";";
-		ss << entry.hasGrail << ";";
-		ss << entry.score << ";";
-		ss << entry.maxHeroLevel << ";";
-		ss << entry.numBattlesNeutral << ";";
-		ss << entry.numBattlesPlayer << ";";
-		ss << entry.numWinBattlesNeutral << ";";
-		ss << entry.numWinBattlesPlayer << ";";
-		ss << entry.numHeroSurrendered << ";";
-		ss << entry.numHeroEscaped << ";";
+		ss << entry.map << sep;
+		ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << sep;
+		ss << entry.day << sep;
+		ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << sep;
+		ss << entry.playerName << sep;
+		ss << entry.team.getNum() << sep;
+		ss << entry.isHuman << sep;
+		ss << static_cast<int>(entry.status) << sep;
+		ss << entry.numberHeroes << sep;
+		ss << entry.numberTowns <<  sep;
+		ss << entry.numberArtifacts << sep;
+		ss << entry.numberDwellings << sep;
+		ss << entry.armyStrength << sep;
+		ss << entry.totalExperience << sep;
+		ss << entry.income << sep;
+		ss << entry.mapExploredRatio << sep;
+		ss << entry.obeliskVisitedRatio << sep;
+		ss << entry.townBuiltRatio << sep;
+		ss << entry.hasGrail << sep;
+		ss << entry.score << sep;
+		ss << entry.maxHeroLevel << sep;
+		ss << entry.numBattlesNeutral << sep;
+		ss << entry.numBattlesPlayer << sep;
+		ss << entry.numWinBattlesNeutral << sep;
+		ss << entry.numWinBattlesPlayer << sep;
+		ss << entry.numHeroSurrendered << sep;
+		ss << entry.numHeroEscaped << sep;
+		ss << entry.eventCapturedTown << sep;
+		ss << entry.eventDefeatedStrongestHero << sep;
 		ss << entry.movementPointsUsed;
 		for(auto & resource : resources)
-			ss << ";" << entry.resources[resource];
+			ss << sep << entry.resources[resource];
 		for(auto & resource : resources)
-			ss << ";" << entry.numMines[resource];
+			ss << sep << entry.numMines[resource];
 		for(auto & resource : resources)
-			ss << ";" << entry.spentResourcesForArmy[resource];
+			ss << sep << entry.spentResourcesForArmy[resource];
 		for(auto & resource : resources)
-			ss << ";" << entry.spentResourcesForBuildings[resource];
+			ss << sep << entry.spentResourcesForBuildings[resource];
 		for(auto & resource : resources)
-			ss << ";" << entry.tradeVolume[resource];
+			ss << sep << entry.tradeVolume[resource];
 		ss << "\r\n";
 	}
 
 	return ss.str();
 }
 
+std::string StatisticDataSet::writeCsv()
+{
+	const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
+	boost::filesystem::create_directories(outPath);
+
+	const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
+	std::ofstream file(filePath.c_str());
+	std::string csv = toCsv(";");
+	file << csv;
+
+	return filePath.string();
+}
+
 std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const PlayerState * ps)
 {
 	std::vector<const CGMine *> tmp;

+ 20 - 3
lib/gameState/GameStatistics.h

@@ -25,6 +25,7 @@ struct DLL_LINKAGE StatisticDataSetEntry
 	time_t timestamp;
     int day;
     PlayerColor player;
+    std::string playerName;
 	TeamID team;
 	bool isHuman;
 	EPlayerStatus status;
@@ -52,6 +53,8 @@ struct DLL_LINKAGE StatisticDataSetEntry
 	TResources spentResourcesForArmy;
 	TResources spentResourcesForBuildings;
 	TResources tradeVolume;
+	bool eventCapturedTown;
+	bool eventDefeatedStrongestHero;
 	si64 movementPointsUsed;
 
 	template <typename Handler> void serialize(Handler &h)
@@ -60,6 +63,8 @@ struct DLL_LINKAGE StatisticDataSetEntry
 		h & timestamp;
 		h & day;
 		h & player;
+		if(h.version >= Handler::Version::STATISTICS_SCREEN)
+			h & playerName;
 		h & team;
 		h & isHuman;
 		h & status;
@@ -87,18 +92,22 @@ struct DLL_LINKAGE StatisticDataSetEntry
 		h & spentResourcesForArmy;
 		h & spentResourcesForBuildings;
 		h & tradeVolume;
+		if(h.version >= Handler::Version::STATISTICS_SCREEN)
+		{
+			h & eventCapturedTown;
+			h & eventDefeatedStrongestHero;
+		}
 		h & movementPointsUsed;
 	}
 };
 
 class DLL_LINKAGE StatisticDataSet
 {
-    std::vector<StatisticDataSetEntry> data;
-
 public:
     void add(StatisticDataSetEntry entry);
 	static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs);
-    std::string toCsv();
+    std::string toCsv(std::string sep);
+    std::string writeCsv();
 
 	struct PlayerAccumulatedValueStorage // holds some actual values needed for stats
 	{
@@ -112,6 +121,8 @@ public:
 		TResources spentResourcesForBuildings;
 		TResources tradeVolume;
 		si64 movementPointsUsed;
+		int lastCapturedTownDay;
+		int lastDefeatedStrongestHeroDay;
 
 		template <typename Handler> void serialize(Handler &h)
 		{
@@ -125,8 +136,14 @@ public:
 			h & spentResourcesForBuildings;
 			h & tradeVolume;
 			h & movementPointsUsed;
+			if(h.version >= Handler::Version::STATISTICS_SCREEN)
+			{
+				h & lastCapturedTownDay;
+				h & lastDefeatedStrongestHeroDay;
+			}
 		}
 	};
+	std::vector<StatisticDataSetEntry> data;
 	std::map<PlayerColor, PlayerAccumulatedValueStorage> accumulatedValues;
 
 	template <typename Handler> void serialize(Handler &h)

+ 4 - 0
lib/networkPacks/PacksForClient.h

@@ -24,6 +24,7 @@
 #include "../gameState/RumorState.h"
 #include "../gameState/QuestInfo.h"
 #include "../gameState/TavernSlot.h"
+#include "../gameState/GameStatistics.h"
 #include "../int3.h"
 #include "../mapping/CMapDefines.h"
 #include "../spells/ViewSpellInt.h"
@@ -435,6 +436,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient
 
 	PlayerColor player;
 	EVictoryLossCheckResult victoryLossCheckResult;
+	StatisticDataSet statistic;
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -442,6 +444,8 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient
 	{
 		h & player;
 		h & victoryLossCheckResult;
+		if (h.version >= Handler::Version::STATISTICS_SCREEN)
+			h & statistic;
 	}
 };
 

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -64,6 +64,7 @@ enum class ESerializationVersion : int32_t
 	CAMPAIGN_REGIONS, // 853 - configurable campaign regions
 	EVENTS_PLAYER_SET, // 854 - map & town events use std::set instead of bitmask to store player list
 	NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed
+	STATISTICS_SCREEN, // 856 - extent statistic functions
 
-	CURRENT = NEW_TOWN_BUILDINGS
+	CURRENT = STATISTICS_SCREEN
 };

+ 10 - 4
server/CGameHandler.cpp

@@ -681,7 +681,7 @@ void CGameHandler::onPlayerTurnEnded(PlayerColor which)
 		heroPool->onNewWeek(which);
 }
 
-void CGameHandler::addStatistics()
+void CGameHandler::addStatistics(StatisticDataSet &stat) const
 {
 	for (const auto & elem : gs->players)
 	{
@@ -690,7 +690,7 @@ void CGameHandler::addStatistics()
 
 		auto data = StatisticDataSet::createEntry(&elem.second, gs);
 
-		gameState()->statistic.add(data);
+		stat.add(data);
 	}
 }
 
@@ -718,6 +718,10 @@ void CGameHandler::onNewTurn()
 			}
 		}
 	}
+	else
+	{
+		addStatistics(gameState()->statistic); // write at end of turn
+	}
 
 	for (const auto & player : gs->players)
 	{
@@ -1036,8 +1040,6 @@ void CGameHandler::onNewTurn()
 	}
 
 	synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that
-
-	addStatistics();
 }
 
 void CGameHandler::start(bool resume)
@@ -1414,6 +1416,8 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne
 	const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(obj);
 	if (town) //town captured
 	{
+		gs->statistic.accumulatedValues[owner].lastCapturedTownDay = gs->getDate(Date::DAY);
+
 		if (owner.isValidPlayer()) //new owner is real player
 		{
 			if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
@@ -3733,6 +3737,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 		PlayerEndsGame peg;
 		peg.player = player;
 		peg.victoryLossCheckResult = victoryLossCheckResult;
+		peg.statistic = StatisticDataSet(gameState()->statistic);
+		addStatistics(peg.statistic); // add last turn befor win / loss
 		sendAndApply(&peg);
 
 		turnOrder->onPlayerEndsGame(player);

+ 2 - 1
server/CGameHandler.h

@@ -14,6 +14,7 @@
 #include "../lib/IGameCallback.h"
 #include "../lib/LoadProgress.h"
 #include "../lib/ScriptHandler.h"
+#include "../lib/gameState/GameStatistics.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -227,7 +228,7 @@ public:
 	void onPlayerTurnStarted(PlayerColor which);
 	void onPlayerTurnEnded(PlayerColor which);
 	void onNewTurn();
-	void addStatistics();
+	void addStatistics(StatisticDataSet &stat) const;
 
 	void handleTimeEvents(PlayerColor player);
 	void handleTownEvents(CGTownInstance *town);

+ 11 - 0
server/battles/BattleResultProcessor.cpp

@@ -480,6 +480,17 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 	// Remove beaten hero
 	if(finishingBattle->loserHero)
 	{
+		//add statistics
+		if(!finishingBattle->isDraw())
+		{
+			ConstTransitivePtr<CGHeroInstance> strongestHero = nullptr;
+			for(auto & hero : gameHandler->gameState()->getPlayerState(finishingBattle->loser)->heroes)
+				if(!strongestHero || hero->exp > strongestHero->exp)
+					strongestHero = hero;
+			if(strongestHero->id == finishingBattle->loserHero->id && strongestHero->level > 5)
+				gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->victor].lastDefeatedStrongestHeroDay = gameHandler->gameState()->getDate(Date::DAY);
+		}
+
 		RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor);
 		gameHandler->sendAndApply(&ro);
 	}

+ 2 - 8
server/processors/PlayerMessageProcessor.cpp

@@ -140,15 +140,9 @@ void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vec
 	if(!isHost)
 		return;
 
-	const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
-	boost::filesystem::create_directories(outPath);
+	std::string path = gameHandler->gameState()->statistic.writeCsv();
 
-	const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
-	std::ofstream file(filePath.c_str());
-	std::string csv = gameHandler->gameState()->statistic.toCsv();
-	file << csv;
-
-	broadcastSystemMessage("Statistic files can be found in " + outPath.string() + " directory\n");
+	broadcastSystemMessage("Statistic files can be found in " + path + " directory\n");
 }
 
 void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector<std::string> & words)