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