1 /* Ship.cpp
2 Copyright (c) 2014 by Michael Zahniser
3
4 Endless Sky is free software: you can redistribute it and/or modify it under the
5 terms of the GNU General Public License as published by the Free Software
6 Foundation, either version 3 of the License, or (at your option) any later version.
7
8 Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE. See the GNU General Public License for more details.
11 */
12
13 #include "Ship.h"
14
15 #include "Audio.h"
16 #include "DataNode.h"
17 #include "DataWriter.h"
18 #include "Effect.h"
19 #include "Files.h"
20 #include "Flotsam.h"
21 #include "text/Format.h"
22 #include "GameData.h"
23 #include "Government.h"
24 #include "Hazard.h"
25 #include "Mask.h"
26 #include "Messages.h"
27 #include "Phrase.h"
28 #include "Planet.h"
29 #include "PlayerInfo.h"
30 #include "Preferences.h"
31 #include "Projectile.h"
32 #include "Random.h"
33 #include "ShipEvent.h"
34 #include "Sound.h"
35 #include "SpriteSet.h"
36 #include "Sprite.h"
37 #include "StellarObject.h"
38 #include "System.h"
39 #include "Visual.h"
40
41 #include <algorithm>
42 #include <cmath>
43 #include <limits>
44 #include <sstream>
45
46 using namespace std;
47
48 namespace {
49 const string FIGHTER_REPAIR = "Repair fighters in";
50 const vector<string> BAY_SIDE = {"inside", "over", "under"};
51 const vector<string> BAY_FACING = {"forward", "left", "right", "back"};
52 const vector<Angle> BAY_ANGLE = {Angle(0.), Angle(-90.), Angle(90.), Angle(180.)};
53
54 const vector<string> ENGINE_SIDE = {"under", "over"};
55 const vector<string> STEERING_FACING = {"none", "left", "right"};
56
57 const double MAXIMUM_TEMPERATURE = 100.;
58
59 const double SCAN_TIME = 60.;
60
61 // Helper function to transfer energy to a given stat if it is less than the
62 // given maximum value.
DoRepair(double & stat,double & available,double maximum)63 void DoRepair(double &stat, double &available, double maximum)
64 {
65 double transfer = max(0., min(available, maximum - stat));
66 stat += transfer;
67 available -= transfer;
68 }
69
70 // Helper function to repair a given stat up to its maximum, limited by
71 // how much repair is available and how much energy, fuel, and heat are available.
72 // Updates the stat, the available amount, and the energy, fuel, and heat amounts.
DoRepair(double & stat,double & available,double maximum,double & energy,double energyCost,double & fuel,double fuelCost,double & heat,double heatCost)73 void DoRepair(double &stat, double &available, double maximum, double &energy, double energyCost, double &fuel, double fuelCost, double &heat, double heatCost)
74 {
75 if(available <= 0. || stat >= maximum)
76 return;
77
78 // Energy, heat, and fuel costs are the energy, fuel, or heat required per unit repaired.
79 if(energyCost > 0.)
80 available = min(available, energy / energyCost);
81 if(fuelCost > 0.)
82 available = min(available, fuel / fuelCost);
83 if(heatCost < 0.)
84 available = min(available, heat / -heatCost);
85
86 double transfer = min(available, maximum - stat);
87 if(transfer > 0.)
88 {
89 stat += transfer;
90 available -= transfer;
91 energy -= transfer * energyCost;
92 fuel -= transfer * fuelCost;
93 heat += transfer * heatCost;
94 }
95 }
96
97 // Helper function to reduce a given status effect according
98 // to its resistance, limited by how much energy, fuel, and heat are available.
99 // Updates the stat and the energy, fuel, and heat amounts.
DoStatusEffect(bool isDeactivated,double & stat,double resistance,double & energy,double energyCost,double & fuel,double fuelCost,double & heat,double heatCost)100 void DoStatusEffect(bool isDeactivated, double &stat, double resistance, double &energy, double energyCost, double &fuel, double fuelCost, double &heat, double heatCost)
101 {
102 if(isDeactivated || resistance <= 0.)
103 {
104 stat = .99 * stat;
105 return;
106 }
107
108 // Calculate how much resistance can be used assuming no
109 // energy or fuel cost.
110 resistance = .99 * stat - max(0., .99 * stat - resistance);
111
112 // Limit the resistance by the available energy, heat, and fuel.
113 if(energyCost > 0.)
114 resistance = min(resistance, energy / energyCost);
115 if(fuelCost > 0.)
116 resistance = min(resistance, fuel / fuelCost);
117 if(heatCost < 0.)
118 resistance = min(resistance, heat / -heatCost);
119
120 // Update the stat, energy, heat, and fuel given how much resistance is being used.
121 if(resistance > 0.)
122 {
123 stat = max(0., .99 * stat - resistance);
124 energy -= resistance * energyCost;
125 fuel -= resistance * fuelCost;
126 heat += resistance * heatCost;
127 }
128 else
129 stat = .99 * stat;
130 }
131 }
132
133
134
135 const vector<string> Ship::CATEGORIES = {
136 "Transport",
137 "Light Freighter",
138 "Heavy Freighter",
139 "Interceptor",
140 "Light Warship",
141 "Medium Warship",
142 "Heavy Warship",
143 "Fighter",
144 "Drone"
145 };
146
147
148
149 // Set of ship types that can be carried in bays.
150 const set<string> Ship::BAY_TYPES = {
151 "Drone",
152 "Fighter"
153 };
154
155
156
157 // Construct and Load() at the same time.
Ship(const DataNode & node)158 Ship::Ship(const DataNode &node)
159 {
160 Load(node);
161 }
162
163
164
Load(const DataNode & node)165 void Ship::Load(const DataNode &node)
166 {
167 if(node.Size() >= 2)
168 {
169 modelName = node.Token(1);
170 pluralModelName = modelName + 's';
171 }
172 if(node.Size() >= 3)
173 {
174 base = GameData::Ships().Get(modelName);
175 variantName = node.Token(2);
176 }
177 isDefined = true;
178
179 government = GameData::PlayerGovernment();
180 equipped.clear();
181
182 // Note: I do not clear the attributes list here so that it is permissible
183 // to override one ship definition with another.
184 bool hasEngine = false;
185 bool hasArmament = false;
186 bool hasBays = false;
187 bool hasExplode = false;
188 bool hasLeak = false;
189 bool hasFinalExplode = false;
190 bool hasOutfits = false;
191 bool hasDescription = false;
192 for(const DataNode &child : node)
193 {
194 const string &key = child.Token(0);
195 bool add = (key == "add");
196 if(add && (child.Size() < 2 || child.Token(1) != "attributes"))
197 {
198 child.PrintTrace("Skipping invalid use of 'add' with " + (child.Size() < 2
199 ? "no key." : "key: " + child.Token(1)));
200 continue;
201 }
202 if(key == "sprite")
203 LoadSprite(child);
204 else if(child.Token(0) == "thumbnail" && child.Size() >= 2)
205 thumbnail = SpriteSet::Get(child.Token(1));
206 else if(key == "name" && child.Size() >= 2)
207 name = child.Token(1);
208 else if(key == "plural" && child.Size() >= 2)
209 pluralModelName = child.Token(1);
210 else if(key == "noun" && child.Size() >= 2)
211 noun = child.Token(1);
212 else if(key == "swizzle" && child.Size() >= 2)
213 customSwizzle = child.Value(1);
214 else if(key == "attributes" || add)
215 {
216 if(!add)
217 baseAttributes.Load(child);
218 else
219 {
220 addAttributes = true;
221 attributes.Load(child);
222 }
223 }
224 else if((key == "engine" || key == "reverse engine" || key == "steering engine") && child.Size() >= 3)
225 {
226 if(!hasEngine)
227 {
228 enginePoints.clear();
229 reverseEnginePoints.clear();
230 steeringEnginePoints.clear();
231 hasEngine = true;
232 }
233 bool reverse = (key == "reverse engine");
234 bool steering = (key == "steering engine");
235
236 vector<EnginePoint> &editPoints = (!steering && !reverse) ? enginePoints :
237 (reverse ? reverseEnginePoints : steeringEnginePoints);
238 editPoints.emplace_back(0.5 * child.Value(1), 0.5 * child.Value(2),
239 (child.Size() > 3 ? child.Value(3) : 1.));
240 EnginePoint &engine = editPoints.back();
241 if(reverse)
242 engine.facing = Angle(180.);
243 for(const DataNode &grand : child)
244 {
245 const string &grandKey = grand.Token(0);
246 if(grandKey == "zoom" && grand.Size() >= 2)
247 engine.zoom = grand.Value(1);
248 else if(grandKey == "angle" && grand.Size() >= 2)
249 engine.facing += Angle(grand.Value(1));
250 else
251 {
252 for(unsigned j = 1; j < ENGINE_SIDE.size(); ++j)
253 if(grandKey == ENGINE_SIDE[j])
254 engine.side = j;
255 if(steering)
256 for(unsigned j = 1; j < STEERING_FACING.size(); ++j)
257 if(grandKey == STEERING_FACING[j])
258 engine.steering = j;
259 }
260 }
261 }
262 else if(key == "gun" || key == "turret")
263 {
264 if(!hasArmament)
265 {
266 armament = Armament();
267 hasArmament = true;
268 }
269 const Outfit *outfit = nullptr;
270 Point hardpoint;
271 if(child.Size() >= 3)
272 {
273 hardpoint = Point(child.Value(1), child.Value(2));
274 if(child.Size() >= 4)
275 outfit = GameData::Outfits().Get(child.Token(3));
276 }
277 else
278 {
279 if(child.Size() >= 2)
280 outfit = GameData::Outfits().Get(child.Token(1));
281 }
282 Angle gunPortAngle = Angle(0.);
283 bool gunPortParallel = false;
284 if(child.HasChildren())
285 {
286 for(const DataNode &grand : child)
287 if(grand.Token(0) == "angle" && grand.Size() >= 2)
288 gunPortAngle = grand.Value(1);
289 else if(grand.Token(0) == "parallel")
290 gunPortParallel = true;
291 else
292 child.PrintTrace("Warning: Child nodes of \"" + key + "\" tokens can only be \"angle\" or \"parallel\":");
293 }
294 if(outfit)
295 ++equipped[outfit];
296 if(key == "gun")
297 armament.AddGunPort(hardpoint, gunPortAngle, gunPortParallel, outfit);
298 else
299 armament.AddTurret(hardpoint, outfit);
300 // Print a warning for the first hardpoint after 32, i.e. only 1 warning per ship.
301 if(armament.Get().size() == 33)
302 child.PrintTrace("Warning: ship has more than 32 weapon hardpoints. Some weapons may not fire:");
303 }
304 else if(key == "never disabled")
305 neverDisabled = true;
306 else if(key == "uncapturable")
307 isCapturable = false;
308 else if(((key == "fighter" || key == "drone") && child.Size() >= 3) ||
309 (key == "bay" && child.Size() >= 4))
310 {
311 // While the `drone` and `fighter` keywords are supported for backwards compatibility, the
312 // standard format is `bay <ship-category>`, with the same signature for other values.
313 string category = "Fighter";
314 int childOffset = 0;
315 if(key == "drone")
316 category = "Drone";
317 else if(key == "bay")
318 {
319 category = child.Token(1);
320 childOffset += 1;
321 }
322 if(!BAY_TYPES.count(category))
323 {
324 child.PrintTrace("Warning: Invalid category defined for bay:");
325 continue;
326 }
327
328 if(!hasBays)
329 {
330 bays.clear();
331 hasBays = true;
332 }
333 bays.emplace_back(child.Value(1 + childOffset), child.Value(2 + childOffset), category);
334 Bay &bay = bays.back();
335 for(int i = 3 + childOffset; i < child.Size(); ++i)
336 {
337 for(unsigned j = 1; j < BAY_SIDE.size(); ++j)
338 if(child.Token(i) == BAY_SIDE[j])
339 bay.side = j;
340 for(unsigned j = 1; j < BAY_FACING.size(); ++j)
341 if(child.Token(i) == BAY_FACING[j])
342 bay.facing = BAY_ANGLE[j];
343 }
344 if(child.HasChildren())
345 for(const DataNode &grand : child)
346 {
347 // Load in the effect(s) to be displayed when the ship launches.
348 if(grand.Token(0) == "launch effect" && grand.Size() >= 2)
349 {
350 int count = grand.Size() >= 3 ? static_cast<int>(grand.Value(2)) : 1;
351 const Effect *e = GameData::Effects().Get(grand.Token(1));
352 bay.launchEffects.insert(bay.launchEffects.end(), count, e);
353 }
354 else if(grand.Token(0) == "angle" && grand.Size() >= 2)
355 bay.facing = Angle(grand.Value(1));
356 else
357 {
358 bool handled = false;
359 for(unsigned i = 1; i < BAY_SIDE.size(); ++i)
360 if(grand.Token(0) == BAY_SIDE[i])
361 {
362 bay.side = i;
363 handled = true;
364 }
365 for(unsigned i = 1; i < BAY_FACING.size(); ++i)
366 if(grand.Token(0) == BAY_FACING[i])
367 {
368 bay.facing = BAY_ANGLE[i];
369 handled = true;
370 }
371 if(!handled)
372 grand.PrintTrace("Child nodes of \"bay\" tokens can only be \"launch effect\", \"angle\", or a facing keyword:");
373 }
374 }
375 }
376 else if(key == "leak" && child.Size() >= 2)
377 {
378 if(!hasLeak)
379 {
380 leaks.clear();
381 hasLeak = true;
382 }
383 Leak leak(GameData::Effects().Get(child.Token(1)));
384 if(child.Size() >= 3)
385 leak.openPeriod = child.Value(2);
386 if(child.Size() >= 4)
387 leak.closePeriod = child.Value(3);
388 leaks.push_back(leak);
389 }
390 else if(key == "explode" && child.Size() >= 2)
391 {
392 if(!hasExplode)
393 {
394 explosionEffects.clear();
395 explosionTotal = 0;
396 hasExplode = true;
397 }
398 int count = (child.Size() >= 3) ? child.Value(2) : 1;
399 explosionEffects[GameData::Effects().Get(child.Token(1))] += count;
400 explosionTotal += count;
401 }
402 else if(key == "final explode" && child.Size() >= 2)
403 {
404 if(!hasFinalExplode)
405 {
406 finalExplosions.clear();
407 hasFinalExplode = true;
408 }
409 int count = (child.Size() >= 3) ? child.Value(2) : 1;
410 finalExplosions[GameData::Effects().Get(child.Token(1))] += count;
411 }
412 else if(key == "outfits")
413 {
414 if(!hasOutfits)
415 {
416 outfits.clear();
417 hasOutfits = true;
418 }
419 for(const DataNode &grand : child)
420 {
421 int count = (grand.Size() >= 2) ? grand.Value(1) : 1;
422 if(count > 0)
423 outfits[GameData::Outfits().Get(grand.Token(0))] += count;
424 else
425 grand.PrintTrace("Skipping invalid outfit count:");
426 }
427 }
428 else if(key == "cargo")
429 cargo.Load(child);
430 else if(key == "crew" && child.Size() >= 2)
431 crew = static_cast<int>(child.Value(1));
432 else if(key == "fuel" && child.Size() >= 2)
433 fuel = child.Value(1);
434 else if(key == "shields" && child.Size() >= 2)
435 shields = child.Value(1);
436 else if(key == "hull" && child.Size() >= 2)
437 hull = child.Value(1);
438 else if(key == "position" && child.Size() >= 3)
439 position = Point(child.Value(1), child.Value(2));
440 else if(key == "system" && child.Size() >= 2)
441 currentSystem = GameData::Systems().Get(child.Token(1));
442 else if(key == "planet" && child.Size() >= 2)
443 {
444 zoom = 0.;
445 landingPlanet = GameData::Planets().Get(child.Token(1));
446 }
447 else if(key == "destination system" && child.Size() >= 2)
448 targetSystem = GameData::Systems().Get(child.Token(1));
449 else if(key == "parked")
450 isParked = true;
451 else if(key == "description" && child.Size() >= 2)
452 {
453 if(!hasDescription)
454 {
455 description.clear();
456 hasDescription = true;
457 }
458 description += child.Token(1);
459 description += '\n';
460 }
461 else if(key != "actions")
462 child.PrintTrace("Skipping unrecognized attribute:");
463 }
464 }
465
466
467
468 // When loading a ship, some of the outfits it lists may not have been
469 // loaded yet. So, wait until everything has been loaded, then call this.
FinishLoading(bool isNewInstance)470 void Ship::FinishLoading(bool isNewInstance)
471 {
472 // All copies of this ship should save pointers to the "explosion" weapon
473 // definition stored safely in the ship model, which will not be destroyed
474 // until GameData is when the program quits. Also copy other attributes of
475 // the base model if no overrides were given.
476 if(GameData::Ships().Has(modelName))
477 {
478 const Ship *model = GameData::Ships().Get(modelName);
479 explosionWeapon = &model->BaseAttributes();
480 if(pluralModelName.empty())
481 pluralModelName = model->pluralModelName;
482 if(noun.empty())
483 noun = model->noun;
484 if(!thumbnail)
485 thumbnail = model->thumbnail;
486 }
487
488 // If this ship has a base class, copy any attributes not defined here.
489 // Exception: uncapturable and "never disabled" flags don't carry over.
490 if(base && base != this)
491 {
492 if(!GetSprite())
493 reinterpret_cast<Body &>(*this) = *base;
494 if(customSwizzle == -1)
495 customSwizzle = base->CustomSwizzle();
496 if(baseAttributes.Attributes().empty())
497 baseAttributes = base->baseAttributes;
498 if(bays.empty() && !base->bays.empty())
499 bays = base->bays;
500 if(enginePoints.empty())
501 enginePoints = base->enginePoints;
502 if(reverseEnginePoints.empty())
503 reverseEnginePoints = base->reverseEnginePoints;
504 if(steeringEnginePoints.empty())
505 steeringEnginePoints = base->steeringEnginePoints;
506 if(explosionEffects.empty())
507 {
508 explosionEffects = base->explosionEffects;
509 explosionTotal = base->explosionTotal;
510 }
511 if(finalExplosions.empty())
512 finalExplosions = base->finalExplosions;
513 if(outfits.empty())
514 outfits = base->outfits;
515 if(description.empty())
516 description = base->description;
517
518 bool hasHardpoints = false;
519 for(const Hardpoint &hardpoint : armament.Get())
520 if(hardpoint.GetPoint())
521 hasHardpoints = true;
522
523 if(!hasHardpoints)
524 {
525 // Check if any hardpoint locations were not specified.
526 auto bit = base->Weapons().begin();
527 auto bend = base->Weapons().end();
528 auto nextGun = armament.Get().begin();
529 auto nextTurret = armament.Get().begin();
530 auto end = armament.Get().end();
531 Armament merged;
532 // Reset the "equipped" map to match exactly what the code below
533 // places in the weapon hardpoints.
534 equipped.clear();
535 for( ; bit != bend; ++bit)
536 {
537 if(!bit->IsTurret())
538 {
539 while(nextGun != end && nextGun->IsTurret())
540 ++nextGun;
541 const Outfit *outfit = (nextGun == end) ? nullptr : nextGun->GetOutfit();
542 merged.AddGunPort(bit->GetPoint() * 2., bit->GetBaseAngle(), bit->IsParallel(), outfit);
543 if(nextGun != end)
544 {
545 if(outfit)
546 ++equipped[outfit];
547 ++nextGun;
548 }
549 }
550 else
551 {
552 while(nextTurret != end && !nextTurret->IsTurret())
553 ++nextTurret;
554 const Outfit *outfit = (nextTurret == end) ? nullptr : nextTurret->GetOutfit();
555 merged.AddTurret(bit->GetPoint() * 2., outfit);
556 if(nextTurret != end)
557 {
558 if(outfit)
559 ++equipped[outfit];
560 ++nextTurret;
561 }
562 }
563 }
564 armament = merged;
565 }
566 }
567 // Check that all the "equipped" weapons actually match what your ship
568 // has, and that they are truly weapons. Remove any excess weapons and
569 // warn if any non-weapon outfits are "installed" in a hardpoint.
570 for(auto &it : equipped)
571 {
572 int excess = it.second - outfits[it.first];
573 if(excess > 0)
574 {
575 // If there are more hardpoints specifying this outfit than there
576 // are instances of this outfit installed, remove some of them.
577 armament.Add(it.first, -excess);
578 it.second -= excess;
579
580 string warning = modelName;
581 if(!name.empty())
582 warning += " \"" + name + "\"";
583 warning += ": outfit \"" + it.first->Name() + "\" equipped but not included in outfit list.";
584 Files::LogError(warning);
585 }
586 else if(!it.first->IsWeapon())
587 {
588 // This ship was specified with a non-weapon outfit in a
589 // hardpoint. Hardpoint::Install removes it, but issue a
590 // warning so the definition can be fixed.
591 string warning = modelName;
592 if(!name.empty())
593 warning += " \"" + name + "\"";
594 warning += ": outfit \"" + it.first->Name() + "\" is not a weapon, but is installed as one.";
595 Files::LogError(warning);
596 }
597 }
598
599 // Mark any drone that has no "automaton" value as an automaton, to
600 // grandfather in the drones from before that attribute existed.
601 if(baseAttributes.Category() == "Drone" && !baseAttributes.Get("automaton"))
602 baseAttributes.Set("automaton", 1.);
603
604 baseAttributes.Set("gun ports", armament.GunCount());
605 baseAttributes.Set("turret mounts", armament.TurretCount());
606
607 if(addAttributes)
608 {
609 // Store attributes from an "add attributes" node in the ship's
610 // baseAttributes so they can be written to the save file.
611 baseAttributes.Add(attributes);
612 addAttributes = false;
613 }
614 // Add the attributes of all your outfits to the ship's base attributes.
615 attributes = baseAttributes;
616 vector<string> undefinedOutfits;
617 for(const auto &it : outfits)
618 {
619 if(!it.first->IsDefined())
620 {
621 undefinedOutfits.emplace_back("\"" + it.first->Name() + "\"");
622 continue;
623 }
624 attributes.Add(*it.first, it.second);
625 // Some ship variant definitions do not specify which weapons
626 // are placed in which hardpoint. Add any weapons that are not
627 // yet installed to the ship's armament.
628 if(it.first->IsWeapon())
629 {
630 int count = it.second;
631 auto eit = equipped.find(it.first);
632 if(eit != equipped.end())
633 count -= eit->second;
634
635 if(count)
636 armament.Add(it.first, count);
637 }
638 }
639 if(!undefinedOutfits.empty())
640 {
641 bool plural = undefinedOutfits.size() > 1;
642 // Print the ship name once, then all undefined outfits. If we're reporting for a stock ship, then it
643 // doesn't have a name, and missing outfits aren't named yet either. A variant name might exist, though.
644 string message;
645 if(isYours)
646 {
647 message = "Player ship " + modelName + " \"" + name + "\":";
648 string PREFIX = plural ? "\n\tUndefined outfit " : " undefined outfit ";
649 for(auto &&outfit : undefinedOutfits)
650 message += PREFIX + outfit;
651 }
652 else
653 {
654 message = variantName.empty() ? "Stock ship \"" + modelName + "\": "
655 : modelName + " variant \"" + variantName + "\": ";
656 message += to_string(undefinedOutfits.size()) + " undefined outfit" + (plural ? "s" : "") + " installed.";
657 }
658
659 Files::LogError(message);
660 }
661 // Inspect the ship's armament to ensure that guns are in gun ports and
662 // turrets are in turret mounts. This can only happen when the armament
663 // is configured incorrectly in a ship or variant definition. Do not
664 // bother printing this warning if the outfit is not fully defined.
665 for(const Hardpoint &hardpoint : armament.Get())
666 {
667 const Outfit *outfit = hardpoint.GetOutfit();
668 if(outfit && outfit->IsDefined()
669 && (hardpoint.IsTurret() != (outfit->Get("turret mounts") != 0.)))
670 {
671 string warning = (!isYours && !variantName.empty()) ? "variant \"" + variantName + "\"" : modelName;
672 if(!name.empty())
673 warning += " \"" + name + "\"";
674 warning += ": outfit \"" + outfit->Name() + "\" installed as a ";
675 warning += (hardpoint.IsTurret() ? "turret but is a gun.\n\tturret" : "gun but is a turret.\n\tgun");
676 warning += to_string(2. * hardpoint.GetPoint().X()) + " " + to_string(2. * hardpoint.GetPoint().Y());
677 warning += " \"" + outfit->Name() + "\"";
678 Files::LogError(warning);
679 }
680 }
681 cargo.SetSize(attributes.Get("cargo space"));
682 equipped.clear();
683 armament.FinishLoading();
684
685 // Figure out how far from center the farthest hardpoint is.
686 weaponRadius = 0.;
687 for(const Hardpoint &hardpoint : armament.Get())
688 weaponRadius = max(weaponRadius, hardpoint.GetPoint().Length());
689
690 // If this ship is being instantiated for the first time, make sure its
691 // crew, fuel, etc. are all refilled.
692 if(isNewInstance)
693 Recharge(true);
694
695 // Add a default "launch effect" to any internal bays if this ship is crewed (i.e. pressurized).
696 for(Bay &bay : bays)
697 if(bay.side == Bay::INSIDE && bay.launchEffects.empty() && Crew())
698 bay.launchEffects.emplace_back(GameData::Effects().Get("basic launch"));
699
700 canBeCarried = BAY_TYPES.count(attributes.Category()) > 0;
701
702 // Issue warnings if this ship has is misconfigured, e.g. is missing required values
703 // or has negative outfit, cargo, weapon, or engine capacity.
704 string warning;
705 for(auto &&attr : set<string>{"outfit space", "cargo space", "weapon capacity", "engine capacity"})
706 {
707 double val = attributes.Get(attr);
708 if(val < 0)
709 warning += attr + ": " + Format::Number(val) + "\n";
710 }
711 if(attributes.Get("drag") <= 0.)
712 {
713 warning += "Defaulting " + string(attributes.Get("drag") ? "invalid" : "missing") + " \"drag\" attribute to 100.0\n";
714 attributes.Set("drag", 100.);
715 }
716 if(!warning.empty())
717 {
718 // This check is mostly useful for variants and stock ships, which have
719 // no names. Print the outfits to facilitate identifying this ship definition.
720 string message = (!name.empty() ? "Ship \"" + name + "\" " : "") + "(" + VariantName() + "):\n";
721 ostringstream outfitNames;
722 outfitNames << "has outfits:\n";
723 for(const auto &it : outfits)
724 outfitNames << '\t' << it.second << " " + it.first->Name() << endl;
725 Files::LogError(message + warning + outfitNames.str());
726 }
727
728 // Ships read from a save file may have non-default shields or hull.
729 // Perform a full IsDisabled calculation.
730 isDisabled = true;
731 isDisabled = IsDisabled();
732
733 // Cache this ship's jump range.
734 jumpRange = JumpRange(false);
735
736 // A saved ship may have an invalid target system. Since all game data is loaded and all player events are
737 // applied at this point, any target system that is not accessible should be cleared. Note: this does not
738 // account for systems accessible via wormholes, but also does not need to as AI will route the ship properly.
739 if(!isNewInstance && targetSystem)
740 {
741 string message = "Warning: " + string(isYours ? "player-owned " : "NPC ") + modelName + " \"" + name + "\": "
742 "Cannot reach target system \"" + targetSystem->Name();
743 if(!currentSystem)
744 {
745 Files::LogError(message + "\" (no current system).");
746 targetSystem = nullptr;
747 }
748 else if(!currentSystem->Links().count(targetSystem) && (!jumpRange || !currentSystem->JumpNeighbors(jumpRange).count(targetSystem)))
749 {
750 Files::LogError(message + "\" by hyperlink or jump from system \"" + currentSystem->Name() + ".\"");
751 targetSystem = nullptr;
752 }
753 }
754 }
755
756
757
758 // Check if this ship (model) and its outfits have been defined.
IsValid() const759 bool Ship::IsValid() const
760 {
761 for(auto &&outfit : outfits)
762 if(!outfit.first->IsDefined())
763 return false;
764
765 return isDefined;
766 }
767
768
769
770 // Save a full description of this ship, as currently configured.
Save(DataWriter & out) const771 void Ship::Save(DataWriter &out) const
772 {
773 out.Write("ship", modelName);
774 out.BeginChild();
775 {
776 out.Write("name", name);
777 if(pluralModelName != modelName + 's')
778 out.Write("plural", pluralModelName);
779 if(!noun.empty())
780 out.Write("noun", noun);
781 SaveSprite(out);
782 if(thumbnail)
783 out.Write("thumbnail", thumbnail->Name());
784
785 if(neverDisabled)
786 out.Write("never disabled");
787 if(!isCapturable)
788 out.Write("uncapturable");
789 if(customSwizzle >= 0)
790 out.Write("swizzle", customSwizzle);
791
792 out.Write("attributes");
793 out.BeginChild();
794 {
795 out.Write("category", baseAttributes.Category());
796 out.Write("cost", baseAttributes.Cost());
797 out.Write("mass", baseAttributes.Mass());
798 for(const auto &it : baseAttributes.FlareSprites())
799 for(int i = 0; i < it.second; ++i)
800 it.first.SaveSprite(out, "flare sprite");
801 for(const auto &it : baseAttributes.FlareSounds())
802 for(int i = 0; i < it.second; ++i)
803 out.Write("flare sound", it.first->Name());
804 for(const auto &it : baseAttributes.ReverseFlareSprites())
805 for(int i = 0; i < it.second; ++i)
806 it.first.SaveSprite(out, "reverse flare sprite");
807 for(const auto &it : baseAttributes.ReverseFlareSounds())
808 for(int i = 0; i < it.second; ++i)
809 out.Write("reverse flare sound", it.first->Name());
810 for(const auto &it : baseAttributes.SteeringFlareSprites())
811 for(int i = 0; i < it.second; ++i)
812 it.first.SaveSprite(out, "steering flare sprite");
813 for(const auto &it : baseAttributes.SteeringFlareSounds())
814 for(int i = 0; i < it.second; ++i)
815 out.Write("steering flare sound", it.first->Name());
816 for(const auto &it : baseAttributes.AfterburnerEffects())
817 for(int i = 0; i < it.second; ++i)
818 out.Write("afterburner effect", it.first->Name());
819 for(const auto &it : baseAttributes.JumpEffects())
820 for(int i = 0; i < it.second; ++i)
821 out.Write("jump effect", it.first->Name());
822 for(const auto &it : baseAttributes.JumpSounds())
823 for(int i = 0; i < it.second; ++i)
824 out.Write("jump sound", it.first->Name());
825 for(const auto &it : baseAttributes.JumpInSounds())
826 for(int i = 0; i < it.second; ++i)
827 out.Write("jump in sound", it.first->Name());
828 for(const auto &it : baseAttributes.JumpOutSounds())
829 for(int i = 0; i < it.second; ++i)
830 out.Write("jump out sound", it.first->Name());
831 for(const auto &it : baseAttributes.HyperSounds())
832 for(int i = 0; i < it.second; ++i)
833 out.Write("hyperdrive sound", it.first->Name());
834 for(const auto &it : baseAttributes.HyperInSounds())
835 for(int i = 0; i < it.second; ++i)
836 out.Write("hyperdrive in sound", it.first->Name());
837 for(const auto &it : baseAttributes.HyperOutSounds())
838 for(int i = 0; i < it.second; ++i)
839 out.Write("hyperdrive out sound", it.first->Name());
840 for(const auto &it : baseAttributes.Attributes())
841 if(it.second)
842 out.Write(it.first, it.second);
843 }
844 out.EndChild();
845
846 out.Write("outfits");
847 out.BeginChild();
848 {
849 using OutfitElement = pair<const Outfit *const, int>;
850 WriteSorted(outfits,
851 [](const OutfitElement *lhs, const OutfitElement *rhs)
852 { return lhs->first->Name() < rhs->first->Name(); },
853 [&out](const OutfitElement &it){
854 if(it.second == 1)
855 out.Write(it.first->Name());
856 else
857 out.Write(it.first->Name(), it.second);
858 });
859 }
860 out.EndChild();
861
862 cargo.Save(out);
863 out.Write("crew", crew);
864 out.Write("fuel", fuel);
865 out.Write("shields", shields);
866 out.Write("hull", hull);
867 out.Write("position", position.X(), position.Y());
868
869 for(const EnginePoint &point : enginePoints)
870 {
871 out.Write("engine", 2. * point.X(), 2. * point.Y());
872 out.BeginChild();
873 out.Write("zoom", point.zoom);
874 out.Write("angle", point.facing.Degrees());
875 out.Write(ENGINE_SIDE[point.side]);
876 out.EndChild();
877
878 }
879 for(const EnginePoint &point : reverseEnginePoints)
880 {
881 out.Write("reverse engine", 2. * point.X(), 2. * point.Y());
882 out.BeginChild();
883 out.Write("zoom", point.zoom);
884 out.Write("angle", point.facing.Degrees() - 180.);
885 out.Write(ENGINE_SIDE[point.side]);
886 out.EndChild();
887 }
888 for(const EnginePoint &point : steeringEnginePoints)
889 {
890 out.Write("steering engine", 2. * point.X(), 2. * point.Y());
891 out.BeginChild();
892 out.Write("zoom", point.zoom);
893 out.Write("angle", point.facing.Degrees());
894 out.Write(ENGINE_SIDE[point.side]);
895 out.Write(STEERING_FACING[point.steering]);
896 out.EndChild();
897 }
898 for(const Hardpoint &hardpoint : armament.Get())
899 {
900 const char *type = (hardpoint.IsTurret() ? "turret" : "gun");
901 if(hardpoint.GetOutfit())
902 out.Write(type, 2. * hardpoint.GetPoint().X(), 2. * hardpoint.GetPoint().Y(),
903 hardpoint.GetOutfit()->Name());
904 else
905 out.Write(type, 2. * hardpoint.GetPoint().X(), 2. * hardpoint.GetPoint().Y());
906 double hardpointAngle = hardpoint.GetBaseAngle().Degrees();
907 if(hardpoint.IsParallel() || hardpointAngle)
908 {
909 out.BeginChild();
910 {
911 if(hardpointAngle)
912 out.Write("angle", hardpointAngle);
913 if(hardpoint.IsParallel())
914 out.Write("parallel");
915 }
916 out.EndChild();
917 }
918 }
919 for(const Bay &bay : bays)
920 {
921 double x = 2. * bay.point.X();
922 double y = 2. * bay.point.Y();
923
924 out.Write("bay", bay.category, x, y);
925
926 if(!bay.launchEffects.empty() || bay.facing.Degrees() || bay.side)
927 {
928 out.BeginChild();
929 {
930 if(bay.facing.Degrees())
931 out.Write("angle", bay.facing.Degrees());
932 if(bay.side)
933 out.Write(BAY_SIDE[bay.side]);
934 for(const Effect *effect : bay.launchEffects)
935 out.Write("launch effect", effect->Name());
936 }
937 out.EndChild();
938 }
939 }
940 for(const Leak &leak : leaks)
941 out.Write("leak", leak.effect->Name(), leak.openPeriod, leak.closePeriod);
942
943 using EffectElement = pair<const Effect *const, int>;
944 auto effectSort = [](const EffectElement *lhs, const EffectElement *rhs)
945 { return lhs->first->Name() < rhs->first->Name(); };
946 WriteSorted(explosionEffects, effectSort, [&out](const EffectElement &it)
947 {
948 if(it.second)
949 out.Write("explode", it.first->Name(), it.second);
950 });
951 WriteSorted(finalExplosions, effectSort, [&out](const EffectElement &it)
952 {
953 if(it.second)
954 out.Write("final explode", it.first->Name(), it.second);
955 });
956
957 if(currentSystem)
958 out.Write("system", currentSystem->Name());
959 else
960 {
961 // A carried ship is saved in its carrier's system.
962 shared_ptr<const Ship> parent = GetParent();
963 if(parent && parent->currentSystem)
964 out.Write("system", parent->currentSystem->Name());
965 }
966 if(landingPlanet)
967 out.Write("planet", landingPlanet->TrueName());
968 if(targetSystem)
969 out.Write("destination system", targetSystem->Name());
970 if(isParked)
971 out.Write("parked");
972 }
973 out.EndChild();
974 }
975
976
977
Name() const978 const string &Ship::Name() const
979 {
980 return name;
981 }
982
983
984
985 // Set / Get the name of this class of ships, e.g. "Marauder Raven."
SetModelName(const string & model)986 void Ship::SetModelName(const string &model)
987 {
988 this->modelName = model;
989 }
990
991
992
ModelName() const993 const string &Ship::ModelName() const
994 {
995 return modelName;
996 }
997
998
999
PluralModelName() const1000 const string &Ship::PluralModelName() const
1001 {
1002 return pluralModelName;
1003 }
1004
1005
1006
1007 // Get the name of this ship as a variant.
VariantName() const1008 const string &Ship::VariantName() const
1009 {
1010 return variantName.empty() ? modelName : variantName;
1011 }
1012
1013
1014
1015 // Get the generic noun (e.g. "ship") to be used when describing this ship.
Noun() const1016 const string &Ship::Noun() const
1017 {
1018 static const string SHIP = "ship";
1019 return noun.empty() ? SHIP : noun;
1020 }
1021
1022
1023
1024 // Get this ship's description.
Description() const1025 const string &Ship::Description() const
1026 {
1027 return description;
1028 }
1029
1030
1031
1032 // Get the shipyard thumbnail for this ship.
Thumbnail() const1033 const Sprite *Ship::Thumbnail() const
1034 {
1035 return thumbnail;
1036 }
1037
1038
1039
1040 // Get this ship's cost.
Cost() const1041 int64_t Ship::Cost() const
1042 {
1043 return attributes.Cost();
1044 }
1045
1046
1047
1048 // Get the cost of this ship's chassis, with no outfits installed.
ChassisCost() const1049 int64_t Ship::ChassisCost() const
1050 {
1051 return baseAttributes.Cost();
1052 }
1053
1054
1055
1056 // Check if this ship is configured in such a way that it would be difficult
1057 // or impossible to fly.
FlightCheck() const1058 vector<string> Ship::FlightCheck() const
1059 {
1060 auto checks = vector<string>{};
1061
1062 double generation = attributes.Get("energy generation") - attributes.Get("energy consumption");
1063 double burning = attributes.Get("fuel energy");
1064 double solar = attributes.Get("solar collection");
1065 double battery = attributes.Get("energy capacity");
1066 double energy = generation + burning + solar + battery;
1067 double fuelChange = attributes.Get("fuel generation") - attributes.Get("fuel consumption");
1068 double fuelCapacity = attributes.Get("fuel capacity");
1069 double fuel = fuelCapacity + fuelChange;
1070 double thrust = attributes.Get("thrust");
1071 double reverseThrust = attributes.Get("reverse thrust");
1072 double afterburner = attributes.Get("afterburner thrust");
1073 double thrustEnergy = attributes.Get("thrusting energy");
1074 double turn = attributes.Get("turn");
1075 double turnEnergy = attributes.Get("turning energy");
1076 double hyperDrive = attributes.Get("hyperdrive");
1077 double jumpDrive = attributes.Get("jump drive");
1078
1079 // Report the first error condition that will prevent takeoff:
1080 if(IdleHeat() >= MaximumHeat())
1081 checks.emplace_back("overheating!");
1082 else if(energy <= 0.)
1083 checks.emplace_back("no energy!");
1084 else if((energy - burning <= 0.) && (fuel <= 0.))
1085 checks.emplace_back("no fuel!");
1086 else if(!thrust && !reverseThrust && !afterburner)
1087 checks.emplace_back("no thruster!");
1088 else if(!turn)
1089 checks.emplace_back("no steering!");
1090
1091 // If no errors were found, check all warning conditions:
1092 if(checks.empty())
1093 {
1094 if(!thrust && !reverseThrust)
1095 checks.emplace_back("afterburner only?");
1096 if(!thrust && !afterburner)
1097 checks.emplace_back("reverse only?");
1098 if(!generation && !solar && !burning)
1099 checks.emplace_back("battery only?");
1100 if(energy < thrustEnergy)
1101 checks.emplace_back("limited thrust?");
1102 if(energy < turnEnergy)
1103 checks.emplace_back("limited turn?");
1104 if(energy - .8 * solar < .2 * (turnEnergy + thrustEnergy))
1105 checks.emplace_back("solar power?");
1106 if(fuel < 0.)
1107 checks.emplace_back("fuel?");
1108 if(!canBeCarried)
1109 {
1110 if(!hyperDrive && !jumpDrive)
1111 checks.emplace_back("no hyperdrive?");
1112 if(fuelCapacity < JumpFuel())
1113 checks.emplace_back("no fuel?");
1114 }
1115 for(const auto &it : outfits)
1116 if(it.first->IsWeapon() && it.first->FiringEnergy() > energy)
1117 {
1118 checks.emplace_back("insufficient energy to fire?");
1119 break;
1120 }
1121 }
1122
1123 return checks;
1124 }
1125
1126
1127
SetPosition(Point position)1128 void Ship::SetPosition(Point position)
1129 {
1130 this->position = position;
1131 }
1132
1133
1134
1135 // Instantiate a newly-created ship in-flight.
Place(Point position,Point velocity,Angle angle)1136 void Ship::Place(Point position, Point velocity, Angle angle)
1137 {
1138 this->position = position;
1139 this->velocity = velocity;
1140 this->angle = angle;
1141
1142 // If landed, place the ship right above the planet.
1143 // Escorts should take off a bit behind their flagships.
1144 if(landingPlanet)
1145 {
1146 landingPlanet = nullptr;
1147 zoom = parent.lock() ? (-.2 + -.8 * Random::Real()) : 0.;
1148 }
1149 else
1150 zoom = 1.;
1151 // Make sure various special status values are reset.
1152 heat = IdleHeat();
1153 ionization = 0.;
1154 disruption = 0.;
1155 slowness = 0.;
1156 shieldDelay = 0;
1157 hullDelay = 0;
1158 isInvisible = !HasSprite();
1159 jettisoned.clear();
1160 hyperspaceCount = 0;
1161 forget = 1;
1162 targetShip.reset();
1163 shipToAssist.reset();
1164 if(government)
1165 SetSwizzle(customSwizzle >= 0 ? customSwizzle : government->GetSwizzle());
1166 }
1167
1168
1169
1170 // Set the name of this particular ship.
SetName(const string & name)1171 void Ship::SetName(const string &name)
1172 {
1173 this->name = name;
1174 }
1175
1176
1177
1178 // Set which system this ship is in.
SetSystem(const System * system)1179 void Ship::SetSystem(const System *system)
1180 {
1181 currentSystem = system;
1182 }
1183
1184
1185
SetPlanet(const Planet * planet)1186 void Ship::SetPlanet(const Planet *planet)
1187 {
1188 zoom = !planet;
1189 landingPlanet = planet;
1190 }
1191
1192
1193
SetGovernment(const Government * government)1194 void Ship::SetGovernment(const Government *government)
1195 {
1196 if(government)
1197 SetSwizzle(customSwizzle >= 0 ? customSwizzle : government->GetSwizzle());
1198 this->government = government;
1199 }
1200
1201
1202
SetIsSpecial(bool special)1203 void Ship::SetIsSpecial(bool special)
1204 {
1205 isSpecial = special;
1206 }
1207
1208
1209
IsSpecial() const1210 bool Ship::IsSpecial() const
1211 {
1212 return isSpecial;
1213 }
1214
1215
1216
SetIsYours(bool yours)1217 void Ship::SetIsYours(bool yours)
1218 {
1219 isYours = yours;
1220 }
1221
1222
1223
IsYours() const1224 bool Ship::IsYours() const
1225 {
1226 return isYours;
1227 }
1228
1229
1230
SetIsParked(bool parked)1231 void Ship::SetIsParked(bool parked)
1232 {
1233 isParked = parked;
1234 }
1235
1236
1237
IsParked() const1238 bool Ship::IsParked() const
1239 {
1240 return isParked;
1241 }
1242
1243
1244
HasDeployOrder() const1245 bool Ship::HasDeployOrder() const
1246 {
1247 return shouldDeploy;
1248 }
1249
1250
1251
SetDeployOrder(bool shouldDeploy)1252 void Ship::SetDeployOrder(bool shouldDeploy)
1253 {
1254 this->shouldDeploy = shouldDeploy;
1255 }
1256
1257
1258
GetPersonality() const1259 const Personality &Ship::GetPersonality() const
1260 {
1261 return personality;
1262 }
1263
1264
1265
SetPersonality(const Personality & other)1266 void Ship::SetPersonality(const Personality &other)
1267 {
1268 personality = other;
1269 }
1270
1271
1272
SetHail(const Phrase & phrase)1273 void Ship::SetHail(const Phrase &phrase)
1274 {
1275 hail = &phrase;
1276 }
1277
1278
1279
GetHail(const PlayerInfo & player) const1280 string Ship::GetHail(const PlayerInfo &player) const
1281 {
1282 map<string, string> subs;
1283
1284 subs["<first>"] = player.FirstName();
1285 subs["<last>"] = player.LastName();
1286 if(player.Flagship())
1287 subs["<ship>"] = player.Flagship()->Name();
1288
1289 subs["<npc>"] = Name();
1290 subs["<system>"] = player.GetSystem()->Name();
1291 subs["<date>"] = player.GetDate().ToString();
1292 subs["<day>"] = player.GetDate().LongString();
1293
1294 string hailStr = hail ? hail->Get() : government ? government->GetHail(isDisabled) : "";
1295 return Format::Replace(hailStr, subs);
1296 }
1297
1298
1299
1300 // Set the commands for this ship to follow this timestep.
SetCommands(const Command & command)1301 void Ship::SetCommands(const Command &command)
1302 {
1303 commands = command;
1304 }
1305
1306
1307
Commands() const1308 const Command &Ship::Commands() const
1309 {
1310 return commands;
1311 }
1312
1313
1314
1315 // Move this ship. A ship may create effects as it moves, in particular if
1316 // it is in the process of blowing up. If this returns false, the ship
1317 // should be deleted.
Move(vector<Visual> & visuals,list<shared_ptr<Flotsam>> & flotsam)1318 void Ship::Move(vector<Visual> &visuals, list<shared_ptr<Flotsam>> &flotsam)
1319 {
1320 // Check if this ship has been in a different system from the player for so
1321 // long that it should be "forgotten." Also eliminate ships that have no
1322 // system set because they just entered a fighter bay.
1323 forget += !isInSystem;
1324 isThrusting = false;
1325 isReversing = false;
1326 isSteering = false;
1327 steeringDirection = 0.;
1328 if((!isSpecial && forget >= 1000) || !currentSystem)
1329 {
1330 MarkForRemoval();
1331 return;
1332 }
1333 isInSystem = false;
1334 if(!fuel || !(attributes.Get("hyperdrive") || attributes.Get("jump drive")))
1335 hyperspaceSystem = nullptr;
1336
1337 // Adjust the error in the pilot's targeting.
1338 personality.UpdateConfusion(commands.IsFiring());
1339
1340 // Generate energy, heat, etc.
1341 DoGeneration();
1342
1343 // Handle ionization effects, etc.
1344 if(ionization)
1345 CreateSparks(visuals, "ion spark", ionization * .1);
1346 if(disruption)
1347 CreateSparks(visuals, "disruption spark", disruption * .1);
1348 if(slowness)
1349 CreateSparks(visuals, "slowing spark", slowness * .1);
1350 // Jettisoned cargo effects (only for ships in the current system).
1351 if(!jettisoned.empty() && !forget)
1352 {
1353 jettisoned.front()->Place(*this);
1354 flotsam.splice(flotsam.end(), jettisoned, jettisoned.begin());
1355 }
1356 int requiredCrew = RequiredCrew();
1357 double slowMultiplier = 1. / (1. + slowness * .05);
1358
1359 // Move the turrets.
1360 if(!isDisabled)
1361 armament.Aim(commands);
1362
1363 if(!isInvisible)
1364 {
1365 // If you are forced to decloak (e.g. by running out of fuel) you can't
1366 // initiate cloaking again until you are fully decloaked.
1367 if(!cloak)
1368 cloakDisruption = max(0., cloakDisruption - 1.);
1369
1370 double cloakingSpeed = attributes.Get("cloak");
1371 bool canCloak = (!isDisabled && cloakingSpeed > 0. && !cloakDisruption
1372 && fuel >= attributes.Get("cloaking fuel")
1373 && energy >= attributes.Get("cloaking energy"));
1374 if(commands.Has(Command::CLOAK) && canCloak)
1375 {
1376 cloak = min(1., cloak + cloakingSpeed);
1377 fuel -= attributes.Get("cloaking fuel");
1378 energy -= attributes.Get("cloaking energy");
1379 heat += attributes.Get("cloaking heat");
1380 }
1381 else if(cloakingSpeed)
1382 {
1383 cloak = max(0., cloak - cloakingSpeed);
1384 // If you're trying to cloak but are unable to (too little energy or
1385 // fuel) you're forced to decloak fully for one frame before you can
1386 // engage cloaking again.
1387 if(commands.Has(Command::CLOAK))
1388 cloakDisruption = max(cloakDisruption, 1.);
1389 }
1390 else
1391 cloak = 0.;
1392 }
1393
1394 if(IsDestroyed())
1395 {
1396 // Make sure the shields are zero, as well as the hull.
1397 shields = 0.;
1398
1399 // Once we've created enough little explosions, die.
1400 if(explosionCount == explosionTotal || forget)
1401 {
1402 if(!forget)
1403 {
1404 const Effect *effect = GameData::Effects().Get("smoke");
1405 double size = Width() + Height();
1406 double scale = .03 * size + .5;
1407 double radius = .2 * size;
1408 int debrisCount = attributes.Mass() * .07;
1409
1410 // Estimate how many new visuals will be added during destruction.
1411 visuals.reserve(visuals.size() + debrisCount + explosionTotal + finalExplosions.size());
1412
1413 for(int i = 0; i < debrisCount; ++i)
1414 {
1415 Angle angle = Angle::Random();
1416 Point effectVelocity = velocity + angle.Unit() * (scale * Random::Real());
1417 Point effectPosition = position + radius * angle.Unit();
1418
1419 visuals.emplace_back(*effect, std::move(effectPosition), std::move(effectVelocity), std::move(angle));
1420 }
1421
1422 for(unsigned i = 0; i < explosionTotal / 2; ++i)
1423 CreateExplosion(visuals, true);
1424 for(const auto &it : finalExplosions)
1425 visuals.emplace_back(*it.first, position, velocity, angle);
1426 // For everything in this ship's cargo hold there is a 25% chance
1427 // that it will survive as flotsam.
1428 for(const auto &it : cargo.Commodities())
1429 Jettison(it.first, Random::Binomial(it.second, .25));
1430 for(const auto &it : cargo.Outfits())
1431 Jettison(it.first, Random::Binomial(it.second, .25));
1432 // Ammunition has a 5% chance to survive as flotsam
1433 for(const auto &it : outfits)
1434 if(it.first->Category() == "Ammunition")
1435 Jettison(it.first, Random::Binomial(it.second, .05));
1436 for(shared_ptr<Flotsam> &it : jettisoned)
1437 it->Place(*this);
1438 flotsam.splice(flotsam.end(), jettisoned);
1439
1440 // Any ships that failed to launch from this ship are destroyed.
1441 for(Bay &bay : bays)
1442 if(bay.ship)
1443 bay.ship->Destroy();
1444 }
1445 energy = 0.;
1446 heat = 0.;
1447 ionization = 0.;
1448 fuel = 0.;
1449 velocity = Point();
1450 MarkForRemoval();
1451 return;
1452 }
1453
1454 // If the ship is dead, it first creates explosions at an increasing
1455 // rate, then disappears in one big explosion.
1456 ++explosionRate;
1457 if(Random::Int(1024) < explosionRate)
1458 CreateExplosion(visuals);
1459
1460 // Handle hull "leaks."
1461 for(const Leak &leak : leaks)
1462 if(leak.openPeriod > 0 && !Random::Int(leak.openPeriod))
1463 {
1464 activeLeaks.push_back(leak);
1465 const vector<Point> &outline = GetMask().Points();
1466 if(outline.size() < 2)
1467 break;
1468 int i = Random::Int(outline.size() - 1);
1469
1470 // Position the leak along the outline of the ship, facing outward.
1471 activeLeaks.back().location = (outline[i] + outline[i + 1]) * .5;
1472 activeLeaks.back().angle = Angle(outline[i] - outline[i + 1]) + Angle(90.);
1473 }
1474 for(Leak &leak : activeLeaks)
1475 if(leak.effect)
1476 {
1477 // Leaks always "flicker" every other frame.
1478 if(Random::Int(2))
1479 visuals.emplace_back(*leak.effect,
1480 angle.Rotate(leak.location) + position,
1481 velocity,
1482 leak.angle + angle);
1483
1484 if(leak.closePeriod > 0 && !Random::Int(leak.closePeriod))
1485 leak.effect = nullptr;
1486 }
1487 }
1488 else if(hyperspaceSystem || hyperspaceCount)
1489 {
1490 // Don't apply external acceleration while jumping.
1491 acceleration = Point();
1492
1493 // Enter hyperspace.
1494 int direction = hyperspaceSystem ? 1 : -1;
1495 hyperspaceCount += direction;
1496 static const int HYPER_C = 100;
1497 static const double HYPER_A = 2.;
1498 static const double HYPER_D = 1000.;
1499 if(hyperspaceSystem)
1500 fuel -= hyperspaceFuelCost / HYPER_C;
1501
1502 // Create the particle effects for the jump drive. This may create 100
1503 // or more particles per ship per turn at the peak of the jump.
1504 if(isUsingJumpDrive && !forget)
1505 {
1506 double sparkAmount = hyperspaceCount * Width() * Height() * .000006;
1507 const map<const Effect *, int> &jumpEffects = attributes.JumpEffects();
1508 if(jumpEffects.empty())
1509 CreateSparks(visuals, "jump drive", sparkAmount);
1510 else
1511 {
1512 // Spread the amount of particle effects created among all jump effects.
1513 sparkAmount /= jumpEffects.size();
1514 for(const auto &effect : jumpEffects)
1515 CreateSparks(visuals, effect.first, sparkAmount);
1516 }
1517 }
1518
1519 if(hyperspaceCount == HYPER_C)
1520 {
1521 currentSystem = hyperspaceSystem;
1522 hyperspaceSystem = nullptr;
1523 targetSystem = nullptr;
1524 // Check if the target planet is in the destination system or not.
1525 const Planet *planet = (targetPlanet ? targetPlanet->GetPlanet() : nullptr);
1526 if(!planet || planet->IsWormhole() || !planet->IsInSystem(currentSystem))
1527 targetPlanet = nullptr;
1528 // Check if your parent has a target planet in this system.
1529 shared_ptr<Ship> parent = GetParent();
1530 if(!targetPlanet && parent && parent->targetPlanet)
1531 {
1532 planet = parent->targetPlanet->GetPlanet();
1533 if(planet && !planet->IsWormhole() && planet->IsInSystem(currentSystem))
1534 targetPlanet = parent->targetPlanet;
1535 }
1536 direction = -1;
1537
1538 // If you have a target planet in the destination system, exit
1539 // hyperpace aimed at it. Otherwise, target the first planet that
1540 // has a spaceport.
1541 Point target;
1542 // Except when you arrive at an extra distance from the target,
1543 // in that case always use the system-center as target.
1544 double extraArrivalDistance = isUsingJumpDrive ? currentSystem->ExtraJumpArrivalDistance() : currentSystem->ExtraHyperArrivalDistance();
1545
1546 if(extraArrivalDistance == 0)
1547 {
1548 if(targetPlanet)
1549 target = targetPlanet->Position();
1550 else
1551 {
1552 for(const StellarObject &object : currentSystem->Objects())
1553 if(object.HasSprite() && object.HasValidPlanet()
1554 && object.GetPlanet()->HasSpaceport())
1555 {
1556 target = object.Position();
1557 break;
1558 }
1559 }
1560 }
1561
1562 if(isUsingJumpDrive)
1563 {
1564 position = target + Angle::Random().Unit() * (300. * (Random::Real() + 1.) + extraArrivalDistance);
1565 return;
1566 }
1567
1568 // Have all ships exit hyperspace at the same distance so that
1569 // your escorts always stay with you.
1570 double distance = (HYPER_C * HYPER_C) * .5 * HYPER_A + HYPER_D;
1571 distance += extraArrivalDistance;
1572 position = (target - distance * angle.Unit());
1573 position += hyperspaceOffset;
1574 // Make sure your velocity is in exactly the direction you are
1575 // traveling in, so that when you decelerate there will not be a
1576 // sudden shift in direction at the end.
1577 velocity = velocity.Length() * angle.Unit();
1578 }
1579 if(!isUsingJumpDrive)
1580 {
1581 velocity += (HYPER_A * direction) * angle.Unit();
1582 if(!hyperspaceSystem)
1583 {
1584 // Exit hyperspace far enough from the planet to be able to land.
1585 // This does not take drag into account, so it is always an over-
1586 // estimate of how long it will take to stop.
1587 // We start decelerating after rotating about 150 degrees (that
1588 // is, about acos(.8) from the proper angle). So:
1589 // Stopping distance = .5*a*(v/a)^2 + (150/turn)*v.
1590 // Exit distance = HYPER_D + .25 * v^2 = stopping distance.
1591 double exitV = max(HYPER_A, MaxVelocity());
1592 double a = (.5 / Acceleration() - .25);
1593 double b = 150. / TurnRate();
1594 double discriminant = b * b - 4. * a * -HYPER_D;
1595 if(discriminant > 0.)
1596 {
1597 double altV = (-b + sqrt(discriminant)) / (2. * a);
1598 if(altV > 0. && altV < exitV)
1599 exitV = altV;
1600 }
1601 if(velocity.Length() <= exitV)
1602 {
1603 velocity = angle.Unit() * exitV;
1604 hyperspaceCount = 0;
1605 }
1606 }
1607 }
1608 position += velocity;
1609 if(GetParent() && GetParent()->currentSystem == currentSystem)
1610 {
1611 hyperspaceOffset = position - GetParent()->position;
1612 double length = hyperspaceOffset.Length();
1613 if(length > 1000.)
1614 hyperspaceOffset *= 1000. / length;
1615 }
1616
1617 return;
1618 }
1619 else if(landingPlanet || zoom < 1.f)
1620 {
1621 // Don't apply external acceleration while landing.
1622 acceleration = Point();
1623
1624 // If a ship was disabled at the very moment it began landing, do not
1625 // allow it to continue landing.
1626 if(isDisabled)
1627 landingPlanet = nullptr;
1628
1629 // Special ships do not disappear forever when they land; they
1630 // just slowly refuel.
1631 if(landingPlanet && zoom)
1632 {
1633 // Move the ship toward the center of the planet while landing.
1634 if(GetTargetStellar())
1635 position = .97 * position + .03 * GetTargetStellar()->Position();
1636 zoom -= .02f;
1637 if(zoom < 0.f)
1638 {
1639 // If this is not a special ship, it ceases to exist when it
1640 // lands on a true planet. If this is a wormhole, the ship is
1641 // instantly transported.
1642 if(landingPlanet->IsWormhole())
1643 {
1644 currentSystem = landingPlanet->WormholeDestination(currentSystem);
1645 for(const StellarObject &object : currentSystem->Objects())
1646 if(object.GetPlanet() == landingPlanet)
1647 position = object.Position();
1648 SetTargetStellar(nullptr);
1649 landingPlanet = nullptr;
1650 }
1651 else if(!isSpecial || personality.IsFleeing())
1652 {
1653 MarkForRemoval();
1654 return;
1655 }
1656
1657 zoom = 0.f;
1658 }
1659 }
1660 // Only refuel if this planet has a spaceport.
1661 else if(fuel >= attributes.Get("fuel capacity")
1662 || !landingPlanet || !landingPlanet->HasSpaceport())
1663 {
1664 zoom = min(1.f, zoom + .02f);
1665 SetTargetStellar(nullptr);
1666 landingPlanet = nullptr;
1667 }
1668 else
1669 fuel = min(fuel + 1., attributes.Get("fuel capacity"));
1670
1671 // Move the ship at the velocity it had when it began landing, but
1672 // scaled based on how small it is now.
1673 if(zoom > 0.f)
1674 position += velocity * zoom;
1675
1676 return;
1677 }
1678 if(isDisabled)
1679 {
1680 // If you're disabled, you can't initiate landing or jumping.
1681 }
1682 else if(commands.Has(Command::LAND) && CanLand())
1683 landingPlanet = GetTargetStellar()->GetPlanet();
1684 else if(commands.Has(Command::JUMP) && IsReadyToJump())
1685 {
1686 hyperspaceSystem = GetTargetSystem();
1687 isUsingJumpDrive = !attributes.Get("hyperdrive") || !currentSystem->Links().count(hyperspaceSystem);
1688 hyperspaceFuelCost = JumpFuel(hyperspaceSystem);
1689 }
1690
1691 if(pilotError)
1692 --pilotError;
1693 else if(pilotOkay)
1694 --pilotOkay;
1695 else if(isDisabled)
1696 {
1697 // If the ship is disabled, don't show a warning message due to missing crew.
1698 }
1699 else if(requiredCrew && static_cast<int>(Random::Int(requiredCrew)) >= Crew())
1700 {
1701 pilotError = 30;
1702 if(parent.lock() || !isYours)
1703 Messages::Add("The " + name + " is moving erratically because there are not enough crew to pilot it.", false);
1704 else
1705 Messages::Add("Your ship is moving erratically because you do not have enough crew to pilot it.", false);
1706 }
1707 else
1708 pilotOkay = 30;
1709
1710 // This ship is not landing or entering hyperspace. So, move it. If it is
1711 // disabled, all it can do is slow down to a stop.
1712 double mass = Mass();
1713 bool isUsingAfterburner = false;
1714 if(isDisabled)
1715 velocity *= 1. - attributes.Get("drag") / mass;
1716 else if(!pilotError)
1717 {
1718 if(commands.Turn())
1719 {
1720 // Check if we are able to turn.
1721 double cost = attributes.Get("turning energy");
1722 if(energy < cost * fabs(commands.Turn()))
1723 commands.SetTurn(commands.Turn() * energy / (cost * fabs(commands.Turn())));
1724
1725 if(commands.Turn())
1726 {
1727 isSteering = true;
1728 steeringDirection = commands.Turn();
1729 // If turning at a fraction of the full rate (either from lack of
1730 // energy or because of tracking a target), only consume a fraction
1731 // of the turning energy and produce a fraction of the heat.
1732 double scale = fabs(commands.Turn());
1733 energy -= scale * cost;
1734 heat += scale * attributes.Get("turning heat");
1735 angle += commands.Turn() * TurnRate() * slowMultiplier;
1736 }
1737 }
1738 double thrustCommand = commands.Has(Command::FORWARD) - commands.Has(Command::BACK);
1739 double thrust = 0.;
1740 if(thrustCommand)
1741 {
1742 // Check if we are able to apply this thrust.
1743 double cost = attributes.Get((thrustCommand > 0.) ?
1744 "thrusting energy" : "reverse thrusting energy");
1745 if(energy < cost)
1746 thrustCommand *= energy / cost;
1747
1748 if(thrustCommand)
1749 {
1750 // If a reverse thrust is commanded and the capability does not
1751 // exist, ignore it (do not even slow under drag).
1752 isThrusting = (thrustCommand > 0.);
1753 isReversing = !isThrusting && attributes.Get("reverse thrust");
1754 thrust = attributes.Get(isThrusting ? "thrust" : "reverse thrust");
1755 if(thrust)
1756 {
1757 double scale = fabs(thrustCommand);
1758 energy -= scale * cost;
1759 heat += scale * attributes.Get(isThrusting ? "thrusting heat" : "reverse thrusting heat");
1760 acceleration += angle.Unit() * (thrustCommand * thrust / mass);
1761 }
1762 }
1763 }
1764 bool applyAfterburner = (commands.Has(Command::AFTERBURNER) || (thrustCommand > 0. && !thrust))
1765 && !CannotAct();
1766 if(applyAfterburner)
1767 {
1768 thrust = attributes.Get("afterburner thrust");
1769 double fuelCost = attributes.Get("afterburner fuel");
1770 double energyCost = attributes.Get("afterburner energy");
1771 if(thrust && fuel >= fuelCost && energy >= energyCost)
1772 {
1773 heat += attributes.Get("afterburner heat");
1774 fuel -= fuelCost;
1775 energy -= energyCost;
1776 acceleration += angle.Unit() * thrust / mass;
1777
1778 // Only create the afterburner effects if the ship is in the player's system.
1779 isUsingAfterburner = !forget;
1780 }
1781 }
1782 }
1783 if(acceleration)
1784 {
1785 acceleration *= slowMultiplier;
1786 Point dragAcceleration = acceleration - velocity * (attributes.Get("drag") / mass);
1787 // Make sure dragAcceleration has nonzero length, to avoid divide by zero.
1788 if(dragAcceleration)
1789 {
1790 // What direction will the net acceleration be if this drag is applied?
1791 // If the net acceleration will be opposite the thrust, do not apply drag.
1792 dragAcceleration *= .5 * (acceleration.Unit().Dot(dragAcceleration.Unit()) + 1.);
1793
1794 // A ship can only "cheat" to stop if it is moving slow enough that
1795 // it could stop completely this frame. This is to avoid overshooting
1796 // when trying to stop and ending up headed in the other direction.
1797 if(commands.Has(Command::STOP))
1798 {
1799 // How much acceleration would it take to come to a stop in the
1800 // direction normal to the ship's current facing? This is only
1801 // possible if the acceleration plus drag vector is in the
1802 // opposite direction from the velocity vector when both are
1803 // projected onto the current facing vector, and the acceleration
1804 // vector is the larger of the two.
1805 double vNormal = velocity.Dot(angle.Unit());
1806 double aNormal = dragAcceleration.Dot(angle.Unit());
1807 if((aNormal > 0.) != (vNormal > 0.) && fabs(aNormal) > fabs(vNormal))
1808 dragAcceleration = -vNormal * angle.Unit();
1809 }
1810 velocity += dragAcceleration;
1811 }
1812 acceleration = Point();
1813 }
1814
1815 // Boarding:
1816 shared_ptr<const Ship> target = GetTargetShip();
1817 // If this is a fighter or drone and it is not assisting someone at the
1818 // moment, its boarding target should be its parent ship.
1819 if(CanBeCarried() && !(target && target == GetShipToAssist()))
1820 target = GetParent();
1821 if(target && !isDisabled)
1822 {
1823 Point dp = (target->position - position);
1824 double distance = dp.Length();
1825 Point dv = (target->velocity - velocity);
1826 double speed = dv.Length();
1827 isBoarding = (distance < 50. && speed < 1. && commands.Has(Command::BOARD));
1828 if(isBoarding && !CanBeCarried())
1829 {
1830 if(!target->IsDisabled() && government->IsEnemy(target->government))
1831 isBoarding = false;
1832 else if(target->IsDestroyed() || target->IsLanding() || target->IsHyperspacing()
1833 || target->GetSystem() != GetSystem())
1834 isBoarding = false;
1835 }
1836 if(isBoarding && !pilotError)
1837 {
1838 Angle facing = angle;
1839 bool left = target->Unit().Cross(facing.Unit()) < 0.;
1840 double turn = left - !left;
1841
1842 // Check if the ship will still be pointing to the same side of the target
1843 // angle if it turns by this amount.
1844 facing += TurnRate() * turn;
1845 bool stillLeft = target->Unit().Cross(facing.Unit()) < 0.;
1846 if(left != stillLeft)
1847 turn = 0.;
1848 angle += TurnRate() * turn;
1849
1850 velocity += dv.Unit() * .1;
1851 position += dp.Unit() * .5;
1852
1853 if(distance < 10. && speed < 1. && (CanBeCarried() || !turn))
1854 {
1855 if(cloak)
1856 {
1857 // Allow the player to get all the way to the end of the
1858 // boarding sequence (including locking on to the ship) but
1859 // not to actually board, if they are cloaked.
1860 if(isYours)
1861 Messages::Add("You cannot board a ship while cloaked.");
1862 }
1863 else
1864 {
1865 isBoarding = false;
1866 bool isEnemy = government->IsEnemy(target->government);
1867 if(isEnemy && Random::Real() < target->Attributes().Get("self destruct"))
1868 {
1869 Messages::Add("The " + target->ModelName() + " \"" + target->Name()
1870 + "\" has activated its self-destruct mechanism.");
1871 GetTargetShip()->SelfDestruct();
1872 }
1873 else
1874 hasBoarded = true;
1875 }
1876 }
1877 }
1878 }
1879
1880 // Clear your target if it is destroyed. This is only important for NPCs,
1881 // because ordinary ships cease to exist once they are destroyed.
1882 target = GetTargetShip();
1883 if(target && target->IsDestroyed() && target->explosionCount >= target->explosionTotal)
1884 targetShip.reset();
1885
1886 // Finally, move the ship and create any movement visuals.
1887 position += velocity;
1888 if(isUsingAfterburner && !Attributes().AfterburnerEffects().empty())
1889 for(const EnginePoint &point : enginePoints)
1890 {
1891 Point pos = angle.Rotate(point) * Zoom() + position;
1892 // Stream the afterburner effects outward in the direction the engines are facing.
1893 Point effectVelocity = velocity - 6. * angle.Unit();
1894 for(auto &&it : Attributes().AfterburnerEffects())
1895 for(int i = 0; i < it.second; ++i)
1896 visuals.emplace_back(*it.first, pos, effectVelocity, angle);
1897 }
1898 }
1899
1900
1901
1902 // Generate energy, heat, etc. (This is called by Move().)
DoGeneration()1903 void Ship::DoGeneration()
1904 {
1905 // First, allow any carried ships to do their own generation.
1906 for(const Bay &bay : bays)
1907 if(bay.ship)
1908 bay.ship->DoGeneration();
1909
1910 // Shield and hull recharge. This uses whatever energy is left over from the
1911 // previous frame, so that it will not steal energy from movement, etc.
1912 if(!isDisabled)
1913 {
1914 // Priority of repairs:
1915 // 1. Ship's own hull
1916 // 2. Ship's own shields
1917 // 3. Hull of carried fighters
1918 // 4. Shields of carried fighters
1919 // 5. Transfer of excess energy and fuel to carried fighters.
1920
1921 const double hullAvailable = attributes.Get("hull repair rate") * (1. + attributes.Get("hull repair multiplier"));
1922 const double hullEnergy = (attributes.Get("hull energy") * (1. + attributes.Get("hull energy multiplier"))) / hullAvailable;
1923 const double hullFuel = (attributes.Get("hull fuel") * (1. + attributes.Get("hull fuel multiplier"))) / hullAvailable;
1924 const double hullHeat = (attributes.Get("hull heat") * (1. + attributes.Get("hull heat multiplier"))) / hullAvailable;
1925 double hullRemaining = hullAvailable;
1926 if(!hullDelay)
1927 DoRepair(hull, hullRemaining, attributes.Get("hull"), energy, hullEnergy, fuel, hullFuel, heat, hullHeat);
1928
1929 const double shieldsAvailable = attributes.Get("shield generation") * (1. + attributes.Get("shield generation multiplier"));
1930 const double shieldsEnergy = (attributes.Get("shield energy") * (1. + attributes.Get("shield energy multiplier"))) / shieldsAvailable;
1931 const double shieldsFuel = (attributes.Get("shield fuel") * (1. + attributes.Get("shield fuel multiplier"))) / shieldsAvailable;
1932 const double shieldsHeat = (attributes.Get("shield heat") * (1. + attributes.Get("shield heat multiplier"))) / shieldsAvailable;
1933 double shieldsRemaining = shieldsAvailable;
1934 if(!shieldDelay)
1935 DoRepair(shields, shieldsRemaining, attributes.Get("shields"), energy, shieldsEnergy, fuel, shieldsFuel, heat, shieldsHeat);
1936
1937 if(!bays.empty())
1938 {
1939 // If this ship is carrying fighters, determine their repair priority.
1940 vector<pair<double, Ship *>> carried;
1941 for(const Bay &bay : bays)
1942 if(bay.ship)
1943 carried.emplace_back(1. - bay.ship->Health(), bay.ship.get());
1944 sort(carried.begin(), carried.end(), (isYours && Preferences::Has(FIGHTER_REPAIR))
1945 // Players may use a parallel strategy, to launch fighters in waves.
1946 ? [] (const pair<double, Ship *> &lhs, const pair<double, Ship *> &rhs)
1947 { return lhs.first > rhs.first; }
1948 // The default strategy is to prioritize the healthiest ship first, in
1949 // order to get fighters back out into the battle as soon as possible.
1950 : [] (const pair<double, Ship *> &lhs, const pair<double, Ship *> &rhs)
1951 { return lhs.first < rhs.first; }
1952 );
1953
1954 // Apply shield and hull repair to carried fighters.
1955 for(const pair<double, Ship *> &it : carried)
1956 {
1957 Ship &ship = *it.second;
1958 if(!hullDelay)
1959 DoRepair(ship.hull, hullRemaining, ship.attributes.Get("hull"), energy, hullEnergy, heat, hullHeat, fuel, hullFuel);
1960 if(!shieldDelay)
1961 DoRepair(ship.shields, shieldsRemaining, ship.attributes.Get("shields"), energy, shieldsEnergy, heat, shieldsHeat, fuel, shieldsFuel);
1962 }
1963
1964 // Now that there is no more need to use energy for hull and shield
1965 // repair, if there is still excess energy, transfer it.
1966 double energyRemaining = min(0., energy - attributes.Get("energy capacity"));
1967 double fuelRemaining = min(0., fuel - attributes.Get("fuel capacity"));
1968 for(const pair<double, Ship *> &it : carried)
1969 {
1970 Ship &ship = *it.second;
1971 DoRepair(ship.energy, energyRemaining, ship.attributes.Get("energy capacity"));
1972 DoRepair(ship.fuel, fuelRemaining, ship.attributes.Get("fuel capacity"));
1973 }
1974 }
1975 // Decrease the shield and hull delays by 1 now that shield generation
1976 // and hull repair have been skipped over.
1977 shieldDelay = max(0, shieldDelay - 1);
1978 hullDelay = max(0, hullDelay - 1);
1979 }
1980
1981 // Handle ionization effects, etc.
1982 // TODO: Mothership gives status resistance to carried ships?
1983 if(ionization)
1984 {
1985 double ionResistance = attributes.Get("ion resistance");
1986 double ionEnergy = attributes.Get("ion resistance energy") / ionResistance;
1987 double ionFuel = attributes.Get("ion resistance fuel") / ionResistance;
1988 double ionHeat = attributes.Get("ion resistance heat") / ionResistance;
1989 DoStatusEffect(isDisabled, ionization, ionResistance, energy, ionEnergy, fuel, ionFuel, heat, ionHeat);
1990 }
1991
1992 if(disruption)
1993 {
1994 double disruptionResistance = attributes.Get("disruption resistance");
1995 double disruptionEnergy = attributes.Get("disruption resistance energy") / disruptionResistance;
1996 double disruptionFuel = attributes.Get("disruption resistance fuel") / disruptionResistance;
1997 double disruptionHeat = attributes.Get("disruption resistance heat") / disruptionResistance;
1998 DoStatusEffect(isDisabled, disruption, disruptionResistance, energy, disruptionEnergy, fuel, disruptionFuel, heat, disruptionHeat);
1999 }
2000
2001 if(slowness)
2002 {
2003 double slowingResistance = attributes.Get("slowing resistance");
2004 double slowingEnergy = attributes.Get("slowing resistance energy") / slowingResistance;
2005 double slowingFuel = attributes.Get("slowing resistance fuel") / slowingResistance;
2006 double slowingHeat = attributes.Get("slowing resistance heat") / slowingResistance;
2007 DoStatusEffect(isDisabled, slowness, slowingResistance, energy, slowingEnergy, fuel, slowingFuel, heat, slowingHeat);
2008 }
2009
2010 // When ships recharge, what actually happens is that they can exceed their
2011 // maximum capacity for the rest of the turn, but must be clamped to the
2012 // maximum here before they gain more. This is so that, for example, a ship
2013 // with no batteries but a good generator can still move.
2014 energy = min(energy, attributes.Get("energy capacity"));
2015 fuel = min(fuel, attributes.Get("fuel capacity"));
2016
2017 heat -= heat * HeatDissipation();
2018 if(heat > MaximumHeat())
2019 isOverheated = true;
2020 else if(heat < .9 * MaximumHeat())
2021 isOverheated = false;
2022
2023 double maxShields = attributes.Get("shields");
2024 shields = min(shields, maxShields);
2025 double maxHull = attributes.Get("hull");
2026 hull = min(hull, maxHull);
2027
2028 isDisabled = isOverheated || hull < MinimumHull() || (!crew && RequiredCrew());
2029
2030 // Whenever not actively scanning, the amount of scan information the ship
2031 // has "decays" over time. For a scanner with a speed of 1, one second of
2032 // uninterrupted scanning is required to successfully scan its target.
2033 // Only apply the decay if not already done scanning the target.
2034 if(cargoScan < SCAN_TIME)
2035 cargoScan = max(0., cargoScan - 1.);
2036 if(outfitScan < SCAN_TIME)
2037 outfitScan = max(0., outfitScan - 1.);
2038
2039 // Update ship supply levels.
2040 energy -= ionization;
2041 if(isDisabled)
2042 PauseAnimation();
2043 else
2044 {
2045 // Ramscoops work much better when close to the system center. Even if a
2046 // ship has no ramscoop, it can harvest a tiny bit of fuel by flying
2047 // close to the star. Carried fighters can't collect fuel or energy this way.
2048 if(currentSystem)
2049 {
2050 double scale = .2 + 1.8 / (.001 * position.Length() + 1);
2051 fuel += currentSystem->SolarWind() * .03 * scale * (sqrt(attributes.Get("ramscoop")) + .05 * scale);
2052
2053 double solarScaling = currentSystem->SolarPower() * scale;
2054 energy += solarScaling * attributes.Get("solar collection");
2055 heat += solarScaling * attributes.Get("solar heat");
2056 }
2057
2058 double coolingEfficiency = CoolingEfficiency();
2059 energy += attributes.Get("energy generation") - attributes.Get("energy consumption");
2060 fuel += attributes.Get("fuel generation");
2061 heat += attributes.Get("heat generation");
2062 heat -= coolingEfficiency * attributes.Get("cooling");
2063
2064 // Convert fuel into energy and heat only when the required amount of fuel is available.
2065 if(attributes.Get("fuel consumption") <= fuel)
2066 {
2067 fuel -= attributes.Get("fuel consumption");
2068 energy += attributes.Get("fuel energy");
2069 heat += attributes.Get("fuel heat");
2070 }
2071
2072 // Apply active cooling. The fraction of full cooling to apply equals
2073 // your ship's current fraction of its maximum temperature.
2074 double activeCooling = coolingEfficiency * attributes.Get("active cooling");
2075 if(activeCooling > 0. && heat > 0. && energy >= 0.)
2076 {
2077 // Although it's a misuse of this feature, handle the case where
2078 // "active cooling" does not require any energy.
2079 double coolingEnergy = attributes.Get("cooling energy");
2080 if(coolingEnergy)
2081 {
2082 double spentEnergy = min(energy, coolingEnergy * min(1., Heat()));
2083 heat -= activeCooling * spentEnergy / coolingEnergy;
2084 energy -= spentEnergy;
2085 }
2086 else
2087 heat -= activeCooling;
2088 }
2089 }
2090
2091 // Don't allow any levels to drop below zero.
2092 fuel = max(0., fuel);
2093 energy = max(0., energy);
2094 heat = max(0., heat);
2095 }
2096
2097
2098
2099 // Launch any ships that are ready to launch.
Launch(list<shared_ptr<Ship>> & ships,vector<Visual> & visuals)2100 void Ship::Launch(list<shared_ptr<Ship>> &ships, vector<Visual> &visuals)
2101 {
2102 // Allow carried ships to launch from a disabled ship, but not from a ship that
2103 // is landing, jumping, or cloaked. If already destroyed (e.g. self-destructing),
2104 // eject any ships still docked, possibly destroying them in the process.
2105 bool ejecting = IsDestroyed();
2106 if(!ejecting && (!commands.Has(Command::DEPLOY) || zoom != 1.f || hyperspaceCount || cloak))
2107 return;
2108
2109 for(Bay &bay : bays)
2110 if(bay.ship && ((bay.ship->Commands().Has(Command::DEPLOY) && !Random::Int(40 + 20 * !bay.ship->attributes.Get("automaton")))
2111 || (ejecting && !Random::Int(6))))
2112 {
2113 // Resupply any ships launching of their own accord.
2114 if(!ejecting)
2115 {
2116 // TODO: Restock fighter weaponry that needs ammo.
2117
2118 // This ship will refuel naturally based on the carrier's fuel
2119 // collection, but the carrier may have some reserves to spare.
2120 double maxFuel = bay.ship->attributes.Get("fuel capacity");
2121 if(maxFuel)
2122 {
2123 double spareFuel = fuel - JumpFuel();
2124 if(spareFuel > 0.)
2125 TransferFuel(min(maxFuel - bay.ship->fuel, spareFuel), bay.ship.get());
2126 // If still low or out-of-fuel, re-stock the carrier and don't launch.
2127 if(bay.ship->fuel < .25 * maxFuel)
2128 {
2129 TransferFuel(bay.ship->fuel, this);
2130 continue;
2131 }
2132 }
2133 }
2134 // Those being ejected may be destroyed if they are already injured.
2135 else if(bay.ship->Health() < Random::Real())
2136 bay.ship->SelfDestruct();
2137
2138 ships.push_back(bay.ship);
2139 double maxV = bay.ship->MaxVelocity() * (1 + bay.ship->IsDestroyed());
2140 Point exitPoint = position + angle.Rotate(bay.point);
2141 // When ejected, ships depart haphazardly.
2142 Angle launchAngle = ejecting ? Angle(exitPoint - position) : angle + bay.facing;
2143 Point v = velocity + (.3 * maxV) * launchAngle.Unit() + (.2 * maxV) * Angle::Random().Unit();
2144 bay.ship->Place(exitPoint, v, launchAngle);
2145 bay.ship->SetSystem(currentSystem);
2146 bay.ship->SetParent(shared_from_this());
2147 bay.ship->UnmarkForRemoval();
2148 // Update the cached sum of carried ship masses.
2149 carriedMass -= bay.ship->Mass();
2150 // Create the desired launch effects.
2151 for(const Effect *effect : bay.launchEffects)
2152 visuals.emplace_back(*effect, exitPoint, velocity, launchAngle);
2153
2154 bay.ship.reset();
2155 }
2156 }
2157
2158
2159
2160 // Check if this ship is boarding another ship.
Board(bool autoPlunder)2161 shared_ptr<Ship> Ship::Board(bool autoPlunder)
2162 {
2163 if(!hasBoarded)
2164 return shared_ptr<Ship>();
2165 hasBoarded = false;
2166
2167 shared_ptr<Ship> victim = GetTargetShip();
2168 if(CannotAct() || !victim || victim->IsDestroyed() || victim->GetSystem() != GetSystem())
2169 return shared_ptr<Ship>();
2170
2171 // For a fighter or drone, "board" means "return to ship."
2172 if(CanBeCarried())
2173 {
2174 SetTargetShip(shared_ptr<Ship>());
2175 if(!victim->IsDisabled() && victim->GetGovernment() == government)
2176 victim->Carry(shared_from_this());
2177 return shared_ptr<Ship>();
2178 }
2179
2180 // Board a friendly ship, to repair or refuel it.
2181 if(!government->IsEnemy(victim->GetGovernment()))
2182 {
2183 SetShipToAssist(shared_ptr<Ship>());
2184 SetTargetShip(shared_ptr<Ship>());
2185 bool helped = victim->isDisabled;
2186 victim->hull = min(max(victim->hull, victim->MinimumHull() * 1.5), victim->attributes.Get("hull"));
2187 victim->isDisabled = false;
2188 // Transfer some fuel if needed.
2189 if(!victim->JumpsRemaining() && CanRefuel(*victim))
2190 {
2191 helped = true;
2192 TransferFuel(victim->JumpFuelMissing(), victim.get());
2193 }
2194 if(helped)
2195 {
2196 pilotError = 120;
2197 victim->pilotError = 120;
2198 }
2199 return victim;
2200 }
2201 if(!victim->IsDisabled())
2202 return shared_ptr<Ship>();
2203
2204 // If the boarding ship is the player, they will choose what to plunder.
2205 // Always take fuel if you can.
2206 victim->TransferFuel(victim->fuel, this);
2207 if(autoPlunder)
2208 {
2209 // Take any commodities that fit.
2210 victim->cargo.TransferAll(cargo, false);
2211
2212 // Pause for two seconds before moving on.
2213 pilotError = 120;
2214 }
2215
2216 // Stop targeting this ship (so you will not board it again right away).
2217 if(!autoPlunder || personality.Disables())
2218 SetTargetShip(shared_ptr<Ship>());
2219 return victim;
2220 }
2221
2222
2223
2224 // Scan the target, if able and commanded to. Return a ShipEvent bitmask
2225 // giving the types of scan that succeeded.
Scan()2226 int Ship::Scan()
2227 {
2228 if(!commands.Has(Command::SCAN) || CannotAct())
2229 return 0;
2230
2231 shared_ptr<const Ship> target = GetTargetShip();
2232 if(!(target && target->IsTargetable()))
2233 return 0;
2234
2235 // The range of a scanner is proportional to the square root of its power.
2236 double cargoDistance = 100. * sqrt(attributes.Get("cargo scan power"));
2237 double outfitDistance = 100. * sqrt(attributes.Get("outfit scan power"));
2238
2239 // Bail out if this ship has no scanners.
2240 if(!cargoDistance && !outfitDistance)
2241 return 0;
2242
2243 // Scanning speed also uses a square root, so you need four scanners to get
2244 // twice the speed out of them.
2245 double cargoSpeed = sqrt(attributes.Get("cargo scan speed"));
2246 if(!cargoSpeed)
2247 cargoSpeed = 1.;
2248 double outfitSpeed = sqrt(attributes.Get("outfit scan speed"));
2249 if(!outfitSpeed)
2250 outfitSpeed = 1.;
2251
2252 // Check how close this ship is to the target it is trying to scan.
2253 double distance = (target->position - position).Length();
2254
2255 // Check if either scanner has finished scanning.
2256 bool startedScanning = false;
2257 bool activeScanning = false;
2258 int result = 0;
2259 auto doScan = [&](double &elapsed, const double speed, const double scannerRange, const int event) -> void
2260 {
2261 if(elapsed < SCAN_TIME && distance < scannerRange)
2262 {
2263 startedScanning |= !elapsed;
2264 activeScanning = true;
2265 // To make up for the scan decay above:
2266 elapsed += speed + 1.;
2267 if(elapsed >= SCAN_TIME)
2268 result |= event;
2269 }
2270 };
2271 doScan(cargoScan, cargoSpeed, cargoDistance, ShipEvent::SCAN_CARGO);
2272 doScan(outfitScan, outfitSpeed, outfitDistance, ShipEvent::SCAN_OUTFITS);
2273
2274 // Play the scanning sound if the actor or the target is the player's ship.
2275 if(isYours || (target->isYours && activeScanning))
2276 Audio::Play(Audio::Get("scan"), Position());
2277
2278 if(startedScanning && isYours)
2279 {
2280 if(!target->Name().empty())
2281 Messages::Add("Attempting to scan the " + target->Noun() + " \"" + target->Name() + "\".", false);
2282 else
2283 Messages::Add("Attempting to scan the selected " + target->Noun() + ".", false);
2284 }
2285 else if(startedScanning && target->isYours)
2286 Messages::Add("The " + government->GetName() + " " + Noun() + " \""
2287 + Name() + "\" is attempting to scan you.", false);
2288
2289 if(target->isYours && !isYours)
2290 {
2291 if(result & ShipEvent::SCAN_CARGO)
2292 Messages::Add("The " + government->GetName() + " " + Noun() + " \""
2293 + Name() + "\" completed its scan of your cargo.");
2294 if(result & ShipEvent::SCAN_OUTFITS)
2295 Messages::Add("The " + government->GetName() + " " + Noun() + " \""
2296 + Name() + "\" completed its scan of your outfits.");
2297 }
2298
2299 return result;
2300 }
2301
2302
2303
2304 // Find out what fraction of the scan is complete.
CargoScanFraction() const2305 double Ship::CargoScanFraction() const
2306 {
2307 return cargoScan / SCAN_TIME;
2308 }
2309
2310
2311
OutfitScanFraction() const2312 double Ship::OutfitScanFraction() const
2313 {
2314 return outfitScan / SCAN_TIME;
2315 }
2316
2317
2318
2319 // Fire any weapons that are ready to fire. If an anti-missile is ready,
2320 // instead of firing here this function returns true and it can be fired if
2321 // collision detection finds a missile in range.
Fire(vector<Projectile> & projectiles,vector<Visual> & visuals)2322 bool Ship::Fire(vector<Projectile> &projectiles, vector<Visual> &visuals)
2323 {
2324 isInSystem = true;
2325 forget = 0;
2326
2327 // A ship that is about to die creates a special single-turn "projectile"
2328 // representing its death explosion.
2329 if(IsDestroyed() && explosionCount == explosionTotal && explosionWeapon)
2330 projectiles.emplace_back(position, explosionWeapon);
2331
2332 if(CannotAct())
2333 return false;
2334
2335 antiMissileRange = 0.;
2336
2337 const vector<Hardpoint> &hardpoints = armament.Get();
2338 for(unsigned i = 0; i < hardpoints.size(); ++i)
2339 {
2340 const Weapon *weapon = hardpoints[i].GetOutfit();
2341 if(weapon && CanFire(weapon))
2342 {
2343 if(weapon->AntiMissile())
2344 antiMissileRange = max(antiMissileRange, weapon->Velocity() + weaponRadius);
2345 else if(commands.HasFire(i))
2346 armament.Fire(i, *this, projectiles, visuals);
2347 }
2348 }
2349
2350 armament.Step(*this);
2351
2352 return antiMissileRange;
2353 }
2354
2355
2356
2357 // Fire an anti-missile.
FireAntiMissile(const Projectile & projectile,vector<Visual> & visuals)2358 bool Ship::FireAntiMissile(const Projectile &projectile, vector<Visual> &visuals)
2359 {
2360 if(projectile.Position().Distance(position) > antiMissileRange)
2361 return false;
2362 if(CannotAct())
2363 return false;
2364
2365 const vector<Hardpoint> &hardpoints = armament.Get();
2366 for(unsigned i = 0; i < hardpoints.size(); ++i)
2367 {
2368 const Weapon *weapon = hardpoints[i].GetOutfit();
2369 if(weapon && CanFire(weapon))
2370 if(armament.FireAntiMissile(i, *this, projectile, visuals))
2371 return true;
2372 }
2373
2374 return false;
2375 }
2376
2377
2378
GetSystem() const2379 const System *Ship::GetSystem() const
2380 {
2381 return currentSystem;
2382 }
2383
2384
2385
2386 // If the ship is landed, get the planet it has landed on.
GetPlanet() const2387 const Planet *Ship::GetPlanet() const
2388 {
2389 return zoom ? nullptr : landingPlanet;
2390 }
2391
2392
2393
IsCapturable() const2394 bool Ship::IsCapturable() const
2395 {
2396 return isCapturable;
2397 }
2398
2399
2400
IsTargetable() const2401 bool Ship::IsTargetable() const
2402 {
2403 return (zoom == 1.f && !explosionRate && !forget && !isInvisible && cloak < 1. && hull >= 0. && hyperspaceCount < 70);
2404 }
2405
2406
2407
IsOverheated() const2408 bool Ship::IsOverheated() const
2409 {
2410 return isOverheated;
2411 }
2412
2413
2414
IsDisabled() const2415 bool Ship::IsDisabled() const
2416 {
2417 if(!isDisabled)
2418 return false;
2419
2420 double minimumHull = MinimumHull();
2421 bool needsCrew = RequiredCrew() != 0;
2422 return (hull < minimumHull || (!crew && needsCrew));
2423 }
2424
2425
2426
IsBoarding() const2427 bool Ship::IsBoarding() const
2428 {
2429 return isBoarding;
2430 }
2431
2432
2433
IsLanding() const2434 bool Ship::IsLanding() const
2435 {
2436 return landingPlanet;
2437 }
2438
2439
2440
2441 // Check if this ship is currently able to begin landing on its target.
CanLand() const2442 bool Ship::CanLand() const
2443 {
2444 if(!GetTargetStellar() || !GetTargetStellar()->GetPlanet() || isDisabled || IsDestroyed())
2445 return false;
2446
2447 if(!GetTargetStellar()->GetPlanet()->CanLand(*this))
2448 return false;
2449
2450 Point distance = GetTargetStellar()->Position() - position;
2451 double speed = velocity.Length();
2452
2453 return (speed < 1. && distance.Length() < GetTargetStellar()->Radius());
2454 }
2455
2456
2457
CannotAct() const2458 bool Ship::CannotAct() const
2459 {
2460 return (zoom != 1.f || isDisabled || hyperspaceCount || pilotError || cloak);
2461 }
2462
2463
2464
Cloaking() const2465 double Ship::Cloaking() const
2466 {
2467 return isInvisible ? 1. : cloak;
2468 }
2469
2470
2471
IsEnteringHyperspace() const2472 bool Ship::IsEnteringHyperspace() const
2473 {
2474 return hyperspaceSystem;
2475 }
2476
2477
2478
IsHyperspacing() const2479 bool Ship::IsHyperspacing() const
2480 {
2481 return hyperspaceCount != 0;
2482 }
2483
2484
2485
2486 // Check if this ship is hyperspacing, specifically via a jump drive.
IsUsingJumpDrive() const2487 bool Ship::IsUsingJumpDrive() const
2488 {
2489 return (hyperspaceSystem || hyperspaceCount) && isUsingJumpDrive;
2490 }
2491
2492
2493
2494 // Check if this ship is currently able to enter hyperspace to it target.
IsReadyToJump(bool waitingIsReady) const2495 bool Ship::IsReadyToJump(bool waitingIsReady) const
2496 {
2497 // Ships can't jump while waiting for someone else, carried, or if already jumping.
2498 if(IsDisabled() || (!waitingIsReady && commands.Has(Command::WAIT))
2499 || hyperspaceCount || !targetSystem || !currentSystem)
2500 return false;
2501
2502 // Check if the target system is valid and there is enough fuel to jump.
2503 double fuelCost = JumpFuel(targetSystem);
2504 if(!fuelCost || fuel < fuelCost)
2505 return false;
2506
2507 Point direction = targetSystem->Position() - currentSystem->Position();
2508 bool isJump = !attributes.Get("hyperdrive") || !currentSystem->Links().count(targetSystem);
2509 double scramThreshold = attributes.Get("scram drive");
2510
2511 // The ship can only enter hyperspace if it is traveling slowly enough
2512 // and pointed in the right direction.
2513 if(!isJump && scramThreshold)
2514 {
2515 double deviation = fabs(direction.Unit().Cross(velocity));
2516 if(deviation > scramThreshold)
2517 return false;
2518 }
2519 else if(velocity.Length() > attributes.Get("jump speed"))
2520 return false;
2521
2522 if(!isJump)
2523 {
2524 // Figure out if we're within one turn step of facing this system.
2525 bool left = direction.Cross(angle.Unit()) < 0.;
2526 Angle turned = angle + TurnRate() * (left - !left);
2527 bool stillLeft = direction.Cross(turned.Unit()) < 0.;
2528
2529 if(left == stillLeft)
2530 return false;
2531 }
2532
2533 return true;
2534 }
2535
2536
2537
2538 // Get this ship's custom swizzle.
CustomSwizzle() const2539 int Ship::CustomSwizzle() const
2540 {
2541 return customSwizzle;
2542 }
2543
2544
2545 // Check if the ship is thrusting. If so, the engine sound should be played.
IsThrusting() const2546 bool Ship::IsThrusting() const
2547 {
2548 return isThrusting;
2549 }
2550
2551
2552
IsReversing() const2553 bool Ship::IsReversing() const
2554 {
2555 return isReversing;
2556 }
2557
2558
2559
IsSteering() const2560 bool Ship::IsSteering() const
2561 {
2562 return isSteering;
2563 }
2564
2565
2566
SteeringDirection() const2567 double Ship::SteeringDirection() const
2568 {
2569 return steeringDirection;
2570 }
2571
2572
2573
2574 // Get the points from which engine flares should be drawn.
EnginePoints() const2575 const vector<Ship::EnginePoint> &Ship::EnginePoints() const
2576 {
2577 return enginePoints;
2578 }
2579
2580
2581
ReverseEnginePoints() const2582 const vector<Ship::EnginePoint> &Ship::ReverseEnginePoints() const
2583 {
2584 return reverseEnginePoints;
2585 }
2586
2587
2588
SteeringEnginePoints() const2589 const vector<Ship::EnginePoint> &Ship::SteeringEnginePoints() const
2590 {
2591 return steeringEnginePoints;
2592 }
2593
2594
2595
2596 // Reduce a ship's hull to low enough to disable it. This is so a ship can be
2597 // created as a derelict.
Disable()2598 void Ship::Disable()
2599 {
2600 shields = 0.;
2601 hull = min(hull, .5 * MinimumHull());
2602 isDisabled = true;
2603 }
2604
2605
2606
2607 // Mark a ship as destroyed.
Destroy()2608 void Ship::Destroy()
2609 {
2610 hull = -1.;
2611 }
2612
2613
2614
2615 // Trigger the death of this ship.
SelfDestruct()2616 void Ship::SelfDestruct()
2617 {
2618 Destroy();
2619 explosionRate = 1024;
2620 }
2621
2622
2623
Restore()2624 void Ship::Restore()
2625 {
2626 hull = 0.;
2627 explosionCount = 0;
2628 explosionRate = 0;
2629 UnmarkForRemoval();
2630 Recharge(true);
2631 }
2632
2633
2634
2635 // Check if this ship has been destroyed.
IsDestroyed() const2636 bool Ship::IsDestroyed() const
2637 {
2638 return (hull < 0.);
2639 }
2640
2641
2642
2643 // Recharge and repair this ship (e.g. because it has landed).
Recharge(bool atSpaceport)2644 void Ship::Recharge(bool atSpaceport)
2645 {
2646 if(IsDestroyed())
2647 return;
2648
2649 if(atSpaceport)
2650 {
2651 crew = min<int>(max(crew, RequiredCrew()), attributes.Get("bunks"));
2652 fuel = attributes.Get("fuel capacity");
2653 }
2654 pilotError = 0;
2655 pilotOkay = 0;
2656
2657 if(atSpaceport || attributes.Get("shield generation"))
2658 shields = attributes.Get("shields");
2659 if(atSpaceport || attributes.Get("hull repair rate"))
2660 hull = attributes.Get("hull");
2661 if(atSpaceport || attributes.Get("energy generation"))
2662 energy = attributes.Get("energy capacity");
2663
2664 heat = IdleHeat();
2665 ionization = 0.;
2666 disruption = 0.;
2667 slowness = 0.;
2668 shieldDelay = 0;
2669 hullDelay = 0;
2670 }
2671
2672
2673
CanRefuel(const Ship & other) const2674 bool Ship::CanRefuel(const Ship &other) const
2675 {
2676 return (fuel - JumpFuel(targetSystem) >= other.JumpFuelMissing());
2677 }
2678
2679
2680
TransferFuel(double amount,Ship * to)2681 double Ship::TransferFuel(double amount, Ship *to)
2682 {
2683 amount = max(fuel - attributes.Get("fuel capacity"), amount);
2684 if(to)
2685 {
2686 amount = min(to->attributes.Get("fuel capacity") - to->fuel, amount);
2687 to->fuel += amount;
2688 }
2689 fuel -= amount;
2690 return amount;
2691 }
2692
2693
2694
2695 // Convert this ship from one government to another, as a result of boarding
2696 // actions (if the player is capturing) or player death (poor decision-making).
WasCaptured(const shared_ptr<Ship> & capturer)2697 void Ship::WasCaptured(const shared_ptr<Ship> &capturer)
2698 {
2699 // Repair up to the point where this ship is just barely not disabled.
2700 hull = min(max(hull, MinimumHull() * 1.5), attributes.Get("hull"));
2701 isDisabled = false;
2702
2703 // Set the new government.
2704 government = capturer->GetGovernment();
2705
2706 // Transfer some crew over. Only transfer the bare minimum unless even that
2707 // is not possible, in which case, share evenly.
2708 int totalRequired = capturer->RequiredCrew() + RequiredCrew();
2709 int transfer = RequiredCrew() - crew;
2710 if(transfer > 0)
2711 {
2712 if(totalRequired > capturer->Crew() + crew)
2713 transfer = max(crew ? 0 : 1, (capturer->Crew() * transfer) / totalRequired);
2714 capturer->AddCrew(-transfer);
2715 AddCrew(transfer);
2716 }
2717
2718 commands.Clear();
2719 // Set the capturer as this ship's parent.
2720 SetParent(capturer);
2721 // Clear this ship's previous targets.
2722 SetTargetShip(shared_ptr<Ship>());
2723 SetTargetStellar(nullptr);
2724 SetTargetSystem(nullptr);
2725 shipToAssist.reset();
2726 targetAsteroid.reset();
2727 targetFlotsam.reset();
2728 hyperspaceSystem = nullptr;
2729 landingPlanet = nullptr;
2730
2731 // This ship behaves like its new parent does.
2732 isSpecial = capturer->isSpecial;
2733 isYours = capturer->isYours;
2734 personality = capturer->personality;
2735
2736 // Fighters should flee a disabled ship, but if the player manages to capture
2737 // the ship before they flee, the fighters are captured, too.
2738 for(const Bay &bay : bays)
2739 if(bay.ship)
2740 bay.ship->WasCaptured(capturer);
2741 // If a flagship is captured, its escorts become independent.
2742 for(const auto &it : escorts)
2743 {
2744 shared_ptr<Ship> escort = it.lock();
2745 if(escort)
2746 escort->parent.reset();
2747 }
2748 // This ship should not care about its now-unallied escorts.
2749 escorts.clear();
2750 }
2751
2752
2753
2754 // Get characteristics of this ship, as a fraction between 0 and 1.
Shields() const2755 double Ship::Shields() const
2756 {
2757 double maximum = attributes.Get("shields");
2758 return maximum ? min(1., shields / maximum) : 0.;
2759 }
2760
2761
2762
Hull() const2763 double Ship::Hull() const
2764 {
2765 double maximum = attributes.Get("hull");
2766 return maximum ? min(1., hull / maximum) : 1.;
2767 }
2768
2769
2770
Fuel() const2771 double Ship::Fuel() const
2772 {
2773 double maximum = attributes.Get("fuel capacity");
2774 return maximum ? min(1., fuel / maximum) : 0.;
2775 }
2776
2777
2778
Energy() const2779 double Ship::Energy() const
2780 {
2781 double maximum = attributes.Get("energy capacity");
2782 return maximum ? min(1., energy / maximum) : (hull > 0.) ? 1. : 0.;
2783 }
2784
2785
2786
2787 // Allow returning a heat value greater than 1 (i.e. conveying how overheated
2788 // this ship has become).
Heat() const2789 double Ship::Heat() const
2790 {
2791 double maximum = MaximumHeat();
2792 return maximum ? heat / maximum : 1.;
2793 }
2794
2795
2796
2797 // Get the ship's "health," where <=0 is disabled and 1 means full health.
Health() const2798 double Ship::Health() const
2799 {
2800 double minimumHull = MinimumHull();
2801 double hullDivisor = attributes.Get("hull") - minimumHull;
2802 double divisor = attributes.Get("shields") + hullDivisor;
2803 // This should not happen, but just in case.
2804 if(divisor <= 0. || hullDivisor <= 0.)
2805 return 0.;
2806
2807 double spareHull = hull - minimumHull;
2808 // Consider hull-only and pooled health, compensating for any reductions by disruption damage.
2809 return min(spareHull / hullDivisor, (spareHull + shields / (1. + disruption * .01)) / divisor);
2810 }
2811
2812
2813
2814 // Get the hull fraction at which this ship is disabled.
DisabledHull() const2815 double Ship::DisabledHull() const
2816 {
2817 double hull = attributes.Get("hull");
2818 double minimumHull = MinimumHull();
2819
2820 return (hull > 0. ? minimumHull / hull : 0.);
2821 }
2822
2823
2824
JumpsRemaining(bool followParent) const2825 int Ship::JumpsRemaining(bool followParent) const
2826 {
2827 // Make sure this ship has some sort of hyperdrive, and if so return how
2828 // many jumps it can make.
2829 double jumpFuel = 0.;
2830 if(!targetSystem && followParent)
2831 {
2832 // If this ship has no destination, the parent's substitutes for it,
2833 // but only if the location is reachable.
2834 auto p = GetParent();
2835 if(p)
2836 jumpFuel = JumpFuel(p->GetTargetSystem());
2837 }
2838 if(!jumpFuel)
2839 jumpFuel = JumpFuel(targetSystem);
2840 return jumpFuel ? fuel / jumpFuel : 0.;
2841 }
2842
2843
2844
JumpFuel(const System * destination) const2845 double Ship::JumpFuel(const System *destination) const
2846 {
2847 // A currently-carried ship requires no fuel to jump, because it cannot jump.
2848 if(!currentSystem)
2849 return 0.;
2850
2851 // If no destination is given, return the maximum fuel per jump.
2852 if(!destination)
2853 return max(JumpDriveFuel(), HyperdriveFuel());
2854
2855 bool linked = currentSystem->Links().count(destination);
2856 // Figure out what sort of jump we're making.
2857 if(attributes.Get("hyperdrive") && linked)
2858 return HyperdriveFuel();
2859
2860 if(attributes.Get("jump drive") && currentSystem->JumpNeighbors(JumpRange()).count(destination))
2861 return JumpDriveFuel((linked || currentSystem->JumpRange()) ? 0. : currentSystem->Position().Distance(destination->Position()));
2862
2863 // If the given system is not a possible destination, return 0.
2864 return 0.;
2865 }
2866
2867
2868
JumpRange(bool getCached) const2869 double Ship::JumpRange(bool getCached) const
2870 {
2871 if(getCached)
2872 return jumpRange;
2873
2874 // Ships without a jump drive have no jump range.
2875 if(!attributes.Get("jump drive"))
2876 return 0.;
2877
2878 // Find the outfit that provides the farthest jump range.
2879 double best = 0.;
2880 // Make it possible for the jump range to be integrated into a ship.
2881 if(baseAttributes.Get("jump drive"))
2882 {
2883 best = baseAttributes.Get("jump range");
2884 if(!best)
2885 best = System::DEFAULT_NEIGHBOR_DISTANCE;
2886 }
2887 // Search through all the outfits.
2888 for(const auto &it : outfits)
2889 if(it.first->Get("jump drive"))
2890 {
2891 double range = it.first->Get("jump range");
2892 if(!range)
2893 range = System::DEFAULT_NEIGHBOR_DISTANCE;
2894 if(!best || range > best)
2895 best = range;
2896 }
2897 return best;
2898 }
2899
2900
2901
2902 // Get the cost of making a jump of the given type (if possible).
HyperdriveFuel() const2903 double Ship::HyperdriveFuel() const
2904 {
2905 // Don't bother searching through the outfits if there is no hyperdrive.
2906 if(!attributes.Get("hyperdrive"))
2907 return JumpDriveFuel();
2908
2909 if(attributes.Get("scram drive"))
2910 return BestFuel("hyperdrive", "scram drive", 150.);
2911
2912 return BestFuel("hyperdrive", "", 100.);
2913 }
2914
2915
2916
JumpDriveFuel(double jumpDistance) const2917 double Ship::JumpDriveFuel(double jumpDistance) const
2918 {
2919 // Don't bother searching through the outfits if there is no jump drive.
2920 if(!attributes.Get("jump drive"))
2921 return 0.;
2922
2923 return BestFuel("jump drive", "", 200., jumpDistance);
2924 }
2925
2926
2927
JumpFuelMissing() const2928 double Ship::JumpFuelMissing() const
2929 {
2930 // Used for smart refueling: transfer only as much as really needed
2931 // includes checking if fuel cap is high enough at all
2932 double jumpFuel = JumpFuel(targetSystem);
2933 if(!jumpFuel || fuel > jumpFuel || jumpFuel > attributes.Get("fuel capacity"))
2934 return 0.;
2935
2936 return jumpFuel - fuel;
2937 }
2938
2939
2940
2941 // Get the heat level at idle.
IdleHeat() const2942 double Ship::IdleHeat() const
2943 {
2944 // This ship's cooling ability:
2945 double coolingEfficiency = CoolingEfficiency();
2946 double cooling = coolingEfficiency * attributes.Get("cooling");
2947 double activeCooling = coolingEfficiency * attributes.Get("active cooling");
2948
2949 // Idle heat is the heat level where:
2950 // heat = heat * diss + heatGen - cool - activeCool * heat / (100 * mass)
2951 // heat = heat * (diss - activeCool / (100 * mass)) + (heatGen - cool)
2952 // heat * (1 - diss + activeCool / (100 * mass)) = (heatGen - cool)
2953 double production = max(0., attributes.Get("heat generation") - cooling);
2954 double dissipation = HeatDissipation() + activeCooling / MaximumHeat();
2955 if(!dissipation) return production ? numeric_limits<double>::max() : 0;
2956 return production / dissipation;
2957 }
2958
2959
2960
2961 // Get the heat dissipation, in heat units per heat unit per frame.
HeatDissipation() const2962 double Ship::HeatDissipation() const
2963 {
2964 return .001 * attributes.Get("heat dissipation");
2965 }
2966
2967
2968
2969 // Get the maximum heat level, in heat units (not temperature).
MaximumHeat() const2970 double Ship::MaximumHeat() const
2971 {
2972 return MAXIMUM_TEMPERATURE * (cargo.Used() + attributes.Mass());
2973 }
2974
2975
2976
2977 // Calculate the multiplier for cooling efficiency.
CoolingEfficiency() const2978 double Ship::CoolingEfficiency() const
2979 {
2980 // This is an S-curve where the efficiency is 100% if you have no outfits
2981 // that create "cooling inefficiency", and as that value increases the
2982 // efficiency stays high for a while, then drops off, then approaches 0.
2983 double x = attributes.Get("cooling inefficiency");
2984 return 2. + 2. / (1. + exp(x / -2.)) - 4. / (1. + exp(x / -4.));
2985 }
2986
2987
2988
Crew() const2989 int Ship::Crew() const
2990 {
2991 return crew;
2992 }
2993
2994
2995
RequiredCrew() const2996 int Ship::RequiredCrew() const
2997 {
2998 if(attributes.Get("automaton"))
2999 return 0;
3000
3001 // Drones do not need crew, but all other ships need at least one.
3002 return max<int>(1, attributes.Get("required crew"));
3003 }
3004
3005
3006
AddCrew(int count)3007 void Ship::AddCrew(int count)
3008 {
3009 crew = min<int>(crew + count, attributes.Get("bunks"));
3010 }
3011
3012
3013
3014 // Check if this is a ship that can be used as a flagship.
CanBeFlagship() const3015 bool Ship::CanBeFlagship() const
3016 {
3017 return RequiredCrew() && Crew() && !IsDisabled();
3018 }
3019
3020
3021
Mass() const3022 double Ship::Mass() const
3023 {
3024 return carriedMass + cargo.Used() + attributes.Mass();
3025 }
3026
3027
3028
TurnRate() const3029 double Ship::TurnRate() const
3030 {
3031 return attributes.Get("turn") / Mass();
3032 }
3033
3034
3035
Acceleration() const3036 double Ship::Acceleration() const
3037 {
3038 double thrust = attributes.Get("thrust");
3039 return (thrust ? thrust : attributes.Get("afterburner thrust")) / Mass();
3040 }
3041
3042
3043
MaxVelocity() const3044 double Ship::MaxVelocity() const
3045 {
3046 // v * drag / mass == thrust / mass
3047 // v * drag == thrust
3048 // v = thrust / drag
3049 double thrust = attributes.Get("thrust");
3050 return (thrust ? thrust : attributes.Get("afterburner thrust")) / attributes.Get("drag");
3051 }
3052
3053
3054
MaxReverseVelocity() const3055 double Ship::MaxReverseVelocity() const
3056 {
3057 return attributes.Get("reverse thrust") / attributes.Get("drag");
3058 }
3059
3060
3061
3062 // This ship just got hit by the given projectile. Take damage according to
3063 // what sort of weapon the projectile it.
TakeDamage(const Projectile & projectile,bool isBlast)3064 int Ship::TakeDamage(const Projectile &projectile, bool isBlast)
3065 {
3066 int type = 0;
3067 const Weapon &weapon = projectile.GetWeapon();
3068 type |= TakeDamage(weapon, 1., projectile.DistanceTraveled(), projectile.Position(), isBlast);
3069
3070 // If this ship was hit directly and did not consider itself an enemy of the
3071 // ship that hit it, it is now "provoked" against that government.
3072 if(!isBlast && projectile.GetGovernment() && !projectile.GetGovernment()->IsEnemy(government)
3073 && (Shields() < .9 || Hull() < .9 || !personality.IsForbearing())
3074 && !personality.IsPacifist() && weapon.DoesDamage())
3075 type |= ShipEvent::PROVOKE;
3076
3077 return type;
3078 }
3079
3080
3081
3082 // This ship just got hit by the given hazard. Take damage according to what
3083 // sort of weapon the hazard has, and create any hit effects as sparks.
TakeHazardDamage(vector<Visual> & visuals,const Hazard * hazard,double strength)3084 void Ship::TakeHazardDamage(vector<Visual> &visuals, const Hazard *hazard, double strength)
3085 {
3086 // Rather than exactly compute the distance between the hazard origin and
3087 // the closest point on the ship, estimate it using the mask's Radius.
3088 double distanceTraveled = position.Length() - GetMask().Radius();
3089 TakeDamage(*hazard, strength, distanceTraveled, Point(), hazard->BlastRadius() > 0.);
3090 for(const auto &effect : hazard->HitEffects())
3091 CreateSparks(visuals, effect.first, effect.second * strength);
3092 }
3093
3094
3095
3096 // Apply a force to this ship, accelerating it. This might be from a weapon
3097 // impact, or from firing a weapon, for example.
ApplyForce(const Point & force,bool gravitational)3098 void Ship::ApplyForce(const Point &force, bool gravitational)
3099 {
3100 if(gravitational)
3101 {
3102 // Treat all ships as if they have a mass of 400. This prevents
3103 // gravitational hit force values from needing to be extremely
3104 // small in order to have a reasonable effect.
3105 acceleration += force / 400.;
3106 return;
3107 }
3108
3109 double currentMass = Mass();
3110 if(!currentMass)
3111 return;
3112
3113 // Reduce acceleration of small ships and increase acceleration of large
3114 // ones by having 30% of the force be based on a fixed mass of 400, i.e. the
3115 // mass of a typical light warship:
3116 acceleration += force * (.3 / 400. + .7 / currentMass);
3117 }
3118
3119
3120
HasBays() const3121 bool Ship::HasBays() const
3122 {
3123 return !bays.empty();
3124 }
3125
3126
3127
3128 // Check how many bays are not occupied at present. This does not check whether
3129 // one of your escorts plans to use that bay.
BaysFree(const string & category) const3130 int Ship::BaysFree(const string &category) const
3131 {
3132 int count = 0;
3133 for(const Bay &bay : bays)
3134 count += (bay.category == category) && !bay.ship;
3135 return count;
3136 }
3137
3138
3139
3140 // Check how many bays this ship has of a given category.
BaysTotal(const string & category) const3141 int Ship::BaysTotal(const string &category) const
3142 {
3143 int count = 0;
3144 for(const Bay &bay : bays)
3145 count += (bay.category == category);
3146 return count;
3147 }
3148
3149
3150
3151 // Check if this ship has a bay free for the given ship, and the bay is
3152 // not reserved for one of its existing escorts.
CanCarry(const Ship & ship) const3153 bool Ship::CanCarry(const Ship &ship) const
3154 {
3155 if(!ship.CanBeCarried())
3156 return false;
3157 // Check only for the category that we are interested in.
3158 const string &category = ship.attributes.Category();
3159
3160 int free = BaysFree(category);
3161 if(!free)
3162 return false;
3163
3164 for(const auto &it : escorts)
3165 {
3166 auto escort = it.lock();
3167 if(escort && escort->attributes.Category() == category)
3168 --free;
3169 }
3170 return (free > 0);
3171 }
3172
3173
3174
AllowCarried(bool allowCarried)3175 void Ship::AllowCarried(bool allowCarried)
3176 {
3177 canBeCarried = allowCarried && BAY_TYPES.count(attributes.Category()) > 0;
3178 }
3179
3180
3181
CanBeCarried() const3182 bool Ship::CanBeCarried() const
3183 {
3184 return canBeCarried;
3185 }
3186
3187
3188
Carry(const shared_ptr<Ship> & ship)3189 bool Ship::Carry(const shared_ptr<Ship> &ship)
3190 {
3191 if(!ship || !ship->CanBeCarried())
3192 return false;
3193
3194 // Check only for the category that we are interested in.
3195 const string &category = ship->attributes.Category();
3196
3197 for(Bay &bay : bays)
3198 if((bay.category == category) && !bay.ship)
3199 {
3200 bay.ship = ship;
3201 ship->SetSystem(nullptr);
3202 ship->SetPlanet(nullptr);
3203 ship->SetTargetSystem(nullptr);
3204 ship->SetTargetStellar(nullptr);
3205 ship->SetParent(shared_from_this());
3206 ship->isThrusting = false;
3207 ship->isReversing = false;
3208 ship->isSteering = false;
3209 ship->commands.Clear();
3210 // If this fighter collected anything in space, try to store it
3211 // (unless this is a player-owned ship).
3212 if(!isYours && cargo.Free() && !ship->Cargo().IsEmpty())
3213 ship->Cargo().TransferAll(cargo);
3214 // Return unused fuel to the carrier, for any launching fighter that needs it.
3215 ship->TransferFuel(ship->fuel, this);
3216
3217 // Update the cached mass of the mothership.
3218 carriedMass += ship->Mass();
3219 return true;
3220 }
3221 return false;
3222 }
3223
3224
3225
UnloadBays()3226 void Ship::UnloadBays()
3227 {
3228 for(Bay &bay : bays)
3229 if(bay.ship)
3230 {
3231 carriedMass -= bay.ship->Mass();
3232 bay.ship->SetSystem(currentSystem);
3233 bay.ship->SetPlanet(landingPlanet);
3234 bay.ship.reset();
3235 }
3236 }
3237
3238
3239
Bays() const3240 const vector<Ship::Bay> &Ship::Bays() const
3241 {
3242 return bays;
3243 }
3244
3245
3246
3247 // Adjust the positions and velocities of any visible carried fighters or
3248 // drones. If any are visible, return true.
PositionFighters() const3249 bool Ship::PositionFighters() const
3250 {
3251 bool hasVisible = false;
3252 for(const Bay &bay : bays)
3253 if(bay.ship && bay.side)
3254 {
3255 hasVisible = true;
3256 bay.ship->position = angle.Rotate(bay.point) * Zoom() + position;
3257 bay.ship->velocity = velocity;
3258 bay.ship->angle = angle + bay.facing;
3259 bay.ship->zoom = zoom;
3260 }
3261 return hasVisible;
3262 }
3263
3264
3265
Cargo()3266 CargoHold &Ship::Cargo()
3267 {
3268 return cargo;
3269 }
3270
3271
3272
Cargo() const3273 const CargoHold &Ship::Cargo() const
3274 {
3275 return cargo;
3276 }
3277
3278
3279
3280 // Display box effects from jettisoning this much cargo.
Jettison(const string & commodity,int tons)3281 void Ship::Jettison(const string &commodity, int tons)
3282 {
3283 cargo.Remove(commodity, tons);
3284
3285 // Jettisoned cargo must carry some of the ship's heat with it. Otherwise
3286 // jettisoning cargo would increase the ship's temperature.
3287 heat -= tons * MAXIMUM_TEMPERATURE * Heat();
3288
3289 for( ; tons > 0; tons -= Flotsam::TONS_PER_BOX)
3290 jettisoned.emplace_back(new Flotsam(commodity, (Flotsam::TONS_PER_BOX < tons) ? Flotsam::TONS_PER_BOX : tons));
3291 }
3292
3293
3294
Jettison(const Outfit * outfit,int count)3295 void Ship::Jettison(const Outfit *outfit, int count)
3296 {
3297 if(count < 0)
3298 return;
3299
3300 cargo.Remove(outfit, count);
3301
3302 // Jettisoned cargo must carry some of the ship's heat with it. Otherwise
3303 // jettisoning cargo would increase the ship's temperature.
3304 double mass = outfit->Mass();
3305 heat -= count * mass * MAXIMUM_TEMPERATURE * Heat();
3306
3307 const int perBox = (mass <= 0.) ? count : (mass > Flotsam::TONS_PER_BOX) ? 1 : static_cast<int>(Flotsam::TONS_PER_BOX / mass);
3308 while(count > 0)
3309 {
3310 jettisoned.emplace_back(new Flotsam(outfit, (perBox < count) ? perBox : count));
3311 count -= perBox;
3312 }
3313 }
3314
3315
3316
Attributes() const3317 const Outfit &Ship::Attributes() const
3318 {
3319 return attributes;
3320 }
3321
3322
3323
BaseAttributes() const3324 const Outfit &Ship::BaseAttributes() const
3325 {
3326 return baseAttributes;
3327 }
3328
3329
3330
3331 // Get outfit information.
Outfits() const3332 const map<const Outfit *, int> &Ship::Outfits() const
3333 {
3334 return outfits;
3335 }
3336
3337
3338
OutfitCount(const Outfit * outfit) const3339 int Ship::OutfitCount(const Outfit *outfit) const
3340 {
3341 auto it = outfits.find(outfit);
3342 return (it == outfits.end()) ? 0 : it->second;
3343 }
3344
3345
3346
3347 // Add or remove outfits. (To remove, pass a negative number.)
AddOutfit(const Outfit * outfit,int count)3348 void Ship::AddOutfit(const Outfit *outfit, int count)
3349 {
3350 if(outfit && count)
3351 {
3352 auto it = outfits.find(outfit);
3353 if(it == outfits.end())
3354 outfits[outfit] = count;
3355 else
3356 {
3357 it->second += count;
3358 if(!it->second)
3359 outfits.erase(it);
3360 }
3361 attributes.Add(*outfit, count);
3362 if(outfit->IsWeapon())
3363 armament.Add(outfit, count);
3364
3365 if(outfit->Get("cargo space"))
3366 cargo.SetSize(attributes.Get("cargo space"));
3367 if(outfit->Get("hull"))
3368 hull += outfit->Get("hull") * count;
3369 // If the added or removed outfit is a jump drive, recalculate
3370 // and cache this ship's jump range.
3371 if(outfit->Get("jump drive"))
3372 jumpRange = JumpRange(false);
3373 }
3374 }
3375
3376
3377
3378 // Get the list of weapons.
GetArmament()3379 Armament &Ship::GetArmament()
3380 {
3381 return armament;
3382 }
3383
3384
3385
Weapons() const3386 const vector<Hardpoint> &Ship::Weapons() const
3387 {
3388 return armament.Get();
3389 }
3390
3391
3392
3393 // Check if we are able to fire the given weapon (i.e. there is enough
3394 // energy, ammo, and fuel to fire it).
CanFire(const Weapon * weapon) const3395 bool Ship::CanFire(const Weapon *weapon) const
3396 {
3397 if(!weapon || !weapon->IsWeapon())
3398 return false;
3399
3400 if(weapon->Ammo())
3401 {
3402 auto it = outfits.find(weapon->Ammo());
3403 if(it == outfits.end() || it->second < weapon->AmmoUsage())
3404 return false;
3405 }
3406
3407 if(energy < weapon->FiringEnergy() + weapon->RelativeFiringEnergy() * attributes.Get("energy capacity"))
3408 return false;
3409 if(fuel < weapon->FiringFuel() + weapon->RelativeFiringFuel() * attributes.Get("fuel capacity"))
3410 return false;
3411 // We do check hull, but we don't check shields. Ships can survive with all shields depleted.
3412 // Ships should not disable themselves, so we check if we stay above minimumHull.
3413 if(hull - MinimumHull() < weapon->FiringHull() + weapon->RelativeFiringHull() * attributes.Get("hull"))
3414 return false;
3415
3416 // If a weapon requires heat to fire, (rather than generating heat), we must
3417 // have enough heat to spare.
3418 if(heat < -(weapon->FiringHeat() + weapon->RelativeFiringHeat() * MaximumHeat()))
3419 return false;
3420 // Repeat this for various effects which shouldn't drop below 0.
3421 if(ionization < -weapon->FiringIon())
3422 return false;
3423 if(disruption < -weapon->FiringDisruption())
3424 return false;
3425 if(slowness < -weapon->FiringSlowing())
3426 return false;
3427
3428 return true;
3429 }
3430
3431
3432
3433 // Fire the given weapon (i.e. deduct whatever energy, ammo, hull, shields
3434 // or fuel it uses and add whatever heat it generates. Assume that CanFire()
3435 // is true.
ExpendAmmo(const Weapon * weapon)3436 void Ship::ExpendAmmo(const Weapon *weapon)
3437 {
3438 if(!weapon)
3439 return;
3440 if(weapon->Ammo())
3441 AddOutfit(weapon->Ammo(), -weapon->AmmoUsage());
3442
3443 energy -= weapon->FiringEnergy() + weapon->RelativeFiringEnergy() * attributes.Get("energy capacity");
3444 fuel -= weapon->FiringFuel() + weapon->RelativeFiringFuel() * attributes.Get("fuel capacity");
3445 heat += weapon->FiringHeat() + weapon->RelativeFiringHeat() * MaximumHeat();
3446 // Weapons fire from within shields, so hull damage goes directly into the hull, while shield damage
3447 // only affects shields.
3448 hull -= weapon->FiringHull() + weapon->RelativeFiringHull() * attributes.Get("hull");
3449 shields -= weapon->FiringShields() + weapon->RelativeFiringShields() * attributes.Get("shields");
3450
3451 // Those values are usually reduced by active shields, but weapons fire from within the shields, so
3452 // it seems more appropriate to apply those damages with a factor 1 directly.
3453 ionization += weapon->FiringIon();
3454 disruption += weapon->FiringDisruption();
3455 slowness += weapon->FiringSlowing();
3456 }
3457
3458
3459
3460 // Each ship can have a target system (to travel to), a target planet (to
3461 // land on) and a target ship (to move to, and attack if hostile).
GetTargetShip() const3462 shared_ptr<Ship> Ship::GetTargetShip() const
3463 {
3464 return targetShip.lock();
3465 }
3466
3467
3468
GetShipToAssist() const3469 shared_ptr<Ship> Ship::GetShipToAssist() const
3470 {
3471 return shipToAssist.lock();
3472 }
3473
3474
3475
GetTargetStellar() const3476 const StellarObject *Ship::GetTargetStellar() const
3477 {
3478 return targetPlanet;
3479 }
3480
3481
3482
GetTargetSystem() const3483 const System *Ship::GetTargetSystem() const
3484 {
3485 return (targetSystem == currentSystem) ? nullptr : targetSystem;
3486 }
3487
3488
3489
3490 // Mining target.
GetTargetAsteroid() const3491 shared_ptr<Minable> Ship::GetTargetAsteroid() const
3492 {
3493 return targetAsteroid.lock();
3494 }
3495
3496
3497
GetTargetFlotsam() const3498 shared_ptr<Flotsam> Ship::GetTargetFlotsam() const
3499 {
3500 return targetFlotsam.lock();
3501 }
3502
3503
3504
3505 // Set this ship's targets.
SetTargetShip(const shared_ptr<Ship> & ship)3506 void Ship::SetTargetShip(const shared_ptr<Ship> &ship)
3507 {
3508 if(ship != GetTargetShip())
3509 {
3510 targetShip = ship;
3511 // When you change targets, clear your scanning records.
3512 cargoScan = 0.;
3513 outfitScan = 0.;
3514 }
3515 targetAsteroid.reset();
3516 }
3517
3518
3519
SetShipToAssist(const shared_ptr<Ship> & ship)3520 void Ship::SetShipToAssist(const shared_ptr<Ship> &ship)
3521 {
3522 shipToAssist = ship;
3523 }
3524
3525
3526
SetTargetStellar(const StellarObject * object)3527 void Ship::SetTargetStellar(const StellarObject *object)
3528 {
3529 targetPlanet = object;
3530 }
3531
3532
3533
SetTargetSystem(const System * system)3534 void Ship::SetTargetSystem(const System *system)
3535 {
3536 targetSystem = system;
3537 }
3538
3539
3540
3541 // Mining target.
SetTargetAsteroid(const shared_ptr<Minable> & asteroid)3542 void Ship::SetTargetAsteroid(const shared_ptr<Minable> &asteroid)
3543 {
3544 targetAsteroid = asteroid;
3545 targetShip.reset();
3546 }
3547
3548
3549
SetTargetFlotsam(const shared_ptr<Flotsam> & flotsam)3550 void Ship::SetTargetFlotsam(const shared_ptr<Flotsam> &flotsam)
3551 {
3552 targetFlotsam = flotsam;
3553 }
3554
3555
3556
SetParent(const shared_ptr<Ship> & ship)3557 void Ship::SetParent(const shared_ptr<Ship> &ship)
3558 {
3559 shared_ptr<Ship> oldParent = parent.lock();
3560 if(oldParent)
3561 oldParent->RemoveEscort(*this);
3562
3563 parent = ship;
3564 if(ship)
3565 ship->AddEscort(*this);
3566 }
3567
3568
3569
GetParent() const3570 shared_ptr<Ship> Ship::GetParent() const
3571 {
3572 return parent.lock();
3573 }
3574
3575
3576
GetEscorts() const3577 const vector<weak_ptr<Ship>> &Ship::GetEscorts() const
3578 {
3579 return escorts;
3580 }
3581
3582
3583
3584 // Add escorts to this ship. Escorts look to the parent ship for movement
3585 // cues and try to stay with it when it lands or goes into hyperspace.
AddEscort(Ship & ship)3586 void Ship::AddEscort(Ship &ship)
3587 {
3588 escorts.push_back(ship.shared_from_this());
3589 }
3590
3591
3592
RemoveEscort(const Ship & ship)3593 void Ship::RemoveEscort(const Ship &ship)
3594 {
3595 auto it = escorts.begin();
3596 for( ; it != escorts.end(); ++it)
3597 if(it->lock().get() == &ship)
3598 {
3599 escorts.erase(it);
3600 return;
3601 }
3602 }
3603
3604
3605
MinimumHull() const3606 double Ship::MinimumHull() const
3607 {
3608 if(neverDisabled)
3609 return 0.;
3610
3611 double maximumHull = attributes.Get("hull");
3612 double absoluteThreshold = attributes.Get("absolute threshold");
3613 if(absoluteThreshold > 0.)
3614 return absoluteThreshold;
3615
3616 double thresholdPercent = attributes.Get("threshold percentage");
3617 double minimumHull = maximumHull * (thresholdPercent > 0. ? min(thresholdPercent, 1.) : max(.15, min(.45, 10. / sqrt(maximumHull))));
3618
3619 return max(0., floor(minimumHull + attributes.Get("hull threshold")));
3620 }
3621
3622
3623
3624 // Find out how much fuel is consumed by the hyperdrive of the given type.
BestFuel(const string & type,const string & subtype,double defaultFuel,double jumpDistance) const3625 double Ship::BestFuel(const string &type, const string &subtype, double defaultFuel, double jumpDistance) const
3626 {
3627 // Find the outfit that provides the least costly hyperjump.
3628 double best = 0.;
3629 // Make it possible for a hyperdrive to be integrated into a ship.
3630 if(baseAttributes.Get(type) && (subtype.empty() || baseAttributes.Get(subtype)))
3631 {
3632 // If a distance was given, then we know that we are making a jump.
3633 // Only use the fuel from a jump drive if it is capable of making
3634 // the given jump. We can guarantee that at least one jump drive
3635 // is capable of making the given jump, as the destination must
3636 // be among the neighbors of the current system.
3637 double jumpRange = baseAttributes.Get("jump range");
3638 if(!jumpRange)
3639 jumpRange = System::DEFAULT_NEIGHBOR_DISTANCE;
3640 // If no distance was given then we're either using a hyperdrive
3641 // or refueling this ship, in which case this if statement will
3642 // always pass.
3643 if(jumpRange >= jumpDistance)
3644 {
3645 best = baseAttributes.Get("jump fuel");
3646 if(!best)
3647 best = defaultFuel;
3648 }
3649 }
3650 // Search through all the outfits.
3651 for(const auto &it : outfits)
3652 if(it.first->Get(type) && (subtype.empty() || it.first->Get(subtype)))
3653 {
3654 double jumpRange = it.first->Get("jump range");
3655 if(!jumpRange)
3656 jumpRange = System::DEFAULT_NEIGHBOR_DISTANCE;
3657 if(jumpRange >= jumpDistance)
3658 {
3659 double fuel = it.first->Get("jump fuel");
3660 if(!fuel)
3661 fuel = defaultFuel;
3662 if(!best || fuel < best)
3663 best = fuel;
3664 }
3665 }
3666 return best;
3667 }
3668
3669
3670
CreateExplosion(vector<Visual> & visuals,bool spread)3671 void Ship::CreateExplosion(vector<Visual> &visuals, bool spread)
3672 {
3673 if(!HasSprite() || !GetMask().IsLoaded() || explosionEffects.empty())
3674 return;
3675
3676 // Bail out if this loops enough times, just in case.
3677 for(int i = 0; i < 10; ++i)
3678 {
3679 Point point((Random::Real() - .5) * Width(),
3680 (Random::Real() - .5) * Height());
3681 if(GetMask().Contains(point, Angle()))
3682 {
3683 // Pick an explosion.
3684 int type = Random::Int(explosionTotal);
3685 auto it = explosionEffects.begin();
3686 for( ; it != explosionEffects.end(); ++it)
3687 {
3688 type -= it->second;
3689 if(type < 0)
3690 break;
3691 }
3692 Point effectVelocity = velocity;
3693 if(spread)
3694 {
3695 double scale = .04 * (Width() + Height());
3696 effectVelocity += Angle::Random().Unit() * (scale * Random::Real());
3697 }
3698 visuals.emplace_back(*it->first, angle.Rotate(point) + position, std::move(effectVelocity), angle);
3699 ++explosionCount;
3700 return;
3701 }
3702 }
3703 }
3704
3705
3706
3707 // Place a "spark" effect, like ionization or disruption.
CreateSparks(vector<Visual> & visuals,const string & name,double amount)3708 void Ship::CreateSparks(vector<Visual> &visuals, const string &name, double amount)
3709 {
3710 CreateSparks(visuals, GameData::Effects().Get(name), amount);
3711 }
3712
3713
3714
CreateSparks(vector<Visual> & visuals,const Effect * effect,double amount)3715 void Ship::CreateSparks(vector<Visual> &visuals, const Effect *effect, double amount)
3716 {
3717 if(forget)
3718 return;
3719
3720 // Limit the number of sparks, depending on the size of the sprite.
3721 amount = min(amount, Width() * Height() * .0006);
3722 // Preallocate capacity, in case we're adding a non-trivial number of sparks.
3723 visuals.reserve(visuals.size() + static_cast<int>(amount));
3724
3725 while(true)
3726 {
3727 amount -= Random::Real();
3728 if(amount <= 0.)
3729 break;
3730
3731 Point point((Random::Real() - .5) * Width(),
3732 (Random::Real() - .5) * Height());
3733 if(GetMask().Contains(point, Angle()))
3734 visuals.emplace_back(*effect, angle.Rotate(point) + position, velocity, angle);
3735 }
3736 }
3737
3738
3739
3740 // A helper method for taking damage from either a projectile or a hazard.
TakeDamage(const Weapon & weapon,double damageScaling,double distanceTraveled,const Point & damagePosition,bool isBlast)3741 int Ship::TakeDamage(const Weapon &weapon, double damageScaling, double distanceTraveled, const Point &damagePosition, bool isBlast)
3742 {
3743 if(isBlast && weapon.IsDamageScaled())
3744 {
3745 // Scale blast damage based on the distance from the blast
3746 // origin and if the projectile uses a trigger radius. The
3747 // point of contact must be measured on the sprite outline.
3748 // scale = (1 + (tr / (2 * br))^2) / (1 + r^4)^2
3749 double blastRadius = max(1., weapon.BlastRadius());
3750 double radiusRatio = weapon.TriggerRadius() / blastRadius;
3751 double k = !radiusRatio ? 1. : (1. + .25 * radiusRatio * radiusRatio);
3752 // Rather than exactly compute the distance between the explosion and
3753 // the closest point on the ship, estimate it using the mask's Radius.
3754 double d = max(0., (damagePosition - position).Length() - GetMask().Radius());
3755 double rSquared = d * d / (blastRadius * blastRadius);
3756 damageScaling *= k / ((1. + rSquared * rSquared) * (1. + rSquared * rSquared));
3757 }
3758 if(weapon.HasDamageDropoff())
3759 damageScaling *= weapon.DamageDropoff(distanceTraveled);
3760
3761 double shieldDamage = (weapon.ShieldDamage() + weapon.RelativeShieldDamage() * attributes.Get("shields"))
3762 * damageScaling / (1. + attributes.Get("shield protection"));
3763 double hullDamage = (weapon.HullDamage() + weapon.RelativeHullDamage() * attributes.Get("hull"))
3764 * damageScaling / (1. + attributes.Get("hull protection"));
3765 double energyDamage = (weapon.EnergyDamage() + weapon.RelativeEnergyDamage() * attributes.Get("energy capacity"))
3766 * damageScaling / (1. + attributes.Get("energy protection"));
3767 double fuelDamage = (weapon.FuelDamage() + weapon.RelativeFuelDamage() * attributes.Get("fuel capacity"))
3768 * damageScaling / (1. + attributes.Get("fuel protection"));
3769 double heatDamage = (weapon.HeatDamage() + weapon.RelativeHeatDamage() * MaximumHeat())
3770 * damageScaling / (1. + attributes.Get("heat protection"));
3771 double ionDamage = weapon.IonDamage() * damageScaling / (1. + attributes.Get("ion protection"));
3772 double disruptionDamage = weapon.DisruptionDamage() * damageScaling / (1. + attributes.Get("disruption protection"));
3773 double slowingDamage = weapon.SlowingDamage() * damageScaling / (1. + attributes.Get("slowing protection"));
3774 double hitForce = weapon.HitForce() * damageScaling / (1. + attributes.Get("force protection"));
3775 bool wasDisabled = IsDisabled();
3776 bool wasDestroyed = IsDestroyed();
3777
3778 double shieldFraction = 1. - max(0., min(1., weapon.Piercing() / (1. + attributes.Get("piercing protection")) - attributes.Get("piercing resistance")));
3779 shieldFraction *= 1. / (1. + disruption * .01);
3780 if(shields <= 0.)
3781 shieldFraction = 0.;
3782 else if(shieldDamage > shields)
3783 shieldFraction = min(shieldFraction, shields / shieldDamage);
3784 shields -= shieldDamage * shieldFraction;
3785 if(shieldDamage && !isDisabled)
3786 {
3787 int disabledDelay = static_cast<int>(attributes.Get("depleted shield delay"));
3788 shieldDelay = max(shieldDelay, (shields <= 0. && disabledDelay) ? disabledDelay : static_cast<int>(attributes.Get("shield delay")));
3789 }
3790 hull -= hullDamage * (1. - shieldFraction);
3791 if(hullDamage && !isDisabled)
3792 hullDelay = max(hullDelay, static_cast<int>(attributes.Get("repair delay")));
3793 // For the following damage types, the total effect depends on how much is
3794 // "leaking" through the shields.
3795 double leakage = (1. - .5 * shieldFraction);
3796 // Code in Ship::Move() will handle making sure the fuel and energy amounts
3797 // stays in the allowable ranges.
3798 energy -= energyDamage * leakage;
3799 fuel -= fuelDamage * leakage;
3800 heat += heatDamage * leakage;
3801 ionization += ionDamage * leakage;
3802 disruption += disruptionDamage * leakage;
3803 slowness += slowingDamage * leakage;
3804
3805 if(hitForce)
3806 {
3807 Point d = position - damagePosition;
3808 double distance = d.Length();
3809 if(distance)
3810 ApplyForce((hitForce / distance) * d, weapon.IsGravitational());
3811 }
3812
3813 // Recalculate the disabled ship check.
3814 isDisabled = true;
3815 isDisabled = IsDisabled();
3816
3817 // Report what happened to this ship from this weapon.
3818 int type = 0;
3819 if(!wasDisabled && isDisabled)
3820 {
3821 type |= ShipEvent::DISABLE;
3822 hullDelay = max(hullDelay, static_cast<int>(attributes.Get("disabled repair delay")));
3823 }
3824 if(!wasDestroyed && IsDestroyed())
3825 type |= ShipEvent::DESTROY;
3826
3827 // Inflicted heat damage may also disable a ship, but does not trigger a "DISABLE" event.
3828 if(heat > MaximumHeat())
3829 {
3830 isOverheated = true;
3831 isDisabled = true;
3832 }
3833 else if(heat < .9 * MaximumHeat())
3834 isOverheated = false;
3835
3836 return type;
3837 }
3838