1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4 Copyright (c) 2013-2017, 2019-2020 Cong Xu
5 All rights reserved.
6
7 Redistribution and use in source and binary forms, with or without
8 modification, are permitted provided that the following conditions are met:
9
10 Redistributions of source code must retain the above copyright notice, this
11 list of conditions and the following disclaimer.
12 Redistributions in binary form must reproduce the above copyright notice,
13 this list of conditions and the following disclaimer in the documentation
14 and/or other materials provided with the distribution.
15
16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 POSSIBILITY OF SUCH DAMAGE.
27 */
28 #include "hud.h"
29
30 #include <assert.h>
31 #include <math.h>
32
33 #include "actors.h"
34 #include "ammo.h"
35 #include "automap.h"
36 #include "draw/draw.h"
37 #include "draw/draw_actor.h"
38 #include "draw/drawtools.h"
39 #include "events.h"
40 #include "font.h"
41 #include "game_events.h"
42 #include "hud_defs.h"
43 #include "mission.h"
44 #include "pic_manager.h"
45 #include "player.h"
46 #include "player_hud.h"
47
HUDInit(HUD * hud,GraphicsDevice * device,struct MissionOptions * mission)48 void HUDInit(HUD *hud, GraphicsDevice *device, struct MissionOptions *mission)
49 {
50 memset(hud, 0, sizeof *hud);
51 hud->mission = mission;
52 strcpy(hud->message, "");
53 hud->messageTicks = 0;
54 hud->device = device;
55 FPSCounterInit(&hud->fpsCounter);
56 WallClockInit(&hud->clock);
57 HUDNumPopupsInit(&hud->numPopups, mission);
58 for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
59 {
60 HUDPlayerInit(&hud->hudPlayers[i]);
61 }
62 hud->showExit = false;
63 }
HUDTerminate(HUD * hud)64 void HUDTerminate(HUD *hud)
65 {
66 HUDNumPopupsTerminate(&hud->numPopups);
67 }
68
HUDDisplayMessage(HUD * hud,const char * msg,int ticks)69 void HUDDisplayMessage(HUD *hud, const char *msg, int ticks)
70 {
71 strcpy(hud->message, msg);
72 hud->messageTicks = ticks;
73 }
74
HUDUpdate(HUD * hud,const int ms)75 void HUDUpdate(HUD *hud, const int ms)
76 {
77 if (hud->messageTicks >= 0)
78 {
79 hud->messageTicks -= ms;
80 if (hud->messageTicks < 0)
81 {
82 hud->messageTicks = 0;
83 }
84 }
85 FPSCounterUpdate(&hud->fpsCounter, ms);
86 WallClockUpdate(&hud->clock, ms);
87 HUDPopupsUpdate(&hud->numPopups, ms);
88
89 for (int i = 0; i < hud->DrawData.NumScreens; i++)
90 {
91 const PlayerData *p = hud->DrawData.Players[i];
92 HUDPlayerUpdate(&hud->hudPlayers[i], p, ms);
93 }
94 }
95
HUDGetDrawData(void)96 HUDDrawData HUDGetDrawData(void)
97 {
98 HUDDrawData drawData;
99 memset(&drawData, 0, sizeof drawData);
100 CA_FOREACH(const PlayerData, p, gPlayerDatas)
101 if (!IsPlayerScreen(p))
102 {
103 continue;
104 }
105 drawData.Players[drawData.NumScreens] = p;
106 drawData.NumScreens++;
107 }
108 return drawData;
109 }
110
111 static void DrawSharedRadar(GraphicsDevice *device, bool showExit)
112 {
113 int w = device->cachedConfig.Res.x;
114 struct vec2i pos = svec2i(w / 2 - AUTOMAP_SIZE / 2, AUTOMAP_PADDING);
115 const struct vec2i playerMidpoint = Vec2ToTile(PlayersGetMidpoint());
116 AutomapDrawRegion(
117 device->gameWindow.renderer, &gMap, pos,
118 svec2i(AUTOMAP_SIZE, AUTOMAP_SIZE), playerMidpoint, AUTOMAP_FLAGS_MASK,
119 showExit);
120 }
121
122 static void DrawPlayerAreas(HUD *hud, const int numViews);
123 static void DrawDeathmatchScores(HUD *hud);
124 static void DrawStateMessage(
125 HUD *hud, const input_device_e pausingDevice,
126 const bool controllerUnplugged);
127 static void DrawHUDMessage(HUD *hud);
128 static void DrawKeycards(HUD *hud);
129 static void DrawMissionTime(HUD *hud);
130 static void DrawObjectiveCounts(HUD *hud);
131 void HUDDraw(
132 HUD *hud, const input_device_e pausingDevice,
133 const bool controllerUnplugged, const int numViews)
134 {
135 if (ConfigGetBool(&gConfig, "Graphics.ShowHUD"))
136 {
137 DrawPlayerAreas(hud, numViews);
138
139 DrawDeathmatchScores(hud);
140 DrawHUDMessage(hud);
141 if (ConfigGetBool(&gConfig, "Interface.ShowFPS"))
142 {
143 FPSCounterDraw(&hud->fpsCounter);
144 }
145 if (ConfigGetBool(&gConfig, "Interface.ShowTime"))
146 {
147 WallClockDraw(&hud->clock);
148 }
149 DrawKeycards(hud);
150 DrawMissionTime(hud);
151 if (HasObjectives(gCampaign.Entry.Mode))
152 {
153 DrawObjectiveCounts(hud);
154 }
155 }
156
157 DrawStateMessage(hud, pausingDevice, controllerUnplugged);
158 }
159
160 static void DrawPlayerAreas(HUD *hud, const int numViews)
161 {
162 int flags = 0;
163
164 Rect2i r = Rect2iNew(svec2i_zero(), hud->device->cachedConfig.Res);
165 if (hud->DrawData.NumScreens <= 1)
166 {
167 // Do nothing
168 }
169 else if (hud->DrawData.NumScreens == 2)
170 {
171 r.Size.x /= 2;
172 }
173 else if (hud->DrawData.NumScreens == 3 || hud->DrawData.NumScreens == 4)
174 {
175 r.Size.x /= 2;
176 r.Size.y /= 2;
177 }
178 else
179 {
180 CASSERT(false, "not implemented");
181 }
182
183 if (hud->DrawData.NumScreens <= 1)
184 {
185 // Do nothing
186 }
187 else if (
188 hud->DrawData.NumScreens > 1 &&
189 ConfigGetEnum(&gConfig, "Interface.Splitscreen") == SPLITSCREEN_NEVER)
190 {
191 flags |= HUDFLAGS_SHARE_SCREEN;
192 }
193 else if (hud->DrawData.NumScreens == 2)
194 {
195 flags |= HUDFLAGS_HALF_SCREEN;
196 }
197 else if (hud->DrawData.NumScreens == 3 || hud->DrawData.NumScreens == 4)
198 {
199 flags |= HUDFLAGS_QUARTER_SCREEN;
200 }
201 else
202 {
203 CASSERT(false, "not implemented");
204 }
205
206 for (int i = 0; i < hud->DrawData.NumScreens; i++)
207 {
208 const PlayerData *p = hud->DrawData.Players[i];
209 int drawFlags = flags;
210 r.Pos = svec2i_zero();
211 if (i & 1)
212 {
213 r.Pos.x = r.Size.x;
214 drawFlags |= HUDFLAGS_PLACE_RIGHT;
215 }
216 if (i >= 2)
217 {
218 r.Pos.y = r.Size.y;
219 drawFlags |= HUDFLAGS_PLACE_BOTTOM;
220 }
221 DrawPlayerHUD(hud, p, drawFlags, i, r, numViews);
222 }
223
224 // Only draw radar once if shared
225 if (ConfigGetBool(&gConfig, "Interface.ShowHUDMap") &&
226 (flags & HUDFLAGS_SHARE_SCREEN) &&
227 IsAutoMapEnabled(gCampaign.Entry.Mode))
228 {
229 DrawSharedRadar(hud->device, hud->showExit);
230 }
231 }
232
233 static void DrawDeathmatchScores(HUD *hud)
234 {
235 // Only draw deathmatch scores if single screen and non-local players exist
236 if (gCampaign.Entry.Mode != GAME_MODE_DEATHMATCH ||
237 hud->DrawData.NumScreens != 1 ||
238 GetNumPlayers(PLAYER_ANY, false, false) <= 1)
239 {
240 return;
241 }
242 FontOpts opts = FontOptsNew();
243 opts.Area = hud->device->cachedConfig.Res;
244 opts.HAlign = ALIGN_END;
245 opts.Mask = colorPurple;
246 const int nameColumn = 45;
247 const int livesColumn = 25;
248 const int killsColumn = 5;
249 int y = 5;
250 opts.Pad.x = nameColumn;
251 FontStrOpt("Player", svec2i(0, y), opts);
252 opts.Pad.x = livesColumn;
253 FontStrOpt("Lives", svec2i(0, y), opts);
254 opts.Pad.x = killsColumn;
255 FontStrOpt("Kills", svec2i(0, y), opts);
256 y += FontH();
257 // Find the player(s) with the most lives and kills
258 int maxLives = 0;
259 int maxKills = 0;
260 CA_FOREACH(const PlayerData, p, gPlayerDatas)
261 maxLives = MAX(maxLives, p->Lives);
262 maxKills = MAX(maxKills, p->Stats.Kills);
263 CA_FOREACH_END()
264 CA_FOREACH(const PlayerData, p, gPlayerDatas)
265 // Player name; red if dead
266 opts.Mask = p->Lives > 0 ? colorWhite : colorRed;
267 opts.Pad.x = nameColumn;
268 FontStrOpt(p->name, svec2i(0, y), opts);
269
270 // lives; cyan if most lives
271 opts.Mask = p->Lives == maxLives ? colorCyan : colorWhite;
272 opts.Pad.x = livesColumn;
273 char buf[32];
274 sprintf(buf, "%d", p->Lives);
275 FontStrOpt(buf, svec2i(0, y), opts);
276
277 // kills; cyan if most kills
278 opts.Mask = p->Stats.Kills == maxKills ? colorCyan : colorWhite;
279 opts.Pad.x = killsColumn;
280 sprintf(buf, "%d", p->Stats.Kills);
281 FontStrOpt(buf, svec2i(0, y), opts);
282 y += FontH();
283 CA_FOREACH_END()
284 }
285
286 static void DrawMissionState(HUD *hud);
287 static void DrawStateMessage(
288 HUD *hud, const input_device_e pausingDevice,
289 const bool controllerUnplugged)
290 {
291 if (controllerUnplugged || pausingDevice != INPUT_DEVICE_UNSET)
292 {
293 // Draw a background overlay
294 color_t overlay = colorBlack;
295 overlay.a = 128;
296 DrawRectangle(
297 hud->device, svec2i_zero(), hud->device->cachedConfig.Res, overlay,
298 true);
299 }
300 if (controllerUnplugged)
301 {
302 struct vec2i pos = svec2i_scale_divide(
303 svec2i_subtract(
304 gGraphicsDevice.cachedConfig.Res,
305 FontStrSize(ARROW_LEFT
306 "Paused" ARROW_RIGHT
307 "\nFoobar\nPlease reconnect controller")),
308 2);
309 const int x = pos.x;
310 FontStr(ARROW_LEFT "Paused" ARROW_RIGHT, pos);
311
312 pos.y += FontH();
313 pos = FontStr("Press ", pos);
314 char buf[256];
315 color_t c = colorWhite;
316 InputGetButtonNameColor(pausingDevice, 0, CMD_ESC, buf, &c);
317 pos = FontStrMask(buf, pos, c);
318 FontStr(" to quit", pos);
319
320 pos.x = x;
321 pos.y += FontH();
322 FontStr("Please reconnect controller", pos);
323 }
324 else if (pausingDevice != INPUT_DEVICE_UNSET)
325 {
326 struct vec2i pos = svec2i_scale_divide(
327 svec2i_subtract(
328 gGraphicsDevice.cachedConfig.Res,
329 FontStrSize("Foo\nPress foo or bar to unpause\nBaz")),
330 2);
331 const int x = pos.x;
332 FontStr(ARROW_LEFT "Paused" ARROW_RIGHT, pos);
333
334 pos.y += FontH();
335 pos = FontStr("Press ", pos);
336 char buf[256];
337 color_t c = colorWhite;
338 InputGetButtonNameColor(pausingDevice, 0, CMD_ESC, buf, &c);
339 pos = FontStrMask(buf, pos, c);
340 FontStr(" again to quit", pos);
341
342 pos.x = x;
343 pos.y += FontH();
344 pos = FontStr("Press ", pos);
345 c = colorWhite;
346 InputGetButtonNameColor(pausingDevice, 0, CMD_BUTTON1, buf, &c);
347 pos = FontStrMask(buf, pos, c);
348 pos = FontStr(" or ", pos);
349 c = colorWhite;
350 InputGetButtonNameColor(pausingDevice, 0, CMD_BUTTON2, buf, &c);
351 pos = FontStrMask(buf, pos, c);
352 FontStr(" to unpause", pos);
353 }
354 else
355 {
356 DrawMissionState(hud);
357 }
358 }
359 static void DrawMissionState(HUD *hud)
360 {
361 char s[50];
362 const int numPlayersAlive =
363 GetNumPlayers(PLAYER_ALIVE_OR_DYING, false, false);
364
365 switch (hud->mission->state)
366 {
367 case MISSION_STATE_WAITING:
368 FontStrCenter("Waiting for players...");
369 break;
370 case MISSION_STATE_PLAY:
371 if (numPlayersAlive == 0 && AreAllPlayersDeadAndNoLives())
372 {
373 if (gPlayerDatas.size == 0)
374 {
375 FontStrCenter("Waiting for players...");
376 }
377 else if (!IsPVP(gCampaign.Entry.Mode))
378 {
379 FontStrCenter("Game Over!");
380 }
381 else
382 {
383 FontStrCenter("All Kill!");
384 }
385 }
386 else if (MissionNeedsMoreRescuesInExit(&gMission))
387 {
388 FontStrCenter("More rescues needed");
389 }
390 break;
391 case MISSION_STATE_PICKUP: {
392 int timeLeft = gMission.pickupTime + PICKUP_LIMIT - gMission.time;
393 sprintf(
394 s, "Pickup in %d seconds\n",
395 (timeLeft + (FPS_FRAMELIMIT - 1)) / FPS_FRAMELIMIT);
396 FontStrCenter(s);
397 }
398 break;
399 default:
400 CASSERT(false, "unknown mission state");
401 break;
402 }
403 }
404
405 static void DrawHUDMessage(HUD *hud)
406 {
407 if (hud->messageTicks > 0 || hud->messageTicks == -1)
408 {
409 // Draw the message centered, and just below the automap
410 struct vec2i pos = svec2i(
411 (hud->device->cachedConfig.Res.x - FontStrW(hud->message)) / 2,
412 AUTOMAP_SIZE + AUTOMAP_PADDING + AUTOMAP_PADDING);
413 const HSV tint = {-1.0, 1.0, Pulse256(hud->mission->time) / 256.0};
414 const color_t mask = ColorTint(colorCyan, tint);
415 FontStrMask(hud->message, pos, mask);
416 }
417 }
418
419 static void DrawKeycards(HUD *hud)
420 {
421 int keyFlags[] = {
422 FLAGS_KEYCARD_YELLOW, FLAGS_KEYCARD_GREEN, FLAGS_KEYCARD_BLUE,
423 FLAGS_KEYCARD_RED};
424 int i;
425 int xOffset = -30;
426 int xOffsetIncr = 20;
427 int yOffset = 20;
428 for (i = 0; i < 4; i++)
429 {
430 if (hud->mission->KeyFlags & keyFlags[i])
431 {
432 CPicDraw(
433 hud->device,
434 &KeyPickupClass(hud->mission->missionData->KeyStyle, i)->Pic,
435 svec2i(CenterX(8) - xOffset, yOffset), NULL);
436 }
437 xOffset += xOffsetIncr;
438 }
439 }
440
441 static void DrawMissionTime(HUD *hud)
442 {
443 char s[50];
444 // Draw elapsed mission time as MM:SS
445 int missionTimeSeconds = gMission.time / FPS_FRAMELIMIT;
446 sprintf(s, "%d:%02d", missionTimeSeconds / 60, missionTimeSeconds % 60);
447
448 FontOpts opts = FontOptsNew();
449 opts.HAlign = ALIGN_CENTER;
450 opts.Area = hud->device->cachedConfig.Res;
451 opts.Pad.y = AUTOMAP_PADDING;
452 FontStrOpt(s, svec2i_zero(), opts);
453 }
454
455 static void DrawObjectiveCounts(HUD *hud)
456 {
457 int x = 45;
458 int y = hud->device->cachedConfig.Res.y - 22;
459 CA_FOREACH(const Objective, o, hud->mission->missionData->Objectives)
460 // Don't draw anything for optional objectives
461 if (!ObjectiveIsRequired(o))
462 {
463 continue;
464 }
465
466 // Objective color dot
467 DrawRectangle(
468 hud->device, svec2i(x, y + 3), svec2i(2, 2), o->color, false);
469
470 x += 5;
471 char s[32];
472 const int itemsLeft = o->Required - o->done;
473 if (itemsLeft > 0)
474 {
475 if (!(o->Flags & OBJECTIVE_UNKNOWNCOUNT))
476 {
477 sprintf(s, "%s: %d", ObjectiveTypeStr(o->Type), itemsLeft);
478 }
479 else
480 {
481 sprintf(s, "%s: ?", ObjectiveTypeStr(o->Type));
482 }
483 }
484 else
485 {
486 strcpy(s, "Done");
487 }
488 FontStr(s, svec2i(x, y));
489
490 HUDNumPopupsDrawObjective(
491 &hud->numPopups, _ca_index, svec2i(x + FontStrW(s) - 8, y));
492
493 x += 40;
494 CA_FOREACH_END()
495 }
496