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