1 #include "vehicle.h" // IWYU pragma: associated
2
3 #include <algorithm>
4 #include <cmath>
5 #include <memory>
6 #include <set>
7 #include <string>
8
9 #include "cata_assert.h"
10 #include "character.h"
11 #include "color.h"
12 #include "debug.h"
13 #include "enums.h"
14 #include "flag.h"
15 #include "game.h"
16 #include "item.h"
17 #include "item_contents.h"
18 #include "item_pocket.h"
19 #include "itype.h"
20 #include "map.h"
21 #include "messages.h"
22 #include "npc.h"
23 #include "ret_val.h"
24 #include "string_formatter.h"
25 #include "translations.h"
26 #include "units.h"
27 #include "value_ptr.h"
28 #include "veh_type.h"
29 #include "vpart_position.h"
30 #include "weather.h"
31
32 static const itype_id fuel_type_battery( "battery" );
33 static const itype_id fuel_type_none( "null" );
34
35 static const itype_id itype_battery( "battery" );
36 static const itype_id itype_muscle( "muscle" );
37
38 /*-----------------------------------------------------------------------------
39 * VEHICLE_PART
40 *-----------------------------------------------------------------------------*/
vehicle_part()41 vehicle_part::vehicle_part()
42 : id( vpart_id::NULL_ID() ) {}
43
vehicle_part(const vpart_id & vp,const std::string & variant_id,const point & dp,item && obj)44 vehicle_part::vehicle_part( const vpart_id &vp, const std::string &variant_id, const point &dp,
45 item &&obj )
46 : mount( dp ), id( vp ), variant( variant_id ), base( std::move( obj ) )
47 {
48 // Mark base item as being installed as a vehicle part
49 base.set_flag( flag_VEHICLE );
50
51 if( base.typeId() != vp->base_item ) {
52 debugmsg( "incorrect vehicle part item, expected: %s, received: %s",
53 vp->base_item.c_str(), base.typeId().c_str() );
54 }
55 }
56
operator bool() const57 vehicle_part::operator bool() const
58 {
59 return id != vpart_id::NULL_ID();
60 }
61
get_base() const62 const item &vehicle_part::get_base() const
63 {
64 return base;
65 }
66
set_base(const item & new_base)67 void vehicle_part::set_base( const item &new_base )
68 {
69 base = new_base;
70 }
71
properties_to_item() const72 item vehicle_part::properties_to_item() const
73 {
74 item tmp = base;
75 tmp.unset_flag( flag_VEHICLE );
76
77 // Cables get special handling: their target coordinates need to remain
78 // stored, and if a cable actually drops, it should be half-connected.
79 if( tmp.has_flag( flag_CABLE_SPOOL ) && !tmp.has_flag( flag_TOW_CABLE ) ) {
80 map &here = get_map();
81 const tripoint local_pos = here.getlocal( target.first );
82 if( !here.veh_at( local_pos ) ) {
83 // That vehicle ain't there no more.
84 tmp.set_flag( flag_NO_DROP );
85 }
86
87 tmp.set_var( "source_x", target.first.x );
88 tmp.set_var( "source_y", target.first.y );
89 tmp.set_var( "source_z", target.first.z );
90 tmp.set_var( "state", "pay_out_cable" );
91 tmp.active = true;
92 }
93
94 // force rationalization of damage values to the middle value of each damage level so
95 // that parts will stack nicely
96 tmp.set_damage( tmp.damage_level() * itype::damage_scale );
97 return tmp;
98 }
99
name(bool with_prefix) const100 std::string vehicle_part::name( bool with_prefix ) const
101 {
102 auto res = info().name();
103
104 if( base.engine_displacement() > 0 ) {
105 res.insert( 0, string_format( _( "%2.1fL " ), base.engine_displacement() / 100.0 ) );
106
107 } else if( wheel_diameter() > 0 ) {
108 res.insert( 0, string_format( _( "%d\" " ), wheel_diameter() ) );
109 }
110
111 if( base.is_faulty() ) {
112 res += _( " (faulty)" );
113 }
114
115 if( base.has_var( "contained_name" ) ) {
116 res += string_format( _( " holding %s" ), base.get_var( "contained_name" ) );
117 }
118
119 if( is_leaking() ) {
120 res += _( " (draining)" );
121 }
122
123 if( with_prefix ) {
124 res.insert( 0, colorize( base.damage_symbol(), base.damage_color() ) + " " );
125 }
126 return res;
127 }
128
hp() const129 int vehicle_part::hp() const
130 {
131 const int dur = info().durability;
132 if( base.max_damage() > 0 ) {
133 return dur - dur * base.damage() / base.max_damage();
134 } else {
135 return dur;
136 }
137 }
138
damage() const139 int vehicle_part::damage() const
140 {
141 return base.damage();
142 }
143
max_damage() const144 int vehicle_part::max_damage() const
145 {
146 return base.max_damage();
147 }
148
damage_level() const149 int vehicle_part::damage_level() const
150 {
151 return base.damage_level();
152 }
153
health_percent() const154 double vehicle_part::health_percent() const
155 {
156 return 1.0 - static_cast<double>( base.damage() ) / base.max_damage();
157 }
158
damage_percent() const159 double vehicle_part::damage_percent() const
160 {
161 return static_cast<double>( base.damage() ) / base.max_damage();
162 }
163
164 /** parts are considered broken at zero health */
is_broken() const165 bool vehicle_part::is_broken() const
166 {
167 return base.damage() >= base.max_damage();
168 }
169
is_cleaner_on() const170 bool vehicle_part::is_cleaner_on() const
171 {
172 const bool is_cleaner = info().has_flag( VPFLAG_AUTOCLAVE ) ||
173 info().has_flag( VPFLAG_DISHWASHER ) ||
174 info().has_flag( VPFLAG_WASHING_MACHINE );
175 return is_cleaner && enabled;
176 }
177
is_unavailable(const bool carried) const178 bool vehicle_part::is_unavailable( const bool carried ) const
179 {
180 return is_broken() || ( has_flag( carried_flag ) && carried );
181 }
182
is_available(const bool carried) const183 bool vehicle_part::is_available( const bool carried ) const
184 {
185 return !is_unavailable( carried );
186 }
187
fuel_current() const188 itype_id vehicle_part::fuel_current() const
189 {
190 if( is_engine() ) {
191 if( ammo_pref.is_null() ) {
192 return info().fuel_type != itype_muscle ? info().fuel_type : itype_id::NULL_ID();
193 } else {
194 return ammo_pref;
195 }
196 }
197
198 return itype_id::NULL_ID();
199 }
200
fuel_set(const itype_id & fuel)201 bool vehicle_part::fuel_set( const itype_id &fuel )
202 {
203 if( is_engine() ) {
204 for( const itype_id &avail : info().engine_fuel_opts() ) {
205 if( fuel == avail ) {
206 ammo_pref = fuel;
207 return true;
208 }
209 }
210 }
211 return false;
212 }
213
ammo_current() const214 itype_id vehicle_part::ammo_current() const
215 {
216 if( is_battery() ) {
217 return itype_battery;
218 }
219
220 if( is_tank() && !base.contents.empty() ) {
221 return base.contents.legacy_front().typeId();
222 }
223
224 if( is_fuel_store( false ) || is_turret() ) {
225 return base.ammo_current() != itype_id::NULL_ID() ? base.ammo_current() : base.ammo_default();
226 }
227
228 return itype_id::NULL_ID();
229 }
230
ammo_capacity(const ammotype & ammo) const231 int vehicle_part::ammo_capacity( const ammotype &ammo ) const
232 {
233 if( is_tank() ) {
234 return item::find_type( ammo_current() )->charges_per_volume( base.get_total_capacity() );
235 }
236
237 if( is_fuel_store( false ) || is_turret() ) {
238 return base.ammo_capacity( ammo );
239 }
240
241 return 0;
242 }
243
ammo_remaining() const244 int vehicle_part::ammo_remaining() const
245 {
246 if( is_tank() ) {
247 return base.contents.empty() ? 0 : base.contents.legacy_front().charges;
248 }
249 if( is_fuel_store( false ) || is_turret() ) {
250 return base.ammo_remaining();
251 }
252
253 return 0;
254 }
255
remaining_ammo_capacity() const256 int vehicle_part::remaining_ammo_capacity() const
257 {
258 return base.remaining_ammo_capacity();
259 }
260
ammo_set(const itype_id & ammo,int qty)261 int vehicle_part::ammo_set( const itype_id &ammo, int qty )
262 {
263 const itype *liquid = item::find_type( ammo );
264
265 // We often check if ammo is set to see if tank is empty, if qty == 0 don't set ammo
266 if( is_tank() && liquid->phase >= phase_id::LIQUID && qty != 0 ) {
267 base.contents.clear_items();
268 const auto stack = units::legacy_volume_factor / std::max( liquid->stack_size, 1 );
269 const int limit = units::from_milliliter( ammo_capacity( item::find_type(
270 ammo )->ammo->type ) ) / stack;
271 // assuming "ammo" isn't really going into a magazine as this is a vehicle part
272 base.put_in( item( ammo, calendar::turn, qty > 0 ? std::min( qty, limit ) : limit ),
273 item_pocket::pocket_type::CONTAINER );
274 return qty;
275 }
276
277 if( is_turret() ) {
278 if( base.is_magazine() ) {
279 return base.ammo_set( ammo, qty ).ammo_remaining();
280 } else if( !base.magazine_default().is_null() ) {
281 item mag( base.magazine_default() );
282 mag.ammo_set( ammo, qty );
283 base.put_in( mag, item_pocket::pocket_type::MAGAZINE_WELL );
284 }
285 }
286
287 if( is_fuel_store() ) {
288 base.ammo_set( ammo, qty >= 0 ? qty : ammo_capacity( item::find_type( ammo )->ammo->type ) );
289 return base.ammo_remaining();
290 }
291
292 return -1;
293 }
294
ammo_unset()295 void vehicle_part::ammo_unset()
296 {
297 if( is_tank() ) {
298 base.contents.clear_items();
299 } else if( is_fuel_store() ) {
300 base.ammo_unset();
301 }
302 }
303
ammo_consume(int qty,const tripoint & pos)304 int vehicle_part::ammo_consume( int qty, const tripoint &pos )
305 {
306 if( is_tank() && !base.contents.empty() ) {
307 const int res = std::min( ammo_remaining(), qty );
308 item &liquid = base.contents.legacy_front();
309 liquid.charges -= res;
310 if( liquid.charges == 0 ) {
311 base.contents.clear_items();
312 }
313 return res;
314 }
315 return base.ammo_consume( qty, pos );
316 }
317
consume_energy(const itype_id & ftype,double energy_j)318 double vehicle_part::consume_energy( const itype_id &ftype, double energy_j )
319 {
320 if( base.contents.empty() || !is_fuel_store() ) {
321 return 0.0f;
322 }
323
324 item &fuel = base.contents.legacy_front();
325 if( fuel.typeId() == ftype ) {
326 cata_assert( fuel.is_fuel() );
327 // convert energy density in MJ/L to J/ml
328 const double energy_p_mL = fuel.fuel_energy() * 1000;
329 const int ml_to_use = static_cast<int>( std::floor( energy_j / energy_p_mL ) );
330 int charges_to_use = fuel.charges_per_volume( ml_to_use * 1_ml );
331
332 if( !charges_to_use ) {
333 return 0.0;
334 }
335 if( charges_to_use > fuel.charges ) {
336 charges_to_use = fuel.charges;
337 base.contents.clear_items();
338 } else {
339 fuel.charges -= charges_to_use;
340 }
341 item fuel_consumed( ftype, calendar::turn, charges_to_use );
342 return energy_p_mL * units::to_milliliter<int>( fuel_consumed.volume( true ) );
343 }
344 return 0.0;
345 }
346
can_reload(const item & obj) const347 bool vehicle_part::can_reload( const item &obj ) const
348 {
349 // first check part is not destroyed and can contain ammo
350 if( !is_fuel_store() ) {
351 return false;
352 }
353
354 if( !obj.is_null() ) {
355 const itype_id obj_type = obj.typeId();
356 if( is_reactor() ) {
357 return base.is_reloadable_with( obj_type );
358 }
359
360 // forbid filling tanks with solids or non-material things
361 if( is_tank() && ( obj.made_of( phase_id::SOLID ) || obj.made_of( phase_id::PNULL ) ) ) {
362 return false;
363 }
364 // forbid putting liquids, gasses, and plasma in things that aren't tanks
365 else if( !obj.made_of( phase_id::SOLID ) && !is_tank() ) {
366 return false;
367 }
368 // prevent mixing of different ammo
369 if( !ammo_current().is_null() && ammo_current() != obj_type ) {
370 return false;
371 }
372 // For storage with set type, prevent filling with different types
373 if( info().fuel_type != fuel_type_none && info().fuel_type != obj_type ) {
374 return false;
375 }
376 // don't fill magazines with inappropriate fuel
377 if( !is_tank() && !base.is_reloadable_with( obj_type ) ) {
378 return false;
379 }
380 }
381 if( base.is_gun() ) {
382 return false;
383 }
384
385 if( is_reactor() ) {
386 return true;
387 }
388
389 if( ammo_current().is_null() ) {
390 return true; // empty tank
391 }
392
393 return ammo_remaining() < ammo_capacity( ammo_current().obj().ammo->type );
394 }
395
process_contents(const tripoint & pos,const bool e_heater)396 void vehicle_part::process_contents( const tripoint &pos, const bool e_heater )
397 {
398 // for now we only care about processing food containers since things like
399 // fuel don't care about temperature yet
400 if( base.has_item_with( []( const item & it ) {
401 return it.needs_processing();
402 } ) ) {
403 temperature_flag flag = temperature_flag::NORMAL;
404 if( e_heater ) {
405 flag = temperature_flag::HEATER;
406 }
407 if( enabled && info().has_flag( VPFLAG_FRIDGE ) ) {
408 flag = temperature_flag::FRIDGE;
409 } else if( enabled && info().has_flag( VPFLAG_FREEZER ) ) {
410 flag = temperature_flag::FREEZER;
411 }
412 base.process( nullptr, pos, 1, flag );
413 }
414 }
415
fill_with(item & liquid,int qty)416 bool vehicle_part::fill_with( item &liquid, int qty )
417 {
418 if( ( is_tank() && !liquid.made_of( phase_id::LIQUID ) ) || !can_reload( liquid ) ) {
419 return false;
420 }
421
422 int charges_max = ammo_capacity( item::find_type( ammo_current() )->ammo->type ) - ammo_remaining();
423 qty = qty < liquid.charges ? qty : liquid.charges;
424
425 if( charges_max < liquid.charges ) {
426 qty = charges_max;
427 }
428
429 liquid.charges -= base.fill_with( liquid, qty );
430
431 return true;
432 }
433
faults() const434 const std::set<fault_id> &vehicle_part::faults() const
435 {
436 return base.faults;
437 }
438
has_fault_flag(const std::string & searched_flag) const439 bool vehicle_part::has_fault_flag( const std::string &searched_flag ) const
440 {
441 return base.has_fault_flag( searched_flag );
442 }
443
faults_potential() const444 std::set<fault_id> vehicle_part::faults_potential() const
445 {
446 return base.faults_potential();
447 }
448
fault_set(const fault_id & f)449 bool vehicle_part::fault_set( const fault_id &f )
450 {
451 if( !faults_potential().count( f ) ) {
452 return false;
453 }
454 base.faults.insert( f );
455 return true;
456 }
457
wheel_area() const458 int vehicle_part::wheel_area() const
459 {
460 return info().wheel_area();
461 }
462
463 /** Get wheel diameter (inches) or return 0 if part is not wheel */
wheel_diameter() const464 int vehicle_part::wheel_diameter() const
465 {
466 return base.is_wheel() ? base.type->wheel->diameter : 0;
467 }
468
469 /** Get wheel width (inches) or return 0 if part is not wheel */
wheel_width() const470 int vehicle_part::wheel_width() const
471 {
472 return base.is_wheel() ? base.type->wheel->width : 0;
473 }
474
crew() const475 npc *vehicle_part::crew() const
476 {
477 if( is_broken() || !crew_id.is_valid() ) {
478 return nullptr;
479 }
480
481 npc *const res = g->critter_by_id<npc>( crew_id );
482 if( !res ) {
483 return nullptr;
484 }
485 return res->is_player_ally() ? res : nullptr;
486 }
487
set_crew(const npc & who)488 bool vehicle_part::set_crew( const npc &who )
489 {
490 if( who.is_dead_state() || !( who.is_walking_with() || who.is_player_ally() ) ) {
491 return false;
492 }
493 if( is_broken() || ( !is_seat() && !is_turret() ) ) {
494 return false;
495 }
496 crew_id = who.getID();
497 return true;
498 }
499
unset_crew()500 void vehicle_part::unset_crew()
501 {
502 crew_id = character_id();
503 }
504
reset_target(const tripoint & pos)505 void vehicle_part::reset_target( const tripoint &pos )
506 {
507 target.first = pos;
508 target.second = pos;
509 }
510
is_engine() const511 bool vehicle_part::is_engine() const
512 {
513 return info().has_flag( VPFLAG_ENGINE );
514 }
515
is_light() const516 bool vehicle_part::is_light() const
517 {
518 const auto &vp = info();
519 return vp.has_flag( VPFLAG_CONE_LIGHT ) ||
520 vp.has_flag( VPFLAG_WIDE_CONE_LIGHT ) ||
521 vp.has_flag( VPFLAG_HALF_CIRCLE_LIGHT ) ||
522 vp.has_flag( VPFLAG_CIRCLE_LIGHT ) ||
523 vp.has_flag( VPFLAG_AISLE_LIGHT ) ||
524 vp.has_flag( VPFLAG_DOME_LIGHT ) ||
525 vp.has_flag( VPFLAG_ATOMIC_LIGHT );
526 }
527
is_fuel_store(bool skip_broke) const528 bool vehicle_part::is_fuel_store( bool skip_broke ) const
529 {
530 if( skip_broke && is_broken() ) {
531 return false;
532 }
533 return is_tank() || base.is_magazine() || is_reactor();
534 }
535
is_tank() const536 bool vehicle_part::is_tank() const
537 {
538 return base.is_watertight_container();
539 }
540
contains_liquid() const541 bool vehicle_part::contains_liquid() const
542 {
543 return is_tank() && !base.contents.empty() &&
544 base.contents.only_item().made_of( phase_id::LIQUID );
545 }
546
is_battery() const547 bool vehicle_part::is_battery() const
548 {
549 return base.is_magazine() && base.ammo_types().count( ammotype( "battery" ) );
550 }
551
is_reactor() const552 bool vehicle_part::is_reactor() const
553 {
554 return info().has_flag( VPFLAG_REACTOR );
555 }
556
is_leaking() const557 bool vehicle_part::is_leaking() const
558 {
559 return health_percent() <= 0.5 && ( is_tank() || is_battery() || is_reactor() );
560 }
561
is_turret() const562 bool vehicle_part::is_turret() const
563 {
564 return base.is_gun();
565 }
566
is_seat() const567 bool vehicle_part::is_seat() const
568 {
569 return info().has_flag( "SEAT" );
570 }
571
info() const572 const vpart_info &vehicle_part::info() const
573 {
574 if( !info_cache ) {
575 info_cache = &id.obj();
576 }
577 return *info_cache;
578 }
579
set_hp(vehicle_part & pt,int qty)580 void vehicle::set_hp( vehicle_part &pt, int qty )
581 {
582 if( qty == pt.info().durability || pt.info().durability <= 0 ) {
583 pt.base.set_damage( 0 );
584
585 } else if( qty == 0 ) {
586 pt.base.set_damage( pt.base.max_damage() );
587
588 } else {
589 pt.base.set_damage( pt.base.max_damage() - pt.base.max_damage() * qty / pt.info().durability );
590 }
591 }
592
mod_hp(vehicle_part & pt,int qty,damage_type dt)593 bool vehicle::mod_hp( vehicle_part &pt, int qty, damage_type dt )
594 {
595 if( pt.info().durability > 0 ) {
596 return pt.base.mod_damage( -( pt.base.max_damage() * qty / pt.info().durability ), dt );
597 } else {
598 return false;
599 }
600 }
601
can_enable(const vehicle_part & pt,bool alert) const602 bool vehicle::can_enable( const vehicle_part &pt, bool alert ) const
603 {
604 if( std::none_of( parts.begin(), parts.end(), [&pt]( const vehicle_part & e ) {
605 return &e == &pt;
606 } ) || pt.removed ) {
607 debugmsg( "Cannot enable removed or non-existent part" );
608 }
609
610 if( pt.is_broken() ) {
611 return false;
612 }
613
614 if( pt.info().has_flag( "PLANTER" ) && !warm_enough_to_plant( get_player_character().pos() ) ) {
615 if( alert ) {
616 add_msg( m_bad, _( "It is too cold to plant anything now." ) );
617 }
618 return false;
619 }
620
621 // TODO: check fuel for combustion engines
622
623 if( pt.info().epower < 0 && fuel_left( fuel_type_battery, true ) <= 0 ) {
624 if( alert ) {
625 add_msg( m_bad, _( "Insufficient power to enable %s" ), pt.name() );
626 }
627 return false;
628 }
629
630 return true;
631 }
632
assign_seat(vehicle_part & pt,const npc & who)633 bool vehicle::assign_seat( vehicle_part &pt, const npc &who )
634 {
635 if( !pt.is_seat() || !pt.set_crew( who ) ) {
636 return false;
637 }
638
639 // NPC's can only be assigned to one seat in the vehicle
640 for( auto &e : parts ) {
641 if( &e == &pt ) {
642 // skip this part
643 continue;
644 }
645
646 if( e.is_seat() ) {
647 const npc *n = e.crew();
648 if( n && n->getID() == who.getID() ) {
649 e.unset_crew();
650 }
651 }
652 }
653
654 return true;
655 }
656
carried_name() const657 std::string vehicle_part::carried_name() const
658 {
659 if( carry_names.empty() ) {
660 return std::string();
661 }
662 return carry_names.top().substr( name_offset );
663 }
664