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