1 /*
2 Copyright (c) 2013-2017, 2020 Cong Xu
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are met:
7
8 Redistributions of source code must retain the above copyright notice, this
9 list of conditions and the following disclaimer.
10 Redistributions in binary form must reproduce the above copyright notice,
11 this list of conditions and the following disclaimer in the documentation
12 and/or other materials provided with the distribution.
13
14 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 POSSIBILITY OF SUCH DAMAGE.
25 */
26 #include "screens_end.h"
27
28 #include <cdogs/events.h>
29 #include <cdogs/font.h>
30 #include <cdogs/grafx_bg.h>
31
32 #include "hiscores.h"
33 #include "menu_utils.h"
34
35 // All ending screens will be drawn in a table/list format, with each player
36 // on a row. This is to support any number of players, since there could be
37 // many players especially from network multiplayer.
38
39 #define PLAYER_LIST_ROW_HEIGHT 16
40 typedef struct
41 {
42 MenuSystem ms;
43 struct vec2i pos;
44 struct vec2i size;
45 int scroll;
46 GameLoopResult (*updateFunc)(GameLoopData *, LoopRunner *);
47 void (*drawFunc)(void *);
48 void *data;
49 // Whether to use a confirmation "Finish" menu at the end
50 // Useful for final screens where we want to view the scores without
51 // accidentally quitting
52 bool hasMenu;
53 bool showWinners;
54 bool showLastMan;
55 // Store player UIDs so we can display the list in a certain order
56 CArray playerUIDs; // of int
57 } PlayerList;
58 static int ComparePlayerScores(const void *v1, const void *v2);
PlayerListNew(GameLoopResult (* updateFunc)(GameLoopData *,LoopRunner *),void (* drawFunc)(void *),void * data,const bool hasMenu,const bool showWinners)59 static PlayerList *PlayerListNew(
60 GameLoopResult (*updateFunc)(GameLoopData *, LoopRunner *),
61 void (*drawFunc)(void *), void *data, const bool hasMenu,
62 const bool showWinners)
63 {
64 PlayerList *pl;
65 CMALLOC(pl, sizeof *pl);
66 pl->pos = svec2i_zero();
67 pl->size = gGraphicsDevice.cachedConfig.Res;
68 pl->scroll = 0;
69 pl->updateFunc = updateFunc;
70 pl->drawFunc = drawFunc;
71 pl->data = data;
72 pl->hasMenu = hasMenu;
73 pl->showWinners = showWinners;
74 CArrayInit(&pl->playerUIDs, sizeof(int));
75 // Collect all players, then order by score descending
76 int playersAlive = 0;
77 CA_FOREACH(const PlayerData, p, gPlayerDatas)
78 CArrayPushBack(&pl->playerUIDs, &p->UID);
79 if (p->Lives > 0)
80 {
81 playersAlive++;
82 }
83 CA_FOREACH_END()
84 qsort(
85 pl->playerUIDs.data, pl->playerUIDs.size, pl->playerUIDs.elemSize,
86 ComparePlayerScores);
87 pl->showLastMan = playersAlive == 1;
88 return pl;
89 }
GetModeScore(const PlayerData * p)90 static int GetModeScore(const PlayerData *p)
91 {
92 // For deathmatch, we count kills instead of score
93 if (gCampaign.Entry.Mode == GAME_MODE_DEATHMATCH)
94 {
95 return p->Totals.Kills;
96 }
97 return p->Totals.Score;
98 }
ComparePlayerScores(const void * v1,const void * v2)99 static int ComparePlayerScores(const void *v1, const void *v2)
100 {
101 const PlayerData *p1 = PlayerDataGetByUID(*(const int *)v1);
102 const PlayerData *p2 = PlayerDataGetByUID(*(const int *)v2);
103 int p1s = GetModeScore(p1);
104 int p2s = GetModeScore(p2);
105 if (p1s > p2s)
106 {
107 return -1;
108 }
109 else if (p1s < p2s)
110 {
111 return 1;
112 }
113 return 0;
114 }
115 static void PlayerListCustomDraw(
116 const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
117 const struct vec2i size, const void *data);
118 static int PlayerListInput(int cmd, void *data);
119 static void PlayerListTerminate(GameLoopData *data);
120 static void PlayerListOnEnter(GameLoopData *data);
121 static void PlayerListOnExit(GameLoopData *data);
122 static void PlayerListDraw(GameLoopData *data);
PlayerListLoop(PlayerList * pl)123 static GameLoopData *PlayerListLoop(PlayerList *pl)
124 {
125 MenuSystemInit(
126 &pl->ms, &gEventHandlers, &gGraphicsDevice, pl->pos, pl->size);
127 menu_t *menuScores = MenuCreateCustom(
128 "View Scores", PlayerListCustomDraw, PlayerListInput, pl);
129 if (pl->hasMenu)
130 {
131 pl->ms.root = MenuCreateNormal("", "", MENU_TYPE_NORMAL, 0);
132 MenuAddSubmenu(pl->ms.root, menuScores);
133 MenuAddSubmenu(pl->ms.root, MenuCreateReturn("Finish", 0));
134 }
135 else
136 {
137 pl->ms.root = menuScores;
138 }
139 pl->ms.allowAborts = true;
140 MenuAddExitType(&pl->ms, MENU_TYPE_RETURN);
141 return GameLoopDataNew(
142 pl, PlayerListTerminate, PlayerListOnEnter, PlayerListOnExit, NULL,
143 pl->updateFunc, PlayerListDraw);
144 }
PlayerListTerminate(GameLoopData * data)145 static void PlayerListTerminate(GameLoopData *data)
146 {
147 PlayerList *pl = data->Data;
148
149 CArrayTerminate(&pl->playerUIDs);
150 MenuSystemTerminate(&pl->ms);
151 CFREE(pl->data);
152 CFREE(pl);
153 }
PlayerListOnEnter(GameLoopData * data)154 static void PlayerListOnEnter(GameLoopData *data)
155 {
156 PlayerList *pl = data->Data;
157
158 if (pl->hasMenu)
159 {
160 pl->ms.current = MenuGetSubmenuByName(pl->ms.root, "View Scores");
161 }
162 else
163 {
164 pl->ms.current = pl->ms.root;
165 }
166 }
PlayerListOnExit(GameLoopData * data)167 static void PlayerListOnExit(GameLoopData *data)
168 {
169 UNUSED(data);
170 MenuPlaySound(MENU_SOUND_SWITCH);
171 }
PlayerListUpdate(GameLoopData * data,LoopRunner * l)172 static GameLoopResult PlayerListUpdate(GameLoopData *data, LoopRunner *l)
173 {
174 PlayerList *pl = data->Data;
175
176 const GameLoopResult result = MenuUpdate(&pl->ms);
177 if (result == UPDATE_RESULT_OK)
178 {
179 LoopRunnerChange(l, HighScoresScreen(&gCampaign, &gGraphicsDevice));
180 }
181 return result;
182 }
PlayerListDraw(GameLoopData * data)183 static void PlayerListDraw(GameLoopData *data)
184 {
185 const PlayerList *pl = data->Data;
186
187 MenuDraw(&pl->ms);
188 }
189 static int PlayerListMaxScroll(const PlayerList *pl);
190 static int PlayerListMaxRows(const PlayerList *pl);
PlayerListCustomDraw(const menu_t * menu,GraphicsDevice * g,const struct vec2i pos,const struct vec2i size,const void * data)191 static void PlayerListCustomDraw(
192 const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
193 const struct vec2i size, const void *data)
194 {
195 UNUSED(menu);
196 UNUSED(g);
197 // Draw players starting from the index
198 // TODO: custom columns
199 const PlayerList *pl = data;
200
201 // First draw the headers
202 const int xStart = pos.x + 80 + (size.x - 320) / 2;
203 int x = xStart;
204 int y = pos.y;
205 FontStrMask("Player", svec2i(x, y), colorPurple);
206 x += 100;
207 FontStrMask("Score", svec2i(x, y), colorPurple);
208 x += 32;
209 FontStrMask("Kills", svec2i(x, y), colorPurple);
210 y += FontH() * 2 + PLAYER_LIST_ROW_HEIGHT + 4;
211 // Then draw the player list
212 int maxScore = -1;
213 for (int i = pl->scroll;
214 i < MIN((int)pl->playerUIDs.size, pl->scroll + PlayerListMaxRows(pl));
215 i++)
216 {
217 const int *playerUID = CArrayGet(&pl->playerUIDs, i);
218 PlayerData *p = PlayerDataGetByUID(*playerUID);
219 if (p == NULL)
220 {
221 continue;
222 }
223 if (maxScore < GetModeScore(p))
224 {
225 maxScore = GetModeScore(p);
226 }
227
228 x = xStart;
229 // Highlight local players using different coloured text
230 const color_t textColor = p->IsLocal ? colorPurple : colorWhite;
231
232 // Draw the players offset on alternate rows
233 DisplayCharacterAndName(
234 svec2i(x + (i & 1) * 16, y + 4), &p->Char, DIRECTION_DOWN, p->name,
235 textColor, p->guns[0]);
236
237 // Draw score
238 x += 100;
239 char buf[256];
240 sprintf(buf, "%d", p->Totals.Score);
241 FontStrMask(buf, svec2i(x, y), textColor);
242
243 // Draw kills
244 x += 32;
245 sprintf(buf, "%d", p->Totals.Kills);
246 FontStrMask(buf, svec2i(x, y), textColor);
247
248 // Draw winner/award text
249 x += 32;
250 if (pl->showWinners && GetModeScore(p) == maxScore)
251 {
252 FontStrMask("Winner!", svec2i(x, y), colorGreen);
253 }
254 else if (
255 pl->showLastMan && p->Lives > 0 &&
256 gCampaign.Entry.Mode == GAME_MODE_DEATHMATCH)
257 {
258 // Only show last man standing on deathmatch mode
259 FontStrMask("Last man standing!", svec2i(x, y), colorGreen);
260 }
261
262 y += PLAYER_LIST_ROW_HEIGHT;
263 }
264
265 // Draw indicator arrows if there's enough to scroll
266 if (pl->scroll > 0)
267 {
268 FontStr(
269 "^", svec2i(CENTER_X(pos, size, FontStrW("^")), pos.y + FontH()));
270 }
271 if (pl->scroll < PlayerListMaxScroll(pl))
272 {
273 FontStr(
274 "v",
275 svec2i(
276 CENTER_X(pos, size, FontStrW("v")), pos.y + size.y - FontH()));
277 }
278
279 // Finally draw any custom stuff
280 if (pl->drawFunc)
281 {
282 pl->drawFunc(pl->data);
283 }
284 }
PlayerListInput(int cmd,void * data)285 static int PlayerListInput(int cmd, void *data)
286 {
287 // Input: up/down scrolls list
288 // CMD 1/2: exit
289 PlayerList *pl = data;
290
291 // Note: players can leave due to network disconnection
292 // Update our lists
293 CA_FOREACH(const int, playerUID, pl->playerUIDs)
294 const PlayerData *p = PlayerDataGetByUID(*playerUID);
295 if (p == NULL)
296 {
297 CArrayDelete(&pl->playerUIDs, _ca_index);
298 _ca_index--;
299 }
300 CA_FOREACH_END()
301
302 if (cmd == CMD_DOWN)
303 {
304 MenuPlaySound(MENU_SOUND_SWITCH);
305 pl->scroll++;
306 }
307 else if (cmd == CMD_UP)
308 {
309 MenuPlaySound(MENU_SOUND_SWITCH);
310 pl->scroll--;
311 }
312 else if (AnyButton(cmd))
313 {
314 MenuPlaySound(MENU_SOUND_BACK);
315 return 1;
316 }
317 // Scroll wrap-around
318 pl->scroll = CLAMP_OPPOSITE(pl->scroll, 0, PlayerListMaxScroll(pl));
319 return 0;
320 }
PlayerListMaxScroll(const PlayerList * pl)321 static int PlayerListMaxScroll(const PlayerList *pl)
322 {
323 return MAX((int)pl->playerUIDs.size - PlayerListMaxRows(pl), 0);
324 }
PlayerListMaxRows(const PlayerList * pl)325 static int PlayerListMaxRows(const PlayerList *pl)
326 {
327 return (pl->size.y - FontH() * 3) / PLAYER_LIST_ROW_HEIGHT - 2;
328 }
329
330 typedef struct
331 {
332 const Campaign *Campaign;
333 const char *FinalWords;
334 } VictoryData;
335 static void VictoryDraw(void *data);
ScreenVictory(Campaign * c)336 GameLoopData *ScreenVictory(Campaign *c)
337 {
338 SoundPlay(&gSoundDevice, StrSound("victory"));
339 VictoryData *data;
340 CMALLOC(data, sizeof *data);
341 data->Campaign = c;
342 const char *finalWordsSingle[] = {
343 "Ha, next time I'll use my good hand",
344 "Over already? I was just warming up...",
345 "There's just no good opposition to be found these days!",
346 "Well, maybe I'll just do my monthly reload then",
347 "Woof woof",
348 "I'll just bury the bones in the back yard, he-he",
349 "I just wish they'd let me try bare-handed",
350 "Rambo? Who's Rambo?",
351 "<in Austrian accent:> I'll be back",
352 "Gee, my trigger finger is sore",
353 "I need more practice. I think I missed a few shots at times"};
354 const char *finalWordsMulti[] = {
355 "United we stand, divided we conquer",
356 "Nothing like good teamwork, is there?",
357 "Which way is the camera?",
358 "We eat bullets for breakfast and have grenades as dessert",
359 "We're so cool we have to wear mittens",
360 };
361 if (GetNumPlayers(PLAYER_ANY, false, true) == 1)
362 {
363 const int numWords = sizeof finalWordsSingle / sizeof(char *);
364 data->FinalWords = finalWordsSingle[rand() % numWords];
365 }
366 else
367 {
368 const int numWords = sizeof finalWordsMulti / sizeof(char *);
369 data->FinalWords = finalWordsMulti[rand() % numWords];
370 }
371 PlayerList *pl =
372 PlayerListNew(PlayerListUpdate, VictoryDraw, data, true, false);
373 pl->pos.y = 75;
374 pl->size.y -= pl->pos.y;
375 MusicPlayFromChunk(
376 &gSoundDevice.music, MUSIC_VICTORY,
377 &gCampaign.Setting.CustomSongs[MUSIC_VICTORY]);
378 return PlayerListLoop(pl);
379 }
VictoryDraw(void * data)380 static void VictoryDraw(void *data)
381 {
382 const VictoryData *vd = data;
383
384 const int w = gGraphicsDevice.cachedConfig.Res.x;
385 FontOpts opts = FontOptsNew();
386 opts.HAlign = ALIGN_CENTER;
387 opts.Area = gGraphicsDevice.cachedConfig.Res;
388 int y = 30;
389
390 // Congratulations text
391 #define CONGRATULATIONS "Congratulations, you have completed "
392 FontStrOpt(CONGRATULATIONS, svec2i(0, y), opts);
393 y += 15;
394 opts.Mask = colorRed;
395 FontStrOpt(vd->Campaign->Setting.Title, svec2i(0, y), opts);
396 y += 15;
397
398 // Final words
399 struct vec2i pos = svec2i((w - FontStrW(vd->FinalWords)) / 2, y);
400 pos = FontChMask('"', pos, colorDarker);
401 pos = FontStrMask(vd->FinalWords, pos, colorPurple);
402 FontChMask('"', pos, colorDarker);
403 }
404
405 static GameLoopResult DogfightScoresUpdate(GameLoopData *data, LoopRunner *l);
ScreenDogfightScores(void)406 GameLoopData *ScreenDogfightScores(void)
407 {
408 // Calculate PVP rounds won
409 int maxScore = 0;
410 CA_FOREACH(PlayerData, p, gPlayerDatas)
411 if (IsPlayerAlive(p))
412 {
413 p->Totals.Score++;
414 maxScore = MAX(maxScore, p->Totals.Score);
415 }
416 CA_FOREACH_END()
417 gCampaign.IsComplete = maxScore == ModeMaxRoundsWon(gCampaign.Entry.Mode);
418 CASSERT(
419 maxScore <= ModeMaxRoundsWon(gCampaign.Entry.Mode),
420 "score exceeds max rounds won");
421
422 PlayerList *pl =
423 PlayerListNew(DogfightScoresUpdate, NULL, NULL, false, false);
424 pl->pos.y = 24;
425 pl->size.y -= pl->pos.y;
426 return PlayerListLoop(pl);
427 }
DogfightScoresUpdate(GameLoopData * data,LoopRunner * l)428 static GameLoopResult DogfightScoresUpdate(GameLoopData *data, LoopRunner *l)
429 {
430 PlayerList *pl = data->Data;
431
432 const GameLoopResult result = MenuUpdate(&pl->ms);
433 if (result == UPDATE_RESULT_OK)
434 {
435 if (gCampaign.IsComplete)
436 {
437 LoopRunnerChange(l, ScreenDogfightFinalScores());
438 }
439 else
440 {
441 LoopRunnerChange(
442 l, HighScoresScreen(&gCampaign, &gGraphicsDevice));
443 }
444 }
445 return result;
446 }
447
ScreenDogfightFinalScores(void)448 GameLoopData *ScreenDogfightFinalScores(void)
449 {
450 SoundPlay(&gSoundDevice, StrSound("victory"));
451 PlayerList *pl = PlayerListNew(PlayerListUpdate, NULL, NULL, true, true);
452 pl->pos.y = 24;
453 pl->size.y -= pl->pos.y;
454 return PlayerListLoop(pl);
455 }
456
ScreenDeathmatchFinalScores(void)457 GameLoopData *ScreenDeathmatchFinalScores(void)
458 {
459 SoundPlay(&gSoundDevice, StrSound("victory"));
460 PlayerList *pl = PlayerListNew(PlayerListUpdate, NULL, NULL, true, true);
461 pl->pos.y = 24;
462 pl->size.y -= pl->pos.y;
463 return PlayerListLoop(pl);
464 }
465