1 /**
2 * @file
3 * @brief Nation code
4 * @note Functions with NAT_*
5 */
6
7 /*
8 Copyright (C) 2002-2013 UFO: Alien Invasion.
9
10 This program is free software; you can redistribute it and/or
11 modify it under the terms of the GNU General Public License
12 as published by the Free Software Foundation; either version 2
13 of the License, or (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
18
19 See the GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 */
25
26 #include "../../cl_shared.h"
27 #include "../../../shared/parse.h"
28 #include "cp_campaign.h"
29 #include "cp_geoscape.h"
30 #include "cp_ufo.h"
31 #include "cp_time.h"
32 #include "save/save_nation.h"
33 #include "../../ui/ui_main.h" /* ui_node_linechart.h */
34 #include "../../ui/node/ui_node_linechart.h" /* lineStrip_t */
35
NAT_GetNationByIDX(const int index)36 nation_t* NAT_GetNationByIDX (const int index)
37 {
38 assert(index >= 0);
39 assert(index < ccs.numNations);
40
41 return &ccs.nations[index];
42 }
43
44 /**
45 * @brief Return a nation-pointer by the nations id (nation_t->id) text.
46 * @param[in] nationID nation id as defined in (nation_t->id)
47 * @return nation_t pointer or nullptr if nothing found (=error).
48 */
NAT_GetNationByID(const char * nationID)49 nation_t* NAT_GetNationByID (const char* nationID)
50 {
51 int i;
52
53 if (!nationID) {
54 Com_Printf("NAT_GetNationByID: nullptr nationID\n");
55 return nullptr;
56 }
57 for (i = 0; i < ccs.numNations; i++) {
58 nation_t* nation = NAT_GetNationByIDX(i);
59 if (Q_streq(nation->id, nationID))
60 return nation;
61 }
62
63 Com_Printf("NAT_GetNationByID: Could not find nation '%s'\n", nationID);
64
65 /* No matching nation found - ERROR */
66 return nullptr;
67 }
68
69
70 /**
71 * @brief Lower happiness of nations depending on alien activity.
72 * @note Daily called
73 * @sa CP_BuildBaseGovernmentLeave
74 */
NAT_UpdateHappinessForAllNations(const float minhappiness)75 void NAT_UpdateHappinessForAllNations (const float minhappiness)
76 {
77 MIS_Foreach(mission) {
78 nation_t* nation = GEO_GetNation(mission->pos);
79 /* Difficulty modifier range is [0, 0.02f] */
80
81 /* Some non-water location have no nation */
82 if (nation) {
83 float happinessFactor;
84 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
85 switch (mission->stage) {
86 case STAGE_TERROR_MISSION:
87 case STAGE_SUBVERT_GOV:
88 case STAGE_RECON_GROUND:
89 case STAGE_SPREAD_XVI:
90 case STAGE_HARVEST:
91 happinessFactor = HAPPINESS_ALIEN_MISSION_LOSS;
92 break;
93 default:
94 /* mission is not active on earth or does not have any influence
95 * on the nation happiness, skip this mission */
96 continue;
97 }
98
99 NAT_SetHappiness(minhappiness, nation, stats->happiness + happinessFactor);
100 Com_DPrintf(DEBUG_CLIENT, "Happiness of nation %s decreased: %.02f\n", nation->name, stats->happiness);
101 }
102 }
103 }
104
105 /**
106 * @brief Get the actual funding of a nation
107 * @param[in] nation Pointer to the nation
108 * @param[in] month idx of the month -- 0 for current month
109 * @return actual funding of a nation
110 * @sa CL_NationsMaxFunding
111 */
NAT_GetFunding(const nation_t * const nation,int month)112 int NAT_GetFunding (const nation_t* const nation, int month)
113 {
114 assert(month >= 0);
115 assert(month < MONTHS_PER_YEAR);
116 return nation->maxFunding * nation->stats[month].happiness;
117 }
118
119 /**
120 * @brief Get the current month nation stats
121 * @param[in] nation Pointer to the nation
122 * @return The current month nation stats
123 */
NAT_GetCurrentMonthInfo(const nation_t * const nation)124 const nationInfo_t* NAT_GetCurrentMonthInfo (const nation_t* const nation)
125 {
126 return &nation->stats[0];
127 }
128
129 /**
130 * @brief Translates the nation happiness float value to a string
131 * @param[in] nation
132 * @return Translated happiness string
133 * @note happiness is between 0 and 1.0
134 */
NAT_GetHappinessString(const nation_t * nation)135 const char* NAT_GetHappinessString (const nation_t* nation)
136 {
137 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
138 if (stats->happiness < 0.015)
139 return _("Giving up");
140 else if (stats->happiness < 0.025)
141 return _("Furious");
142 else if (stats->happiness < 0.04)
143 return _("Angry");
144 else if (stats->happiness < 0.06)
145 return _("Mad");
146 else if (stats->happiness < 0.10)
147 return _("Upset");
148 else if (stats->happiness < 0.20)
149 return _("Tolerant");
150 else if (stats->happiness < 0.30)
151 return _("Neutral");
152 else if (stats->happiness < 0.50)
153 return _("Content");
154 else if (stats->happiness < 0.70)
155 return _("Pleased");
156 else if (stats->happiness < 0.95)
157 return _("Happy");
158 else
159 return _("Exuberant");
160 }
161
162 /**
163 * @brief Updates the nation happiness
164 * @param[in] minhappiness Minimum value of mean happiness before the game is lost
165 * @param[in] nation The nation to update the happiness for
166 * @param[in] happiness The new happiness value to set for the given nation
167 */
NAT_SetHappiness(const float minhappiness,nation_t * nation,const float happiness)168 void NAT_SetHappiness (const float minhappiness, nation_t* nation, const float happiness)
169 {
170 const char* oldString = NAT_GetHappinessString(nation);
171 const char* newString;
172 nationInfo_t* stats = &nation->stats[0];
173 const float oldHappiness = stats->happiness;
174 const float middleHappiness = (minhappiness + 1.0) / 2;
175 notify_t notifyType = NT_NUM_NOTIFYTYPE;
176
177 stats->happiness = happiness;
178 if (stats->happiness < 0.0f)
179 stats->happiness = 0.0f;
180 else if (stats->happiness > 1.0f)
181 stats->happiness = 1.0f;
182
183 newString = NAT_GetHappinessString(nation);
184
185 if (oldString != newString) {
186 Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer),
187 _("Nation %s changed happiness from %s to %s"), _(nation->name), oldString, newString);
188 notifyType = NT_HAPPINESS_CHANGED;
189 } else if (oldHappiness > middleHappiness && happiness < middleHappiness) {
190 Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer),
191 _("Nation %s changed happiness to %s"), _(nation->name), newString);
192 notifyType = NT_HAPPINESS_PLEASED;
193 } else if (happiness < minhappiness && oldHappiness > minhappiness) {
194 Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), /** @todo need to more specific message */
195 _("Happiness of nation %s is %s and less than minimal happiness allowed to the campaign"), _(nation->name), newString);
196 notifyType = NT_HAPPINESS_MIN;
197 } else {
198 return;
199 }
200
201 MSO_CheckAddNewMessage(notifyType, _("Nation changed happiness"), cp_messageBuffer);
202 }
203
204 /**
205 * @brief Nation saving callback
206 * @param[out] p XML Node structure, where we write the information to
207 */
NAT_SaveXML(xmlNode_t * p)208 bool NAT_SaveXML (xmlNode_t* p)
209 {
210 int i;
211 xmlNode_t* n = cgi->XML_AddNode(p, SAVE_NATION_NATIONS);
212
213 for (i = 0; i < ccs.numNations; i++) {
214 nation_t* nation = NAT_GetNationByIDX(i);
215 xmlNode_t* s;
216 int j;
217
218 if (!nation)
219 continue;
220
221 s = cgi->XML_AddNode(n, SAVE_NATION_NATION);
222 cgi->XML_AddString(s, SAVE_NATION_ID, nation->id);
223 for (j = 0; j < MONTHS_PER_YEAR; j++) {
224 const nationInfo_t* stats = &nation->stats[j];
225 xmlNode_t* ss;
226
227 if (!stats->inuse)
228 continue;
229
230 ss = cgi->XML_AddNode(s, SAVE_NATION_MONTH);
231 cgi->XML_AddInt(ss, SAVE_NATION_MONTH_IDX, j);
232 cgi->XML_AddFloat(ss, SAVE_NATION_HAPPINESS, stats->happiness);
233 cgi->XML_AddInt(ss, SAVE_NATION_XVI, stats->xviInfection);
234 }
235 }
236 return true;
237 }
238
239 /**
240 * @brief Nation loading xml callback
241 * @param[in] p XML Node structure, where we get the information from
242 */
NAT_LoadXML(xmlNode_t * p)243 bool NAT_LoadXML (xmlNode_t* p)
244 {
245 xmlNode_t* n;
246 xmlNode_t* s;
247
248 n = cgi->XML_GetNode(p, SAVE_NATION_NATIONS);
249 if (!n)
250 return false;
251
252 /* nations loop */
253 for (s = cgi->XML_GetNode(n, SAVE_NATION_NATION); s; s = cgi->XML_GetNextNode(s, n, SAVE_NATION_NATION)) {
254 xmlNode_t* ss;
255 nation_t* nation = NAT_GetNationByID(cgi->XML_GetString(s, SAVE_NATION_ID));
256
257 if (!nation)
258 return false;
259
260 /* month loop */
261 for (ss = cgi->XML_GetNode(s, SAVE_NATION_MONTH); ss; ss = cgi->XML_GetNextNode(ss, s, SAVE_NATION_MONTH)) {
262 int monthIDX = cgi->XML_GetInt(ss, SAVE_NATION_MONTH_IDX, MONTHS_PER_YEAR);
263 nationInfo_t* stats = &nation->stats[monthIDX];
264
265 if (monthIDX < 0 || monthIDX >= MONTHS_PER_YEAR)
266 return false;
267
268 stats->inuse = true;
269 stats->happiness = cgi->XML_GetFloat(ss, SAVE_NATION_HAPPINESS, 0.0);
270 stats->xviInfection = cgi->XML_GetInt(ss, SAVE_NATION_XVI, 0);
271 }
272 }
273 return true;
274 }
275
276 /*==========================================
277 Parsing
278 ==========================================*/
279
280 static const value_t nation_vals[] = {
281 {"name", V_TRANSLATION_STRING, offsetof(nation_t, name), 0},
282 {"pos", V_POS, offsetof(nation_t, pos), MEMBER_SIZEOF(nation_t, pos)},
283 {"color", V_COLOR, offsetof(nation_t, color), MEMBER_SIZEOF(nation_t, color)},
284 {"funding", V_INT, offsetof(nation_t, maxFunding), MEMBER_SIZEOF(nation_t, maxFunding)},
285 {"happiness", V_FLOAT, offsetof(nation_t, stats[0].happiness), MEMBER_SIZEOF(nation_t, stats[0].happiness)},
286 {"soldiers", V_INT, offsetof(nation_t, maxSoldiers), MEMBER_SIZEOF(nation_t, maxSoldiers)},
287 {"scientists", V_INT, offsetof(nation_t, maxScientists), MEMBER_SIZEOF(nation_t, maxScientists)},
288 {"workers", V_INT, offsetof(nation_t, maxWorkers), MEMBER_SIZEOF(nation_t, maxWorkers)},
289 {"pilots", V_INT, offsetof(nation_t, maxPilots), MEMBER_SIZEOF(nation_t, maxPilots)},
290
291 {nullptr, V_NULL, 0, 0}
292 };
293
294 /**
295 * @brief Parse the nation data from script file
296 * @param[in] name Name or ID of the found nation
297 * @param[in] text The text of the nation node
298 * @sa nation_vals
299 * @sa CL_ParseScriptFirst
300 * @note write into cp_campaignPool - free on every game restart and reparse
301 */
CL_ParseNations(const char * name,const char ** text)302 void CL_ParseNations (const char* name, const char** text)
303 {
304 nation_t* nation;
305 int i;
306
307 if (ccs.numNations >= MAX_NATIONS) {
308 Com_Printf("CL_ParseNations: nation number exceeding maximum number of nations: %i\n", MAX_NATIONS);
309 return;
310 }
311
312 /* search for nations with same name */
313 for (i = 0; i < ccs.numNations; i++) {
314 const nation_t* n = NAT_GetNationByIDX(i);
315 if (Q_streq(name, n->id))
316 break;
317 }
318 if (i < ccs.numNations) {
319 Com_Printf("CL_ParseNations: nation def \"%s\" with same name found, second ignored\n", name);
320 return;
321 }
322
323 /* initialize the nation */
324 nation = &ccs.nations[ccs.numNations];
325 OBJZERO(*nation);
326 nation->idx = ccs.numNations;
327 nation->stats[0].inuse = true;
328
329 if (Com_ParseBlock(name, text, nation, nation_vals, cp_campaignPool)) {
330 ccs.numNations++;
331
332 Com_DPrintf(DEBUG_CLIENT, "...found nation %s\n", name);
333 nation->id = Mem_PoolStrDup(name, cp_campaignPool, 0);
334 }
335 }
336
337 /**
338 * @brief Finds a city by it's scripted identifier
339 * @param[in] cityId Scripted ID of the city
340 */
CITY_GetById(const char * cityId)341 city_t* CITY_GetById (const char* cityId)
342 {
343 LIST_Foreach(ccs.cities, city_t, city) {
344 if (Q_streq(cityId, city->id))
345 return city;
346 }
347 return nullptr;
348 }
349
350 /**
351 * @brief Finds a city by it's geoscape coordinates
352 * @param[in] pos Position of the city
353 */
CITY_GetByPos(vec2_t pos)354 city_t* CITY_GetByPos (vec2_t pos)
355 {
356 LIST_Foreach(ccs.cities, city_t, city) {
357 if (Vector2Equal(pos, city->pos))
358 return city;
359 }
360 return nullptr;
361 }
362
363 static const value_t city_vals[] = {
364 {"name", V_TRANSLATION_STRING, offsetof(city_t, name), 0},
365 {"pos", V_POS, offsetof(city_t, pos), MEMBER_SIZEOF(city_t, pos)},
366
367 {nullptr, V_NULL, 0, 0}
368 };
369
370 /**
371 * @brief Parse the city data from script file
372 * @param[in] name ID of the found nation
373 * @param[in] text The text of the nation node
374 */
CITY_Parse(const char * name,const char ** text)375 void CITY_Parse (const char* name, const char** text)
376 {
377 city_t newCity;
378
379 /* search for cities with same name */
380 if (CITY_GetById(name)) {
381 Com_Printf("CITY_Parse: city def \"%s\" with same name found, second ignored\n", name);
382 return;
383 }
384
385 OBJZERO(newCity);
386 newCity.idx = ccs.numCities;
387
388 if (Com_ParseBlock(name, text, &newCity, city_vals, cp_campaignPool)) {
389 ccs.numCities++;
390 newCity.id = Mem_PoolStrDup(name, cp_campaignPool, 0);
391 /* Add city to the list */
392 LIST_Add(&ccs.cities, newCity);
393 }
394 }
395
396 /**
397 * @brief Checks the parsed nations and cities for errors
398 * @return false if there are errors - true otherwise
399 */
NAT_ScriptSanityCheck(void)400 bool NAT_ScriptSanityCheck (void)
401 {
402 int error = 0;
403
404 /* Check if there is at least one map fitting city parameter for terror mission */
405 LIST_Foreach(ccs.cities, city_t, city) {
406 bool cityCanBeUsed = false;
407 bool parametersFit = false;
408 ufoType_t ufoTypes[UFO_MAX];
409 int numTypes;
410 const mapDef_t* md;
411
412 if (!city->name) {
413 error++;
414 Com_Printf("...... city '%s' has no name\n", city->id);
415 }
416
417 if (MapIsWater(GEO_GetColor(city->pos, MAPTYPE_TERRAIN, nullptr))) {
418 error++;
419 Com_Printf("...... city '%s' has a position in the water\n", city->id);
420 }
421
422 numTypes = CP_TerrorMissionAvailableUFOs(nullptr, ufoTypes);
423
424 MapDef_ForeachSingleplayerCampaign(md) {
425 if (md->storyRelated)
426 continue;
427
428 if (GEO_PositionFitsTCPNTypes(city->pos, md->terrains, md->cultures, md->populations, nullptr)) {
429 int i;
430 /* this map fits city parameter, check if we have some terror mission UFOs available for this map */
431 parametersFit = true;
432
433 /* no UFO on this map (LIST_ContainsString doesn't like empty string) */
434 if (!md->ufos) {
435 continue;
436 }
437
438 /* loop must be backward, as we remove items */
439 for (i = numTypes - 1 ; i >= 0; i--) {
440 if (cgi->LIST_ContainsString(md->ufos, cgi->Com_UFOTypeToShortName(ufoTypes[i]))) {
441 REMOVE_ELEM(ufoTypes, i, numTypes);
442 }
443 }
444 }
445 if (numTypes == 0) {
446 cityCanBeUsed = true;
447 break;
448 }
449 }
450
451 if (!cityCanBeUsed) {
452 error++;
453 Com_Printf("...... city '%s' can't be used in game: it has no map fitting parameters\n", city->id);
454 if (parametersFit) {
455 int i;
456 Com_Printf(" (No map fitting");
457 for (i = 0 ; i < numTypes; i++)
458 Com_Printf(" %s", cgi->Com_UFOTypeToShortName(ufoTypes[i]));
459 Com_Printf(")\n");
460 }
461 GEO_PrintParameterStringByPos(city->pos);
462 }
463 }
464
465 return !error;
466 }
467
468 /*=====================================
469 Menu functions
470 =====================================*/
471
472
CP_NationStatsClick_f(void)473 static void CP_NationStatsClick_f (void)
474 {
475 int num;
476
477 if (cgi->Cmd_Argc() < 2) {
478 Com_Printf("Usage: %s <num>\n", cgi->Cmd_Argv(0));
479 return;
480 }
481
482 /* Which entry in the list? */
483 num = atoi(cgi->Cmd_Argv(1));
484 if (num < 0 || num >= ccs.numNations)
485 return;
486
487 cgi->UI_PushWindow("nations");
488 cgi->Cbuf_AddText("nation_select %i\n", num);
489 }
490
491 /** Space for month-lines with 12 points for each nation. */
492 static screenPoint_t fundingPts[MAX_NATIONS][MONTHS_PER_YEAR];
493 static int usedFundPtslist = 0;
494 /** Space for 1 line (2 points) for each nation. */
495 static screenPoint_t colorLinePts[MAX_NATIONS][2];
496 static int usedColPtslists = 0;
497
498 static const vec4_t graphColors[MAX_NATIONS] = {
499 {1.0, 0.5, 0.5, 1.0},
500 {0.5, 1.0, 0.5, 1.0},
501 {0.5, 0.5, 1.0, 1.0},
502 {1.0, 0.0, 0.0, 1.0},
503 {0.0, 1.0, 0.0, 1.0},
504 {0.0, 0.0, 1.0, 1.0},
505 {1.0, 1.0, 0.0, 1.0},
506 {0.0, 1.0, 1.0, 1.0}
507 };
508 static const vec4_t graphColorSelected = {1, 1, 1, 1};
509
510 /**
511 * @brief Search the maximum (current) funding from all the nations (in all logged months).
512 * @note nation->maxFunding is _not_ the real funding value.
513 * @return The maximum funding value.
514 * @todo Extend to other values?
515 * @sa NAT_GetFunding
516 */
CL_NationsMaxFunding(void)517 static int CL_NationsMaxFunding (void)
518 {
519 int m, n;
520 int max = 0;
521
522 for (n = 0; n < ccs.numNations; n++) {
523 const nation_t* nation = NAT_GetNationByIDX(n);
524 for (m = 0; m < MONTHS_PER_YEAR; m++) {
525 if (nation->stats[m].inuse) {
526 const int funding = NAT_GetFunding(nation, m);
527 if (max < funding)
528 max = funding;
529 } else {
530 /* Abort this months-loops */
531 break;
532 }
533 }
534 }
535 return max;
536 }
537
538 static int selectedNation = 0;
539
540 static lineStrip_t fundingLineStrip[MAX_NATIONS];
541
542 /**
543 * @brief Draws a graph for the funding values over time.
544 * @param[in] nation The nation to draw the graph for.
545 * @param[in] node A pointer to a "linestrip" node that we want to draw in.
546 * @param[out] funding Funding graph data in a lineStrip_t
547 * @param[in] maxFunding The upper limit of the graph - all values will be scaled to this one.
548 * @param[in] color If this is -1 draw the line for the current selected nation
549 * @todo Somehow the display of the months isn't really correct right now (straight line :-/)
550 */
CL_NationDrawStats(const nation_t * nation,uiNode_t * node,lineStrip_t * funding,int maxFunding,int color)551 static void CL_NationDrawStats (const nation_t* nation, uiNode_t* node, lineStrip_t* funding, int maxFunding, int color)
552 {
553 int width, height, dx;
554 int m;
555 int minFunding = 0;
556 int ptsNumber = 0;
557 float dy;
558
559 if (!nation || !node)
560 return;
561
562 /** @todo should be into the chart node code */
563 width = (int)node->box.size[0];
564 height = (int)node->box.size[1];
565 dx = (int)(width / MONTHS_PER_YEAR);
566
567 if (minFunding != maxFunding)
568 dy = (float) height / (maxFunding - minFunding);
569 else
570 dy = 0;
571
572 /* Generate pointlist. */
573 /** @todo Sort this in reverse? -> Having current month on the right side? */
574 for (m = 0; m < MONTHS_PER_YEAR; m++) {
575 if (nation->stats[m].inuse) {
576 const int fund = NAT_GetFunding(nation, m);
577 fundingPts[usedFundPtslist][m].x = (m * dx);
578 fundingPts[usedFundPtslist][m].y = height - dy * (fund - minFunding);
579 ptsNumber++;
580 } else {
581 break;
582 }
583 }
584
585 /* Guarantee displayable data even for only one month */
586 if (ptsNumber == 1) {
587 /* Set the second point half the distance to the next month to the right - small horizontal line. */
588 fundingPts[usedFundPtslist][1].x = fundingPts[usedFundPtslist][0].x + (int)(0.5 * width / MONTHS_PER_YEAR);
589 fundingPts[usedFundPtslist][1].y = fundingPts[usedFundPtslist][0].y;
590 ptsNumber++;
591 }
592
593 /* Link graph to node */
594 funding->pointList = (int*)fundingPts[usedFundPtslist];
595 funding->numPoints = ptsNumber;
596 if (color < 0) {
597 const nation_t* nation = NAT_GetNationByIDX(selectedNation);
598 cgi->Cvar_Set("mn_nat_symbol", "nations/%s", nation->id);
599 Vector4Copy(graphColorSelected, funding->color);
600 } else {
601 Vector4Copy(graphColors[color], funding->color);
602 }
603
604 usedFundPtslist++;
605 }
606
607 static lineStrip_t colorLineStrip[MAX_NATIONS];
608
609 /**
610 * @brief Shows the current nation list + statistics.
611 * @note See menu_stats.ufo
612 */
CL_NationStatsUpdate_f(void)613 static void CL_NationStatsUpdate_f (void)
614 {
615 int i;
616 uiNode_t* colorNode;
617 uiNode_t* graphNode;
618 int dy = 10;
619
620 usedColPtslists = 0;
621
622 colorNode = cgi->UI_GetNodeByPath("nations.nation_graph_colors");
623 if (colorNode) {
624 dy = (int)(colorNode->box.size[1] / MAX_NATIONS);
625 }
626
627 for (i = 0; i < ccs.numNations; i++) {
628 const nation_t* nation = NAT_GetNationByIDX(i);
629 lineStrip_t* color = &colorLineStrip[i];
630 const int funding = NAT_GetFunding(nation, 0);
631
632 OBJZERO(*color);
633
634 if (i > 0)
635 colorLineStrip[i - 1].next = color;
636
637 if (selectedNation == i) {
638 cgi->UI_ExecuteConfunc("nation_marksel %i", i);
639 } else {
640 cgi->UI_ExecuteConfunc("nation_markdesel %i", i);
641 }
642 cgi->Cvar_Set(va("mn_nat_name%i",i), "%s", _(nation->name));
643 cgi->Cvar_Set(va("mn_nat_fund%i",i), _("%i c"), funding);
644
645 if (colorNode) {
646 colorLinePts[usedColPtslists][0].x = 0;
647 colorLinePts[usedColPtslists][0].y = (int)colorNode->box.size[1] - (int)colorNode->box.size[1] + dy * i;
648 colorLinePts[usedColPtslists][1].x = (int)colorNode->box.size[0];
649 colorLinePts[usedColPtslists][1].y = colorLinePts[usedColPtslists][0].y;
650
651 color->pointList = (int*)colorLinePts[usedColPtslists];
652 color->numPoints = 2;
653
654 if (i == selectedNation) {
655 Vector4Copy(graphColorSelected, color->color);
656 } else {
657 Vector4Copy(graphColors[i], color->color);
658 }
659
660 usedColPtslists++;
661 }
662 }
663
664 cgi->UI_RegisterLineStrip(LINESTRIP_COLOR, &colorLineStrip[0]);
665
666 /* Hide unused nation-entries. */
667 for (i = ccs.numNations; i < MAX_NATIONS; i++) {
668 cgi->UI_ExecuteConfunc("nation_hide %i", i);
669 }
670
671 /** @todo Display summary of nation info */
672
673 /* Display graph of nations-values so far. */
674 graphNode = cgi->UI_GetNodeByPath("nations.nation_graph_funding");
675 if (graphNode) {
676 const int maxFunding = CL_NationsMaxFunding();
677 usedFundPtslist = 0;
678 for (i = 0; i < ccs.numNations; i++) {
679 const nation_t* nation = NAT_GetNationByIDX(i);
680 lineStrip_t* funding = &fundingLineStrip[i];
681
682 /* init the structure */
683 OBJZERO(*funding);
684
685 if (i > 0)
686 fundingLineStrip[i - 1].next = funding;
687
688 if (i == selectedNation) {
689 CL_NationDrawStats(nation, graphNode, funding, maxFunding, -1);
690 } else {
691 CL_NationDrawStats(nation, graphNode, funding, maxFunding, i);
692 }
693 }
694
695 cgi->UI_RegisterLineStrip(LINESTRIP_FUNDING, &fundingLineStrip[0]);
696 }
697 }
698
699 /**
700 * @brief Select nation and display all relevant information for it.
701 */
CL_NationSelect_f(void)702 static void CL_NationSelect_f (void)
703 {
704 int nat;
705
706 if (cgi->Cmd_Argc() < 2) {
707 Com_Printf("Usage: %s <nat_idx>\n", cgi->Cmd_Argv(0));
708 return;
709 }
710
711 nat = atoi(cgi->Cmd_Argv(1));
712 if (nat < 0 || nat >= ccs.numNations) {
713 Com_Printf("Invalid nation index: %is\n",nat);
714 return;
715 }
716
717 selectedNation = nat;
718 CL_NationStatsUpdate_f();
719 }
720
721 #ifdef DEBUG
722 /**
723 * @brief Debug function to list all cities in game.
724 * @note Called with debug_listcities.
725 */
NAT_ListCities_f(void)726 static void NAT_ListCities_f (void)
727 {
728 LIST_Foreach(ccs.cities, city_t, city) {
729 Com_Printf("City '%s' -- position (%0.1f, %0.1f)\n", city->id, city->pos[0], city->pos[1]);
730 GEO_PrintParameterStringByPos(city->pos);
731 }
732 }
733
734 /**
735 * @brief Scriptfunction to list all parsed nations with their current values
736 * @note called with debug_listnations
737 */
NAT_NationList_f(void)738 static void NAT_NationList_f (void)
739 {
740 int i;
741 for (i = 0; i < ccs.numNations; i++) {
742 const nation_t* nation = &ccs.nations[i];
743 Com_Printf("Nation ID: %s\n", nation->id);
744 Com_Printf("...max-funding %i c\n", nation->maxFunding);
745 Com_Printf("...happiness %0.2f\n", nation->stats[0].happiness);
746 Com_Printf("...xviInfection %i\n", nation->stats[0].xviInfection);
747 Com_Printf("...max-soldiers %i\n", nation->maxSoldiers);
748 Com_Printf("...max-scientists %i\n", nation->maxScientists);
749 Com_Printf("...max-workers %i\n", nation->maxWorkers);
750 Com_Printf("...max-pilots %i\n", nation->maxPilots);
751 Com_Printf("...color r:%.2f g:%.2f b:%.2f a:%.2f\n", nation->color[0], nation->color[1], nation->color[2], nation->color[3]);
752 Com_Printf("...pos x:%.0f y:%.0f\n", nation->pos[0], nation->pos[1]);
753 }
754 }
755 #endif
756
757 /**
758 * @brief Update the nation data from all parsed nation each month
759 * @note give us nation support by:
760 * * credits
761 * * new soldiers
762 * * new scientists
763 * * new pilots
764 * @note Called from CP_CampaignRun
765 * @sa CP_CampaignRun
766 * @sa B_CreateEmployee
767 * @todo This is very redundant with CP_StatsUpdate_f. Investigate and clean up.
768 */
NAT_HandleBudget(const campaign_t * campaign)769 void NAT_HandleBudget (const campaign_t* campaign)
770 {
771 char message[1024];
772 int cost;
773 int totalIncome = 0;
774 int totalExpenditure = 0;
775 int initialCredits = ccs.credits;
776 const salary_t* salary = &campaign->salaries;
777
778 /* Refreshes the pilot global list. Pilots who are already hired are unchanged, but all other
779 * pilots are replaced. The new pilots is evenly distributed between the nations that are happy (happiness > 0). */
780 E_RefreshUnhiredEmployeeGlobalList(EMPL_PILOT, true);
781
782 for (int i = 0; i < ccs.numNations; i++) {
783 const nation_t* nation = NAT_GetNationByIDX(i);
784 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
785 const int funding = NAT_GetFunding(nation, 0);
786 int newScientists = 0, newSoldiers = 0, newPilots = 0, newWorkers = 0;
787
788 totalIncome += funding;
789
790 for (int j = 0; 0.25 + j < (float) nation->maxScientists * stats->happiness * ccs.curCampaign->employeeRate; j++) {
791 /* Create a scientist, but don't auto-hire her. */
792 E_CreateEmployee(EMPL_SCIENTIST, nation, nullptr);
793 newScientists++;
794 }
795
796 if (stats->happiness > 0) {
797 for (int j = 0; 0.25 + j < (float) nation->maxSoldiers * stats->happiness * ccs.curCampaign->employeeRate; j++) {
798 /* Create a soldier. */
799 E_CreateEmployee(EMPL_SOLDIER, nation, nullptr);
800 newSoldiers++;
801 }
802 }
803 /* pilots */
804 if (stats->happiness > 0) {
805 for (int j = 0; 0.25 + j < (float) nation->maxPilots * stats->happiness * ccs.curCampaign->employeeRate; j++) {
806 /* Create a pilot. */
807 E_CreateEmployee(EMPL_PILOT, nation, nullptr);
808 newPilots++;
809 }
810 }
811
812 for (int j = 0; 0.25 + j * 2 < (float) nation->maxWorkers * stats->happiness * ccs.curCampaign->employeeRate; j++) {
813 /* Create a worker. */
814 E_CreateEmployee(EMPL_WORKER, nation, nullptr);
815 newWorkers++;
816 }
817
818 Com_sprintf(message, sizeof(message), _("Gained %i %s, %i %s, %i %s, %i %s, and %i %s from nation %s (%s)"),
819 funding, ngettext("credit", "credits", funding),
820 newScientists, ngettext("scientist", "scientists", newScientists),
821 newSoldiers, ngettext("soldier", "soldiers", newSoldiers),
822 newPilots, ngettext("pilot", "pilots", newPilots),
823 newWorkers, ngettext("worker", "workers", newWorkers),
824 _(nation->name), NAT_GetHappinessString(nation));
825 MS_AddNewMessage(_("Notice"), message);
826 }
827
828 for (int i = 0; i < MAX_EMPL; i++) {
829 cost = 0;
830 E_Foreach(i, employee) {
831 if (!employee->isHired())
832 continue;
833 const rank_t* rank = CL_GetRankByIdx(employee->chr.score.rank);
834 cost += CP_GetSalaryBaseEmployee(salary, employee->getType())
835 + rank->level * CP_GetSalaryRankBonusEmployee(salary, employee->getType());
836 }
837 totalExpenditure += cost;
838
839 if (cost == 0)
840 continue;
841
842 Com_sprintf(message, sizeof(message), _("Paid %i credits to: %s"), cost, E_GetEmployeeString((employeeType_t)i, 1));
843 MS_AddNewMessage(_("Notice"), message);
844 }
845
846 cost = 0;
847 AIR_Foreach(aircraft) {
848 if (aircraft->status == AIR_CRASHED)
849 continue;
850 cost += aircraft->price * salary->aircraftFactor / salary->aircraftDivisor;
851 }
852 totalExpenditure += cost;
853
854 if (cost != 0) {
855 Com_sprintf(message, sizeof(message), _("Paid %i credits for aircraft"), cost);
856 MS_AddNewMessage(_("Notice"), message);
857 }
858
859 base_t* base = nullptr;
860 while ((base = B_GetNext(base)) != nullptr) {
861 cost = CP_GetSalaryUpKeepBase(salary, base);
862 totalExpenditure += cost;
863
864 Com_sprintf(message, sizeof(message), _("Paid %i credits for upkeep of %s"), cost, base->name);
865 MS_AddNewMessage(_("Notice"), message);
866 }
867
868 cost = CP_GetSalaryAdministrative(salary);
869 Com_sprintf(message, sizeof(message), _("Paid %i credits for administrative overhead."), cost);
870 totalExpenditure += cost;
871 MS_AddNewMessage(_("Notice"), message);
872
873 if (initialCredits < 0) {
874 const float interest = initialCredits * campaign->salaries.debtInterest;
875
876 cost = (int)ceil(interest);
877 Com_sprintf(message, sizeof(message), _("Paid %i credits in interest on your debt."), cost);
878 totalExpenditure += cost;
879 MS_AddNewMessage(_("Notice"), message);
880 }
881 CP_UpdateCredits(ccs.credits - totalExpenditure + totalIncome);
882 CP_GameTimeStop();
883 }
884
885 /**
886 * @brief Backs up each nation's relationship values.
887 * @note Right after the copy the stats for the current month are the same as the ones from the (end of the) previous month.
888 * They will change while the curent month is running of course :)
889 * @todo other stuff to back up?
890 */
NAT_BackupMonthlyData(void)891 void NAT_BackupMonthlyData (void)
892 {
893 /**
894 * Back up nation relationship .
895 * "inuse" is copied as well so we do not need to set it anywhere.
896 */
897 for (int nat = 0; nat < ccs.numNations; nat++) {
898 nation_t* nation = NAT_GetNationByIDX(nat);
899
900 for (int i = MONTHS_PER_YEAR - 1; i > 0; i--) { /* Reverse copy to not overwrite with wrong data */
901 nation->stats[i] = nation->stats[i - 1];
902 }
903 }
904 }
905
906 /**
907 * @brief Init actions for nation-subsystem
908 */
NAT_InitStartup(void)909 void NAT_InitStartup (void)
910 {
911 cgi->Cmd_AddCommand("nation_stats_click", CP_NationStatsClick_f, nullptr);
912 cgi->Cmd_AddCommand("nation_update", CL_NationStatsUpdate_f, "Shows the current nation list + statistics.");
913 cgi->Cmd_AddCommand("nation_select", CL_NationSelect_f, "Select nation and display all relevant information for it.");
914 #ifdef DEBUG
915 cgi->Cmd_AddCommand("debug_listcities", NAT_ListCities_f, "Debug function to list all cities in game.");
916 cgi->Cmd_AddCommand("debug_listnations", NAT_NationList_f, "List all nations on the game console");
917 #endif
918 }
919
920 /**
921 * @brief Closing actions for nation-subsystem
922 */
NAT_Shutdown(void)923 void NAT_Shutdown (void)
924 {
925 cgi->LIST_Delete(&ccs.cities);
926
927 cgi->Cmd_RemoveCommand("nation_stats_click");
928 cgi->Cmd_RemoveCommand("nation_update");
929 cgi->Cmd_RemoveCommand("nation_select");
930 #ifdef DEBUG
931 cgi->Cmd_RemoveCommand("debug_listcities");
932 cgi->Cmd_RemoveCommand("debug_listnations");
933 #endif
934 }
935