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