1 /* Outfit.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 "Outfit.h"
14 
15 #include "Audio.h"
16 #include "Body.h"
17 #include "DataNode.h"
18 #include "Effect.h"
19 #include "GameData.h"
20 #include "SpriteSet.h"
21 
22 #include <algorithm>
23 #include <cmath>
24 
25 using namespace std;
26 
27 namespace {
28 	const double EPS = 0.0000000001;
29 
30 	// A mapping of attribute names to specifically-allowed minimum values. Based on the
31 	// specific usage of the attribute, the allowed minimum value is chosen to avoid
32 	// disallowed or undesirable behaviors (such as dividing by zero).
33 	const auto MINIMUM_OVERRIDES = map<string, double>{
34 		// Attributes which are present and map to zero may have any value.
35 		{"cooling energy", 0.},
36 		{"hull energy", 0.},
37 		{"hull fuel", 0.},
38 		{"hull heat", 0.},
39 		{"hull threshold", 0.},
40 		{"shield energy", 0.},
41 		{"shield fuel", 0.},
42 		{"shield heat", 0.},
43 		{"disruption resistance energy", 0.},
44 		{"disruption resistance fuel", 0.},
45 		{"disruption resistance heat", 0.},
46 		{"ion resistance energy", 0.},
47 		{"ion resistance fuel", 0.},
48 		{"ion resistance heat", 0.},
49 		{"slowing resistance energy", 0.},
50 		{"slowing resistance fuel", 0.},
51 		{"slowing resistance heat", 0.},
52 
53 		// "Protection" attributes appear in denominators and are incremented by 1.
54 		{"disruption protection", -0.99},
55 		{"energy protection", -0.99},
56 		{"force protection", -0.99},
57 		{"fuel protection", -0.99},
58 		{"heat protection", -0.99},
59 		{"hull protection", -0.99},
60 		{"ion protection", -0.99},
61 		{"piercing protection", -0.99},
62 		{"shield protection", -0.99},
63 		{"slowing protection", -0.99},
64 
65 		// "Multiplier" attributes appear in numerators and are incremented by 1.
66 		{"hull repair multiplier", -1.},
67 		{"hull energy multiplier", -1.},
68 		{"hull fuel multiplier", -1.},
69 		{"hull heat multiplier", -1.},
70 		{"shield generation multiplier", -1.},
71 		{"shield energy multiplier", -1.},
72 		{"shield fuel multiplier", -1.},
73 		{"shield heat multiplier", -1.}
74 	};
75 
AddFlareSprites(vector<pair<Body,int>> & thisFlares,const pair<Body,int> & it,int count)76 	void AddFlareSprites(vector<pair<Body, int>> &thisFlares, const pair<Body, int> &it, int count)
77 	{
78 		auto oit = find_if(thisFlares.begin(), thisFlares.end(),
79 			[&it](const pair<Body, int> &flare)
80 			{
81 				return it.first.GetSprite() == flare.first.GetSprite();
82 			}
83 		);
84 
85 		if(oit == thisFlares.end())
86 			thisFlares.emplace_back(it.first, count * it.second);
87 		else
88 			oit->second += count * it.second;
89 	}
90 
91 	// Used to add the contents of one outfit's map to another, while also
92 	// erasing any key with a value of zero.
93 	template <class T>
MergeMaps(map<const T *,int> & thisMap,const map<const T *,int> & otherMap,int count)94 	void MergeMaps(map<const T *, int> &thisMap, const map<const T *, int> &otherMap, int count)
95 	{
96 		for(const auto &it : otherMap)
97 		{
98 			thisMap[it.first] += count * it.second;
99 			if(thisMap[it.first] == 0)
100 				thisMap.erase(it.first);
101 		}
102 	}
103 }
104 
105 const vector<string> Outfit::CATEGORIES = {
106 	"Guns",
107 	"Turrets",
108 	"Secondary Weapons",
109 	"Ammunition",
110 	"Systems",
111 	"Power",
112 	"Engines",
113 	"Hand to Hand",
114 	"Special"
115 };
116 
117 
118 
Load(const DataNode & node)119 void Outfit::Load(const DataNode &node)
120 {
121 	if(node.Size() >= 2)
122 	{
123 		name = node.Token(1);
124 		pluralName = name + 's';
125 	}
126 	isDefined = true;
127 
128 	for(const DataNode &child : node)
129 	{
130 		if(child.Token(0) == "category" && child.Size() >= 2)
131 			category = child.Token(1);
132 		else if(child.Token(0) == "plural" && child.Size() >= 2)
133 			pluralName = child.Token(1);
134 		else if(child.Token(0) == "flare sprite" && child.Size() >= 2)
135 		{
136 			flareSprites.emplace_back(Body(), 1);
137 			flareSprites.back().first.LoadSprite(child);
138 		}
139 		else if(child.Token(0) == "reverse flare sprite" && child.Size() >= 2)
140 		{
141 			reverseFlareSprites.emplace_back(Body(), 1);
142 			reverseFlareSprites.back().first.LoadSprite(child);
143 		}
144 		else if(child.Token(0) == "steering flare sprite" && child.Size() >= 2)
145 		{
146 			steeringFlareSprites.emplace_back(Body(), 1);
147 			steeringFlareSprites.back().first.LoadSprite(child);
148 		}
149 		else if(child.Token(0) == "flare sound" && child.Size() >= 2)
150 			++flareSounds[Audio::Get(child.Token(1))];
151 		else if(child.Token(0) == "reverse flare sound" && child.Size() >= 2)
152 			++reverseFlareSounds[Audio::Get(child.Token(1))];
153 		else if(child.Token(0) == "steering flare sound" && child.Size() >= 2)
154 			++steeringFlareSounds[Audio::Get(child.Token(1))];
155 		else if(child.Token(0) == "afterburner effect" && child.Size() >= 2)
156 			++afterburnerEffects[GameData::Effects().Get(child.Token(1))];
157 		else if(child.Token(0) == "jump effect" && child.Size() >= 2)
158 			++jumpEffects[GameData::Effects().Get(child.Token(1))];
159 		else if(child.Token(0) == "hyperdrive sound" && child.Size() >= 2)
160 			++hyperSounds[Audio::Get(child.Token(1))];
161 		else if(child.Token(0) == "hyperdrive in sound" && child.Size() >= 2)
162 			++hyperInSounds[Audio::Get(child.Token(1))];
163 		else if(child.Token(0) == "hyperdrive out sound" && child.Size() >= 2)
164 			++hyperOutSounds[Audio::Get(child.Token(1))];
165 		else if(child.Token(0) == "jump sound" && child.Size() >= 2)
166 			++jumpSounds[Audio::Get(child.Token(1))];
167 		else if(child.Token(0) == "jump in sound" && child.Size() >= 2)
168 			++jumpInSounds[Audio::Get(child.Token(1))];
169 		else if(child.Token(0) == "jump out sound" && child.Size() >= 2)
170 			++jumpOutSounds[Audio::Get(child.Token(1))];
171 		else if(child.Token(0) == "flotsam sprite" && child.Size() >= 2)
172 			flotsamSprite = SpriteSet::Get(child.Token(1));
173 		else if(child.Token(0) == "thumbnail" && child.Size() >= 2)
174 			thumbnail = SpriteSet::Get(child.Token(1));
175 		else if(child.Token(0) == "weapon")
176 			LoadWeapon(child);
177 		else if(child.Token(0) == "ammo" && child.Size() >= 2)
178 		{
179 			// Non-weapon outfits can have ammo so that storage outfits
180 			// properly remove excess ammo when the storage is sold, instead
181 			// of blocking the sale of the outfit until the ammo is sold first.
182 			ammo = make_pair(GameData::Outfits().Get(child.Token(1)), 0);
183 		}
184 		else if(child.Token(0) == "description" && child.Size() >= 2)
185 		{
186 			description += child.Token(1);
187 			description += '\n';
188 		}
189 		else if(child.Token(0) == "cost" && child.Size() >= 2)
190 			cost = child.Value(1);
191 		else if(child.Token(0) == "mass" && child.Size() >= 2)
192 			mass = child.Value(1);
193 		else if(child.Token(0) == "licenses")
194 		{
195 			for(const DataNode &grand : child)
196 				licenses.push_back(grand.Token(0));
197 		}
198 		else if(child.Token(0) == "jump range" && child.Size() >= 2)
199 		{
200 			// Jump range must be positive.
201 			attributes[child.Token(0)] = max(0., child.Value(1));
202 		}
203 		else if(child.Size() >= 2)
204 			attributes[child.Token(0)] = child.Value(1);
205 		else
206 			child.PrintTrace("Skipping unrecognized attribute:");
207 	}
208 
209 	// Only outfits with the jump drive and jump range attributes can
210 	// use the jump range, so only keep track of the jump range on
211 	// viable outfits.
212 	if(attributes.Get("jump drive") && attributes.Get("jump range"))
213 		GameData::AddJumpRange(attributes.Get("jump range"));
214 
215 	// Legacy support for turrets that don't specify a turn rate:
216 	if(IsWeapon() && attributes.Get("turret mounts") && !TurretTurn() && !AntiMissile())
217 	{
218 		SetTurretTurn(4.);
219 		node.PrintTrace("Warning: Deprecated use of a turret without specified \"turret turn\":");
220 	}
221 	// Convert any legacy cargo / outfit scan definitions into power & speed,
222 	// so no runtime code has to check for both.
223 	auto convertScan = [&](string &&kind) -> void
224 	{
225 		const string label = kind + " scan";
226 		double initial = attributes.Get(label);
227 		if(initial)
228 		{
229 			attributes[label] = 0.;
230 			node.PrintTrace("Warning: Deprecated use of \"" + label + "\" instead of \""
231 					+ label + " power\" and \"" + label + " speed\":");
232 
233 			// A scan value of 300 is equivalent to a scan power of 9.
234 			attributes[label + " power"] += initial * initial * .0001;
235 			// The default scan speed of 1 is unrelated to the magnitude of the scan value.
236 			// It may have been already specified, and if so, should not be increased.
237 			if(!attributes.Get(label + " speed"))
238 				attributes[label + " speed"] = 1.;
239 		}
240 	};
241 	convertScan("outfit");
242 	convertScan("cargo");
243 }
244 
245 
246 
247 // Check if this outfit has been defined via Outfit::Load (vs. only being referred to).
IsDefined() const248 bool Outfit::IsDefined() const
249 {
250 	return isDefined;
251 }
252 
253 
254 
255 // When writing to the player's save, the reference name is used even if this
256 // outfit was not fully defined (i.e. belongs to an inactive plugin).
Name() const257 const string &Outfit::Name() const
258 {
259 	return name;
260 }
261 
262 
263 
SetName(const string & name)264 void Outfit::SetName(const string &name)
265 {
266 	this->name = name;
267 }
268 
269 
270 
PluralName() const271 const string &Outfit::PluralName() const
272 {
273 	return pluralName;
274 }
275 
276 
277 
Category() const278 const string &Outfit::Category() const
279 {
280 	return category;
281 }
282 
283 
284 
Description() const285 const string &Outfit::Description() const
286 {
287 	return description;
288 }
289 
290 
291 
292 // Get the licenses needed to purchase this outfit.
Licenses() const293 const vector<string> &Outfit::Licenses() const
294 {
295 	return licenses;
296 }
297 
298 
299 
300 // Get the image to display in the outfitter when buying this item.
Thumbnail() const301 const Sprite *Outfit::Thumbnail() const
302 {
303 	return thumbnail;
304 }
305 
306 
307 
Get(const char * attribute) const308 double Outfit::Get(const char *attribute) const
309 {
310 	return attributes.Get(attribute);
311 }
312 
313 
314 
Get(const string & attribute) const315 double Outfit::Get(const string &attribute) const
316 {
317 	return Get(attribute.c_str());
318 }
319 
320 
321 
Attributes() const322 const Dictionary &Outfit::Attributes() const
323 {
324 	return attributes;
325 }
326 
327 
328 
329 // Determine whether the given number of instances of the given outfit can
330 // be added to a ship with the attributes represented by this instance. If
331 // not, return the maximum number that can be added.
CanAdd(const Outfit & other,int count) const332 int Outfit::CanAdd(const Outfit &other, int count) const
333 {
334 	for(const auto &at : other.attributes)
335 	{
336 		// The minimum allowed value of most attributes is 0. Some attributes
337 		// have special functionality when negative, though, and are therefore
338 		// allowed to have values less than 0.
339 		double minimum = 0.;
340 		auto it = MINIMUM_OVERRIDES.find(at.first);
341 		if(it != MINIMUM_OVERRIDES.end())
342 		{
343 			minimum = it->second;
344 			// An override of exactly 0 means the attribute may have any value.
345 			if(!minimum)
346 				continue;
347 		}
348 		double value = Get(at.first);
349 		// Allow for rounding errors:
350 		if(value + at.second * count < minimum - EPS)
351 			count = (value - minimum) / -at.second + EPS;
352 	}
353 
354 	return count;
355 }
356 
357 
358 
359 // For tracking a combination of outfits in a ship: add the given number of
360 // instances of the given outfit to this outfit.
Add(const Outfit & other,int count)361 void Outfit::Add(const Outfit &other, int count)
362 {
363 	cost += other.cost * count;
364 	mass += other.mass * count;
365 	for(const auto &at : other.attributes)
366 	{
367 		attributes[at.first] += at.second * count;
368 		if(fabs(attributes[at.first]) < EPS)
369 			attributes[at.first] = 0.;
370 	}
371 
372 	for(const auto &it : other.flareSprites)
373 		AddFlareSprites(flareSprites, it, count);
374 	for(const auto &it : other.reverseFlareSprites)
375 		AddFlareSprites(reverseFlareSprites, it, count);
376 	for(const auto &it : other.steeringFlareSprites)
377 		AddFlareSprites(steeringFlareSprites, it, count);
378 	MergeMaps(flareSounds, other.flareSounds, count);
379 	MergeMaps(reverseFlareSounds, other.reverseFlareSounds, count);
380 	MergeMaps(steeringFlareSounds, other.steeringFlareSounds, count);
381 	MergeMaps(afterburnerEffects, other.afterburnerEffects, count);
382 	MergeMaps(jumpEffects, other.jumpEffects, count);
383 	MergeMaps(hyperSounds, other.hyperSounds, count);
384 	MergeMaps(hyperInSounds, other.hyperInSounds, count);
385 	MergeMaps(hyperOutSounds, other.hyperOutSounds, count);
386 	MergeMaps(jumpSounds, other.jumpSounds, count);
387 	MergeMaps(jumpInSounds, other.jumpInSounds, count);
388 	MergeMaps(jumpOutSounds, other.jumpOutSounds, count);
389 }
390 
391 
392 
393 // Modify this outfit's attributes.
Set(const char * attribute,double value)394 void Outfit::Set(const char *attribute, double value)
395 {
396 	attributes[attribute] = value;
397 }
398 
399 
400 
401 // Get this outfit's engine flare sprite, if any.
FlareSprites() const402 const vector<pair<Body, int>> &Outfit::FlareSprites() const
403 {
404 	return flareSprites;
405 }
406 
407 
408 
ReverseFlareSprites() const409 const vector<pair<Body, int>> &Outfit::ReverseFlareSprites() const
410 {
411 	return reverseFlareSprites;
412 }
413 
414 
415 
SteeringFlareSprites() const416 const vector<pair<Body, int>> &Outfit::SteeringFlareSprites() const
417 {
418 	return steeringFlareSprites;
419 }
420 
421 
422 
FlareSounds() const423 const map<const Sound *, int> &Outfit::FlareSounds() const
424 {
425 	return flareSounds;
426 }
427 
428 
429 
ReverseFlareSounds() const430 const map<const Sound *, int> &Outfit::ReverseFlareSounds() const
431 {
432 	return reverseFlareSounds;
433 }
434 
435 
436 
SteeringFlareSounds() const437 const map<const Sound *, int> &Outfit::SteeringFlareSounds() const
438 {
439 	return steeringFlareSounds;
440 }
441 
442 
443 
444 // Get the afterburner effect, if any.
AfterburnerEffects() const445 const map<const Effect *, int> &Outfit::AfterburnerEffects() const
446 {
447 	return afterburnerEffects;
448 }
449 
450 
451 
452 // Get this oufit's jump effects and sounds, if any.
JumpEffects() const453 const map<const Effect *, int> &Outfit::JumpEffects() const
454 {
455 	return jumpEffects;
456 }
457 
458 
459 
HyperSounds() const460 const map<const Sound *, int> &Outfit::HyperSounds() const
461 {
462 	return hyperSounds;
463 }
464 
465 
466 
HyperInSounds() const467 const map<const Sound *, int> &Outfit::HyperInSounds() const
468 {
469 	return hyperInSounds;
470 }
471 
472 
473 
HyperOutSounds() const474 const map<const Sound *, int> &Outfit::HyperOutSounds() const
475 {
476 	return hyperOutSounds;
477 }
478 
479 
480 
JumpSounds() const481 const map<const Sound *, int> &Outfit::JumpSounds() const
482 {
483 	return jumpSounds;
484 }
485 
486 
487 
JumpInSounds() const488 const map<const Sound *, int> &Outfit::JumpInSounds() const
489 {
490 	return jumpInSounds;
491 }
492 
493 
494 
JumpOutSounds() const495 const map<const Sound *, int> &Outfit::JumpOutSounds() const
496 {
497 	return jumpOutSounds;
498 }
499 
500 
501 
502 // Get the sprite this outfit uses when dumped into space.
FlotsamSprite() const503 const Sprite *Outfit::FlotsamSprite() const
504 {
505 	return flotsamSprite;
506 }
507