1 /**
2 * @file
3 * @brief Geoscape/Map management
4 */
5
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17
18 See the GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23 */
24
25 #include "../../cl_shared.h"
26 #include "../../ui/ui_dataids.h"
27 #include "../../ui/node/ui_node_geoscape.h"
28 #include "cp_overlay.h"
29 #include "cp_campaign.h"
30 #include "cp_popup.h"
31 #include "cp_mapfightequip.h"
32 #include "cp_geoscape.h"
33 #include "cp_missions.h"
34 #include "cp_ufo.h"
35 #include "cp_time.h"
36 #include "cp_xvi.h"
37
38 static uiNode_t* geoscapeNode;
39
40 #ifdef DEBUG
41 static cvar_t* debug_showInterest;
42 #endif
43
44 #define GLOBE_ROTATE -90
45 #define ZOOM_LIMIT 2.5f
46
47 /*
48 ==============================================================
49 MULTI SELECTION DEFINITION
50 ==============================================================
51 */
52
53 #define MULTISELECT_MAXSELECT 6 /**< Maximal count of elements that can be selected at once */
54
55 /**
56 * @brief Types of elements that can be selected on the map
57 */
58 typedef enum {
59 MULTISELECT_TYPE_BASE,
60 MULTISELECT_TYPE_INSTALLATION,
61 MULTISELECT_TYPE_MISSION,
62 MULTISELECT_TYPE_AIRCRAFT,
63 MULTISELECT_TYPE_UFO,
64 MULTISELECT_TYPE_NONE
65 } multiSelectType_t;
66
67
68 /**
69 * @brief Structure to manage the multi selection
70 */
71 typedef struct multiSelect_s {
72 int nbSelect; /**< Count of currently selected elements */
73 multiSelectType_t selectType[MULTISELECT_MAXSELECT]; /**< Type of currently selected elements */
74 int selectId[MULTISELECT_MAXSELECT]; /**< Identifier of currently selected element */
75 char popupText[2048]; /**< Text to display in popup_multi_selection menu */
76 } multiSelect_t;
77
78 static multiSelect_t multiSelect; /**< Data to manage the multi selection */
79
80
81 /*
82 ==============================================================
83 STATIC DEFINITION
84 ==============================================================
85 */
86
87 /* Functions */
88 static bool GEO_IsPositionSelected(const uiNode_t* node, const vec2_t pos, int x, int y);
89
90 /* static variables */
91 static char textStandard[2048]; /**< Buffer to display standard text on the geoscape */
92 static int centerOnEventIdx; /**< Current Event centered on 3D geoscape */
93
94 /* Colors */
95 static const vec4_t green = {0.0f, 1.0f, 0.0f, 0.8f};
96 static const vec4_t yellow = {1.0f, 0.874f, 0.294f, 1.0f};
97 static const vec4_t red = {1.0f, 0.0f, 0.0f, 0.8f};
98
99 static const float defaultBaseAngle = -90.0f; /**< Default angle value for 3D models like bases */
100
101 static byte* terrainPic; /**< this is the terrain mask for separating the climate
102 * zone and water by different color values */
103 static int terrainWidth, terrainHeight; /**< the width and height for the terrain pic. */
104
105 static byte* culturePic; /**< this is the mask for separating the culture
106 * zone and water by different color values */
107 static int cultureWidth, cultureHeight; /**< the width and height for the culture pic. */
108
109 static byte* populationPic; /**< this is the mask for separating the population rate
110 * zone and water by different color values */
111 static int populationWidth, populationHeight; /**< the width and height for the population pic. */
112
113 static byte* nationsPic; /**< this is the nation mask - separated
114 * by colors given in nations.ufo. */
115 static int nationsWidth, nationsHeight; /**< the width and height for the nation pic. */
116
117
118 /*
119 ==============================================================
120 CLICK ON MAP and MULTI SELECTION FUNCTIONS
121 ==============================================================
122 */
123
124 /**
125 * @brief Add an element in the multiselection list
126 */
GEO_MultiSelectListAddItem(multiSelectType_t itemType,int itemID,const char * itemDescription,const char * itemName)127 static void GEO_MultiSelectListAddItem (multiSelectType_t itemType, int itemID,
128 const char* itemDescription, const char* itemName)
129 {
130 Q_strcat(multiSelect.popupText, sizeof(multiSelect.popupText), "%s\t%s\n", itemDescription, itemName);
131 multiSelect.selectType[multiSelect.nbSelect] = itemType;
132 multiSelect.selectId[multiSelect.nbSelect] = itemID;
133 multiSelect.nbSelect++;
134 }
135
136 /**
137 * @brief Execute action for 1 element of the multi selection
138 * Param cgi->Cmd_Argv(1) is the element selected in the popup_multi_selection menu
139 */
GEO_MultiSelectExecuteAction_f(void)140 static void GEO_MultiSelectExecuteAction_f (void)
141 {
142 int selected, id;
143 aircraft_t* aircraft;
144 bool multiSelection = false;
145
146 if (cgi->Cmd_Argc() < 2) {
147 /* Direct call from code, not from a popup menu */
148 selected = 0;
149 } else {
150 /* Call from a geoscape popup menu (popup_multi_selection) */
151 cgi->UI_PopWindow(false);
152 selected = atoi(cgi->Cmd_Argv(1));
153 multiSelection = true;
154 }
155
156 if (selected < 0 || selected >= multiSelect.nbSelect)
157 return;
158 id = multiSelect.selectId[selected];
159
160 /* Execute action on element */
161 switch (multiSelect.selectType[selected]) {
162 case MULTISELECT_TYPE_BASE: /* Select a base */
163 if (id >= B_GetCount())
164 break;
165 GEO_ResetAction();
166 B_SelectBase(B_GetFoundedBaseByIDX(id));
167 break;
168 case MULTISELECT_TYPE_INSTALLATION: {
169 /* Select an installation */
170 installation_t* ins = INS_GetByIDX(id);
171 if (ins) {
172 GEO_ResetAction();
173 INS_SelectInstallation(ins);
174 }
175 break;
176 }
177 case MULTISELECT_TYPE_MISSION: {
178 mission_t* mission = GEO_GetSelectedMission();
179 /* Select a mission */
180 if (ccs.mapAction == MA_INTERCEPT && mission && mission == MIS_GetByIdx(id)) {
181 CL_DisplayPopupInterceptMission(mission);
182 return;
183 }
184 mission = GEO_SelectMission(MIS_GetByIdx(id));
185 if (multiSelection) {
186 /* if we come from a multiSelection menu, there is no need to open this popup twice to go to a mission */
187 CL_DisplayPopupInterceptMission(mission);
188 return;
189 }
190 break;
191 }
192 case MULTISELECT_TYPE_AIRCRAFT: /* Selection of an aircraft */
193 aircraft = AIR_AircraftGetFromIDX(id);
194 if (aircraft == nullptr) {
195 Com_DPrintf(DEBUG_CLIENT, "GEO_MultiSelectExecuteAction: selection of an unknown aircraft idx %i\n", id);
196 return;
197 }
198
199 if (GEO_IsAircraftSelected(aircraft)) {
200 /* Selection of an already selected aircraft */
201 CL_DisplayPopupAircraft(aircraft); /* Display popup_aircraft */
202 } else {
203 /* Selection of an unselected aircraft */
204 GEO_SelectAircraft(aircraft);
205 if (multiSelection)
206 /* if we come from a multiSelection menu, there is no need to open this popup twice to choose an action */
207 CL_DisplayPopupAircraft(aircraft);
208 }
209 break;
210 case MULTISELECT_TYPE_UFO : /* Selection of a UFO */
211 /* Get the ufo selected */
212 if (id < 0 || id >= ccs.numUFOs)
213 return;
214 aircraft = UFO_GetByIDX(id);
215
216 if (GEO_IsUFOSelected(aircraft)) {
217 /* Selection of a already selected ufo */
218 CL_DisplayPopupInterceptUFO(aircraft);
219 } else {
220 /* Selection of a unselected ufo */
221 GEO_SelectUFO(aircraft);
222 if (multiSelection)
223 /* if we come from a multiSelection menu, there is no need to open this popup twice to choose an action */
224 CL_DisplayPopupInterceptUFO(GEO_GetSelectedUFO());
225 }
226 break;
227 case MULTISELECT_TYPE_NONE : /* Selection of an element that has been removed */
228 break;
229 default:
230 Com_DPrintf(DEBUG_CLIENT, "GEO_MultiSelectExecuteAction: selection of an unknown element type %i\n",
231 multiSelect.selectType[selected]);
232 break;
233 }
234 }
235
GEO_IsRadarOverlayActivated(void)236 bool GEO_IsRadarOverlayActivated (void)
237 {
238 return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_RADAR;
239 }
240
GEO_IsNationOverlayActivated(void)241 static inline bool GEO_IsNationOverlayActivated (void)
242 {
243 return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_NATION;
244 }
245
GEO_IsXVIOverlayActivated(void)246 static inline bool GEO_IsXVIOverlayActivated (void)
247 {
248 return cgi->Cvar_GetInteger("cl_geoscape_overlay") & OVERLAY_XVI;
249 }
250
251 /**
252 * @brief Click on the map/geoscape
253 * @param[in] node UI Node of the geoscape map
254 * @param[in] x Mouse click X coordinate
255 * @param[in] y Mouse click Y coordinate
256 * @param[in] pos Geoscape (longitude, latitude) coordinate of the click
257 * @return True if the event is used for something
258 */
GEO_Click(const uiNode_t * node,int x,int y,const vec2_t pos)259 bool GEO_Click (const uiNode_t* node, int x, int y, const vec2_t pos)
260 {
261 aircraft_t* ufo;
262 base_t* base;
263
264 switch (ccs.mapAction) {
265 case MA_NEWBASE:
266 /* new base construction */
267 /** @todo make this a function in cp_base.c - B_BuildBaseAtPos */
268 if (!MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr))) {
269 if (B_GetCount() < MAX_BASES) {
270 Vector2Copy(pos, ccs.newBasePos);
271 CP_GameTimeStop();
272 cgi->Cmd_ExecuteString("mn_set_base_title");
273 cgi->UI_PushWindow("popup_newbase");
274 return true;
275 }
276 return false;
277 }
278 break;
279 case MA_NEWINSTALLATION:
280 if (!MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr))) {
281 Vector2Copy(pos, ccs.newBasePos);
282 CP_GameTimeStop();
283 cgi->UI_PushWindow("popup_newinstallation");
284 return true;
285 }
286 break;
287 case MA_UFORADAR:
288 cgi->UI_PushWindow("popup_intercept_ufo");
289 break;
290 default:
291 break;
292 }
293
294 /* Init data for multi selection */
295 multiSelect.nbSelect = 0;
296 OBJZERO(multiSelect.popupText);
297 /* Get selected missions */
298 MIS_Foreach(tempMission) {
299 if (multiSelect.nbSelect >= MULTISELECT_MAXSELECT)
300 break;
301 if (tempMission->stage == STAGE_NOT_ACTIVE || !tempMission->onGeoscape)
302 continue;
303 if (tempMission->pos && GEO_IsPositionSelected(node, tempMission->pos, x, y))
304 GEO_MultiSelectListAddItem(MULTISELECT_TYPE_MISSION, MIS_GetIdx(tempMission), _("Mission"), MIS_GetName(tempMission));
305 }
306
307 /* Get selected aircraft which belong */
308 AIR_Foreach(aircraft) {
309 if (AIR_IsAircraftOnGeoscape(aircraft) && aircraft->fuel > 0 && GEO_IsPositionSelected(node, aircraft->pos, x, y))
310 GEO_MultiSelectListAddItem(MULTISELECT_TYPE_AIRCRAFT, aircraft->idx, _("Aircraft"), aircraft->name);
311 }
312
313 /* Get selected bases */
314 base = nullptr;
315 while ((base = B_GetNext(base)) != nullptr && multiSelect.nbSelect < MULTISELECT_MAXSELECT) {
316 if (GEO_IsPositionSelected(node, base->pos, x, y))
317 GEO_MultiSelectListAddItem(MULTISELECT_TYPE_BASE, base->idx, _("Base"), base->name);
318 }
319
320 /* Get selected installations */
321 INS_Foreach(installation) {
322 if (multiSelect.nbSelect >= MULTISELECT_MAXSELECT)
323 break;
324 if (GEO_IsPositionSelected(node, installation->pos, x, y))
325 GEO_MultiSelectListAddItem(MULTISELECT_TYPE_INSTALLATION, installation->idx, _("Installation"), installation->name);
326 }
327
328 /* Get selected ufos */
329 ufo = nullptr;
330 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr)
331 if (AIR_IsAircraftOnGeoscape(ufo) && GEO_IsPositionSelected(node, ufo->pos, x, y))
332 GEO_MultiSelectListAddItem(MULTISELECT_TYPE_UFO, UFO_GetGeoscapeIDX(ufo), _("UFO Sighting"), UFO_GetName(ufo));
333
334 if (multiSelect.nbSelect == 1) {
335 /* Execute directly action for the only one element selected */
336 cgi->Cmd_ExecuteString("multi_select_click");
337 return true;
338 } else if (multiSelect.nbSelect > 1) {
339 /* Display popup for multi selection */
340 cgi->UI_RegisterText(TEXT_MULTISELECTION, multiSelect.popupText);
341 CP_GameTimeStop();
342 cgi->UI_PushWindow("popup_multi_selection");
343 return true;
344 } else {
345 aircraft_t* aircraft = GEO_GetSelectedAircraft();
346 /* Nothing selected */
347 if (!aircraft) {
348 GEO_ResetAction();
349 return false;
350 }
351
352 if (AIR_IsAircraftOnGeoscape(aircraft) && AIR_AircraftHasEnoughFuel(aircraft, pos)) {
353 /* Move the selected aircraft to the position clicked */
354 GEO_CalcLine(aircraft->pos, pos, &aircraft->route);
355 aircraft->status = AIR_TRANSIT;
356 aircraft->aircraftTarget = nullptr;
357 aircraft->time = 0;
358 aircraft->point = 0;
359 return true;
360 }
361 }
362 return false;
363 }
364
365
366 /*
367 ==============================================================
368 GEOSCAPE DRAWING AND COORDINATES
369 ==============================================================
370 */
371
372 /**
373 * @brief Transform a 2D position on the map to screen coordinates.
374 * @param[in] node Menu node
375 * @param[in] pos vector that holds longitude and latitude
376 * @param[out] x normalized (rotated and scaled) x value of mouseclick
377 * @param[out] y normalized (rotated and scaled) y value of mouseclick
378 * @param[out] z z value of the given latitude and longitude - might also be nullptr if not needed
379 * @return true if the point is visible, false else (if it's outside the node or on the wrong side of the earth).
380 */
GEO_3DMapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y,int * z)381 static bool GEO_3DMapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y, int* z)
382 {
383 vec2_t mid;
384 vec3_t v, v1, rotationAxis;
385 const float radius = GLOBE_RADIUS;
386
387 PolarToVec(pos, v);
388
389 /* rotate the vector to switch of reference frame.
390 * We switch from the static frame of the earth to the local frame of the player */
391 VectorSet(rotationAxis, 0, 0, 1);
392 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
393 RotatePointAroundVector(v1, rotationAxis, v, - data.angles[PITCH]);
394
395 VectorSet(rotationAxis, 0, 1, 0);
396 RotatePointAroundVector(v, rotationAxis, v1, - data.angles[YAW]);
397
398 /* set mid to the coordinates of the center of the globe */
399 Vector2Set(mid, data.mapPos[0] + data.mapSize[0] / 2.0f, data.mapPos[1] + data.mapSize[1] / 2.0f);
400
401 /* We now convert those coordinates relative to the center of the globe to coordinates of the screen
402 * (which are relative to the upper left side of the screen) */
403 *x = (int) (mid[0] - radius * v[1]);
404 *y = (int) (mid[1] - radius * v[0]);
405
406 if (z)
407 *z = (int) (radius * v[2]);
408
409 /* if the point is on the wrong side of the earth, the player cannot see it */
410 if (v[2] > 0)
411 return false;
412
413 /* if the point is outside the screen, the player cannot see it */
414 if (*x < data.mapPos[0] && *y < data.mapPos[1]
415 && *x > data.mapPos[0] + data.mapSize[0]
416 && *y > data.mapPos[1] + data.mapSize[1])
417 return false;
418
419 return true;
420 }
421
422 /**
423 * @brief Transform a 2D position on the map to screen coordinates.
424 * @param[in] node Menu node
425 * @param[in] pos Position on the map described by longitude and latitude
426 * @param[out] x X coordinate on the screen
427 * @param[out] y Y coordinate on the screen
428 * @returns true if the screen position is within the boundaries of the menu
429 * node. Otherwise returns false.
430 */
GEO_MapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y)431 static bool GEO_MapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y)
432 {
433 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
434 /* get "raw" position */
435 float sx = pos[0] / 360 + data.center[0] - 0.5;
436
437 /* shift it on screen */
438 if (sx < -0.5f)
439 sx += 1.0f;
440 else if (sx > +0.5f)
441 sx -= 1.0f;
442
443 *x = data.mapPos[0] + 0.5f * data.mapSize[0] - sx * data.mapSize[0] * data.zoom;
444 *y = data.mapPos[1] + 0.5f * data.mapSize[1] - (pos[1] / 180.0f + data.center[1] - 0.5f) * data.mapSize[1] * data.zoom;
445
446 if (*x < data.mapPos[0] && *y < data.mapPos[1]
447 && *x > data.mapPos[0] + data.mapSize[0]
448 && *y > data.mapPos[1] + data.mapSize[1])
449 return false;
450 return true;
451 }
452
453 /**
454 * @brief Call either GEO_MapToScreen or GEO_3DMapToScreen depending on the geoscape you're using.
455 * @param[in] node Menu node
456 * @param[in] pos Position on the map described by longitude and latitude
457 * @param[out] x Pointer to the X coordinate on the screen
458 * @param[out] y Pointer to the Y coordinate on the screen
459 * @param[out] z Pointer to the Z coordinate on the screen (may be nullptr if not needed)
460 * @returns true if pos corresponds to a point which is visible on the screen. Otherwise returns false.
461 */
GEO_AllMapToScreen(const uiNode_t * node,const vec2_t pos,int * x,int * y,int * z)462 static bool GEO_AllMapToScreen (const uiNode_t* node, const vec2_t pos, int* x, int* y, int* z)
463 {
464 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
465 if (!data.flatgeoscape)
466 return GEO_3DMapToScreen(node, pos, x, y, z);
467
468 if (z)
469 *z = -10;
470 return GEO_MapToScreen(node, pos, x, y);
471 }
472
473 /**
474 * @brief maximum distance (in pixel) to get a valid mouse click
475 * @note this is for a 1024 * 768 screen
476 */
477 #define UI_MAP_DIST_SELECTION 15
478 /**
479 * @brief Tell if the specified position is considered clicked
480 */
GEO_IsPositionSelected(const uiNode_t * node,const vec2_t pos,int x,int y)481 static bool GEO_IsPositionSelected (const uiNode_t* node, const vec2_t pos, int x, int y)
482 {
483 int msx, msy;
484
485 if (GEO_AllMapToScreen(node, pos, &msx, &msy, nullptr))
486 if (x >= msx - UI_MAP_DIST_SELECTION && x <= msx + UI_MAP_DIST_SELECTION
487 && y >= msy - UI_MAP_DIST_SELECTION && y <= msy + UI_MAP_DIST_SELECTION)
488 return true;
489
490 return false;
491 }
492
493 /**
494 * @brief Draws a 3D marker on geoscape if the player can see it.
495 * @param[in] node Menu node.
496 * @param[in] pos Longitude and latitude of the marker to draw.
497 * @param[in] theta Angle (degree) of the model to the horizontal.
498 * @param[in] model The name of the model of the marker.
499 * @param[in] skin Number of modelskin to draw on marker
500 */
GEO_Draw3DMarkerIfVisible(const uiNode_t * node,const vec2_t pos,float theta,const char * model,int skin)501 static void GEO_Draw3DMarkerIfVisible (const uiNode_t* node, const vec2_t pos, float theta, const char* model, int skin)
502 {
503 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
504 if (data.flatgeoscape) {
505 int x, y;
506 vec3_t screenPos;
507
508 GEO_AllMapToScreen(node, pos, &x, &y, nullptr);
509 VectorSet(screenPos, x, y, 0);
510 /* models are used on 2D geoscape for aircraft */
511 cgi->R_Draw2DMapMarkers(screenPos, theta, model, skin);
512 } else {
513 cgi->R_Draw3DMapMarkers(data.mapPos, data.mapSize, data.angles, pos, theta, GLOBE_RADIUS, model, skin);
514 }
515 }
516
517 /**
518 * @brief Calculate the shortest way to go from start to end on a sphere
519 * @param[in] start The point you start from
520 * @param[in] end The point you go to
521 * @param[out] line Contains the shortest path to go from start to end
522 * @sa GEO_MapDrawLine
523 */
GEO_CalcLine(const vec2_t start,const vec2_t end,mapline_t * line)524 void GEO_CalcLine (const vec2_t start, const vec2_t end, mapline_t* line)
525 {
526 vec3_t s, e, v;
527 vec3_t normal;
528 vec2_t trafo, sa, ea;
529 float cosTrafo, sinTrafo;
530 float phiStart, phiEnd, dPhi, phi;
531 float* p;
532 int i, n;
533
534 /* get plane normal */
535 PolarToVec(start, s);
536 PolarToVec(end, e);
537 /* Procedure below won't work if start is the same like end */
538 if (VectorEqual(s, e)) {
539 line->distance = 0;
540 line->numPoints = 2;
541 Vector2Set(line->point[0], end[0], end[1]);
542 Vector2Set(line->point[1], end[0], end[1]);
543 return;
544 }
545
546 CrossProduct(s, e, normal);
547 VectorNormalize(normal);
548
549 /* get transformation */
550 VecToPolar(normal, trafo);
551 cosTrafo = cos(trafo[1] * torad);
552 sinTrafo = sin(trafo[1] * torad);
553
554 sa[0] = start[0] - trafo[0];
555 sa[1] = start[1];
556 PolarToVec(sa, s);
557 ea[0] = end[0] - trafo[0];
558 ea[1] = end[1];
559 PolarToVec(ea, e);
560
561 phiStart = atan2(s[1], cosTrafo * s[2] - sinTrafo * s[0]);
562 phiEnd = atan2(e[1], cosTrafo * e[2] - sinTrafo * e[0]);
563
564 /* get waypoints */
565 if (phiEnd < phiStart - M_PI)
566 phiEnd += 2 * M_PI;
567 if (phiEnd > phiStart + M_PI)
568 phiEnd -= 2 * M_PI;
569
570 n = (phiEnd - phiStart) / M_PI * LINE_MAXSEG;
571 if (n > 0)
572 n = n + 1;
573 else
574 n = -n + 1;
575
576 line->distance = fabs(phiEnd - phiStart) / n * todeg;
577 line->numPoints = n + 1;
578 /* make sure we do not exceed route array size */
579 assert(line->numPoints <= LINE_MAXPTS);
580 dPhi = (phiEnd - phiStart) / n;
581 p = nullptr;
582 for (phi = phiStart, i = 0; i <= n; phi += dPhi, i++) {
583 const float* last = p;
584 p = line->point[i];
585 VectorSet(v, -sinTrafo * cos(phi), sin(phi), cosTrafo * cos(phi));
586 VecToPolar(v, p);
587 p[0] += trafo[0];
588
589 if (!last) {
590 while (p[0] < -180.0)
591 p[0] += 360.0;
592 while (p[0] > +180.0)
593 p[0] -= 360.0;
594 } else {
595 while (p[0] - last[0] > +180.0)
596 p[0] -= 360.0;
597 while (p[0] - last[0] < -180.0)
598 p[0] += 360.0;
599 }
600 }
601 }
602
603 /**
604 * @brief Draw a path on a menu node (usually the 2D geoscape map)
605 * @param[in] node The menu node which will be used for drawing dimensions.
606 * This is usually the geoscape menu node.
607 * @param[in] line The path which is to be drawn
608 * @sa GEO_CalcLine
609 */
GEO_MapDrawLine(const uiNode_t * node,const mapline_t * line)610 static void GEO_MapDrawLine (const uiNode_t* node, const mapline_t* line)
611 {
612 const vec4_t color = {1, 0.5, 0.5, 1};
613 screenPoint_t pts[LINE_MAXPTS];
614 screenPoint_t* p;
615 int i, start, old;
616
617 /* draw */
618 cgi->R_Color(color);
619 start = 0;
620 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
621 old = data.mapSize[0] / 2;
622 for (i = 0, p = pts; i < line->numPoints; i++, p++) {
623 GEO_MapToScreen(node, line->point[i], &p->x, &p->y);
624
625 /* If we cross longitude 180 degree (right/left edge of the screen), draw the first part of the path */
626 if (i > start && abs(p->x - old) > data.mapSize[0] / 2) {
627 /* shift last point */
628 int diff;
629
630 if (p->x - old > data.mapSize[0] / 2)
631 diff = -data.mapSize[0] * data.zoom;
632 else
633 diff = data.mapSize[0] * data.zoom;
634 p->x += diff;
635
636 /* wrap around screen border */
637 cgi->R_DrawLineStrip(i - start, (int*)(&pts));
638
639 /* first path of the path is drawn, now we begin the second part of the path */
640 /* shift first point, continue drawing */
641 start = i;
642 pts[0].x = p[-1].x - diff;
643 pts[0].y = p[-1].y;
644 p = pts;
645 }
646 old = p->x;
647 }
648
649 cgi->R_DrawLineStrip(i - start, (int*)(&pts));
650 cgi->R_Color(nullptr);
651 }
652
653 /**
654 * @brief Draw a path on a menu node (usually the 3Dgeoscape map)
655 * @param[in] node The menu node which will be used for drawing dimensions.
656 * This is usually the 3Dgeoscape menu node.
657 * @param[in] line The path which is to be drawn
658 * @sa GEO_CalcLine
659 */
GEO_3DMapDrawLine(const uiNode_t * node,const mapline_t * line)660 static void GEO_3DMapDrawLine (const uiNode_t* node, const mapline_t* line)
661 {
662 const vec4_t color = {1, 0.5, 0.5, 1};
663 screenPoint_t pts[LINE_MAXPTS];
664 int numPoints, start;
665
666 start = 0;
667 numPoints = 0;
668
669 /* draw only when the point of the path is visible */
670 cgi->R_Color(color);
671 for (int i = 0; i < line->numPoints; i++) {
672 if (GEO_3DMapToScreen(node, line->point[i], &pts[i].x, &pts[i].y, nullptr))
673 numPoints++;
674 else if (!numPoints)
675 /* the point which is not drawn is at the beginning of the path */
676 start++;
677 }
678
679 cgi->R_DrawLineStrip(numPoints, (int*)(&pts[start]));
680 cgi->R_Color(nullptr);
681 }
682
683 #define CIRCLE_DRAW_POINTS 60
684 /**
685 * @brief Draw equidistant points from a given point on a menu node
686 * @param[in] node The menu node which will be used for drawing dimensions.
687 * This is usually the geoscape menu node.
688 * @param[in] center The latitude and longitude of center point
689 * @param[in] angle The angle defining the distance of the equidistant points to center
690 * @param[in] color The color for drawing
691 * @sa RADAR_DrawCoverage
692 */
GEO_MapDrawEquidistantPoints(const uiNode_t * node,const vec2_t center,const float angle,const vec4_t color)693 static void GEO_MapDrawEquidistantPoints (const uiNode_t* node, const vec2_t center, const float angle, const vec4_t color)
694 {
695 int i, xCircle, yCircle;
696 screenPoint_t pts[CIRCLE_DRAW_POINTS + 1];
697 vec2_t posCircle;
698 bool oldDraw = false;
699 int numPoints = 0;
700 vec3_t initialVector, rotationAxis, currentPoint, centerPos;
701
702 cgi->R_Color(color);
703
704 /* Set centerPos corresponding to cartesian coordinates of the center point */
705 PolarToVec(center, centerPos);
706
707 /* Find a perpendicular vector to centerPos, and rotate centerPos around it to obtain one point distant of angle from centerPos */
708 PerpendicularVector(rotationAxis, centerPos);
709 RotatePointAroundVector(initialVector, rotationAxis, centerPos, angle);
710
711 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
712 /* Now, each equidistant point is given by a rotation around centerPos */
713 for (i = 0; i <= CIRCLE_DRAW_POINTS; i++) {
714 bool draw = false;
715 const float degrees = i * 360.0f / (float)CIRCLE_DRAW_POINTS;
716 RotatePointAroundVector(currentPoint, centerPos, initialVector, degrees);
717 VecToPolar(currentPoint, posCircle);
718 if (GEO_AllMapToScreen(node, posCircle, &xCircle, &yCircle, nullptr)) {
719 draw = true;
720 if (data.flatgeoscape && numPoints != 0 && abs(pts[numPoints - 1].x - xCircle) > 512)
721 oldDraw = false;
722 }
723
724 /* if moving from a point of the screen to a distant one, draw the path we already calculated, and begin a new path
725 * (to avoid unwanted lines) */
726 if (draw != oldDraw && i != 0) {
727 cgi->R_DrawLineStrip(numPoints, (int*)(&pts));
728 numPoints = 0;
729 }
730 /* if the current point is to be drawn, add it to the path */
731 if (draw) {
732 pts[numPoints].x = xCircle;
733 pts[numPoints].y = yCircle;
734 numPoints++;
735 }
736 /* update value of oldDraw */
737 oldDraw = draw;
738 }
739
740 /* Draw the last path */
741 cgi->R_DrawLineStrip(numPoints, (int*)(&pts));
742 cgi->R_Color(nullptr);
743 }
744
745 /**
746 * @brief Return the angle of a model given its position and destination, on 3D geoscape.
747 * @param[in] start Latitude and longitude of the position of the model.
748 * @param[in] end Latitude and longitude of aimed point.
749 * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
750 * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
751 * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
752 */
GEO_AngleOfPath3D(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)753 static float GEO_AngleOfPath3D (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
754 {
755 vec3_t start3D, end3D, north3D, ortToDest, ortToPole, v;
756 const vec2_t northPole = {0.0f, 90.0f}; /**< Position of the north pole (used to know where the 'up' side is) */
757
758 PolarToVec(start, start3D);
759 PolarToVec(end, end3D);
760 PolarToVec(northPole, north3D);
761
762 /* calculate the vector othogonal to movement */
763 CrossProduct(start3D, end3D, ortToDest);
764 VectorNormalize(ortToDest);
765 if (ortVector) {
766 VectorCopy(ortToDest, ortVector);
767 }
768
769 /* calculate the vector othogonal to north pole (from model location) */
770 CrossProduct(start3D, north3D, ortToPole);
771 VectorNormalize(ortToPole);
772
773 /**
774 * @todo Save the value angle instead of direction: we don't need a vector here,
775 * we could just compare angle to current angle.
776 */
777 /* smooth change of direction if the model is not idle */
778 if (direction) {
779 VectorSubtract(ortToDest, direction, v);
780 const float dist = VectorLength(v);
781 if (dist > 0.01) {
782 vec3_t rotationAxis;
783 CrossProduct(direction, ortToDest, rotationAxis);
784 VectorNormalize(rotationAxis);
785 RotatePointAroundVector(v, rotationAxis, direction, 5.0);
786 VectorCopy(v, direction);
787 VectorSubtract(ortToDest, direction, v);
788 if (VectorLength(v) < dist)
789 VectorCopy(direction, ortToDest);
790 else
791 VectorCopy(ortToDest, direction);
792 }
793 }
794
795 /* calculate the angle the model is making at earth surface with north pole direction */
796 float angle = todeg * acos(DotProduct(ortToDest, ortToPole));
797 /* with arcos, you only get the absolute value of the angle: get the sign */
798 CrossProduct(ortToDest, ortToPole, v);
799 if (DotProduct(start3D, v) < 0)
800 angle = - angle;
801
802 return angle;
803 }
804
805 /**
806 * @brief Return the angle of a model given its position and destination, on 2D geoscape.
807 * @param[in] start Latitude and longitude of the position of the model.
808 * @param[in] end Latitude and longitude of aimed point.
809 * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
810 * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
811 * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
812 */
GEO_AngleOfPath2D(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)813 static float GEO_AngleOfPath2D (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
814 {
815 vec3_t start3D, end3D, tangentVector, v, rotationAxis;
816
817 /* calculate the vector tangent to movement */
818 PolarToVec(start, start3D);
819 PolarToVec(end, end3D);
820 if (ortVector) {
821 CrossProduct(start3D, end3D, ortVector);
822 VectorNormalize(ortVector);
823 CrossProduct(ortVector, start3D, tangentVector);
824 } else {
825 CrossProduct(start3D, end3D, v);
826 CrossProduct(v, start3D, tangentVector);
827 }
828 VectorNormalize(tangentVector);
829
830 /* smooth change of direction if the model is not idle */
831 if (direction) {
832 VectorSubtract(tangentVector, direction, v);
833 const float dist = VectorLength(v);
834 if (dist > 0.01) {
835 CrossProduct(direction, tangentVector, rotationAxis);
836 VectorNormalize(rotationAxis);
837 RotatePointAroundVector(v, rotationAxis, direction, 5.0);
838 VectorSubtract(tangentVector, direction, v);
839 if (VectorLength(v) < dist)
840 VectorCopy(direction, tangentVector);
841 else
842 VectorCopy(tangentVector, direction);
843 }
844 }
845
846 VectorSet(rotationAxis, 0, 0, 1);
847 RotatePointAroundVector(v, rotationAxis, tangentVector, - start[0]);
848 VectorSet(rotationAxis, 0, 1, 0);
849 RotatePointAroundVector(tangentVector, rotationAxis, v, start[1] + 90.0f);
850
851 /* calculate the orientation angle of the model around axis perpendicular to the screen */
852 float angle = todeg * atan(tangentVector[0] / tangentVector[1]);
853 if (tangentVector[1] > 0)
854 angle -= 90.0f;
855 else
856 angle += 90.0f;
857
858 return angle;
859 }
860
861 /**
862 * @brief Select which function should be used for calculating the direction of model on 2D or 3D geoscape.
863 * @param[in] start Latitude and longitude of the position of the model.
864 * @param[in] end Latitude and longitude of aimed point.
865 * @param[in] direction vec3_t giving current direction of the model (nullptr if the model is idle).
866 * @param[out] ortVector If not nullptr, this will be filled with the normalized vector around which rotation allows to go toward @c direction.
867 * @return Angle (degrees) of rotation around the radius axis of earth for @c start going toward @c end. Zero value is the direction of North pole.
868 */
GEO_AngleOfPath(const vec2_t start,const vec2_t end,vec3_t direction,vec3_t ortVector)869 float GEO_AngleOfPath (const vec2_t start, const vec2_t end, vec3_t direction, vec3_t ortVector)
870 {
871 uiNode_t* node = geoscapeNode;
872 if (!node)
873 return 0.0f;
874
875 const mapExtraData_t &data = UI_MAPEXTRADATA(node);
876 if (!data.flatgeoscape)
877 return GEO_AngleOfPath3D(start, end, direction, ortVector);
878 return GEO_AngleOfPath2D(start, end, direction, ortVector);
879 }
880
881 /**
882 * @brief Will set the vector for the geoscape position
883 * @param[in] flatgeoscape True for 2D geoscape
884 * @param[out] vector The output vector. A two-dim vector for the flat geoscape, and a three-dim vector for the 3d geoscape
885 * @param[in] objectPos The position vector of the object to transform.
886 */
GEO_ConvertObjectPositionToGeoscapePosition(bool flatgeoscape,float * vector,const vec2_t objectPos)887 static void GEO_ConvertObjectPositionToGeoscapePosition (bool flatgeoscape, float* vector, const vec2_t objectPos)
888 {
889 if (flatgeoscape)
890 Vector2Set(vector, objectPos[0], objectPos[1]);
891 else
892 VectorSet(vector, objectPos[0], -objectPos[1], 0);
893 }
894
895 /**
896 * @brief center to a mission
897 */
GEO_GetMissionAngle(bool flatgeoscape,float * vector,int id)898 static void GEO_GetMissionAngle (bool flatgeoscape, float* vector, int id)
899 {
900 mission_t* mission = MIS_GetByIdx(id);
901 if (mission == nullptr)
902 return;
903 GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, mission->pos);
904 GEO_SelectMission(mission);
905 }
906
907 /**
908 * @brief center to a ufo
909 */
GEO_GetUFOAngle(bool flatgeoscape,float * vector,int idx)910 static void GEO_GetUFOAngle (bool flatgeoscape, float* vector, int idx)
911 {
912 aircraft_t* ufo;
913
914 /* Cycle through UFOs (only those visible on geoscape) */
915 ufo = nullptr;
916 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
917 if (ufo->idx != idx)
918 continue;
919 GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, ufo->pos);
920 GEO_SelectUFO(ufo);
921 return;
922 }
923 }
924
925
926 /**
927 * @brief Start center to the selected point
928 */
GEO_StartCenter(uiNode_t * node)929 static void GEO_StartCenter (uiNode_t* node)
930 {
931 mapExtraData_t &data = UI_MAPEXTRADATA(node);
932 if (data.flatgeoscape) {
933 /* case 2D geoscape */
934 vec2_t diff;
935
936 Vector2Set(data.smoothFinal2DGeoscapeCenter, 0.5f - data.smoothFinal2DGeoscapeCenter[0] / 360.0f,
937 0.5f - data.smoothFinal2DGeoscapeCenter[1] / 180.0f);
938 if (data.smoothFinal2DGeoscapeCenter[1] < 0.5 / ZOOM_LIMIT)
939 data.smoothFinal2DGeoscapeCenter[1] = 0.5 / ZOOM_LIMIT;
940 if (data.smoothFinal2DGeoscapeCenter[1] > 1.0 - 0.5 / ZOOM_LIMIT)
941 data.smoothFinal2DGeoscapeCenter[1] = 1.0 - 0.5 / ZOOM_LIMIT;
942 diff[0] = data.smoothFinal2DGeoscapeCenter[0] - data.center[0];
943 diff[1] = data.smoothFinal2DGeoscapeCenter[1] - data.center[1];
944 data.smoothDeltaLength = sqrt(diff[0] * diff[0] + diff[1] * diff[1]);
945 } else {
946 /* case 3D geoscape */
947 vec3_t diff;
948
949 data.smoothFinalGlobeAngle[1] += GLOBE_ROTATE;
950 VectorSubtract(data.smoothFinalGlobeAngle, data.angles, diff);
951 data.smoothDeltaLength = VectorLength(diff);
952 }
953
954 data.smoothFinalZoom = ZOOM_LIMIT;
955 data.smoothDeltaZoom = fabs(data.smoothFinalZoom - data.zoom);
956 data.smoothRotation = true;
957 }
958
959 /**
960 * @brief Start to rotate or shift the globe to the given position
961 * @param[in] pos Longitude and latitude of the position to center on
962 */
GEO_CenterPosition(const vec2_t pos)963 void GEO_CenterPosition (const vec2_t pos)
964 {
965 uiNode_t* node = geoscapeNode;
966 if (!node)
967 return;
968 mapExtraData_t &data = UI_MAPEXTRADATA(node);
969 const bool flatgeoscape = data.flatgeoscape;
970 float* vector;
971 if (flatgeoscape)
972 vector = data.smoothFinal2DGeoscapeCenter;
973 else
974 vector = data.smoothFinalGlobeAngle;
975
976 GEO_ConvertObjectPositionToGeoscapePosition(flatgeoscape, vector, pos);
977 GEO_StartCenter(node);
978 }
979
980 /**
981 * @brief Center the view and select an object from the geoscape
982 */
GEO_SelectObject_f(void)983 static void GEO_SelectObject_f (void)
984 {
985 uiNode_t* node = geoscapeNode;
986 if (!node)
987 return;
988
989 if (cgi->Cmd_Argc() != 3) {
990 Com_Printf("Usage: %s <mission|ufo> <id>\n", cgi->Cmd_Argv(0));
991 return;
992 }
993
994 const char* type = cgi->Cmd_Argv(1);
995 const int idx = atoi(cgi->Cmd_Argv(2));
996 mapExtraData_t &data = UI_MAPEXTRADATA(node);
997 const bool flatgeoscape = data.flatgeoscape;
998
999 float* vector;
1000 if (flatgeoscape)
1001 vector = data.smoothFinal2DGeoscapeCenter;
1002 else
1003 vector = data.smoothFinalGlobeAngle;
1004
1005 if (Q_streq(type, "mission"))
1006 GEO_GetMissionAngle(flatgeoscape, vector, idx);
1007 else if (Q_streq(type, "ufo"))
1008 GEO_GetUFOAngle(flatgeoscape, vector, idx);
1009 else {
1010 Com_Printf("GEO_SelectObject_f: type %s unsupported.", type);
1011 return;
1012 }
1013 GEO_StartCenter(node);
1014 }
1015
1016 /**
1017 * @brief Returns position of the model corresponding to centerOnEventIdx.
1018 * @param[out] pos the position of the object
1019 */
GEO_GetGeoscapeAngle(vec2_t pos)1020 static void GEO_GetGeoscapeAngle (vec2_t pos)
1021 {
1022 int counter = 0;
1023 int maxEventIdx;
1024 const int numMissions = CP_CountMissionOnGeoscape();
1025 aircraft_t* ufo;
1026 base_t* base;
1027 int numBases = B_GetCount();
1028
1029 /* If the value of maxEventIdx is too big or to low, restart from beginning */
1030 maxEventIdx = numMissions + numBases + INS_GetCount() - 1;
1031 base = nullptr;
1032 while ((base = B_GetNext(base)) != nullptr) {
1033 AIR_ForeachFromBase(aircraft, base) {
1034 if (AIR_IsAircraftOnGeoscape(aircraft))
1035 maxEventIdx++;
1036 }
1037 }
1038 ufo = nullptr;
1039 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr)
1040 maxEventIdx++;
1041
1042 /* if there's nothing to center the view on, just go to 0,0 pos */
1043 if (maxEventIdx < 0) {
1044 Vector2Copy(vec2_origin, pos);
1045 return;
1046 }
1047
1048 /* check centerOnEventIdx is within the bounds */
1049 if (centerOnEventIdx < 0)
1050 centerOnEventIdx = maxEventIdx;
1051 if (centerOnEventIdx > maxEventIdx)
1052 centerOnEventIdx = 0;
1053
1054 /* Cycle through missions */
1055 if (centerOnEventIdx < numMissions) {
1056 MIS_Foreach(mission) {
1057 if (!mission->onGeoscape)
1058 continue;
1059 if (counter == centerOnEventIdx) {
1060 Vector2Copy(mission->pos, pos);
1061 GEO_SelectMission(mission);
1062 return;
1063 }
1064 counter++;
1065 }
1066 }
1067 counter = numMissions;
1068
1069 /* Cycle through bases */
1070 if (centerOnEventIdx < numBases + counter) {
1071 base = nullptr;
1072 while ((base = B_GetNext(base)) != nullptr) {
1073 if (counter == centerOnEventIdx) {
1074 Vector2Copy(base->pos, pos);
1075 return;
1076 }
1077 counter++;
1078 }
1079 }
1080 counter += numBases;
1081
1082 /* Cycle through installations */
1083 if (centerOnEventIdx < INS_GetCount() + counter) {
1084 INS_Foreach(inst) {
1085 if (counter == centerOnEventIdx) {
1086 Vector2Copy(inst->pos, pos);
1087 return;
1088 }
1089 counter++;
1090 }
1091 }
1092 counter += INS_GetCount();
1093
1094 /* Cycle through aircraft (only those present on geoscape) */
1095 AIR_Foreach(aircraft) {
1096 if (AIR_IsAircraftOnGeoscape(aircraft)) {
1097 if (centerOnEventIdx == counter) {
1098 Vector2Copy(aircraft->pos, pos);
1099 GEO_SelectAircraft(aircraft);
1100 return;
1101 }
1102 counter++;
1103 }
1104 }
1105
1106 /* Cycle through UFOs (only those visible on geoscape) */
1107 ufo = nullptr;
1108 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1109 if (centerOnEventIdx == counter) {
1110 Vector2Copy(ufo->pos, pos);
1111 GEO_SelectUFO(ufo);
1112 return;
1113 }
1114 counter++;
1115 }
1116 }
1117
1118 /**
1119 * @brief Switch to next model on 2D and 3D geoscape.
1120 * @note Set @c smoothRotation to @c true to allow a smooth rotation in GEO_Draw.
1121 * @note This function sets the value of smoothFinalGlobeAngle (for 3D) or smoothFinal2DGeoscapeCenter (for 2D),
1122 * which contains the final value that ccs.angles or ccs.centre must respectively take.
1123 */
GEO_CenterOnPoint_f(void)1124 void GEO_CenterOnPoint_f (void)
1125 {
1126 if (!Q_streq(cgi->UI_GetActiveWindowName(), "geoscape"))
1127 return;
1128
1129 centerOnEventIdx++;
1130
1131 uiNode_t* node = geoscapeNode;
1132 if (!node)
1133 return;
1134
1135 vec2_t pos;
1136 GEO_GetGeoscapeAngle(pos);
1137 GEO_CenterPosition(pos);
1138 }
1139
1140 #define BULLET_SIZE 1
1141 /**
1142 * @brief Draws a bunch of bullets on the geoscape map
1143 * @param[in] node Pointer to the node in which you want to draw the bullets.
1144 * @param[in] pos
1145 * @sa GEO_Draw
1146 */
GEO_DrawBullets(const uiNode_t * node,const vec3_t pos)1147 static void GEO_DrawBullets (const uiNode_t* node, const vec3_t pos)
1148 {
1149 int x, y;
1150
1151 if (GEO_AllMapToScreen(node, pos, &x, &y, nullptr))
1152 cgi->R_DrawFill(x, y, BULLET_SIZE, BULLET_SIZE, yellow);
1153 }
1154
1155 /**
1156 * @brief Draws a energy beam on the geoscape map (laser/particle)
1157 * @param[in] node Pointer to the node in which you want to draw.
1158 * @param[in] start Start position of the shot (on geoscape)
1159 * @param[in] end End position of the shot (on geoscape)
1160 * @param[in] color color of the beam
1161 * @sa GEO_Draw
1162 */
GEO_DrawBeam(const uiNode_t * node,const vec3_t start,const vec3_t end,const vec4_t color)1163 static void GEO_DrawBeam (const uiNode_t* node, const vec3_t start, const vec3_t end, const vec4_t color)
1164 {
1165 int points[4];
1166
1167 if (!GEO_AllMapToScreen(node, start, &(points[0]), &(points[1]), nullptr))
1168 return;
1169 if (!GEO_AllMapToScreen(node, end, &(points[2]), &(points[3]), nullptr))
1170 return;
1171
1172 cgi->R_Color(color);
1173 cgi->R_DrawLine(points, 2.0);
1174 cgi->R_Color(nullptr);
1175 }
1176
GEO_RenderImage(int x,int y,const char * image)1177 static inline void GEO_RenderImage (int x, int y, const char* image)
1178 {
1179 cgi->R_DrawImageCentered(x, y, image);
1180 }
1181
1182 #define SELECT_CIRCLE_RADIUS 1.5f + 3.0f / UI_MAPEXTRADATACONST(node).zoom
1183
1184 /**
1185 * @brief Draws one mission on the geoscape map (2D and 3D)
1186 * @param[in] node The menu node which will be used for drawing markers.
1187 * @param[in] mission Pointer to the mission to draw.
1188 */
GEO_DrawMapOneMission(const uiNode_t * node,const mission_t * mission)1189 static void GEO_DrawMapOneMission (const uiNode_t* node, const mission_t* mission)
1190 {
1191 int x, y;
1192 const bool isCurrentSelectedMission = GEO_IsMissionSelected(mission);
1193
1194 if (isCurrentSelectedMission)
1195 cgi->Cvar_Set("mn_mapdaytime", GEO_IsNight(mission->pos) ? _("Night") : _("Day"));
1196
1197 if (!GEO_AllMapToScreen(node, mission->pos, &x, &y, nullptr))
1198 return;
1199
1200 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1201 if (isCurrentSelectedMission) {
1202 /* Draw circle around the mission */
1203 if (data.flatgeoscape) {
1204 if (mission->active) {
1205 GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1206 } else {
1207 GEO_RenderImage(x, y, "pics/geoscape/circle");
1208 }
1209 } else {
1210 if (!mission->active)
1211 GEO_MapDrawEquidistantPoints(node, mission->pos, SELECT_CIRCLE_RADIUS, yellow);
1212 }
1213 }
1214
1215 /* Draw mission model (this must be called after drawing the selection circle so that the model is rendered on top of it)*/
1216 if (!data.flatgeoscape) {
1217 GEO_RenderImage(x, y, "pics/geoscape/mission");
1218 } else {
1219 GEO_Draw3DMarkerIfVisible(node, mission->pos, defaultBaseAngle, MIS_GetModel(mission), 0);
1220 }
1221
1222 cgi->UI_DrawString("f_verysmall", ALIGN_UL, x + 10, y, MIS_GetName(mission));
1223 }
1224
1225 /**
1226 * @brief Draw only the "wire" Radar coverage.
1227 * @param[in] node The menu node where radar coverage will be drawn.
1228 * @param[in] radar Pointer to the radar that will be drawn.
1229 * @param[in] pos Position of the radar.
1230 */
GEO_DrawRadarLineCoverage(const uiNode_t * node,const radar_t * radar,const vec2_t pos)1231 static void GEO_DrawRadarLineCoverage (const uiNode_t* node, const radar_t* radar, const vec2_t pos)
1232 {
1233 const vec4_t color = {1., 1., 1., .4};
1234 GEO_MapDrawEquidistantPoints(node, pos, radar->range, color);
1235 GEO_MapDrawEquidistantPoints(node, pos, radar->trackingRange, color);
1236 }
1237
1238 /**
1239 * @brief Draw only the "wire" part of the radar coverage in geoscape
1240 * @param[in] node The menu node where radar coverage will be drawn.
1241 * @param[in] radar Pointer to the radar that will be drawn.
1242 * @param[in] pos Position of the radar.
1243 */
GEO_DrawRadarInMap(const uiNode_t * node,const radar_t * radar,const vec2_t pos)1244 static void GEO_DrawRadarInMap (const uiNode_t* node, const radar_t* radar, const vec2_t pos)
1245 {
1246 /* Show radar range zones */
1247 GEO_DrawRadarLineCoverage(node, radar, pos);
1248
1249 /* everything below is drawn only if there is at least one detected UFO */
1250 if (!radar->numUFOs)
1251 return;
1252
1253 /* Draw lines from radar to ufos sensored */
1254 int x, y;
1255 const bool display = GEO_AllMapToScreen(node, pos, &x, &y, nullptr);
1256 if (!display)
1257 return;
1258
1259 screenPoint_t pts[2];
1260 pts[0].x = x;
1261 pts[0].y = y;
1262
1263 /* Set color */
1264 const vec4_t color = {1., 1., 1., .3};
1265 cgi->R_Color(color);
1266 for (int i = 0; i < radar->numUFOs; i++) {
1267 const aircraft_t* ufo = radar->ufos[i];
1268 if (UFO_IsUFOSeenOnGeoscape(ufo) && GEO_AllMapToScreen(node, ufo->pos, &x, &y, nullptr)) {
1269 pts[1].x = x;
1270 pts[1].y = y;
1271 cgi->R_DrawLineStrip(2, (int*)pts);
1272 }
1273 }
1274 cgi->R_Color(nullptr);
1275 }
1276
1277 /**
1278 * @brief Draws one installation on the geoscape map (2D and 3D)
1279 * @param[in] node The menu node which will be used for drawing markers.
1280 * @param[in] installation Pointer to the installation to draw.
1281 * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1282 * @param[in] font Default font.
1283 * @pre installation is not nullptr.
1284 */
GEO_DrawMapOneInstallation(const uiNode_t * node,const installation_t * installation,bool oneUFOVisible,const char * font)1285 static void GEO_DrawMapOneInstallation (const uiNode_t* node, const installation_t* installation,
1286 bool oneUFOVisible, const char* font)
1287 {
1288 const installationTemplate_t* tpl = installation->installationTemplate;
1289 int x, y;
1290
1291 /* Draw weapon range if at least one UFO is visible */
1292 if (oneUFOVisible && AII_InstallationCanShoot(installation)) {
1293 int i;
1294 for (i = 0; i < tpl->maxBatteries; i++) {
1295 const aircraftSlot_t* slot = &installation->batteries[i].slot;
1296 if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1297 GEO_MapDrawEquidistantPoints(node, installation->pos,
1298 slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1299 }
1300 }
1301 }
1302
1303 /* Draw installation radar (only the "wire" style part) */
1304 if (GEO_IsRadarOverlayActivated())
1305 GEO_DrawRadarInMap(node, &installation->radar, installation->pos);
1306
1307 /* Draw installation */
1308 if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
1309 GEO_Draw3DMarkerIfVisible(node, installation->pos, defaultBaseAngle, tpl->model, 0);
1310 } else if (GEO_MapToScreen(node, installation->pos, &x, &y)) {
1311 GEO_RenderImage(x, y, tpl->image);
1312 }
1313
1314 /* Draw installation names */
1315 if (GEO_AllMapToScreen(node, installation->pos, &x, &y, nullptr))
1316 cgi->UI_DrawString(font, ALIGN_UL, x, y + 10, installation->name);
1317 }
1318
1319 /**
1320 * @brief Draws one base on the geoscape map (2D and 3D)
1321 * @param[in] node The menu node which will be used for drawing markers.
1322 * @param[in] base Pointer to the base to draw.
1323 * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1324 * @param[in] font Default font.
1325 */
GEO_DrawMapOneBase(const uiNode_t * node,const base_t * base,bool oneUFOVisible,const char * font)1326 static void GEO_DrawMapOneBase (const uiNode_t* node, const base_t* base,
1327 bool oneUFOVisible, const char* font)
1328 {
1329 int x, y;
1330
1331 /* Draw weapon range if at least one UFO is visible */
1332 if (oneUFOVisible && AII_BaseCanShoot(base)) {
1333 int i;
1334 for (i = 0; i < base->numBatteries; i++) {
1335 const aircraftSlot_t* slot = &base->batteries[i].slot;
1336 if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1337 GEO_MapDrawEquidistantPoints(node, base->pos,
1338 slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1339 }
1340 }
1341 for (i = 0; i < base->numLasers; i++) {
1342 const aircraftSlot_t* slot = &base->lasers[i].slot;
1343 if (slot->item && slot->ammoLeft != 0 && slot->installationTime == 0) {
1344 GEO_MapDrawEquidistantPoints(node, base->pos,
1345 slot->ammo->craftitem.stats[AIR_STATS_WRANGE], red);
1346 }
1347 }
1348 }
1349
1350 /* Draw base radar (only the "wire" style part) */
1351 if (GEO_IsRadarOverlayActivated())
1352 GEO_DrawRadarInMap(node, &base->radar, base->pos);
1353
1354 /* Draw base */
1355 if (!UI_MAPEXTRADATACONST(node).flatgeoscape) {
1356 if (B_IsUnderAttack(base))
1357 /* two skins - second skin is for baseattack */
1358 GEO_Draw3DMarkerIfVisible(node, base->pos, defaultBaseAngle, "geoscape/base", 1);
1359 else
1360 GEO_Draw3DMarkerIfVisible(node, base->pos, defaultBaseAngle, "geoscape/base", 0);
1361 } else if (GEO_MapToScreen(node, base->pos, &x, &y)) {
1362 if (B_IsUnderAttack(base))
1363 GEO_RenderImage(x, y, "pics/geoscape/baseattack");
1364 else
1365 GEO_RenderImage(x, y, "pics/geoscape/base");
1366 }
1367
1368 /* Draw base names */
1369 if (GEO_AllMapToScreen(node, base->pos, &x, &y, nullptr))
1370 cgi->UI_DrawString(font, ALIGN_UL, x, y + 10, base->name);
1371 }
1372
1373 /**
1374 * @brief Draws health bar for an aircraft (either phalanx or ufo)
1375 * @param[in] node Pointer to the meunode to draw in
1376 * @param[in] aircraft Pointer to the aircraft to draw for
1377 * @note if max health (AIR_STATS_DAMAGE) <= 0 no healthbar drawn
1378 */
GEO_DrawAircraftHealthBar(const uiNode_t * node,const aircraft_t * aircraft)1379 static void GEO_DrawAircraftHealthBar (const uiNode_t* node, const aircraft_t* aircraft)
1380 {
1381 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1382 const int width = 8 * data.zoom;
1383 const int height = 1 * data.zoom * 0.9f;
1384 vec4_t color;
1385 int centerX;
1386 int centerY;
1387 bool visible;
1388
1389 if (!aircraft)
1390 return;
1391 if (aircraft->stats[AIR_STATS_DAMAGE] <= 0)
1392 return;
1393
1394 if (((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE]) <= .33f) {
1395 Vector4Copy(red, color);
1396 } else if (((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE]) <= .75f) {
1397 Vector4Copy(yellow, color);
1398 } else {
1399 Vector4Copy(green, color);
1400 }
1401
1402 if (!data.flatgeoscape)
1403 visible = GEO_3DMapToScreen(node, aircraft->pos, ¢erX, ¢erY, nullptr);
1404 else
1405 visible = GEO_AllMapToScreen(node, aircraft->pos, ¢erX, ¢erY, nullptr);
1406
1407 if (visible) {
1408 const vec4_t bordercolor = {1, 1, 1, 1};
1409 cgi->R_DrawFill(centerX - width / 2 , centerY - 5 * data.zoom, round(width * ((float)aircraft->damage / aircraft->stats[AIR_STATS_DAMAGE])), height, color);
1410 cgi->R_DrawRect(centerX - width / 2, centerY - 5 * data.zoom, width, height, bordercolor, 1.0, 1);
1411 }
1412 }
1413
1414 /**
1415 * @brief Draws one Phalanx aircraft on the geoscape map (2D and 3D)
1416 * @param[in] node The menu node which will be used for drawing markers.
1417 * @param[in] aircraft Pointer to the aircraft to draw.
1418 * @param[in] oneUFOVisible Is there at least one UFO visible on the geoscape?
1419 */
GEO_DrawMapOnePhalanxAircraft(const uiNode_t * node,aircraft_t * aircraft,bool oneUFOVisible)1420 static void GEO_DrawMapOnePhalanxAircraft (const uiNode_t* node, aircraft_t* aircraft, bool oneUFOVisible)
1421 {
1422 float angle;
1423
1424 /* Draw aircraft radar (only the "wire" style part) */
1425 if (GEO_IsRadarOverlayActivated())
1426 GEO_DrawRadarInMap(node, &aircraft->radar, aircraft->pos);
1427
1428 /* Draw only the bigger weapon range on geoscape: more detail will be given on airfight map */
1429 if (oneUFOVisible)
1430 GEO_MapDrawEquidistantPoints(node, aircraft->pos, aircraft->stats[AIR_STATS_WRANGE] / 1000.0f, red);
1431
1432 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1433 /* Draw aircraft route */
1434 if (aircraft->status >= AIR_TRANSIT) {
1435 /* aircraft is moving */
1436 mapline_t path;
1437
1438 path.numPoints = aircraft->route.numPoints - aircraft->point;
1439 /** @todo : check why path.numPoints can be sometime equal to -1 */
1440 if (path.numPoints > 1) {
1441 memcpy(path.point, aircraft->pos, sizeof(vec2_t));
1442 memcpy(path.point + 1, aircraft->route.point + aircraft->point + 1, (path.numPoints - 1) * sizeof(vec2_t));
1443 if (!data.flatgeoscape)
1444 GEO_3DMapDrawLine(node, &path);
1445 else
1446 GEO_MapDrawLine(node, &path);
1447 }
1448 angle = GEO_AngleOfPath(aircraft->pos, aircraft->route.point[aircraft->route.numPoints - 1], aircraft->direction, nullptr);
1449 } else {
1450 /* aircraft is idle */
1451 angle = 0.0f;
1452 }
1453
1454 /* Draw a circle around selected aircraft */
1455 if (GEO_IsAircraftSelected(aircraft)) {
1456 int x;
1457 int y;
1458
1459 if (!data.flatgeoscape)
1460 GEO_MapDrawEquidistantPoints(node, aircraft->pos, SELECT_CIRCLE_RADIUS, yellow);
1461 else {
1462 GEO_AllMapToScreen(node, aircraft->pos, &x, &y, nullptr);
1463 GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1464 }
1465
1466 /* Draw a circle around the ufo pursued by selected aircraft */
1467 if (aircraft->status == AIR_UFO && GEO_AllMapToScreen(node, aircraft->aircraftTarget->pos, &x, &y, nullptr)) {
1468 if (!data.flatgeoscape)
1469 GEO_MapDrawEquidistantPoints(node, aircraft->aircraftTarget->pos, SELECT_CIRCLE_RADIUS, yellow);
1470 else
1471 GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1472 }
1473 }
1474
1475 /* Draw aircraft (this must be called after drawing the selection circle so that the aircraft is drawn on top of it)*/
1476 GEO_Draw3DMarkerIfVisible(node, aircraft->pos, angle, aircraft->model, 0);
1477
1478 /** @todo we should only show healthbar if the aircraft is fighting but it's a slow algo */
1479 if (oneUFOVisible || cgi->Cvar_GetInteger("debug_showcrafthealth") >= 1)
1480 GEO_DrawAircraftHealthBar(node, aircraft);
1481 }
1482
1483 /**
1484 * @brief Assembles a string for a mission that is on the geoscape
1485 * @param[in] mission The mission to get the description for
1486 * @param[out] buffer The target buffer to store the text in
1487 * @param[in] size The size of the target buffer
1488 * @return A pointer to the buffer that was given to this function
1489 */
GEO_GetMissionText(char * buffer,size_t size,const mission_t * mission)1490 static const char* GEO_GetMissionText (char* buffer, size_t size, const mission_t* mission)
1491 {
1492 assert(mission);
1493 Com_sprintf(buffer, size, _("Name: %s\nObjective: %s"),
1494 MIS_GetName(mission), (mission->mapDef) ? _(mission->mapDef->description) : _("Unknown"));
1495 return buffer;
1496 }
1497
1498 /**
1499 * @brief Assembles a string for an aircraft that is on the geoscape
1500 * @param[in] aircraft The aircraft to get the description for
1501 * @param[out] buffer The target buffer to store the text in
1502 * @param[in] size The size of the target buffer
1503 * @return A pointer to the buffer that was given to this function
1504 */
GEO_GetAircraftText(char * buffer,size_t size,const aircraft_t * aircraft)1505 static const char* GEO_GetAircraftText (char* buffer, size_t size, const aircraft_t* aircraft)
1506 {
1507 if (aircraft->status == AIR_UFO) {
1508 const float distance = GetDistanceOnGlobe(aircraft->pos, aircraft->aircraftTarget->pos);
1509 Com_sprintf(buffer, size, _("Name:\t%s (%i/%i)\n"), aircraft->name, AIR_GetTeamSize(aircraft), aircraft->maxTeamSize);
1510 Q_strcat(buffer, size, _("Status:\t%s\n"), AIR_AircraftStatusToName(aircraft));
1511 if (aircraft->stats[AIR_STATS_DAMAGE] > 0)
1512 Q_strcat(buffer, size, _("Health:\t%3.0f%%\n"), (double)aircraft->damage * 100 / aircraft->stats[AIR_STATS_DAMAGE]);
1513 Q_strcat(buffer, size, _("Distance to target:\t\t%.0f\n"), distance);
1514 Q_strcat(buffer, size, _("Speed:\t%i km/h\n"), AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1515 Q_strcat(buffer, size, _("Fuel:\t%i/%i\n"), AIR_AircraftMenuStatsValues(aircraft->fuel, AIR_STATS_FUELSIZE),
1516 AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_FUELSIZE], AIR_STATS_FUELSIZE));
1517 Q_strcat(buffer, size, _("ETA:\t%sh\n"), CP_SecondConvert((float)SECONDS_PER_HOUR * distance / aircraft->stats[AIR_STATS_SPEED]));
1518 } else {
1519 Com_sprintf(buffer, size, _("Name:\t%s (%i/%i)\n"), aircraft->name, AIR_GetTeamSize(aircraft), aircraft->maxTeamSize);
1520 Q_strcat(buffer, size, _("Status:\t%s\n"), AIR_AircraftStatusToName(aircraft));
1521 if (aircraft->stats[AIR_STATS_DAMAGE] > 0)
1522 Q_strcat(buffer, size, _("Health:\t%3.0f%%\n"), (double)aircraft->damage * 100 / aircraft->stats[AIR_STATS_DAMAGE]);
1523 Q_strcat(buffer, size, _("Speed:\t%i km/h\n"), AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1524 Q_strcat(buffer, size, _("Fuel:\t%i/%i\n"), AIR_AircraftMenuStatsValues(aircraft->fuel, AIR_STATS_FUELSIZE),
1525 AIR_AircraftMenuStatsValues(aircraft->stats[AIR_STATS_FUELSIZE], AIR_STATS_FUELSIZE));
1526 if (aircraft->status != AIR_IDLE) {
1527 const float distance = GetDistanceOnGlobe(aircraft->pos,
1528 aircraft->route.point[aircraft->route.numPoints - 1]);
1529 Q_strcat(buffer, size, _("ETA:\t%sh\n"), CP_SecondConvert((float)SECONDS_PER_HOUR * distance / aircraft->stats[AIR_STATS_SPEED]));
1530 }
1531 }
1532 return buffer;
1533 }
1534
1535 /**
1536 * @brief Assembles a string for a UFO that is on the geoscape
1537 * @param[in] ufo The UFO to get the description for
1538 * @param[out] buffer The target buffer to store the text in
1539 * @param[in] size The size of the target buffer
1540 * @return A pointer to the buffer that was given to this function
1541 */
GEO_GetUFOText(char * buffer,size_t size,const aircraft_t * ufo)1542 static const char* GEO_GetUFOText (char* buffer, size_t size, const aircraft_t* ufo)
1543 {
1544 Com_sprintf(buffer, size, "%s\n", UFO_GetName(ufo));
1545 Q_strcat(buffer, size, _("Speed: %i km/h\n"), AIR_AircraftMenuStatsValues(ufo->stats[AIR_STATS_SPEED], AIR_STATS_SPEED));
1546 return buffer;
1547 }
1548
1549 /**
1550 * @brief Will add missions and UFOs to the geoscape dock panel
1551 */
GEO_UpdateGeoscapeDock(void)1552 void GEO_UpdateGeoscapeDock (void)
1553 {
1554 char buf[512];
1555 aircraft_t* ufo;
1556
1557 cgi->UI_ExecuteConfunc("clean_geoscape_object");
1558
1559 /* draw mission pics */
1560 MIS_Foreach(mission) {
1561 if (!mission->onGeoscape)
1562 continue;
1563 cgi->UI_ExecuteConfunc("add_geoscape_object mission %i \"%s\" \"%s\n%s\"",
1564 mission->idx, MIS_GetModel(mission), MIS_GetName(mission),
1565 (mission->mapDef) ? _(mission->mapDef->description) : "");
1566 }
1567
1568 /* draws ufos */
1569 ufo = nullptr;
1570 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1571 const unsigned int ufoIDX = UFO_GetGeoscapeIDX(ufo);
1572 cgi->UI_ExecuteConfunc("add_geoscape_object ufo %i %s \"%s\"",
1573 ufoIDX, ufo->model, GEO_GetUFOText(buf, sizeof(buf), ufo));
1574 }
1575 }
1576
1577 /**
1578 * @brief Draws all ufos, aircraft, bases and so on to the geoscape map (2D and 3D)
1579 * @param[in] node The menu node which will be used for drawing markers.
1580 * @note This is a drawing function only, called each time a frame is drawn. Therefore
1581 * you should not use this function to calculate eg. the distance between 2 items on the geoscape
1582 * (you should instead calculate it just after one of the items moved -- distance is not
1583 * going to change when you rotate the earth around itself and the time is stopped eg.).
1584 * @sa GEO_Draw
1585 */
GEO_DrawMarkers(const uiNode_t * node)1586 void GEO_DrawMarkers (const uiNode_t* node)
1587 {
1588 int i;
1589 const char* font;
1590 aircraft_t* ufo;
1591 base_t* base;
1592
1593 const vec4_t white = {1.f, 1.f, 1.f, 0.7f};
1594 int maxInterpolationPoints;
1595
1596 assert(node);
1597
1598 /* font color on geoscape */
1599 cgi->R_Color(node->color);
1600 /* default font */
1601 font = cgi->UI_GetFontFromNode(node);
1602
1603 /* check if at least 1 UFO is visible */
1604 const bool oneUFOVisible = UFO_GetNextOnGeoscape(nullptr) != nullptr;
1605
1606 /* draw mission pics */
1607 MIS_Foreach(mission) {
1608 if (!mission->onGeoscape)
1609 continue;
1610 GEO_DrawMapOneMission(node, mission);
1611 }
1612
1613 /* draw installations */
1614 INS_Foreach(installation) {
1615 GEO_DrawMapOneInstallation(node, installation, oneUFOVisible, font);
1616 }
1617
1618 /* draw bases */
1619 base = nullptr;
1620 while ((base = B_GetNext(base)) != nullptr)
1621 GEO_DrawMapOneBase(node, base, oneUFOVisible, font);
1622
1623 /* draw all aircraft */
1624 AIR_Foreach(aircraft) {
1625 if (AIR_IsAircraftOnGeoscape(aircraft))
1626 GEO_DrawMapOnePhalanxAircraft(node, aircraft, oneUFOVisible);
1627 }
1628
1629 /* draws ufos */
1630 ufo = nullptr;
1631 while ((ufo = UFO_GetNextOnGeoscape(ufo)) != nullptr) {
1632 #ifdef DEBUG
1633 /* in debug mode you execute set showufos 1 to see the ufos on geoscape */
1634 if (cgi->Cvar_GetInteger("debug_showufos")) {
1635 /* Draw ufo route */
1636 if (!UI_MAPEXTRADATACONST(node).flatgeoscape)
1637 GEO_3DMapDrawLine(node, &ufo->route);
1638 else
1639 GEO_MapDrawLine(node, &ufo->route);
1640 } else
1641 #endif
1642 {
1643 const float angle = GEO_AngleOfPath(ufo->pos, ufo->route.point[ufo->route.numPoints - 1], ufo->direction, nullptr);
1644 const mapExtraData_t &data = UI_MAPEXTRADATACONST(node);
1645
1646 if (!data.flatgeoscape)
1647 GEO_MapDrawEquidistantPoints(node, ufo->pos, SELECT_CIRCLE_RADIUS, white);
1648
1649 if (GEO_IsUFOSelected(ufo)) {
1650 if (!data.flatgeoscape) {
1651 GEO_MapDrawEquidistantPoints(node, ufo->pos, SELECT_CIRCLE_RADIUS, yellow);
1652 } else {
1653 int x, y;
1654 GEO_AllMapToScreen(node, ufo->pos, &x, &y, nullptr);
1655 GEO_RenderImage(x, y, "pics/geoscape/circleactive");
1656 }
1657 }
1658 GEO_Draw3DMarkerIfVisible(node, ufo->pos, angle, ufo->model, 0);
1659
1660 /** @todo we should only show healthbar if aircraft is fighting but it's a slow algo */
1661 if (RS_IsResearched_ptr(ufo->tech)
1662 || cgi->Cvar_GetInteger("debug_showcrafthealth") >= 1)
1663 GEO_DrawAircraftHealthBar(node, ufo);
1664 }
1665 }
1666
1667 if (ccs.gameTimeScale > 0)
1668 maxInterpolationPoints = floor(1.0f / (ccs.frametime * (float)ccs.gameTimeScale));
1669 else
1670 maxInterpolationPoints = 0;
1671
1672 /* draws projectiles */
1673 for (i = 0; i < ccs.numProjectiles; i++) {
1674 aircraftProjectile_t* projectile = &ccs.projectiles[i];
1675 vec3_t drawPos = {0, 0, 0};
1676
1677 if (projectile->hasMoved) {
1678 projectile->hasMoved = false;
1679 VectorCopy(projectile->pos[0], drawPos);
1680 } else {
1681 if (maxInterpolationPoints > 2 && projectile->numInterpolationPoints < maxInterpolationPoints) {
1682 /* If a new point hasn't been given and there is at least 3 points need to be filled in then
1683 * use linear interpolation to draw the points until a new projectile point is provided.
1684 * The reason you need at least 3 points is that acceptable results can be achieved with 2 or less
1685 * gaps in points so don't add the overhead of interpolation. */
1686 const float xInterpolStep = (projectile->projectedPos[0][0] - projectile->pos[0][0]) / (float)maxInterpolationPoints;
1687 projectile->numInterpolationPoints += 1;
1688 drawPos[0] = projectile->pos[0][0] + (xInterpolStep * projectile->numInterpolationPoints);
1689 LinearInterpolation(projectile->pos[0], projectile->projectedPos[0], drawPos[0], drawPos[1]);
1690 } else {
1691 VectorCopy(projectile->pos[0], drawPos);
1692 }
1693 }
1694
1695 if (projectile->bullets) {
1696 GEO_DrawBullets(node, drawPos);
1697 } else if (projectile->beam) {
1698 vec3_t start;
1699 vec3_t end;
1700
1701 if (projectile->attackingAircraft)
1702 VectorCopy(projectile->attackingAircraft->pos, start);
1703 else
1704 VectorCopy(projectile->attackerPos, start);
1705
1706 if (projectile->aimedAircraft)
1707 VectorCopy(projectile->aimedAircraft->pos, end);
1708 else
1709 VectorCopy(projectile->idleTarget, end);
1710
1711 GEO_DrawBeam(node, start, end, projectile->aircraftItem->craftitem.beamColor);
1712 } else {
1713 GEO_Draw3DMarkerIfVisible(node, drawPos, projectile->angle, projectile->aircraftItem->model, 0);
1714 }
1715 }
1716
1717 const bool showXVI = CP_IsXVIVisible();
1718 static char buffer[512] = "";
1719
1720 /* Draw nation names */
1721 for (i = 0; i < ccs.numNations; i++) {
1722 const nation_t* nation = NAT_GetNationByIDX(i);
1723 int x, y;
1724 if (GEO_AllMapToScreen(node, nation->pos, &x, &y, nullptr))
1725 cgi->UI_DrawString("f_verysmall", ALIGN_UC, x , y, _(nation->name));
1726 if (showXVI) {
1727 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
1728 Q_strcat(buffer, sizeof(buffer), _("%s\t%i%%\n"), _(nation->name), stats->xviInfection);
1729 }
1730 }
1731
1732 if (showXVI)
1733 cgi->UI_RegisterText(TEXT_XVI, buffer);
1734 else
1735 cgi->UI_ResetData(TEXT_XVI);
1736
1737 cgi->R_Color(nullptr);
1738 }
1739
1740 /**
1741 * @brief Draw the geoscape
1742 * @param[in] data Geoscape status data structure
1743 */
GEO_Draw(geoscapeData_t * data)1744 void GEO_Draw (geoscapeData_t* data)
1745 {
1746 if (!CP_IsRunning()) {
1747 data->active = false;
1748 return;
1749 }
1750
1751 data->active = true;
1752 data->map = ccs.curCampaign->map;
1753 data->nationOverlay = GEO_IsNationOverlayActivated();
1754 data->xviOverlay = GEO_IsXVIOverlayActivated();
1755 data->radarOverlay = GEO_IsRadarOverlayActivated();
1756 data->date = ccs.date;
1757
1758 geoscapeNode = static_cast<uiNode_t* >(data->geoscapeNode);
1759
1760 mission_t* mission = GEO_GetSelectedMission();
1761 /* display text */
1762 cgi->UI_ResetData(TEXT_STANDARD);
1763 switch (ccs.mapAction) {
1764 case MA_NEWBASE:
1765 cgi->UI_RegisterText(TEXT_STANDARD, _("Select the desired location of the new base on the map.\n"));
1766 return;
1767 case MA_NEWINSTALLATION:
1768 cgi->UI_RegisterText(TEXT_STANDARD, _("Select the desired location of the new installation on the map.\n"));
1769 return;
1770 case MA_BASEATTACK:
1771 if (mission)
1772 break;
1773 cgi->UI_RegisterText(TEXT_STANDARD, _("Aliens are attacking our base at this very moment.\n"));
1774 return;
1775 case MA_INTERCEPT:
1776 if (mission)
1777 break;
1778 cgi->UI_RegisterText(TEXT_STANDARD, _("Select ufo or mission on map\n"));
1779 return;
1780 case MA_UFORADAR:
1781 if (mission)
1782 break;
1783 cgi->UI_RegisterText(TEXT_STANDARD, _("UFO in radar range\n"));
1784 return;
1785 case MA_NONE:
1786 break;
1787 }
1788
1789 /* Nothing is displayed yet */
1790 if (mission) {
1791 cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetMissionText(textStandard, sizeof(textStandard), mission));
1792 } else if (GEO_GetSelectedAircraft() != nullptr) {
1793 const aircraft_t* aircraft = GEO_GetSelectedAircraft();
1794 if (AIR_IsAircraftInBase(aircraft)) {
1795 cgi->UI_RegisterText(TEXT_STANDARD, nullptr);
1796 GEO_ResetAction();
1797 return;
1798 }
1799 cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetAircraftText(textStandard, sizeof(textStandard), aircraft));
1800 } else if (GEO_GetSelectedUFO() != nullptr) {
1801 cgi->UI_RegisterText(TEXT_STANDARD, GEO_GetUFOText(textStandard, sizeof(textStandard), GEO_GetSelectedUFO()));
1802 } else {
1803 #ifdef DEBUG
1804 if (debug_showInterest->integer) {
1805 static char t[64];
1806 Com_sprintf(t, lengthof(t), "Interest level: %i\n", ccs.overallInterest);
1807 cgi->UI_RegisterText(TEXT_STANDARD, t);
1808 } else
1809 #endif
1810 cgi->UI_RegisterText(TEXT_STANDARD, "");
1811 }
1812 }
1813
1814 /**
1815 * @brief No more special action on the geoscape
1816 */
GEO_ResetAction(void)1817 void GEO_ResetAction (void)
1818 {
1819 /* don't allow a reset when no base is set up */
1820 if (B_AtLeastOneExists())
1821 ccs.mapAction = MA_NONE;
1822
1823 GEO_SetInterceptorAircraft(nullptr);
1824 GEO_SetSelectedMission(nullptr);
1825 GEO_SetSelectedAircraft(nullptr);
1826 GEO_SetSelectedUFO(nullptr);
1827
1828 if (!radarOverlayWasSet)
1829 GEO_DeactivateOverlay("radar");
1830 }
1831
1832 /**
1833 * @brief Select the specified ufo on the geoscape
1834 */
GEO_SelectUFO(aircraft_t * ufo)1835 void GEO_SelectUFO (aircraft_t* ufo)
1836 {
1837 GEO_ResetAction();
1838 GEO_SetSelectedUFO(ufo);
1839 }
1840
1841 /**
1842 * @brief Select the specified aircraft on the geoscape
1843 */
GEO_SelectAircraft(aircraft_t * aircraft)1844 void GEO_SelectAircraft (aircraft_t* aircraft)
1845 {
1846 GEO_ResetAction();
1847 GEO_SetSelectedAircraft(aircraft);
1848 }
1849
1850 /**
1851 * @brief Select the specified mission
1852 * @param[in] mission Pointer to the mission to select
1853 * @return pointer to the selected mission
1854 */
GEO_SelectMission(mission_t * mission)1855 mission_t* GEO_SelectMission (mission_t* mission)
1856 {
1857 if (!mission || GEO_IsMissionSelected(mission))
1858 return GEO_GetSelectedMission();
1859 GEO_ResetAction();
1860 ccs.mapAction = MA_INTERCEPT;
1861 GEO_SetSelectedMission(mission);
1862 return GEO_GetSelectedMission();
1863 }
1864
1865 /**
1866 * @brief Notify that a mission has been removed
1867 */
GEO_NotifyMissionRemoved(const mission_t * mission)1868 void GEO_NotifyMissionRemoved (const mission_t* mission)
1869 {
1870 /* Unselect the current selected mission if it's the same */
1871 if (GEO_IsMissionSelected(mission))
1872 GEO_ResetAction();
1873
1874 GEO_UpdateGeoscapeDock();
1875 }
1876
1877 /**
1878 * @brief Notify that a UFO has been removed
1879 * @param[in] ufo Pointer to the ufo has been removed
1880 * @param[in] destroyed True if the UFO has been destroyed, false if it's been only set invisible (landed)
1881 */
GEO_NotifyUFORemoved(const aircraft_t * ufo,bool destroyed)1882 void GEO_NotifyUFORemoved (const aircraft_t* ufo, bool destroyed)
1883 {
1884 GEO_UpdateGeoscapeDock();
1885
1886 if (GEO_GetSelectedUFO() == nullptr)
1887 return;
1888
1889 /* Unselect the current selected ufo if it's the same */
1890 if (GEO_IsUFOSelected(ufo))
1891 GEO_ResetAction();
1892 else if (destroyed && ccs.geoscape.selectedUFO > ufo)
1893 /** @todo convert to linked list */
1894 ccs.geoscape.selectedUFO--;
1895 }
1896
1897 /**
1898 * @brief Notify that an aircraft has been removed from game
1899 * @param[in] aircraft Pointer to the aircraft that has been removed
1900 */
GEO_NotifyAircraftRemoved(const aircraft_t * aircraft)1901 void GEO_NotifyAircraftRemoved (const aircraft_t* aircraft)
1902 {
1903 /* Unselect the current selected ufo if its the same */
1904 if (GEO_IsAircraftSelected(aircraft) || GEO_IsInterceptorSelected(aircraft))
1905 GEO_ResetAction();
1906 }
1907
1908 /**
1909 * @brief Translate nation map color to nation
1910 * @sa GEO_GetColor
1911 * @param[in] pos Map Coordinates to get the nation from
1912 * @return returns the nation pointer with the given color on nationPic at given pos
1913 * @return nullptr if no nation with the given color value was found
1914 * @note The coordinates already have to be transformed to map coordinates via GEO_ScreenToMap
1915 */
GEO_GetNation(const vec2_t pos)1916 nation_t* GEO_GetNation (const vec2_t pos)
1917 {
1918 int i;
1919 const byte* color = GEO_GetColor(pos, MAPTYPE_NATIONS, nullptr);
1920 const vec3_t fcolor = {color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f};
1921 #ifdef PARANOID
1922 Com_DPrintf(DEBUG_CLIENT, "GEO_GetNation: color value for %.0f:%.0f is r:%i, g:%i, b: %i\n", pos[0], pos[1], color[0], color[1], color[2]);
1923 #endif
1924 for (i = 0; i < ccs.numNations; i++) {
1925 nation_t* nation = NAT_GetNationByIDX(i);
1926 /* compare the first three color values with color value at pos */
1927 /* 0.02 x 255 = 5.1, which allow a variation of +-5 for each color components */
1928 if (VectorEqualEpsilon(nation->color, fcolor, 0.02))
1929 return nation;
1930 }
1931 Com_DPrintf(DEBUG_CLIENT, "GEO_GetNation: No nation found at %.0f:%.0f - color: %i:%i:%i\n", pos[0], pos[1], color[0], color[1], color[2]);
1932 return nullptr;
1933 }
1934
1935
1936 /**
1937 * @brief Translate color value to terrain type
1938 * @sa GEO_GetColor
1939 * @param[in] color the color value from the terrain mask
1940 * @return returns the zone name
1941 * @note never may return a null pointer or an empty string
1942 * @note Make sure, that there are textures with the same name in base/textures/tex_terrain
1943 */
GEO_GetTerrainType(const byte * const color)1944 const char* GEO_GetTerrainType (const byte* const color)
1945 {
1946 if (MapIsDesert(color))
1947 return "desert";
1948 else if (MapIsArctic(color))
1949 return "arctic";
1950 else if (MapIsWater(color))
1951 return "water";
1952 else if (MapIsMountain(color))
1953 return "mountain";
1954 else if (MapIsTropical(color))
1955 return "tropical";
1956 else if (MapIsCold(color))
1957 return "cold";
1958 else if (MapIsWasted(color))
1959 return "wasted";
1960 return "grass";
1961 }
1962
1963 /**
1964 * @brief Translate color value to culture type
1965 * @sa GEO_GetColor
1966 * @param[in] color the color value from the culture mask
1967 * @return returns the zone name
1968 * @note never may return a null pointer or an empty string
1969 */
GEO_GetCultureType(const byte * color)1970 static const char* GEO_GetCultureType (const byte* color)
1971 {
1972 if (MapIsWater(color))
1973 return "water";
1974 else if (MapIsEastern(color))
1975 return "eastern";
1976 else if (MapIsWestern(color))
1977 return "western";
1978 else if (MapIsOriental(color))
1979 return "oriental";
1980 else if (MapIsAfrican(color))
1981 return "african";
1982 return "western";
1983 }
1984
1985 /**
1986 * @brief Translate color value to population type
1987 * @sa GEO_GetColor
1988 * @param[in] color the color value from the population mask
1989 * @return returns the zone name
1990 * @note never may return a null pointer or an empty string
1991 */
GEO_GetPopulationType(const byte * color)1992 static const char* GEO_GetPopulationType (const byte* color)
1993 {
1994 if (MapIsWater(color))
1995 return "water";
1996 else if (MapIsUrban(color))
1997 return "urban";
1998 else if (MapIsSuburban(color))
1999 return "suburban";
2000 else if (MapIsVillage(color))
2001 return "village";
2002 else if (MapIsRural(color))
2003 return "rural";
2004 return "nopopulation";
2005 }
2006
2007 /**
2008 * @brief Determine the terrain type under a given position
2009 * @sa GEO_GetColor
2010 * @param[in] pos Map Coordinates to get the terrain type from
2011 * @param[out] coast GEO_GetColor will set this to true if the given position is a coast line.
2012 * @return returns the zone name
2013 */
GEO_GetTerrainTypeByPos(const vec2_t pos,bool * coast)2014 static inline const char* GEO_GetTerrainTypeByPos (const vec2_t pos, bool *coast)
2015 {
2016 const byte* color = GEO_GetColor(pos, MAPTYPE_TERRAIN, coast);
2017 return GEO_GetTerrainType(color);
2018 }
2019
2020 /**
2021 * @brief Determine the culture type under a given position
2022 * @sa GEO_GetColor
2023 * @param[in] pos Map Coordinates to get the culture type from
2024 * @return returns the zone name
2025 */
GEO_GetCultureTypeByPos(const vec2_t pos)2026 static inline const char* GEO_GetCultureTypeByPos (const vec2_t pos)
2027 {
2028 const byte* color = GEO_GetColor(pos, MAPTYPE_CULTURE, nullptr);
2029 return GEO_GetCultureType(color);
2030 }
2031
2032 /**
2033 * @brief Determine the population type under a given position
2034 * @sa GEO_GetColor
2035 * @param[in] pos Map Coordinates to get the population type from
2036 * @return returns the zone name
2037 */
GEO_GetPopulationTypeByPos(const vec2_t pos)2038 static inline const char* GEO_GetPopulationTypeByPos (const vec2_t pos)
2039 {
2040 const byte* color = GEO_GetColor(pos, MAPTYPE_POPULATION, nullptr);
2041 return GEO_GetPopulationType(color);
2042 }
2043
2044 /**
2045 * @brief Get number of civilian on a map at given position.
2046 * @param[in] pos Position where the mission takes place.
2047 * @return Number of civilian.
2048 * @sa CP_CreateCivilianTeam
2049 */
GEO_GetCivilianNumberByPosition(const vec2_t pos)2050 int GEO_GetCivilianNumberByPosition (const vec2_t pos)
2051 {
2052 const byte* color = GEO_GetColor(pos, MAPTYPE_POPULATION, nullptr);
2053
2054 if (MapIsWater(color))
2055 cgi->Com_Error(ERR_DROP, "GEO_GetPopulationType: Trying to get number of civilian in a position on water");
2056
2057 if (MapIsUrban(color))
2058 return 10;
2059 else if (MapIsSuburban(color))
2060 return 8;
2061 else if (MapIsVillage(color))
2062 return 6;
2063 else if (MapIsRural(color))
2064 return 4;
2065 else if (MapIsNopopulation(color))
2066 return 2;
2067
2068 return 0;
2069 }
2070
2071 /**
2072 * @brief Prints positions parameter in console.
2073 * @param[in] pos Location (latitude, longitude) where you want to check
2074 * @note Used for printing in console, do not translate.
2075 * @sa NAT_ScriptSanityCheck
2076 */
GEO_PrintParameterStringByPos(const vec2_t pos)2077 void GEO_PrintParameterStringByPos (const vec2_t pos)
2078 {
2079 bool coast = false;
2080 const char* terrainType = GEO_GetTerrainTypeByPos(pos, &coast);
2081 const char* cultureType = GEO_GetCultureTypeByPos(pos);
2082 const char* populationType = GEO_GetPopulationTypeByPos(pos);
2083
2084 Com_Printf (" (Terrain: %s, Culture: %s, Population: %s, Coast: %s)\n",
2085 terrainType, cultureType, populationType, coast ? "true" : "false");
2086 }
2087
2088 /**
2089 * @brief Check that a position (in latitude / longitude) is within boundaries.
2090 * @param[in,out] pos Pointer to the 2 elements vector giving the position.
2091 */
GEO_CheckPositionBoundaries(float * pos)2092 void GEO_CheckPositionBoundaries (float* pos)
2093 {
2094 while (pos[0] > 180.0)
2095 pos[0] -= 360.0;
2096 while (pos[0] < -180.0)
2097 pos[0] += 360.0;
2098 while (pos[1] > 90.0)
2099 pos[1] -= 180.0;
2100 while (pos[1] < -90.0)
2101 pos[1] += 180.0;
2102 }
2103
2104 /**
2105 * @brief Check whether given position is Day or Night.
2106 * @param[in] pos Given position.
2107 * @return True if given position is Night.
2108 */
GEO_IsNight(const vec2_t pos)2109 bool GEO_IsNight (const vec2_t pos)
2110 {
2111 float p, q, a, root, x;
2112
2113 /* set p to hours (we don't use ccs.day here because we need a float value) */
2114 p = (float) ccs.date.sec / SECONDS_PER_DAY;
2115 /* convert current day to angle (-pi on 1st january, pi on 31 december) */
2116 q = (ccs.date.day + p) * (2 * M_PI / DAYS_PER_YEAR_AVG) - M_PI;
2117 p = (0.5 + pos[0] / 360 - p) * (2 * M_PI) - q;
2118 a = -sin(pos[1] * torad);
2119 root = sqrt(1.0 - a * a);
2120 x = sin(p) * root * sin(q) - (a * SIN_ALPHA + cos(p) * root * COS_ALPHA) * cos(q);
2121 return (x > 0);
2122 }
2123
2124 /**
2125 * @brief Returns the color value from geoscape of a certain mask (terrain, culture or population) at a given position.
2126 * @param[in] pos vec2_t Value of position on map to get the color value from.
2127 * pos is longitude and latitude
2128 * @param[in] type determine the map to get the color from (there are different masks)
2129 * one for the climatezone (bases made use of this - there are grass, ice and desert
2130 * base tiles available) and one for the nations
2131 * @param[out] coast The function will set this to true if the given position is a coast line.
2132 * This can be @c nullptr if you are not interested in this fact.
2133 * @return Returns the color value at given position.
2134 * @note terrainPic, culturePic and populationPic are pointers to an rgba image in memory
2135 */
GEO_GetColor(const vec2_t pos,mapType_t type,bool * coast)2136 const byte* GEO_GetColor (const vec2_t pos, mapType_t type, bool *coast)
2137 {
2138 int x, y;
2139 int width, height;
2140 const byte* mask;
2141 const byte* color;
2142
2143 switch (type) {
2144 case MAPTYPE_TERRAIN:
2145 mask = terrainPic;
2146 width = terrainWidth;
2147 height = terrainHeight;
2148 break;
2149 case MAPTYPE_CULTURE:
2150 mask = culturePic;
2151 width = cultureWidth;
2152 height = cultureHeight;
2153 break;
2154 case MAPTYPE_POPULATION:
2155 mask = populationPic;
2156 width = populationWidth;
2157 height = populationHeight;
2158 break;
2159 case MAPTYPE_NATIONS:
2160 mask = nationsPic;
2161 width = nationsWidth;
2162 height = nationsHeight;
2163 break;
2164 default:
2165 cgi->Com_Error(ERR_DROP, "Unknown maptype %i\n", type);
2166 }
2167
2168 assert(mask);
2169
2170 /** @todo add EQUAL_EPSILON here? */
2171 assert(pos[0] >= -180);
2172 assert(pos[0] <= 180);
2173 assert(pos[1] >= -90);
2174 assert(pos[1] <= 90);
2175
2176 /* get coordinates */
2177 x = (180 - pos[0]) / 360 * width;
2178 x--; /* we start from 0 */
2179 y = (90 - pos[1]) / 180 * height;
2180 y--; /* we start from 0 */
2181 if (x < 0)
2182 x = 0;
2183 if (y < 0)
2184 y = 0;
2185
2186 /* 4 => RGBA */
2187 /* terrainWidth is the width of the image */
2188 /* this calculation returns the pixel in col x and in row y */
2189 assert(4 * (x + y * width) < width * height * 4);
2190 color = mask + 4 * (x + y * width);
2191 if (coast != nullptr) {
2192 if (MapIsWater(color)) {
2193 *coast = false;
2194 } else {
2195 /* only check four directions */
2196 const int gap = 4;
2197 if (x > gap) {
2198 const byte* coastCheck = mask + 4 * ((x - gap) + y * width);
2199 *coast = MapIsWater(coastCheck);
2200 }
2201 if (!*coast && x < width - 1 - gap) {
2202 const byte* coastCheck = mask + 4 * ((x + gap) + y * width);
2203 *coast = MapIsWater(coastCheck);
2204 }
2205
2206 if (!*coast) {
2207 if (y > gap) {
2208 const byte* coastCheck = mask + 4 * (x + (y - gap) * width);
2209 *coast = MapIsWater(coastCheck);
2210 }
2211 if (!*coast && y < height - 1 - gap) {
2212 const byte* coastCheck = mask + 4 * (x + (y + gap) * width);
2213 *coast = MapIsWater(coastCheck);
2214 }
2215 }
2216 }
2217 }
2218
2219 return color;
2220 }
2221
2222 /**
2223 * @brief Minimum distance between a new mission and an existing base.
2224 */
2225 static const float MIN_DIST_BASE = 4.0f;
2226
2227 /**
2228 * @brief Check if given pos is close to an existing base.
2229 * @return Pointer to the base if one base is closer than MIN_DIST_BASE from pos, nullptr else
2230 */
GEO_PositionCloseToBase(const vec2_t pos)2231 base_t* GEO_PositionCloseToBase (const vec2_t pos)
2232 {
2233 base_t* base = nullptr;
2234 while ((base = B_GetNext(base)) != nullptr)
2235 if (GetDistanceOnGlobe(pos, base->pos) < MIN_DIST_BASE)
2236 return base;
2237
2238 return nullptr;
2239 }
2240
2241 /**
2242 * @brief Checks for a given location, if it fulfills all criteria given via parameters (terrain, culture, population, nation type)
2243 * @param[in] pos Location to be tested
2244 * @param[in] terrainTypes A linkedList_t containing a list of strings determining the terrain types to be tested for (e.g. "grass") may be nullptr
2245 * @param[in] cultureTypes A linkedList_t containing a list of strings determining the culture types to be tested for (e.g. "western") may be nullptr
2246 * @param[in] populationTypes A linkedList_t containing a list of strings determining the population types to be tested for (e.g. "suburban") may be nullptr
2247 * @param[in] nations A linkedList_t containing a list of strings determining the nations to be tested for (e.g. "asia") may be nullptr
2248 * @return true if a location was found, otherwise false. If the map is over water, return false
2249 * @note The name TCPNTypes comes from terrain, culture, population, nation types
2250 */
GEO_PositionFitsTCPNTypes(const vec2_t pos,const linkedList_t * terrainTypes,const linkedList_t * cultureTypes,const linkedList_t * populationTypes,const linkedList_t * nations)2251 bool GEO_PositionFitsTCPNTypes (const vec2_t pos, const linkedList_t* terrainTypes, const linkedList_t* cultureTypes, const linkedList_t* populationTypes, const linkedList_t* nations)
2252 {
2253 bool coast = false;
2254 const char* terrainType = GEO_GetTerrainTypeByPos(pos, &coast);
2255 const char* cultureType = GEO_GetCultureTypeByPos(pos);
2256 const char* populationType = GEO_GetPopulationTypeByPos(pos);
2257
2258 if (MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr)))
2259 return false;
2260
2261 if (!terrainTypes || cgi->LIST_ContainsString(terrainTypes, terrainType) || (coast && cgi->LIST_ContainsString(terrainTypes, "coast"))) {
2262 if (!cultureTypes || cgi->LIST_ContainsString(cultureTypes, cultureType)) {
2263 if (!populationTypes || cgi->LIST_ContainsString(populationTypes, populationType)) {
2264 const nation_t* nationAtPos = GEO_GetNation(pos);
2265 if (!nations)
2266 return true;
2267 if (nationAtPos && (!nations || cgi->LIST_ContainsString(nations, nationAtPos->id))) {
2268 return true;
2269 }
2270 }
2271 }
2272 }
2273
2274 return false;
2275 }
2276
GEO_Shutdown(void)2277 void GEO_Shutdown (void)
2278 {
2279 Mem_Free(terrainPic);
2280 terrainPic = nullptr;
2281
2282 Mem_Free(culturePic);
2283 culturePic = nullptr;
2284
2285 Mem_Free(populationPic);
2286 populationPic = nullptr;
2287
2288 Mem_Free(nationsPic);
2289 nationsPic = nullptr;
2290 }
2291
GEO_Init(const char * map)2292 void GEO_Init (const char* map)
2293 {
2294 /* load terrain mask */
2295 cgi->R_LoadImage(va("pics/geoscape/%s_terrain", map), &terrainPic, &terrainWidth, &terrainHeight);
2296 if (!terrainPic || !terrainWidth || !terrainHeight)
2297 cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_terrain in pics/geoscape", map);
2298
2299 /* load culture mask */
2300 cgi->R_LoadImage(va("pics/geoscape/%s_culture", map), &culturePic, &cultureWidth, &cultureHeight);
2301 if (!culturePic || !cultureWidth || !cultureHeight)
2302 cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_culture in pics/geoscape", map);
2303
2304 /* load population mask */
2305 cgi->R_LoadImage(va("pics/geoscape/%s_population", map), &populationPic, &populationWidth, &populationHeight);
2306 if (!populationPic || !populationWidth || !populationHeight)
2307 cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_population in pics/geoscape", map);
2308
2309 /* load nations mask */
2310 cgi->R_LoadImage(va("pics/geoscape/%s_nations", map), &nationsPic, &nationsWidth, &nationsHeight);
2311 if (!nationsPic || !nationsWidth || !nationsHeight)
2312 cgi->Com_Error(ERR_DROP, "Couldn't load map mask %s_nations in pics/geoscape", map);
2313 }
2314
GEO_Reset(const char * map)2315 void GEO_Reset (const char* map)
2316 {
2317 GEO_Shutdown();
2318 GEO_Init(map);
2319 GEO_ResetAction();
2320 GEO_UpdateGeoscapeDock();
2321 }
2322
2323 /**
2324 * @brief Notify that a UFO disappears on radars
2325 */
GEO_NotifyUFODisappear(const aircraft_t * ufo)2326 void GEO_NotifyUFODisappear (const aircraft_t* ufo)
2327 {
2328 /* Unselect the currently selected ufo if it's the same */
2329 if (GEO_IsUFOSelected(ufo))
2330 GEO_ResetAction();
2331
2332 GEO_UpdateGeoscapeDock();
2333 }
2334
2335 /**
2336 * @brief Switch overlay (turn on / off)
2337 * @param[in] overlayID Name of the overlay you want to switch.
2338 */
GEO_SetOverlay(const char * overlayID)2339 void GEO_SetOverlay (const char* overlayID)
2340 {
2341 const int value = cgi->Cvar_GetInteger("cl_geoscape_overlay");
2342 if (Q_streq(overlayID, "nations")) {
2343 cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_NATION);
2344 return;
2345 }
2346
2347 /* do nothing while the first base is not build */
2348 if (!B_AtLeastOneExists())
2349 return;
2350
2351 if (Q_streq(overlayID, "xvi")) {
2352 cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_XVI);
2353 } else if (Q_streq(overlayID, "radar")) {
2354 cgi->Cvar_SetValue("cl_geoscape_overlay", value ^ OVERLAY_RADAR);
2355 if (GEO_IsRadarOverlayActivated())
2356 RADAR_UpdateWholeRadarOverlay();
2357 }
2358 }
2359
2360 /**
2361 * @brief Console command to call GEO_SetOverlay.
2362 */
GEO_SetOverlay_f(void)2363 static void GEO_SetOverlay_f (void)
2364 {
2365 const char* arg;
2366
2367 if (cgi->Cmd_Argc() != 2) {
2368 Com_Printf("Usage: %s <nations|xvi|radar>\n", cgi->Cmd_Argv(0));
2369 return;
2370 }
2371
2372 arg = cgi->Cmd_Argv(1);
2373 GEO_SetOverlay(arg);
2374
2375 /* save last decision player took on radar display, in order to be able to restore it later */
2376 if (Q_streq(arg, "radar"))
2377 radarOverlayWasSet = GEO_IsRadarOverlayActivated();
2378 }
2379
2380 /**
2381 * @brief Remove overlay.
2382 * @param[in] overlayID Name of the overlay you want to turn off.
2383 */
GEO_DeactivateOverlay(const char * overlayID)2384 void GEO_DeactivateOverlay (const char* overlayID)
2385 {
2386 if (Q_streq(overlayID, "nations")) {
2387 if (GEO_IsNationOverlayActivated())
2388 GEO_SetOverlay("nations");
2389 } else if (Q_streq(overlayID, "xvi")) {
2390 if (GEO_IsXVIOverlayActivated())
2391 GEO_SetOverlay("xvi");
2392 } else if (Q_streq(overlayID, "radar")) {
2393 if (GEO_IsRadarOverlayActivated())
2394 GEO_SetOverlay("radar");
2395 }
2396 }
2397
2398 /**
2399 * @brief Console command to call GEO_DeactivateOverlay.
2400 */
GEO_DeactivateOverlay_f(void)2401 static void GEO_DeactivateOverlay_f (void)
2402 {
2403 const char* arg;
2404
2405 if (cgi->Cmd_Argc() != 2) {
2406 Com_Printf("Usage: %s <nations|xvi|radar>\n", cgi->Cmd_Argv(0));
2407 return;
2408 }
2409
2410 arg = cgi->Cmd_Argv(1);
2411 GEO_DeactivateOverlay(arg);
2412 }
2413
2414 /**
2415 * @brief Initialise MAP/Geoscape
2416 */
GEO_InitStartup(void)2417 void GEO_InitStartup (void)
2418 {
2419 cgi->Cmd_AddCommand("multi_select_click", GEO_MultiSelectExecuteAction_f, nullptr);
2420 cgi->Cmd_AddCommand("map_overlay", GEO_SetOverlay_f, "Set the geoscape overlay");
2421 cgi->Cmd_AddCommand("map_deactivateoverlay", GEO_DeactivateOverlay_f, "Deactivate overlay");
2422 cgi->Cmd_AddCommand("map_selectobject", GEO_SelectObject_f, "Select an object and center on it");
2423 cgi->Cmd_AddCommand("mn_mapaction_reset", GEO_ResetAction, nullptr);
2424
2425 #ifdef DEBUG
2426 debug_showInterest = cgi->Cvar_Get("debug_showinterest", "0", CVAR_DEVELOPER, "Shows the global interest value on geoscape");
2427 #endif
2428 }
2429