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