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