1 /**
2  * @file
3  * @brief Airfight related stuff.
4  * @todo Somehow i need to know which alien race was in the ufo we shoot down
5  * I need this info for spawning the crash site @sa CP_CreateBattleParameters
6  */
7 
8 /*
9 Copyright (C) 2002-2013 UFO: Alien Invasion.
10 
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License
13 as published by the Free Software Foundation; either version 2
14 of the License, or (at your option) any later version.
15 
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19 
20 See the GNU General Public License for more details.
21 
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 */
26 
27 #include "../../cl_shared.h"
28 #include "cp_campaign.h"
29 #include "cp_mapfightequip.h"
30 #include "cp_geoscape.h"
31 #include "cp_ufo.h"
32 #include "cp_missions.h"
33 #include "save/save_airfight.h"
34 #include "../../sound/s_main.h"
35 
36 /**
37  * @brief Remove a projectile from ccs.projectiles
38  * @param[in] projectile The projectile to remove
39  * @sa AIRFIGHT_AddProjectile
40  */
AIRFIGHT_RemoveProjectile(aircraftProjectile_t * projectile)41 static void AIRFIGHT_RemoveProjectile (aircraftProjectile_t* projectile)
42 {
43 	const ptrdiff_t num = (ptrdiff_t)(projectile - ccs.projectiles);
44 	REMOVE_ELEM_ADJUST_IDX(ccs.projectiles, num, ccs.numProjectiles);
45 }
46 
47 /**
48  * @brief Add a projectile in ccs.projectiles
49  * @param[in] attackingBase the attacking base in ccs.bases[]. nullptr is the attacker is an aircraft or a samsite.
50  * @param[in] attackingInstallation the attacking samsite in ccs.installations[]. nullptr is the attacker is an aircraft or a base.
51  * @param[in] attacker Pointer to the attacking aircraft
52  * @param[in] target Pointer to the target aircraft
53  * @param[in] weaponSlot Pointer to the weapon slot that fires the projectile.
54  * @note we already checked in AIRFIGHT_ChooseWeapon that the weapon has still ammo
55  * @sa AIRFIGHT_RemoveProjectile
56  * @sa AII_ReloadWeapon for the aircraft item reload code
57  */
AIRFIGHT_AddProjectile(const base_t * attackingBase,const installation_t * attackingInstallation,aircraft_t * attacker,aircraft_t * target,aircraftSlot_t * weaponSlot)58 static bool AIRFIGHT_AddProjectile (const base_t* attackingBase, const installation_t* attackingInstallation, aircraft_t* attacker, aircraft_t* target, aircraftSlot_t* weaponSlot)
59 {
60 	aircraftProjectile_t* projectile;
61 
62 	if (ccs.numProjectiles >= MAX_PROJECTILESONGEOSCAPE) {
63 		Com_DPrintf(DEBUG_CLIENT, "Too many projectiles on map\n");
64 		return false;
65 	}
66 
67 	projectile = &ccs.projectiles[ccs.numProjectiles];
68 
69 	if (!weaponSlot->ammo) {
70 		Com_Printf("AIRFIGHT_AddProjectile: Error - no ammo assigned\n");
71 		return false;
72 	}
73 
74 	assert(weaponSlot->item);
75 
76 	projectile->aircraftItem = weaponSlot->ammo;
77 	if (attackingBase) {
78 		projectile->attackingAircraft = nullptr;
79 		VectorSet(projectile->pos[0], attackingBase->pos[0], attackingBase->pos[1], 0);
80 		VectorSet(projectile->attackerPos, attackingBase->pos[0], attackingBase->pos[1], 0);
81 	} else if (attackingInstallation) {
82 		projectile->attackingAircraft = nullptr;
83 		VectorSet(projectile->pos[0], attackingInstallation->pos[0], attackingInstallation->pos[1], 0);
84 		VectorSet(projectile->attackerPos, attackingInstallation->pos[0], attackingInstallation->pos[1], 0);
85 	} else {
86 		assert(attacker);
87 		projectile->attackingAircraft = attacker;
88 		VectorSet(projectile->pos[0], attacker->pos[0], attacker->pos[1], 0);
89 		/* attacker may move, use attackingAircraft->pos */
90 		VectorSet(projectile->attackerPos, 0, 0, 0);
91 	}
92 
93 	projectile->numProjectiles++;
94 
95 	assert(target);
96 	projectile->aimedAircraft = target;
97 	VectorSet(projectile->idleTarget, 0, 0, 0);
98 
99 	projectile->time = 0;
100 	projectile->angle = 0.0f;
101 
102 	projectile->bullets = weaponSlot->item->craftitem.bullets;
103 	projectile->beam = weaponSlot->item->craftitem.beam;
104 	projectile->rocket = !projectile->bullets && !projectile->beam;
105 
106 	weaponSlot->ammoLeft--;
107 	if (weaponSlot->ammoLeft <= 0)
108 		AII_ReloadWeapon(weaponSlot);
109 
110 	ccs.numProjectiles++;
111 
112 	const char* sound;
113 	if (projectile->bullets) {
114 		sound = "geoscape/combat-gun";
115 	} else if (projectile->beam) {
116 		sound = "geoscape/combat-airlaser";
117 	} else if (projectile->rocket) {
118 		sound = "geoscape/combat-rocket";
119 	} else {
120 		sound = nullptr;
121 	}
122 
123 	if (sound != nullptr)
124 		cgi->S_StartLocalSample(sound, 1.0f);
125 
126 	return true;
127 }
128 
129 #ifdef DEBUG
130 /**
131  * @brief List all projectiles on map to console.
132  * @note called with debug_listprojectile
133  */
AIRFIGHT_ProjectileList_f(void)134 static void AIRFIGHT_ProjectileList_f (void)
135 {
136 	int i;
137 
138 	for (i = 0; i < ccs.numProjectiles; i++) {
139 		Com_Printf("%i. (idx: %i)\n", i, ccs.projectiles[i].idx);
140 		Com_Printf("... type '%s'\n", ccs.projectiles[i].aircraftItem->id);
141 		if (ccs.projectiles[i].attackingAircraft)
142 			Com_Printf("... shooting aircraft '%s'\n", ccs.projectiles[i].attackingAircraft->id);
143 		else
144 			Com_Printf("... base is shooting, or shooting aircraft is destroyed\n");
145 		if (ccs.projectiles[i].aimedAircraft)
146 			Com_Printf("... aiming aircraft '%s'\n", ccs.projectiles[i].aimedAircraft->id);
147 		else
148 			Com_Printf("... aiming idle target at (%.02f, %.02f)\n",
149 				ccs.projectiles[i].idleTarget[0], ccs.projectiles[i].idleTarget[1]);
150 	}
151 }
152 #endif
153 
154 /**
155  * @brief Change destination of projectile to an idle point of the map, close to its former target.
156  * @param[in] projectile The projectile to update
157  */
AIRFIGHT_MissTarget(aircraftProjectile_t * projectile)158 static void AIRFIGHT_MissTarget (aircraftProjectile_t* projectile)
159 {
160 	vec3_t newTarget;
161 	float offset;
162 
163 	assert(projectile);
164 
165 	if (projectile->aimedAircraft) {
166 		VectorCopy(projectile->aimedAircraft->pos, newTarget);
167 		projectile->aimedAircraft = nullptr;
168 	} else {
169 		VectorCopy(projectile->idleTarget, newTarget);
170 	}
171 
172 	/* get the distance between the projectile and target */
173 	const float distance = GetDistanceOnGlobe(projectile->pos[0], newTarget);
174 
175 	/* Work out how much the projectile should miss the target by.  We dont want it too close
176 	 * or too far from the original target.
177 	 * * 1/3 distance between target and projectile * random (range -0.5 to 0.5)
178 	 * * Then make sure the value is at least greater than 0.1 or less than -0.1 so that
179 	 *   the projectile doesn't land too close to the target. */
180 	offset = (distance / 3) * (frand() - 0.5f);
181 
182 	if (abs(offset) < 0.1f)
183 		offset = 0.1f;
184 
185 	newTarget[0] = newTarget[0] + offset;
186 	newTarget[1] = newTarget[1] + offset;
187 
188 	VectorCopy(newTarget, projectile->idleTarget);
189 }
190 
191 /**
192  * @brief Check if the selected weapon can shoot.
193  * @param[in] slot Pointer to the weapon slot to shoot with.
194  * @param[in] distance distance between the weapon and the target.
195  * @return 0 AIRFIGHT_WEAPON_CAN_SHOOT if the weapon can shoot,
196  * -1 AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT if it can't shoot atm,
197  * -2 AIRFIGHT_WEAPON_CAN_NEVER_SHOOT if it will never be able to shoot.
198  */
AIRFIGHT_CheckWeapon(const aircraftSlot_t * slot,float distance)199 int AIRFIGHT_CheckWeapon (const aircraftSlot_t* slot, float distance)
200 {
201 	assert(slot);
202 
203 	/* check if there is a functional weapon in this slot */
204 	if (!slot->item || slot->installationTime != 0)
205 		return AIRFIGHT_WEAPON_CAN_NEVER_SHOOT;
206 
207 	/* check if there is still ammo in this weapon */
208 	if (!slot->ammo || (slot->ammoLeft <= 0))
209 		return AIRFIGHT_WEAPON_CAN_NEVER_SHOOT;
210 
211 	/* check if the target is within range of this weapon */
212 	if (distance > slot->ammo->craftitem.stats[AIR_STATS_WRANGE])
213 		return AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT;
214 
215 	/* check if weapon is reloaded */
216 	if (slot->delayNextShot > 0)
217 		return AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT;
218 
219 	return AIRFIGHT_WEAPON_CAN_SHOOT;
220 }
221 
222 /**
223  * @brief Choose the weapon an attacking aircraft will use to fire on a target.
224  * @param[in] slot Pointer to the first weapon slot of attacking base or aircraft.
225  * @param[in] maxSlot maximum number of weapon slots in attacking base or aircraft.
226  * @param[in] pos position of attacking base or aircraft.
227  * @param[in] targetPos Pointer to the aimed aircraft.
228  * @return indice of the slot to use (in array weapons[]),
229  * -1 AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT no weapon to use atm,
230  * -2 AIRFIGHT_WEAPON_CAN_NEVER_SHOOT if no weapon to use at all.
231  * @sa AIRFIGHT_CheckWeapon
232  */
AIRFIGHT_ChooseWeapon(const aircraftSlot_t * slot,int maxSlot,const vec2_t pos,const vec2_t targetPos)233 int AIRFIGHT_ChooseWeapon (const aircraftSlot_t* slot, int maxSlot, const vec2_t pos, const vec2_t targetPos)
234 {
235 	int slotIdx = AIRFIGHT_WEAPON_CAN_NEVER_SHOOT;
236 	int i;
237 	float distance0 = 99999.9f;
238 	const float distance = GetDistanceOnGlobe(pos, targetPos);
239 
240 	/* We choose the usable weapon with the smallest range */
241 	for (i = 0; i < maxSlot; i++) {
242 		const int weaponStatus = AIRFIGHT_CheckWeapon(slot + i, distance);
243 
244 		/* set slotIdx to AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT if needed */
245 		/* this will only happen if weapon_state is AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT
246 		 * and no weapon has been found that can shoot. */
247 		if (weaponStatus > slotIdx)
248 			slotIdx = AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT;
249 
250 		/* select this weapon if this is the one with the shortest range */
251 		if (weaponStatus >= AIRFIGHT_WEAPON_CAN_SHOOT && distance < distance0) {
252 			slotIdx = i;
253 			distance0 = distance;
254 		}
255 	}
256 	return slotIdx;
257 }
258 
259 /**
260  * @brief Calculate the probability to hit the enemy.
261  * @param[in] shooter Pointer to the attacking aircraft (may be nullptr if a base fires the projectile).
262  * @param[in] target Pointer to the aimed aircraft (may be nullptr if a target is a base).
263  * @param[in] slot Slot containing the weapon firing.
264  * @return Probability to hit the target (0 when you don't have a chance, 1 (or more) when you're sure to hit).
265  * @note that modifiers due to electronics, weapons, and shield are already taken into account in AII_UpdateAircraftStats
266  * @sa AII_UpdateAircraftStats
267  * @sa AIRFIGHT_ExecuteActions
268  * @sa AIRFIGHT_ChooseWeapon
269  * @pre slotIdx must have a weapon installed, with ammo available (see AIRFIGHT_ChooseWeapon)
270  * @todo This probability should also depend on the pilot skills, when they will be implemented.
271  */
AIRFIGHT_ProbabilityToHit(const aircraft_t * shooter,const aircraft_t * target,const aircraftSlot_t * slot)272 static float AIRFIGHT_ProbabilityToHit (const aircraft_t* shooter, const aircraft_t* target, const aircraftSlot_t* slot)
273 {
274 	float probability = 0.0f;
275 
276 	if (!slot->item) {
277 		Com_Printf("AIRFIGHT_ProbabilityToHit: no weapon assigned to attacking aircraft\n");
278 		return probability;
279 	}
280 
281 	if (!slot->ammo) {
282 		Com_Printf("AIRFIGHT_ProbabilityToHit: no ammo in weapon of attacking aircraft\n");
283 		return probability;
284 	}
285 
286 	/* Take Base probability from the ammo of the attacking aircraft */
287 	probability = slot->ammo->craftitem.stats[AIR_STATS_ACCURACY];
288 	Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Base probability: %f\n", probability);
289 
290 	/* Modify this probability by items of the attacking aircraft (stats is in percent) */
291 	if (shooter)
292 		probability *= shooter->stats[AIR_STATS_ACCURACY] / 100.0f;
293 
294 	Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Probability after accounting items of attacker: %f\n", probability);
295 
296 	/* Modify this probability by items of the aimed aircraft (stats is in percent) */
297 	if (target)
298 		probability /= target->stats[AIR_STATS_ECM] / 100.0f;
299 
300 	Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Probability after accounting ECM of target: %f\n", probability);
301 
302 	/* If shooter is a PHALANX craft, check the targeting skills of the pilot */
303 	if (shooter && shooter->type != AIRCRAFT_UFO) {
304 		if (shooter->pilot) {
305 			/**
306 			 * Targeting skill increases hit chance for shooter
307 			 * With this equation, max increase (0.29) is reached at skill level 70. Any higher skill rating actually
308 			 * reduces the bonus, so a skill cap should be placed at 70 when skill increase is implemented for pilots.
309 			 */
310 			probability += ( ( ( 1.4f - ( shooter->pilot->chr.score.skills[SKILL_TARGETING] / 100.0f ) ) * ( shooter->pilot->chr.score.skills[SKILL_TARGETING] / 100.0f ) ) - 0.2f );
311 
312 			Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Probability after accounting targeting skill of shooter: %f\n",
313 					probability);
314 		}
315 	}
316 
317 	/* If target is a PHALANX craft, check the evading skills of the pilot */
318 	if (target && target->type != AIRCRAFT_UFO) {
319 		if (target->pilot) {
320 			/**
321 			 * Evasion skill decreases hit chance for shooter
322 			 * With this equation, max decrease (0.29) is reached at skill level 70. Any higher skill rating actually
323 			 * reduces the bonus, so a skill cap should be placed at 70 when skill increase is implemented for pilots.
324 			 */
325 			probability -= ( ( ( 1.4f - ( target->pilot->chr.score.skills[SKILL_EVADING] / 100.0f ) ) * ( target->pilot->chr.score.skills[SKILL_EVADING] / 100.0f ) ) - 0.2f );
326 
327 			Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Probability after accounting evasion skill of target: %f\n",
328 					probability);
329 		}
330 	}
331 
332 	/* Probability should not exceed 0.95 so there is always a chance to miss */
333 	probability = std::min(probability, 0.95f);
334 
335 	Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ProbabilityToHit: Probability to hit: %f\n", probability);
336 
337 	return probability;
338 }
339 
340 /**
341  * @brief Decide what an attacking aircraft can do.
342  * @param[in] campaign The campaign data structure
343  * @param[in] shooter The aircraft we attack with.
344  * @param[in] target The ufo we are going to attack.
345  * @todo Implement me and display an attack popup.
346  */
AIRFIGHT_ExecuteActions(const campaign_t * campaign,aircraft_t * shooter,aircraft_t * target)347 void AIRFIGHT_ExecuteActions (const campaign_t* campaign, aircraft_t* shooter, aircraft_t* target)
348 {
349 	/* some asserts */
350 	assert(shooter);
351 	assert(target);
352 
353 	/* Check if the attacking aircraft can shoot */
354 	const int slotIdx = AIRFIGHT_ChooseWeapon(shooter->weapons, shooter->maxWeapons, shooter->pos, target->pos);
355 
356 	/* if weapon found that can shoot */
357 	if (slotIdx >= AIRFIGHT_WEAPON_CAN_SHOOT) {
358 		aircraftSlot_t* weaponSlot = &shooter->weapons[slotIdx];
359 		const objDef_t* ammo = weaponSlot->ammo;
360 
361 		/* shoot */
362 		if (AIRFIGHT_AddProjectile(nullptr, nullptr, shooter, target, weaponSlot)) {
363 			/* will we miss the target ? */
364 			const float probability = frand();
365 			Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ExecuteActions: %s - Random probability to hit: %f\n", shooter->name, probability);
366 			weaponSlot->delayNextShot = ammo->craftitem.weaponDelay;
367 
368 			const float calculatedProbability = AIRFIGHT_ProbabilityToHit(shooter, target, shooter->weapons + slotIdx);
369 			Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ExecuteActions: %s - Calculated probability to hit: %f\n", shooter->name, calculatedProbability);
370 
371 			if (probability > calculatedProbability)
372 				AIRFIGHT_MissTarget(&ccs.projectiles[ccs.numProjectiles - 1]);
373 
374 			if (shooter->type != AIRCRAFT_UFO) {
375 				/* Maybe UFO is going to shoot back ? */
376 				UFO_CheckShootBack(campaign, target, shooter);
377 			} else {
378 				/* an undetected UFO within radar range and firing should become detected */
379 				if (!shooter->detected && RADAR_CheckRadarSensored(shooter->pos)) {
380 					/* stop time and notify */
381 					MSO_CheckAddNewMessage(NT_UFO_ATTACKING,_("Notice"), va(_("A UFO is shooting at %s"), target->name));
382 					RADAR_AddDetectedUFOToEveryRadar(shooter);
383 					UFO_DetectNewUFO(shooter);
384 				}
385 			}
386 		}
387 	} else if (slotIdx == AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT) {
388 		/* no ammo to fire atm (too far or reloading), pursue target */
389 		if (shooter->type == AIRCRAFT_UFO) {
390 			/** @todo This should be calculated only when target destination changes, or when aircraft speed changes.
391 			 *  @sa AIR_GetDestination */
392 			UFO_SendPursuingAircraft(shooter, target);
393 		} else
394 			AIR_SendAircraftPursuingUFO(shooter, target);
395 	} else {
396 		/* no ammo left, or no weapon, proceed with mission */
397 		if (shooter->type == AIRCRAFT_UFO) {
398 			shooter->aircraftTarget = nullptr;		/* reset target */
399 			CP_UFOProceedMission(campaign, shooter);
400 		} else {
401 			MS_AddNewMessage(_("Notice"), _("Our aircraft has no more ammo left - returning to home base now."));
402 			AIR_AircraftReturnToBase(shooter);
403 		}
404 	}
405 }
406 
407 /**
408  * @brief Set all projectile aiming a given aircraft to an idle destination.
409  * @param[in] aircraft Pointer to the aimed aircraft.
410  * @note This function is called when @c aircraft is destroyed.
411  * @sa AIRFIGHT_ActionsAfterAirfight
412  */
AIRFIGHT_RemoveProjectileAimingAircraft(const aircraft_t * aircraft)413 void AIRFIGHT_RemoveProjectileAimingAircraft (const aircraft_t* aircraft)
414 {
415 	aircraftProjectile_t* projectile;
416 	int idx = 0;
417 
418 	if (!aircraft)
419 		return;
420 
421 	for (projectile = ccs.projectiles; idx < ccs.numProjectiles; projectile++, idx++) {
422 		if (projectile->aimedAircraft != aircraft)
423 			continue;
424 
425 		AIRFIGHT_MissTarget(projectile);
426 		if (projectile->attackingAircraft && projectile->attackingAircraft->homebase) {
427 			assert(!AIR_IsUFO(projectile->attackingAircraft));
428 			AIR_AircraftReturnToBase(projectile->attackingAircraft);
429 		}
430 	}
431 }
432 
433 /**
434  * @brief Set all projectile attackingAircraft pointers to nullptr
435  * @param[in] aircraft Pointer to the destroyed aircraft.
436  * @note This function is called when @c aircraft is destroyed.
437  */
AIRFIGHT_UpdateProjectileForDestroyedAircraft(const aircraft_t * aircraft)438 static void AIRFIGHT_UpdateProjectileForDestroyedAircraft (const aircraft_t* aircraft)
439 {
440 	aircraftProjectile_t* projectile;
441 	int idx;
442 
443 	for (idx = 0, projectile = ccs.projectiles; idx < ccs.numProjectiles; projectile++, idx++) {
444 		const aircraft_t* attacker = projectile->attackingAircraft;
445 
446 		if (attacker == aircraft)
447 			projectile->attackingAircraft = nullptr;
448 	}
449 }
450 
451 /**
452  * @brief Actions to execute when a fight is done.
453  * @param[in] campaign The campaign data structure
454  * @param[in] shooter Pointer to the aircraft that fired the projectile.
455  * @param[in] aircraft Pointer to the aircraft which was destroyed (alien or phalanx).
456  * @param[in] phalanxWon true if PHALANX won, false if UFO won.
457  * @note Some of these mission values are redone (and not reloaded) in CP_Load
458  * @note shooter may be nullptr
459  * @sa UFO_DestroyAllUFOsOnGeoscape_f
460  * @sa CP_Load
461  * @sa CP_SpawnCrashSiteMission
462  */
AIRFIGHT_ActionsAfterAirfight(const campaign_t * campaign,aircraft_t * shooter,aircraft_t * aircraft,bool phalanxWon)463 void AIRFIGHT_ActionsAfterAirfight (const campaign_t* campaign, aircraft_t* shooter, aircraft_t* aircraft, bool phalanxWon)
464 {
465 	if (phalanxWon) {
466 		const byte* color;
467 
468 		assert(aircraft);
469 
470 		/* change destination of other projectiles aiming aircraft */
471 		AIRFIGHT_RemoveProjectileAimingAircraft(aircraft);
472 		/* now update the projectile for the destroyed aircraft, too */
473 		AIRFIGHT_UpdateProjectileForDestroyedAircraft(aircraft);
474 
475 		/* don't remove ufo from global array: the mission is not over yet
476 		 * UFO are removed from game only at the end of the mission
477 		 * (in case we need to know what item to collect e.g.) */
478 
479 		/* get the color value of the map at the crash position */
480 		color = GEO_GetColor(aircraft->pos, MAPTYPE_TERRAIN, nullptr);
481 		/* if this color value is not the value for water ...
482 		 * and we hit the probability to spawn a crashsite mission */
483 		if (!MapIsWater(color)) {
484 			CP_SpawnCrashSiteMission(aircraft);
485 		} else {
486 			Com_DPrintf(DEBUG_CLIENT, "AIRFIGHT_ActionsAfterAirfight: zone: %s (%i:%i:%i)\n", GEO_GetTerrainType(color), color[0], color[1], color[2]);
487 			MS_AddNewMessage(_("Interception"), _("UFO interception successful -- UFO lost to sea."));
488 			CP_MissionIsOverByUFO(aircraft);
489 		}
490 
491 		/* skill increase (for aircraft only, base defences skip) */
492 		if (shooter) {
493 			/* Increase targeting skill of pilot who destroyed UFO. Never more than 70, see AIRFIGHT_ProbabilityToHit() */
494 			shooter->pilot->chr.score.skills[SKILL_TARGETING] += 1;
495 			shooter->pilot->chr.score.skills[SKILL_TARGETING] = std::min(shooter->pilot->chr.score.skills[SKILL_TARGETING], 70);
496 
497 			/* Increase evasion skill of pilot who destroyed UFO if the aircraft it attacked can carry weapons.
498 			 * Never more than 70, see AIRFIGHT_ProbabilityToHit() */
499 			if (aircraft->maxWeapons > 0) {
500 				shooter->pilot->chr.score.skills[SKILL_EVADING] += 1;
501 				shooter->pilot->chr.score.skills[SKILL_EVADING] = std::min(shooter->pilot->chr.score.skills[SKILL_EVADING], 70);
502 			}
503 		}
504 	} else {
505 		/* change destination of other projectiles aiming aircraft */
506 		AIRFIGHT_RemoveProjectileAimingAircraft(aircraft);
507 
508 		/* and now update the projectile pointers (there still might be some in the air
509 		 * of the current destroyed aircraft) - this is needed not send the aircraft
510 		 * back to base as soon as the projectiles will hit their target */
511 		AIRFIGHT_UpdateProjectileForDestroyedAircraft(aircraft);
512 
513 		/* notify UFOs that a phalanx aircraft has been destroyed */
514 		UFO_NotifyPhalanxAircraftRemoved(aircraft);
515 
516 		if (!MapIsWater(GEO_GetColor(aircraft->pos, MAPTYPE_TERRAIN, nullptr))) {
517 			CP_SpawnRescueMission(aircraft, shooter);
518 		} else {
519 			/* Destroy the aircraft and everything onboard - the aircraft pointer
520 			 * is no longer valid after this point */
521 			bool pilotSurvived = false;
522 			if (AIR_PilotSurvivedCrash(aircraft))
523 				pilotSurvived = true;
524 
525 			AIR_DestroyAircraft(aircraft, pilotSurvived);
526 
527 			if (pilotSurvived)
528 				MS_AddNewMessage(_("Interception"), _("Pilot ejected from craft"), MSG_STANDARD);
529 			else
530 				MS_AddNewMessage(_("Interception"), _("Pilot killed in action"), MSG_STANDARD);
531 		}
532 
533 		/* Make UFO proceed with its mission, if it has not been already destroyed */
534 		if (shooter)
535 			CP_UFOProceedMission(campaign, shooter);
536 
537 		MS_AddNewMessage(_("Interception"), _("You've lost the battle"), MSG_DEATH);
538 	}
539 }
540 
541 /**
542  * @brief Check if some projectiles on geoscape reached their destination.
543  * @note Destination is not necessarily an aircraft, in case the projectile missed its initial target.
544  * @param[in] projectile Pointer to the projectile
545  * @param[in] movement distance that the projectile will do up to next draw of geoscape
546  * @sa AIRFIGHT_CampaignRunProjectiles
547  */
AIRFIGHT_ProjectileReachedTarget(const aircraftProjectile_t * projectile,float movement)548 static bool AIRFIGHT_ProjectileReachedTarget (const aircraftProjectile_t* projectile, float movement)
549 {
550 	float distance;
551 
552 	if (!projectile->aimedAircraft)
553 		/* the target is idle, its position is in idleTarget*/
554 		distance = GetDistanceOnGlobe(projectile->idleTarget, projectile->pos[0]);
555 	else {
556 		/* the target is moving, pointer to the other aircraft is aimedAircraft */
557 		distance = GetDistanceOnGlobe(projectile->aimedAircraft->pos, projectile->pos[0]);
558 	}
559 
560 	/* projectile reaches its target */
561 	if (distance < movement)
562 		return true;
563 
564 	assert(projectile->aircraftItem);
565 
566 	/* check if the projectile went farther than it's range */
567 	distance = (float) projectile->time * projectile->aircraftItem->craftitem.weaponSpeed / (float)SECONDS_PER_HOUR;
568 	if (distance > projectile->aircraftItem->craftitem.stats[AIR_STATS_WRANGE])
569 		return true;
570 
571 	return false;
572 }
573 
574 /**
575  * @brief Calculates the damage value for the airfight
576  * @param[in] od The ammo object definition of the craft item
577  * @param[in] target The aircraft the ammo hits
578  * @return the damage the hit causes
579  * @sa AII_UpdateAircraftStats
580  * @note ECM is handled in AIRFIGHT_ProbabilityToHit
581  */
AIRFIGHT_GetDamage(const objDef_t * od,const aircraft_t * target)582 static int AIRFIGHT_GetDamage (const objDef_t* od, const aircraft_t* target)
583 {
584 	int damage;
585 
586 	assert(od);
587 
588 	/* already destroyed - do nothing */
589 	if (target->damage <= 0)
590 		return 0;
591 
592 	/* base damage is given by the ammo */
593 	damage = od->craftitem.weaponDamage;
594 
595 	/* reduce damages with shield target */
596 	damage -= target->stats[AIR_STATS_SHIELD];
597 
598 	return damage;
599 }
600 
601 /**
602  * @brief Solve the result of one projectile hitting an aircraft.
603  * @param[in] campaign The campaign data structure
604  * @param[in] projectile Pointer to the projectile.
605  * @note the target loose (base damage - shield of target) hit points
606  */
AIRFIGHT_ProjectileHits(const campaign_t * campaign,aircraftProjectile_t * projectile)607 static void AIRFIGHT_ProjectileHits (const campaign_t* campaign, aircraftProjectile_t* projectile)
608 {
609 	aircraft_t* target;
610 
611 	assert(projectile);
612 	target = projectile->aimedAircraft;
613 	assert(target);
614 
615 	/* if the aircraft is not on geoscape anymore, do nothing (returned to base) */
616 	if (AIR_IsAircraftInBase(target))
617 		return;
618 
619 	const int damage = AIRFIGHT_GetDamage(projectile->aircraftItem, target);
620 
621 	/* apply resulting damages - but only if damage > 0 - because the target might
622 	 * already be destroyed, and we don't want to execute the actions after airfight
623 	 * for every projectile */
624 	if (damage > 0) {
625 		assert(target->damage > 0);
626 		target->damage -= damage;
627 		if (target->damage <= 0) {
628 			/* Target is destroyed */
629 			AIRFIGHT_ActionsAfterAirfight(campaign, projectile->attackingAircraft, target, target->type == AIRCRAFT_UFO);
630 			cgi->S_StartLocalSample("geoscape/combat-explosion", 1.0f);
631 		} else {
632 			if (projectile->rocket)
633 				cgi->S_StartLocalSample("geoscape/combat-rocket-exp", 1.0f);
634 		}
635 	}
636 }
637 
638 /**
639  * @brief Get the next point in the object path based on movement converting the positions from
640  * polar coordinates to vector for the calculation and back again to be returned.
641  * @param[in] movement The distance that the object needs to move.
642  * @param[in] originalPoint The point from which the object is moving.
643  * @param[in] orthogonalVector The orthogonal vector.
644  * @param[out] finalPoint The next point from the original point + movement in "angle" direction.
645  */
AIRFIGHT_GetNextPointInPathFromVector(const float * movement,const vec2_t originalPoint,const vec3_t orthogonalVector,vec2_t finalPoint)646 static void AIRFIGHT_GetNextPointInPathFromVector (const float* movement, const vec2_t originalPoint, const vec3_t orthogonalVector, vec2_t finalPoint)
647 {
648 	vec3_t startPoint, finalVectorPoint;
649 
650 	PolarToVec(originalPoint, startPoint);
651 	RotatePointAroundVector(finalVectorPoint, orthogonalVector, startPoint, *movement);
652 	VecToPolar(finalVectorPoint, finalPoint);
653 }
654 
655 /**
656  * @brief Get the next point in the object path based on movement.
657  * @param[in] movement The distance that the object needs to move.
658  * @param[in] originalPoint The point from which the object is moving.
659  * @param[in] targetPoint The final point to which the object is moving.
660  * @param[out] angle The direction that the object moving in.
661  * @param[out] finalPoint The next point from the original point + movement in "angle" direction.
662  * @param[out] orthogonalVector The orthogonal vector.
663  */
AIRFIGHT_GetNextPointInPath(const float * movement,const vec2_t originalPoint,const vec2_t targetPoint,float * angle,vec2_t finalPoint,vec3_t orthogonalVector)664 static void AIRFIGHT_GetNextPointInPath (const float* movement, const vec2_t originalPoint, const vec2_t targetPoint, float* angle, vec2_t finalPoint, vec3_t orthogonalVector)
665 {
666 	*angle = GEO_AngleOfPath(originalPoint, targetPoint, nullptr, orthogonalVector);
667 	AIRFIGHT_GetNextPointInPathFromVector(movement, originalPoint, orthogonalVector, finalPoint);
668 }
669 
670 /**
671  * @brief Update values of projectiles.
672  * @param[in] campaign The campaign data structure
673  * @param[in] dt Time elapsed since last call of this function.
674  */
AIRFIGHT_CampaignRunProjectiles(const campaign_t * campaign,int dt)675 void AIRFIGHT_CampaignRunProjectiles (const campaign_t* campaign, int dt)
676 {
677 	int idx;
678 
679 	/* ccs.numProjectiles is changed in AIRFIGHT_RemoveProjectile */
680 	for (idx = ccs.numProjectiles - 1; idx >= 0; idx--) {
681 		aircraftProjectile_t* projectile = &ccs.projectiles[idx];
682 		const float movement = (float) dt * projectile->aircraftItem->craftitem.weaponSpeed / (float)SECONDS_PER_HOUR;
683 		projectile->time += dt;
684 		projectile->hasMoved = true;
685 		projectile->numInterpolationPoints = 0;
686 
687 		/* Check if the projectile reached its destination (aircraft or idle point) */
688 		if (AIRFIGHT_ProjectileReachedTarget(projectile, movement)) {
689 			/* check if it got the ennemy */
690 			if (projectile->aimedAircraft)
691 				AIRFIGHT_ProjectileHits(campaign, projectile);
692 
693 			/* remove the missile from ccs.projectiles[] */
694 			AIRFIGHT_RemoveProjectile(projectile);
695 		} else {
696 			float angle;
697 			vec3_t ortogonalVector, finalPoint, projectedPoint;
698 
699 			/* missile is moving towards its target */
700 			if (projectile->aimedAircraft) {
701 				AIRFIGHT_GetNextPointInPath(&movement, projectile->pos[0], projectile->aimedAircraft->pos, &angle, finalPoint, ortogonalVector);
702 				AIRFIGHT_GetNextPointInPath(&movement, finalPoint, projectile->aimedAircraft->pos, &angle, projectedPoint, ortogonalVector);
703 			} else {
704 				AIRFIGHT_GetNextPointInPath(&movement, projectile->pos[0], projectile->idleTarget, &angle, finalPoint, ortogonalVector);
705 				AIRFIGHT_GetNextPointInPath(&movement, finalPoint, projectile->idleTarget, &angle, projectedPoint, ortogonalVector);
706 			}
707 
708 			/* update angle of the projectile */
709 			projectile->angle = angle;
710 			VectorCopy(finalPoint, projectile->pos[0]);
711 			VectorCopy(projectedPoint, projectile->projectedPos[0]);
712 		}
713 	}
714 }
715 
716 /**
717  * @brief Check if one type of battery (missile or laser) can shoot now.
718  * @param[in] base Pointer to the firing base.
719  * @param[in] weapons The base weapon to check and fire.
720  * @param[in] maxWeapons The number of weapons in that base.
721  */
AIRFIGHT_BaseShoot(const base_t * base,baseWeapon_t * weapons,int maxWeapons)722 static void AIRFIGHT_BaseShoot (const base_t* base, baseWeapon_t* weapons, int maxWeapons)
723 {
724 	int i;
725 
726 	for (i = 0; i < maxWeapons; i++) {
727 		aircraft_t* target = weapons[i].target;
728 		aircraftSlot_t* slot = &(weapons[i].slot);
729 		/* if no target, can't shoot */
730 		if (!target)
731 			continue;
732 
733 		/* If the weapon is not ready in base, can't shoot. */
734 		if (slot->installationTime > 0)
735 			continue;
736 
737 		/* if weapon is reloading, can't shoot */
738 		if (slot->delayNextShot > 0)
739 			continue;
740 
741 		/* check that the ufo is still visible */
742 		if (!UFO_IsUFOSeenOnGeoscape(target)) {
743 			weapons[i].target = nullptr;
744 			continue;
745 		}
746 
747 		/* Check if we can still fire on this target. */
748 		const float distance = GetDistanceOnGlobe(base->pos, target->pos);
749 		const int test = AIRFIGHT_CheckWeapon(slot, distance);
750 		/* weapon unable to shoot, reset target */
751 		if (test == AIRFIGHT_WEAPON_CAN_NEVER_SHOOT) {
752 			weapons[i].target = nullptr;
753 			continue;
754 		}
755 		/* we can't shoot with this weapon atm, wait to see if UFO comes closer */
756 		else if (test == AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT)
757 			continue;
758 		/* target is too far, wait to see if UFO comes closer */
759 		else if (distance > slot->ammo->craftitem.stats[AIR_STATS_WRANGE])
760 			continue;
761 
762 		/* shoot */
763 		if (AIRFIGHT_AddProjectile(base, nullptr, nullptr, target, slot)) {
764 			slot->delayNextShot = slot->ammo->craftitem.weaponDelay;
765 			/* will we miss the target ? */
766 			if (frand() > AIRFIGHT_ProbabilityToHit(nullptr, target, slot))
767 				AIRFIGHT_MissTarget(&ccs.projectiles[ccs.numProjectiles - 1]);
768 		}
769 	}
770 }
771 
772 /**
773  * @brief Check if one type of battery (missile or laser) can shoot now.
774  * @param[in] installation Pointer to the firing intallation.
775  * @param[in] weapons The installation weapons to check and fire.
776  * @param[in] maxWeapons The number of weapons in that installation.
777  */
AIRFIGHT_InstallationShoot(const installation_t * installation,baseWeapon_t * weapons,int maxWeapons)778 static void AIRFIGHT_InstallationShoot (const installation_t* installation, baseWeapon_t* weapons, int maxWeapons)
779 {
780 	int i;
781 
782 	for (i = 0; i < maxWeapons; i++) {
783 		aircraft_t* target = weapons[i].target;
784 		aircraftSlot_t* slot = &(weapons[i].slot);
785 		/* if no target, can't shoot */
786 		if (!target)
787 			continue;
788 
789 		/* If the weapon is not ready in base, can't shoot. */
790 		if (slot->installationTime > 0)
791 			continue;
792 
793 		/* if weapon is reloading, can't shoot */
794 		if (slot->delayNextShot > 0)
795 			continue;
796 
797 		/* check that the ufo is still visible */
798 		if (!UFO_IsUFOSeenOnGeoscape(target)) {
799 			weapons[i].target = nullptr;
800 			continue;
801 		}
802 
803 		/* Check if we can still fire on this target. */
804 		const float distance = GetDistanceOnGlobe(installation->pos, target->pos);
805 		const int test = AIRFIGHT_CheckWeapon(slot, distance);
806 		/* weapon unable to shoot, reset target */
807 		if (test == AIRFIGHT_WEAPON_CAN_NEVER_SHOOT) {
808 			weapons[i].target = nullptr;
809 			continue;
810 		}
811 		/* we can't shoot with this weapon atm, wait to see if UFO comes closer */
812 		else if (test == AIRFIGHT_WEAPON_CAN_NOT_SHOOT_AT_THE_MOMENT)
813 			continue;
814 		/* target is too far, wait to see if UFO comes closer */
815 		else if (distance > slot->ammo->craftitem.stats[AIR_STATS_WRANGE])
816 			continue;
817 
818 		/* shoot */
819 		if (AIRFIGHT_AddProjectile(nullptr, installation, nullptr, target, slot)) {
820 			slot->delayNextShot = slot->ammo->craftitem.weaponDelay;
821 			/* will we miss the target ? */
822 			if (frand() > AIRFIGHT_ProbabilityToHit(nullptr, target, slot))
823 				AIRFIGHT_MissTarget(&ccs.projectiles[ccs.numProjectiles - 1]);
824 		}
825 	}
826 }
827 
828 /**
829  * @brief Run base defences.
830  * @param[in] dt Time elapsed since last call of this function.
831  */
AIRFIGHT_CampaignRunBaseDefence(int dt)832 void AIRFIGHT_CampaignRunBaseDefence (int dt)
833 {
834 	base_t* base;
835 
836 	base = nullptr;
837 	while ((base = B_GetNext(base)) != nullptr) {
838 		int idx;
839 
840 		if (B_IsUnderAttack(base))
841 			continue;
842 
843 		for (idx = 0; idx < base->numBatteries; idx++) {
844 			baseWeapon_t* battery = &base->batteries[idx];
845 			aircraftSlot_t* slot = &battery->slot;
846 			if (slot->delayNextShot > 0)
847 				slot->delayNextShot -= dt;
848 			if (slot->ammoLeft <= 0)
849 				AII_ReloadWeapon(slot);
850 		}
851 
852 		for (idx = 0; idx < base->numLasers; idx++) {
853 			baseWeapon_t* battery = &base->lasers[idx];
854 			aircraftSlot_t* slot = &battery->slot;
855 			if (slot->delayNextShot > 0)
856 				slot->delayNextShot -= dt;
857 			if (slot->ammoLeft <= 0)
858 				AII_ReloadWeapon(slot);
859 		}
860 
861 		if (AII_BaseCanShoot(base)) {
862 			if (B_GetBuildingStatus(base, B_DEFENCE_MISSILE))
863 				AIRFIGHT_BaseShoot(base, base->batteries, base->numBatteries);
864 			if (B_GetBuildingStatus(base, B_DEFENCE_LASER))
865 				AIRFIGHT_BaseShoot(base, base->lasers, base->numLasers);
866 		}
867 	}
868 
869 	INS_Foreach(installation) {
870 		if (installation->installationStatus != INSTALLATION_WORKING)
871 			continue;
872 
873 		if (installation->installationTemplate->maxBatteries <= 0)
874 			continue;
875 
876 		for (int idx = 0; idx < installation->installationTemplate->maxBatteries; idx++) {
877 			baseWeapon_t* battery = &installation->batteries[idx];
878 			aircraftSlot_t* slot = &battery->slot;
879 			if (slot->delayNextShot > 0)
880 				slot->delayNextShot -= dt;
881 			if (slot->ammoLeft <= 0)
882 				AII_ReloadWeapon(slot);
883 		}
884 
885 		if (AII_InstallationCanShoot(installation)) {
886 			AIRFIGHT_InstallationShoot(installation, installation->batteries, installation->installationTemplate->maxBatteries);
887 		}
888 	}
889 }
890 
891 /**
892  * @brief Save callback for savegames in XML Format
893  * @param[out] parent XML Node structure, where we write the information to
894  */
AIRFIGHT_SaveXML(xmlNode_t * parent)895 bool AIRFIGHT_SaveXML (xmlNode_t* parent)
896 {
897 	int i;
898 
899 	for (i = 0; i < ccs.numProjectiles; i++) {
900 		int j;
901 		aircraftProjectile_t* projectile = &ccs.projectiles[i];
902 		xmlNode_t* node = cgi->XML_AddNode(parent, SAVE_AIRFIGHT_PROJECTILE);
903 
904 		cgi->XML_AddString(node, SAVE_AIRFIGHT_ITEMID, projectile->aircraftItem->id);
905 		for (j = 0; j < projectile->numProjectiles; j++)
906 			cgi->XML_AddPos2(node, SAVE_AIRFIGHT_POS, projectile->pos[j]);
907 		cgi->XML_AddPos3(node, SAVE_AIRFIGHT_IDLETARGET, projectile->idleTarget);
908 
909 		cgi->XML_AddInt(node, SAVE_AIRFIGHT_TIME, projectile->time);
910 		cgi->XML_AddFloat(node, SAVE_AIRFIGHT_ANGLE, projectile->angle);
911 		cgi->XML_AddBoolValue(node, SAVE_AIRFIGHT_BULLET, projectile->bullets);
912 		cgi->XML_AddBoolValue(node, SAVE_AIRFIGHT_BEAM, projectile->beam);
913 
914 		if (projectile->attackingAircraft) {
915 			xmlNode_t* attacking =  cgi->XML_AddNode(node, SAVE_AIRFIGHT_ATTACKINGAIRCRAFT);
916 
917 			cgi->XML_AddBoolValue(attacking, SAVE_AIRFIGHT_ISUFO, projectile->attackingAircraft->type == AIRCRAFT_UFO);
918 			if (projectile->attackingAircraft->type == AIRCRAFT_UFO)
919 				cgi->XML_AddInt(attacking, SAVE_AIRFIGHT_AIRCRAFTIDX, UFO_GetGeoscapeIDX(projectile->attackingAircraft));
920 			else
921 				cgi->XML_AddInt(attacking, SAVE_AIRFIGHT_AIRCRAFTIDX, projectile->attackingAircraft->idx);
922 		}
923 		cgi->XML_AddPos3(node, SAVE_AIRFIGHT_ATTACKERPOS, projectile->attackerPos);
924 
925 		if (projectile->aimedAircraft) {
926 			xmlNode_t* aimed =  cgi->XML_AddNode(node, SAVE_AIRFIGHT_AIMEDAIRCRAFT);
927 
928 			cgi->XML_AddBoolValue(aimed, SAVE_AIRFIGHT_ISUFO, projectile->aimedAircraft->type == AIRCRAFT_UFO);
929 			if (projectile->aimedAircraft->type == AIRCRAFT_UFO)
930 				cgi->XML_AddInt(aimed, SAVE_AIRFIGHT_AIRCRAFTIDX, UFO_GetGeoscapeIDX(projectile->aimedAircraft));
931 			else
932 				cgi->XML_AddInt(aimed, SAVE_AIRFIGHT_AIRCRAFTIDX, projectile->aimedAircraft->idx);
933 		}
934 	}
935 
936 	return true;
937 }
938 
939 /**
940  * @brief Load callback for savegames in XML Format
941  * @param[in] parent XML Node structure, where we get the information from
942  */
AIRFIGHT_LoadXML(xmlNode_t * parent)943 bool AIRFIGHT_LoadXML (xmlNode_t* parent)
944 {
945 	int i;
946 	xmlNode_t* node;
947 
948 	for (i = 0, node = cgi->XML_GetNode(parent, SAVE_AIRFIGHT_PROJECTILE); i < MAX_PROJECTILESONGEOSCAPE && node;
949 			node = cgi->XML_GetNextNode(node, parent, SAVE_AIRFIGHT_PROJECTILE), i++) {
950 		technology_t* tech = RS_GetTechByProvided(cgi->XML_GetString(node, SAVE_AIRFIGHT_ITEMID));
951 		int j;
952 		xmlNode_t* positions;
953 		xmlNode_t* attackingAircraft;
954 		xmlNode_t* aimedAircraft;
955 		aircraftProjectile_t* projectile = &ccs.projectiles[i];
956 
957 		if (!tech) {
958 			Com_Printf("AIR_Load: Could not get technology of projectile %i\n", i);
959 			return false;
960 		}
961 
962 		projectile->aircraftItem = INVSH_GetItemByID(tech->provides);
963 
964 		for (j = 0, positions = cgi->XML_GetPos2(node, SAVE_AIRFIGHT_POS, projectile->pos[0]); j < MAX_MULTIPLE_PROJECTILES && positions;
965 			j++, positions = cgi->XML_GetNextPos2(positions, node, SAVE_AIRFIGHT_POS, projectile->pos[j]))
966 			;
967 		projectile->numProjectiles = j;
968 		cgi->XML_GetPos3(node, SAVE_AIRFIGHT_IDLETARGET, projectile->idleTarget);
969 
970 		projectile->time = cgi->XML_GetInt(node, SAVE_AIRFIGHT_TIME, 0);
971 		projectile->angle = cgi->XML_GetFloat(node, SAVE_AIRFIGHT_ANGLE, 0.0);
972 		projectile->bullets = cgi->XML_GetBool(node, SAVE_AIRFIGHT_BULLET, false);
973 		projectile->beam = cgi->XML_GetBool(node, SAVE_AIRFIGHT_BEAM, false);
974 
975 		if ((attackingAircraft = cgi->XML_GetNode(node, SAVE_AIRFIGHT_ATTACKINGAIRCRAFT))) {
976 			if (cgi->XML_GetBool(attackingAircraft, SAVE_AIRFIGHT_ISUFO, false))
977 				/** @todo 0 as default might be incorrect */
978 				projectile->attackingAircraft = UFO_GetByIDX(cgi->XML_GetInt(attackingAircraft, SAVE_AIRFIGHT_AIRCRAFTIDX, 0));
979 			else
980 				projectile->attackingAircraft = AIR_AircraftGetFromIDX(cgi->XML_GetInt(attackingAircraft, SAVE_AIRFIGHT_AIRCRAFTIDX, AIRCRAFT_INVALID));
981 		} else {
982 			projectile->attackingAircraft = nullptr;
983 		}
984 		cgi->XML_GetPos3(node, SAVE_AIRFIGHT_ATTACKERPOS, projectile->attackerPos);
985 
986 		if ((aimedAircraft = cgi->XML_GetNode(node, SAVE_AIRFIGHT_AIMEDAIRCRAFT))) {
987 			if (cgi->XML_GetBool(aimedAircraft, SAVE_AIRFIGHT_ISUFO, false))
988 				/** @todo 0 as default might be incorrect */
989 				projectile->aimedAircraft = UFO_GetByIDX(cgi->XML_GetInt(aimedAircraft, SAVE_AIRFIGHT_AIRCRAFTIDX, 0));
990 			else
991 				projectile->aimedAircraft = AIR_AircraftGetFromIDX(cgi->XML_GetInt(aimedAircraft, SAVE_AIRFIGHT_AIRCRAFTIDX, AIRCRAFT_INVALID));
992 		} else {
993 			projectile->aimedAircraft = nullptr;
994 		}
995 	}
996 	ccs.numProjectiles = i;
997 
998 	return true;
999 }
1000 
1001 /**
1002  * @sa UI_InitStartup
1003  */
AIRFIGHT_InitStartup(void)1004 void AIRFIGHT_InitStartup (void)
1005 {
1006 #ifdef DEBUG
1007 	cgi->Cmd_AddCommand("debug_listprojectile", AIRFIGHT_ProjectileList_f, "Print Projectiles information to game console");
1008 #endif
1009 }
1010