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