Skip to content
Permalink
Tree: b4a409fb41
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
6558 lines (5754 sloc) 217 KB
#include "vehicle.h"
#include "coordinate_conversions.h"
#include "map.h"
#include "mapbuffer.h"
#include "output.h"
#include "game.h"
#include "map.h"
#include "item.h"
#include "item_group.h"
#include "veh_interact.h"
#include "cursesdef.h"
#include "catacharset.h"
#include "overmapbuffer.h"
#include "messages.h"
#include "iexamine.h"
#include "string_formatter.h"
#include "ui.h"
#include "debug.h"
#include "sounds.h"
#include "translations.h"
#include "ammo.h"
#include "options.h"
#include "material.h"
#include "monster.h"
#include "npc.h"
#include "veh_type.h"
#include "trap.h"
#include "itype.h"
#include "submap.h"
#include "mapdata.h"
#include "mtype.h"
#include "weather.h"
#include "json.h"
#include "map_iterator.h"
#include "vehicle_selector.h"
#include "cata_utility.h"
#include <sstream>
#include <stdlib.h>
#include <set>
#include <queue>
#include <math.h>
#include <array>
#include <numeric>
#include <algorithm>
#include <cassert>
/*
* Speed up all those if ( blarg == "structure" ) statements that are used everywhere;
* assemble "structure" once here instead of repeatedly later.
*/
static const itype_id fuel_type_none("null");
static const itype_id fuel_type_gasoline("gasoline");
static const itype_id fuel_type_diesel("diesel");
static const itype_id fuel_type_battery("battery");
static const itype_id fuel_type_water("water_clean");
static const itype_id fuel_type_muscle("muscle");
static const std::string part_location_structure("structure");
static const fault_id fault_belt( "fault_engine_belt_drive" );
static const fault_id fault_diesel( "fault_engine_pump_diesel" );
static const fault_id fault_glowplug( "fault_engine_glow_plug" );
static const fault_id fault_immobiliser( "fault_engine_immobiliser" );
static const fault_id fault_pump( "fault_engine_pump_fuel" );
static const fault_id fault_starter( "fault_engine_starter" );
static const fault_id fault_filter_air( "fault_engine_filter_air" );
static const fault_id fault_filter_fuel( "fault_engine_filter_fuel" );
const skill_id skill_mechanics( "mechanics" );
const efftype_id effect_stunned( "stunned" );
// Vehicle stack methods.
std::list<item>::iterator vehicle_stack::erase( std::list<item>::iterator it )
{
return myorigin->remove_item(part_num, it);
}
void vehicle_stack::push_back( const item &newitem )
{
myorigin->add_item(part_num, newitem);
}
void vehicle_stack::insert_at( std::list<item>::iterator index,
const item &newitem )
{
myorigin->add_item_at(part_num, index, newitem);
}
units::volume vehicle_stack::max_volume() const
{
if( myorigin->part_flag( part_num, "CARGO" ) && !myorigin->parts[part_num].is_broken() ) {
return myorigin->parts[part_num].info().size;
}
return 0;
}
// Vehicle class methods.
vehicle::vehicle(const vproto_id &type_id, int init_veh_fuel, int init_veh_status): type(type_id)
{
turn_dir = 0;
face.init(0);
move.init(0);
of_turn_carry = 0;
if( !type.str().empty() && type.is_valid() ) {
const vehicle_prototype &proto = type.obj();
// Copy the already made vehicle. The blueprint is created when the json data is loaded
// and is guaranteed to be valid (has valid parts etc.).
*this = *proto.blueprint;
init_state(init_veh_fuel, init_veh_status);
}
precalc_mounts(0, pivot_rotation[0], pivot_anchor[0]);
refresh();
}
vehicle::vehicle() : vehicle( vproto_id() )
{
smx = 0;
smy = 0;
smz = 0;
}
vehicle::~vehicle() = default;
void vehicle::set_hp( vehicle_part &pt, int qty )
{
if( qty == pt.info().durability ) {
pt.base.set_damage( 0 );
} else if( qty == 0 ) {
pt.base.set_damage( pt.base.max_damage() );
} else {
double k = pt.base.max_damage() / double( pt.info().durability );
pt.base.set_damage( pt.base.max_damage() - ( qty * k ) );
}
}
bool vehicle::mod_hp( vehicle_part &pt, int qty, damage_type dt )
{
double k = pt.base.max_damage() / double( pt.info().durability );
return pt.base.mod_damage( - qty * k, dt );
}
bool vehicle::player_in_control(player const& p) const
{
// Debug switch to prevent vehicles from skidding
// without having to place the player in them.
if( tags.count( "IN_CONTROL_OVERRIDE" ) ) {
return true;
}
int veh_part;
if( g->m.veh_at( p.pos(), veh_part ) == this &&
part_with_feature(veh_part, VPFLAG_CONTROLS, false) >= 0 && p.controlling_vehicle ) {
return true;
}
return remote_controlled( p );
}
bool vehicle::remote_controlled(player const &p) const
{
vehicle *veh = g->remoteveh();
if( veh != this ) {
return false;
}
auto remote = all_parts_with_feature( "REMOTE_CONTROLS", true );
for( int part : remote ) {
if( rl_dist( p.pos(), tripoint( global_pos() + parts[part].precalc[0], p.posz() ) ) <= 40 ) {
return true;
}
}
add_msg(m_bad, _("Lost connection with the vehicle due to distance!"));
g->setremoteveh( nullptr );
return false;
}
/** Checks all parts to see if frames are missing (as they might be when
* loading from a game saved before the vehicle construction rules overhaul). */
void vehicle::add_missing_frames()
{
static const vpart_id frame_id( "frame_vertical" );
const vpart_info &frame_part = frame_id.obj(); // NOT static, could be different each time
//No need to check the same (x, y) spot more than once
std::set< std::pair<int, int> > locations_checked;
for (auto &i : parts) {
int next_x = i.mount.x;
int next_y = i.mount.y;
std::pair<int, int> mount_location = std::make_pair(next_x, next_y);
if(locations_checked.count(mount_location) == 0) {
std::vector<int> parts_here = parts_at_relative(next_x, next_y, false);
bool found = false;
for( auto &elem : parts_here ) {
if( part_info( elem ).location == part_location_structure ) {
found = true;
break;
}
}
if( !found ) {
// Install missing frame
parts.emplace_back( frame_part.get_id(), next_x, next_y, item( frame_part.item ) );
}
}
locations_checked.insert(mount_location);
}
}
// Called when loading a vehicle that predates steerable wheels.
// Tries to convert some wheels to steerable versions on the front axle.
void vehicle::add_steerable_wheels()
{
int axle = INT_MIN;
std::vector< std::pair<int, vpart_id> > wheels;
// Find wheels that have steerable versions.
// Convert the wheel(s) with the largest x value.
for (size_t p = 0; p < parts.size(); ++p) {
if (part_flag(p, "STEERABLE") || part_flag(p, "TRACKED")) {
// Has a wheel that is inherently steerable
// (e.g. unicycle, casters), this vehicle doesn't
// need conversion.
return;
}
if (parts[p].mount.x < axle) {
// there is another axle in front of this
continue;
}
if( part_flag( p, VPFLAG_WHEEL ) ) {
vpart_id steerable_id( part_info( p ).get_id().str() + "_steerable" );
if( steerable_id.is_valid() ) {
// We can convert this.
if (parts[p].mount.x != axle) {
// Found a new axle further forward than the
// existing one.
wheels.clear();
axle = parts[p].mount.x;
}
wheels.push_back(std::make_pair(static_cast<int>( p ), steerable_id));
}
}
}
// Now convert the wheels to their new types.
for (auto &wheel : wheels) {
parts[ wheel.first ].id = wheel.second;
}
}
void vehicle::init_state(int init_veh_fuel, int init_veh_status)
{
// vehicle parts excluding engines are by default turned off
for( auto &pt : parts ) {
pt.enabled = pt.base.is_engine();
}
bool destroySeats = false;
bool destroyControls = false;
bool destroyTank = false;
bool destroyEngine = false;
bool destroyTires = false;
bool blood_covered = false;
bool blood_inside = false;
bool has_no_key = false;
bool destroyAlarm = false;
// More realistically it should be -5 days old
last_update = 0;
// veh_fuel_multiplier is percentage of fuel
// 0 is empty, 100 is full tank, -1 is random 7% to 35%
int veh_fuel_mult = init_veh_fuel;
if (init_veh_fuel == - 1) {
veh_fuel_mult = rng (1,7);
}
if (init_veh_fuel > 100) {
veh_fuel_mult = 100;
}
// veh_status is initial vehicle damage
// -1 = light damage (DEFAULT)
// 0 = undamaged
// 1 = disabled, destroyed tires OR engine
int veh_status = -1;
if (init_veh_status == 0) {
veh_status = 0;
}
if (init_veh_status == 1) {
int rand = rng( 1, 100 );
veh_status = 1;
if( rand <= 5 ) { // seats are destroyed 5%
destroySeats = true;
} else if( rand <= 15 ) { // controls are destroyed 10%
destroyControls = true;
veh_fuel_mult += rng (0, 7); // add 0-7% more fuel if controls are destroyed
} else if( rand <= 23 ) { // battery, minireactor or gasoline tank are destroyed 8%
destroyTank = true;
} else if( rand <= 29 ) { // engine are destroyed 6%
destroyEngine = true;
veh_fuel_mult += rng (3, 12); // add 3-12% more fuel if engine is destroyed
} else if( rand <= 66 ) { // tires are destroyed 37%
destroyTires = true;
veh_fuel_mult += rng (0, 18); // add 0-18% more fuel if tires are destroyed
} else { // vehicle locked 34%
has_no_key = true;
}
}
// if locked, 16% chance something damaged
if( one_in(6) && has_no_key ) {
if( one_in(3) ) {
destroyTank = true;
} else if( one_in(2) ) {
destroyEngine = true;
} else {
destroyTires = true;
}
} else if( !one_in(3) ){
//most cars should have a destroyed alarm
destroyAlarm = true;
}
//Provide some variety to non-mint vehicles
if( veh_status != 0 ) {
//Leave engine running in some vehicles, if the engine has not been destroyed
if( veh_fuel_mult > 0 && all_parts_with_feature("ENGINE", true).size() > 0 &&
one_in(8) && !destroyEngine && !has_no_key && has_engine_type_not(fuel_type_muscle, true) ) {
engine_on = true;
}
auto light_head = one_in( 20 );
auto light_dome = one_in( 16 );
auto light_aisle = one_in( 8 );
auto light_overh = one_in( 4 );
auto light_atom = one_in( 2 );
for( auto &pt : parts ) {
if( pt.has_flag( VPFLAG_CONE_LIGHT ) ) {
pt.enabled = light_head;
} else if( pt.has_flag( VPFLAG_DOME_LIGHT ) ) {
pt.enabled = light_dome;
} else if( pt.has_flag( VPFLAG_AISLE_LIGHT ) ) {
pt.enabled = light_aisle;
} else if( pt.has_flag( VPFLAG_CIRCLE_LIGHT ) ) {
pt.enabled = light_overh;
} else if( pt.has_flag( VPFLAG_ATOMIC_LIGHT ) ) {
pt.enabled = light_atom;
}
}
if( one_in(10) ) {
blood_covered = true;
}
if( one_in(8) ) {
blood_inside = true;
}
for( auto e : get_parts( "FRIDGE" ) ) {
e->enabled = true;
}
for( auto e : get_parts( "WATER_PURIFIER" ) ) {
e->enabled = true;
}
}
bool blood_inside_set = false;
int blood_inside_x = 0;
int blood_inside_y = 0;
for( size_t p = 0; p < parts.size(); p++ ) {
auto &pt = parts[ p ];
if( part_flag( p, "REACTOR" ) ) {
// De-hardcoded reactors. Should always start active
parts[ p ].enabled = true;
}
if( pt.is_battery() ) {
if( veh_fuel_mult == 100 ) { // Mint condition vehicle
pt.ammo_set( "battery", pt.ammo_capacity() );
} else if( one_in( 2 ) && veh_fuel_mult > 0 ) { // Randomize battery ammo a bit
pt.ammo_set( "battery", pt.ammo_capacity() * ( veh_fuel_mult + rng( 0, 10 ) ) / 100 );
} else if( one_in( 2 ) && veh_fuel_mult > 0 ) {
pt.ammo_set( "battery", pt.ammo_capacity() * ( veh_fuel_mult - rng( 0, 10 ) ) / 100 );
} else {
pt.ammo_set( "battery", pt.ammo_capacity() * veh_fuel_mult / 100 );
}
}
if( pt.is_tank() && type->parts[p].fuel != "null" ) {
int qty = pt.ammo_capacity() * veh_fuel_mult / 100;
qty *= std::max( item::find_type( type->parts[p].fuel )->stack_size, 1 );
qty /= to_milliliter( units::legacy_volume_factor );
pt.ammo_set( type->parts[ p ].fuel, qty );
}
if (part_flag(p, "OPENABLE")) { // doors are closed
if(!parts[p].open && one_in(4)) {
open(p);
}
}
if (part_flag(p, "BOARDABLE")) { // no passengers
parts[p].remove_flag(vehicle_part::passenger_flag);
}
// initial vehicle damage
if (veh_status == 0) {
// Completely mint condition vehicle
set_hp( parts[ p ], part_info( p ).durability );
} else {
//a bit of initial damage :)
//clamp 4d8 to the range of [8,20]. 8=broken, 20=undamaged.
int broken = 8;
int unhurt = 20;
int roll = dice( 4, 8 );
if(roll < unhurt){
if (roll <= broken) {
set_hp( parts[ p ], 0 );
parts[ p ].ammo_unset(); //empty broken batteries and fuel tanks
} else {
set_hp( parts[ p ], ( roll - broken ) / double( unhurt - broken ) * part_info( p ).durability );
}
} else {
set_hp( parts[ p ], part_info( p ).durability );
}
if( part_flag( p, VPFLAG_ENGINE ) ) {
// If possible set an engine fault rather than destroying the engine outright
if( destroyEngine && parts[ p ].faults_potential().empty() ) {
set_hp( parts[ p ], 0 );
} else if( destroyEngine || one_in( 3 ) ) {
do {
parts[ p ].fault_set( random_entry( parts[ p ].faults_potential() ) );
} while( one_in( 3 ) );
}
} else if ((destroySeats && (part_flag(p, "SEAT") || part_flag(p, "SEATBELT"))) ||
(destroyControls && (part_flag(p, "CONTROLS") || part_flag(p, "SECURITY"))) ||
(destroyAlarm && part_flag(p, "SECURITY"))) {
set_hp( parts[ p ], 0 );
}
// Fuel tanks should be emptied as well
if( destroyTank && pt.is_tank() ) {
set_hp( pt, 0 );
pt.ammo_unset();
}
//Solar panels have 25% of being destroyed
if (part_flag(p, "SOLAR_PANEL") && one_in(4)) {
set_hp( parts[ p ], 0 );
}
/* Bloodsplatter the front-end parts. Assume anything with x > 0 is
* the "front" of the vehicle (since the driver's seat is at (0, 0).
* We'll be generous with the blood, since some may disappear before
* the player gets a chance to see the vehicle. */
if(blood_covered && parts[p].mount.x > 0) {
if(one_in(3)) {
//Loads of blood. (200 = completely red vehicle part)
parts[p].blood = rng(200, 600);
} else {
//Some blood
parts[p].blood = rng(50, 200);
}
}
if(blood_inside) {
// blood is splattered around (blood_inside_x, blood_inside_y),
// coordinates relative to mount point; the center is always a seat
if (blood_inside_set) {
int distSq = std::pow((blood_inside_x - parts[p].mount.x), 2) +
std::pow((blood_inside_y - parts[p].mount.y), 2);
if (distSq <= 1) {
parts[p].blood = rng(200, 400) - distSq * 100;
}
} else if (part_flag(p, "SEAT")) {
// Set the center of the bloody mess inside
blood_inside_x = parts[p].mount.x;
blood_inside_y = parts[p].mount.y;
blood_inside_set = true;
}
}
}
//sets the vehicle to locked, if there is no key and an alarm part exists
if( part_flag( p, "SECURITY" ) && has_no_key && !parts[p].is_broken() ) {
is_locked = true;
if( one_in( 2 ) ) {
// if vehicle has immobilizer 50% chance to add additional fault
parts[ p ].fault_set( fault_immobiliser );
}
}
}
// destroy tires until the vehicle is not drivable
if( destroyTires && !wheelcache.empty() ) {
int tries = 0;
while( valid_wheel_config( false ) && tries < 100 ) {
// wheel config is still valid, destroy the tire.
set_hp( parts[random_entry( wheelcache )], 0 );
tries++;
}
}
invalidate_mass();
}
/**
* Smashes up a vehicle that has already been placed; used for generating
* very damaged vehicles. Additionally, any spot where two vehicles overlapped
* (ie, any spot with multiple frames) will be completely destroyed, as that
* was the collision point.
*/
void vehicle::smash() {
for( auto &part : parts ) {
//Skip any parts already mashed up or removed.
if( part.is_broken() || part.removed ) {
continue;
}
std::vector<int> parts_in_square = parts_at_relative( part.mount.x, part.mount.y );
int structures_found = 0;
for( auto &square_part_index : parts_in_square ) {
if (part_info(square_part_index).location == part_location_structure) {
structures_found++;
}
}
if(structures_found > 1) {
//Destroy everything in the square
for( int idx : parts_in_square ) {
mod_hp( parts[ idx ], 0 - parts[ idx ].hp(), DT_BASH );
parts[ idx ].ammo_unset();
}
continue;
}
//Everywhere else, drop by 10-120% of max HP (anything over 100 = broken)
if( mod_hp( part, 0 - ( rng_float( 0.1f, 1.2f ) * part.info().durability ) ), DT_BASH ) {
part.ammo_unset();
}
}
}
const std::string vehicle::disp_name() const
{
return string_format( _("the %s"), name.c_str() );
}
int vehicle::lift_strength() const
{
units::mass mass = total_mass();
return std::max( mass / 10000_gram, 1 );
}
void vehicle::control_doors() {
std::vector< int > door_motors = all_parts_with_feature( "DOOR_MOTOR", true );
std::vector< int > doors_with_motors; // Indices of doors
std::vector< tripoint > locations; // Locations used to display the doors
doors_with_motors.reserve( door_motors.size() * 2); // it is possible to have one door to open and one to close for single motor
locations.reserve( door_motors.size() * 2);
if( door_motors.empty() ) {
debugmsg( "vehicle::control_doors called but no door motors found" );
return;
}
uimenu pmenu;
pmenu.title = _("Select door to toggle");
int doors[2]; // one door to open and one to close
for( int p : door_motors ) {
doors[0] = next_part_to_open(p);
doors[1] = next_part_to_close(p);
for (int door : doors) {
if (door == -1)
continue;
int val = doors_with_motors.size();
doors_with_motors.push_back(door);
locations.push_back(tripoint(global_pos() + parts[p].precalc[0], smz));
const char *actname = parts[door].open ? _("Close") : _("Open");
pmenu.addentry(val, true, MENU_AUTOASSIGN, "%s %s", actname, parts[ door ].name().c_str() );
}
}
pmenu.addentry( doors_with_motors.size(), true, 'q', _("Cancel") );
pointmenu_cb callback( locations );
pmenu.callback = &callback;
pmenu.w_y = 0; // Move the menu so that we can see our vehicle
pmenu.query();
if( pmenu.ret >= 0 && pmenu.ret < (int)doors_with_motors.size() ) {
int part = doors_with_motors[pmenu.ret];
open_or_close( part, !(parts[part].open) );
}
}
void vehicle::control_engines() {
int e_toggle = 0;
bool dirty = false;
//count active engines
int active_count = 0;
for (size_t e = 0; e < engines.size(); ++e){
if (is_part_on(engines[e])){
active_count++;
}
}
//show menu until user finishes
while( e_toggle >= 0 && e_toggle < (int)engines.size() ) {
e_toggle = select_engine();
if( e_toggle >= 0 && e_toggle < (int)engines.size() &&
(active_count > 1 || !is_part_on(engines[e_toggle]))) {
active_count += (!is_part_on(engines[e_toggle])) ? 1 : -1;
toggle_specific_engine(e_toggle, !is_part_on(engines[e_toggle]));
dirty = true;
}
}
if( !dirty ) { return; }
// if current velocity greater than new configuration safe speed
// drop down cruise velocity.
int safe_vel = safe_velocity();
if( velocity > safe_vel ) {
cruise_velocity = safe_vel;
}
if( engine_on ) {
add_msg( _("You turn off the %s's engines to change their configurations."), name.c_str() );
engine_on = false;
} else if( !g->u.controlling_vehicle ) {
add_msg( _("You change the %s's engine configuration."), name.c_str() );
return;
}
start_engines();
}
int vehicle::select_engine() {
uimenu tmenu;
std::string name;
tmenu.text = _("Toggle which?");
for( size_t e = 0; e < engines.size(); ++e ) {
name = parts[ engines[ e ] ].name();
tmenu.addentry( e, true, -1, "[%s] %s",
((parts[engines[e]].enabled) ? "x" : " ") , name.c_str() );
}
tmenu.addentry(-1, true, 'q', _("Finish"));
tmenu.query();
return tmenu.ret;
}
void vehicle::toggle_specific_engine(int e,bool on) {
toggle_specific_part( engines[e], on );
}
void vehicle::toggle_specific_part(int p,bool on) {
parts[p].enabled = on;
}
bool vehicle::is_engine_type_on(int e, const itype_id &ft) const
{
return is_engine_on(e) && is_engine_type(e, ft);
}
bool vehicle::has_engine_type(const itype_id &ft, bool const enabled) const
{
for( size_t e = 0; e < engines.size(); ++e ) {
if( is_engine_type(e, ft) && (!enabled || is_engine_on(e)) ) {
return true;
}
}
return false;
}
bool vehicle::has_engine_type_not(const itype_id &ft, bool const enabled) const
{
for( size_t e = 0; e < engines.size(); ++e ) {
if( !is_engine_type(e, ft) && (!enabled || is_engine_on(e)) ) {
return true;
}
}
return false;
}
bool vehicle::is_engine_type(const int e, const itype_id &ft) const {
return part_info(engines[e]).fuel_type == ft;
}
bool vehicle::is_engine_on(int const e) const
{
return !parts[ engines[ e ] ].is_broken() && is_part_on( engines[ e ] );
}
bool vehicle::is_part_on(int const p) const
{
return parts[p].enabled;
}
bool vehicle::is_alternator_on(int const a) const
{
auto alt = parts[ alternators [ a ] ];
if( alt.is_broken() ) {
return false;
}
return std::any_of( engines.begin(), engines.end(), [this,&alt]( int idx ) {
auto& eng = parts [ idx ];
return eng.enabled && !eng.is_broken() && eng.mount == alt.mount && !eng.faults().count( fault_belt );
} );
}
bool vehicle::has_security_working() const
{
bool found_security = false;
for (size_t s = 0; s < speciality.size(); s++){
if( part_flag( speciality[ s ], "SECURITY" ) && !parts[ speciality[ s ] ].is_broken() ) {
found_security = true;
break;
}
}
return found_security;
}
bool vehicle::interact_vehicle_locked()
{
if (is_locked){
const inventory &crafting_inv = g->u.crafting_inventory();
add_msg(_("You don't find any keys in the %s."), name.c_str());
if( crafting_inv.has_quality( quality_id( "SCREW" ) ) ) {
if (query_yn(_("You don't find any keys in the %s. Attempt to hotwire vehicle?"),
name.c_str())) {
///\EFFECT_MECHANICS speeds up vehicle hotwiring
int mechanics_skill = g->u.get_skill_level( skill_mechanics );
int hotwire_time = 6000 / ((mechanics_skill > 0)? mechanics_skill : 1);
//assign long activity
g->u.assign_activity( activity_id( "ACT_HOTWIRE_CAR" ), hotwire_time, -1, INT_MIN, _( "Hotwire" ) );
// use part 0 as the reference point
point q = coord_translate(parts[0].mount);
g->u.activity.values.push_back(global_x() + q.x);//[0]
g->u.activity.values.push_back(global_y() + q.y);//[1]
g->u.activity.values.push_back(g->u.get_skill_level( skill_mechanics ));//[2]
} else {
if( has_security_working() && query_yn(_("Trigger the %s's Alarm?"), name.c_str()) ) {
is_alarm_on = true;
} else {
add_msg(_("You leave the controls alone."));
}
}
} else {
add_msg(_("You could use a screwdriver to hotwire it."));
}
}
return !(is_locked);
}
void vehicle::smash_security_system(){
//get security and controls location
int s = -1;
int c = -1;
for (size_t d = 0; d < speciality.size(); d++){
int p = speciality[d];
if( part_flag( p, "SECURITY" ) && !parts[ p ].is_broken() ) {
s = p;
c = part_with_feature(s, "CONTROLS");
break;
}
}
//controls and security must both be valid
if (c >= 0 && s >= 0){
///\EFFECT_MECHANICS reduces chance of damaging controls when smashing security system
int skill = g->u.get_skill_level( skill_mechanics );
int percent_controls = 70 / (1 + skill);
int percent_alarm = (skill+3) * 10;
int rand = rng(1,100);
if (percent_controls > rand) {
damage_direct (c, part_info(c).durability / 4);
if( parts[ c ].removed || parts[ c ].is_broken() ) {
g->u.controlling_vehicle = false;
is_alarm_on = false;
add_msg(_("You destroy the controls..."));
} else {
add_msg(_("You damage the controls."));
}
}
if (percent_alarm > rand) {
damage_direct (s, part_info(s).durability / 5);
// chance to disable alarm immediately, or disable on destruction
if( percent_alarm / 4 > rand || parts[ s ].is_broken() ) {
is_alarm_on = false;
}
}
add_msg((is_alarm_on) ? _("The alarm keeps going.") : _("The alarm stops."));
} else {
debugmsg("No security system found on vehicle.");
}
}
void vehicle::use_controls( const tripoint &pos )
{
std::vector<uimenu_entry> options;
std::vector<std::function<void()>> actions;
auto const keybind = [&]( std::string const &opt ) {
auto const keys = input_context( "VEHICLE" ).keys_bound_to( opt );
return keys.empty() ? ' ' : keys.front();
};
bool remote = g->remoteveh() == this;
bool has_electronic_controls = false;
if( remote ) {
options.emplace_back( _( "Stop controlling" ), keybind( "RELEASE_CONTROLS" ) );
actions.push_back( [&]{
g->u.controlling_vehicle = false;
g->setremoteveh( nullptr );
add_msg( _( "You stop controlling the vehicle." ) );
refresh();
} );
has_electronic_controls = has_part( "CTRL_ELECTRONIC" ) || has_part( "REMOTE_CONTROLS" );
} else if( g->m.veh_at( pos ) == this ) {
if( g->u.controlling_vehicle ) {
options.emplace_back( _( "Let go of controls" ), keybind( "RELEASE_CONTROLS" ) );
actions.push_back( [&]{
g->u.controlling_vehicle = false;
add_msg( _( "You let go of the controls." ) );
refresh();
} );
}
has_electronic_controls = !get_parts( pos, "CTRL_ELECTRONIC" ).empty();
}
if( get_parts( pos, "CONTROLS" ).empty() && !has_electronic_controls ) {
add_msg( m_info, _( "No controls there" ) );
return;
}
if( has_part( "ENGINE" ) ) {
if( g->u.controlling_vehicle || ( remote && engine_on ) ) {
options.emplace_back( _( "Stop driving" ), keybind( "TOGGLE_ENGINE" ) );
actions.push_back( [&] {
if( engine_on && has_engine_type_not( fuel_type_muscle, true ) ){
add_msg( _( "You turn the engine off and let go of the controls." ) );
} else {
add_msg( _( "You let go of the controls." ) );
}
engine_on = false;
g->u.controlling_vehicle = false;
g->setremoteveh( nullptr );
refresh();
} );
} else if( has_engine_type_not(fuel_type_muscle, true ) ) {
options.emplace_back( engine_on ? _( "Turn off the engine" ) : _( "Turn on the engine" ), keybind( "TOGGLE_ENGINE" ) );
actions.push_back( [&] {
if( engine_on ) {
engine_on = false;
add_msg( _( "You turn the engine off." ) );
} else {
start_engines();
}
refresh();
} );
}
}
if( has_part( "HORN") ) {
options.emplace_back( _( "Honk horn" ), keybind( "SOUND_HORN" ) );
actions.push_back( [&]{ honk_horn(); refresh(); } );
}
auto add_toggle = [&]( const std::string &name, char key, const std::string &flag ) {
// fetch matching parts and abort early if none found
auto found = get_parts( flag );
if( found.empty() ) {
return;
}
// can this menu option be selected by the user?
bool allow = true;
// determine target state - currently parts of similar type are all switched concurrently
bool state = std::none_of( found.begin(), found.end(), []( const vehicle_part *e ) {
return e->enabled;
} );
// if toggled part potentially usable check if could be enabled now (sufficient fuel etc.)
if( state ) {
allow = std::any_of( found.begin(), found.end(), [&]( const vehicle_part *e ) {
return can_enable( *e );
} );
}
auto msg = string_format( state ? _( "Turn on %s" ) : _( "Turn off %s" ), name.c_str() );
options.emplace_back( -1, allow, key, msg );
actions.push_back( [=]{
for( vehicle_part *e : found ) {
if( e->enabled != state ) {
add_msg( state ? _( "Turned on %s" ) : _( "Turned off %s." ), e->name().c_str() );
e->enabled = state;
}
}
refresh();
} );
};
add_toggle( _( "reactor" ), keybind( "TOGGLE_REACTOR" ), "REACTOR" );
if( has_electronic_controls ) {
add_toggle( _( "headlights" ), keybind( "TOGGLE_HEADLIGHT" ), "CONE_LIGHT" );
add_toggle( _( "overhead lights" ), keybind( "TOGGLE_OVERHEAD_LIGHT" ), "CIRCLE_LIGHT" );
add_toggle( _( "aisle lights" ), keybind( "TOGGLE_AISLE_LIGHT" ), "AISLE_LIGHT" );
add_toggle( _( "dome lights" ), keybind( "TOGGLE_DOME_LIGHT" ), "DOME_LIGHT" );
add_toggle( _( "atomic lights" ), keybind( "TOGGLE_ATOMIC_LIGHT" ), "ATOMIC_LIGHT" );
add_toggle( _( "stereo" ), keybind( "TOGGLE_STEREO" ), "STEREO" );
add_toggle( _( "chimes" ), keybind( "TOGGLE_CHIMES" ), "CHIMES" );
add_toggle( _( "fridge" ), keybind( "TOGGLE_FRIDGE" ), "FRIDGE" );
add_toggle( _( "recharger" ), keybind( "TOGGLE_RECHARGER" ), "RECHARGE" );
add_toggle( _( "plow" ), keybind( "TOGGLE_PLOW" ), "PLOW" );
add_toggle( _( "reaper" ), keybind( "TOGGLE_REAPER" ), "REAPER" );
add_toggle( _( "planter" ), keybind( "TOGGLE_PLANTER" ), "PLANTER" );
add_toggle( _( "scoop" ), keybind( "TOGGLE_SCOOP" ), "SCOOP" );
add_toggle( _( "water purifier" ), keybind( "TOGGLE_WATER_PURIFIER" ), "WATER_PURIFIER" );
if( has_part( "DOOR_MOTOR" ) ) {
options.emplace_back( _( "Toggle doors" ), keybind( "TOGGLE_DOORS" ) );
actions.push_back( [&]{ control_doors(); refresh(); } );
}
options.emplace_back( cruise_on ? _( "Disable cruise control" ) : _( "Enable cruise control" ),
keybind( "TOGGLE_CRUISE_CONTROL" ) );
actions.emplace_back( [&]{
cruise_on = !cruise_on;
add_msg( cruise_on ? _( "Cruise control turned on" ) : _( "Cruise control turned off" ) );
refresh();
} );
}
options.emplace_back( tracking_on ? _( "Forget vehicle position" ) : _( "Remember vehicle position" ),
keybind( "TOGGLE_TRACKING" ) );
actions.push_back( [&] {
if( tracking_on ) {
overmap_buffer.remove_vehicle( this );
tracking_on = false;
add_msg( _( "You stop keeping track of the vehicle position." ) );
} else {
overmap_buffer.add_vehicle( this );
tracking_on = true;
add_msg( _( "You start keeping track of this vehicle's position." ) );
}
refresh();
} );
if( ( is_foldable() || tags.count( "convertible" ) ) && !remote ) {
options.emplace_back( string_format( _( "Fold %s" ), name.c_str() ), keybind( "FOLD_VEHICLE" ) );
actions.push_back( [&]{ fold_up(); } );
}
if( has_part( "ENGINE" ) ) {
options.emplace_back( _( "Control individual engines" ), keybind( "CONTROL_ENGINES" ) );
actions.push_back( [&]{ control_engines(); refresh(); } );
}
if( is_alarm_on ) {
if( velocity == 0 && !remote ) {
options.emplace_back( _( "Try to disarm alarm." ), keybind( "TOGGLE_ALARM" ) );
actions.push_back( [&]{ smash_security_system(); refresh(); } );
} else if( has_electronic_controls && has_part( "SECURITY" ) ) {
options.emplace_back( _( "Trigger alarm" ), keybind( "TOGGLE_ALARM" ) );
actions.push_back( [&]{
is_alarm_on = true;
add_msg( _( "You trigger the alarm" ) );
refresh();
} );
}
}
if( has_part( "TURRET" ) ) {
options.emplace_back( _( "Set turret targeting modes" ), keybind( "TURRET_TARGET_MODE" ) );
actions.push_back( [&]{ turrets_set_targeting(); refresh(); } );
options.emplace_back( _( "Set turret firing modes" ), keybind( "TURRET_FIRE_MODE" ) );
actions.push_back( [&]{ turrets_set_mode(); refresh(); } );
// We can also fire manual turrets with ACTION_FIRE while standing at the controls.
options.emplace_back( _( "Aim turrets manually" ), keybind( "TURRET_MANUAL_AIM" ) );
actions.push_back( [&]{ turrets_aim_and_fire( true, false ); refresh(); } );
// This lets us manually override and set the target for the automatic turrets instead.
options.emplace_back( _( "Aim automatic turrets" ), keybind( "TURRET_MANUAL_OVERRIDE" ) );
actions.push_back( [&]{ turrets_aim( false, true ); refresh(); } );
options.emplace_back( _( "Aim individual turret" ), keybind( "TURRET_SINGLE_FIRE" ) );
actions.push_back( [&]{ turrets_aim_single(); refresh(); } );
}
if( has_electronic_controls && (camera_on || ( has_part( "CAMERA" ) && has_part( "CAMERA_CONTROL" ) ) ) ) {
options.emplace_back( camera_on ? _( "Turn off camera system" ) : _( "Turn on camera system" ), keybind( "TOGGLE_CAMERA") );
actions.push_back( [&]{
if( camera_on ) {
camera_on = false;
add_msg( _("Camera system disabled") );
} else if( fuel_left(fuel_type_battery, true) ) {
camera_on = true;
add_msg( _("Camera system enabled") );
} else {
add_msg( _("Camera system won't turn on") );
}
refresh();
} );
}
if( !interact_vehicle_locked() ) {
return;
}
uimenu menu;
menu.return_invalid = true;
menu.text = _( "Vehicle controls" );
menu.entries = options;
menu.query();
if( menu.ret >= 0 ) {
actions[menu.ret]();
// Don't access `this` from here on, one of the actions above is to call
// fold_up(), which may have deleted `this` object.
}
}
bool vehicle::fold_up() {
const bool can_be_folded = is_foldable();
const bool is_convertible = (tags.count("convertible") > 0);
if( ! ( can_be_folded || is_convertible ) ) {
debugmsg(_("Tried to fold non-folding vehicle %s"), name.c_str());
return false;
}
if( g->u.controlling_vehicle ) {
add_msg(m_warning, _("As the pitiless metal bars close on your nether regions, you reconsider trying to fold the %s while riding it."), name.c_str());
return false;
}
if( velocity > 0 ) {
add_msg(m_warning, _("You can't fold the %s while it's in motion."), name.c_str());
return false;
}
add_msg(_("You painstakingly pack the %s into a portable configuration."), name.c_str());
std::string itype_id = "folding_bicycle";
for( const auto &elem : tags ) {
if( elem.compare( 0, 12, "convertible:" ) == 0 ) {
itype_id = elem.substr( 12 );
break;
}
}
// create a folding [non]bicycle item
item bicycle( can_be_folded ? "generic_folded_vehicle" : "folding_bicycle", calendar::turn );
// Drop stuff in containers on ground
for (size_t p = 0; p < parts.size(); p++) {
if( part_flag( p, "CARGO" ) ) {
for( auto &elem : get_items(p) ) {
g->m.add_item_or_charges( g->u.pos(), elem );
}
while( !get_items(p).empty() ) {
get_items(p).erase( get_items(p).begin() );
}
}
}
unboard_all();
// Store data of all parts, iuse::unfold_bicyle only loads
// some of them, some are expect to be
// vehicle specific and therefore constant (like id, mount).
// Writing everything here is easier to manage, as only
// iuse::unfold_bicyle has to adopt to changes.
try {
std::ostringstream veh_data;
JsonOut json(veh_data);
json.write(parts);
bicycle.set_var( "folding_bicycle_parts", veh_data.str() );
} catch( const JsonError &e ) {
debugmsg("Error storing vehicle: %s", e.c_str());
}
if (can_be_folded) {
bicycle.set_var( "weight", to_gram( total_mass() ) );
bicycle.set_var( "volume", total_folded_volume() / units::legacy_volume_factor );
bicycle.set_var( "name", string_format(_("folded %s"), name.c_str()) );
bicycle.set_var( "vehicle_name", name );
// TODO: a better description?
bicycle.set_var( "description", string_format(_("A folded %s."), name.c_str()) );
}
g->m.add_item_or_charges( g->u.pos(), bicycle );
g->m.destroy_vehicle(this);
// TODO: take longer to fold bigger vehicles
// TODO: make this interruptible
g->u.moves -= 500;
return true;
}
double vehicle::engine_cold_factor( const int e ) const
{
if( !is_engine_type( e, fuel_type_diesel ) ) { return 0.0; }
int eff_temp = g->get_temperature();
if( !parts[ engines[ e ] ].faults().count( fault_glowplug ) ) {
eff_temp = std::min( eff_temp, 20 );
}
return 1.0 - (std::max( 0, std::min( 30, eff_temp ) ) / 30.0);
}
int vehicle::engine_start_time( const int e ) const
{
if( !is_engine_on( e ) || is_engine_type( e, fuel_type_muscle ) ||
!fuel_left( part_info( engines[e] ).fuel_type ) ) { return 0; }
const double dmg = 1.0 - double( parts[engines[e]].hp() ) / part_info( engines[e] ).durability;
// non-linear range [100-1000]; f(0.0) = 100, f(0.6) = 250, f(0.8) = 500, f(0.9) = 1000
// diesel engines with working glow plugs always start with f = 0.6 (or better)
const int cold = ( 1 / tanh( 1 - std::min( engine_cold_factor( e ), 0.9 ) ) ) * 100;
return ( part_power( engines[ e ], true ) / 16 ) + ( 100 * dmg ) + cold;
}
bool vehicle::start_engine( const int e )
{
if( !is_engine_on( e ) ) { return false; }
const vpart_info &einfo = part_info( engines[e] );
const vehicle_part &eng = parts[ engines[ e ] ];
if( fuel_left( einfo.fuel_type ) <= 0 && einfo.fuel_type != fuel_type_none ) {
if( einfo.fuel_type == fuel_type_muscle ) {
add_msg( _("The %s's mechanism is out of reach!"), name.c_str() );
} else {
add_msg( _("Looks like the %1$s is out of %2$s."), eng.name().c_str(),
item::nname( einfo.fuel_type ).c_str() );
}
return false;
}
const double dmg = 1.0 - ((double)parts[engines[e]].hp() / einfo.durability);
const int engine_power = part_power( engines[e], true );
const double cold_factor = engine_cold_factor( e );
if( einfo.fuel_type != fuel_type_muscle ) {
if( einfo.fuel_type == fuel_type_gasoline && dmg > 0.75 && one_in( 20 ) ) {
backfire( e );
} else {
const tripoint pos = global_part_pos3( engines[e] );
sounds::ambient_sound( pos, engine_start_time( e ) / 10, "" );
}
}
// Immobilizers need removing before the vehicle can be started
if( eng.faults().count( fault_immobiliser ) ) {
add_msg( _( "The %s makes a long beeping sound." ), eng.name().c_str() );
return false;
}
// Engine with starter motors can fail on both battery and starter motor
if( eng.faults_potential().count( fault_starter ) ) {
if( eng.faults().count( fault_starter ) ) {
add_msg( _( "The %s makes a single clicking sound." ), eng.name().c_str() );
return false;
}
const int penalty = ( engine_power * dmg / 2 ) + ( engine_power * cold_factor / 5 );
if( discharge_battery( ( engine_power + penalty ) / 10, true ) != 0 ) {
add_msg( _( "The %s makes a rapid clicking sound." ), eng.name().c_str() );
return false;
}
}
// Engines always fail to start with faulty fuel pumps
if( eng.faults().count( fault_pump ) || eng.faults().count( fault_diesel ) ) {
add_msg( _( "The %s quickly stutters out." ), eng.name().c_str() );
return false;
}
// Damaged engines have a chance of failing to start
if( x_in_y( dmg * 100, 120 ) ) {
add_msg( _( "The %s makes a terrible clanking sound." ), eng.name().c_str() );
return false;
}
return true;
}
void vehicle::start_engines( const bool take_control )
{
bool has_engine = std::any_of( engines.begin(), engines.end(), [&]( int idx ) {
return parts[ idx ].enabled && !parts[ idx ].is_broken();
} );
// if no engines enabled then enable all before trying to start the vehicle
if( !has_engine ) {
for( auto idx : engines ) {
if( !parts[ idx ].is_broken() ) {
parts[ idx ].enabled = true;
}
}
}
int start_time = 0;
// record the first usable engine as the referenced position checked at the end of the engine starting activity
bool has_starting_engine_position = false;
tripoint starting_engine_position;
for( size_t e = 0; e < engines.size(); ++e ) {
if( !has_starting_engine_position && !parts[ engines[ e ] ].is_broken() && parts[ engines[ e ] ].enabled ) {
starting_engine_position = global_part_pos3( engines[ e ] );
has_starting_engine_position = true;
}
has_engine = has_engine || is_engine_on( e );
start_time = std::max( start_time, engine_start_time( e ) );
}
if(!has_starting_engine_position){
starting_engine_position = global_pos3();
}
if( !has_engine ) {
add_msg( m_info, _("The %s doesn't have an engine!"), name.c_str() );
return;
}
if( take_control && !g->u.controlling_vehicle ) {
g->u.controlling_vehicle = true;
add_msg( _("You take control of the %s."), name.c_str() );
}
g->u.assign_activity( activity_id( "ACT_START_ENGINES" ), start_time );
g->u.activity.placement = starting_engine_position - g->u.pos();
g->u.activity.values.push_back( take_control );
}
void vehicle::backfire( const int e ) const
{
const int power = part_power( engines[e], true );
const tripoint pos = global_part_pos3( engines[e] );
//~ backfire sound
sounds::ambient_sound( pos, 40 + (power / 30), _( "BANG!" ) );
}
void vehicle::honk_horn()
{
const bool no_power = ! fuel_left( fuel_type_battery, true );
bool honked = false;
for( size_t p = 0; p < parts.size(); ++p ) {
if( ! part_flag( p, "HORN" ) ) {
continue;
}
//Only bicycle horn doesn't need electricity to work
const vpart_info &horn_type = part_info( p );
if( ( horn_type.get_id() != vpart_id( "horn_bicycle" ) ) && no_power ) {
continue;
}
if( ! honked ) {
add_msg( _("You honk the horn!") );
honked = true;
}
//Get global position of horn
const auto horn_pos = global_part_pos3( p );
//Determine sound
if( horn_type.bonus >= 110 ) {
//~ Loud horn sound
sounds::sound( horn_pos, horn_type.bonus, _("HOOOOORNK!") );
} else if( horn_type.bonus >= 80 ) {
//~ Moderate horn sound
sounds::sound( horn_pos, horn_type.bonus, _("BEEEP!") );
} else {
//~ Weak horn sound
sounds::sound( horn_pos, horn_type.bonus, _("honk.") );
}
}
if( ! honked ) {
add_msg( _("You honk the horn, but nothing happens.") );
}
}
void vehicle::beeper_sound()
{
// No power = no sound
if( fuel_left( fuel_type_battery, true ) == 0 ) {
return;
}
const bool odd_turn = calendar::once_every( 2_turns );
for( size_t p = 0; p < parts.size(); ++p ) {
if( !part_flag( p, "BEEPER" ) ) {
continue;
}
if( ( odd_turn && part_flag( p, VPFLAG_EVENTURN ) ) ||
( !odd_turn && part_flag( p, VPFLAG_ODDTURN ) ) ) {
continue;
}
const vpart_info &beeper_type = part_info( p );
//~ Beeper sound
sounds::sound( global_part_pos3( p ), beeper_type.bonus, _( "beep!" ) );
}
}
void vehicle::play_music()
{
for( auto e : get_parts( "STEREO", true ) ) {
iuse::play_music( g->u, global_part_pos3( *e ), 15, 30 );
}
}
void vehicle::play_chimes()
{
if( !one_in( 3 ) ) {
return;
}
for( auto e : get_parts( "CHIMES", true ) ) {
sounds::sound( global_part_pos3( *e ), 40, _( "a simple melody blaring from the loudspeakers." ) );
}
}
const vpart_info& vehicle::part_info (int index, bool include_removed) const
{
if (index < (int)parts.size()) {
if (!parts[index].removed || include_removed) {
return parts[index].info();
}
}
return vpart_id::NULL_ID().obj();
}
// engines & alternators all have power.
// engines provide, whilst alternators consume.
int vehicle::part_power(int const index, bool const at_full_hp) const
{
if( !part_flag(index, VPFLAG_ENGINE) &&
!part_flag(index, VPFLAG_ALTERNATOR) ) {
return 0; // not an engine.
}
const vehicle_part& vp = parts[ index ];
int pwr = vp.base.engine_displacement();
if( pwr == 0 ) {
pwr = vp.info().power;
}
if (part_info(index).fuel_type == fuel_type_muscle) {
int pwr_factor = (part_flag(index, "MUSCLE_LEGS") ? 5 : 0) +
(part_flag(index, "MUSCLE_ARMS") ? 2 : 0);
///\EFFECT_STR increases power produced for MUSCLE_* vehicles
pwr += int(((g->u).str_cur - 8) * pwr_factor);
}
if( pwr < 0 ) {
return pwr; // Consumers always draw full power, even if broken
}
if( at_full_hp ) {
return pwr; // Assume full hp
}
// Damaged engines give less power, but gas/diesel handle it better
if( part_info(index).fuel_type == fuel_type_gasoline ||
part_info(index).fuel_type == fuel_type_diesel ) {
return pwr * (0.25 + (0.75 * ((double)parts[index].hp() / part_info(index).durability)));
} else {
return double( pwr * parts[index].hp() ) / part_info(index).durability;
}
}
// alternators, solar panels, reactors, and accessories all have epower.
// alternators, solar panels, and reactors provide, whilst accessories consume.
int vehicle::part_epower(int const index) const
{
int e = part_info(index).epower;
if( e < 0 ) {
return e; // Consumers always draw full power, even if broken
}
return e * parts[ index ].hp() / part_info(index).durability;
}
int vehicle::epower_to_power(int const epower)
{
// Convert epower units (watts) to power units
// Used primarily for calculating battery charge/discharge
// TODO: convert batteries to use energy units based on watts (watt-ticks?)
constexpr int conversion_factor = 373; // 373 epower == 373 watts == 1 power == 0.5 HP
int power = epower / conversion_factor;
// epower remainder results in chance at additional charge/discharge
if (x_in_y(abs(epower % conversion_factor), conversion_factor)) {
power += epower >= 0 ? 1 : -1;
}
return power;
}
int vehicle::power_to_epower(int const power)
{
// Convert power units to epower units (watts)
// Used primarily for calculating battery charge/discharge
// TODO: convert batteries to use energy units based on watts (watt-ticks?)
constexpr int conversion_factor = 373; // 373 epower == 373 watts == 1 power == 0.5 HP
return power * conversion_factor;
}
bool vehicle::has_structural_part(int const dx, int const dy) const
{
std::vector<int> parts_here = parts_at_relative(dx, dy, false);
for( auto &elem : parts_here ) {
if( part_info( elem ).location == part_location_structure &&
!part_info( elem ).has_flag( "PROTRUSION" ) ) {
return true;
}
}
return false;
}
/**
* Returns whether or not the vehicle has a structural part queued for removal,
* @return true if a structural is queue for removal, false if not.
* */
bool vehicle::is_structural_part_removed() const
{
for( size_t i = 0; i < parts.size(); ++i ) {
if (parts[i].removed &&
part_info( i, true ).location == part_location_structure) {
return true;
}
}
return false;
}
/**
* Returns whether or not the vehicle part with the given id can be mounted in
* the specified square.
* @param dx The local x-coordinate to mount in.
* @param dy The local y-coordinate to mount in.
* @param id The id of the part to install.
* @return true if the part can be mounted, false if not.
*/
bool vehicle::can_mount(int const dx, int const dy, const vpart_id &id) const
{
//The part has to actually exist.
if( !id.is_valid() ) {
return false;
}
//It also has to be a real part, not the null part
const vpart_info &part = id.obj();
if(part.has_flag("NOINSTALL")) {
return false;
}
const std::vector<int> parts_in_square = parts_at_relative(dx, dy, false);
//First part in an empty square MUST be a structural part
if(parts_in_square.empty() && part.location != part_location_structure) {
return false;
}
//No other part can be placed on a protrusion
if(!parts_in_square.empty() && part_info(parts_in_square[0]).has_flag("PROTRUSION")) {
return false;
}
//No part type can stack with itself, or any other part in the same slot
for( const auto &elem : parts_in_square ) {
const vpart_info &other_part = parts[elem].info();
//Parts with no location can stack with each other (but not themselves)
if( part.get_id() == other_part.get_id() ||
(!part.location.empty() && part.location == other_part.location)) {
return false;
}
// Until we have an interface for handling multiple components with CARGO space,
// exclude them from being mounted in the same tile.
if( part.has_flag( "CARGO" ) && other_part.has_flag( "CARGO" ) ) {
return false;
}
}
// All parts after the first must be installed on or next to an existing part
// the exception is when a single tile only structural object is being repaired
if(!parts.empty()) {
if(!is_structural_part_removed() &&
!has_structural_part(dx, dy) &&
!has_structural_part(dx+1, dy) &&
!has_structural_part(dx, dy+1) &&
!has_structural_part(dx-1, dy) &&
!has_structural_part(dx, dy-1)) {
return false;
}
}
// only one muscle engine allowed
if( part.has_flag(VPFLAG_ENGINE) && part.fuel_type == fuel_type_muscle &&
has_engine_type(fuel_type_muscle, false) ) {
return false;
}
// Alternators must be installed on a gas engine
if(part.has_flag(VPFLAG_ALTERNATOR)) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( VPFLAG_ENGINE ) &&
( part_info( elem ).fuel_type == fuel_type_gasoline ||
part_info( elem ).fuel_type == fuel_type_diesel ||
part_info( elem ).fuel_type == fuel_type_muscle)) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
//Seatbelts must be installed on a seat
if(part.has_flag("SEATBELT")) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "BELTABLE" ) ) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
//Internal must be installed into a cargo area.
if(part.has_flag("INTERNAL")) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "CARGO" ) ) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
// curtains must be installed on (reinforced)windshields
// TODO: do this automatically using "location":"on_mountpoint"
if (part.has_flag("CURTAIN")) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "WINDOW" ) ) {
anchor_found = true;
}
}
if (!anchor_found) {
return false;
}
}
// Security system must be installed on controls
if(part.has_flag("ON_CONTROLS")) {
bool anchor_found = false;
for( std::vector<int>::const_iterator it = parts_in_square.begin();
it != parts_in_square.end(); ++it ) {
if(part_info(*it).has_flag("CONTROLS")) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
// Cargo locks must go on lockable cargo containers
// TODO: do this automatically using "location":"on_mountpoint"
if(part.has_flag("CARGO_LOCKING")) {
bool anchor_found = false;
for( std::vector<int>::const_iterator it = parts_in_square.begin();
it != parts_in_square.end(); ++it ) {
if(part_info(*it).has_flag("LOCKABLE_CARGO")) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
//Swappable storage battery must be installed on a BATTERY_MOUNT
if(part.has_flag("NEEDS_BATTERY_MOUNT")) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "BATTERY_MOUNT" ) ) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
//Door motors need OPENABLE
if( part.has_flag( "DOOR_MOTOR" ) ) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "OPENABLE" ) ) {
anchor_found = true;
}
}
if(!anchor_found) {
return false;
}
}
//Mirrors cannot be mounted on OPAQUE parts
if( part.has_flag( "VISION" ) && !part.has_flag( "CAMERA" ) ) {
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "OPAQUE" ) ) {
return false;
}
}
}
//and vice versa
if( part.has_flag( "OPAQUE" ) ) {
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "VISION" ) &&
!part_info( elem ).has_flag( "CAMERA" ) ) {
return false;
}
}
}
//Turrets must be installed on a turret mount
if( part.has_flag( "TURRET" ) ) {
bool anchor_found = false;
for( const auto &elem : parts_in_square ) {
if( part_info( elem ).has_flag( "TURRET_MOUNT" ) ) {
anchor_found = true;
break;
}
}
if( !anchor_found ) {
return false;
}
}
//Anything not explicitly denied is permitted
return true;
}
bool vehicle::can_unmount(int const p) const
{
if(p < 0 || p > (int)parts.size()) {
return false;
}
int dx = parts[p].mount.x;
int dy = parts[p].mount.y;
std::vector<int> parts_in_square = parts_at_relative(dx, dy, false);
// Can't remove an engine if there's still an alternator there
if(part_flag(p, VPFLAG_ENGINE) && part_with_feature(p, VPFLAG_ALTERNATOR) >= 0) {
return false;
}
//Can't remove a seat if there's still a seatbelt there
if(part_flag(p, "BELTABLE") && part_with_feature(p, "SEATBELT") >= 0) {
return false;
}
// Can't remove a window with curtains still on it
if(part_flag(p, "WINDOW") && part_with_feature(p, "CURTAIN") >=0) {
return false;
}
//Can't remove controls if there's something attached
if(part_flag(p, "CONTROLS") && part_with_feature(p, "ON_CONTROLS") >= 0) {
return false;
}
//Can't remove a battery mount if there's still a battery there
if(part_flag(p, "BATTERY_MOUNT") && part_with_feature(p, "NEEDS_BATTERY_MOUNT") >= 0) {
return false;
}
//Can't remove a turret mount if there's still a turret there
if( part_flag( p, "TURRET_MOUNT" ) && part_with_feature( p, "TURRET" ) >= 0 ) {
return false;
}
//Structural parts have extra requirements
if(part_info(p).location == part_location_structure) {
/* To remove a structural part, there can be only structural parts left
* in that square (might be more than one in the case of wreckage) */
for( auto &elem : parts_in_square ) {
if( part_info( elem ).location != part_location_structure ) {
return false;
}
}
//If it's the last part in the square...
if(parts_in_square.size() == 1) {
/* This is the tricky part: We can't remove a part that would cause
* the vehicle to 'break into two' (like removing the middle section
* of a quad bike, for instance). This basically requires doing some
* breadth-first searches to ensure previously connected parts are
* still connected. */
//First, find all the squares connected to the one we're removing
std::vector<vehicle_part> connected_parts;
for(int i = 0; i < 4; i++) {
int next_x = i < 2 ? (i == 0 ? -1 : 1) : 0;
int next_y = i < 2 ? 0 : (i == 2 ? -1 : 1);
std::vector<int> parts_over_there = parts_at_relative(dx + next_x, dy + next_y, false);
//Ignore empty squares
if(!parts_over_there.empty()) {
//Just need one part from the square to track the x/y
connected_parts.push_back(parts[parts_over_there[0]]);
}
}
/* If size = 0, it's the last part of the whole vehicle, so we're OK
* If size = 1, it's one protruding part (ie, bicycle wheel), so OK
* Otherwise, it gets complicated... */
if(connected_parts.size() > 1) {
/* We'll take connected_parts[0] to be the target part.
* Every other part must have some path (that doesn't involve
* the part about to be removed) to the target part, in order
* for the part to be legally removable. */
for(auto const &next_part : connected_parts) {
if(!is_connected(connected_parts[0], next_part, parts[p])) {
//Removing that part would break the vehicle in two
return false;
}
}
}
}
}
//Anything not explicitly denied is permitted
return true;
}
/**
* Performs a breadth-first search from one part to another, to see if a path
* exists between the two without going through the excluded part. Used to see
* if a part can be legally removed.
* @param to The part to reach.
* @param from The part to start the search from.
* @param excluded_part The part that is being removed and, therefore, should not
* be included in the path.
* @return true if a path exists without the excluded part, false otherwise.
*/
bool vehicle::is_connected(vehicle_part const &to, vehicle_part const &from, vehicle_part const &excluded_part) const
{
const auto target = to.mount;
const auto excluded = excluded_part.mount;
//Breadth-first-search components
std::list<vehicle_part> discovered;
vehicle_part current_part;
std::list<vehicle_part> searched;
//We begin with just the start point
discovered.push_back(from);
while(!discovered.empty()) {
current_part = discovered.front();
discovered.pop_front();
auto current = current_part.mount;
for(int i = 0; i < 4; i++) {
point next( current.x + (i < 2 ? (i == 0 ? -1 : 1) : 0),
current.y + (i < 2 ? 0 : (i == 2 ? -1 : 1)) );
if( next == target ) {
//Success!
return true;
} else if( next == excluded ) {
//There might be a path, but we're not allowed to go that way
continue;
}
std::vector<int> parts_there = parts_at_relative(next.x, next.y);
if(!parts_there.empty()) {
vehicle_part next_part = parts[parts_there[0]];
//Only add the part if we haven't been here before
bool found = false;
for( auto &elem : discovered ) {
if( elem.mount == next ) {
found = true;
break;
}
}
if(!found) {
for( auto &elem : searched ) {
if( elem.mount == next ) {
found = true;
break;
}
}
}
if(!found) {
discovered.push_back(next_part);
}
}
}
//Now that that's done, we've finished exploring here
searched.push_back(current_part);
}
//If we completely exhaust the discovered list, there's no path
return false;
}
/**
* Installs a part into this vehicle.
* @param dx The x coordinate of where to install the part.
* @param dy The y coordinate of where to install the part.
* @param id The string ID of the part to install. (see vehicle_parts.json)
* @param force Skip check of whether we can mount the part here.
* @return false if the part could not be installed, true otherwise.
*/
int vehicle::install_part( int dx, int dy, const vpart_id &id, bool force )
{
if( !( force || can_mount( dx, dy, id ) ) ) {
return -1;
}
return install_part( dx, dy, vehicle_part( id, dx, dy, item( id.obj().item ) ) );
}
int vehicle::install_part( int dx, int dy, const vpart_id &id, item&& obj, bool force )
{
if( !( force || can_mount ( dx, dy, id ) ) ) {
return -1;
}
return install_part(dx, dy, vehicle_part( id, dx, dy, std::move( obj ) ) );
}
int vehicle::install_part( int dx, int dy, const vehicle_part &new_part )
{
// Should be checked before installing the part
bool enable = false;
if( new_part.is_engine() ) {
enable = true;
} else {
// @todo: read toggle groups from JSON
static const std::vector<std::string> enable_like = {{
"CONE_LIGHT",
"CIRCLE_LIGHT",
"AISLE_LIGHT",
"DOME_LIGHT",
"ATOMIC_LIGHT",
"STEREO",
"CHIMES",
"FRIDGE",
"RECHARGE",
"PLOW",
"REAPER",
"PLANTER",
"SCOOP",
"WATER_PURIFIER"
}};
for( const std::string &flag : enable_like ) {
if( new_part.info().has_flag( flag ) ) {
enable = has_part( flag, true );
break;
}
}
}
parts.push_back( new_part );
auto &pt = parts.back();
pt.enabled = enable;
pt.mount.x = dx;
pt.mount.y = dy;
refresh();
return parts.size() - 1;
}
/**
* Mark a part as removed from the vehicle.
* @return bool true if the vehicle's 0,0 point shifted.
*/
bool vehicle::remove_part( int p )
{
if( p >= (int)parts.size() ) {
debugmsg("Tried to remove part %d but only %d parts!", p, parts.size());
return false;
}
if( parts[p].removed ) {
/* This happens only when we had to remove part, because it was depending on
* other part (using recursive remove_part() call) - currently curtain
* depending on presence of window and seatbelt depending on presence of seat.
*/
return false;
}
int x = parts[p].precalc[0].x;
int y = parts[p].precalc[0].y;
tripoint part_loc( global_x() + x, global_y() + y, smz );
// If `p` has flag `parent_flag`, remove child with flag `child_flag`
// Returns true if removal occurs
const auto remove_dependent_part = [&]( const std::string& parent_flag,
const std::string& child_flag ) {
if( part_flag( p, parent_flag ) ) {
int dep = part_with_feature( p, child_flag, false );
if( dep >= 0 ) {
item it = parts[dep].properties_to_item();
g->m.add_item_or_charges( part_loc, it );
remove_part( dep );
return true;
}
}
return false;
};
// if a windshield is removed (usually destroyed) also remove curtains
// attached to it.
if( remove_dependent_part( "WINDOW", "CURTAIN" ) || part_flag( p, VPFLAG_OPAQUE ) ) {
g->m.set_transparency_cache_dirty( smz );
}
remove_dependent_part( "SEAT", "SEATBELT" );
remove_dependent_part( "BATTERY_MOUNT", "NEEDS_BATTERY_MOUNT" );
// Unboard any entities standing on removed boardable parts
if( part_flag( p, "BOARDABLE" ) ) {
std::vector<int> bp = boarded_parts();
for( auto &elem : bp ) {
if( elem == p ) {
g->m.unboard_vehicle( part_loc );
}
}
}
// Update current engine configuration if needed
if( part_flag( p, "ENGINE" ) && engines.size() > 1 ) {
bool any_engine_on = false;
for( auto &e : engines ) {
if( e != p && is_part_on( e ) ) {
any_engine_on = true;
break;
}
}
if( !any_engine_on ) {
engine_on = false;
for( auto &e : engines ) {
toggle_specific_part( e, true );
}
}
}
parts[p].removed = true;
removed_part_count++;
// If the player is currently working on the removed part, stop them as it's futile now.
const player_activity &act = g->u.activity;
if( act.id() == activity_id( "ACT_VEHICLE" ) && act.moves_left > 0 && act.values.size() > 6 ) {
if( g->m.veh_at( tripoint( act.values[0], act.values[1], g->u.posz() ) ) == this ) {
if( act.values[6] >= p ) {
g->u.cancel_activity();
add_msg( m_info, _( "The vehicle part you were working on has gone!" ) );
}
}
}
const auto iter = labels.find( label( parts[p].mount.x, parts[p].mount.y ) );
const bool no_label = iter != labels.end();
const bool grab_found = g->u.grab_type == OBJECT_VEHICLE && g->u.grab_point == part_loc;
// Checking these twice to avoid calling the relatively expensive parts_at_relative() unnecessarily.
if( no_label || grab_found ) {
if( parts_at_relative( parts[p].mount.x, parts[p].mount.y, false ).empty() ) {
if( no_label ) {
labels.erase( iter );
}
if( grab_found ) {
add_msg( m_info, _( "The vehicle part you were holding has been destroyed!" ) );
g->u.grab_type = OBJECT_NONE;
g->u.grab_point = tripoint_zero;
}
}
}
for( auto &i : get_items( p ) ) {
// Note: this can spawn items on the other side of the wall!
tripoint dest( part_loc.x + rng( -3, 3 ), part_loc.y + rng( -3, 3 ), smz );
g->m.add_item_or_charges( dest, i );
}
g->m.dirty_vehicle_list.insert( this );
refresh();
return shift_if_needed();
}
void vehicle::part_removal_cleanup() {
bool changed = false;
for( std::vector<vehicle_part>::iterator it = parts.begin(); it != parts.end(); /* noop */ ) {
if( it->removed ) {
auto items = get_items( std::distance( parts.begin(), it ) );
while( !items.empty() ) {
items.erase( items.begin() );
}
it = parts.erase( it );
changed = true;
}
else {
++it;
}
}
removed_part_count = 0;
if( changed || parts.empty() ) {
refresh();
if( parts.empty() ) {
g->m.destroy_vehicle( this );
return;
} else {
g->m.update_vehicle_cache( this, smz );
}
}
shift_if_needed();
refresh(); // Rebuild cached indices
}
item_location vehicle::part_base( int p )
{
return item_location( vehicle_cursor( *this, p ), &parts[ p ].base );
}
int vehicle::find_part( const item& it ) const
{
auto idx = std::find_if( parts.begin(), parts.end(), [&it]( const vehicle_part& e ) {
return &e.base == &it;
} );
return idx != parts.end() ? std::distance( parts.begin(), idx ) : INT_MIN;
}
/**
* Breaks the specified part into the pieces defined by its breaks_into entry.
* @param p The index of the part to break.
* @param x The map x-coordinate to place pieces at (give or take).
* @param y The map y-coordinate to place pieces at (give or take).
* @param scatter If true, pieces are scattered near the target square.
*/
void vehicle::break_part_into_pieces(int p, int x, int y, bool scatter) {
const std::string& group = part_info(p).breaks_into_group;
if( group.empty() ) {
return;
}
for( item& piece : item_group::items_from( group, calendar::turn ) ) {
// TODO: balance audit, ensure that less pieces are generated than one would need
// to build the component (smash a vehicle box that took 10 lumps of steel,
// find 12 steel lumps scattered after atom-smashing it with a tree trunk)
const int actual_x = scatter ? x + rng(-SCATTER_DISTANCE, SCATTER_DISTANCE) : x;
const int actual_y = scatter ? y + rng(-SCATTER_DISTANCE, SCATTER_DISTANCE) : y;
tripoint dest( actual_x, actual_y, smz );
g->m.add_item_or_charges( dest, piece );
}
}
std::vector<int> vehicle::parts_at_relative (const int dx, const int dy, bool const use_cache) const
{
if ( use_cache == false ) {
std::vector<int> res;
for (size_t i = 0; i < parts.size(); i++) {
if (parts[i].mount.x == dx && parts[i].mount.y == dy && !parts[i].removed) {
res.push_back ((int)i);
}
}
return res;
} else {
const auto &iter = relative_parts.find( point( dx, dy ) );
if ( iter != relative_parts.end() ) {
return iter->second;
} else {
std::vector<int> res;
return res;
}
}
}
int vehicle::part_with_feature (int part, vpart_bitflags const flag, bool unbroken) const
{
if (part_flag(part, flag)) {
return part;
}
const auto it = relative_parts.find( parts[part].mount );
if ( it != relative_parts.end() ) {
const std::vector<int> & parts_here = it->second;
for (auto &i : parts_here) {
if( part_flag( i, flag ) && ( !unbroken || !parts[i].is_broken() ) ) {
return i;
}
}
}
return -1;
}
int vehicle::part_with_feature (int part, const std::string &flag, bool unbroken) const
{
return part_with_feature_at_relative(parts[part].mount, flag, unbroken);
}
int vehicle::part_with_feature_at_relative (const point &pt, const std::string &flag, bool unbroken) const
{
std::vector<int> parts_here = parts_at_relative(pt.x, pt.y, false);
for( auto &elem : parts_here ) {
if( part_flag( elem, flag ) && ( !unbroken || !parts[ elem ].is_broken() ) ) {
return elem;
}
}
return -1;
}
bool vehicle::has_part( const std::string &flag, bool enabled ) const
{
return std::any_of( parts.begin(), parts.end(), [&flag,&enabled]( const vehicle_part &e ) {
return !e.removed && ( !enabled || e.enabled ) && !e.is_broken() && e.info().has_flag( flag );
} );
}
bool vehicle::has_part( const tripoint &pos, const std::string &flag, bool enabled ) const
{
auto px = pos.x - global_x();
auto py = pos.y - global_y();
for( const auto &e : parts ) {
if( e.precalc[0].x != px || e.precalc[0].y != py ) {
continue;
}
if( !e.removed && ( !enabled || e.enabled ) && !e.is_broken() && e.info().has_flag( flag ) ) {
return true;
}
}
return false;
}
// All 4 functions below look identical except for flag type and consts
template<typename Vehicle, typename Flag, typename Vector>
void get_parts_helper( Vehicle &veh, const Flag &flag, Vector &ret, bool enabled )
{
for( auto &e : veh.parts ) {
if( !e.removed && ( !enabled || e.enabled ) && !e.is_broken() && e.info().has_flag( flag ) ) {
ret.emplace_back( &e );
}
}
}
std::vector<vehicle_part *> vehicle::get_parts( const std::string &flag, bool enabled )
{
std::vector<vehicle_part *> res;
get_parts_helper( *this, flag, res, enabled );
return res;
}
std::vector<const vehicle_part *> vehicle::get_parts( const std::string &flag, bool enabled ) const
{
std::vector<const vehicle_part *> res;
get_parts_helper( *this, flag, res, enabled );
return res;
}
std::vector<vehicle_part *> vehicle::get_parts( vpart_bitflags flag, bool enabled )
{
std::vector<vehicle_part *> res;
get_parts_helper( *this, flag, res, enabled );
return res;
}
std::vector<const vehicle_part *> vehicle::get_parts( vpart_bitflags flag, bool enabled ) const
{
std::vector<const vehicle_part *> res;
get_parts_helper( *this, flag, res, enabled );
return res;
}
std::vector<vehicle_part *> vehicle::get_parts( const tripoint &pos, const std::string &flag, bool enabled )
{
std::vector<vehicle_part *> res;
for( auto &e : parts ) {
if( e.precalc[ 0 ].x != pos.x - global_x() ||
e.precalc[ 0 ].y != pos.y - global_y() ) {
continue;
}
if( !e.removed && ( !enabled || e.enabled ) && !e.is_broken() && ( flag.empty() || e.info().has_flag( flag ) ) ) {
res.push_back( &e );
}
}
return res;
}
std::vector<const vehicle_part *> vehicle::get_parts( const tripoint &pos, const std::string &flag, bool enabled ) const
{
std::vector<const vehicle_part *> res;
for( const auto &e : parts ) {
if( e.precalc[ 0 ].x != pos.x - global_x() ||
e.precalc[ 0 ].y != pos.y - global_y() ) {
continue;
}
if( !e.removed && ( !enabled || e.enabled ) && !e.is_broken() && ( flag.empty() || e.info().has_flag( flag ) ) ) {
res.push_back( &e );
}
}
return res;
}
bool vehicle::can_enable( const vehicle_part &pt, bool alert ) const
{
if( std::none_of( parts.begin(), parts.end(), [&pt]( const vehicle_part &e ) { return &e == &pt; } ) || pt.removed ) {
debugmsg( "Cannot enable removed or non-existent part" );
}
if( pt.is_broken() ) {
return false;
}
if( pt.info().has_flag( "PLANTER" ) && !warm_enough_to_plant() ) {
if( alert ) {
add_msg( m_bad, _( "It is too cold to plant anything now." ) );
}
return false;
}
// @todo: check fuel for combustion engines
if( pt.info().epower < 0 && fuel_left( fuel_type_battery, true ) <= 0 ) {
if( alert ) {
add_msg( m_bad, _( "Insufficient power to enable %s" ), pt.name().c_str() );
}
return false;
}
return true;
}
/**
* Returns the label at the coordinates given (mount coordinates)
*/
std::string const& vehicle::get_label(int const x, int const y) const
{
auto const it = labels.find(label(x, y));
if (it == labels.end()) {
static std::string const fallback;
return fallback;
}
return it->text;
}
/**
* Sets the label at the coordinates given (mount coordinates)
*/
void vehicle::set_label(int x, int y, std::string text)
{
auto const it = labels.find(label(x, y));
if (it == labels.end()) {
labels.insert(label(x, y, std::move(text)));
} else {
// labels should really be a map
labels.insert(labels.erase(it), label(x, y, std::move(text)));
}
}
int vehicle::next_part_to_close( int p, bool outside ) const
{
std::vector<int> parts_here = parts_at_relative(parts[p].mount.x, parts[p].mount.y);
// We want reverse, since we close the outermost thing first (curtains), and then the innermost thing (door)
for(std::vector<int>::reverse_iterator part_it = parts_here.rbegin();
part_it != parts_here.rend();
++part_it)
{
if(part_flag(*part_it, VPFLAG_OPENABLE)
&& !parts[ *part_it ].is_broken()
&& parts[*part_it].open == 1
&& (!outside || !part_flag(*part_it, "OPENCLOSE_INSIDE")) )
{
return *part_it;
}
}
return -1;
}
int vehicle::next_part_to_open(int p, bool outside) const
{
std::vector<int> parts_here = parts_at_relative(parts[p].mount.x, parts[p].mount.y);
// We want forwards, since we open the innermost thing first (curtains), and then the innermost thing (door)
for( auto &elem : parts_here ) {
if( part_flag( elem, VPFLAG_OPENABLE ) && !parts[ elem ].is_broken() && parts[elem].open == 0 &&
( !outside || !part_flag( elem, "OPENCLOSE_INSIDE" ) ) ) {
return elem;
}
}
return -1;
}
/**
* Returns all parts in the vehicle with the given flag, optionally checking
* to only return unbroken parts.
* If performance becomes an issue, certain lists (such as wheels) could be
* cached and fast-returned here, but this is currently linear-time with
* respect to the number of parts in the vehicle.
* @param feature The flag (such as "WHEEL" or "CONE_LIGHT") to find.
* @param unbroken true if only unbroken parts should be returned, false to
* return all matching parts.
* @return A list of indices to all the parts with the specified feature.
*/
std::vector<int> vehicle::all_parts_with_feature(const std::string& feature, bool const unbroken) const
{
std::vector<int> parts_found;
for( size_t part_index = 0; part_index < parts.size(); ++part_index ) {
if(part_info(part_index).has_flag(feature) &&
( !unbroken || !parts[ part_index ].is_broken() ) ) {
parts_found.push_back(part_index);
}
}
return parts_found;
}
std::vector<int> vehicle::all_parts_with_feature(vpart_bitflags feature, bool const unbroken) const
{
std::vector<int> parts_found;
for( size_t part_index = 0; part_index < parts.size(); ++part_index ) {
if(part_info(part_index).has_flag(feature) &&
( !unbroken || !parts[ part_index ].is_broken() ) ) {
parts_found.push_back(part_index);
}
}
return parts_found;
}
/**
* Returns all parts in the vehicle that exist in the given location slot. If
* the empty string is passed in, returns all parts with no slot.
* @param location The location slot to get parts for.
* @return A list of indices to all parts with the specified location.
*/
std::vector<int> vehicle::all_parts_at_location(const std::string& location) const
{
std::vector<int> parts_found;
for( size_t part_index = 0; part_index < parts.size(); ++part_index ) {
if(part_info(part_index).location == location && !parts[part_index].removed) {
parts_found.push_back(part_index);
}
}
return parts_found;
}
bool vehicle::part_flag (int part, const std::string &flag) const
{
if (part < 0 || part >= (int)parts.size() || parts[part].removed) {
return false;
} else {
return part_info(part).has_flag(flag);
}
}
bool vehicle::part_flag( int part, const vpart_bitflags flag) const
{
if (part < 0 || part >= (int)parts.size() || parts[part].removed) {
return false;
} else {
return part_info(part).has_flag(flag);
}
}
int vehicle::part_at(int const dx, int const dy) const
{
for (size_t p = 0; p < parts.size(); p++) {
if (parts[p].precalc[0].x == dx && parts[p].precalc[0].y == dy && !parts[p].removed) {
return (int)p;
}
}
return -1;
}
int vehicle::global_part_at(int const x, int const y) const
{
return part_at(x - global_x(), y - global_y());
}
/**
* Given a vehicle part which is inside of this vehicle, returns the index of
* that part. This exists solely because activities relating to vehicle editing
* require the index of the vehicle part to be passed around.
* @param part The part to find.
* @param check_removed Check whether this part can be removed
* @return The part index, -1 if it is not part of this vehicle.
*/
int vehicle::index_of_part(const vehicle_part *const part, bool const check_removed) const
{
if(part != NULL) {
for( size_t index = 0; index < parts.size(); ++index ) {
// @note Doesn't this have a bunch of copy overhead?
vehicle_part next_part = parts[index];
if (!check_removed && next_part.removed) {
continue;
}
if( part->id == next_part.id && part->mount == next_part.mount ) {
return index;
}
}
}
return -1;
}
/**
* Returns which part (as an index into the parts list) is the one that will be
* displayed for the given square. Returns -1 if there are no parts in that
* square.
* @param local_x The local x-coordinate.
* @param local_y The local y-coordinate.
* @return The index of the part that will be displayed.
*/
int vehicle::part_displayed_at(int const local_x, int const local_y) const
{
// Z-order is implicitly defined in game::load_vehiclepart, but as
// numbers directly set on parts rather than constants that can be
// used elsewhere. A future refactor might be nice but this way
// it's clear where the magic number comes from.
const int ON_ROOF_Z = 9;
std::vector<int> parts_in_square = parts_at_relative(local_x, local_y);
if(parts_in_square.empty()) {
return -1;
}
bool in_vehicle = g->u.in_vehicle;
if (in_vehicle) {
// They're in a vehicle, but are they in /this/ vehicle?
std::vector<int> psg_parts = boarded_parts();
in_vehicle = false;
for( auto &psg_part : psg_parts ) {
if( get_passenger( psg_part ) == &( g->u ) ) {
in_vehicle = true;
break;
}
}
}
int hide_z_at_or_above = (in_vehicle) ? (ON_ROOF_Z) : INT_MAX;
int top_part = 0;
for(size_t index = 1; index < parts_in_square.size(); index++) {
if((part_info(parts_in_square[top_part]).z_order <
part_info(parts_in_square[index]).z_order) &&
(part_info(parts_in_square[index]).z_order <
hide_z_at_or_above)) {
top_part = index;
}
}
return parts_in_square[top_part];
}
int vehicle::roof_at_part( const int part ) const
{
std::vector<int> parts_in_square = parts_at_relative( parts[part].mount.x, parts[part].mount.y );
for( const int p : parts_in_square ) {
if( part_info( p ).location == "on_roof" || part_flag( p, "ROOF" ) ) {
return p;
}
}
return -1;
}
char vehicle::part_sym( const int p, const bool exact ) const
{
if (p < 0 || p >= (int)parts.size() || parts[p].removed) {
return ' ';
}
const int displayed_part = exact ? p : part_displayed_at(parts[p].mount.x, parts[p].mount.y);
if (part_flag (displayed_part, VPFLAG_OPENABLE) && parts[displayed_part].open) {
return '\''; // open door
} else {
return parts[ displayed_part ].is_broken() ?
part_info(displayed_part).sym_broken : part_info(displayed_part).sym;
}
}
// similar to part_sym(int p) but for use when drawing SDL tiles. Called only by cata_tiles during draw_vpart
// vector returns at least 1 element, max of 2 elements. If 2 elements the second denotes if it is open or damaged
vpart_id vehicle::part_id_string(int const p, char &part_mod) const
{
part_mod = 0;
if( p < 0 || p >= (int)parts.size() || parts[p].removed ) {
return vpart_id::NULL_ID();
}
int displayed_part = part_displayed_at(parts[p].mount.x, parts[p].mount.y);
const vpart_id idinfo = parts[displayed_part].id;
if (part_flag (displayed_part, VPFLAG_OPENABLE) && parts[displayed_part].open) {
part_mod = 1; // open
} else if( parts[ displayed_part ].is_broken() ){
part_mod = 2; // broken
}
return idinfo;
}
nc_color vehicle::part_color( const int p, const bool exact ) const
{
if (p < 0 || p >= (int)parts.size()) {
return c_black;
}
nc_color col;
int parm = -1;
//If armoring is present and the option is set, it colors the visible part
if( get_option<bool>( "VEHICLE_ARMOR_COLOR" ) ) {
parm = part_with_feature(p, VPFLAG_ARMOR, false);
}
if( parm >= 0 ) {
col = part_info(parm).color;
} else {
const int displayed_part = exact ? p : part_displayed_at(parts[p].mount.x, parts[p].mount.y);
if (displayed_part < 0 || displayed_part >= (int)parts.size()) {
return c_black;
}
if (parts[displayed_part].blood > 200) {
col = c_red;
} else if (parts[displayed_part].blood > 0) {
col = c_light_red;
} else if (parts[displayed_part].is_broken()) {
col = part_info(displayed_part).color_broken;
} else {
col = part_info(displayed_part).color;
}
}
if( exact ) {
return col;
}
// curtains turn windshields gray
int curtains = part_with_feature(p, VPFLAG_CURTAIN, false);
if (curtains >= 0) {
if (part_with_feature(p, VPFLAG_WINDOW, true) >= 0 && !parts[curtains].open)
col = part_info(curtains).color;
}
//Invert colors for cargo parts with stuff in them
int cargo_part = part_with_feature(p, VPFLAG_CARGO);
if(cargo_part > 0 && !get_items(cargo_part).empty()) {
return invert_color(col);
} else {
return col;
}
}
/**
* Prints a list of all parts to the screen inside of a boxed window, possibly
* highlighting a selected one.
* @param win The window to draw in.
* @param y1 The y-coordinate to start drawing at.
* @param max_y Draw no further than this y-coordinate.
* @param width The width of the window.
* @param p The index of the part being examined.
* @param hl The index of the part to highlight (if any).
*/
int vehicle::print_part_desc( const catacurses::window &win, int y1, const int max_y, int width, int p, int hl /*= -1*/ ) const
{
if (p < 0 || p >= (int)parts.size()) {
return y1;
}
std::vector<int> pl = this->parts_at_relative(parts[p].mount.x, parts[p].mount.y);
int y = y1;
for (size_t i = 0; i < pl.size(); i++)
{
if ( y >= max_y ) {
mvwprintz( win, y, 1, c_yellow, _( "More parts here..." ) );
++y;
break;
}
const vehicle_part& vp = parts[ pl [ i ] ];
nc_color col_cond = vp.is_broken() ? c_dark_gray : vp.base.damage_color();
std::string partname = vp.name();
if( vp.is_tank() && vp.ammo_current() != "null" ) {
partname += string_format( " (%s)", item::nname( vp.ammo_current() ).c_str() );
}
if( part_flag( pl[i], "CARGO" ) ) {
//~ used/total volume of a cargo vehicle part
partname += string_format( _(" (vol: %s/%s %s)"),
format_volume( stored_volume( pl[i] ) ).c_str(),
format_volume( max_volume( pl[i] ) ).c_str(),
volume_units_abbr() );
}
bool armor = part_flag(pl[i], "ARMOR");
std::string left_sym, right_sym;
if(armor) {
left_sym = "("; right_sym = ")";
} else if(part_info(pl[i]).location == part_location_structure) {
left_sym = "["; right_sym = "]";
} else {
left_sym = "-"; right_sym = "-";
}
nc_color sym_color = ( int )i == hl ? hilite( c_light_gray ) : c_light_gray;
mvwprintz( win, y, 1, sym_color, left_sym );
trim_and_print( win, y, 2, getmaxx( win ) - 4,
( int )i == hl ? hilite( col_cond ) : col_cond, partname );
wprintz( win, sym_color, right_sym );
if (i == 0 && is_inside(pl[i])) {
//~ indicates that a vehicle part is inside
mvwprintz(win, y, width-2-utf8_width(_("Interior")), c_light_gray, _("Interior"));
} else if (i == 0) {
//~ indicates that a vehicle part is outside
mvwprintz(win, y, width-2-utf8_width(_("Exterior")), c_light_gray, _("Exterior"));
}
y++;
}
// print the label for this location
const std::string label = get_label(parts[p].mount.x, parts[p].mount.y);
if( !label.empty() && y <= max_y ) {
mvwprintz(win, y++, 1, c_light_red, _("Label: %s"), label.c_str());
}
return y;
}
/**
* Returns an array of fuel types that can be printed
* @return An array of printable fuel type ids
*/
std::vector<itype_id> vehicle::get_printable_fuel_types() const
{
std::set<itype_id> opts;
for( const auto &pt : parts ) {
if( ( pt.is_tank() || pt.is_battery() || pt.is_reactor() ) && pt.ammo_current() != "null" ) {
opts.emplace( pt.ammo_current() );
}
}
std::vector<itype_id> res( opts.begin(), opts.end() );
std::sort( res.begin(), res.end(), [&]( const itype_id &lhs, const itype_id &rhs ) {
return basic_consumption( rhs ) < basic_consumption( lhs );
} );
return res;
}
/**
* Prints all of the fuel indicators of the vehicle
* @param win Pointer to the window to draw in.
* @param y Y location to draw at.
* @param x X location to draw at.
* @param start_index Starting index in array of fuel gauges to start reading from
* @param fullsize true if it's expected to print multiple rows
* @param verbose true if there should be anything after the gauge (either the %, or number)
* @param desc true if the name of the fuel should be at the end
* @param isHorizontal true if the menu is not vertical
*/
void vehicle::print_fuel_indicators( const catacurses::window &win, int y, int x, int start_index, bool fullsize, bool verbose, bool desc, bool isHorizontal ) const
{
auto fuels = get_printable_fuel_types();
if( !fullsize ) {
if( !fuels.empty() ) {
print_fuel_indicator( win, y, x, fuels.front(), verbose, desc );
}
return;
}
int yofs = 0;
int max_gauge = ((isHorizontal) ? 12 : 5) + start_index;
int max_size = std::min((int)fuels.size(), max_gauge);
for( int i = start_index; i < max_size; i++ ) {
const itype_id &f = fuels[i];
print_fuel_indicator( win, y + yofs, x, f, verbose, desc );
if (fullsize) {
yofs++;
}
}
// check if the current index is less than the max size minus 12 or 5, to indicate that there's more
if((start_index < (int)fuels.size() - ((isHorizontal) ? 12 : 5)) && fullsize) {
mvwprintz( win, y + yofs, x, c_light_green, ">" );
wprintz( win, c_light_gray, " for more" );
}
}
/**
* Prints a fuel gauge for a vehicle
* @param w Pointer to the window to draw in.
* @param y Y location to draw at.
* @param x X location to draw at.
* @param fuel_type ID of the fuel type to draw
* @param verbose true if there should be anything after the gauge (either the %, or number)
* @param desc true if the name of the fuel should be at the end
*/
void vehicle::print_fuel_indicator( const catacurses::window &win, int y, int x, itype_id fuel_type, bool verbose, bool desc ) const
{
const char fsyms[5] = { 'E', '\\', '|', '/', 'F' };
nc_color col_indf1 = c_light_gray;
int cap = fuel_capacity( fuel_type );
int f_left = fuel_left( fuel_type );
nc_color f_color = item::find_type( fuel_type )->color;
mvwprintz(win, y, x, col_indf1, "E...F");
int amnt = cap > 0 ? f_left * 99 / cap : 0;
int indf = (amnt / 20) % 5;
mvwprintz( win, y, x + indf, f_color, "%c", fsyms[indf] );
if (verbose) {
if( debug_mode ) {
mvwprintz( win, y, x + 6, f_color, "%d/%d", f_left, cap );
} else {
mvwprintz( win, y, x + 6, f_color, "%d", (f_left * 100) / cap );
wprintz( win, c_light_gray, "%c", 045 );
}
}
if (desc) {
wprintz(win, c_light_gray, " - %s", item::nname( fuel_type ).c_str() );
}
}
point vehicle::coord_translate (const point &p) const
{
point q;
coord_translate(pivot_rotation[0], pivot_anchor[0], p, q);
return q;
}
void vehicle::coord_translate (int dir, const point &pivot, const point &p, point &q) const
{
tileray tdir (dir);
tdir.advance (p.x - pivot.x);
q.x = tdir.dx() + tdir.ortho_dx(p.y - pivot.y);
q.y = tdir.dy() + tdir.ortho_dy(p.y - pivot.y);
}
void vehicle::precalc_mounts (int idir, int dir, const point &pivot)
{
if (idir < 0 || idir > 1)
idir = 0;
for (auto &p : parts)
{
if (p.removed) {
continue;
}
coord_translate (dir, pivot, p.mount, p.precalc[idir]);
}
pivot_anchor[idir] = pivot;
pivot_rotation[idir] = dir;
}
std::vector<int> vehicle::boarded_parts() const
{
std::vector<int> res;
for (size_t p = 0; p < parts.size(); p++) {
if (part_flag (p, VPFLAG_BOARDABLE) &&
parts[p].has_flag(vehicle_part::passenger_flag)) {
res.push_back ((int)p);
}
}
return res;
}
player *vehicle::get_passenger(int p) const
{
p = part_with_feature (p, VPFLAG_BOARDABLE, false);
if (p >= 0 && parts[p].has_flag(vehicle_part::passenger_flag))
{
return g->critter_by_id<player>( parts[p].passenger_id );
}
return 0;
}
int vehicle::global_x() const
{
return smx * SEEX + posx;
}
int vehicle::global_y() const
{
return smy * SEEY + posy;
}
point vehicle::global_pos() const
{
return point( smx * SEEX + posx, smy * SEEY + posy );
}
tripoint vehicle::global_pos3() const
{
return tripoint( smx * SEEX + posx, smy * SEEY + posy, smz );
}
tripoint vehicle::global_part_pos3( const int &index ) const
{
return global_part_pos3( parts[ index ] );
}
tripoint vehicle::global_part_pos3( const vehicle_part &pt ) const
{
return global_pos3() + pt.precalc[ 0 ];
}
point vehicle::real_global_pos() const
{
return g->m.getabs( global_x(), global_y() );
}
tripoint vehicle::real_global_pos3() const
{
return g->m.getabs( tripoint( global_x(), global_y(), smz ) );
}
void vehicle::set_submap_moved( int x, int y )
{
const point old_msp = real_global_pos();
smx = x;
smy = y;
if( !tracking_on ) {
return;
}
overmap_buffer.move_vehicle( this, old_msp );
}
units::mass vehicle::total_mass() const
{
if( mass_dirty ) {
refresh_mass();
}
return mass_cache;
}
units::volume vehicle::total_folded_volume() const
{
units::volume m = 0;
for( size_t i = 0; i < parts.size(); i++ ) {
if( parts[i].removed ) {
continue;
}
m += part_info(i).folded_volume;
}
return m;
}
const point &vehicle::rotated_center_of_mass() const
{
// @todo: Bring back caching of this point
calc_mass_center( true );
return mass_center_precalc;
}
const point &vehicle::local_center_of_mass() const
{
if( mass_center_no_precalc_dirty ) {
calc_mass_center( false );
}
return mass_center_no_precalc;
}
point vehicle::pivot_displacement() const
{
// precalc_mounts always produces a result that puts the pivot point at (0,0).
// If the pivot point changes, this artificially moves the vehicle, as the position
// of the old pivot point will appear to move from (posx+0, posy+0) to some other point
// (posx+dx,posy+dy) even if there is no change in vehicle position or rotation.
// This method finds that movement so it can be canceled out when actually moving
// the vehicle.
// rotate the old pivot point around the new pivot point with the old rotation angle
point dp;
coord_translate(pivot_rotation[0], pivot_anchor[1], pivot_anchor[0], dp);
return dp;
}
int vehicle::fuel_left (const itype_id & ftype, bool recurse) const
{
int fl = std::accumulate( parts.begin(), parts.end(), 0, [&ftype]( const int &lhs, const vehicle_part &rhs ) {
return lhs + ( rhs.ammo_current() == ftype ? rhs.ammo_remaining() : 0 );
} );
if(recurse && ftype == fuel_type_battery) {
auto fuel_counting_visitor = [&] (vehicle const* veh, int amount, int) {
return amount + veh->fuel_left(ftype, false);
};
// HAX: add 1 to the initial amount so traversal doesn't immediately stop just
// 'cause we have 0 fuel left in the current vehicle. Subtract the 1 immediately
// after traversal.
fl = traverse_vehicle_graph(this, fl + 1, fuel_counting_visitor) - 1;
}
//muscle engines have infinite fuel
if (ftype == fuel_type_muscle) {
int part_under_player;
// @todo: Allow NPCs to power those
vehicle *veh = g->m.veh_at( g->u.pos(), part_under_player );
bool player_controlling = player_in_control(g->u);
//if the engine in the player tile is a muscle engine, and player is controlling vehicle
if( veh == this && player_controlling && part_under_player >= 0 ) {
int p = part_with_feature(part_under_player, VPFLAG_ENGINE);
if( p >= 0 && part_info(p).fuel_type == fuel_type_muscle && is_part_on( p ) ) {
fl += 10;
}
}
}
return fl;
}
int vehicle::fuel_capacity (const itype_id &ftype) const
{
return std::accumulate( parts.begin(), parts.end(), 0, [&ftype]( const int &lhs, const vehicle_part &rhs ) {
return lhs + ( rhs.ammo_current() == ftype ? rhs.ammo_capacity() : 0 );
} );
}
int vehicle::drain( const itype_id & ftype, int amount )
{
if( ftype == fuel_type_battery ) {
// Batteries get special handling to take advantage of jumper
// cables -- discharge_battery knows how to recurse properly
// (including taking cable power loss into account).
int remnant = discharge_battery(amount, true);
// discharge_battery returns amount of charges that were not
// found anywhere in the power network, whereas this function
// returns amount of charges consumed; simple subtraction.
return amount - remnant;
}
int drained = 0;
for( auto &p : parts ) {
if( amount <= 0 ) {
break;
}
if( p.ammo_current() == ftype ) {
int qty = p.ammo_consume( amount, global_part_pos3( p ) );
drained += qty;
amount -= qty;
}
}
invalidate_mass();
return drained;
}
int vehicle::basic_consumption(const itype_id &ftype) const
{
int fcon = 0;
for( size_t e = 0; e < engines.size(); ++e ) {
if(is_engine_type_on(e, ftype)) {
if( part_info( engines[e] ).fuel_type == fuel_type_battery &&
part_epower( engines[e] ) >= 0 ) {
// Electric engine - use epower instead
fcon -= epower_to_power( part_epower( engines[e] ) );
} else if( !is_engine_type( e, fuel_type_muscle ) ) {
fcon += part_power( engines[e] );
if( parts[ e ].faults().count( fault_filter_air ) ) {
fcon *= 2;
}
}
}
}
return fcon;
}
int vehicle::total_power(bool const fueled) const
{
int pwr = 0;
int cnt = 0;
for (size_t e = 0; e < engines.size(); e++) {
int p = engines[e];
if (is_engine_on(e) && (fuel_left (part_info(p).fuel_type) || !fueled)) {
pwr += part_power(p);
cnt++;
}
}
for (size_t a = 0; a < alternators.size();a++){
int p = alternators[a];
if (is_alternator_on(a)) {
pwr += part_power(p); // alternators have negative power
}
}
if (cnt > 1) {
pwr = pwr * 4 / (4 + cnt -1);
}
return pwr;
}
int vehicle::acceleration(bool const fueled) const
{
if( ( engine_on && has_engine_type_not( fuel_type_muscle, true ) ) || skidding ) {
return safe_velocity( fueled ) * k_mass() / ( 1 + strain () ) / 10;
} else if ((has_engine_type(fuel_type_muscle, true))){
//limit vehicle weight for muscle engines
const units::mass mass = total_mass() / 1000;
///\EFFECT_STR caps vehicle weight for muscle engines
const units::mass move_mass = std::max( g->u.str_cur * 25_gram, 150_gram ) * 1000;
if (mass <= move_mass) {
return (int) (safe_velocity (fueled) * k_mass() / (1 + strain ()) / 10);
} else {
return 0;
}
}
else {
return 0;
}
}
int vehicle::max_velocity(bool const fueled) const
{
return total_power (fueled) * 80;
}
bool vehicle::do_environmental_effects()
{
bool needed = false;
// check for smoking parts
for( size_t p = 0; p < parts.size(); p++ ) {
auto part_pos = global_pos3() + parts[p].precalc[0];
/* Only lower blood level if:
* - The part is outside.
* - The weather is any effect that would cause the player to be wet. */
if( parts[p].blood > 0 && g->m.is_outside( part_pos ) ) {
needed = true;
if( g->weather >= WEATHER_DRIZZLE && g->weather <= WEATHER_ACID_RAIN ) {
parts[p].blood--;
}
}
}
return needed;
}
int vehicle::safe_velocity(bool const fueled) const
{
int pwrs = 0;
int cnt = 0;
for (size_t e = 0; e < engines.size(); e++){
if (is_engine_on(e) &&
(!fueled || is_engine_type(e, fuel_type_muscle) ||
fuel_left (part_info(engines[e]).fuel_type))) {
int m2c = 100;
if (is_engine_type(e, fuel_type_gasoline)) {
m2c = 60;
} else if(is_engine_type(e, fuel_type_diesel)) {
m2c = 65;
} else if(is_engine_type(e, fuel_type_battery)) {
m2c = 90;
} else if(is_engine_type(e, fuel_type_muscle)) {
m2c = 45;
}
if( parts[ engines[ e ] ].faults().count( fault_filter_fuel ) ) {
m2c *= 0.6;
}
pwrs += part_power(engines[e]) * m2c / 100;
cnt++;
}
}
for (int a = 0; a < (int)alternators.size(); a++){
if (is_alternator_on(a)){
pwrs += part_power(alternators[a]); // alternator parts have negative power
}
}
if (cnt > 0) {
pwrs = pwrs * 4 / (4 + cnt -1);
}
return (int) (pwrs * k_dynamics() * k_mass()) * 80;
}
void vehicle::spew_smoke( double joules, int part, int density )
{
if( rng( 1, 10000 ) > joules ) {
return;
}
point p = parts[part].mount;
density = std::max( joules / 10000, double( density ) );
// Move back from engine/muffler until we find an open space
while( relative_parts.find(p) != relative_parts.end() ) {
p.x += ( velocity < 0 ? 1 : -1 );
}
point q = coord_translate(p);
tripoint dest( global_x() + q.x, global_y() + q.y, smz );
g->m.adjust_field_strength( dest, fd_smoke, density );
}
/**
* Generate noise or smoke from a vehicle with engines turned on
* load = how hard the engines are working, from 0.0 until 1.0
* time = how many seconds to generated smoke for
*/
void vehicle::noise_and_smoke( double load, double time )
{
const std::array<int, 8> sound_levels = {{ 0, 15, 30, 60, 100, 140, 180, INT_MAX }};
const std::array<std::string, 8> sound_msgs = {{
"", _("hummm!"), _("whirrr!"), _("vroom!"), _("roarrr!"), _("ROARRR!"),
_("BRRROARRR!!"), _("BRUMBRUMBRUMBRUM!!!")
}};
double noise = 0.0;
double mufflesmoke = 0.0;
double muffle = 1.0, m;
int exhaust_part = -1;
for( size_t p = 0; p < parts.size(); p++ ) {
if( part_flag(p, "MUFFLER") ) {
m = 1.0 - (1.0 - part_info(p).bonus / 100.0) * double( parts[p].hp() ) / part_info(p).durability;
if( m < muffle ) {
muffle = m;
exhaust_part = int(p);
}
}
}
bool bad_filter = false;
for( size_t e = 0; e < engines.size(); e++ ) {
int p = engines[e];
if( is_engine_on(e) &&
(is_engine_type(e, fuel_type_muscle) || fuel_left (part_info(p).fuel_type)) ) {
double pwr = 10.0; // Default noise if nothing else found, shouldn't happen
double max_pwr = double(power_to_epower(part_power(p, true)))/40000;
double cur_pwr = load * max_pwr;
if( is_engine_type(e, fuel_type_gasoline) || is_engine_type(e, fuel_type_diesel)) {
if( is_engine_type( e, fuel_type_gasoline ) ) {
double dmg = 1.0 - double( parts[p].hp() ) / part_info( p ).durability;
if( parts[ p ].base.faults.count( fault_filter_fuel ) ) {
dmg = 1.0;
}
if( dmg > 0.75 && one_in( 200 - ( 150 * dmg ) ) ) {
backfire( e );
}
}
double j = power_to_epower(part_power(p, true)) * load * time * muffle;
if( parts[ p ].base.faults.count( fault_filter_air ) ) {
bad_filter = true;
j *= j;
}
if( (exhaust_part == -1) && engine_on ) {
spew_smoke( j, p, bad_filter ? MAX_FIELD_DENSITY : 1 );
} else {
mufflesmoke += j;
}
pwr = (cur_pwr*15 + max_pwr*3 + 5) * muffle;
} else if(is_engine_type(e, fuel_type_battery)) {
pwr = cur_pwr*3;
} else if(is_engine_type(e, fuel_type_muscle)) {
pwr = cur_pwr*5;
}
noise = std::max(noise, pwr); // Only the loudest engine counts.
}
}
if( (exhaust_part != -1) && engine_on &&
has_engine_type_not(fuel_type_muscle, true)) { // No engine, no smoke
spew_smoke( mufflesmoke, exhaust_part, bad_filter ? MAX_FIELD_DENSITY : 1 );
}
// Even a vehicle with engines off will make noise traveling at high speeds
noise = std::max( noise, double(fabs(velocity/500.0)) );
int lvl = 0;
if( one_in(4) && rng(0, 30) < noise &&
has_engine_type_not(fuel_type_muscle, true)) {
while( noise > sound_levels[lvl] ) {
lvl++;
}
}
sounds::ambient_sound( global_pos3(), noise, sound_msgs[lvl] );
}
float vehicle::wheel_area( bool boat ) const
{
float total_area = 0.0f;
const auto &wheel_indices = boat ? floating : wheelcache;
for( auto &wheel_index : wheel_indices ) {
total_area += parts[ wheel_index ].base.wheel_area();
}
return total_area;
}
float vehicle::k_friction() const
{
// calculate safe speed reduction due to wheel friction
constexpr float fr0 = 9000.0;
return fr0 / ( fr0 + wheel_area( false ) ) ;
}
float vehicle::k_aerodynamics() const
{
const int max_obst = 13;
int obst[max_obst];
for( auto &elem : obst ) {
elem = 0;
}
std::vector<int> structure_indices = all_parts_at_location(part_location_structure);
for( auto &structure_indice : structure_indices ) {
int p = structure_indice;
int frame_size = part_with_feature(p, VPFLAG_OBSTACLE) ? 30 : 10;
int pos = parts[p].mount.y + max_obst / 2;
if (pos < 0) {
pos = 0;
}
if (pos >= max_obst) {
pos = max_obst -1;
}
if (obst[pos] < frame_size) {
obst[pos] = frame_size;
}
}
int frame_obst = 0;
for( auto &elem : obst ) {
frame_obst += elem;
}
float ae0 = 200.0;
// calculate aerodynamic coefficient
float ka = ( ae0 / (ae0 + frame_obst) );
return ka;
}
float vehicle::k_dynamics() const
{
return ( k_aerodynamics() * k_friction() );
}
float vehicle::k_mass() const
{
// @todo: Remove this sum, apply only the relevant wheel type
float wa = wheel_area( false ) + wheel_area( true );
if( wa <= 0 ) {
return 0;
}
float ma0 = 50.0;
// calculate safe speed reduction due to mass
float km = ma0 / ( ma0 + to_kilogram( total_mass() ) / ( wa * 8.0f / 9.0f ) );
return km;
}
float vehicle::k_traction( float wheel_traction_area ) const
{
if( wheel_traction_area <= 0.01f ) {
return 0.0f;
}
const float mass_penalty = ( 1.0f - wheel_traction_area / wheel_area( !floating.empty() ) ) * to_kilogram( total_mass() );
float traction = std::min( 1.0f, wheel_traction_area / mass_penalty );
add_msg( m_debug, "%s has traction %.2f", name.c_str(), traction );
// For now make it easy until it gets properly balanced: add a low cap of 0.1
return std::max( 0.1f, traction );
}
float vehicle::drag() const
{
return -extra_drag;
}
float vehicle::strain() const
{
int mv = max_velocity();
int sv = safe_velocity();
if( mv <= sv ) {
mv = sv + 1;
}
if( velocity < sv && velocity > -sv ) {
return 0;
} else {
return (float) (abs(velocity) - sv) / (float) (mv - sv);
}
}
bool vehicle::sufficient_wheel_config( bool boat ) const
{
std::vector<int> floats = all_parts_with_feature(VPFLAG_FLOATS);
// @todo: Remove the limitations that boats can't move on land
if( boat || !floats.empty() ) {
return boat && floats.size() > 2;
}
std::vector<int> wheel_indices = all_parts_with_feature(VPFLAG_WHEEL);
if(wheel_indices.empty()) {
// No wheels!
return false;
} else if(wheel_indices.size() == 1) {
//Has to be a stable wheel, and one wheel can only support a 1-3 tile vehicle
if( !part_info(wheel_indices[0]).has_flag("STABLE") ||
all_parts_at_location(part_location_structure).size() > 3) {
return false;
}
}
return true;
}
bool vehicle::balanced_wheel_config( bool boat ) const
{
int xmin = INT_MAX;
int ymin = INT_MAX;
int xmax = INT_MIN;
int ymax = INT_MIN;
// find the bounding box of the wheels
// TODO: find convex hull instead
const auto &indices = boat ? floating : wheelcache;
for( auto &w : indices ) {
const auto &pt = parts[ w ].mount;
xmin = std::min( xmin, pt.x );
ymin = std::min( ymin, pt.y );
xmax = std::max( xmax, pt.x );
ymax = std::max( ymax, pt.y );
}
const point &com = local_center_of_mass();
if( com.x < xmin || com.x > xmax || com.y < ymin || com.y > ymax ) {
return false; // center of mass not inside support of wheels (roughly)
}
return true;
}
bool vehicle::valid_wheel_config( bool boat ) const
{
return sufficient_wheel_config( boat ) && balanced_wheel_config( boat );
}
float vehicle::steering_effectiveness() const
{
if (!floating.empty()) {
// I'M ON A BOAT
return 1.0;
}
if (steering.empty()) {
return -1.0; // No steering installed
}
// For now, you just need one wheel working for 100% effective steering.
// TODO: return something less than 1.0 if the steering isn't so good
// (unbalanced, long wheelbase, back-heavy vehicle with front wheel steering,
// etc)
for (int p : steering) {
if( !parts[ p ].is_broken() ) {
return 1.0;
}
}
// We have steering, but it's all broken.
return 0.0;
}
float vehicle::handling_difficulty() const
{
const float steer = std::max( 0.0f, steering_effectiveness() );
const float ktraction = k_traction( g->m.vehicle_wheel_traction( *this ) );
const float kmass = k_mass();
const float aligned = std::max( 0.0f, 1.0f - ( face_vec() - dir_vec() ).magnitude() );
constexpr float tile_per_turn = 10 * 100;
// TestVehicle: perfect steering, kmass, moving on road at 100 mph (10 tiles per turn) = 0.0
// TestVehicle but on grass (0.75 friction) = 2.5
// TestVehicle but overloaded (0.5 kmass) = 5
// TestVehicle but with bad steering (0.5 steer) and overloaded (0.5 kmass) = 10
// TestVehicle but on fungal bed (0.5 friction), bad steering and overloaded = 15
// TestVehicle but turned 90 degrees during this turn (0 align) = 10
const float diff_mod = ( ( 1.0f - steer ) + ( 1.0f - kmass ) + ( 1.0f - ktraction ) + ( 1.0f - aligned ) );
return velocity * diff_mod / tile_per_turn;
}
std::map<itype_id, int> vehicle::fuel_usage() const
{
std::map<itype_id, int> ret;
for( size_t i = 0; i < engines.size(); i++ ) {
// Note: functions with "engine" in name do NOT take part indices
// @todo: Use part indices and not engine vector indices
if( !is_engine_on( i ) ) {
continue;
}
const size_t e = engines[ i ];
const auto &info = part_info( e );
static const itype_id null_fuel_type( "null" );
if( info.fuel_type == null_fuel_type ) {
continue;
}
// @todo: Get rid of this special case
if( info.fuel_type == fuel_type_battery ) {
// Motor epower is in negatives
ret[ fuel_type_battery ] -= epower_to_power( part_epower( e ) );
} else if( !is_engine_type( i, fuel_type_muscle ) ) {
int usage = part_power( e );
if( parts[ e ].faults().count( fault_filter_air ) ) {
usage *= 2;
}
ret[ info.fuel_type ] += usage;
}
}
return ret;
}
float vehicle::drain_energy( const itype_id &ftype, float energy )
{
float drained = 0.0f;
for( auto &p : parts ) {
if( energy <= 0.0f ) {
break;
}
float consumed = p.consume_energy( ftype, energy );
drained += consumed;
energy -= consumed;
}
invalidate_mass();
return drained;
}
void vehicle::consume_fuel( double load = 1.0 )
{
float st = strain();
for( auto &fuel_pr : fuel_usage() ) {
auto &ft = fuel_pr.first;
int amnt_fuel_use = fuel_pr.second;
// In kilojoules
double amnt_precise = double( amnt_fuel_use );
amnt_precise *= load * ( 1.0 + st * st * 100.0 );
double remainder = fuel_remainder[ ft ];
amnt_precise -= remainder;
if( amnt_precise > 0.0f ) {
fuel_remainder[ ft ] = amnt_precise - drain_energy( ft, amnt_precise );
} else {
fuel_remainder[ ft ] = -amnt_precise;
}
add_msg( m_debug, "%s consumes %s: amount %.2f, remainder %.2f",
name.c_str(), ft.c_str(), amnt_precise, fuel_remainder[ ft ] );
}
//do this with chance proportional to current load
// But only if the player is actually there!
if( load > 0 && one_in( (int) (1 / load) ) &&
fuel_left( fuel_type_muscle ) > 0 ) {
//charge bionics when using muscle engine
if (g->u.has_bionic( bionic_id( "bio_torsionratchet" ) ) ) {
g->u.charge_power(1);
}
//cost proportional to strain
int mod = 1 + 4 * st;
if (one_in(10)) {
g->u.mod_hunger(mod);
g->u.mod_thirst(mod);
g->u.mod_fatigue(mod);
}
g->u.mod_stat( "stamina", -mod * 20);
}
}
std::vector<vehicle_part *> vehicle::lights( bool active )
{
std::vector<vehicle_part *> res;
for( auto &e : parts ) {
if( ( !active || e.enabled ) && !e.is_broken() && e.is_light() ) {
res.push_back( &e );
}
}
return res;
}
void vehicle::power_parts()
{
int epower = 0;
for( const auto *pt : get_parts( VPFLAG_ENABLED_DRAINS_EPOWER, true ) ) {
epower += pt->info().epower;
}
// Consumers of epower
if( is_alarm_on ) epower += alarm_epower;
if( camera_on ) epower += camera_epower;
// Engines: can both produce (plasma) or consume (gas, diesel)
// Gas engines require epower to run for ignition system, ECU, etc.
int engine_epower = 0;
if( engine_on ) {
for( size_t e = 0; e < engines.size(); ++e ) {
// Electric engines consume power when actually used, not passively
if( is_engine_on( e ) && !is_engine_type(e, fuel_type_battery) ) {
engine_epower += part_epower( engines[e] );
}
}
epower += engine_epower;
// Producers of epower
// If the engine is on, the alternators are working.
int alternators_epower = 0;
int alternators_power = 0;
for( size_t p = 0; p < alternators.size(); ++p ) {
if(is_alternator_on(p)) {
alternators_epower += part_info(alternators[p]).epower;
alternators_power += part_power(alternators[p]);
}
}
if(alternators_epower > 0) {
alternator_load = (float)abs(alternators_power);
epower += alternators_epower;
}
}
int epower_capacity_left = power_to_epower(fuel_capacity(fuel_type_battery) - fuel_left(fuel_type_battery));
if( has_part( "REACTOR", true ) && epower_capacity_left - epower > 0 ) {
// Still not enough surplus epower to fully charge battery
// Produce additional epower from any reactors
bool reactor_working = false;
for( auto &elem : reactors ) {
if( !parts[ elem ].is_broken() && parts[elem].ammo_remaining() > 0 ) {
// Efficiency: one unit of fuel is this many units of battery
// Note: One battery is roughly 373 units of epower
const int efficiency = part_info( elem ).power;
const int avail_fuel = parts[elem].ammo_remaining() * efficiency;
const int elem_epower = std::min( part_epower( elem ), power_to_epower( avail_fuel ) );
// Cap output at what we can achieve and utilize
const int reactors_output = std::min( elem_epower, epower_capacity_left - epower );
// Units of fuel consumed before adjustment for efficiency
const int battery_consumed = epower_to_power( reactors_output );
// Fuel consumed in actual units of the resource
int fuel_consumed = battery_consumed / efficiency;
// Remainder has a chance of resulting in more fuel consumption
if( x_in_y( battery_consumed % efficiency, efficiency ) ) {
fuel_consumed++;
}
parts[ elem ].ammo_consume( fuel_consumed, global_part_pos3( elem ) );
reactor_working = true;
epower += reactors_output;
}
}
if( !reactor_working ) {
// All reactors out of fuel or destroyed
for( auto pt : get_parts( "REACTOR" ) ) {
pt->enabled = false;
}
if( player_in_control(g->u) || g->u.sees( global_pos3() ) ) {
add_msg( _("The %s's reactor dies!"), name.c_str() );
}
}
}
int battery_deficit = 0;
if(epower > 0) {
// store epower surplus in battery
charge_battery(epower_to_power(epower));
} else if(epower < 0) {
// draw epower deficit from battery
battery_deficit = discharge_battery(abs(epower_to_power(epower)));
}
if( battery_deficit != 0 ) {
// Scoops need a special case since they consume power during actual use
for( auto *pt : get_parts( "SCOOP" ) ) {
pt->enabled = false;
}
// Rechargers need special case since they consume power on demand
for( auto *pt : get_parts( "RECHARGE" ) ) {
pt->enabled = false;
}
for( auto *pt : get_parts( VPFLAG_ENABLED_DRAINS_EPOWER, true ) ) {
if( pt->info().epower < 0 ) {
pt->enabled = false;
}
}
is_alarm_on = false;
camera_on = false;
if( player_in_control( g->u ) || g->u.sees( global_pos3() ) ) {
add_msg( _("The %s's battery dies!"), name.c_str() );
}
if( engine_epower < 0 ) {
// Not enough epower to run gas engine ignition system
engine_on = false;
if( player_in_control( g->u ) || g->u.sees( global_pos3() ) ) {
add_msg( _("The %s's engine dies!"), name.c_str() );
}
}
}
}
vehicle* vehicle::find_vehicle( const tripoint &where )
{
// Is it in the reality bubble?
tripoint veh_local = g->m.getlocal( where );
vehicle* veh = g->m.veh_at( veh_local );
if( veh != nullptr ) {
return veh;
}
// Nope. Load up its submap...
point veh_in_sm = point( where.x, where.y );
point veh_sm = ms_to_sm_remain( veh_in_sm );
auto sm = MAPBUFFER.lookup_submap( veh_sm.x, veh_sm.y, where.z );
if( sm == nullptr ) {
return nullptr;
}
// ...find the right vehicle inside it...
for( auto &elem : sm->vehicles ) {
vehicle *found_veh = elem;
point veh_location( found_veh->posx, found_veh->posy );
if( veh_in_sm == veh_location ) {
veh = found_veh;
break;
}
}
// ...and hand it over.
return veh;
}
template <typename Func, typename Vehicle>
int vehicle::traverse_vehicle_graph(Vehicle *start_veh, int amount, Func action)
{
// Breadth-first search! Initialize the queue with a pointer to ourselves and go!
std::queue< std::pair<Vehicle*, int> > connected_vehs;
std::set<Vehicle*> visited_vehs;
connected_vehs.push(std::make_pair(start_veh, 0));
while(amount > 0 && connected_vehs.size() > 0) {
auto current_node = connected_vehs.front();
Vehicle *current_veh = current_node.first;
int current_loss = current_node.second;
visited_vehs.insert(current_veh);
connected_vehs.pop();
g->u.add_msg_if_player(m_debug, "Traversing graph with %d power", amount);
for(auto &p : current_veh->loose_parts) {
if(!current_veh->part_info(p).has_flag("POWER_TRANSFER")) {
continue; // ignore loose parts that aren't power transfer cables
}
auto target_veh = vehicle::find_vehicle(current_veh->parts[p].target.second);
if(target_veh == nullptr || visited_vehs.count(target_veh) > 0) {
// Either no destination here (that vehicle's rolled away or off-map) or
// we've already looked at that vehicle.
continue;
}
// Add this connected vehicle to the queue of vehicles to search next,
// but only if we haven't seen this one before.
if(visited_vehs.count(target_veh) < 1) {
int target_loss = current_loss + current_veh->part_info(p).epower;
connected_vehs.push(std::make_pair(target_veh, target_loss));
float loss_amount = ((float)amount * (float)target_loss) / 100;
g->u.add_msg_if_player(m_debug, "Visiting remote %p with %d power (loss %f, which is %d percent)",
(void*)target_veh, amount, loss_amount, target_loss);
amount = action(target_veh, amount, (int)loss_amount);
g->u.add_msg_if_player(m_debug, "After remote %p, %d power", (void*)target_veh, amount);
if(amount < 1) {
break; // No more charge to donate away.
}
}
}
}
return amount;
}
int vehicle::charge_battery (int amount, bool include_other_vehicles)
{
for( auto &p : parts ) {
if( amount <= 0 ) {
break;
}
if( !p.is_broken() && p.is_battery() ) {
int qty = std::min( long( amount ), p.ammo_capacity() - p.ammo_remaining() );
p.ammo_set( fuel_type_battery, p.ammo_remaining() + qty );
amount -= qty;
}
}
auto charge_visitor = [] (vehicle* veh, int amount, int lost) {
g->u.add_msg_if_player(m_debug, "CH: %d", amount - lost);
return veh->charge_battery(amount - lost, false);
};
if(amount > 0 && include_other_vehicles) { // still a bit of charge we could send out...
amount = traverse_vehicle_graph(this, amount, charge_visitor);
}
return amount;
}
int vehicle::discharge_battery (int amount, bool recurse)
{
for( auto &p : parts ) {
if( amount <= 0 ) {
break;
}
if( !p.is_broken() && p.is_battery() ) {
amount -= p.ammo_consume( amount, global_part_pos3( p ) );
}
}
auto discharge_visitor = [] (vehicle* veh, int amount, int lost) {
g->u.add_msg_if_player(m_debug, "CH: %d", amount + lost);
return veh->discharge_battery(amount + lost, false);
};
if(amount > 0 && recurse) { // need more power!
amount = traverse_vehicle_graph(this, amount, discharge_visitor);
}
return amount; // non-zero if we weren't able to fulfill demand.
}
void vehicle::do_engine_damage(size_t e, int strain) {
if( is_engine_on(e) && !is_engine_type(e, fuel_type_muscle) &&
fuel_left(part_info(engines[e]).fuel_type) && rng (1, 100) < strain ) {
int dmg = rng(strain * 2, strain * 4);
damage_direct( engines[e], dmg );
if(one_in(2)) {
add_msg(_("Your engine emits a high pitched whine."));
} else {
add_msg(_("Your engine emits a loud grinding sound."));
}
}
}
void vehicle::idle(bool on_map) {
int engines_power = 0;
float idle_rate;
if (engine_on && total_power() > 0) {
for (size_t e = 0; e < engines.size(); e++){
size_t p = engines[e];
if (fuel_left(part_info(p).fuel_type) && is_engine_on(e)) {
engines_power += part_power(engines[e]);
}
}
idle_rate = (float)alternator_load / (float)engines_power;
if (idle_rate < 0.01) idle_rate = 0.01; // minimum idle is 1% of full throttle
consume_fuel(idle_rate);
if (on_map) {
noise_and_smoke( idle_rate, 6.0 );
}
} else {
if( engine_on && g->u.sees( global_pos3() ) &&
has_engine_type_not(fuel_type_muscle, true) ) {
add_msg(_("The %s's engine dies!"), name.c_str());
}
engine_on = false;
}
if( !warm_enough_to_plant() ) {
for( auto e : get_parts( "PLANTER", true ) ) {
if( g->u.sees( global_pos3() ) ) {
add_msg( _( "The %s's planter turns off due to low temperature." ), name.c_str() );
}
e->enabled = false;
}
}
if( has_part( "STEREO", true ) ) {
play_music();
}
if( has_part( "CHIMES", true ) ) {
play_chimes();
}
if (on_map && is_alarm_on) {
alarm();
}
if( on_map ) {
update_time( calendar::turn );
}
}
void vehicle::on_move(){
if( has_part( "SCOOP", true ) ) {
operate_scoop();
}
if( has_part( "PLANTER", true ) ) {
operate_planter();
}
if( has_part( "PLOW", true ) ) {
operate_plow();
}
if( has_part( "REAPER", true ) ) {
operate_reaper();
}
occupied_cache_time = calendar::before_time_starts;
}
void vehicle::operate_plow(){
for( const int plow_id : all_parts_with_feature( "PLOW" ) ){
const tripoint start_plow = global_pos3() + parts[plow_id].precalc[0];
if( g->m.has_flag("DIGGABLE", start_plow) ){
g->m.ter_set( start_plow, t_dirtmound );
} else {
const int speed = velocity;
const int v_damage = rng( 3, speed );
damage( plow_id, v_damage, DT_BASH, false );
sounds::sound( start_plow, v_damage, _("Clanggggg!") );
}
}
}
void vehicle::operate_reaper(){
const tripoint &veh_start = global_pos3();
for( const int reaper_id : all_parts_with_feature( "REAPER" ) ){
const tripoint reaper_pos = veh_start + parts[ reaper_id ].precalc[ 0 ];
const int plant_produced = rng( 1, parts[ reaper_id ].info().bonus );
const int seed_produced = rng( 1, 3 );
const units::volume max_pickup_volume = parts[ reaper_id ].info().size / 20;
if( g->m.furn( reaper_pos ) != f_plant_harvest ||
!g->m.has_items( reaper_pos ) ) {
continue;
}
const item& seed = g->m.i_at( reaper_pos ).front();
if( seed.typeId() == "fungal_seeds" ||
seed.typeId() == "marloss_seed" ) {
// Otherworldly plants, the earth-made reaper can not handle those.
continue;
}
g->m.furn_set( reaper_pos, f_null );
g->m.i_clear( reaper_pos );
for( auto &i : iexamine::get_harvest_items(
*seed.type, plant_produced, seed_produced, false ) ) {
g->m.add_item_or_charges( reaper_pos, i );
}
sounds::sound( reaper_pos, rng( 10, 25 ), _("Swish") );
if( part_flag( reaper_id, "CARGO" ) ) {
map_stack stack( g->m.i_at( reaper_pos ) );
for( auto iter = stack.begin(); iter != stack.end(); ) {
if( ( iter->volume() <= max_pickup_volume ) &&
add_item( reaper_id, *iter ) ) {
iter = stack.erase( iter );
} else {
++iter;
}
}
}
}
}
void vehicle::operate_planter(){
std::vector<int> planters = all_parts_with_feature("PLANTER");
for( int planter_id : planters ){
const tripoint &loc = global_pos3() + parts[planter_id].precalc[0];
vehicle_stack v = get_items(planter_id);
for( auto i = v.begin(); i != v.end(); i++ ){
if( i->is_seed() ){
// If it is an "advanced model" then it will avoid damaging itself or becoming damaged. It's a real feature.
if( g->m.ter(loc) != t_dirtmound && part_flag(planter_id, "ADVANCED_PLANTER" ) ) {
//then don't put the item there.
break;
} else if( g->m.ter(loc) == t_dirtmound ) {
g->m.furn_set(loc, f_plant_seed);
} else if( !g->m.has_flag( "DIGGABLE", loc ) ) {
//If it isn't diggable terrain, then it will most likely be damaged.
damage( planter_id, rng(1, 10), DT_BASH, false );
sounds::sound( loc, rng(10,20), _("Clink"));
}
if( !i->count_by_charges() || i->charges == 1 ) {
i->set_age( 0 );
g->m.add_item( loc, *i );
v.erase( i );
} else {
item tmp = *i;
tmp.charges = 1;
tmp.set_age( 0 );
g->m.add_item( loc, tmp );
i->charges--;
}
break;
}
}
}
}
void vehicle::operate_scoop()
{
std::vector<int> scoops = all_parts_with_feature( "SCOOP" );
for( int scoop : scoops ) {
const int chance_to_damage_item = 9;
const units::volume max_pickup_volume = parts[scoop].info().size / 10;
const std::array<std::string, 4> sound_msgs = {{
_("Whirrrr"), _("Ker-chunk"), _("Swish"), _("Cugugugugug")
}};
sounds::sound( global_pos3() + parts[scoop].precalc[0], rng( 20, 35 ),
sound_msgs[rng( 0, 3 )] );
std::vector<tripoint> parts_points;
for( const tripoint &current :
g->m.points_in_radius( global_pos3() + parts[scoop].precalc[0], 1 ) ) {
parts_points.push_back( current );
}
for( const tripoint &position : parts_points ) {
g->m.mop_spills( position );
if( !g->m.has_items( position ) ) {
continue;
}
item *that_item_there = nullptr;
const map_stack q = g->m.i_at( position );
if( g->m.has_flag( "SEALED", position) ) {
continue;//ignore it. Street sweepers are not known for their ability to harvest crops.
}
size_t itemdex = 0;
for( auto it : q ) {
if( it.volume() < max_pickup_volume ) {
that_item_there = g->m.item_from( position, itemdex );
break;
}
itemdex++;
}
if( !that_item_there ) {
continue;
}
if( one_in( chance_to_damage_item ) && that_item_there->damage() < that_item_there->max_damage() ) {
//The scoop will not destroy the item, but it may damage it a bit.
that_item_there->inc_damage( DT_BASH );
//The scoop gets a lot louder when breaking an item.
sounds::sound( position, rng(10, that_item_there->volume() / units::legacy_volume_factor * 2 + 10),
_("BEEEThump") );
}
const int battery_deficit = discharge_battery( that_item_there->weight() / 1_gram *
-part_epower( scoop ) / rng( 8, 15 ) );
if( battery_deficit == 0 && add_item( scoop, *that_item_there ) ) {
g->m.i_rem( position, itemdex );
} else {
break;
}
}
}
}
void vehicle::alarm() {
if( one_in(4) ) {
//first check if the alarm is still installed
bool found_alarm = has_security_working();
//if alarm found, make noise, else set alarm disabled
if( found_alarm ) {
const std::array<std::string, 4> sound_msgs = {{
_("WHOOP WHOOP"), _("NEEeu NEEeu NEEeu"), _("BLEEEEEEP"), _("WREEP")
}};
sounds::sound( global_pos3(), (int) rng(45,80), sound_msgs[rng(0,3)] );
if( one_in(1000) ) {
is_alarm_on = false;
}
} else {
is_alarm_on = false;
}
}
}
void vehicle::slow_leak()
{
// for each badly damaged tanks (lower than 50% health), leak a small amount
for( auto &p : parts ) {
auto dmg = double( p.hp() ) / p.info().durability;
if( dmg > 0.5 || p.ammo_remaining() <= 0 ) {
continue;
}
auto fuel = p.ammo_current();
if( fuel != fuel_type_gasoline && fuel != fuel_type_diesel &&
fuel != fuel_type_battery && fuel != fuel_type_water ) {
continue; // not a liquid fuel or battery
}
int qty = std::max( ( 0.5 - dmg ) * ( 0.5 - dmg) * p.ammo_remaining() / 10, 1.0 );
// damaged batteries self-discharge without leaking
if( fuel != fuel_type_battery ) {
item leak( fuel, calendar::turn, qty );
point q = coord_translate( p.mount );
tripoint dest( global_x() + q.x, global_y() + q.y, smz );
g->m.add_item_or_charges( dest, leak );
}
p.ammo_consume( qty, global_part_pos3( p ) );
}
}
void vehicle::thrust( int thd ) {
//if vehicle is stopped, set target direction to forward.
//ensure it is not skidding. Set turns used to 0.
if( velocity == 0 ) {
turn_dir = face.dir();
move = face;
of_turn_carry = 0;
last_turn = 0;
skidding = false;
}
if( has_part( "STEREO", true ) ) {
play_music();
}
if( has_part( "CHIMES", true ) ) {
play_chimes();
}
// No need to change velocity
if( !thd ) {
return;
}
bool pl_ctrl = player_in_control( g->u );
// No need to change velocity if there are no wheels
if( !valid_wheel_config( !floating.empty() ) && velocity == 0 ) {
if( pl_ctrl ) {
if( floating.empty() ) {
add_msg(_("The %s doesn't have enough wheels to move!"), name.c_str());
} else {
add_msg(_("The %s is too leaky!"), name.c_str());
}
}
return;
}
// Accelerate (true) or brake (false)
bool thrusting = true;
if( velocity ) {
int sgn = (velocity < 0) ? -1 : 1;
thrusting = (sgn == thd);
}
// @todo: Pass this as an argument to avoid recalculating
float traction = k_traction( g->m.vehicle_wheel_traction( *this ) );
int accel = acceleration() * traction;
if( thrusting && accel == 0 ) {
if( pl_ctrl ) {
add_msg( _("The %s is too heavy for its engine(s)!"), name.c_str() );
}
return;
}
int max_vel = max_velocity() * traction;
// Get braking power
int brake = 30 * k_mass();
int brk = abs(velocity) * brake / 100;
if (brk < accel) {
brk = accel;
}
if (brk < 10 * 100) {
brk = 10 * 100;
}
//pos or neg if accelerator or brake
int vel_inc = ((thrusting) ? accel : brk) * thd;
if( thd == -1 && thrusting ) {
//accelerate 60% if going backward
vel_inc = .6 * vel_inc;
}
// Keep exact cruise control speed
if( cruise_on ) {
if( thd > 0 ) {
vel_inc = std::min( vel_inc, cruise_velocity - velocity );
} else {
vel_inc = std::max( vel_inc, cruise_velocity - velocity );
}
}
//find power ratio used of engines max
double load;
if( cruise_on ) {
load = ((float)abs(vel_inc)) / std::max((thrusting ? accel : brk),1);
} else {
load = (thrusting ? 1.0 : 0.0);
}
// only consume resources if engine accelerating
if (load >= 0.01 && thrusting) {
//abort if engines not operational
if( total_power () <= 0 || !engine_on || accel == 0 ) {
if (pl_ctrl) {
if( total_power( false ) <= 0 ) {
add_msg( m_info, _("The %s doesn't have an engine!"), name.c_str() );
} else if( has_engine_type( fuel_type_muscle, true ) ) {
add_msg( m_info, _("The %s's mechanism is out of reach!"), name.c_str() );
} else if( !engine_on ) {
add_msg( _("The %s's engine isn't on!"), name.c_str() );
} else if( traction < 0.01f ) {
add_msg( _("The %s is stuck."), name.c_str() );
} else {
add_msg( _("The %s's engine emits a sneezing sound."), name.c_str() );
}
}
cruise_velocity = 0;
return;
}
//make noise and consume fuel
noise_and_smoke (load);
consume_fuel (load);
//break the engines a bit, if going too fast.
int strn = (int) (strain () * strain() * 100);
for (size_t e = 0; e < engines.size(); e++){
do_engine_damage(e, strn);
}
}
//wheels aren't facing the right way to change velocity properly
//lower down, since engines should be getting damaged anyway
if( skidding ) {
return;
}
//change vehicles velocity
if( (velocity > 0 && velocity + vel_inc < 0) ||
(velocity < 0 && velocity + vel_inc > 0) ) {
//velocity within braking distance of 0
stop ();
} else {
// Increase velocity up to max_vel or min_vel, but not above.
const int min_vel = -max_vel / 4;
if( vel_inc > 0 ) {
// Don't allow braking by accelerating (could happen with damaged engines)
velocity = std::max( velocity, std::min( velocity + vel_inc, max_vel ) );
} else {
velocity = std::min( velocity, std::max( velocity + vel_inc, min_vel ) );
}
}
}
void vehicle::cruise_thrust (int amount)
{
if( amount == 0 ) {
return;
}
int safe_vel = safe_velocity();
int max_vel = max_velocity();
int max_rev_vel = -max_vel / 4;
//if the safe velocity is between the cruise velocity and its next value, set to safe velocity
if( (cruise_velocity < safe_vel && safe_vel < (cruise_velocity + amount)) ||
(cruise_velocity > safe_vel && safe_vel > (cruise_velocity + amount)) ){
cruise_velocity = safe_vel;
} else {
if (amount < 0 && (cruise_velocity == safe_vel || cruise_velocity == max_vel)){
// If coming down from safe_velocity or max_velocity decrease by one so
// the rounding below will drop velocity to a multiple of amount.
cruise_velocity += -1;
} else if( amount > 0 && cruise_velocity == max_rev_vel ) {
// If increasing from max_rev_vel, do the opposite.
cruise_velocity += 1;
} else {
// Otherwise just add the amount.
cruise_velocity += amount;
}
// Integer round to lowest multiple of amount.
// The result is always equal to the original or closer to zero,
// even if negative
cruise_velocity = (cruise_velocity / abs(amount)) * abs(amount);
}
// Can't have a cruise speed faster than max speed
// or reverse speed faster than max reverse speed.
if (cruise_velocity > max_vel) {
cruise_velocity = max_vel;
} else if (cruise_velocity < max_rev_vel) {
cruise_velocity = max_rev_vel;
}
}
void vehicle::turn( int deg )
{
if (deg == 0) {
return;
}
if (velocity < 0) {
deg = -deg;
}
last_turn = deg;
turn_dir += deg;
if (turn_dir < 0) {
turn_dir += 360;
}
if (turn_dir >= 360) {
turn_dir -= 360;
}
}
void vehicle::stop ()
{
velocity = 0;
skidding = false;
move = face;
last_turn = 0;
of_turn_carry = 0;
}
bool vehicle::collision( std::vector<veh_collision> &colls,
const tripoint &dp,
bool just_detect, bool bash_floor )
{
/*
* Big TODO:
* Rewrite this function so that it has "pre-collision" phase (detection)
* and "post-collision" phase (applying damage).
* Then invoke the functions cyclically (pre-post-pre-post-...) until
* velocity == 0 or no collision happens.
* Make all post-collisions in a given phase use the same momentum.
*
* How it works right now: find the first obstacle, then ram it over and over
* until either the obstacle is removed or the vehicle stops.
* Bug: when ramming a critter without enough force to send it flying,
* the vehicle will phase into it.
*/
if( dp.z != 0 && ( dp.x != 0 || dp.y != 0 ) ) {
// Split into horizontal + vertical
return collision( colls, tripoint( dp.x, dp.y, 0 ), just_detect, bash_floor ) ||
collision( colls, tripoint( 0, 0, dp.z ), just_detect, bash_floor );
}
if( dp.z == -1 && !bash_floor ) {
// First check current level, then the one below if current had no collisions
// Bash floors on the current one, but not on the one below.
if( collision( colls, tripoint( 0, 0, 0 ), just_detect, true ) ) {
return true;
}
}
const bool vertical = bash_floor || dp.z != 0;
const int &coll_velocity = vertical ? vertical_velocity : velocity;
if( !just_detect && coll_velocity == 0 ) {
debugmsg( "Collision check on stationary vehicle %s", name.c_str() );
just_detect = true;
}
const int velocity_before = coll_velocity;
const int sign_before = sgn( velocity_before );
std::vector<int> structural_indices = all_parts_at_location(part_location_structure);
for( size_t i = 0; i < structural_indices.size(); i++ ) {
const int p = structural_indices[i];
// Coordinates of where part will go due to movement (dx/dy/dz)
// and turning (precalc[1])
const tripoint dsp = global_pos3() + dp + parts[p].precalc[1];
veh_collision coll = part_collision( p, dsp, just_detect, bash_floor );
if( coll.type == veh_coll_nothing ) {
continue;
}
colls.push_back( coll );
if( just_detect ) {
// DO insert the first collision so we can tell what was it
return true;
}
const int velocity_after = coll_velocity;
// A hack for falling vehicles: restore the velocity so that it hits at full force everywhere
// TODO: Make this more elegant
if( vertical ) {
vertical_velocity = velocity_before;
} else if( !just_detect && sgn( velocity_after ) != sign_before ) {
// Sign of velocity inverted, collisions would be in wrong direction
break;
}
}
if( structural_indices.empty() ) {
// Hack for dirty vehicles that didn't yet get properly removed
veh_collision fake_coll;
fake_coll.type = veh_coll_other;
colls.push_back( fake_coll );
velocity = 0;
vertical_velocity = 0;
add_msg( m_debug, "Collision check on a dirty vehicle %s", name.c_str() );
return true;
}
return !colls.empty();
}
// A helper to make sure mass and density is always calculated the same way
void terrain_collision_data( const tripoint &p, bool bash_floor,
float &mass, float &density, float &elastic )
{
elastic = 0.30;
// Just a rough rescale for now to obtain approximately equal numbers
const int bash_min = g->m.bash_resistance( p, bash_floor );
const int bash_max = g->m.bash_strength( p, bash_floor );
mass = ( bash_min + bash_max ) / 2;
density = bash_min;
}
veh_collision vehicle::part_collision( int part, const tripoint &p,
bool just_detect, bool bash_floor )
{
// Vertical collisions need to be handled differently
// All collisions have to be either fully vertical or fully horizontal for now
const bool vert_coll = bash_floor || p.z != smz;
const bool pl_ctrl = player_in_control( g->u );
Creature *critter = g->critter_at( p, true );
player *ph = dynamic_cast<player*>( critter );
Creature *driver = pl_ctrl ? &g->u : nullptr;
// If in a vehicle assume it's this one
if( ph != nullptr && ph->in_vehicle ) {
critter = nullptr;
ph = nullptr;
}
int target_part = -1;
vehicle *oveh = g->m.veh_at( p, target_part );
// Disable vehicle/critter collisions when bashing floor
// TODO: More elegant code
const bool is_veh_collision = !bash_floor && oveh != nullptr && oveh != this;
const bool is_body_collision = !bash_floor && critter != nullptr;
veh_collision ret;
ret.type = veh_coll_nothing;
ret.part = part;
// Vehicle collisions are a special case. just return the collision.
// The map takes care of the dynamic stuff.
if( is_veh_collision ) {
ret.type = veh_coll_veh;
//"imp" is too simplistic for vehicle-vehicle collisions
ret.target = oveh;
ret.target_part = target_part;
ret.target_name = oveh->disp_name();
return ret;
}
// Non-vehicle collisions can't happen when the vehicle is not moving
int &coll_velocity = vert_coll ? vertical_velocity : velocity;
if( !just_detect && coll_velocity == 0 ) {
return ret;
}
// Damage armor before damaging any other parts
// Actually target, not just damage - spiked plating will "hit back", for example
const int armor_part = part_with_feature( ret.part, VPFLAG_ARMOR );
if( armor_part >= 0 ) {
ret.part = armor_part;
}
int dmg_mod = part_info( ret.part ).dmg_mod;
// Let's calculate type of collision & mass of object we hit
float mass2 = 0;
float e = 0.3; // e = 0 -> plastic collision
// e = 1 -> inelastic collision
float part_dens = 0; //part density
if( is_body_collision ) {
// Check any monster/NPC/player on the way
ret.type = veh_coll_body; // body
ret.target = critter;
e = 0.30;
part_dens = 15;
switch( critter->get_size() ) {
case MS_TINY: // Rodent
mass2 = 1;
break;
case MS_SMALL: // Half human
mass2 = 41;
break;
default:
case MS_MEDIUM: // Human
mass2 = 82;
break;
case MS_LARGE: // Cow
mass2 = 400;
break;
case MS_HUGE: // TAAAANK
mass2 = 1000;
break;
}
ret.target_name = critter->disp_name();
} else if( ( bash_floor && g->m.is_bashable_ter_furn( p, true ) ) ||
( g->m.is_bashable_ter_furn( p, false ) && g->m.move_cost_ter_furn( p ) != 2 &&
// Don't collide with tiny things, like flowers, unless we have a wheel in our space.
(part_with_feature(ret.part, VPFLAG_WHEEL) >= 0 ||
!g->m.has_flag_ter_or_furn("TINY", p)) &&
// Protrusions don't collide with short terrain.
// Tiny also doesn't, but it's already excluded unless there's a wheel present.
!(part_with_feature(ret.part, "PROTRUSION") >= 0 &&
g->m.has_flag_ter_or_furn("SHORT", p)) &&
// These are bashable, but don't interact with vehicles.
!g->m.has_flag_ter_or_furn("NOCOLLIDE", p) ) ) {
// Movecost 2 indicates flat terrain like a floor, no collision there.
ret.type = veh_coll_bashable;
terrain_collision_data( p, bash_floor, mass2, part_dens, e );
ret.target_name = g->m.disp_name( p );
} else if( g->m.impassable_ter_furn( p ) ||
( bash_floor && !g->m.has_flag( TFLAG_NO_FLOOR, p ) ) ) {
ret.type = veh_coll_other; // not destructible
mass2 = 1000;
e = 0.10;
part_dens = 80;
ret.target_name = g->m.disp_name( p );
}
if( ret.type == veh_coll_nothing || just_detect ) {
// Hit nothing or we aren't actually hitting
return ret;
}
// Calculate mass AFTER checking for collision
// because it involves iterating over all cargo
const float mass = to_kilogram( total_mass() );
//Calculate damage resulting from d_E
const itype *type = item::find_type( part_info( ret.part ).item );
const auto &mats = type->materials;
float vpart_dens = 0;
if( !mats.empty() ) {
for( auto &mat_id : mats ) {
vpart_dens += mat_id.obj().density();
}
vpart_dens /= mats.size(); // average
}
//k=100 -> 100% damage on part
//k=0 -> 100% damage on obj
float material_factor = (part_dens - vpart_dens)*0.5;
material_factor = std::max( -25.0f, std::min( 25.0f, material_factor ) );
// factor = -25 if mass is much greater than mass2
// factor = +25 if mass2 is much greater than mass
const float weight_factor = mass >= mass2 ?
-25 * ( log(mass) - log(mass2) ) / log(mass) :
25 * ( log(mass2) - log(mass) ) / log(mass2);
float k = 50 + material_factor + weight_factor;
k = std::max( 10.0f, std::min( 90.0f, k ) );
bool smashed = true;
std::string snd; // NOTE: Unused!
float dmg = 0.0f;
float part_dmg = 0.0f;
// Calculate Impulse of car
int turns_stunned = 0;
const int prev_velocity = coll_velocity;
const int vel_sign = sgn( coll_velocity );
// Velocity of the object we're hitting
// Assuming it starts at 0, but we'll probably hit it many times
// in one collision, so accumulate the velocity gain from each hit.
float vel2 = 0.0f;
do {
smashed = false;
// Impulse of vehicle
const float vel1 = coll_velocity / 100.0f;
// Velocity of car after collision
const float vel1_a = (mass*vel1 + mass2*vel2 + e*mass2*(vel2 - vel1)) / (mass + mass2);
// Velocity of object after collision
const float vel2_a = (mass*vel1 + mass2*vel2 + e*mass *(vel1 - vel2)) / (mass + mass2);
// Lost energy at collision -> deformation energy -> damage
const float E_before = 0.5f * (mass * vel1 * vel1) + 0.5f * (mass2 * vel2 * vel2);
const float E_after = 0.5f * (mass * vel1_a*vel1_a) + 0.5f * (mass2 * vel2_a*vel2_a);
const float d_E = E_before - E_after;
if( d_E <= 0 ) {
// Deformation energy is signed
// If it's negative, it means something went wrong
// But it still does happen sometimes...
if( fabs(vel1_a) < fabs(vel1) ) {
// Lower vehicle's speed to prevent infinite loops
coll_velocity = vel1_a * 90;
}
if( fabs(vel2_a) > fabs(vel2) ) {
vel2 = vel2_a;
}
continue;
}
add_msg( m_debug, "Deformation energy: %.2f", d_E );
// Damage calculation
// Damage dealt overall
dmg += d_E / 400;
// Damage for vehicle-part
// Always if no critters, otherwise if critter is real
if( critter == nullptr || !critter->is_hallucination() ) {
part_dmg = dmg * k / 100;
add_msg( m_debug, "Part collision damage: %.2f", part_dmg );
}
// Damage for object
const float obj_dmg = dmg * (100-k)/100;
if( ret.type == veh_coll_other ) {
} else if( ret.type == veh_coll_bashable ) {
// Something bashable -- use map::bash to determine outcome
// NOTE: Floor bashing disabled for balance reasons
// Floor values are still used to set damage dealt to vehicle
smashed = g->m.is_bashable_ter_furn( p, false ) &&
g->m.bash_resistance( p, bash_floor ) <= obj_dmg &&
g->m.bash( p, obj_dmg, false, false, false, this ).success;
if( smashed ) {
if( g->m.is_bashable_ter_furn( p, bash_floor ) ) {
// There's new terrain there to smash
smashed = false;
terrain_collision_data( p, bash_floor, mass2, part_dens, e );
ret.target_name = g->m.disp_name( p );
} else if( g->m.impassable_ter_furn( p ) ) {
// There's new terrain there, but we can't smash it!
smashed = false;
ret.type = veh_coll_other;
mass2 = 1000;
e = 0.10;
part_dens = 80;
ret.target_name = g->m.disp_name( p );
}
}
} else if( ret.type == veh_coll_body ) {
int dam = obj_dmg*dmg_mod/100;
// No blood from hallucinations
if( !critter->is_hallucination() ) {
if( part_flag( ret.part, "SHARP" ) ) {
parts[ret.part].blood += (20 + dam) * 5;
} else if( dam > rng ( 10, 30 ) ) {
parts[ret.part].blood += (10 + dam / 2) * 5;
}
check_environmental_effects = true;
}
turns_stunned = ( rng( 0, dam ) > 10 ) + ( rng( 0, dam ) > 40 );
if( turns_stunned > 0 ) {
critter->add_effect( effect_stunned, turns_stunned );
}
if( ph != nullptr ) {
ph->hitall( dam, 40, driver );
} else {
const int armor = part_flag( ret.part, "SHARP" ) ?
critter->get_armor_cut( bp_torso ) :
critter->get_armor_bash( bp_torso );
dam = std::max( 0, dam - armor );
critter->apply_damage( driver, bp_torso, dam );
add_msg( m_debug, "Critter collision damage: %d", dam );
}
// Don't fling if vertical - critter got smashed into the ground
if( !vert_coll ) {
if( fabs(vel2_a) > 10.0f ||
fabs(e * mass * vel1_a) > fabs(mass2 * (10.0f - vel2_a)) ) {
const int angle = rng( -60, 60 );
// Also handle the weird case when we don't have enough force
// but still have to push (in such case compare momentum)
const float push_force = std::max<float>( fabs( vel2_a ), 10.1f );
// move.dir is where the vehicle is facing. If velocity is negative,
// we're moving backwards and have to adjust the angle accordingly.
const int angle_sum = angle + move.dir() + ( vel2_a > 0 ? 0 : 180 );
g->fling_creature( critter, angle_sum, push_force );
} else if( fabs( vel2_a ) > fabs( vel2 ) ) {
vel2 = vel2_a;
} else {
// Vehicle's momentum isn't big enough to push the critter
velocity = 0;
break;
}
if( critter->is_dead_state() ) {
smashed = true;
} else {
// Only count critter as pushed away if it actually changed position
smashed = (critter->pos() != p);
}
}
}
if( critter == nullptr || !critter->is_hallucination() ) {
coll_velocity = vel1_a * ( smashed ? 100 : 90 );
}
// Stop processing when sign inverts, not when we reach 0
} while( !smashed && sgn( coll_velocity ) == vel_sign );
// Apply special effects from collision.
if( critter != nullptr ) {
if( !critter->is_hallucination() ) {
if( pl_ctrl ) {
if( turns_stunned > 0 ) {
//~ 1$s - vehicle name, 2$s - part name, 3$s - NPC or monster
add_msg (m_warning, _("Your %1$s's %2$s rams into %3$s and stuns it!"),
name.c_str(), parts[ ret.part ].name().c_str(), ret.target_name.c_str());
} else {
//~ 1$s - vehicle name, 2$s - part name, 3$s - NPC or monster
add_msg (m_warning, _("Your %1$s's %2$s rams into %3$s!"),
name.c_str(), parts[ ret.part ].name().c_str(), ret.target_name.c_str());
}
}
if( part_flag( ret.part, "SHARP" ) ) {
critter->bleed();
} else {
sounds::sound( p, 20, snd );
}
}
} else {
if( pl_ctrl ) {
if( snd.length() > 0 ) {
//~ 1$s - vehicle name, 2$s - part name, 3$s - collision object name, 4$s - sound message
add_msg (m_warning, _("Your %1$s's %2$s rams into %3$s with a %4$s"),
name.c_str(), parts[ ret.part ].name().c_str(), ret.target_name.c_str(), snd.c_str());
} else {
//~ 1$s - vehicle name, 2$s - part name, 3$s - collision object name
add_msg (m_warning, _("Your %1$s's %2$s rams into %3$s."),
name.c_str(), parts[ ret.part ].name().c_str(), ret.target_name.c_str());
}
}
sounds::sound(p, smashed ? 80 : 50, snd );
}
if( smashed && !vert_coll ) {
int turn_amount = rng( 1, 3 ) * sqrt((double)part_dmg);
turn_amount /= 15;
if( turn_amount < 1 ) {
turn_amount = 1;
}
turn_amount *= 15;
if( turn_amount > 120 ) {
turn_amount = 120;
}
int turn_roll = rng( 0, 100 );
// Probability of skidding increases with higher delta_v
if( turn_roll < std::abs((prev_velocity - coll_velocity) / 100.0f * 2.0f) ) {
//delta_v = vel1 - vel1_a
//delta_v = 50 mph -> 100% probability of skidding
//delta_v = 25 mph -> 50% probability of skidding
skidding = true;
turn( one_in( 2 ) ? turn_amount : -turn_amount );
}
}
ret.imp = part_dmg;
return ret;
}
void vehicle::handle_trap( const tripoint &p, int part )
{
int pwh = part_with_feature( part, VPFLAG_WHEEL );
if( pwh < 0 ) {
return;
}
const trap &tr = g->m.tr_at(p);
const trap_id t = tr.loadid;
int noise = 0;
int chance = 100;
int expl = 0;
int shrap = 0;
int part_damage = 0;
std::string snd;
// @todo: make trapfuncv?
if ( t == tr_bubblewrap ) {
noise = 18;
snd = _("Pop!");
} else if ( t == tr_beartrap || t == tr_beartrap_buried ) {
noise = 8;
snd = _("SNAP!");
part_damage = 300;
g->m.remove_trap(p);
g->m.spawn_item(p, "beartrap");
} else if ( t == tr_nailboard || t == tr_caltrops ) {
part_damage = 300;
} else if ( t == tr_blade ) {
noise = 1;
snd = _("Swinnng!");
part_damage = 300;
} else if ( t == tr_crossbow ) {
chance = 30;
noise = 1;
snd = _("Clank!");
part_damage = 300;
g->m.remove_trap(p);
g->m.spawn_item(p, "crossbow");
g->m.spawn_item(p, "string_6");
if (!one_in(10)) {
g->m.spawn_item(p, "bolt_steel");
}
} else if ( t == tr_shotgun_2 || t == tr_shotgun_1 ) {
noise = 60;
snd = _("Bang!");
chance = 70;
part_damage = 300;
if (t == tr_shotgun_2) {
g->m.trap_set(p, tr_shotgun_1);
} else {
g->m.remove_trap(p);
g->m.spawn_item(p, "shotgun_s");
g->m.spawn_item(p, "string_6");
}
} else if ( t == tr_landmine_buried || t == tr_landmine ) {
expl = 10;
shrap = 8;
g->m.remove_trap(p);
part_damage = 1000;
} else if ( t == tr_boobytrap ) {
expl = 18;
shrap = 12;
part_damage = 1000;
} else if ( t == tr_dissector ) {
noise = 10;
snd = _("BRZZZAP!");
part_damage = 500;
} else if( t == tr_sinkhole || t == tr_pit || t == tr_spike_pit || t == tr_glass_pit ) {
part_damage = 500;
} else if( t == tr_ledge ) {
falling = true;
// Don't print message
return;
} else if( t == tr_lava ) {
part_damage = 500;
//@todo Make this damage not only wheels, but other parts too, especially tanks with flammable fuel
} else {
return;
}
if( g->u.sees(p) ) {
if( g->u.knows_trap( p ) ) {
//~ %1$s: name of the vehicle; %2$s: name of the related vehicle part; %3$s: trap name
add_msg(m_bad, _("The %1$s's %2$s runs over %3$s."), name.c_str(),
parts[ part ].name().c_str(), tr.name().c_str() );
} else {
add_msg(m_bad, _("The %1$s's %2$s runs over something."), name.c_str(),
parts[ part ].name().c_str() );
}
}
if (noise > 0) {
sounds::sound(p, noise, snd);
}
if( part_damage && chance >= rng (1, 100) ) {
// Hit the wheel directly since it ran right over the trap.
damage_direct( pwh, part_damage );
}
if( expl > 0 ) {
g->explosion( p, expl, 0.5f, false, shrap );
}
}
// total volume of all the things
units::volume vehicle::stored_volume(int const part) const
{
return get_items( part ).stored_volume();
}
units::volume vehicle::max_volume(int const part) const
{
return get_items( part ).max_volume();
}
units::volume vehicle::free_volume(int const part) const
{
return get_items( part ).free_volume();
}
void vehicle::make_active( item_location &loc )
{
item *target = loc.get_item();
if( !target->needs_processing() ) {
return;
}
auto cargo_parts = get_parts( loc.position(), "CARGO" );
if( cargo_parts.empty() ) {
return;
}
// System insures that there is only one part in this vector.
vehicle_part *cargo_part = cargo_parts.front();
auto &item_stack = cargo_part->items;
auto item_index = std::find_if( item_stack.begin(), item_stack.end(),
[&target]( const item &i ) { return &i == target; } );
active_items.add( item_index, cargo_part->mount );
}
long vehicle::add_charges( int part, const item &itm )
{
if( !itm.count_by_charges() ) {
debugmsg( "Add charges was called for an item not counted by charges!" );
return 0;
}
const long ret = get_items( part ).amount_can_fit( itm );
if( ret == 0 ) {
return 0;
}
item itm_copy = itm;
itm_copy.charges = ret;
return add_item( part, itm_copy ) ? ret : 0;
}
bool vehicle::add_item( int part, const item &itm )
{
if( part < 0 || part >= ( int )parts.size() ) {
debugmsg( "int part (%d) is out of range", part );
return false;
}
// const int max_weight = ?! // TODO: weight limit, calculation per vpart & vehicle stats, not a hard user limit.
// add creaking sounds and damage to overloaded vpart, outright break it past a certain point, or when hitting bumps etc
if( parts[ part ].base.is_gun() ) {
if( !itm.is_ammo() || itm.ammo_type() != parts[ part ].base.ammo_type() ) {
return false;
}
}
bool charge = itm.count_by_charges();
vehicle_stack istack = get_items( part );
const long to_move = istack.amount_can_fit( itm );
if( to_move == 0 || ( charge && to_move < itm.charges ) ) {
return false; // @add_charges should be used in the latter case
}
if( charge ) {
item *here = istack.stacks_with( itm );
if( here ) {
invalidate_mass();
return here->merge_charges( itm );
}
}
return add_item_at( part, parts[part].items.end(), itm );
}
bool vehicle::add_item( vehicle_part &pt, const item &obj )
{
int idx = index_of_part( &pt );
if( idx < 0 ) {
debugmsg( "Tried to add item to invalid part" );
return false;
}
return add_item( idx, obj );
}
bool vehicle::add_item_at(int part, std::list<item>::iterator index, item itm)
{
if( itm.is_bucket_nonempty() ) {
for( auto &elem : itm.contents ) {
g->m.add_item_or_charges( global_part_pos3( part ), elem );
}
itm.contents.clear();
}
const auto new_pos = parts[part].items.insert( index, itm );
if( itm.needs_processing() ) {
active_items.add( new_pos, parts[part].mount );
}
invalidate_mass();
return true;
}
bool vehicle::remove_item( int part, int itemdex )
{
if( itemdex < 0 || itemdex >= (int)parts[part].items.size() ) {
return false;
}
remove_item( part, std::next(parts[part].items.begin(), itemdex) );
return true;
}
bool vehicle::remove_item( int part, const item *it )
{
bool rc = false;
std::list<item>& veh_items = parts[part].items;
for( auto iter = veh_items.begin(); iter != veh_items.end(); iter++ ) {
//delete the item if the pointer memory addresses are the same
if( it == &*iter ) {
remove_item(part, iter);
rc = true;
break;
}
}
return rc;
}
std::list<item>::iterator vehicle::remove_item( int part, std::list<item>::iterator it )
{
std::list<item>& veh_items = parts[part].items;
if( active_items.has( it, parts[part].mount ) ) {
active_items.remove( it, parts[part].mount );
}
invalidate_mass();
return veh_items.erase(it);
}
vehicle_stack vehicle::get_items(int const part)
{
return vehicle_stack( &parts[part].items, global_pos() + parts[part].precalc[0],
this, part );
}
vehicle_stack vehicle::get_items( int const part ) const
{
// HACK: callers could modify items through this
// TODO: a const version of vehicle_stack is needed
return const_cast<vehicle*>(this)->get_items(part);
}
void vehicle::place_spawn_items()
{
if( !type.is_valid() ) {
return;
}
for( const auto &pt : type->parts ) {
if( pt.with_ammo ) {
int turret = part_with_feature_at_relative( pt.pos, "TURRET" );
if( turret >= 0 && x_in_y( pt.with_ammo, 100 ) ) {
parts[ turret ].ammo_set( random_entry( pt.ammo_types ), rng( pt.ammo_qty.first, pt.ammo_qty.second ) );
}
}
}
for( const auto& spawn : type.obj().item_spawns ) {
if( rng( 1, 100 ) <= spawn.chance ) {
int part = part_with_feature_at_relative( spawn.pos, "CARGO", false );
if( part < 0 ) {
debugmsg( "No CARGO parts at (%d, %d) of %s!", spawn.pos.x, spawn.pos.y, name.c_str() );
} else {
// if vehicle part is broken only 50% of items spawn and they will be variably damaged
bool broken = parts[ part ].is_broken();
if( broken && one_in( 2 ) ) {
continue;
}
std::vector<item> created;
for( const itype_id& e : spawn.item_ids ) {
created.emplace_back( item( e ).in_its_container() );
}
for( const std::string& e : spawn.item_groups ) {
created.emplace_back( item_group::item_from( e, calendar::turn ) );
}
for( item& e : created ) {
if( e.is_null() ) {
continue;
}
if( broken && e.mod_damage( rng( 1, e.max_damage() ) ) ) {
continue; // we destroyed the item
}
if( e.is_tool() || e.is_gun() || e.is_magazine() ) {
bool spawn_ammo = rng( 0, 99 ) < spawn.with_ammo && e.ammo_remaining() == 0;
bool spawn_mag = rng( 0, 99 ) < spawn.with_magazine && !e.magazine_integral() && !e.magazine_current();
if( spawn_mag ) {
e.contents.emplace_back( e.magazine_default(), e.birthday() );
}
if( spawn_ammo ) {
e.ammo_set( e.ammo_type()->default_ammotype() );
}
}
add_item( part, e);
}
}
}
}
}
void vehicle::gain_moves()
{
if( velocity != 0 || falling ) {
if( loose_parts.size() > 0 ) {
shed_loose_parts();
}
of_turn = 1 + of_turn_carry;
} else {
of_turn = 0;
}
of_turn_carry = 0;
// cruise control TODO: enable for NPC?
if( player_in_control(g->u) && cruise_on && cruise_velocity != velocity ) {
thrust( (cruise_velocity) > velocity ? 1 : -1 );
}
// Force off-map vehicles to load by visiting them every time we gain moves.
// Shouldn't be too expensive if there aren't fifty trillion vehicles in the graph...
// ...and if there are, it's the player's fault for putting them there.
auto nil_visitor = [] (vehicle*, int amount, int) { return amount; };
traverse_vehicle_graph(this, 1, nil_visitor);
if( check_environmental_effects ) {
check_environmental_effects = do_environmental_effects();
}
// turrets which are enabled will try to reload and then automatically fire
// Turrets which are disabled but have targets set are a special case
for( auto e : turrets() ) {
if( e->enabled || e->target.second != e->target.first ) {
automatic_fire_turret( *e );
}
}
if( velocity < 0 ) {
beeper_sound();
}
}
/**
* Refreshes all caches and refinds all parts. Used after the vehicle has had a part added or removed.
* Makes indices of different part types so they're easy to find. Also calculates power drain.
*/
void vehicle::refresh()
{
alternators.clear();
engines.clear();
reactors.clear();
solar_panels.clear();
funnels.clear();
relative_parts.clear();
loose_parts.clear();
wheelcache.clear();
steering.clear();
speciality.clear();
floating.clear();
tracking_epower = 0;
alternator_load = 0;
camera_epower = 0;
extra_drag = 0;
// Used to sort part list so it displays properly when examining
struct sort_veh_part_vector {
vehicle *veh;
inline bool operator() (const int p1, const int p2) {
return veh->part_info(p1).list_order < veh->part_info(p2).list_order;
}
} svpv = { this };
std::vector<int>::iterator vii;
// Main loop over all vehicle parts.
for( size_t p = 0; p < parts.size(); p++ ) {
const vpart_info& vpi = part_info( p );
if( parts[p].removed ) {
continue;
}
if( vpi.has_flag(VPFLAG_ALTERNATOR) ) {
alternators.push_back( p );
}
if( vpi.has_flag(VPFLAG_ENGINE) ) {
engines.push_back( p );
}
if( vpi.has_flag("REACTOR") ) {
reactors.push_back( p );
}
if( vpi.has_flag(VPFLAG_SOLAR_PANEL) ) {
solar_panels.push_back( p );
}
if( vpi.has_flag("FUNNEL") ) {
funnels.push_back( p );
}
if( vpi.has_flag("UNMOUNT_ON_MOVE") ) {
loose_parts.push_back(p);
}
if( vpi.has_flag( VPFLAG_WHEEL ) ) {
wheelcache.push_back( p );
}
if (vpi.has_flag("STEERABLE") || vpi.has_flag("TRACKED")) {
// TRACKED contributes to steering effectiveness but
// (a) doesn't count as a steering axle for install difficulty
// (b) still contributes to drag for the center of steering calculation
steering.push_back(p);
}
if (vpi.has_flag("SECURITY")){
speciality.push_back(p);
}
if( vpi.has_flag( "CAMERA" ) ) {
camera_epower += vpi.epower;
}
if( vpi.has_flag( VPFLAG_FLOATS ) ) {
floating.push_back( p );
}
if( parts[ p ].enabled ) {
if( vpi.has_flag( "PLOW" ) ) {
extra_drag += vpi.power;
}
if( vpi.has_flag( "PLANTER" ) ) {
extra_drag += vpi.power;
}
if( vpi.has_flag( "REAPER" ) ) {
extra_drag += vpi.power;
}
}
// Build map of point -> all parts in that point
const point pt = parts[p].mount;
// This will keep the parts at point pt sorted
vii = std::lower_bound( relative_parts[pt].begin(), relative_parts[pt].end(), static_cast<int>( p ), svpv );
relative_parts[pt].insert( vii, p );
}
// NB: using the _old_ pivot point, don't recalc here, we only do that when moving!
precalc_mounts( 0, pivot_rotation[0], pivot_anchor[0] );
check_environmental_effects = true;
insides_dirty = true;
invalidate_mass();
}
const point &vehicle::pivot_point() const {
if (pivot_dirty) {
refresh_pivot();
}
return pivot_cache;
}
void vehicle::refresh_pivot() const {
// Const method, but messes with mutable fields
pivot_dirty = false;
if( wheelcache.empty() || !valid_wheel_config( false ) ) {
// No usable wheels, use CoM (dragging)
pivot_cache = local_center_of_mass();
return;
}
// The model here is:
//
// We are trying to rotate around some point (xc,yc)
// This produces a friction force / moment from each wheel resisting the
// rotation. We want to find the point that minimizes that resistance.
//
// For a given wheel w at (xw,yw), find:
// weight(w): a scaling factor for the friction force based on wheel
// size, brokenness, steerability/orientation
// center_dist: the distance from (xw,yw) to (xc,yc)
// centerline_angle: the angle between the X axis and a line through
// (xw,yw) and (xc,yc)
//
// Decompose the force into two components, assuming that the wheel is
// aligned along the X axis and we want to apply different weightings to
// the in-line vs perpendicular parts of the force:
//
// Resistance force in line with the wheel (X axis)
// Fi = weightI(w) * center_dist * sin(centerline_angle)
// Resistance force perpendicular to the wheel (Y axis):
// Fp = weightP(w) * center_dist * cos(centerline_angle);
//
// Then find the moment that these two forces would apply around (xc,yc)
// moment(w) = center_dist * cos(centerline_angle) * Fi +
// center_dist * sin(centerline_angle) * Fp
//
// Note that:
// cos(centerline_angle) = (xw-xc) / center_dist
// sin(centerline_angle) = (yw-yc) / center_dist
// -> moment(w) = weightP(w)*(xw-xc)^2 + weightI(w)*(yw-yc)^2
// = weightP(w)*xc^2 - 2*weightP(w)*xc*xw + weightP(w)*xw^2 +
// weightI(w)*yc^2 - 2*weightI(w)*yc*yw + weightI(w)*yw^2
//
// which happily means that the X and Y axes can be handled independently.
// We want to minimize sum(moment(w)) due to wheels w=0,1,..., which
// occurs when:
//
// sum( 2*xc*weightP(w) - 2*weightP(w)*xw ) = 0
// -> xc = (weightP(0)*x0 + weightP(1)*x1 + ...) /
// (weightP(0) + weightP(1) + ...)
// sum( 2*yc*weightI(w) - 2*weightI(w)*yw ) = 0
// -> yc = (weightI(0)*y0 + weightI(1)*y1 + ...) /
// (weightI(0) + weightI(1) + ...)
//
// so it turns into a fairly simple weighted average of the wheel positions.
float xc_numerator = 0, xc_denominator = 0;
float yc_numerator = 0, yc_denominator = 0;
for (int p : wheelcache) {
const auto &wheel = parts[p];
// @todo: load on tire?
float contact_area = wheel.wheel_area();
float weight_i; // weighting for the in-line part
float weight_p; // weighting for the perpendicular part
if( wheel.is_broken() ) {
// broken wheels don't roll on either axis
weight_i = contact_area * 2;
weight_p = contact_area * 2;
} else if (wheel.info().has_flag("STEERABLE")) {
// Unbroken steerable wheels can handle motion on both axes
// (but roll a little more easily inline)
weight_i = contact_area * 0.1;
weight_p = contact_area * 0.2;
} else {
// Regular wheels resist perpendicular motion
weight_i = contact_area * 0.1;
weight_p = contact_area;
}
xc_numerator += weight_p * wheel.mount.x;
yc_numerator += weight_i * wheel.mount.y;
xc_denominator += weight_p;
yc_denominator += weight_i;
}
if (xc_denominator < 0.1 || yc_denominator < 0.1) {
debugmsg("vehicle::refresh_pivot had a bad weight: xc=%.3f/%.3f yc=%.3f/%.3f",
xc_numerator, xc_denominator, yc_numerator, yc_denominator);
pivot_cache = local_center_of_mass();
} else {
pivot_cache.x = round(xc_numerator / xc_denominator);
pivot_cache.y = round(yc_numerator / yc_denominator);
}
}
void vehicle::remove_remote_part(int part_num) {
auto veh = find_vehicle(parts[part_num].target.second);
// If the target vehicle is still there, ask it to remove its part
if (veh != nullptr) {
auto pos = global_pos3() + parts[part_num].precalc[0];
tripoint local_abs = g->m.getabs( pos );
for( size_t j = 0; j < veh->loose_parts.size(); j++) {
int remote_partnum = veh->loose_parts[j];
auto remote_part = &veh->parts[remote_partnum];
if( veh->part_flag(remote_partnum, "POWER_TRANSFER") && remote_part->target.first == local_abs) {
veh->remove_part(remote_partnum);
return;
}
}
}
}
void vehicle::shed_loose_parts() {
// remove_part rebuilds the loose_parts vector, when all of those parts have been removed,
// it will stay empty.
while( !loose_parts.empty() ) {
int const elem = loose_parts.front();
if( part_flag( elem, "POWER_TRANSFER" ) ) {
remove_remote_part( elem );
}
auto part = &parts[elem];
auto pos = global_pos3() + part->precalc[0];
item drop = part->properties_to_item();
g->m.add_item_or_charges( pos, drop );
remove_part( elem );
}
}
void vehicle::refresh_insides ()
{
insides_dirty = false;
for (size_t p = 0; p < parts.size(); p++) {
if (parts[p].removed) {
continue;
}
/* If there's no roof, or there is a roof but it's broken, it's outside.
* (Use short-circuiting && so broken frames don't screw this up) */
if ( !( part_with_feature( p, "ROOF" ) >= 0 && !parts[ p ].is_broken() ) ) {
parts[p].inside = false;
continue;
}
parts[p].inside = true; // inside if not otherwise
for (int i = 0; i < 4; i++) { // let's check four neighbor parts
int ndx = i < 2? (i == 0? -1 : 1) : 0;
int ndy = i < 2? 0 : (i == 2? - 1: 1);
std::vector<int> parts_n3ar = parts_at_relative (parts[p].mount.x + ndx,
parts[p].mount.y + ndy);
bool cover = false; // if we aren't covered from sides, the roof at p won't save us
for (auto &j : parts_n3ar) {
if( part_flag( j, "ROOF" ) && !parts[ j ].is_broken() ) { // another roof -- cover
cover = true;
break;
}
else
if( part_flag( j, "OBSTACLE" ) && !parts[ j ].is_broken() ) {
// found an obstacle, like board or windshield or door
if (parts[j].inside || (part_flag(j, "OPENABLE") && parts[j].open)) {
continue; // door and it's open -- can't cover
}
cover = true;
break;
}
//Otherwise keep looking, there might be another part in that square
}
if (!cover) {
parts[p].inside = false;
break;
}
}
}
}
bool vehicle::is_inside(int const p) const
{
if (p < 0 || p >= (int)parts.size()) {
return false;
}
if (insides_dirty) {
// TODO: this is a bit of a hack as refresh_insides has side effects
// this should be called elsewhere and not in a function that intends to just query
const_cast<vehicle*>(this)->refresh_insides();
}
return parts[p].inside;
}
void vehicle::unboard_all ()
{
std::vector<int> bp = boarded_parts();
for( auto &i : bp ) {
g->m.unboard_vehicle( tripoint( global_x() + parts[i].precalc[0].x,
global_y() + parts[i].precalc[0].y,
smz ) );
}
}
int vehicle::damage( int p, int dmg, damage_type type, bool aimed )
{
if( dmg < 1 ) {
return dmg;
}
std::vector<int> pl = parts_at_relative( parts[p].mount.x, parts[p].mount.y );
if( pl.empty() ) {
// We ran out of non removed parts at this location already.
return dmg;
}
if( !aimed ) {
bool found_obs = false;
for( auto &i : pl ) {
if( part_flag( i, "OBSTACLE" ) &&
(!part_flag( i, "OPENABLE" ) || !parts[i].open) ) {
found_obs = true;
break;
}
}
if( !found_obs ) { // not aimed at this tile and no obstacle here -- fly through
return dmg;
}
}
int target_part = random_entry( pl );
// door motor mechanism is protected by closed doors
if( part_flag( target_part, "DOOR_MOTOR" ) ) {
// find the most strong openable that is not open
int strongest_door_part = -1;
int strongest_door_durability = INT_MIN;
for( int part : pl ) {
if( part_flag( part, "OPENABLE" ) && !parts[part].open ) {
int door_durability = part_info( part ).durability;
if (door_durability > strongest_door_durability) {
strongest_door_part = part;
strongest_door_durability = door_durability;
}
}
}
// if we found a closed door, target it instead of the door_motor
if (strongest_door_part != -1) {
target_part = strongest_door_part;
}
}
int damage_dealt;
int armor_part = part_with_feature( p, "ARMOR" );
if( armor_part < 0 ) {
// Not covered by armor -- damage part
damage_dealt = damage_direct( target_part, dmg, type );
} else {
// Covered by armor -- hit both armor and part, but reduce damage by armor's reduction
int protection = part_info( armor_part ).damage_reduction[ type ];
// Parts on roof aren't protected
bool overhead = part_flag( target_part, "ROOF" ) || part_info( target_part ).location == "on_roof";
// Calling damage_direct may remove the damaged part
// completely, therefore the other index (target_part) becomes
// wrong if target_part > armor_part.
// Damaging the part with the higher index first is save,
// as removing a part only changes indices after the
// removed part.
if( armor_part < target_part ) {
damage_direct( target_part, overhead ? dmg : dmg - protection, type );
damage_dealt = damage_direct( armor_part, dmg, type );
} else {
damage_dealt = damage_direct( armor_part, dmg, type );
damage_direct( target_part, overhead ? dmg : dmg - protection, type );
}
}
return damage_dealt;
}
void vehicle::damage_all( int dmg1, int dmg2, damage_type type, const point &impact )
{
if( dmg2 < dmg1 ) {
std::swap( dmg1, dmg2 );
}
if( dmg1 < 1 ) {
return;
}
for( size_t p = 0; p < parts.size(); p++ ) {
int distance = 1 + square_dist( parts[p].mount.x, parts[p].mount.y, impact.x, impact.y );
if( distance > 1 && part_info(p).location == part_location_structure &&
!part_info(p).has_flag("PROTRUSION") ) {
damage_direct( p, rng( dmg1, dmg2 ) / (distance * distance), type );
}
}
}
/**
* Shifts all parts of the vehicle by the given amounts, and then shifts the
* vehicle itself in the opposite direction. The end result is that the vehicle
* appears to have not moved. Useful for re-zeroing a vehicle to ensure that a
* (0, 0) part is always present.
* @param delta How much to shift along each axis
*/
void vehicle::shift_parts( const point delta )
{
// Don't invalidate the active item cache's location!
active_items.subtract_locations( delta );
for( auto &elem : parts ) {
elem.mount -= delta;
}
decltype(labels) new_labels;
for( auto &l : labels ) {
new_labels.insert( label( l.x - delta.x, l.y - delta.y, l.text ) );
}
labels = new_labels;
pivot_anchor[0] -= delta;
refresh();
//Need to also update the map after this
g->m.reset_vehicle_cache( smz );
}
/**
* Detect if the vehicle is currently missing a 0,0 part, and
* adjust if necessary.
* @return bool true if the shift was needed.
*/
bool vehicle::shift_if_needed() {
if( !parts_at_relative(0, 0).empty() ) {
// Shifting is not needed.
return false;
}
//Find a frame, any frame, to shift to
for ( size_t next_part = 0; next_part < parts.size(); ++next_part ) {
if ( part_info(next_part).location == "structure"
&& !part_info(next_part).has_flag("PROTRUSION")
&& !parts[next_part].removed) {
shift_parts( parts[next_part].mount );
refresh();
return true;
}
}
// There are only parts with PROTRUSION left, choose one of them.
for ( size_t next_part = 0; next_part < parts.size(); ++next_part ) {
if ( !parts[next_part].removed ) {
shift_parts( parts[next_part].mount );
refresh();
return true;
}
}
return false;
}
int vehicle::break_off( int p, int dmg )
{
/* Already-destroyed part - chance it could be torn off into pieces.
* Chance increases with damage, and decreases with part max durability
* (so lights, etc are easily removed; frames and plating not so much) */
if( rng( 0, part_info(p).durability / 10 ) >= dmg ) {
return dmg;
}
const auto pos = global_part_pos3( p );
if( part_info(p).location == part_location_structure ) {
// For structural parts, remove other parts first
std::vector<int> parts_in_square = parts_at_relative( parts[p].mount.x, parts[p].mount.y );
for( int index = parts_in_square.size() - 1; index >= 0; index-- ) {
// Ignore the frame being destroyed
if( parts_in_square[index] == p ) {
continue;
}
if( parts[ parts_in_square[ index ] ].is_broken() ) {
// Tearing off a broken part - break it up
if( g->u.sees( pos ) ) {
add_msg(m_bad, _("The %s's %s breaks into pieces!"), name.c_str(),
parts[ parts_in_square[ index ] ].name().c_str() );
}
break_part_into_pieces(parts_in_square[index], pos.x, pos.y, true);
} else {
// Intact (but possibly damaged) part - remove it in one piece
if( g->u.sees( pos ) ) {
add_msg(m_bad, _("The %1$s's %2$s is torn off!"), name.c_str(),
parts[ parts_in_square[ index ] ].name().c_str() );
}
item part_as_item = parts[parts_in_square[index]].properties_to_item();
g->m.add_item_or_charges( pos, part_as_item );
}
remove_part( parts_in_square[index] );
}
/* After clearing the frame, remove it if normally legal to do
* so (it's not holding the vehicle together). At a later date,
* some more complicated system (such as actually making two
* vehicles from the split parts) would be ideal. */
if( can_unmount(p) ) {
if( g->u.sees( pos ) ) {
add_msg(m_bad, _("The %1$s's %2$s is destroyed!"),
name.c_str(), parts[ p ].name().c_str() );
}
break_part_into_pieces( p, pos.x, pos.y, true );
remove_part(p);
}
} else {
//Just break it off
if( g->u.sees( pos ) ) {
add_msg(m_bad, _("The %1$s's %2$s is destroyed!"),
name.c_str(), parts[ p ].name().c_str() );
}
break_part_into_pieces( p, pos.x, pos.y, true );
remove_part( p );
}
return dmg;
}
bool vehicle::explode_fuel( int p, damage_type type )
{
const itype_id &ft = part_info(p).fuel_type;
struct fuel_explosion {
// TODO: Move the values below to jsons
int explosion_chance_hot ;
int explosion_chance_cold;
float explosion_factor;
bool fiery_explosion;
float fuel_size_factor;
};
static const std::map<itype_id, fuel_explosion> explosive_fuels = {{
{ fuel_type_gasoline, { 2, 5, 1.0f, true, 0.1f } },
{ fuel_type_diesel, { 20, 1000, 0.2f, false, 0.1f } }
}};
const auto iter = explosive_fuels.find( ft );
if( iter == explosive_fuels.end() ) {
// Not on the list means not explosive
return false;
}
const fuel_explosion &data = iter->second;
const int pow = 120 * (1 - exp(data.explosion_factor / -5000 * (parts[p].ammo_remaining() * data.fuel_size_factor)));
//debugmsg( "damage check dmg=%d pow=%d amount=%d", dmg, pow, parts[p].amount );
if( parts[ p ].is_broken() ) {
leak_fuel( parts[ p ] );
}
int explosion_chance = type == DT_HEAT ? data.explosion_chance_hot : data.explosion_chance_cold;
if( one_in( explosion_chance ) ) {
g->u.add_memorial_log(pgettext("memorial_male","The fuel tank of the %s exploded!"),
pgettext("memorial_female", "The fuel tank of the %s exploded!"),
name.c_str());
g->explosion( global_part_pos3( p ), pow, 0.7, data.fiery_explosion );
mod_hp( parts[p], 0 - parts[ p ].hp(), DT_HEAT );
parts[p].ammo_unset();
}
return true;
}
int vehicle::damage_direct( int p, int dmg, damage_type type )
{
if( parts[p].is_broken() ) {
return break_off( p, dmg );
}
int tsh = std::min( 20, part_info(p).durability / 10 );
if( dmg < tsh && type != DT_TRUE ) {
if( type == DT_HEAT && parts[p].is_tank() ) {
explode_fuel( p, type );
}
return dmg;
}
dmg -= std::min<int>( dmg, part_info( p ).damage_reduction[ type ] );
int dres = dmg - parts[p].hp();
if( mod_hp( parts[ p ], 0 - dmg, type ) ) {
insides_dirty = true;
pivot_dirty = true;
// destroyed parts lose any contained fuels, battery charges or ammo
leak_fuel( parts [ p ] );
for( const auto &e : parts[p].items ) {
g->m.add_item_or_charges( global_part_pos3( p ), e );
}
parts[p].items.clear();
invalidate_mass();
}
if( parts[p].is_tank() ) {
explode_fuel( p, type );
} else if( parts[ p ].is_broken() && part_flag(p, "UNMOUNT_ON_DAMAGE") ) {
g->m.spawn_item( global_part_pos3( p ), part_info( p ).item, 1, 0, calendar::turn );
remove_part( p );
}
return std::max( dres, 0 );
}
void vehicle::leak_fuel( vehicle_part &pt )
{
// only liquid fuels from non-empty tanks can leak out onto map tiles
if( !pt.is_tank() || pt.ammo_remaining() <= 0 ) {
return;
}
// leak in random directions but prefer closest tiles and avoid walls or other obstacles
auto tiles = closest_tripoints_first( 1, global_part_pos3( pt ) );
tiles.erase( std::remove_if( tiles.begin(), tiles.end(), []( const tripoint& e ) {
return !g->m.passable( e );
} ), tiles.end() );
// leak up to 1/3 of remaining fuel per iteration and continue until the part is empty
auto *fuel = item::find_type( pt.ammo_current() );
while( !tiles.empty() && pt.ammo_remaining() ) {
int qty = pt.ammo_consume( rng( 0, std::max( pt.ammo_remaining() / 3, 1L ) ), global_part_pos3( pt ) );
if( qty > 0 ) {
g->m.add_item_or_charges( random_entry( tiles ), item( fuel, calendar::turn, qty ) );
}
}
pt.ammo_unset();
}
std::map<itype_id, long> vehicle::fuels_left() const
{
std::map<itype_id, long> result;
for( const auto &p : parts ) {
if( p.is_tank() && p.ammo_current() != "null" ) {
result[ p.ammo_current() ] += p.ammo_remaining();
}
}
return result;
}
bool vehicle::assign_seat( vehicle_part &pt, const npc& who )
{
if( !pt.is_seat() || !pt.set_crew( who ) ) {
return false;
}
// NPC's can only be assigned to one seat in the vehicle
for( auto &e : parts ) {
if( &e == &pt ) {
continue; // skip this part
}
if( e.is_seat() ) {
const npc *n = e.crew();
if( n && n->getID() == who.getID() ) {
e.unset_crew();
}
}
}
return true;
}
/**
* Opens an openable part at the specified index. If it's a multipart, opens
* all attached parts as well.
* @param part_index The index in the parts list of the part to open.
*/
void vehicle::open(int part_index)
{
if(!part_info(part_index).has_flag("OPENABLE")) {
debugmsg("Attempted to open non-openable part %d (%s) on a %s!", part_index,
parts[ part_index ].name().c_str(), name.c_str());
} else {
open_or_close(part_index, true);
}
}
/**
* Opens an openable part at the specified index. If it's a multipart, opens
* all attached parts as well.
* @param part_index The index in the parts list of the part to open.
*/
void vehicle::close(int part_index)
{
if(!part_info(part_index).has_flag("OPENABLE")) {
debugmsg("Attempted to close non-closeable part %d (%s) on a %s!", part_index,
parts[ part_index ].name().c_str(), name.c_str());
} else {
open_or_close(part_index, false);
}
}
void vehicle::open_all_at(int p)
{
std::vector<int> parts_here = parts_at_relative(parts[p].mount.x, parts[p].mount.y);
for( auto &elem : parts_here ) {
if( part_flag( elem, VPFLAG_OPENABLE ) ) {
// Note that this will open multi-square and non-multipart parts in the tile. This
// means that adjacent open multi-square openables can still have closed stuff
// on same tile after this function returns
open( elem );
}
}
}
void vehicle::open_or_close(int const part_index, bool const opening)
{
parts[part_index].open = opening ? 1 : 0;
insides_dirty = true;
g->m.set_transparency_cache_dirty( smz );
if (!part_info(part_index).has_flag("MULTISQUARE")) {
return;
}
/* Find all other closed parts with the same ID in adjacent squares.
* This is a tighter restriction than just looking for other Multisquare
* Openable parts, and stops trunks from opening side doors and the like. */
for( size_t next_index = 0; next_index < parts.size(); ++next_index ) {
if (parts[next_index].removed) {
continue;
}
//Look for parts 1 square off in any cardinal direction
const int dx = parts[next_index].mount.x - parts[part_index].mount.x;
const int dy = parts[next_index].mount.y - parts[part_index].mount.y;
const int delta = dx * dx + dy * dy;
const bool is_near = (delta == 1);
const bool is_id = part_info(next_index).get_id() == part_info(part_index).get_id();
const bool do_next = !!parts[next_index].open ^ opening;
if (is_near && is_id && do_next) {
open_or_close(next_index, opening);
}
}
}
// A chance to stop skidding if moving in roughly the faced direction
void vehicle::possibly_recover_from_skid() {
if( last_turn > 13 ) {
// Turning on the initial skid is delayed, so move==face, initially. This filters out that case.
return;
}
rl_vec2d mv = move_vec();
rl_vec2d fv = face_vec();
float dot = mv.dot_product(fv);
// Threshold of recovery is Gaussianesque.
if( fabs( dot ) * 100 > dice( 9,20 ) ){
add_msg(_("The %s recovers from its skid."), name.c_str());
skidding = false; // face_vec takes over.
velocity *= dot; // Wheels absorb horizontal velocity.
if(dot < -.8){
// Pointed backwards, velo-wise.
velocity *= -1; // Move backwards.
}
move = face;
}
}
// if not skidding, move_vec == face_vec, mv <dot> fv == 1, velocity*1 is returned.
float vehicle::forward_velocity() const
{
rl_vec2d mv = move_vec();
rl_vec2d fv = face_vec();
float dot = mv.dot_product(fv);
return velocity * dot;
}
rl_vec2d vehicle::velo_vec() const
{
rl_vec2d ret;
if(skidding)
ret = move_vec();
else
ret = face_vec();
ret = ret.normalized();
ret = ret * velocity;
return ret;
}
inline rl_vec2d degree_to_vec( double degrees )
{
return rl_vec2d( cos( degrees * M_PI/180 ), sin( degrees * M_PI/180 ) );
}
// normalized.
rl_vec2d vehicle::move_vec() const
{
return degree_to_vec( move.dir() );
}
// normalized.
rl_vec2d vehicle::face_vec() const
{
return degree_to_vec( face.dir() );
}
rl_vec2d vehicle::dir_vec() const
{
return degree_to_vec( turn_dir );
}
float get_collision_factor(float const delta_v)
{
if (std::abs(delta_v) <= 31) {
return ( 1 - ( 0.9 * std::abs(delta_v) ) / 31 );
} else {
return 0.1;
}
}
bool vehicle::is_foldable() const
{
for (size_t i = 0; i < parts.size(); i++) {
if (!part_flag(i, "FOLDABLE")) {
return false;
}
}
return true;
}
bool vehicle::restore(const std::string &data)
{
std::istringstream veh_data(data);
try {
JsonIn json(veh_data);
parts.clear();
json.read(parts);
} catch( const JsonError &e ) {
debugmsg("Error restoring vehicle: %s", e.c_str());
return false;
}
refresh();
face.init(0);
turn_dir = 0;
turn(0);
precalc_mounts(0, pivot_rotation[0], pivot_anchor[0]);
precalc_mounts(1, pivot_rotation[1], pivot_anchor[1]);
return true;
}
int vehicle::obstacle_at_part( int p ) const
{
if( part_flag( p, VPFLAG_OBSTACLE ) && !parts[ p ].is_broken() ) {
return p;
}
int part = part_with_feature( p, VPFLAG_OBSTACLE, true );
if( part < 0 ) {
return -1; // No obstacle here
}
if( part_flag( part, VPFLAG_OPENABLE ) && parts[part].open ) {
return -1; // Open door here
}
return part;
}
std::set<tripoint> &vehicle::get_points( const bool force_refresh )
{
if( force_refresh || occupied_cache_time != calendar::turn ) {
occupied_cache_time = calendar::turn;
occupied_points.clear();
tripoint pos = global_pos3();
for( const auto &p : parts ) {
const auto &pt = p.precalc[0];
occupied_points.insert( tripoint( pos.x + pt.x, pos.y + pt.y, pos.z ) );
}
}
return occupied_points;
}
inline int modulo(int v, int m) {
// C++11: negative v and positive m result in negative v%m (or 0),
// but this is supposed to be mathematical modulo: 0 <= v%m < m,
const int r = v % m;
// Adding m in that (and only that) case.
return r >= 0 ? r : r + m;
}
bool is_sm_tile_outside( const tripoint &real_global_pos )
{
const tripoint smp = ms_to_sm_copy( real_global_pos );
const int px = modulo( real_global_pos.x, SEEX );
const int py = modulo( real_global_pos.y, SEEY );
auto sm = MAPBUFFER.lookup_submap( smp );
if( sm == nullptr ) {
debugmsg( "is_sm_tile_outside(): couldn't find submap %d,%d,%d", smp.x, smp.y, smp.z );
return false;
}
if( px < 0 || px >= SEEX || py < 0 || py >= SEEY ) {
debugmsg("err %d,%d", px, py);
return false;
}
return !(sm->ter[px][py].obj().has_flag(TFLAG_INDOORS) ||
sm->get_furn(px, py).obj().has_flag(TFLAG_INDOORS));
}
void vehicle::update_time( const time_point &update_to )
{
if( smz < 0 ) {
return;
}
const time_point update_from = last_update;
if( update_to < update_from ) {
// Special case going backwards in time - that happens
last_update = update_to;
return;
}
if( update_to >= update_from && update_to - update_from < 1_minutes ) {
// We don't need to check every turn
return;
}
last_update = update_to;
// Weather stuff, only for z-levels >= 0
// TODO: Have it wash cars from blood?
if( funnels.empty() && solar_panels.empty() ) {
return;
}
// Get one weather data set per vehicle, they don't differ much across vehicle area
const tripoint veh_loc = real_global_pos3();
auto accum_weather = sum_conditions( update_from, update_to, veh_loc );
for( int idx : funnels ) {
const auto &pt = parts[idx];
// we need an unbroken funnel mounted on the exterior of the vehicle
if( pt.is_broken() || !is_sm_tile_outside( veh_loc + pt.precalc[0] ) ) {
continue;
}
// we need an empty tank (or one already containing water) below the funnel
auto tank = std::find_if( parts.begin(), parts.end(), [&pt]( const vehicle_part &e ) {
return pt.mount == e.mount && e.is_tank() && ( e.can_reload( "water" ) || e.can_reload( "water_clean" ) );
} );
if( tank == parts.end() ) {
continue;
}
double area = pow( pt.info().size / units::legacy_volume_factor, 2 ) * M_PI;
int qty = divide_roll_remainder( funnel_charges_per_turn( area, accum_weather.rain_amount ), 1.0 );
double cost_to_purify = epower_to_power( ( qty + ( tank->can_reload( "water_clean" ) ? tank->ammo_remaining() : 0 ) )
* item::find_type( "water_purifier" )->charges_to_use() );
if( qty > 0 ) {
if( has_part( global_part_pos3( pt ), "WATER_PURIFIER", true ) && ( fuel_left( "battery" ) > cost_to_purify ) ) {
tank->ammo_set( "water_clean", tank->ammo_remaining() + qty );
discharge_battery( cost_to_purify );
} else {
tank->ammo_set( "water", tank->ammo_remaining() + qty );
}
invalidate_mass();
}
}
if( !solar_panels.empty() ) {
int epower = 0;
for( int part : solar_panels ) {
if( parts[ part ].is_broken() ) {
continue;
}
const tripoint part_loc = veh_loc + parts[part].precalc[0];
if( !is_sm_tile_outside( part_loc ) ) {
continue;
}
epower += ( part_epower( part ) * accum_weather.sunlight ) / DAYLIGHT_LEVEL;
}
if( epower > 0 ) {
add_msg( m_debug, "%s got %d epower from solar panels", name.c_str(), epower );
charge_battery( epower_to_power( epower ) );
}
}
}
/*-----------------------------------------------------------------------------
* VEHICLE_PART
*-----------------------------------------------------------------------------*/
vehicle_part::vehicle_part()
: mount( 0, 0 ), id( vpart_id::NULL_ID() ) {}
vehicle_part::vehicle_part( const vpart_id& vp, int const dx, int const dy, item&& obj )
: mount( dx, dy ), id( vp ), base( std::move( obj ) )
{
// Mark base item as being installed as a vehicle part
base.item_tags.insert( "VEHICLE" );
if( base.typeId() != vp->item ) {
debugmsg( "incorrect vehicle part item, expected: %s, received: %s",
vp->item.c_str(), base.typeId().c_str() );
}
}
vehicle_part::operator bool() const {
return id != vpart_id::NULL_ID();
}
const item& vehicle_part::get_base() const
{
return base;
}
item vehicle_part::properties_to_item() const
{
item tmp = base;
tmp.item_tags.erase( "VEHICLE" );
// Cables get special handling: their target coordinates need to remain
// stored, and if a cable actually drops, it should be half-connected.
if( tmp.has_flag("CABLE_SPOOL") ) {
tripoint local_pos = g->m.getlocal(target.first);
if(g->m.veh_at( local_pos ) == nullptr) {
tmp.item_tags.insert("NO_DROP"); // That vehicle ain't there no more.
}
tmp.set_var( "source_x", target.first.x );
tmp.set_var( "source_y", target.first.y );
tmp.set_var( "source_z", target.first.z );
tmp.set_var( "state", "pay_out_cable" );
tmp.active = true;
}
return tmp;
}
std::string vehicle_part::name() const {
auto res = info().name();
if( base.engine_displacement() > 0 ) {
res.insert( 0, string_format( _( "%2.1fL " ), base.engine_displacement() / 100.0 ) );
} else if( wheel_diameter() > 0 ) {
res.insert( 0, string_format( _( "%d\" " ), wheel_diameter() ) );
}
if( base.is_faulty() ) {
res += ( _( " (faulty)" ) );
}
return res;
}
int vehicle_part::hp() const
{
double dur = info().durability;
return dur - ( dur * base.damage() / base.max_damage() );
}
float vehicle_part::damage() const
{
return base.damage();
}
/** parts are considered broken at zero health */
bool vehicle_part::is_broken() const
{
return base.damage() >= base.max_damage();
}
itype_id vehicle_part::ammo_current() const
{
if( is_battery() ) {
return "battery";
}
if( is_reactor() || is_turret() ) {
return base.ammo_current();
}
if( is_tank() && !base.contents.empty() ) {
return base.contents.front().typeId();
}
if( is_engine() ) {
return info().fuel_type != "muscle" ? info().fuel_type : "null";
}
return "null";
}
long vehicle_part::ammo_capacity() const
{
if( is_battery() || is_reactor() || is_turret() ) {
return base.ammo_capacity();
}
if( base.is_watertight_container() ) {
return base.get_container_capacity() / std::max( item::find_type( ammo_current() )->volume, units::from_milliliter( 1 ) );
}
return 0;
}
long vehicle_part::ammo_remaining() const
{
if( is_battery() || is_reactor() || is_turret() ) {
return base.ammo_remaining();
}
if( base.is_watertight_container() ) {
return base.contents.empty() ? 0 : base.contents.back().charges;
}
return 0;
}
int vehicle_part::ammo_set( const itype_id &ammo, long qty )
{
if( is_turret() ) {
return base.ammo_set( ammo, qty ).ammo_remaining();
}
if( is_battery() || is_reactor() ) {
base.ammo_set( ammo, qty >= 0 ? qty : ammo_capacity() );
return base.ammo_remaining();
}
const itype *liquid = item::find_type( ammo );
if( is_tank() && liquid->phase == LIQUID ) {
base.contents.clear();
auto stack = units::legacy_volume_factor / std::max( liquid->stack_size, 1 );
long limit = units::from_milliliter( ammo_capacity() ) / stack;
base.emplace_back( ammo, calendar::turn, qty >= 0 ? std::min( qty, limit ) : limit );
return qty;
}
return -1;
}
void vehicle_part::ammo_unset() {
if( is_battery() || is_reactor() || is_turret() ) {
base.ammo_unset();
} else if( is_tank() ) {
base.contents.clear();
}
}
long vehicle_part::ammo_consume( long qty, const tripoint& pos )
{
if( is_battery() || is_reactor() ) {
return base.ammo_consume( qty, pos );
}
int res = std::min( ammo_remaining(), qty );
if( base.is_watertight_container() && !base.contents.empty() ) {
item& liquid = base.contents.back();
liquid.charges -= res;
if( liquid.charges == 0 ) {
base.contents.clear();
}
}
return res;
}
float vehicle_part::consume_energy( const itype_id &ftype, float energy )
{
if( base.contents.empty() || ( !is_battery() && !is_reactor() && !base.is_watertight_container() ) ) {
return 0.0f;
}
item &fuel = base.contents.back();
if( fuel.typeId() != ftype ) {
return 0.0f;
}
assert( fuel.is_fuel() );
float energy_per_unit = fuel.fuel_energy();
long charges_to_use = static_cast<int>( std::ceil( energy / energy_per_unit ) );
if( charges_to_use > fuel.charges ) {
long had_charges = fuel.charges;
base.contents.clear();
return had_charges * energy_per_unit;
}
fuel.charges -= charges_to_use;
return charges_to_use * energy_per_unit;
}
bool vehicle_part::can_reload( const itype_id &obj ) const
{
// first check part is not destroyed and can contain ammo
if( is_broken() || ammo_capacity() <= 0 ) {
return false;
}
if( is_reactor() ) {
return base.is_reloadable_with( obj );
}
if( is_tank() ) {
if( !obj.empty() ) {
// forbid filling tanks with non-liquids
if( item::find_type( obj )->phase != LIQUID ) {
return false;
}
// prevent mixing of different liquids
if( ammo_current() != "null" && ammo_current() != obj ) {
return false;
}
}
// For tanks with set type, prevent filling with different types
if( info().fuel_type != fuel_type_none && info().fuel_type != obj ) {
return false;
}
return ammo_remaining() < ammo_capacity();
}
return false;
}
bool vehicle_part::fill_with( item &liquid, long qty )
{
if( liquid.active || liquid.rotten() ) {
// cannot refill using active liquids (those that rot) due to #18570
return false;
}
if( !is_tank() || !can_reload( liquid.typeId() ) ) {
return false;
}
base.fill_with( liquid, qty );
return true;
}
const std::set<fault_id>& vehicle_part::faults() const
{
return base.faults;
}
std::set<fault_id> vehicle_part::faults_potential() const
{
return base.faults_potential();
}
bool vehicle_part::fault_set( const fault_id &f )
{
if( !faults_potential().count( f ) ) {
return false;
}
base.faults.insert( f );
return true;
}
int vehicle_part::wheel_area() const
{
return base.is_wheel() ? base.type->wheel->diameter * base.type->wheel->width : 0;
}
/** Get wheel diameter (inches) or return 0 if part is not wheel */
int vehicle_part::wheel_diameter() const
{
return base.is_wheel() ? base.type->wheel->diameter : 0;
}
/** Get wheel width (inches) or return 0 if part is not wheel */
int vehicle_part::wheel_width() const
{
return base.is_wheel() ? base.type->wheel->width : 0;
}
npc * vehicle_part::crew() const
{
if( is_broken() || crew_id < 0 ) {
return nullptr;
}
npc *const res = g->critter_by_id<npc>( crew_id );
if( !res ) {
return nullptr;
}
return res->is_friend() ? res : nullptr;
}
bool vehicle_part::set_crew( const npc &who )
{
if( who.is_dead_state() || !who.is_friend() ) {
return false;
}
if( is_broken() || ( !is_seat() && !is_turret() ) ) {
return false;
}
crew_id = who.getID();
return true;
}
void vehicle_part::unset_crew()
{
crew_id = -1;
}
void vehicle_part::reset_target( tripoint pos )
{
target.first = pos;
target.second = pos;
}
bool vehicle_part::is_engine() const
{
return info().has_flag( VPFLAG_ENGINE );
}
bool vehicle_part::is_light() const
{
const auto &vp = info();
return vp.has_flag( VPFLAG_CONE_LIGHT ) ||
vp.has_flag( VPFLAG_CIRCLE_LIGHT ) ||
vp.has_flag( VPFLAG_AISLE_LIGHT ) ||
vp.has_flag( VPFLAG_DOME_LIGHT ) ||
vp.has_flag( VPFLAG_ATOMIC_LIGHT );
}
bool vehicle_part::is_tank() const
{
return base.is_watertight_container();
}
bool vehicle_part::is_battery() const
{
return base.is_magazine() && base.ammo_type() == "battery";
}
bool vehicle_part::is_reactor() const
{
return info().has_flag( "REACTOR" );
}
bool vehicle_part::is_turret() const
{
return base.is_gun();
}
bool vehicle_part::is_seat() const
{
return info().has_flag( "SEAT" );
}
const vpart_info &vehicle_part::info() const
{
if( !info_cache ) {
info_cache = &id.obj();
}
return *info_cache;
}
void vehicle::invalidate_mass()
{
mass_dirty = true;
mass_center_precalc_dirty = true;
mass_center_no_precalc_dirty = true;
// Anything that affects mass will also affect the pivot
pivot_dirty = true;
}
void vehicle::refresh_mass() const
{
calc_mass_center( true );
}
void vehicle::calc_mass_center( bool use_precalc ) const
{
units::quantity<float, units::mass::unit_type> xf = 0;
units::quantity<float, units::mass::unit_type> yf = 0;
units::mass m_total = 0;
for( size_t i = 0; i < parts.size(); i++ )
{
if( parts[i].removed ) {
continue;
}
units::mass m_part = 0;
const auto &pi = part_info( i );
m_part += parts[i].base.weight();
for( const auto &j : get_items( i ) ) {
//m_part += j.type->weight;
// Change back to the above if it runs too slowly
m_part += j.weight();
}
if( pi.has_flag( VPFLAG_BOARDABLE ) && parts[i].has_flag( vehicle_part::passenger_flag ) ) {
const player *p = get_passenger( i );
// Sometimes flag is wrongly set, don't crash!
m_part += p != nullptr ? p->get_weight() : units::mass( 0 );
}
if( use_precalc ) {
xf += parts[i].precalc[0].x * m_part;
yf += parts[i].precalc[0].y * m_part;
} else {
xf += parts[i].mount.x * m_part;
yf += parts[i].mount.y * m_part;
}
m_total += m_part;
}
mass_cache = m_total;
mass_dirty = false;
const float x = xf / mass_cache;
const float y = yf / mass_cache;
if( use_precalc ) {
mass_center_precalc.x = round( x );
mass_center_precalc.y = round( y );
mass_center_precalc_dirty = false;
} else {
mass_center_no_precalc.x = round( x );
mass_center_no_precalc.y = round( y );
mass_center_no_precalc_dirty = false;
}
}
void vehicle::use_washing_machine( int p ) {
bool detergent_is_enough = g->u.crafting_inventory().has_charges( "detergent", 5 );
auto items = get_items( p );
static const std::string filthy( "FILTHY" );
bool filthy_items = std::all_of( items.begin(), items.end(), []( const item & i ) {
return i.has_flag( filthy );
} );
if( parts[p].enabled ) {
parts[p].enabled = false;
add_msg( m_bad,
_( "You turn the washing machine off before it's finished the program, and open its lid." ) );
} else if( fuel_left( "water" ) < 24 ) {
add_msg( m_bad, _( "You need 24 charges of water in tanks of the %s to fill the washing machine." ),
name.c_str() );
} else if( !detergent_is_enough ) {
add_msg( m_bad, _( "You need 5 charges of detergent for the washing machine." ) );
} else if( !filthy_items ) {
add_msg( m_bad,
_( "You need to remove all non-filthy items from the washing machine to start the washing program." ) );
} else {
parts[p].enabled = true;
for( auto &n : items ) {
n.set_age( 0 );
}
drain( "water", 24 );
std::vector<item_comp> detergent;
detergent.push_back( item_comp( "detergent", 5 ) );
g->u.consume_items( detergent );
add_msg( m_good,
_( "You pour some detergent into the washing machine, close its lid, and turn it on. The washing machine is being filled with water from vehicle tanks." ) );
}
}
You can’t perform that action at this time.