1 /*
2 	Copyright (c) 2013-2021 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 "briefing_screens.h"
27 
28 #include <cdogs/draw/draw.h>
29 #include <cdogs/draw/draw_actor.h>
30 #include <cdogs/events.h>
31 #include <cdogs/files.h>
32 #include <cdogs/font.h>
33 #include <cdogs/grafx_bg.h>
34 #include <cdogs/music.h>
35 #include <cdogs/objective.h>
36 
37 #include "animated_counter.h"
38 #include "autosave.h"
39 #include "hiscores.h"
40 #include "menu_utils.h"
41 #include "password.h"
42 #include "prep.h"
43 #include "prep_equip.h"
44 #include "screens_end.h"
45 
46 static void DrawObjectiveInfo(const Objective *o, const struct vec2i pos);
47 
48 typedef struct
49 {
50 	EventWaitResult waitResult;
51 	CampaignSetting *c;
52 } ScreenCampaignIntroData;
53 static void CampaignIntroTerminate(GameLoopData *data);
54 static void CampaignIntroOnEnter(GameLoopData *data);
55 static void CampaignIntroOnExit(GameLoopData *data);
56 static void CampaignIntroInput(GameLoopData *data);
57 static GameLoopResult CampaignIntroUpdate(GameLoopData *data, LoopRunner *l);
58 static void CampaignIntroDraw(GameLoopData *data);
ScreenCampaignIntro(CampaignSetting * c)59 GameLoopData *ScreenCampaignIntro(CampaignSetting *c)
60 {
61 	ScreenCampaignIntroData *data;
62 	CMALLOC(data, sizeof *data);
63 	data->c = c;
64 	return GameLoopDataNew(
65 		data, CampaignIntroTerminate, CampaignIntroOnEnter,
66 		CampaignIntroOnExit, CampaignIntroInput, CampaignIntroUpdate,
67 		CampaignIntroDraw);
68 }
CampaignIntroTerminate(GameLoopData * data)69 static void CampaignIntroTerminate(GameLoopData *data)
70 {
71 	ScreenCampaignIntroData *mData = data->Data;
72 	CFREE(mData);
73 }
CampaignIntroOnEnter(GameLoopData * data)74 static void CampaignIntroOnEnter(GameLoopData *data)
75 {
76 	ScreenCampaignIntroData *mData = data->Data;
77 	MusicPlayFromChunk(
78 		&gSoundDevice.music, MUSIC_MENU, &mData->c->CustomSongs[MUSIC_MENU]);
79 }
CampaignIntroOnExit(GameLoopData * data)80 static void CampaignIntroOnExit(GameLoopData *data)
81 {
82 	const ScreenCampaignIntroData *sData = data->Data;
83 	if (sData->waitResult != EVENT_WAIT_CANCEL)
84 	{
85 		MenuPlaySound(MENU_SOUND_ENTER);
86 	}
87 	else
88 	{
89 		CampaignUnload(&gCampaign);
90 	}
91 }
CampaignIntroInput(GameLoopData * data)92 static void CampaignIntroInput(GameLoopData *data)
93 {
94 	ScreenCampaignIntroData *sData = data->Data;
95 	sData->waitResult = EventWaitForAnyKeyOrButton();
96 }
CampaignIntroUpdate(GameLoopData * data,LoopRunner * l)97 static GameLoopResult CampaignIntroUpdate(GameLoopData *data, LoopRunner *l)
98 {
99 	const ScreenCampaignIntroData *sData = data->Data;
100 
101 	if (!IsIntroNeeded(gCampaign.Entry.Mode) ||
102 		sData->waitResult == EVENT_WAIT_OK)
103 	{
104 		// Switch to num players selection
105 		LoopRunnerChange(
106 			l, NumPlayersSelection(&gGraphicsDevice, &gEventHandlers));
107 	}
108 	else if (sData->waitResult == EVENT_WAIT_CANCEL)
109 	{
110 		LoopRunnerPop(l);
111 	}
112 	return UPDATE_RESULT_OK;
113 }
CampaignIntroDraw(GameLoopData * data)114 static void CampaignIntroDraw(GameLoopData *data)
115 {
116 	const ScreenCampaignIntroData *sData = data->Data;
117 
118 	BlitClearBuf(&gGraphicsDevice);
119 	const int w = gGraphicsDevice.cachedConfig.Res.x;
120 	const int h = gGraphicsDevice.cachedConfig.Res.y;
121 	const int y = h / 4;
122 
123 	// Display title + author
124 	char *buf;
125 	CMALLOC(buf, strlen(sData->c->Title) + strlen(sData->c->Author) + 16);
126 	sprintf(buf, "%s by %s", sData->c->Title, sData->c->Author);
127 	FontOpts opts = FontOptsNew();
128 	opts.HAlign = ALIGN_CENTER;
129 	opts.Area = gGraphicsDevice.cachedConfig.Res;
130 	opts.Pad.y = y - 25;
131 	FontStrOpt(buf, svec2i_zero(), opts);
132 	CFREE(buf);
133 
134 	// Display campaign description
135 	// allow some slack for newlines
136 	if (strlen(sData->c->Description) > 0)
137 	{
138 		CMALLOC(buf, strlen(sData->c->Description) * 2);
139 		// Pad about 1/6th of the screen width total (1/12th left and right)
140 		FontSplitLines(sData->c->Description, buf, w * 5 / 6);
141 		FontStr(buf, svec2i(w / 12, y));
142 		CFREE(buf);
143 	}
144 
145 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.screen);
146 }
147 
148 typedef struct
149 {
150 	char *Title;
151 	FontOpts TitleOpts;
152 	char Password[32];
153 	FontOpts PasswordOpts;
154 	int TypewriterCount;
155 	char *Description;
156 	char *TypewriterBuf;
157 	struct vec2i DescriptionPos;
158 	struct vec2i ObjectiveDescPos;
159 	struct vec2i ObjectiveInfoPos;
160 	int ObjectiveHeight;
161 	CampaignSetting *C;
162 	const struct MissionOptions *MissionOptions;
163 	EventWaitResult waitResult;
164 } MissionBriefingData;
165 static void MissionBriefingTerminate(GameLoopData *data);
166 static void MissionBriefingOnEnter(GameLoopData *data);
167 static void MissionBriefingOnExit(GameLoopData *data);
168 static void MissionBriefingInput(GameLoopData *data);
169 static GameLoopResult MissionBriefingUpdate(GameLoopData *data, LoopRunner *l);
170 static void MissionBriefingDraw(GameLoopData *data);
ScreenMissionBriefing(CampaignSetting * c,const struct MissionOptions * m)171 GameLoopData *ScreenMissionBriefing(
172 	CampaignSetting *c, const struct MissionOptions *m)
173 {
174 	const int w = gGraphicsDevice.cachedConfig.Res.x;
175 	const int h = gGraphicsDevice.cachedConfig.Res.y;
176 	const int y = h / 4;
177 	MissionBriefingData *mData;
178 	CCALLOC(mData, sizeof *mData);
179 	mData->waitResult = EVENT_WAIT_CONTINUE;
180 
181 	// Title
182 	if (m->missionData->Title)
183 	{
184 		CMALLOC(mData->Title, strlen(m->missionData->Title) + 32);
185 		sprintf(
186 			mData->Title, "Mission %d: %s", m->index + 1,
187 			m->missionData->Title);
188 		mData->TitleOpts = FontOptsNew();
189 		mData->TitleOpts.HAlign = ALIGN_CENTER;
190 		mData->TitleOpts.Area = gGraphicsDevice.cachedConfig.Res;
191 		mData->TitleOpts.Pad.y = y - 25;
192 	}
193 
194 	// Description
195 	if (m->missionData->Description)
196 	{
197 		// Split the description, and prepare it for typewriter effect
198 		// allow some slack for newlines
199 		CMALLOC(
200 			mData->Description, strlen(m->missionData->Description) * 2 + 1);
201 		CCALLOC(
202 			mData->TypewriterBuf, strlen(m->missionData->Description) * 2 + 1);
203 		// Pad about 1/6th of the screen width total (1/12th left and right)
204 		FontSplitLines(
205 			m->missionData->Description, mData->Description, w * 5 / 6);
206 		mData->DescriptionPos = svec2i(w / 12, y);
207 
208 		// Objectives
209 		mData->ObjectiveDescPos =
210 			svec2i(w / 6, y + FontStrH(mData->Description) + h / 10);
211 		mData->ObjectiveInfoPos =
212 			svec2i(w - (w / 6), mData->ObjectiveDescPos.y + FontH());
213 		mData->ObjectiveHeight = h / 12;
214 	}
215 	mData->C = c;
216 	mData->MissionOptions = m;
217 
218 	return GameLoopDataNew(
219 		mData, MissionBriefingTerminate, MissionBriefingOnEnter,
220 		MissionBriefingOnExit, MissionBriefingInput, MissionBriefingUpdate,
221 		MissionBriefingDraw);
222 }
MissionBriefingTerminate(GameLoopData * data)223 static void MissionBriefingTerminate(GameLoopData *data)
224 {
225 	MissionBriefingData *mData = data->Data;
226 
227 	CFREE(mData->Title);
228 	CFREE(mData->Description);
229 	CFREE(mData->TypewriterBuf);
230 	CFREE(mData);
231 }
MissionBriefingOnEnter(GameLoopData * data)232 static void MissionBriefingOnEnter(GameLoopData *data)
233 {
234 	MissionBriefingData *mData = data->Data;
235 	if (IsMissionBriefingNeeded(gCampaign.Entry.Mode, mData->Description))
236 	{
237 		MusicPlayFromChunk(
238 			&gSoundDevice.music, MUSIC_BRIEFING,
239 			&mData->C->CustomSongs[MUSIC_BRIEFING]);
240 	}
241 }
MissionBriefingOnExit(GameLoopData * data)242 static void MissionBriefingOnExit(GameLoopData *data)
243 {
244 	const MissionBriefingData *mData = data->Data;
245 
246 	if (mData->waitResult == EVENT_WAIT_OK)
247 	{
248 		MenuPlaySound(MENU_SOUND_ENTER);
249 	}
250 	else
251 	{
252 		CampaignUnload(&gCampaign);
253 	}
254 }
MissionBriefingInput(GameLoopData * data)255 static void MissionBriefingInput(GameLoopData *data)
256 {
257 	MissionBriefingData *mData = data->Data;
258 
259 	int cmds[MAX_LOCAL_PLAYERS];
260 	memset(cmds, 0, sizeof cmds);
261 	GetPlayerCmds(&gEventHandlers, &cmds);
262 	if (mData->Description)
263 	{
264 		// Check for player input; if any then skip to the end of the briefing
265 		for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
266 		{
267 			if (AnyButton(cmds[i]))
268 			{
269 				// If the typewriter is still going, skip to end
270 				if (mData->TypewriterCount <= (int)strlen(mData->Description))
271 				{
272 					strcpy(mData->TypewriterBuf, mData->Description);
273 					mData->TypewriterCount = (int)strlen(mData->Description);
274 					return;
275 				}
276 				// Otherwise, exit out of loop
277 				mData->waitResult = EVENT_WAIT_OK;
278 			}
279 		}
280 	}
281 	// Check if anyone pressed escape
282 	if (EventIsEscape(&gEventHandlers, cmds, GetMenuCmd(&gEventHandlers)))
283 	{
284 		mData->waitResult = EVENT_WAIT_CANCEL;
285 	}
286 }
MissionBriefingUpdate(GameLoopData * data,LoopRunner * l)287 static GameLoopResult MissionBriefingUpdate(GameLoopData *data, LoopRunner *l)
288 {
289 	MissionBriefingData *mData = data->Data;
290 
291 	if (!IsMissionBriefingNeeded(gCampaign.Entry.Mode, mData->Description))
292 	{
293 		mData->waitResult = EVENT_WAIT_OK;
294 		goto bail;
295 	}
296 
297 	// Check exit conditions from input
298 	if (mData->waitResult != EVENT_WAIT_CONTINUE)
299 	{
300 		goto bail;
301 	}
302 
303 	// Update the typewriter effect
304 	if (mData->TypewriterCount <= (int)strlen(mData->Description))
305 	{
306 		mData->TypewriterBuf[mData->TypewriterCount] =
307 			mData->Description[mData->TypewriterCount];
308 		mData->TypewriterCount++;
309 		return UPDATE_RESULT_DRAW;
310 	}
311 
312 	// Auto skip if on demo mode
313 	if (gEventHandlers.DemoQuitTimer > 0)
314 	{
315 		mData->waitResult = EVENT_WAIT_OK;
316 		goto bail;
317 	}
318 
319 	return UPDATE_RESULT_OK;
320 
321 bail:
322 	if (mData->waitResult == EVENT_WAIT_OK)
323 	{
324 		LoopRunnerChange(l, PlayerEquip());
325 	}
326 	else
327 	{
328 		LoopRunnerPop(l);
329 	}
330 	return UPDATE_RESULT_OK;
331 }
MissionBriefingDraw(GameLoopData * data)332 static void MissionBriefingDraw(GameLoopData *data)
333 {
334 	const MissionBriefingData *mData = data->Data;
335 
336 	BlitClearBuf(&gGraphicsDevice);
337 
338 	// Mission title
339 	FontStrOpt(mData->Title, svec2i_zero(), mData->TitleOpts);
340 	// Display password
341 	FontStrOpt(mData->Password, svec2i_zero(), mData->PasswordOpts);
342 	// Display description with typewriter effect
343 	FontStr(mData->TypewriterBuf, mData->DescriptionPos);
344 	// Display objectives
345 	CA_FOREACH(
346 		const Objective, o, mData->MissionOptions->missionData->Objectives)
347 	// Do not brief optional objectives
348 	if (o->Required == 0)
349 	{
350 		continue;
351 	}
352 	struct vec2i offset = svec2i(0, _ca_index * mData->ObjectiveHeight);
353 	FontStr(o->Description, svec2i_add(mData->ObjectiveDescPos, offset));
354 	// Draw the icons slightly offset so that tall icons don't overlap each
355 	// other
356 	offset.x = -16 * (_ca_index & 1);
357 	DrawObjectiveInfo(o, svec2i_add(mData->ObjectiveInfoPos, offset));
358 	CA_FOREACH_END()
359 
360 	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.screen);
361 }
362 
363 #define PERFECT_BONUS 500
364 
365 typedef struct
366 {
367 	const PlayerData *Pd;
368 	AnimatedCounter Score;
369 	AnimatedCounter Total;
370 	AnimatedCounter HealthResurrection;
371 	AnimatedCounter ButcherNinjaFriendly;
372 } PlayerSummaryDrawData;
373 typedef struct
374 {
375 	MenuSystem ms;
376 	const Campaign *c;
377 	struct MissionOptions *m;
378 	bool completed;
379 	AnimatedCounter AccessBonus;
380 	AnimatedCounter TimeBonus;
381 	PlayerSummaryDrawData pDatas[MAX_LOCAL_PLAYERS];
382 } MissionSummaryData;
383 static void MissionSummaryTerminate(GameLoopData *data);
384 static void MissionSummaryOnEnter(GameLoopData *data);
385 static GameLoopResult MissionSummaryUpdate(GameLoopData *data, LoopRunner *l);
386 static void MissionSummaryDraw(GameLoopData *data);
387 static void MissionSummaryMenuDraw(
388 	const menu_t *menu, GraphicsDevice *g, const struct vec2i p,
389 	const struct vec2i size, const void *data);
ScreenMissionSummary(const Campaign * c,struct MissionOptions * m,const bool completed)390 GameLoopData *ScreenMissionSummary(
391 	const Campaign *c, struct MissionOptions *m, const bool completed)
392 {
393 	MissionSummaryData *mData;
394 	CCALLOC(mData, sizeof *mData);
395 
396 	const int h = FontH() * 10;
397 	MenuSystemInit(
398 		&mData->ms, &gEventHandlers, &gGraphicsDevice,
399 		svec2i(0, gGraphicsDevice.cachedConfig.Res.y - h),
400 		svec2i(gGraphicsDevice.cachedConfig.Res.x, h));
401 	mData->ms.current = mData->ms.root =
402 		MenuCreateNormal("", "", MENU_TYPE_NORMAL, 0);
403 	// Use return code 0 for whether to continue the game
404 	if (completed)
405 	{
406 		MenuAddSubmenu(mData->ms.root, MenuCreateReturn("Continue", 0));
407 	}
408 	else
409 	{
410 		MenuAddSubmenu(mData->ms.root, MenuCreateReturn("Replay mission", 0));
411 		MenuAddSubmenu(mData->ms.root, MenuCreateReturn("Back to menu", 1));
412 	}
413 	mData->ms.allowAborts = true;
414 	MenuAddExitType(&mData->ms, MENU_TYPE_RETURN);
415 	MenuSystemAddCustomDisplay(&mData->ms, MissionSummaryMenuDraw, mData);
416 
417 	mData->c = c;
418 	mData->m = m;
419 	mData->completed = completed;
420 
421 	return GameLoopDataNew(
422 		mData, MissionSummaryTerminate, MissionSummaryOnEnter, NULL, NULL,
423 		MissionSummaryUpdate, MissionSummaryDraw);
424 }
MissionSummaryTerminate(GameLoopData * data)425 static void MissionSummaryTerminate(GameLoopData *data)
426 {
427 	MissionSummaryData *mData = data->Data;
428 
429 	MenuSystemTerminate(&mData->ms);
430 	AnimatedCounterTerminate(&mData->AccessBonus);
431 	AnimatedCounterTerminate(&mData->TimeBonus);
432 	for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
433 	{
434 		AnimatedCounterTerminate(&mData->pDatas[i].Score);
435 		AnimatedCounterTerminate(&mData->pDatas[i].Total);
436 		AnimatedCounterTerminate(&mData->pDatas[i].HealthResurrection);
437 		AnimatedCounterTerminate(&mData->pDatas[i].ButcherNinjaFriendly);
438 	}
439 	CFREE(mData);
440 }
441 static bool AreAnySurvived(void);
442 static int GetAccessBonus(const struct MissionOptions *m);
443 static int GetTimeBonus(const struct MissionOptions *m, int *secondsOut);
444 static void ApplyBonuses(PlayerData *p, const int bonus);
445 static int GetHealthBonus(const PlayerData *p);
446 static int GetResurrectionFee(const PlayerData *p);
447 static int GetButcherPenalty(const PlayerData *p);
448 static int GetNinjaBonus(const PlayerData *p);
449 static int GetFriendlyBonus(const PlayerData *p);
MissionSummaryOnEnter(GameLoopData * data)450 static void MissionSummaryOnEnter(GameLoopData *data)
451 {
452 	MissionSummaryData *mData = data->Data;
453 
454 	if (mData->completed && CanLevelSelect(mData->c->Entry.Mode))
455 	{
456 		AutosaveAdd(
457 			&gAutosave, &mData->c->Entry, mData->m->index,
458 			mData->m->NextMission, &gPlayerDatas);
459 		AutosaveSave(&gAutosave, GetConfigFilePath(AUTOSAVE_FILE));
460 	}
461 
462 	// Calculate bonus scores
463 	// Bonuses only apply if at least one player has lived
464 	const int accessBonus = GetAccessBonus(mData->m);
465 	if (AreAnySurvived())
466 	{
467 		int bonus = 0;
468 		// Objective bonuses
469 		CA_FOREACH(const Objective, o, mData->m->missionData->Objectives)
470 		if (ObjectiveIsPerfect(o))
471 		{
472 			bonus += PERFECT_BONUS;
473 		}
474 		CA_FOREACH_END()
475 		bonus += accessBonus;
476 		bonus += GetTimeBonus(mData->m, NULL);
477 
478 		CA_FOREACH(PlayerData, p, gPlayerDatas)
479 		ApplyBonuses(p, bonus);
480 		CA_FOREACH_END()
481 	}
482 
483 	// Skip menu
484 	if (mData->m->missionData->SkipDebrief)
485 	{
486 		return;
487 	}
488 
489 	if (mData->completed)
490 	{
491 		MusicPlayFromChunk(
492 			&gSoundDevice.music, MUSIC_END,
493 			&gCampaign.Setting.CustomSongs[MUSIC_END]);
494 	}
495 	else
496 	{
497 		MusicPlayFromChunk(
498 			&gSoundDevice.music, MUSIC_LOSE,
499 			&gCampaign.Setting.CustomSongs[MUSIC_LOSE]);
500 	}
501 
502 	// Init mission bonuses
503 	if (AreAnySurvived())
504 	{
505 		if (accessBonus > 0)
506 		{
507 			mData->AccessBonus =
508 				AnimatedCounterNew("Access bonus: ", accessBonus);
509 		}
510 		int seconds;
511 		const int timeBonus = GetTimeBonus(mData->m, &seconds);
512 		char buf[256];
513 		sprintf(buf, "Time bonus: %d secs x 25 = ", seconds);
514 		mData->TimeBonus = AnimatedCounterNew(buf, timeBonus);
515 	}
516 
517 	// Init per-player summaries
518 	int idx = 0;
519 	CA_FOREACH(PlayerData, pd, gPlayerDatas)
520 	if (!pd->IsLocal)
521 	{
522 		continue;
523 	}
524 	mData->pDatas[idx].Pd = pd;
525 	mData->pDatas[idx].Score = AnimatedCounterNew("Score: ", pd->Stats.Score);
526 	mData->pDatas[idx].Total = AnimatedCounterNew("Total: ", pd->Totals.Score);
527 	if (pd->survived)
528 	{
529 		const int healthBonus = GetHealthBonus(pd);
530 		if (healthBonus != 0)
531 		{
532 			mData->pDatas[idx].HealthResurrection =
533 				AnimatedCounterNew("Health bonus: ", healthBonus);
534 		}
535 		const int resurrectionFee = GetResurrectionFee(pd);
536 		if (resurrectionFee != 0)
537 		{
538 			mData->pDatas[idx].HealthResurrection =
539 				AnimatedCounterNew("Resurrection fee: ", resurrectionFee);
540 		}
541 
542 		const int butcherPenalty = GetButcherPenalty(pd);
543 		if (butcherPenalty != 0)
544 		{
545 			mData->pDatas[idx].ButcherNinjaFriendly =
546 				AnimatedCounterNew("Butcher penalty: ", butcherPenalty);
547 		}
548 		const int ninjaBonus = GetNinjaBonus(pd);
549 		if (ninjaBonus != 0)
550 		{
551 			mData->pDatas[idx].ButcherNinjaFriendly =
552 				AnimatedCounterNew("Ninja bonus: ", ninjaBonus);
553 		}
554 		const int friendlyBonus = GetFriendlyBonus(pd);
555 		if (friendlyBonus != 0)
556 		{
557 			mData->pDatas[idx].ButcherNinjaFriendly =
558 				AnimatedCounterNew("Friendly bonus: ", friendlyBonus);
559 		}
560 	}
561 	idx++;
562 	CA_FOREACH_END()
563 }
MissionSummaryUpdate(GameLoopData * data,LoopRunner * l)564 static GameLoopResult MissionSummaryUpdate(GameLoopData *data, LoopRunner *l)
565 {
566 	MissionSummaryData *mData = data->Data;
567 
568 	GameLoopResult result = MenuUpdate(&mData->ms);
569 	if (result == UPDATE_RESULT_DRAW)
570 	{
571 		bool done = true;
572 		done = AnimatedCounterUpdate(&mData->AccessBonus, 1) && done;
573 		done = AnimatedCounterUpdate(&mData->TimeBonus, 1) && done;
574 		for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
575 		{
576 			done = AnimatedCounterUpdate(&mData->pDatas[i].Score, 1) && done;
577 			done = AnimatedCounterUpdate(&mData->pDatas[i].Total, 1) && done;
578 			done = AnimatedCounterUpdate(
579 					   &mData->pDatas[i].HealthResurrection, 1) &&
580 				   done;
581 			done = AnimatedCounterUpdate(
582 					   &mData->pDatas[i].ButcherNinjaFriendly, 1) &&
583 				   done;
584 		}
585 
586 		// Skip after animations are done if in demo mode
587 		if (done && gEventHandlers.DemoQuitTimer > 0)
588 		{
589 			result = UPDATE_RESULT_OK;
590 		}
591 	}
592 
593 	if (result == UPDATE_RESULT_OK || mData->m->missionData->SkipDebrief)
594 	{
595 		gCampaign.IsComplete =
596 			mData->completed &&
597 			mData->m->NextMission == (int)gCampaign.Setting.Missions.size;
598 		if (gCampaign.IsComplete)
599 		{
600 			LoopRunnerChange(l, ScreenVictory(&gCampaign));
601 		}
602 		else if (!mData->completed)
603 		{
604 			// Check if we want to return to menu or replay mission
605 			gCampaign.IsQuit = mData->ms.current->u.returnCode == 1;
606 			LoopRunnerPop(l);
607 		}
608 		else
609 		{
610 			LoopRunnerChange(
611 				l, HighScoresScreen(&gCampaign, &gGraphicsDevice));
612 		}
613 	}
614 	return result;
615 }
MissionSummaryDraw(GameLoopData * data)616 static void MissionSummaryDraw(GameLoopData *data)
617 {
618 	const MissionSummaryData *mData = data->Data;
619 
620 	MenuDraw(&mData->ms);
621 }
AreAnySurvived(void)622 static bool AreAnySurvived(void)
623 {
624 	CA_FOREACH(const PlayerData, p, gPlayerDatas)
625 	if (p->survived)
626 	{
627 		return true;
628 	}
629 	CA_FOREACH_END()
630 	return false;
631 }
GetAccessBonus(const struct MissionOptions * m)632 static int GetAccessBonus(const struct MissionOptions *m)
633 {
634 	return KeycardCount(m->KeyFlags) * 50;
635 }
GetTimeBonus(const struct MissionOptions * m,int * secondsOut)636 static int GetTimeBonus(const struct MissionOptions *m, int *secondsOut)
637 {
638 	int seconds = 60 + (int)m->missionData->Objectives.size * 30 -
639 				  m->time / FPS_FRAMELIMIT;
640 	if (seconds < 0)
641 	{
642 		seconds = 0;
643 	}
644 	if (secondsOut)
645 	{
646 		*secondsOut = seconds;
647 	}
648 	return seconds * 25;
649 }
ApplyBonuses(PlayerData * p,const int bonus)650 static void ApplyBonuses(PlayerData *p, const int bonus)
651 {
652 	// Apply bonuses to surviving players only
653 	if (!p->survived)
654 	{
655 		return;
656 	}
657 
658 	p->Totals.Score += bonus;
659 
660 	// Other per-player bonuses
661 	p->Totals.Score += GetHealthBonus(p);
662 	p->Totals.Score += GetResurrectionFee(p);
663 	p->Totals.Score += GetButcherPenalty(p);
664 	p->Totals.Score += GetNinjaBonus(p);
665 	p->Totals.Score += GetFriendlyBonus(p);
666 }
GetHealthBonus(const PlayerData * p)667 static int GetHealthBonus(const PlayerData *p)
668 {
669 	const int maxHealth = ModeMaxHealth(gCampaign.Entry.Mode);
670 	return p->hp > maxHealth - 50 ? (p->hp + 50 - maxHealth) * 10 : 0;
671 }
GetResurrectionFee(const PlayerData * p)672 static int GetResurrectionFee(const PlayerData *p)
673 {
674 	return p->hp <= 0 ? -500 : 0;
675 }
GetButcherPenalty(const PlayerData * p)676 static int GetButcherPenalty(const PlayerData *p)
677 {
678 	if (p->Stats.Friendlies > 5 && p->Stats.Friendlies > p->Stats.Kills / 2)
679 	{
680 		return -100 * p->Stats.Friendlies;
681 	}
682 	return 0;
683 }
684 // TODO: amend ninja bonus to check if no shots fired
GetNinjaBonus(const PlayerData * p)685 static int GetNinjaBonus(const PlayerData *p)
686 {
687 	if (PlayerGetNumWeapons(p) == 1)
688 	{
689 		const WeaponClass *wc = NULL;
690 		for (int i = 0; i < MAX_GUNS; i++)
691 		{
692 			if (p->guns[i] != NULL)
693 			{
694 				wc = p->guns[i];
695 				break;
696 			}
697 		}
698 		if (wc != NULL && !WeaponClassCanShoot(wc) &&
699 			p->Stats.Friendlies == 0 && p->Stats.Kills > 5)
700 		{
701 			return 50 * p->Stats.Kills;
702 		}
703 	}
704 	return 0;
705 }
GetFriendlyBonus(const PlayerData * p)706 static int GetFriendlyBonus(const PlayerData *p)
707 {
708 	return (p->Stats.Kills == 0 && p->Stats.Friendlies == 0) ? 500 : 0;
709 }
710 static void DrawPlayerSummary(
711 	const struct vec2i pos, const struct vec2i size,
712 	const PlayerSummaryDrawData *data);
MissionSummaryMenuDraw(const menu_t * menu,GraphicsDevice * g,const struct vec2i p,const struct vec2i size,const void * data)713 static void MissionSummaryMenuDraw(
714 	const menu_t *menu, GraphicsDevice *g, const struct vec2i p,
715 	const struct vec2i size, const void *data)
716 {
717 	UNUSED(menu);
718 	UNUSED(p);
719 	UNUSED(size);
720 	const MissionSummaryData *mData = data;
721 
722 	const int w = gGraphicsDevice.cachedConfig.Res.x;
723 	const int h = gGraphicsDevice.cachedConfig.Res.y;
724 
725 	// Display objectives and bonuses
726 	struct vec2i pos = svec2i(w / 6, h / 2 + h / 10);
727 	int idx = 1;
728 	CA_FOREACH(const Objective, o, mData->m->missionData->Objectives)
729 	// Do not mention optional objectives with none completed
730 	if (o->done == 0 && !ObjectiveIsRequired(o))
731 	{
732 		continue;
733 	}
734 
735 	// Objective icon
736 	DrawObjectiveInfo(o, svec2i_add(pos, svec2i(-26, FontH())));
737 
738 	// Objective completion text
739 	char s[100];
740 	sprintf(
741 		s, "Objective %d: %d of %d, %d required", idx, o->done, o->Count,
742 		o->Required);
743 	FontOpts opts = FontOptsNew();
744 	opts.Area = g->cachedConfig.Res;
745 	opts.Pad = pos;
746 	if (!ObjectiveIsRequired(o))
747 	{
748 		// Show optional objectives in purple
749 		opts.Mask = colorPurple;
750 	}
751 	FontStrOpt(s, svec2i_zero(), opts);
752 
753 	// Objective status text
754 	opts = FontOptsNew();
755 	opts.HAlign = ALIGN_END;
756 	opts.Area = g->cachedConfig.Res;
757 	opts.Pad = pos;
758 	if (!ObjectiveIsComplete(o))
759 	{
760 		opts.Mask = colorRed;
761 		FontStrOpt("Failed", svec2i_zero(), opts);
762 	}
763 	else if (ObjectiveIsPerfect(o) && AreAnySurvived())
764 	{
765 		opts.Mask = colorGreen;
766 		char buf[16];
767 		sprintf(buf, "Perfect: %d", PERFECT_BONUS);
768 		FontStrOpt(buf, svec2i_zero(), opts);
769 	}
770 	else if (ObjectiveIsRequired(o))
771 	{
772 		FontStrOpt("Done", svec2i_zero(), opts);
773 	}
774 	else
775 	{
776 		FontStrOpt("Bonus!", svec2i_zero(), opts);
777 	}
778 
779 	pos.y += 15;
780 	idx++;
781 	CA_FOREACH_END()
782 
783 	// Draw other bonuses
784 	if (mData->AccessBonus.prefix)
785 	{
786 		AnimatedCounterDraw(&mData->AccessBonus, pos);
787 		pos.y += FontH() + 1;
788 	}
789 	if (mData->TimeBonus.prefix)
790 	{
791 		AnimatedCounterDraw(&mData->TimeBonus, pos);
792 	}
793 
794 	// Draw per-player summaries
795 	struct vec2i playerSize;
796 	switch (GetNumPlayers(PLAYER_ANY, false, true))
797 	{
798 	case 1:
799 		playerSize = svec2i(w, h / 2);
800 		DrawPlayerSummary(svec2i_zero(), playerSize, &mData->pDatas[0]);
801 		break;
802 	case 2:
803 		// side by side
804 		playerSize = svec2i(w / 2, h / 2);
805 		DrawPlayerSummary(svec2i_zero(), playerSize, &mData->pDatas[0]);
806 		DrawPlayerSummary(svec2i(w / 2, 0), playerSize, &mData->pDatas[1]);
807 		break;
808 	case 3: // fallthrough
809 	case 4:
810 		// 2x2
811 		playerSize = svec2i(w / 2, h / 4);
812 		DrawPlayerSummary(svec2i_zero(), playerSize, &mData->pDatas[0]);
813 		DrawPlayerSummary(svec2i(w / 2, 0), playerSize, &mData->pDatas[1]);
814 		DrawPlayerSummary(svec2i(0, h / 4), playerSize, &mData->pDatas[2]);
815 		if (idx == 4)
816 		{
817 			DrawPlayerSummary(
818 				svec2i(w / 2, h / 4), playerSize, &mData->pDatas[3]);
819 		}
820 		break;
821 	default:
822 		CASSERT(false, "not implemented");
823 		break;
824 	}
825 }
826 // Display compact player summary, with player on left half and score summaries
827 // on right half
DrawPlayerSummary(const struct vec2i pos,const struct vec2i size,const PlayerSummaryDrawData * data)828 static void DrawPlayerSummary(
829 	const struct vec2i pos, const struct vec2i size,
830 	const PlayerSummaryDrawData *data)
831 {
832 	char s[50];
833 	const int totalTextHeight = FontH() * 7;
834 	// display text on right half
835 	struct vec2i textPos =
836 		svec2i(pos.x + size.x / 2, CENTER_Y(pos, size, totalTextHeight));
837 
838 	DisplayCharacterAndName(
839 		svec2i_add(pos, svec2i(size.x / 4, size.y / 2)), &data->Pd->Char,
840 		DIRECTION_DOWN, data->Pd->name, colorWhite, data->Pd->guns[0]);
841 
842 	if (data->Pd->survived)
843 	{
844 		FontStr("Completed mission", textPos);
845 	}
846 	else
847 	{
848 		FontStrMask("Failed mission", textPos, colorRed);
849 	}
850 
851 	textPos.y += 2 * FontH();
852 	AnimatedCounterDraw(&data->Score, textPos);
853 	textPos.y += FontH();
854 	AnimatedCounterDraw(&data->Total, textPos);
855 	textPos.y += FontH();
856 	sprintf(
857 		s, "Missions: %d", data->Pd->missions + (data->Pd->survived ? 1 : 0));
858 	FontStr(s, textPos);
859 	textPos.y += FontH();
860 
861 	if (data->HealthResurrection.prefix)
862 	{
863 		AnimatedCounterDraw(&data->HealthResurrection, textPos);
864 		textPos.y += FontH();
865 	}
866 	if (data->ButcherNinjaFriendly.prefix)
867 	{
868 		AnimatedCounterDraw(&data->ButcherNinjaFriendly, textPos);
869 	}
870 }
871 
DrawObjectiveInfo(const Objective * o,const struct vec2i pos)872 static void DrawObjectiveInfo(const Objective *o, const struct vec2i pos)
873 {
874 	const CharacterStore *store = &gCampaign.Setting.characters;
875 
876 	switch (o->Type)
877 	{
878 	case OBJECTIVE_KILL: {
879 		const Character *cd = CArrayGet(
880 			&store->OtherChars, CharacterStoreGetSpecialId(store, 0));
881 		DrawHead(gGraphicsDevice.gameWindow.renderer, cd, DIRECTION_DOWN, pos);
882 	}
883 	break;
884 	case OBJECTIVE_RESCUE: {
885 		const Character *cd = CArrayGet(
886 			&store->OtherChars, CharacterStoreGetPrisonerId(store, 0));
887 		DrawHead(gGraphicsDevice.gameWindow.renderer, cd, DIRECTION_DOWN, pos);
888 	}
889 	break;
890 	case OBJECTIVE_COLLECT:
891 		CPicDraw(
892 			&gGraphicsDevice, &o->u.Pickup->Pic,
893 			svec2i_subtract(pos, svec2i(-4, -4)), NULL);
894 		break;
895 	case OBJECTIVE_DESTROY: {
896 		struct vec2i picOffset;
897 		const Pic *p = MapObjectGetPic(o->u.MapObject, &picOffset);
898 		PicRender(
899 			p, gGraphicsDevice.gameWindow.renderer, svec2i_add(pos, picOffset),
900 			colorWhite, 0, svec2_one(), SDL_FLIP_NONE, Rect2iZero());
901 	}
902 	break;
903 	case OBJECTIVE_INVESTIGATE:
904 		// Don't draw
905 		return;
906 	default:
907 		CASSERT(false, "Unknown objective type");
908 		return;
909 	}
910 }
911