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