1 /*
2  * Copyright (C) 2012 Me and My Shadow
3  *
4  * This file is part of Me and My Shadow.
5  *
6  * Me and My Shadow is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Me and My Shadow is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Me and My Shadow.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include <stdio.h>
21 #include <string>
22 #include <vector>
23 #include <map>
24 #include "StatisticsManager.h"
25 #include "StatisticsScreen.h"
26 #include "Globals.h"
27 #include "Functions.h"
28 #include "ThemeManager.h"
29 #include "InputManager.h"
30 #include "GUIListBox.h"
31 #include "GUIScrollBar.h"
32 #include "EasterEggScreen.h"
33 #include <SDL_ttf.h>
34 
35 #include "libs/tinyformat/tinyformat.h"
36 
37 using namespace std;
38 
39 //GUI events are handled here.
40 //name: The name of the element that invoked the event.
41 //obj: Pointer to the object that invoked the event.
42 //eventType: Integer containing the type of event.
GUIEventCallback_OnEvent(ImageManager & imageManager,SDL_Renderer & renderer,std::string name,GUIObject * obj,int eventType)43 void StatisticsScreen::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
44 	//Check what type of event it was.
45 	if(eventType==GUIEventClick){
46 		if(name=="cmdBack"){
47 			//Goto the main menu.
48 			setNextState(STATE_MENU);
49 		}
50 	}
51 }
52 
53 //Constructor.
StatisticsScreen(ImageManager & imageManager,SDL_Renderer & renderer)54 StatisticsScreen::StatisticsScreen(ImageManager& imageManager, SDL_Renderer& renderer){
55 	//Update in-game time.
56 	statsMgr.updatePlayTime();
57 
58 	//Render the title.
59 	title = titleTextureFromText(renderer, _("Achievements and Statistics"), objThemes.getTextColor(false), SCREEN_WIDTH);
60 
61 
62 	//Create GUI.
63     createGUI(imageManager, renderer);
64 }
65 
66 //Destructor.
~StatisticsScreen()67 StatisticsScreen::~StatisticsScreen(){
68 	//Delete the GUI.
69 	if(GUIObjectRoot){
70 		delete GUIObjectRoot;
71 		GUIObjectRoot=NULL;
72 	}
73 }
74 
75 //we are so lazy that we just use height of the first text, ignore the others
76 #define DRAW_PLAYER_STATISTICS(name,var,fmt) { \
77     SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,name,objThemes.getTextColor(true))); \
78     SurfacePtr stats = createSurface(w,surface->h); \
79     SDL_FillRect(stats.get(),NULL,-1); \
80     applySurface(4,0,surface.get(),stats.get(),NULL); \
81     y=surface->h; \
82     surface.reset(TTF_RenderUTF8_Blended(fontText, \
83 		tfm::format(fmt,statsMgr.player##var+statsMgr.shadow##var).c_str(), \
84 		objThemes.getTextColor(true))); \
85     applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
86     surface.reset(TTF_RenderUTF8_Blended(fontText, \
87 		tfm::format(fmt,statsMgr.player##var).c_str(), \
88 		objThemes.getTextColor(true))); \
89     applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
90     surface.reset(TTF_RenderUTF8_Blended(fontText, \
91 		tfm::format(fmt,statsMgr.shadow##var).c_str(), \
92 		objThemes.getTextColor(true))); \
93     applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
94     list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats))); /* add it to list box */ \
95 }
96 
97 //Add an item to the listbox, that displays "name1", and "var1" formatted with "format"
98 //we are so lazy that we just use height of the first text, ignore the others
99 template <class T1>
drawMiscStatistics1(SDL_Renderer & renderer,int w,GUIListBox * list,const char * name1,const T1 var1,const char * format1)100 static void drawMiscStatistics1(SDL_Renderer& renderer, int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1){
101 	//create new surface
102     SurfacePtr nameSurface(TTF_RenderUTF8_Blended(fontGUISmall,name1,objThemes.getTextColor(true)));
103 
104     SurfacePtr stats=createSurface(w, nameSurface->h);
105     SDL_FillRect(stats.get(),NULL,-1);
106 
107     applySurface(4,0,nameSurface.get(),stats.get(),NULL);
108     const int x=nameSurface->w+8;
109     const int y=nameSurface->h;
110 
111     SurfacePtr formatSurface(TTF_RenderUTF8_Blended(fontText,
112 		tfm::format(format1,var1).c_str(),
113 		objThemes.getTextColor(true)));
114     //NOTE: SDL2 port. Not halving the y value here as this ends up looking better.
115     applySurface(x,y-formatSurface->h,formatSurface.get(),stats.get(),NULL);
116 
117 	//add it to list box
118     list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
119 
120 	//over
121     //return stats;
122 }
123 
124 //NOTE: Disabled this for the SDL2 port for now. It looks a bit off anyhow.
125 //Might want to make a more general method that draws as many "cells" as there is space.
126 //Draws two stats on one line if there is space.
127 //we are so lazy that we just use height of the first text, ignore the others
128 /*template <class T1,class T2>
129 static void drawMiscStatistics2(int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1,const char* name2,const T2 var2,const char* format2){
130 	SDL_Surface* stats=drawMiscStatistics1(w,list,name1,var1,format1);
131 
132 	//Check if the width is enough
133 	if(w>=800){
134 		//draw name
135 		SDL_Surface* surface=TTF_RenderUTF8_Blended(fontGUISmall,name2,objThemes.getTextColor(true));
136 		applySurface(w/2-8,stats->h-surface->h,surface,stats,NULL);
137 		int x=surface->w+w/2;
138 		SDL_FreeSurface(surface);
139 
140 		//draw value
141 		char s[1024];
142 		//FIXME: Use tfm::format instead of sprintf to enable locale support
143 		FIXME_sprintf(s,format2,var2);
144 		surface=TTF_RenderUTF8_Blended(fontText,s,objThemes.getTextColor(true));
145 		applySurface(x,(stats->h-surface->h)/2,surface,stats,NULL);
146 		SDL_FreeSurface(surface);
147 	}else{
148 		//Split into two rows
149 		drawMiscStatistics1(w,list,name2,var2,format2);
150 	}
151 }*/
152 
addAchievements(ImageManager & imageManager,SDL_Renderer & renderer,GUIListBox * list,bool revealUnknownAchievements)153 void StatisticsScreen::addAchievements(ImageManager& imageManager, SDL_Renderer &renderer, GUIListBox *list, bool revealUnknownAchievements) {
154 	for (int idx = 0; achievementList[idx].id != NULL; ++idx) {
155 		time_t *lpt = NULL;
156 
157 		map<string, OwnedAchievement>::iterator it = statsMgr.achievements.find(achievementList[idx].id);
158 		if (it != statsMgr.achievements.end()) {
159 			lpt = &it->second.achievedTime;
160 		}
161 
162 		AchievementInfo info = achievementList[idx];
163 		if (revealUnknownAchievements) {
164 			if (info.displayStyle == ACHIEVEMENT_HIDDEN || info.displayStyle == ACHIEVEMENT_TITLE) {
165 				info.displayStyle = ACHIEVEMENT_ALL;
166 			}
167 		}
168 
169 		SDL_Rect r;
170 		r.x = r.y = 0;
171 		r.w = list->width - 16;
172 		auto surface = statsMgr.createAchievementSurface(renderer, &info, &r, false, lpt);
173 
174 		if (surface){
175 			list->addItem(renderer, "", surface);
176 		}
177 	}
178 }
179 
180 //Method that will create the GUI.
createGUI(ImageManager & imageManager,SDL_Renderer & renderer)181 void StatisticsScreen::createGUI(ImageManager& imageManager, SDL_Renderer &renderer){
182 	//Create the root element of the GUI.
183 	if(GUIObjectRoot){
184 		delete GUIObjectRoot;
185 		GUIObjectRoot=NULL;
186 	}
187     GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
188 
189 	//Create back button.
190     GUIObject* obj=new GUIButton(imageManager,renderer,SCREEN_WIDTH*0.5,SCREEN_HEIGHT-60,-1,36,_("Back"),0,true,true,GUIGravityCenter);
191 	obj->name="cmdBack";
192 	obj->eventCallback=this;
193 	GUIObjectRoot->addChild(obj);
194 
195 	//Create list box.
196     listBox=new GUISingleLineListBox(imageManager,renderer,(SCREEN_WIDTH-500)/2,104,500,32);
197 	listBox->addItem(_("Achievements"));
198 	listBox->addItem(_("Statistics"));
199 	listBox->value=0;
200 	GUIObjectRoot->addChild(listBox);
201 
202 	//Create list box for achievements.
203     GUIListBox *list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72);
204 	list->selectable=false;
205 	GUIObjectRoot->addChild(list);
206 	lists.clear();
207 	lists.push_back(list);
208 
209 	addAchievements(imageManager, renderer, list);
210 
211 	//Now create list box for statistics.
212     list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72,true,false);
213 	list->selectable=false;
214 	GUIObjectRoot->addChild(list);
215 	lists.push_back(list);
216 
217 	//Load needed pictures.
218 	//FIXME: hard-coded image path
219     //TODO: Might want to consider not caching these as most other stuff use textures now.
220     SDL_Surface* bmPlayer=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/player.png");
221     SDL_Surface* bmShadow=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/shadow.png");
222     SDL_Surface* bmMedal=imageManager.loadImage(getDataPath()+"gfx/medals.png");
223 
224 	SDL_Rect r;
225 	int x,y,w=SCREEN_WIDTH-128;
226     SharedTexture h_bar = [&](){
227         //The horizontal bar.
228         SurfacePtr h_bar(createSurface(w,2));
229 		SDL_Color c = objThemes.getTextColor(true);
230 		Uint32 clr=SDL_MapRGB(h_bar->format,c.r,c.g,c.b);
231         SDL_FillRect(h_bar.get(),NULL,clr);
232         return textureFromSurface(renderer, std::move(h_bar));
233     }();
234 
235 	//Player and shadow specific statistics
236 	//The header.
237     {
238         SurfacePtr stats = createSurface(w, 44);
239         SDL_FillRect(stats.get(),NULL,-1);
240 
241         SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Total"),objThemes.getTextColor(true)));
242         applySurface(w-260-surface->w,stats->h-surface->h,surface.get(),stats.get(),NULL);
243         //FIXME: hard-coded player and shadow images
244         r.x=0;r.y=0;r.w=23;r.h=40;
245         applySurface(w-140-r.w,stats.get()->h-40,bmPlayer,stats.get(),&r);
246         applySurface(w-20-r.w,stats.get()->h-40,bmShadow,stats.get(),&r);
247 
248         list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
249     }
250 
251 	//Each items.
252     {
253         DRAW_PLAYER_STATISTICS(_("Traveling distance (m)"),TravelingDistance,"%0.1f");
254         DRAW_PLAYER_STATISTICS(_("Jump times"),Jumps,"%d");
255         DRAW_PLAYER_STATISTICS(_("Die times"),Dies,"%d");
256         DRAW_PLAYER_STATISTICS(_("Squashed times"),Squashed,"%d");
257     }
258 
259 	//Game specific statistics.
260     list->addItem(renderer, "",h_bar);
261 
262     auto drawMiscStats = [&](const char* name1,const int var1,const char* format1) {
263         drawMiscStatistics1(renderer, w, list, name1, var1, format1);
264     };
265 
266     drawMiscStats(_("Recordings:"),statsMgr.recordTimes,"%d");
267     drawMiscStats(_("Switch pulled times:"),statsMgr.switchTimes,"%d");
268     drawMiscStats(_("Swap times:"),statsMgr.swapTimes,"%d");
269     drawMiscStats(_("Save times:"),statsMgr.saveTimes,"%d");
270     drawMiscStats(_("Load times:"),statsMgr.loadTimes,"%d");
271 
272 	//Level specific statistics
273     list->addItem(renderer, "",h_bar);
274     {
275         SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Completed levels:"),objThemes.getTextColor(true)));
276         SurfacePtr stats = createSurface(w, surface->h);
277         SDL_FillRect(stats.get(),NULL,-1);
278 
279         applySurface(4,0,surface.get(),stats.get(),NULL);
280         x=surface->w+8;
281         y=surface->h;
282 
283         surface.reset(TTF_RenderUTF8_Blended(fontText,
284 			tfm::format("%d", statsMgr.completedLevels).c_str(),
285 			objThemes.getTextColor(true)));
286 
287         applySurface(x,(y-surface->h),surface.get(),stats.get(),NULL);
288 
289 		surface.reset(TTF_RenderUTF8_Blended(fontText,
290 			tfm::format("%d", statsMgr.completedLevels - statsMgr.goldLevels - statsMgr.silverLevels).c_str(),
291 			objThemes.getTextColor(true)));
292         applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
293         r.x=0;r.y=0;r.w=30;r.h=30;
294         applySurface(w-260-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
295 
296 		surface.reset(TTF_RenderUTF8_Blended(fontText,
297 			tfm::format("%d", statsMgr.silverLevels).c_str(),
298 			objThemes.getTextColor(true)));
299         applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
300         r.x+=30;
301         applySurface(w-140-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
302 
303 		surface.reset(TTF_RenderUTF8_Blended(fontText,
304 			tfm::format("%d", statsMgr.goldLevels).c_str(),
305 			objThemes.getTextColor(true)));
306         applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
307         r.x+=30;
308         applySurface(w-20-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
309 
310         list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats)));
311     }
312 
313 	//Other statistics.
314     list->addItem(renderer, "",h_bar);
315 
316     drawMiscStatistics1(renderer,w,list,_("In-game time:"),
317 		tfm::format("%02d:%02d:%02d", statsMgr.playTime / 3600, (statsMgr.playTime / 60) % 60, statsMgr.playTime % 60),
318 		"%s");
319     drawMiscStatistics1(renderer,w,list,_("Level editing time:"),
320 		tfm::format("%02d:%02d:%02d", statsMgr.levelEditTime / 3600, (statsMgr.levelEditTime / 60) % 60, statsMgr.levelEditTime % 60),
321 		"%s");
322 
323     drawMiscStats(_("Created levels:"),statsMgr.createdLevels,"%d");
324 }
325 
326 //In this method all the key and mouse events should be handled.
327 //NOTE: The GUIEvents won't be handled here.
handleEvents(ImageManager & imageManager,SDL_Renderer & renderer)328 void StatisticsScreen::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
329 	//Check if we need to quit, if so enter the exit state.
330 	if(event.type==SDL_QUIT){
331 		setNextState(STATE_EXIT);
332 	}
333 
334 	//Check horizontal movement
335 	int value = listBox->value;
336 	if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
337 		isKeyboardOnly = true;
338 		value++;
339 		if (value >= (int)listBox->item.size()) value = 0;
340 	} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
341 		isKeyboardOnly = true;
342 		value--;
343 		if (value < 0) value = listBox->item.size() - 1;
344 	}
345 	listBox->value = value;
346 
347 	//Check vertical movement
348 	if (value >= 0 && value < (int)lists.size()) {
349 		if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
350 			isKeyboardOnly = true;
351 			lists[value]->scrollScrollbar(-1);
352 		} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
353 			isKeyboardOnly = true;
354 			lists[value]->scrollScrollbar(1);
355 		}
356 	}
357 
358 	//Yet another cheat "ls -la" which reveals all unknown achievements
359 	static char input[6];
360 	static int inputLen = 0;
361 	if (value == 0) {
362 		if (event.type == SDL_KEYDOWN) {
363 			if (event.key.keysym.sym >= 32 && event.key.keysym.sym <= 126) {
364 				if (inputLen < sizeof(input)) input[inputLen] = event.key.keysym.sym;
365 				inputLen++;
366 			} else {
367 				if (event.key.keysym.sym == SDLK_RETURN && inputLen == 6 &&
368 					input[0] == 'l' && input[1] == 's' && input[2] == ' ' && input[3] == '-' && input[4] == 'l' && input[5] == 'a')
369 				{
370 					if (easterEggScreen(imageManager, renderer)) {
371 						//new achievement
372 						statsMgr.newAchievement("cheat");
373 
374 						//reload achievement list with hidden achievements revealed
375 						lists[0]->clearItems();
376 						addAchievements(imageManager, renderer, lists[0], true);
377 					}
378 				}
379 				inputLen = 0;
380 			}
381 		}
382 	} else {
383 		inputLen = 0;
384 	}
385 
386 	//Check if the escape button is pressed, if so go back to the main menu.
387 	if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
388 		setNextState(STATE_MENU);
389 	}
390 }
391 
392 //All the logic that needs to be done should go in this method.
logic(ImageManager &,SDL_Renderer &)393 void StatisticsScreen::logic(ImageManager&, SDL_Renderer&){
394 }
395 
396 //This method handles all the rendering.
render(ImageManager &,SDL_Renderer & renderer)397 void StatisticsScreen::render(ImageManager&, SDL_Renderer& renderer){
398 	//Draw background.
399     objThemes.getBackground(true)->draw(renderer);
400 	objThemes.getBackground(true)->updateAnimation();
401 
402 	//Draw title.
403     drawTitleTexture(SCREEN_WIDTH, *title, renderer);
404 
405 	//Draw statistics.
406 	int value=listBox->value;
407 	for(unsigned int i=0;i<lists.size();i++){
408 		lists[i]->visible=(i==value);
409 	}
410 }
411 
412 //Method that will be called when the screen size has been changed in runtime.
resize(ImageManager & imageManager,SDL_Renderer & renderer)413 void StatisticsScreen::resize(ImageManager &imageManager, SDL_Renderer &renderer){
414 	//Recreate the gui to fit the new resolution.
415     createGUI(imageManager, renderer);
416 }
417