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