1 /*
2 	C-Dogs SDL
3 	A port of the legendary (and fun) action/arcade cdogs.
4 
5 	Copyright (c) 2013-2015, 2018, 2020-2021 Cong Xu
6 	All rights reserved.
7 
8 	Redistribution and use in source and binary forms, with or without
9 	modification, are permitted provided that the following conditions are met:
10 
11 	Redistributions of source code must retain the above copyright notice, this
12 	list of conditions and the following disclaimer.
13 	Redistributions in binary form must reproduce the above copyright notice,
14 	this list of conditions and the following disclaimer in the documentation
15 	and/or other materials provided with the distribution.
16 
17 	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 	ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 	POSSIBILITY OF SUCH DAMAGE.
28 */
29 #include "weapon_menu.h"
30 
31 #include <assert.h>
32 
33 #include <cdogs/ai_coop.h>
34 #include <cdogs/font.h>
35 
36 #define NO_GUN_LABEL "(None)"
37 #define END_MENU_LABEL "(End)"
38 
WeaponSetSelected(menu_t * menu,const WeaponClass * wc,const bool selected)39 static void WeaponSetSelected(
40 	menu_t *menu, const WeaponClass *wc, const bool selected)
41 {
42 	if (wc == NULL)
43 	{
44 		return;
45 	}
46 	CA_FOREACH(menu_t, subMenu, menu->u.normal.subMenus)
47 	if (strcmp(subMenu->name, wc->name) == 0)
48 	{
49 		subMenu->color = selected ? colorYellow : colorWhite;
50 		break;
51 	}
52 	CA_FOREACH_END()
53 }
54 
GetSelectedGun(const menu_t * menu)55 static const WeaponClass *GetSelectedGun(const menu_t *menu)
56 {
57 	const menu_t *subMenu =
58 		CArrayGet(&menu->u.normal.subMenus, menu->u.normal.index);
59 	if (strcmp(subMenu->name, NO_GUN_LABEL) == 0)
60 	{
61 		return NULL;
62 	}
63 	return StrWeaponClass(subMenu->name);
64 }
65 
WeaponSelect(menu_t * menu,int cmd,void * data)66 static void WeaponSelect(menu_t *menu, int cmd, void *data)
67 {
68 	WeaponMenuData *d = data;
69 
70 	if (cmd & CMD_BUTTON1)
71 	{
72 		// Add the selected weapon to the slot
73 		d->SelectedGun = GetSelectedGun(menu);
74 		d->SelectResult = WEAPON_MENU_SELECT;
75 		if (d->SelectedGun == NULL)
76 		{
77 			MenuPlaySound(MENU_SOUND_SWITCH);
78 			return;
79 		}
80 		SoundPlay(&gSoundDevice, d->SelectedGun->SwitchSound);
81 	}
82 	else if (cmd & CMD_BUTTON2)
83 	{
84 		d->SelectedGun = GetSelectedGun(menu);
85 		d->SelectResult = WEAPON_MENU_CANCEL;
86 		MenuPlaySound(MENU_SOUND_BACK);
87 	}
88 }
89 
90 static void DisplayGunIcon(
91 	const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
92 	const struct vec2i size, const void *data);
93 static void AddEquippedMenuItem(
94 	menu_t *menu, const PlayerData *p, const int slot, const bool enabled);
PostInputEquipMenu(menu_t * menu,int cmd,void * data)95 static void PostInputEquipMenu(menu_t *menu, int cmd, void *data)
96 {
97 	MenuDisplayPlayerData *d = data;
98 
99 	// Rotate player using left/right keys
100 	const int dx = (cmd & CMD_LEFT) ? 1 : ((cmd & CMD_RIGHT) ? -1 : 0);
101 	if (dx != 0)
102 	{
103 		d->Dir = (direction_e)CLAMP_OPPOSITE(
104 			(int)d->Dir + dx, DIRECTION_UP, DIRECTION_UPLEFT);
105 		char buf[CDOGS_PATH_MAX];
106 		const PlayerData *p = PlayerDataGetByUID(d->PlayerUID);
107 		sprintf(buf, "footsteps/%s", p->Char.Class->Footsteps);
108 		SoundPlay(&gSoundDevice, StrSound(buf));
109 	}
110 
111 	// Display gun based on menu index
112 	d->GunIdx = MIN(menu->u.normal.index, MAX_WEAPONS);
113 }
CreateEquippedWeaponsMenu(MenuSystem * ms,EventHandlers * handlers,GraphicsDevice * g,const struct vec2i pos,const struct vec2i size,const PlayerData * p,const CArray * weapons,MenuDisplayPlayerData * display)114 static void CreateEquippedWeaponsMenu(
115 	MenuSystem *ms, EventHandlers *handlers, GraphicsDevice *g,
116 	const struct vec2i pos, const struct vec2i size, const PlayerData *p,
117 	const CArray *weapons, MenuDisplayPlayerData *display)
118 {
119 	const struct vec2i maxTextSize = FontStrSize("LongestWeaponName");
120 	struct vec2i dPos = pos;
121 	dPos.x -= size.x; // move to left half of screen
122 	const struct vec2i weaponsPos = svec2i(
123 		dPos.x + size.x * 3 / 4 - maxTextSize.x / 2,
124 		CENTER_Y(dPos, size, 0) + 2);
125 	const int numRows = MAX_GUNS + 1 + MAX_GRENADES + 1 + 1;
126 	const struct vec2i weaponsSize = svec2i(size.x, FontH() * numRows);
127 
128 	MenuSystemInit(ms, handlers, g, weaponsPos, weaponsSize);
129 	ms->align = MENU_ALIGN_LEFT;
130 	ms->root = ms->current = MenuCreateNormal("", "", MENU_TYPE_NORMAL, 0);
131 	MenuAddExitType(ms, MENU_TYPE_RETURN);
132 
133 	// Count number of guns/grenades, and disable extra menu items
134 	int numGuns = 0;
135 	int numGrenades = 0;
136 	CA_FOREACH(const WeaponClass *, wc, *weapons)
137 	if ((*wc)->Type == GUNTYPE_GRENADE)
138 	{
139 		numGrenades++;
140 	}
141 	else
142 	{
143 		numGuns++;
144 	}
145 	CA_FOREACH_END()
146 	int i;
147 	for (i = 0; i < MAX_GUNS; i++)
148 	{
149 		const bool submenuEnabled = i < numGuns;
150 		AddEquippedMenuItem(ms->root, p, i, submenuEnabled);
151 	}
152 	MenuAddSubmenu(ms->root, MenuCreateSeparator("--Grenades--"));
153 	for (; i < MAX_GUNS + MAX_GRENADES; i++)
154 	{
155 		const bool submenuEnabled = i - MAX_GUNS < numGrenades;
156 		AddEquippedMenuItem(ms->root, p, i, submenuEnabled);
157 	}
158 	MenuAddSubmenu(
159 		ms->root, MenuCreateNormal(END_MENU_LABEL, "", MENU_TYPE_NORMAL, 0));
160 
161 	MenuSetCustomDisplay(ms->root, DisplayGunIcon, NULL);
162 	MenuSetPostInputFunc(ms->root, PostInputEquipMenu, display);
163 
164 	// Pre-select the End menu
165 	ms->root->u.normal.index = (int)(ms->root->u.normal.subMenus.size - 1);
166 }
SetEquippedMenuItemName(menu_t * menu,const PlayerData * p,const int slot)167 static void SetEquippedMenuItemName(
168 	menu_t *menu, const PlayerData *p, const int slot)
169 {
170 	CFREE(menu->name);
171 	if (p->guns[slot] != NULL)
172 	{
173 		CSTRDUP(menu->name, p->guns[slot]->name);
174 	}
175 	else
176 	{
177 		CSTRDUP(menu->name, NO_GUN_LABEL);
178 	}
179 }
AddEquippedMenuItem(menu_t * menu,const PlayerData * p,const int slot,const bool enabled)180 static void AddEquippedMenuItem(
181 	menu_t *menu, const PlayerData *p, const int slot, const bool enabled)
182 {
183 	menu_t *submenu = MenuCreateReturn("", slot);
184 	SetEquippedMenuItemName(submenu, p, slot);
185 	menu_t *addedMenu = MenuAddSubmenu(menu, submenu);
186 	addedMenu->isDisabled = !enabled;
187 }
188 
189 static menu_t *CreateGunMenu(
190 	const CArray *weapons, const struct vec2i menuSize, const bool isGrenade,
191 	WeaponMenuData *data);
192 static void DisplayDescriptionGunIcon(
193 	const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
194 	const struct vec2i size, const void *data);
WeaponMenuCreate(WeaponMenu * menu,const CArray * weapons,const int numPlayers,const int player,const int playerUID,EventHandlers * handlers,GraphicsDevice * graphics)195 void WeaponMenuCreate(
196 	WeaponMenu *menu, const CArray *weapons, const int numPlayers,
197 	const int player, const int playerUID, EventHandlers *handlers,
198 	GraphicsDevice *graphics)
199 {
200 	MenuSystem *ms = &menu->ms;
201 	WeaponMenuData *data = &menu->data;
202 	struct vec2i pos, size;
203 	int w = graphics->cachedConfig.Res.x;
204 	int h = graphics->cachedConfig.Res.y;
205 
206 	data->display.PlayerUID = playerUID;
207 	data->display.currentMenu = NULL;
208 	data->display.Dir = DIRECTION_DOWN;
209 	data->PlayerUID = playerUID;
210 
211 	switch (numPlayers)
212 	{
213 	case 1:
214 		// Single menu, entire screen
215 		pos = svec2i(w / 2, 0);
216 		size = svec2i(w / 2, h);
217 		break;
218 	case 2:
219 		// Two menus, side by side
220 		pos = svec2i(player * w / 2 + w / 4, 0);
221 		size = svec2i(w / 4, h);
222 		break;
223 	case 3:
224 	case 4:
225 		// Four corners
226 		pos = svec2i((player & 1) * w / 2 + w / 4, (player / 2) * h / 2);
227 		size = svec2i(w / 4, h / 2);
228 		break;
229 	default:
230 		CASSERT(false, "not implemented");
231 		pos = svec2i(w / 2, 0);
232 		size = svec2i(w / 2, h);
233 		break;
234 	}
235 	MenuSystemInit(ms, handlers, graphics, pos, size);
236 	ms->align = MENU_ALIGN_LEFT;
237 	PlayerData *pData = PlayerDataGetByUID(playerUID);
238 	menu->gunMenu = CreateGunMenu(weapons, size, false, data);
239 	menu->grenadeMenu = CreateGunMenu(weapons, size, true, data);
240 	MenuSystemAddCustomDisplay(ms, MenuDisplayPlayer, &data->display);
241 	MenuSystemAddCustomDisplay(
242 		ms, MenuDisplayPlayerControls, &data->PlayerUID);
243 
244 	// Create equipped weapons menu
245 	CreateEquippedWeaponsMenu(
246 		&menu->msEquip, handlers, graphics, pos, size, pData, weapons,
247 		&data->display);
248 
249 	// For AI players, pre-pick their weapons and go straight to menu end
250 	if (pData->inputDevice == INPUT_DEVICE_AI)
251 	{
252 		menu->msEquip.current =
253 			MenuGetSubmenuByName(menu->msEquip.root, END_MENU_LABEL);
254 		AICoopSelectWeapons(pData, player, weapons);
255 	}
256 }
CreateGunMenu(const CArray * weapons,const struct vec2i menuSize,const bool isGrenade,WeaponMenuData * data)257 static menu_t *CreateGunMenu(
258 	const CArray *weapons, const struct vec2i menuSize, const bool isGrenade,
259 	WeaponMenuData *data)
260 {
261 	menu_t *menu = MenuCreateNormal("", "", MENU_TYPE_NORMAL, 0);
262 	menu->u.normal.maxItems = 11;
263 	MenuAddSubmenu(menu, MenuCreate(NO_GUN_LABEL, MENU_TYPE_BASIC));
264 	CA_FOREACH(const WeaponClass *, wc, *weapons)
265 	if (((*wc)->Type == GUNTYPE_GRENADE) != isGrenade)
266 	{
267 		continue;
268 	}
269 	menu_t *gunMenu;
270 	if ((*wc)->Description != NULL)
271 	{
272 		// Gun description menu
273 		gunMenu = MenuCreateNormal((*wc)->name, "", MENU_TYPE_NORMAL, 0);
274 		char *buf;
275 		CMALLOC(buf, strlen((*wc)->Description) * 2);
276 		FontSplitLines((*wc)->Description, buf, menuSize.x * 5 / 6);
277 		MenuAddSubmenu(gunMenu, MenuCreateBack(buf));
278 		CFREE(buf);
279 		gunMenu->u.normal.isSubmenusAlt = true;
280 		MenuSetCustomDisplay(gunMenu, DisplayDescriptionGunIcon, *wc);
281 	}
282 	else
283 	{
284 		gunMenu = MenuCreate((*wc)->name, MENU_TYPE_BASIC);
285 	}
286 	MenuAddSubmenu(menu, gunMenu);
287 	CA_FOREACH_END()
288 
289 	MenuSetPostInputFunc(menu, WeaponSelect, data);
290 
291 	MenuSetCustomDisplay(menu, DisplayGunIcon, NULL);
292 
293 	return menu;
294 }
DisplayGunIcon(const menu_t * menu,GraphicsDevice * g,const struct vec2i pos,const struct vec2i size,const void * data)295 static void DisplayGunIcon(
296 	const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
297 	const struct vec2i size, const void *data)
298 {
299 	UNUSED(data);
300 	if (menu->isDisabled)
301 	{
302 		return;
303 	}
304 	// Display a gun icon next to the currently selected weapon
305 	const WeaponClass *wc = GetSelectedGun(menu);
306 	if (wc == NULL)
307 	{
308 		return;
309 	}
310 	const int menuItems = MenuGetNumMenuItemsShown(menu);
311 	const int textScroll =
312 		-menuItems * FontH() / 2 +
313 		(menu->u.normal.index - menu->u.normal.scroll) * FontH();
314 	const struct vec2i iconPos = svec2i(
315 		pos.x - wc->Icon->size.x - 4,
316 		pos.y + size.y / 2 + textScroll + (FontH() - wc->Icon->size.y) / 2);
317 	PicRender(
318 		wc->Icon, g->gameWindow.renderer, iconPos, colorWhite, 0, svec2_one(),
319 		SDL_FLIP_NONE, Rect2iZero());
320 }
DisplayDescriptionGunIcon(const menu_t * menu,GraphicsDevice * g,const struct vec2i pos,const struct vec2i size,const void * data)321 static void DisplayDescriptionGunIcon(
322 	const menu_t *menu, GraphicsDevice *g, const struct vec2i pos,
323 	const struct vec2i size, const void *data)
324 {
325 	UNUSED(menu);
326 	UNUSED(size);
327 	const WeaponClass *wc = data;
328 	// Display the gun just to the left of the description text
329 	const struct vec2i iconPos =
330 		svec2i(pos.x - wc->Icon->size.x - 4, pos.y + size.y / 2);
331 	PicRender(
332 		wc->Icon, g->gameWindow.renderer, iconPos, colorWhite, 0, svec2_one(),
333 		SDL_FLIP_NONE, Rect2iZero());
334 }
335 
WeaponMenuTerminate(WeaponMenu * menu)336 void WeaponMenuTerminate(WeaponMenu *menu)
337 {
338 	MenuSystemTerminate(&menu->ms);
339 }
340 
WeaponMenuUpdate(WeaponMenu * menu,const int cmd)341 void WeaponMenuUpdate(WeaponMenu *menu, const int cmd)
342 {
343 	PlayerData *p = PlayerDataGetByUID(menu->data.PlayerUID);
344 	if (menu->equipping)
345 	{
346 		MenuProcessCmd(&menu->ms, cmd);
347 		menu_t *equipMenu = menu->data.EquipSlot < MAX_GUNS
348 								? menu->gunMenu
349 								: menu->grenadeMenu;
350 		switch (menu->data.SelectResult)
351 		{
352 		case WEAPON_MENU_NONE:
353 			break;
354 		case WEAPON_MENU_SELECT:
355 			// Deselect menu items due to changed equipment
356 			WeaponSetSelected(equipMenu, p->guns[menu->data.EquipSlot], false);
357 			// See if the selected gun is already equipped; if so swap it with
358 			// the current slot
359 			for (int i = 0; i < MAX_WEAPONS; i++)
360 			{
361 				if (p->guns[i] == menu->data.SelectedGun)
362 				{
363 					p->guns[i] = p->guns[menu->data.EquipSlot];
364 					break;
365 				}
366 			}
367 			p->guns[menu->data.EquipSlot] = menu->data.SelectedGun;
368 			// fallthrough
369 		case WEAPON_MENU_CANCEL:
370 			// Switch back to equip menu
371 			menu->equipping = false;
372 			// Update menu names based on weapons
373 			CA_FOREACH(menu_t, submenu, menu->msEquip.root->u.normal.subMenus)
374 			if (submenu->type == MENU_TYPE_RETURN)
375 			{
376 				SetEquippedMenuItemName(submenu, p, submenu->u.returnCode);
377 			}
378 			CA_FOREACH_END()
379 			break;
380 		default:
381 			CASSERT(false, "unhandled case");
382 			break;
383 		}
384 		// Select menu items where the player already has the weapon
385 		for (int i = 0; i < MAX_WEAPONS; i++)
386 		{
387 			WeaponSetSelected(equipMenu, p->guns[i], true);
388 		}
389 	}
390 	else
391 	{
392 		MenuProcessCmd(&menu->msEquip, cmd);
393 		if (MenuIsExit(&menu->msEquip) &&
394 			strcmp(menu->msEquip.current->name, END_MENU_LABEL) != 0)
395 		{
396 			// Open weapon selection menu
397 			menu->equipping = true;
398 			menu->data.EquipSlot = menu->msEquip.current->u.returnCode;
399 			menu->msEquip.current = menu->msEquip.root;
400 			menu->data.SelectResult = WEAPON_MENU_NONE;
401 		}
402 	}
403 
404 	// Display the gun/grenade menu based on which submenu is hovered
405 	if (!menu->equipping)
406 	{
407 		const menu_t *hoveredEquipMenu = CArrayGet(
408 			&menu->msEquip.root->u.normal.subMenus,
409 			menu->msEquip.root->u.normal.index);
410 		if (hoveredEquipMenu->type == MENU_TYPE_RETURN)
411 		{
412 			const int equipSlot = hoveredEquipMenu->u.returnCode;
413 			menu->ms.current =
414 				equipSlot < MAX_GUNS ? menu->gunMenu : menu->grenadeMenu;
415 		}
416 		else
417 		{
418 			menu->ms.current = NULL;
419 		}
420 	}
421 
422 	// Disable the equip/weapon menus based on equipping state
423 	MenuSetDisabled(menu->msEquip.root, menu->equipping);
424 	if (menu->ms.current)
425 	{
426 		MenuSetDisabled(menu->ms.current, !menu->equipping);
427 	}
428 
429 	// Disable "Done" if no weapons selected
430 	menu_t *endMenuItem =
431 		MenuGetSubmenuByName(menu->msEquip.root, END_MENU_LABEL);
432 	MenuSetDisabled(endMenuItem, PlayerGetNumWeapons(p) == 0);
433 }
434 
WeaponMenuIsDone(const WeaponMenu * menu)435 bool WeaponMenuIsDone(const WeaponMenu *menu)
436 {
437 	return strcmp(menu->msEquip.current->name, END_MENU_LABEL) == 0;
438 }
439 
WeaponMenuDraw(const WeaponMenu * menu)440 void WeaponMenuDraw(const WeaponMenu *menu)
441 {
442 	MenuDisplay(&menu->ms);
443 	MenuDisplay(&menu->msEquip);
444 }
445