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