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