1 /**
2  * @file
3  * @brief single player automatic (quick, simulated) missions, without going to the battlescape.
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 "../../cl_inventory.h"
27 #include "cp_auto_mission.h"
28 #include "cp_campaign.h"
29 #include "cp_geoscape.h"
30 #include "cp_missions.h"
31 #include "cp_mission_triggers.h"
32 #include "../../../shared/mathlib_extra.h"
33 #include "math.h"
34 #include "cp_mission_callbacks.h"
35 
36 /**
37  * @brief Possible types of teams that can fight in an auto mission battle.
38  */
39 typedef enum autoMission_teamType_s {
40 	AUTOMISSION_TEAM_TYPE_PLAYER,				/**< Human player-controlled team.  Includes soldiers as well as downed pilots. */
41 	AUTOMISSION_TEAM_TYPE_ALIEN,				/**< AI-controlled alien team. */
42 	AUTOMISSION_TEAM_TYPE_CIVILIAN,				/**< AI-controlled civilians that can be healthy or infected. */
43 
44 	AUTOMISSION_TEAM_TYPE_MAX
45 } autoMissionTeamType_t;
46 
47 #define MAX_SOLDIERS_AUTOMISSION MAX_TEAMS * AUTOMISSION_TEAM_TYPE_MAX
48 
49 /**
50  * @brief One unit (soldier/alien/civilian) of the autobattle
51  * @todo add attack and defence scores
52  */
53 typedef struct autoUnit_s {
54 	int idx;
55 	autoMissionTeamType_t team;					/**< Team of the unit */
56 	character_t* chr;							/**< Character */
57 	double attackStrength;						/**< How good at attacking a unit is, from 0.0 (can't fight at all) to 1.0 (Higher is better) */
58 	double defendStrength;						/**< A unit's armor or protection, from 0.0 (no armor, can't even dodge/stationary) to 1.0 (Higher is better) */
59 } autoUnit_t;
60 
61 /**
62  * @brief Data structure for a simulated or auto mission.
63  */
64 typedef struct autoMissionBattle_s {
65 	bool isHostile[AUTOMISSION_TEAM_TYPE_MAX][AUTOMISSION_TEAM_TYPE_MAX];		/**< Friendly or hostile?  Params are [Each team] [Each other team] */
66 	short nUnits[AUTOMISSION_TEAM_TYPE_MAX];							/**< Number of units (soldiers, aliens, UGVs, whatever) on each team, hardcoded MAX of 64 per team */
67 	short actUnits[AUTOMISSION_TEAM_TYPE_MAX];							/**< Number of active units (soldiers, aliens, UGVs, whatever) on each team, hardcoded MAX of 64 per team */
68 
69 	double scoreTeamEquipment[AUTOMISSION_TEAM_TYPE_MAX];			/**< Number from 0.f to 1.f, represents how good a team's equipment is (higher is better) */
70 	double scoreTeamSkill[AUTOMISSION_TEAM_TYPE_MAX];				/**< Number from 0.f to 1.f, represents how good a team's abilities are (higher is better) */
71 	double scoreTeamDifficulty[AUTOMISSION_TEAM_TYPE_MAX];		/**< Number from 0.f to 1.f, represents a team's global fighting ability, difficulty, or misc. adjustments (higher is better) */
72 
73 	autoUnit_t units[AUTOMISSION_TEAM_TYPE_MAX][MAX_SOLDIERS_AUTOMISSION];		/**< Units data */
74 
75 	int teamAccomplishment[AUTOMISSION_TEAM_TYPE_MAX];							/**< Used for calculating experience gain, and for friendly fire (including hit civilians) */
76 
77 	int winningTeam;								/**< Which team is victorious */
78 	missionResults_t* results;						/**< Manual mission "compatible" result structure */
79 } autoMissionBattle_t;
80 
81 /**
82  * @brief Constants for automission experience gain factors
83  * @todo make these scripted in campaign definitions maybe
84 */
85 #define SKILL_AWARD_SCALE 0.3f
86 #define ABILITY_AWARD_SCALE 0.06f
87 
88 #define AM_IsPlayer(type) ((type) == AUTOMISSION_TEAM_TYPE_PLAYER)
89 #define AM_IsAlien(type)  ((type) == AUTOMISSION_TEAM_TYPE_ALIEN)
90 #define AM_IsCivilian(type) ((type) == AUTOMISSION_TEAM_TYPE_CIVILIAN)
91 #define AM_SetHostile(battle, team, otherTeam, value) (battle)->isHostile[(team)][(otherTeam)] = (value)
92 #define AM_IsHostile(battle, team, otherTeam) ((battle)->isHostile[(team)][(otherTeam)])
93 
94 #define AM_GetUnit(battle, teamIdx, unitIdx) (&battle->units[teamIdx][unitIdx])
95 #define AM_IsUnitActive(unit) (((unit)->chr->HP > 0) && ((unit)->chr->HP > (unit)->chr->STUN))
96 
97 /**
98  * @brief Clears, initializes, or resets a single auto mission, sets default values.
99  * @param[in,out] battle The battle that should be initialized to defaults
100  */
AM_ClearBattle(autoMissionBattle_t * battle)101 static void AM_ClearBattle (autoMissionBattle_t* battle)
102 {
103 	int team;
104 
105 	assert(battle != nullptr);
106 
107 	OBJZERO(*battle);
108 
109 	for (team = 0; team < AUTOMISSION_TEAM_TYPE_MAX; team++) {
110 		int otherTeam;
111 
112 		battle->scoreTeamDifficulty[team] = 0.5;
113 		battle->scoreTeamEquipment[team] = 0.5;
114 		battle->scoreTeamSkill[team] = 0.5;
115 
116 		for (otherTeam = 0; otherTeam < AUTOMISSION_TEAM_TYPE_MAX; otherTeam++) {
117 			/* If you forget to set this and run a battle, everyone will just kill each other by default */
118 			battle->isHostile[team][otherTeam] = true;
119 		}
120 	}
121 
122 	battle->winningTeam = -1;
123 	battle->results = nullptr;
124 }
125 
126 /**
127  * @brief Adds team data for a specified team in an auto-mission object, from a (player) aircraft.
128  * @param[in,out] battle The auto mission battle to add team data to
129  * @param[in] teamNum The team number in the auto mission instance to update
130  * @param[in] aircraft The aircraft to get data from
131  * @param[in] campaign The current campaign (for retrieving difficulty level)
132  * @note This function actually gets the data from the campaign object, using the aircraft data to
133  * find out which of all the employees are on the aircraft (in the mission)
134  */
AM_FillTeamFromAircraft(autoMissionBattle_t * battle,const autoMissionTeamType_t teamNum,const aircraft_t * aircraft,const campaign_t * campaign)135 static void AM_FillTeamFromAircraft (autoMissionBattle_t* battle, const autoMissionTeamType_t teamNum, const aircraft_t* aircraft, const campaign_t* campaign)
136 {
137 	int teamSize;
138 	int unitsAlive;
139 
140 	assert(teamNum < AUTOMISSION_TEAM_TYPE_MAX);
141 	assert(battle != nullptr);
142 	assert(aircraft != nullptr);
143 
144 	teamSize = 0;
145 	unitsAlive = 0;
146 	LIST_Foreach(aircraft->acTeam, Employee, employee) {
147 		autoUnit_t* unit = AM_GetUnit(battle, teamNum, teamSize);
148 
149 		unit->chr = &employee->chr;
150 		unit->team = teamNum;
151 		unit->idx = teamSize;
152 
153 		teamSize++;
154 		if (employee->chr.HP > 0)
155 			unitsAlive++;
156 
157 		if (teamSize >= MAX_SOLDIERS_AUTOMISSION)
158 			break;
159 	}
160 	battle->nUnits[teamNum] = teamSize;
161 	battle->actUnits[teamNum] = unitsAlive;
162 
163 	if (teamSize == 0) {
164 		Com_DPrintf(DEBUG_CLIENT, "Warning: Attempt to add soldiers to an auto-mission from an aircraft with no soldiers onboard.\n");
165 		Com_DPrintf(DEBUG_CLIENT, "--- Note: Aliens might win this mission by default because they are un-challenged, with no resistance!\n");
166 	}
167 	if (unitsAlive == 0) {
168 		Com_DPrintf(DEBUG_CLIENT, "Warning: Attempt to add team to auto battle where all the units on the team are DEAD!\n");
169 		Com_DPrintf(DEBUG_CLIENT, "--- Note: This team will LOSE the battle by default.\n");
170 	}
171 
172 	/* NOTE:  For now these are hard-coded to values based upon general campaign difficulty.
173 	 * --- In the future, it might be easier to set this according to a scripted value in a .ufo
174 	 * --- file, with other campaign info.  Reminder:  Higher floating point values mean better
175 	 * --- soldiers, and therefore make an easier fight for the player. */
176 	switch (campaign->difficulty) {
177 	case 4:
178 		battle->scoreTeamDifficulty[teamNum] = 0.30;
179 		break;
180 	case 3:
181 		battle->scoreTeamDifficulty[teamNum] = 0.35;
182 		break;
183 	case 2:
184 		battle->scoreTeamDifficulty[teamNum] = 0.40;
185 		break;
186 	case 1:
187 		battle->scoreTeamDifficulty[teamNum] = 0.45;
188 		break;
189 	case 0:
190 		battle->scoreTeamDifficulty[teamNum] = 0.50;
191 		break;
192 	case -1:
193 		battle->scoreTeamDifficulty[teamNum] = 0.55;
194 		break;
195 	case -2:
196 		battle->scoreTeamDifficulty[teamNum] = 0.60;
197 		break;
198 	case -3:
199 		battle->scoreTeamDifficulty[teamNum] = 0.65;
200 		break;
201 	case -4:
202 		battle->scoreTeamDifficulty[teamNum] = 0.70;
203 		break;
204 	default:
205 		battle->scoreTeamDifficulty[teamNum] = 0.50;
206 	}
207 }
208 
209 /**
210  * @brief Create character for a Unit
211  * @param[out] unit The unit to create character for
212  * @param[in] teamDef The team definition of the unit
213  * @param[in] ed The equipment to use
214  * @sa AM_DestroyUnitChr
215  */
AM_CreateUnitChr(autoUnit_t * unit,const teamDef_t * teamDef,const equipDef_t * ed)216 static void AM_CreateUnitChr (autoUnit_t* unit, const teamDef_t* teamDef, const equipDef_t* ed)
217 {
218 	unit->chr = Mem_PoolAllocType(character_t, cp_campaignPool);
219 	cgi->CL_GenerateCharacter(unit->chr, teamDef->id);
220 
221 	if (ed) {
222 		if (teamDef->robot && teamDef->onlyWeapon) {
223 			const objDef_t* weapon = teamDef->onlyWeapon;
224 			if (weapon->numAmmos > 0)
225 				cgi->INV_EquipActorRobot(&unit->chr->inv, weapon);
226 			else if (weapon->fireTwoHanded)
227 				cgi->INV_EquipActorMelee(&unit->chr->inv, teamDef);
228 			else
229 				Com_Printf("AM_CreateUnitChr: weapon %s has no ammo assigned and must not be fired two handed\n", weapon->id);
230 		} else {
231 			/* Pack equipment. */
232 			if (teamDef->weapons)
233 				cgi->INV_EquipActor(unit->chr, ed, cgi->GAME_GetChrMaxLoad(unit->chr));
234 		}
235 	}
236 }
237 
238 /**
239  * @brief Destroys character of a Unit
240  * @param[out] unit The unit to create character for
241  * @sa AM_CreateUnitChr
242  */
AM_DestroyUnitChr(autoUnit_t * unit)243 static void AM_DestroyUnitChr (autoUnit_t* unit)
244 {
245 	cgi->INV_DestroyInventory(&unit->chr->inv);
246 	Mem_Free(unit->chr);
247 }
248 
249 /**
250  * @brief Creates team data for alien and civilian teams based on the mission parameters data.
251  * @param[in,out] battle The auto mission battle to add team data to
252  * @param[in] missionParams Mission parameters data to use
253  */
AM_FillTeamFromBattleParams(autoMissionBattle_t * battle,const battleParam_t * missionParams)254 static void AM_FillTeamFromBattleParams (autoMissionBattle_t* battle, const battleParam_t* missionParams)
255 {
256 	assert(battle);
257 	assert(missionParams);
258 
259 	/* Aliens */
260 	battle->nUnits[AUTOMISSION_TEAM_TYPE_ALIEN] = missionParams->aliens;
261 	battle->actUnits[AUTOMISSION_TEAM_TYPE_ALIEN] = missionParams->aliens;
262 	if (missionParams->aliens > 0) {
263 		const equipDef_t* ed = cgi->INV_GetEquipmentDefinitionByID(missionParams->alienEquipment);
264 		const alienTeamGroup_t* alienTeamGroup = missionParams->alienTeamGroup;
265 		int unitIDX;
266 		for (unitIDX = 0; unitIDX < missionParams->aliens; unitIDX++) {
267 			const teamDef_t* teamDef = alienTeamGroup->alienTeams[rand() % alienTeamGroup->numAlienTeams];
268 			autoUnit_t* unit = AM_GetUnit(battle, AUTOMISSION_TEAM_TYPE_ALIEN, unitIDX);
269 
270 			AM_CreateUnitChr(unit, teamDef, ed);
271 			unit->team = AUTOMISSION_TEAM_TYPE_ALIEN;
272 			unit->idx = unitIDX;
273 		}
274 		battle->scoreTeamSkill[AUTOMISSION_TEAM_TYPE_ALIEN] = (frand() * 0.6f) + 0.2f;
275 	}
276 
277 	/* Civilians (if any) */
278 	battle->nUnits[AUTOMISSION_TEAM_TYPE_CIVILIAN] = missionParams->civilians;
279 	battle->actUnits[AUTOMISSION_TEAM_TYPE_CIVILIAN] = missionParams->civilians;
280 	if (missionParams->civilians > 0) {
281 		const teamDef_t* teamDef = cgi->Com_GetTeamDefinitionByID(missionParams->civTeam);
282 		int unitIDX;
283 		for (unitIDX = 0; unitIDX < missionParams->civilians; unitIDX++) {
284 			autoUnit_t* unit = AM_GetUnit(battle, AUTOMISSION_TEAM_TYPE_CIVILIAN, unitIDX);
285 
286 			AM_CreateUnitChr(unit, teamDef, nullptr);
287 			unit->team = AUTOMISSION_TEAM_TYPE_CIVILIAN;
288 			unit->idx = unitIDX;
289 		}
290 		battle->scoreTeamSkill[AUTOMISSION_TEAM_TYPE_CIVILIAN] = (frand() * 0.5f) + 0.05f;
291 	}
292 }
293 
294 /**
295  * @brief Run this on an auto mission battle before the battle is actually simulated, to set
296  * default values for who will attack which team. If you forget to do this before battle simulation, all teams will default
297  * to "free for all" (Everyone will try to kill everyone else).
298  * @param[in, out] battle The battle to set up team hostility values for.
299  * @param[in] civsInfected Set to @c true if civs have XVI influence, otherwise @c false for a normal mission.
300  */
AM_SetDefaultHostilities(autoMissionBattle_t * battle,const bool civsInfected)301 static void AM_SetDefaultHostilities (autoMissionBattle_t* battle, const bool civsInfected)
302 {
303 	int i;
304 	bool civsInverted = !civsInfected;
305 
306 	for (i = AUTOMISSION_TEAM_TYPE_PLAYER; i < AUTOMISSION_TEAM_TYPE_MAX; i++) {
307 		int j;
308 		const autoMissionTeamType_t team = (autoMissionTeamType_t)i;
309 		if (battle->actUnits[team] <= 0)
310 			continue;
311 
312 		for (j = AUTOMISSION_TEAM_TYPE_PLAYER; j < AUTOMISSION_TEAM_TYPE_MAX; j++) {
313 			const autoMissionTeamType_t otherTeam = (autoMissionTeamType_t)j;
314 			if (battle->actUnits[otherTeam] <= 0)
315 				continue;
316 
317 			if (AM_IsPlayer(team)) {
318 				if (AM_IsAlien(otherTeam))
319 					AM_SetHostile(battle, team, otherTeam, true);
320 				else if (AM_IsPlayer(otherTeam))
321 					AM_SetHostile(battle, team, otherTeam, false);
322 				else if (AM_IsCivilian(otherTeam))
323 					AM_SetHostile(battle, team, otherTeam, civsInfected);
324 			} else if (AM_IsAlien(team)) {
325 				if (AM_IsAlien(otherTeam))
326 					AM_SetHostile(battle, team, otherTeam, false);
327 				else if (AM_IsPlayer(otherTeam))
328 					AM_SetHostile(battle, team, otherTeam, true);
329 				else if (AM_IsCivilian(otherTeam))
330 					AM_SetHostile(battle, team, otherTeam, civsInverted);
331 			} else if (AM_IsCivilian(team)) {
332 				if (AM_IsAlien(otherTeam))
333 					AM_SetHostile(battle, team, otherTeam, civsInverted);
334 				else if (AM_IsPlayer(otherTeam))
335 					AM_SetHostile(battle, team, otherTeam, civsInfected);
336 				else if (AM_IsCivilian(otherTeam))
337 					AM_SetHostile(battle, team, otherTeam, false);
338 			}
339 		}
340 	}
341 }
342 
343 /**
344  * @brief Calcuates Team strength scores for autobattle
345  * @param[in, out] battle The battle to set up teamscores for
346  */
AM_CalculateTeamScores(autoMissionBattle_t * battle)347 static void AM_CalculateTeamScores (autoMissionBattle_t* battle)
348 {
349 	int unitTotal = 0;
350 	int isHostileTotal = 0;
351 	int totalActiveTeams = 0;
352 	int lastActiveTeam = -1;
353 	int isHostileCount;
354 	int team;
355 	/* Sums of various values */
356 	double teamPooledHealth[AUTOMISSION_TEAM_TYPE_MAX];
357 	double teamPooledHealthMax[AUTOMISSION_TEAM_TYPE_MAX];
358 	double teamPooledUnitsHealthy[AUTOMISSION_TEAM_TYPE_MAX];
359 	double teamPooledUnitsTotal[AUTOMISSION_TEAM_TYPE_MAX];
360 	/* Ratios */
361 	double teamRatioHealthyUnits[AUTOMISSION_TEAM_TYPE_MAX];
362 	double teamRatioHealthTotal[AUTOMISSION_TEAM_TYPE_MAX];
363 	int currentUnit;
364 
365 	for (team = 0; team < AUTOMISSION_TEAM_TYPE_MAX; team++) {
366 		unitTotal += battle->nUnits[team];
367 
368 		if (battle->actUnits[team] > 0) {
369 			lastActiveTeam = team;
370 			totalActiveTeams++;
371 		}
372 		for (isHostileCount = 0; isHostileCount < AUTOMISSION_TEAM_TYPE_MAX; isHostileCount++) {
373 			if (battle->nUnits[isHostileCount] <= 0)
374 				continue;
375 
376 			if (battle->isHostile[team][isHostileCount] && battle->actUnits[team] > 0)
377 				isHostileTotal++;
378 		}
379 	}
380 
381 	/* sanity checks */
382 	if (unitTotal == 0)
383 		cgi->Com_Error(ERR_DROP, "Grand total of ZERO units are fighting in auto battle, something is wrong.");
384 
385 	if (unitTotal < 0)
386 		cgi->Com_Error(ERR_DROP, "Negative number of total units are fighting in auto battle, something is VERY wrong!");
387 
388 	if (isHostileTotal <= 0)
389 		cgi->Com_Error(ERR_DROP, "No team has any other team hostile toward it, no battle is possible!");
390 
391 	if (totalActiveTeams <= 0)
392 		cgi->Com_Error(ERR_DROP, "No Active teams detected in Auto Battle!");
393 
394 	if (totalActiveTeams == 1) {
395 		Com_DPrintf(DEBUG_CLIENT, "Note: Only one active team detected, this team will win the auto mission battle by default.\n");
396 		battle->winningTeam = lastActiveTeam;
397 		return;
398 	}
399 
400 	/* Set up teams */
401 	for (team = 0; team < AUTOMISSION_TEAM_TYPE_MAX; team++) {
402 		teamPooledHealth[team] = 0.0;
403 		teamPooledHealthMax[team] = 0.0;
404 		teamPooledUnitsHealthy[team] = 0.0;
405 		teamPooledUnitsTotal[team] = 0.0;
406 
407 		if (battle->actUnits[team] > 0) {
408 			double skillAdjCalc;
409 			double skillAdjCalcAbs;
410 
411 			for (currentUnit = 0; currentUnit < battle->nUnits[team]; currentUnit++) {
412 				autoUnit_t* unit = AM_GetUnit(battle, team, currentUnit);
413 				const character_t* chr = unit->chr;
414 
415 				if (chr->HP <= 0)
416 					continue;
417 
418 				teamPooledHealth[team] += chr->HP;
419 				teamPooledHealthMax[team] += chr->maxHP;
420 				teamPooledUnitsTotal[team] += 1.0;
421 				if (chr->HP == chr->maxHP)
422 					teamPooledUnitsHealthy[team] += 1.0;
423 			}
424 			/* We shouldn't be dividing by zero here. */
425 			assert(teamPooledHealthMax[team] > 0.0);
426 			assert(teamPooledUnitsTotal[team] > 0.0);
427 
428 			teamRatioHealthTotal[team] = teamPooledHealth[team] / teamPooledHealthMax[team];
429 			teamRatioHealthyUnits[team] = teamPooledUnitsHealthy[team] / teamPooledUnitsTotal[team];
430 
431 			/* In DEBUG mode, these should help with telling where things are at what time, for bug-hunting purposes. */
432 			/* Note (Destructavator):  Is there a better way to implement this?  Is there a set protocol for this type of thing? */
433 			Com_DPrintf(DEBUG_CLIENT, "Team %i has calculated ratio of healthy units of %f.\n",
434 					team, teamRatioHealthyUnits[team]);
435 			Com_DPrintf(DEBUG_CLIENT, "Team %i has calculated ratio of health values of %f.\n",
436 					team, teamRatioHealthTotal[team]);
437 
438 			/** @todo speaking names please */
439 			skillAdjCalc = teamRatioHealthyUnits[team] + teamRatioHealthTotal[team];
440 			skillAdjCalc *= 0.50;
441 			skillAdjCalc = FpCurve1D_u_in(skillAdjCalc, 0.50, 0.50);
442 			skillAdjCalc -= 0.50;
443 			skillAdjCalcAbs = fabs(skillAdjCalc);
444 			if (skillAdjCalc > 0.0)
445 				battle->scoreTeamSkill[team] = ChkDNorm_Inv (FpCurveUp (battle->scoreTeamSkill[team], skillAdjCalcAbs) );
446 			else if (skillAdjCalc < 0.0)
447 				battle->scoreTeamSkill[team] = ChkDNorm (FpCurveDn (battle->scoreTeamSkill[team], skillAdjCalcAbs) );
448 			/* if (skillAdjCalc == exact 0.0), no change to team's skill. */
449 
450 			Com_DPrintf(DEBUG_CLIENT, "Team %i has adjusted skill rating of %f.\n",
451 					team, battle->scoreTeamSkill[team]);
452 		}
453 	}
454 }
455 
456 /**
457  * @brief returns a randomly selected active team
458  * @param[in] battle The battle we fight
459  * @param[in] currTeam Current team we search for
460  * @param[in] enemy If the team should be enemy or friendly
461  */
AM_GetRandomTeam(autoMissionBattle_t * battle,int currTeam,bool enemy)462 static int AM_GetRandomTeam (autoMissionBattle_t* battle, int currTeam, bool enemy)
463 {
464 	int eTeam;
465 
466 	assert(battle);
467 	assert(currTeam >= 0 && currTeam < AUTOMISSION_TEAM_TYPE_MAX);
468 
469 	/* select a team randomly */
470 	eTeam = rand () % AUTOMISSION_TEAM_TYPE_MAX;
471 	/* if selected team is active and it's hostility match, we're ready */
472 	if (battle->actUnits[eTeam] > 0 && AM_IsHostile(battle, currTeam, eTeam) == enemy) {
473 		return eTeam;
474 	} else {
475 		int nextTeam;
476 
477 		/* if not, check next */
478 		for (nextTeam = (eTeam + 1) % AUTOMISSION_TEAM_TYPE_MAX; nextTeam != eTeam; nextTeam = (nextTeam + 1) % AUTOMISSION_TEAM_TYPE_MAX) {
479 			if (battle->actUnits[nextTeam] > 0 && AM_IsHostile(battle, currTeam, nextTeam) == enemy)
480 				return nextTeam;
481 		}
482 		/* none found */
483 		return AUTOMISSION_TEAM_TYPE_MAX;
484 	}
485 }
486 
487 /**
488  * @brief returns a randomly selected alive unit from a team
489  * @param[in] battle The battle we fight
490  * @param[in] team Team to get unit from
491  */
AM_GetRandomActiveUnitOfTeam(autoMissionBattle_t * battle,int team)492 static autoUnit_t* AM_GetRandomActiveUnitOfTeam (autoMissionBattle_t* battle, int team)
493 {
494 	int idx;
495 	autoUnit_t* unit;
496 
497 	assert(battle);
498 	if (team < 0 || team >= AUTOMISSION_TEAM_TYPE_MAX)
499 		return nullptr;
500 	if (battle->actUnits[team] <= 0)
501 		return nullptr;
502 	if (battle->nUnits[team] <= 0)
503 		return nullptr;
504 
505 	/* select a unit randomly */
506 	idx = rand() % battle->nUnits[team];
507 	unit = AM_GetUnit(battle, team, idx);
508 
509 	/* if (s)he is active (alive, not stunned), we're ready */
510 	if (AM_IsUnitActive(unit)) {
511 		return unit;
512 	} else {
513 		int nextIdx;
514 
515 		/* if not active, check next */
516 		for (nextIdx = (idx + 1) % battle->nUnits[team]; nextIdx != idx; nextIdx = (nextIdx + 1) % battle->nUnits[team]) {
517 			unit = AM_GetUnit(battle, team, nextIdx);
518 			if (AM_IsUnitActive(unit))
519 				return unit;
520 		}
521 		/* none found */
522 		return nullptr;
523 	}
524 }
525 
526 /**
527  * @brief returns a randomly selected active unit
528  * @param[in] battle The battle we fight
529  * @param[in] currTeam Current team we search for
530  * @param[in] enemy If the team should be enemy or friendly
531  */
AM_GetRandomActiveUnit(autoMissionBattle_t * battle,int currTeam,bool enemy)532 static autoUnit_t* AM_GetRandomActiveUnit (autoMissionBattle_t* battle, int currTeam, bool enemy)
533 {
534 	int eTeam;
535 	int nextTeam;
536 	autoUnit_t* unit;
537 
538 	assert(battle);
539 	assert(currTeam >= 0 && currTeam < AUTOMISSION_TEAM_TYPE_MAX);
540 
541 	eTeam = AM_GetRandomTeam(battle, currTeam, enemy);
542 	if (eTeam >= AUTOMISSION_TEAM_TYPE_MAX)
543 		return nullptr;
544 
545 	unit = AM_GetRandomActiveUnitOfTeam(battle, eTeam);
546 	if (unit)
547 		return unit;
548 
549 	/* if not, check next */
550 	for (nextTeam = (eTeam + 1) % AUTOMISSION_TEAM_TYPE_MAX; nextTeam != eTeam; nextTeam = (nextTeam + 1) % AUTOMISSION_TEAM_TYPE_MAX) {
551 		if (battle->actUnits[nextTeam] > 0 && AM_IsHostile(battle, currTeam, nextTeam) == enemy) {
552 			unit = AM_GetRandomActiveUnitOfTeam(battle, nextTeam);
553 			if (unit)
554 				return unit;
555 		}
556 	}
557 	/* none found */
558 	return nullptr;
559 }
560 
561 /**
562  * @brief Check and do attack on a team
563  * @param[in, out] battle The battle we fight
564  * @param[in] currUnit Soldier idx who attacks
565  * @param[in] effective Effectiveness of the attack
566  */
AM_CheckFire(autoMissionBattle_t * battle,autoUnit_t * currUnit,autoUnit_t * eUnit,const double effective)567 static bool AM_CheckFire (autoMissionBattle_t* battle, autoUnit_t* currUnit, autoUnit_t* eUnit, const double effective)
568 {
569 	character_t* currChr = currUnit->chr;
570 	chrScoreGlobal_t* score = &currChr->score;
571 	character_t* eChr = eUnit->chr;
572 	double calcRand = frand();
573 	int strikeDamage;
574 
575 	if (AM_IsHostile(battle, currUnit->team, eUnit->team)) {
576 		if (calcRand > effective)
577 			return false;
578 		strikeDamage = (int) (100.0 * battle->scoreTeamDifficulty[currUnit->team] * (effective - calcRand) / effective);
579 		battle->teamAccomplishment[currUnit->team] += strikeDamage;
580 	} else {
581 		if (calcRand >= (0.050 - (effective * 0.050)))
582 			return false;
583 		strikeDamage = (int) (100.0 * (1.0 - battle->scoreTeamDifficulty[currUnit->team]) * calcRand);
584 		battle->teamAccomplishment[currUnit->team] -= strikeDamage;
585 	}
586 
587 	eChr->HP = std::max(0, eChr->HP - strikeDamage);
588 	/* Wound the target */
589 	if (eChr->HP > 0)
590 		eChr->wounds.treatmentLevel[eChr->teamDef->bodyTemplate->getRandomBodyPart()] += strikeDamage;
591 
592 	/* If target is still active, continue */
593 	if (AM_IsUnitActive(eUnit))
594 		return true;
595 
596 #if DEBUG
597 	Com_Printf("AutoBattle: Team: %d Unit: %d killed Team: %d Unit: %d\n", currUnit->team, currUnit->idx, eUnit->team, eUnit->idx);
598 #endif
599 	battle->actUnits[eUnit->team]--;
600 
601 	switch (currUnit->team) {
602 	case AUTOMISSION_TEAM_TYPE_PLAYER:
603 		switch (eUnit->team) {
604 		case AUTOMISSION_TEAM_TYPE_PLAYER:
605 			battle->results->ownSurvived--;
606 			battle->results->ownKilledFriendlyFire++;
607 			score->kills[KILLED_TEAM] += 1;
608 			break;
609 		case AUTOMISSION_TEAM_TYPE_ALIEN:
610 			battle->results->aliensSurvived--;
611 			battle->results->aliensKilled++;
612 			score->kills[KILLED_ENEMIES] += 1;
613 			break;
614 		case AUTOMISSION_TEAM_TYPE_CIVILIAN:
615 			battle->results->civiliansSurvived--;
616 			battle->results->civiliansKilledFriendlyFire++;
617 			score->kills[KILLED_CIVILIANS] += 1;
618 			break;
619 		default:
620 			break;
621 		}
622 		break;
623 	case AUTOMISSION_TEAM_TYPE_ALIEN:
624 		switch (eUnit->team) {
625 		case AUTOMISSION_TEAM_TYPE_PLAYER:
626 			battle->results->ownSurvived--;
627 			battle->results->ownKilled++;
628 			break;
629 		case AUTOMISSION_TEAM_TYPE_ALIEN:
630 			battle->results->aliensSurvived--;
631 			battle->results->aliensKilled++;
632 			break;
633 		case AUTOMISSION_TEAM_TYPE_CIVILIAN:
634 			battle->results->civiliansSurvived--;
635 			battle->results->civiliansKilled++;
636 			break;
637 		default:
638 			break;
639 		}
640 		break;
641 	case AUTOMISSION_TEAM_TYPE_CIVILIAN:
642 		switch (eUnit->team) {
643 		case AUTOMISSION_TEAM_TYPE_PLAYER:
644 			battle->results->ownSurvived--;
645 			battle->results->ownKilledFriendlyFire++;
646 			break;
647 		case AUTOMISSION_TEAM_TYPE_ALIEN:
648 			battle->results->aliensSurvived--;
649 			battle->results->aliensKilled++;
650 			break;
651 		case AUTOMISSION_TEAM_TYPE_CIVILIAN:
652 			battle->results->civiliansSurvived--;
653 			battle->results->civiliansKilledFriendlyFire++;
654 			break;
655 		default:
656 			break;
657 		}
658 		break;
659 	default:
660 		break;
661 	}
662 	return true;
663 }
664 
665 /**
666  * @brief Make Unit attack his enemies (or friends)
667  * @param[in, out] battle The battle we fight
668  * @param[in] currUnit Unit that attacks
669  * @param[in] effective Effectiveness of the attack
670  */
AM_UnitAttackEnemy(autoMissionBattle_t * battle,autoUnit_t * currUnit,const double effective)671 static bool AM_UnitAttackEnemy (autoMissionBattle_t* battle, autoUnit_t* currUnit, const double effective)
672 {
673 	autoUnit_t* eUnit;
674 
675 	eUnit = AM_GetRandomActiveUnit(battle, currUnit->team, true);
676 	/* no more enemies */
677 	if (eUnit == nullptr)
678 		return false;
679 
680 	/* shot an enemy */
681 	if (!AM_CheckFire(battle, currUnit, eUnit, effective)) {
682 		/* if failed, attack a friendly */
683 		eUnit = AM_GetRandomActiveUnit(battle, currUnit->team, false);
684 		if (eUnit != nullptr)
685 			AM_CheckFire(battle, currUnit, eUnit, effective);
686 	}
687 
688 	return true;
689 }
690 
691 /**
692  * @brief Main Battle loop function
693  * @param[in, out] battle The battle we fight
694  */
AM_DoFight(autoMissionBattle_t * battle)695 static void AM_DoFight (autoMissionBattle_t* battle)
696 {
697 	bool combatActive = true;
698 
699 #ifdef DEBUG
700 	int teamID;
701 
702 	Com_Printf("Auto battle started\n");
703 	for (teamID = 0; teamID < AUTOMISSION_TEAM_TYPE_MAX; teamID++) {
704 		Com_Printf("Team %d Units: %d\n", teamID, battle->nUnits[teamID]);
705 	}
706 #endif
707 
708 	while (combatActive) {
709 		int team;
710 		for (team = 0; team < AUTOMISSION_TEAM_TYPE_MAX; team++) {
711 			int aliveUnits;
712 			int currentUnit;
713 
714 			/** @todo Drop this if and implement correct weapon/attack strength checks */
715 			if (team == AUTOMISSION_TEAM_TYPE_CIVILIAN)
716 				continue;
717 
718 			if (battle->actUnits[team] <= 0)
719 				continue;
720 
721 			aliveUnits = 0;
722 			/* Is this unit still alive (has any health left?) */
723 			for (currentUnit = 0; currentUnit < battle->nUnits[team]; currentUnit++) {
724 				autoUnit_t* unit = AM_GetUnit(battle, team, currentUnit);
725 				character_t* chr = unit->chr;
726 				/* Wounded units don't fight quite as well */
727 				const double hpLeftRatio = chr->HP / chr->maxHP;
728 				const double effective = FpCurveDn(battle->scoreTeamSkill[team], hpLeftRatio * 0.50);
729 
730 				if (!AM_IsUnitActive(unit))
731 					continue;
732 
733 				Com_DPrintf(DEBUG_CLIENT, "Unit %i on team %i has adjusted attack rating of %f.\n",
734 						currentUnit, team, battle->scoreTeamSkill[team]);
735 
736 				aliveUnits++;
737 				combatActive = AM_UnitAttackEnemy(battle, unit, effective);
738 			}
739 		}
740 	}
741 
742 	/* Set results */
743 	if (battle->actUnits[AUTOMISSION_TEAM_TYPE_PLAYER] <= 0) {
744 		battle->results->state = LOST;
745 		battle->winningTeam = AUTOMISSION_TEAM_TYPE_ALIEN;
746 	} else {
747 		battle->winningTeam = AUTOMISSION_TEAM_TYPE_PLAYER;
748 		battle->results->state = WON;
749 	}
750 }
751 
752 /**
753  * @brief This will display on-screen, for the player, results of the auto mission.
754  * @param[in] battle Autobattle structure with the results
755  * @todo results should be set in missionResult and this code should be merged with manual
756  * mission result screen code, possibly in a new file: cp_mission_callbacks.c/h
757  */
AM_DisplayResults(const autoMissionBattle_t * battle)758 static void AM_DisplayResults (const autoMissionBattle_t* battle)
759 {
760 	assert(battle);
761 
762 	cgi->Cvar_SetValue("cp_mission_tryagain", 0);
763 	if (battle->results->state == WON) {
764 		cgi->UI_PushWindow("won");
765 		if (battle->teamAccomplishment[AUTOMISSION_TEAM_TYPE_PLAYER] > battle->teamAccomplishment[AUTOMISSION_TEAM_TYPE_ALIEN])
766 			MS_AddNewMessage(_("Notice"), _("You've won the battle"));
767 		else
768 			MS_AddNewMessage(_("Notice"), _("You've defeated the enemy, but did poorly, and many civilians were killed"));
769 	} else {
770 		cgi->UI_PushWindow("lost");
771 		MS_AddNewMessage(_("Notice"), _("You've lost the battle"));
772 	}
773 }
774 
775 /**
776  * @brief Move equipment carried by the soldier/alien to the aircraft's itemcargo bay
777  * @param[in, out] aircraft The craft with the team (and thus equipment) onboard.
778  * @param[in, out] chr The character whose inventory should be moved
779  */
AM_MoveCharacterInventoryIntoItemCargo(aircraft_t * aircraft,character_t * chr)780 static void AM_MoveCharacterInventoryIntoItemCargo (aircraft_t* aircraft, character_t* chr)
781 {
782 	assert(aircraft != nullptr);
783 	assert(chr != nullptr);
784 
785 	/* add items to itemcargo */
786 	const Container* cont = nullptr;
787 	while ((cont = chr->inv.getNextCont(cont))) {
788 		Item* item = nullptr;
789 		while ((item = cont->getNextItem(item))) {
790 			if (item->def()) {
791 				AII_CollectItem(aircraft, item->def(), 1);
792 
793 				if (item->getAmmoLeft() && item->ammoDef())
794 					AII_CollectItem(aircraft, item->ammoDef(), 1);
795 			}
796 		}
797 	}
798 }
799 
800 /**
801  * @brief Collect alien bodies and items after battle
802  * @param[out] aircraft Mission aircraft that will bring stuff home
803  * @param[in] battle The battle we fought
804  */
AM_AlienCollect(aircraft_t * aircraft,const autoMissionBattle_t * battle)805 static void AM_AlienCollect (aircraft_t* aircraft, const autoMissionBattle_t* battle)
806 {
807 	int unitIDX;
808 	int collected = 0;
809 
810 	assert(aircraft);
811 	assert(battle);
812 
813 	/* Aliens */
814 	for (unitIDX = 0; unitIDX < battle->nUnits[AUTOMISSION_TEAM_TYPE_ALIEN]; unitIDX++) {
815 		const autoUnit_t* unit = AM_GetUnit(battle, AUTOMISSION_TEAM_TYPE_ALIEN, unitIDX);
816 
817 		if (AM_IsUnitActive(unit))
818 			continue;
819 
820 		AM_MoveCharacterInventoryIntoItemCargo(aircraft, unit->chr);
821 		AL_AddAlienTypeToAircraftCargo(aircraft, unit->chr->teamDef, 1, unit->chr->HP <= 0);
822 		collected++;
823 	}
824 
825 	if (collected > 0)
826 		MS_AddNewMessage(_("Notice"), _("Collected alien bodies"));
827 }
828 
829 /**
830  * @brief This looks at a finished auto battle, and uses values from it to kill or lower health of surviving soldiers on a
831  * mission drop ship as appropriate.  It also hands out some experience to soldiers that survive.
832  * @param[in] battle The battle we fought
833  * @param[in, out] aircraft Dropship soldiers are on
834  */
AM_UpdateSurivorsAfterBattle(const autoMissionBattle_t * battle,struct aircraft_s * aircraft)835 static void AM_UpdateSurivorsAfterBattle (const autoMissionBattle_t* battle, struct aircraft_s* aircraft)
836 {
837 	assert(battle);
838 	assert(battle->results);
839 
840 	const int battleExperience = std::max(0, battle->teamAccomplishment[AUTOMISSION_TEAM_TYPE_PLAYER]);
841 	int unit = 0;
842 
843 	LIST_Foreach(aircraft->acTeam, Employee, soldier) {
844 		if (unit >= MAX_SOLDIERS_AUTOMISSION)
845 			break;
846 
847 		unit++;
848 
849 		character_t* chr = &soldier->chr;
850 		/* dead soldiers are removed in CP_MissionEnd, just move their inventory to itemCargo */
851 		if (chr->HP <= 0) {
852 			if (battle->results->state == WON)
853 				AM_MoveCharacterInventoryIntoItemCargo(aircraft, &soldier->chr);
854 			E_RemoveInventoryFromStorage(soldier);
855 			continue;
856 		}
857 
858 		chrScoreGlobal_t* score = &chr->score;
859 		for (int expCount = 0; expCount < ABILITY_NUM_TYPES; expCount++) {
860 			const int maxXP = CP_CharacterGetMaxExperiencePerMission(static_cast<abilityskills_t>(expCount));
861 			const int gainedXP = std::min(maxXP, static_cast<int>(battleExperience * ABILITY_AWARD_SCALE * frand()));
862 			score->experience[expCount] += gainedXP;
863 			cgi->Com_DPrintf(DEBUG_CLIENT, "AM_UpdateSurivorsAfterBattle: Soldier %s earned %d experience points in skill #%d (total experience: %d).\n",
864 						chr->name, gainedXP, expCount, chr->score.experience[expCount]);
865 		}
866 
867 		for (int expCount = ABILITY_NUM_TYPES; expCount < SKILL_NUM_TYPES; expCount++) {
868 			const int maxXP = CP_CharacterGetMaxExperiencePerMission(static_cast<abilityskills_t>(expCount));
869 			const int gainedXP = std::min(maxXP, static_cast<int>(battleExperience * SKILL_AWARD_SCALE * frand()));
870 			score->experience[expCount] += gainedXP;
871 			cgi->Com_DPrintf(DEBUG_CLIENT, "AM_UpdateSurivorsAfterBattle: Soldier %s earned %d experience points in skill #%d (total experience: %d).\n",
872 						chr->name, gainedXP, expCount, chr->score.experience[expCount]);
873 		}
874 		/* Health isn't part of abilityskills_t, so it needs to be handled separately. */
875 		const int maxXP = CP_CharacterGetMaxExperiencePerMission(SKILL_NUM_TYPES);
876 		const int gainedXP = std::min(maxXP, static_cast<int>(battleExperience * ABILITY_AWARD_SCALE * frand()));
877 		score->experience[SKILL_NUM_TYPES] += gainedXP;
878 		cgi->Com_DPrintf(DEBUG_CLIENT, "AM_UpdateSurivorsAfterBattle: Soldier %s earned %d experience points in skill #%d (total experience: %d).\n",
879 					chr->name, gainedXP, SKILL_NUM_TYPES, chr->score.experience[SKILL_NUM_TYPES]);
880 
881 		CP_UpdateCharacterSkills(chr);
882 	}
883 }
884 
885 /**
886  * @brief Clean up alien and civilian teams
887  * @param[in,out] battle The common autobattle descriptor structure
888  */
AM_CleanBattleParameters(autoMissionBattle_t * battle)889 static void AM_CleanBattleParameters (autoMissionBattle_t* battle)
890 {
891 	int unitIDX;
892 
893 	assert(battle);
894 
895 	/* Aliens */
896 	for (unitIDX = 0; unitIDX < battle->nUnits[AUTOMISSION_TEAM_TYPE_ALIEN]; unitIDX++) {
897 		autoUnit_t* unit = AM_GetUnit(battle, AUTOMISSION_TEAM_TYPE_ALIEN, unitIDX);
898 
899 		AM_DestroyUnitChr(unit);
900 	}
901 
902 	/* Civilians */
903 	for (unitIDX = 0; unitIDX < battle->nUnits[AUTOMISSION_TEAM_TYPE_CIVILIAN]; unitIDX++) {
904 		autoUnit_t* unit = AM_GetUnit(battle, AUTOMISSION_TEAM_TYPE_CIVILIAN, unitIDX);
905 
906 		AM_DestroyUnitChr(unit);
907 	}
908 }
909 
910 /**
911  * @brief Handles the auto mission
912  * @param[in,out] mission The mission to auto play
913  * @param[in,out] aircraft The aircraft (or fake aircraft in case of a base attack)
914  * @param[in] campaign The campaign data structure
915  * @param[in] battleParameters Structure that holds the battle related parameters
916  * @param[out] results Result of the mission
917  */
AM_Go(mission_t * mission,aircraft_t * aircraft,const campaign_t * campaign,const battleParam_t * battleParameters,missionResults_t * results)918 void AM_Go (mission_t* mission, aircraft_t* aircraft, const campaign_t* campaign, const battleParam_t* battleParameters, missionResults_t* results)
919 {
920 	autoMissionBattle_t autoBattle;
921 
922 	assert(mission);
923 	assert(aircraft);
924 	assert(aircraft->homebase);
925 
926 	if (mission && mission->mapDef && mission->mapDef->storyRelated) {
927 		Com_Printf("Story-related mission cannot be done via automission\n");
928 		return;
929 	}
930 
931 	AM_ClearBattle(&autoBattle);
932 	autoBattle.results = results;
933 	AM_FillTeamFromAircraft(&autoBattle, AUTOMISSION_TEAM_TYPE_PLAYER, aircraft, campaign);
934 	AM_FillTeamFromBattleParams(&autoBattle, battleParameters);
935 	AM_SetDefaultHostilities(&autoBattle, false);
936 	AM_CalculateTeamScores(&autoBattle);
937 
938 	OBJZERO(*results);
939 	results->ownSurvived = autoBattle.nUnits[AUTOMISSION_TEAM_TYPE_PLAYER];
940 	results->aliensSurvived = autoBattle.nUnits[AUTOMISSION_TEAM_TYPE_ALIEN];
941 	results->civiliansSurvived = autoBattle.nUnits[AUTOMISSION_TEAM_TYPE_CIVILIAN];
942 	results->mission = mission;
943 
944 	ccs.eMission = aircraft->homebase->storage; /* copied, including arrays inside! */
945 
946 	AM_DoFight(&autoBattle);
947 
948 	AM_UpdateSurivorsAfterBattle(&autoBattle, aircraft);
949 	if (results->state == WON)
950 		AM_AlienCollect(aircraft, &autoBattle);
951 
952 	MIS_InitResultScreen(results);
953 	if (ccs.missionResultCallback) {
954 		ccs.missionResultCallback(results);
955 	}
956 
957 	AM_DisplayResults(&autoBattle);
958 	AM_CleanBattleParameters(&autoBattle);
959 }
960 
961 /**
962  * @brief Init actions for automission-subsystem
963  */
AM_InitStartup(void)964 void AM_InitStartup (void)
965 {
966 }
967 
968 /**
969  * @brief Closing actions for automission-subsystem
970  */
AM_Shutdown(void)971 void AM_Shutdown (void)
972 {
973 }
974