diff --git a/data/json/vehicleparts/vehicle_parts.json b/data/json/vehicleparts/vehicle_parts.json index 4f7aadc9cdeff..bd4ac173d891e 100644 --- a/data/json/vehicleparts/vehicle_parts.json +++ b/data/json/vehicleparts/vehicle_parts.json @@ -2455,7 +2455,7 @@ "broken_symbol": "*", "broken_color": "dark_gray", "damage_modifier": 10, - "epower": "1 W", + "epower": "1 kW", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "durability": 120, "description": "Thick copper cable with leads on either end. Attach one end to one vehicle and the other to another, and you can transfer electrical power between the two.", @@ -2474,7 +2474,7 @@ "broken_symbol": "*", "broken_color": "dark_gray", "damage_modifier": 10, - "epower": "1 W", + "epower": "1 kW", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "durability": 120, "description": "A long orange extension cord for connecting appliances. Currently plugged in.", @@ -2493,7 +2493,7 @@ "broken_symbol": "*", "broken_color": "dark_gray", "damage_modifier": 10, - "epower": "1 W", + "epower": "1 kW", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "durability": 120, "description": "An extra long 30m orange extension cord for connecting outdoor appliances. Currently plugged in.", @@ -2514,7 +2514,7 @@ "damage_modifier": 10, "durability": 120, "description": "Very thick copper cable with leads on either end. Attach one end to one vehicle and the other to another, and you can transfer electrical power between the two.", - "epower": "5 W", + "epower": "5 kW", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "item": "jumper_cable_heavy", "requirements": { "removal": { "time": "5 s" } }, diff --git a/data/mods/TEST_DATA/appliance.json b/data/mods/TEST_DATA/appliance.json index cb2e249014a35..7641f1776632e 100644 --- a/data/mods/TEST_DATA/appliance.json +++ b/data/mods/TEST_DATA/appliance.json @@ -97,7 +97,7 @@ "broken_symbol": "*", "broken_color": "dark_gray", "damage_modifier": 10, - "epower": "1 W", + "epower": "1 kW", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "durability": 120, "description": "A long orange extension cord for connecting appliances. Currently plugged in.", @@ -105,5 +105,44 @@ "requirements": { "removal": { "time": "5 s" } }, "flags": [ "NOINSTALL", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ] + }, + { + "type": "TOOL", + "id": "test_power_cord_25_loss", + "name": { "str": "test_power_cord_25_loss item" }, + "description": "test 25% loss extension cord.", + "to_hit": 1, + "color": "dark_gray", + "symbol": "&", + "material": [ "steel", "plastic" ], + "volume": "500 ml", + "weight": "75 g", + "bashing": 2, + "category": "tools", + "price": 1, + "price_postapoc": 100, + "max_charges": 3, + "initial_charges": 3, + "use_action": [ "CABLE_ATTACH" ], + "flags": [ "CABLE_SPOOL", "POWER_CORD", "SINGLE_USE" ] + }, + { + "type": "vehicle_part", + "id": "test_power_cord_25_loss", + "name": { "str": "test 25% loss extension cord part" }, + "symbol": "{", + "categories": [ "other" ], + "color": "yellow", + "broken_symbol": "*", + "broken_color": "dark_gray", + "damage_modifier": 10, + "epower": "25 kW", + "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", + "durability": 120, + "description": "A long orange extension cord for connecting appliances. Currently plugged in.", + "item": "test_power_cord_25_loss", + "requirements": { "removal": { "time": "5 s" } }, + "flags": [ "NOINSTALL", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ] } ] diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index eb413830d7ab7..7245523883220 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -1339,11 +1339,11 @@ void activity_handlers::fill_liquid_do_turn( player_activity *act, Character *yo veh = &vp->vehicle(); part = act_ref.values[4]; if( source_veh && - source_veh->fuel_left( liquid.typeId(), false, ( veh ? std::function { [&]( const vehicle_part & pa ) + source_veh->fuel_left( liquid.typeId(), ( veh ? std::function { [&]( const vehicle_part & pa ) { return &veh->part( part ) != &pa; } - } : return_true ) ) <= 0 ) { + } : return_true ) ) <= 0 ) { act_ref.set_to_null(); return; } diff --git a/src/activity_item_handling.cpp b/src/activity_item_handling.cpp index 3ddb7080a7861..16252798e4cbf 100644 --- a/src/activity_item_handling.cpp +++ b/src/activity_item_handling.cpp @@ -972,7 +972,7 @@ static bool are_requirements_nearby( const cata::optional &vp = here.veh_at( elem ).part_with_tool( itype_welder ); if( vp ) { - const int veh_battery = vp->vehicle().fuel_left( itype_battery, true ); + const int veh_battery = vp->vehicle().fuel_left( itype_battery ); item welder( itype_welder, calendar::turn_zero ); welder.charges = veh_battery; diff --git a/src/game.cpp b/src/game.cpp index 2c4f9bc28348b..d883813835492 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2482,7 +2482,7 @@ vehicle *game::remoteveh() tripoint vp; remote_veh_string >> vp.x >> vp.y >> vp.z; vehicle *veh = veh_pointer_or_null( m.veh_at( vp ) ); - if( veh && veh->fuel_left( itype_battery, true ) > 0 ) { + if( veh && veh->fuel_left( itype_battery ) > 0 ) { remoteveh_cache = veh; } else { remoteveh_cache = nullptr; diff --git a/src/iuse.cpp b/src/iuse.cpp index c065f787ea1c1..6a8b8cd546259 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -7911,7 +7911,7 @@ static vehicle *pickveh( const tripoint ¢er, bool advanced ) for( wrapped_vehicle &veh : get_map().get_vehicles() ) { vehicle *&v = veh.v; if( rl_dist( center, v->global_pos3() ) < 40 && - v->fuel_left( itype_battery, true ) > 0 && + v->fuel_left( itype_battery ) > 0 && ( !empty( v->get_avail_parts( advctrl ) ) || ( !advanced && !empty( v->get_avail_parts( ctrl ) ) ) ) ) { vehs.push_back( v ); @@ -7952,7 +7952,7 @@ cata::optional iuse::remoteveh( Character *p, item *it, bool t, const tripo } else if( remote == nullptr ) { p->add_msg_if_player( _( "Lost contact with the vehicle." ) ); stop = true; - } else if( remote->fuel_left( itype_battery, true ) == 0 ) { + } else if( remote->fuel_left( itype_battery ) == 0 ) { p->add_msg_if_player( m_bad, _( "The vehicle's battery died." ) ); stop = true; } @@ -10023,7 +10023,7 @@ cata::optional iuse::voltmeter( Character *p, item *, bool, const tripoint p->add_msg_if_player( _( "There's nothing to measure there." ) ); return cata::nullopt; } - if( vp->vehicle().fuel_left( itype_battery, true ) ) { + if( vp->vehicle().fuel_left( itype_battery ) ) { p->add_msg_if_player( _( "The %1$s has voltage." ), vp->vehicle().name ); } else { p->add_msg_if_player( _( "The %1$s has no voltage." ), vp->vehicle().name ); diff --git a/src/map.cpp b/src/map.cpp index ab01daf32e61c..ba15782e360b9 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -585,17 +585,21 @@ void map::vehmove() } } dirty_vehicle_list.clear(); - std::set origins; + std::map vehs; // value true means in on map for( int zlev = minz; zlev <= maxz; ++zlev ) { const level_cache *cache = get_cache_lazy( zlev ); - if( cache ) { - for( vehicle *veh : cache->vehicle_list ) { - origins.emplace( veh ); + if( !cache ) { + continue; + } + for( vehicle *veh : cache->vehicle_list ) { + vehs[veh] = true; // force on map vehicles to true + for( const std::pair &pair : veh->search_connected_vehicles() ) { + vehs.emplace( pair.first, false ); // add with 'false' if does not exist (off map) } } } - for( const std::pair &veh_pair : vehicle::enumerate_vehicles( origins ) ) { - veh_pair.first->idle( veh_pair.second ); + for( const std::pair &veh_pair : vehs ) { + veh_pair.first->idle( /* on_map = */ veh_pair.second ); } // refresh vehicle zones for moved vehicles @@ -5212,7 +5216,7 @@ static void process_vehicle_items( vehicle &cur_veh, int part ) ( n.type->battery && n.type->battery->max_capacity > n.energy_remaining() ) ) { int power = recharge_part.info().bonus; while( power >= 1000 || x_in_y( power, 1000 ) ) { - const int missing = cur_veh.discharge_battery( 1, true ); + const int missing = cur_veh.discharge_battery( 1 ); // Around 85% efficient; a few of the discharges don't actually recharge if( missing == 0 && !one_in( 7 ) ) { if( n.is_vehicle_battery() ) { diff --git a/src/veh_appliance.cpp b/src/veh_appliance.cpp index 963d85347e19d..8320120b0e312 100644 --- a/src/veh_appliance.cpp +++ b/src/veh_appliance.cpp @@ -126,11 +126,7 @@ veh_app_interact::veh_app_interact( vehicle &veh, const point &p ) // @returns true if a battery part exists on any vehicle connected to veh static bool has_battery_in_grid( vehicle *veh ) { - const std::map veh_map = vehicle::enumerate_vehicles( { veh } ); - return std::any_of( veh_map.begin(), veh_map.end(), - []( const std::pair &p ) { - return !p.first->batteries.empty(); - } ); + return !veh->search_connected_batteries().empty(); } void veh_app_interact::init_ui_windows() @@ -225,8 +221,11 @@ void veh_app_interact::draw_info() } // Battery power output - units::power charge_rate = veh->net_battery_charge_rate( true, true ); - print_charge( _( "Grid battery power flow: " ), charge_rate, row ); + units::power grid_flow = 0_W; + for( const std::pair &pair : veh->search_connected_vehicles() ) { + grid_flow += pair.first->net_battery_charge_rate( /* include_reactors = */ true ); + } + print_charge( _( "Grid battery power flow: " ), grid_flow, row ); row++; // Reactor power output diff --git a/src/veh_interact.cpp b/src/veh_interact.cpp index f54a8d4730f9f..9b5d03dee7ba6 100644 --- a/src/veh_interact.cpp +++ b/src/veh_interact.cpp @@ -1423,7 +1423,7 @@ void veh_interact::calc_overview() overview_opts.clear(); overview_headers.clear(); - units::power epower = veh->net_battery_charge_rate(); + units::power epower = veh->net_battery_charge_rate( /* include_reactors = */ true ); overview_headers["1_ENGINE"] = [this]( const catacurses::window & w, int y ) { trim_and_print( w, point( 1, y ), getmaxx( w ) - 2, c_light_gray, string_format( _( "Engines: %sSafe %4d kW %sMax %4d kW" ), diff --git a/src/vehicle.cpp b/src/vehicle.cpp index c899115c81c46..8855ed7de93b2 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -3291,10 +3291,24 @@ point vehicle::pivot_displacement() const return dp.xy(); } -int64_t vehicle::fuel_left( const itype_id &ftype, bool recurse, +int64_t vehicle::fuel_left( const itype_id &ftype, const std::function &filter ) const { int64_t fl = 0; + if( ftype == fuel_type_battery ) { + for( const std::pair &pair : search_connected_vehicles() ) { + const vehicle &veh = *pair.first; + const float loss = pair.second; + for( const int part_idx : veh.batteries ) { + const vehicle_part &vp = veh.parts[part_idx]; + if( vp.ammo_current() != fuel_type_battery || !filter( vp ) ) { + continue; + } + fl += vp.ammo_remaining() * ( 1.0f - loss ); + } + } + return fl; + } for( const int i : fuel_containers ) { const vehicle_part &part = parts[i]; @@ -3307,17 +3321,6 @@ int64_t vehicle::fuel_left( const itype_id &ftype, bool recurse, fl += part.ammo_remaining(); } - 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 ) { Character &player_character = get_player_character(); @@ -3348,20 +3351,31 @@ int64_t vehicle::fuel_left( const itype_id &ftype, bool recurse, return fl; } -int vehicle::engine_fuel_left( const vehicle_part &vp, bool recurse ) const +int vehicle::engine_fuel_left( const vehicle_part &vp ) const { - return fuel_left( vp.fuel_current(), recurse ); + return fuel_left( vp.fuel_current() ); } int vehicle::fuel_capacity( const itype_id &ftype ) const { - vehicle_part_range vpr = get_all_parts(); - return std::accumulate( vpr.begin(), vpr.end(), 0, [&ftype]( const int &lhs, - const vpart_reference & rhs ) { - cata::value_ptr a_val = item::find_type( ftype )->ammo; - return lhs + ( rhs.part().ammo_current() == ftype ? - rhs.part().ammo_capacity( !!a_val ? a_val->type : ammotype::NULL_ID() ) : - 0 ); + if( ftype == fuel_type_battery ) { // batteries get special treatment due to power cables + int64_t capacity = 0; + for( const std::pair &pair : search_connected_vehicles() ) { + const vehicle &veh = *pair.first; + for( const int part_idx : veh.batteries ) { + const vehicle_part &vp = veh.parts[part_idx]; + capacity += vp.ammo_capacity( fuel_type_battery->ammo->type ); + } + } + return capacity; + } + const vehicle_part_range vpr = get_all_parts(); + return std::accumulate( vpr.begin(), vpr.end(), int64_t { 0 }, + [&ftype]( const int64_t &lhs, const vpart_reference & rhs ) { + if( rhs.part().ammo_current() == ftype && ftype->ammo ) { + return lhs + rhs.part().ammo_capacity( ftype->ammo->type ); + } + return lhs; } ); } @@ -3372,7 +3386,7 @@ int vehicle::drain( const itype_id &ftype, int amount, // 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 ); + int remnant = discharge_battery( amount ); // discharge_battery returns amount of charges that were not // found anywhere in the power network, whereas this function @@ -4694,26 +4708,18 @@ std::pair vehicle::battery_power_level() const std::pair vehicle::connected_battery_power_level() const { + int total_epower_remaining = 0; int total_epower_capacity = 0; - int remaining_epower = 0; - - std::tie( remaining_epower, total_epower_capacity ) = battery_power_level(); - - auto get_power_visitor = [&]( vehicle const * veh, int amount, int ) { - int other_total_epower_capacity = 0; - int other_remaining_epower = 0; - - std::tie( other_remaining_epower, other_total_epower_capacity ) = veh->battery_power_level(); - - total_epower_capacity += other_total_epower_capacity; - remaining_epower += other_remaining_epower; - - return amount; - }; - traverse_vehicle_graph( this, 1, get_power_visitor ); + for( const std::pair &pair : search_connected_vehicles() ) { + int epower_remaining; + int epower_capacity; + std::tie( epower_remaining, epower_capacity ) = pair.first->battery_power_level(); + total_epower_remaining += epower_remaining; + total_epower_capacity += epower_capacity; + } - return std::make_pair( remaining_epower, total_epower_capacity ); + return std::make_pair( total_epower_remaining, total_epower_capacity ); } bool vehicle::start_engine( vehicle_part &vp, bool turn_on ) @@ -4831,26 +4837,11 @@ units::power vehicle::total_water_wheel_epower() const return epower; } -units::power vehicle::net_battery_charge_rate( bool include_reactors, - bool connected_vehicles ) const +units::power vehicle::net_battery_charge_rate( bool include_reactors ) const { - if( connected_vehicles ) { - units::power battery_w = net_battery_charge_rate( include_reactors, false ); - - auto net_battery_visitor = [&]( vehicle const * veh, int, int ) { - battery_w += veh->net_battery_charge_rate( include_reactors, false ); - return 1; - }; - - traverse_vehicle_graph( this, 1, net_battery_visitor ); - - return battery_w; - - } else { - return total_engine_epower() + total_alternator_epower() + total_accessory_epower() + - total_solar_epower() + total_wind_epower() + total_water_wheel_epower() + - ( include_reactors ? active_reactor_epower( false ) : 0_W ); - } + return total_engine_epower() + total_alternator_epower() + total_accessory_epower() + + total_solar_epower() + total_wind_epower() + total_water_wheel_epower() + + ( include_reactors ? active_reactor_epower( false ) : 0_W ); } units::power vehicle::active_reactor_epower( bool connected_vehicles ) const @@ -4877,7 +4868,7 @@ units::power vehicle::active_reactor_epower( bool connected_vehicles ) const int batteries_need = std::max( 0, total_battery_capacity - total_battery_left ); // How much battery are others adding/draining? - units::power others_w = net_battery_charge_rate( false ); + units::power others_w = net_battery_charge_rate( /* include_reactors = */ false ); int others_bat = power_to_energy_bat( others_w, 1_turns ); // How much battery will the reactors add? @@ -5083,159 +5074,208 @@ vehicle *vehicle::find_vehicle( const tripoint &where ) return nullptr; } -std::map vehicle::enumerate_vehicles( const std::set &origins ) +template // Templated to support const and non-const vehicle* +std::map vehicle::search_connected_vehicles( Vehicle *start ) { - std::map result; // the bool represents if vehicle ptr is in origins set - const auto enumerate_visitor = [&result]( vehicle * veh, int amount, int /* loss_amount */ ) { - result.emplace( veh, false ); // only add if element is not present already. - return amount; - }; - for( vehicle *veh : origins ) { - result[veh] = true; // add or overwrite the value - traverse_vehicle_graph( veh, 1, enumerate_visitor ); - } - return result; -} - -template -int vehicle::traverse_vehicle_graph( Vehicle *start_veh, int amount, Func action ) -{ - if( start_veh->loose_parts.empty() ) { - return amount; - } - // Breadth-first search! Initialize the queue with a pointer to ourselves and go! - std::vector< std::pair > connected_vehs = std::vector< std::pair > { std::make_pair( start_veh, 0 ) }; - std::vector visited_vehs; - std::vector visited_targets; - - while( amount > 0 && !connected_vehs.empty() ) { - auto current_node = connected_vehs.back(); - Vehicle *current_veh = current_node.first; - int current_loss = current_node.second; + std::map distances; // distance represents sum of cable losses + std::vector queue; - visited_vehs.push_back( current_veh ); - connected_vehs.pop_back(); + distances[start] = 0; + queue.emplace_back( start ); + constexpr float infinity_distance = 10000.0f; // should be enough to represent "infinity" - add_msg_debug( debugmode::DF_VEHICLE, "Traversing graph with %d power", amount ); + // Run dijkstra to get shortest distance tree of paths + // Tree will span from self(root) to other connected vehicles + // where distance metric is power transfer loss ( resistance to heat inefficiency ) + while( !queue.empty() ) { + Vehicle *const veh = queue.back(); + queue.pop_back(); - for( int 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 + for( const int part_idx : veh->loose_parts ) { // graph "edges" are POWER_TRANSFER parts + const vehicle_part &vp = veh->part( part_idx ); + const vpart_info &vpi = vp.info(); + if( !vpi.has_flag( "POWER_TRANSFER" ) ) { + continue; } - if( std::find( visited_targets.begin(), visited_targets.end(), - current_veh->parts[p].target.second ) != visited_targets.end() ) { - // If we've already looked at the target location, don't bother the expensive vehicle lookup. + Vehicle *const v_next = vehicle::find_vehicle( vp.target.second ); + if( v_next == nullptr ) { // vehicle's rolled away or off-map continue; } + // try insert infinity for initial unvisited node distance + distances.insert( { v_next, infinity_distance } ); - visited_targets.push_back( current_veh->parts[p].target.second ); - - vehicle *target_veh = vehicle::find_vehicle( current_veh->parts[p].target.second ); - if( target_veh == nullptr || - std::find( visited_vehs.begin(), visited_vehs.end(), target_veh ) != visited_vehs.end() ) { - // Either no destination here (that vehicle's rolled away or off-map) or - // we've already looked at that vehicle. - continue; + const float loss = units::to_kilowatt( vpi.epower ) / 100.0f; + const float new_dist = loss + distances[veh]; + if( distances[v_next] > new_dist ) { + distances[v_next] = new_dist; + queue.emplace_back( v_next ); } + } + } + return distances; +} - // Add this connected vehicle to the queue of vehicles to search next, - // but only if we haven't seen this one before (checked above) - int target_loss = current_loss + units::to_kilowatt( current_veh->part_info( p ).epower ); - connected_vehs.push_back( std::make_pair( target_veh, target_loss ) ); - // current_veh could be invalid after this point +std::map vehicle::search_connected_vehicles() +{ + return search_connected_vehicles( this ); +} - float loss_amount = ( static_cast( amount ) * static_cast( target_loss ) ) / 100.0f; - add_msg_debug( debugmode::DF_VEHICLE, - "Visiting remote %p with %d power (loss %f, which is %d percent)", - static_cast( target_veh ), amount, loss_amount, target_loss ); +std::map vehicle::search_connected_vehicles() const +{ + return search_connected_vehicles( this ); +} - amount = action( target_veh, amount, static_cast( loss_amount ) ); - add_msg_debug( debugmode::DF_VEHICLE, "After remote %p, %d power", - static_cast( target_veh ), amount ); +std::map vehicle::search_connected_batteries() +{ + std::map result; - if( amount < 1 ) { - break; // No more charge to donate away. + for( const std::pair &pair : search_connected_vehicles() ) { + vehicle *veh = pair.first; + const float efficiency = pair.second; + for( const int part_idx : veh->batteries ) { + const vpart_reference vpr( *veh, part_idx ); + if( vpr.part().is_fake ) { + continue; } + result.emplace( vpr, efficiency ); } } - return amount; + + return result; +} + +// helper method to calculate power loss weighted by capacity +static double weighted_power_loss( const std::map &batteries ) +{ + double res = 0.0; // sum of power losses + int64_t total_capacity = 0; // sum of capacity of all batteries + for( const std::pair &pair : batteries ) { + vehicle_part &vp = pair.first.part(); + const int capacity = vp.ammo_capacity( ammo_battery ); + total_capacity += capacity; + res += pair.second * capacity; + } + return res / total_capacity; +} + +// helper method to take a map of batteries, amount of charge, total capacity of batteries +// and distribute given charge_kj over the batteries as evenly as possible +static void distribute_charge_evenly( const std::map &batteries, + int64_t charge_kj, int64_t total_capacity_kj ) +{ + int64_t distributed = 0; + for( const std::pair &pair : batteries ) { + vehicle_part &vp = pair.first.part(); + const int bat_capacity = vp.ammo_capacity( ammo_battery ); + const float fraction = static_cast( bat_capacity ) / total_capacity_kj; + const int portion = charge_kj * fraction; + vp.ammo_set( fuel_type_battery, portion ); + distributed += portion; + } + if( distributed < charge_kj ) { // dump indivisible remainder sequentially + for( const std::pair &pair : batteries ) { + vehicle_part &vp = pair.first.part(); + const int64_t bat_charge = vp.ammo_remaining(); + const int64_t bat_capacity = vp.ammo_capacity( ammo_battery ); + const int chargeable = std::min( charge_kj - distributed, bat_capacity - bat_charge ); + vp.ammo_set( fuel_type_battery, bat_charge + chargeable ); + distributed += chargeable; + if( distributed >= charge_kj ) { + break; + } + } + } + if( distributed < charge_kj ) { // safeguard, this shouldn't happen + debugmsg( "no capacity to distribute distribute %d kJ", charge_kj - distributed ); + } } -int vehicle::charge_battery( int amount, bool include_other_vehicles ) +int64_t vehicle::battery_left( bool apply_loss ) const { - // Key parts by percentage charge level. - std::multimap chargeable_parts; - for( vehicle_part &p : parts ) { - if( p.is_available() && p.is_battery() && - p.ammo_capacity( ammo_battery ) > p.ammo_remaining() ) { - chargeable_parts.insert( { ( p.ammo_remaining() * 100 ) / p.ammo_capacity( ammo_battery ), &p } ); - } - } - while( amount > 0 && !chargeable_parts.empty() ) { - // Grab first part, charge until it reaches the next %, then re-insert with new % key. - auto iter = chargeable_parts.begin(); - int charge_level = iter->first; - vehicle_part *p = iter->second; - chargeable_parts.erase( iter ); - // Calculate number of charges to reach the next %, but insure it's at least - // one more than current charge. - int next_charge_level = ( ( charge_level + 1 ) * p->ammo_capacity( ammo_battery ) ) / 100; - next_charge_level = std::max( next_charge_level, p->ammo_remaining() + 1 ); - int qty = std::min( amount, next_charge_level - p->ammo_remaining() ); - p->ammo_set( fuel_type_battery, p->ammo_remaining() + qty ); - amount -= qty; - if( p->ammo_capacity( ammo_battery ) > p->ammo_remaining() ) { - chargeable_parts.insert( { ( p->ammo_remaining() * 100 ) / p->ammo_capacity( ammo_battery ), p } ); - } - } - - auto charge_visitor = []( vehicle * veh, int amount, int lost ) { - add_msg_debug( debugmode::DF_VEHICLE, "CH: %d", amount - lost ); - return veh->charge_battery( amount - lost, false ); - }; + int64_t ret = 0; + for( const std::pair &pair : search_connected_vehicles() ) { + const vehicle &veh = *pair.first; + const float efficiency = 1.0f - ( apply_loss ? pair.second : 0.0f ); + for( const int part_idx : veh.batteries ) { + const vehicle_part &vp = veh.parts[part_idx]; + ret += vp.ammo_remaining() * efficiency; + } + } + return ret; +} - if( amount > 0 && include_other_vehicles ) { // still a bit of charge we could send out... - amount = traverse_vehicle_graph( this, amount, charge_visitor ); +int vehicle::charge_battery( int amount, bool apply_loss ) +{ + const std::map batteries = search_connected_batteries(); + if( amount == 0 || batteries.empty() ) { + return amount; // nothing to do + } + const double loss = apply_loss ? weighted_power_loss( batteries ) : 0.0; + int64_t total_charge = 0; // sum of current charge of all batteries + int64_t total_capacity = 0; // sum of capacity of all batteries + for( const std::pair &pair : batteries ) { + vehicle_part &vp = pair.first.part(); + total_charge += vp.ammo_remaining(); + total_capacity += vp.ammo_capacity( ammo_battery ); } + const int64_t chargeable = total_capacity - total_charge; + int64_t lost_amount = roll_remainder( amount * loss ); + int64_t lossy_amount = amount; + int64_t charged = amount - lost_amount; + if( charged > chargeable ) { // no battery capacity to absorb all charge, recalculate loss + charged = chargeable; // cap at the maximum possible charge + lost_amount = roll_remainder( charged * loss ); + lossy_amount = charged + lost_amount; + } + total_charge += charged; + const int tried_charging = amount; + amount -= charged + lost_amount; + + distribute_charge_evenly( batteries, total_charge, total_capacity ); - return amount; + add_msg_debug( debugmode::DF_VEHICLE, + "batteries: %d, loss: %.3f, tried charging: %d kJ, actual charged: %d kJ, usable: %d kJ, lost: %d kJ, excess: %d kJ", + batteries.size(), loss, tried_charging, lossy_amount, charged, lost_amount, amount ); + + return amount; // non zero if batteries couldn't absorb the entire amount } -int vehicle::discharge_battery( int amount, bool recurse ) +int vehicle::discharge_battery( int amount, bool apply_loss ) { - // Key parts by percentage charge level. - std::multimap dischargeable_parts; - for( vehicle_part &p : parts ) { - if( p.is_available() && p.is_battery() && p.ammo_remaining() > 0 && !p.is_fake ) { - dischargeable_parts.insert( { ( p.ammo_remaining() * 100 ) / p.ammo_capacity( ammo_battery ), &p } ); - } + const std::map batteries = search_connected_batteries(); + if( amount == 0 || batteries.empty() ) { + return amount; // nothing to do } - while( amount > 0 && !dischargeable_parts.empty() ) { - // Grab first part, discharge until it reaches the next %, then re-insert with new % key. - auto iter = std::prev( dischargeable_parts.end() ); - const int prev_charge_level = iter->first - 1; - vehicle_part *p = iter->second; - dischargeable_parts.erase( iter ); - // Calculate number of charges to reach the previous %. - int prev_charge_amount = std::max( 0, prev_charge_level * p->ammo_capacity( ammo_battery ) ) / 100; - int amount_to_discharge = std::min( p->ammo_remaining() - prev_charge_amount, amount ); - p->ammo_consume( amount_to_discharge, global_part_pos3( *p ) ); - amount -= amount_to_discharge; - if( p->ammo_remaining() > 0 ) { - dischargeable_parts.insert( { ( p->ammo_remaining() * 100 ) / p->ammo_capacity( ammo_battery ), p } ); - } + const double loss = apply_loss ? weighted_power_loss( batteries ) : 0.0; + int64_t total_charge = 0; // sum of current charge of all batteries + int64_t total_capacity = 0; // sum of capacity of all batteries + for( const std::pair &pair : batteries ) { + vehicle_part &vp = pair.first.part(); + total_charge += vp.ammo_remaining(); + total_capacity += vp.ammo_capacity( ammo_battery ); } - auto discharge_visitor = []( vehicle * veh, int amount, int lost ) { - add_msg_debug( debugmode::DF_VEHICLE, "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 ); + int64_t discharged = amount; + int64_t lost_amount = roll_remainder( amount * loss ); + int64_t lossy_amount = amount + lost_amount; + if( lossy_amount > total_charge ) { // not enough available, recalculate loss + lossy_amount = total_charge; // cap at the maximum possible discharge + lost_amount = roll_remainder( lossy_amount * loss ); + discharged = lossy_amount - lost_amount; } + total_charge -= lossy_amount; + const int tried_discharging = amount; + amount -= discharged; + + distribute_charge_evenly( batteries, total_charge, total_capacity ); + + add_msg_debug( debugmode::DF_VEHICLE, + "batteries: %d, loss: %.3f, tried discharging: %d kJ, actual discharged: %d kJ, usable: %d kJ, lost: %d kJ, missing: %d kJ", + batteries.size(), loss, tried_discharging, lossy_amount, discharged, lost_amount, + amount ); - return amount; // non-zero if we weren't able to fulfill demand. + return amount; // non zero if batteries couldn't provide the entire amount } void vehicle::do_engine_damage( vehicle_part &vp, int strain ) @@ -5683,13 +5723,10 @@ void vehicle::gain_moves() thrust( 0 ); } - // Force off-map vehicles to load by visiting them every time we gain moves. + // Force off-map connected vehicles to load by visiting them every time we gain moves. // This is expensive so we allow a slightly stale result if( calendar::once_every( 5_turns ) ) { - auto nil_visitor = []( vehicle *, int amount, int ) { - return amount; - }; - traverse_vehicle_graph( this, 1, nil_visitor ); + search_connected_vehicles(); } if( check_environmental_effects ) { @@ -7321,6 +7358,13 @@ Character *vpart_reference::get_passenger() const return vehicle().get_passenger( part_index() ); } +bool vpart_position::operator<( const vpart_position &other ) const +{ + const ::vehicle *const v1 = &vehicle(); + const ::vehicle *const v2 = &other.vehicle(); + return std::make_pair( v1, part_index_ ) < std::make_pair( v2, other.part_index_ ); +} + point vpart_position::mount() const { return vehicle().part( part_index() ).mount; @@ -7468,7 +7512,7 @@ void vehicle::update_time( const time_point &update_to ) const cata::optional vp_purifier = vpart_position( *this, idx ) .part_with_tool( itype_pseudo_water_purifier ); - if( vp_purifier && ( fuel_left( itype_battery, true ) > cost_to_purify ) ) { + if( vp_purifier && ( fuel_left( itype_battery ) > cost_to_purify ) ) { tank->ammo_set( itype_water_clean, c_qty ); discharge_battery( cost_to_purify ); } else { diff --git a/src/vehicle.h b/src/vehicle.h index 27b900ddde443..f8e32ab7b6e16 100644 --- a/src/vehicle.h +++ b/src/vehicle.h @@ -813,21 +813,25 @@ class vehicle */ static vehicle *find_vehicle( const tripoint &where ); - /** - * Traverses the graph of connected vehicles, starting from start_veh, and continuing - * along all vehicles connected by some kind of POWER_TRANSFER part. - * @param start_veh The vehicle to start traversing from. NB: the start_vehicle is - * assumed to have been already visited! - * @param amount An amount of power to traverse with. This is passed back to the visitor, - * and reset to the visitor's return value at each step. - * @param action A function(vehicle* veh, int amount, int loss) returning int. The function - * may do whatever it desires, and may be a lambda (including a capturing lambda). - * NB: returning 0 from a visitor will stop traversal immediately! - * @return The last visitor's return value. - */ - template - static int traverse_vehicle_graph( Vehicle *start_veh, int amount, Func action ); + /// Returns a map of connected vehicle pointers to power loss factor: + /// Keys are vehicles connected by POWER_TRANSFER parts, includes self + /// Values are line loss, 0.01 corresponds to 1% charge loss to wire resistance + /// May load the connected vehicles' submaps + /// Templated to support const and non-const vehicle* + template + static std::map search_connected_vehicles( Vehicle *start ); public: + //! @copydoc vehicle::search_connected_vehicles( Vehicle *start ) + std::map search_connected_vehicles(); + //! @copydoc vehicle::search_connected_vehicles( Vehicle *start ) + std::map search_connected_vehicles() const; + + /// Returns a map of connected battery references to power loss factor + /// Keys are batteries in vehicles (includes self) connected by POWER_TRANSFER parts + /// Values are line loss, 0.01 corresponds to 1% charge loss to wire resistance + /// May load the connected vehicles' submaps + std::map search_connected_batteries(); + vehicle( map &placed_on, const vproto_id &type_id, int init_veh_fuel = -1, int init_veh_status = -1, bool may_spawn_locked = false ); vehicle(); @@ -1265,11 +1269,11 @@ class vehicle std::list fuel_items_left(); // Checks how much certain fuel left in tanks. - int64_t fuel_left( const itype_id &ftype, bool recurse = false, + int64_t fuel_left( const itype_id &ftype, const std::function &filter = return_true ) const; // Checks how much of an engine's current fuel is left in the tanks. - int engine_fuel_left( const vehicle_part &vp, bool recurse = false ) const; + int engine_fuel_left( const vehicle_part &vp ) const; // Returns total vehicle fuel capacity for the given fuel type int fuel_capacity( const itype_id &ftype ) const; @@ -1321,8 +1325,7 @@ class vehicle // Total power drain across all vehicle accessories. units::power total_accessory_epower() const; // Net power draw or drain on batteries. - units::power net_battery_charge_rate( bool include_reactors = true, - bool connected_vehicles = false ) const; + units::power net_battery_charge_rate( bool include_reactors ) const; // Maximum available power available from all reactors. Power from // reactors is only drawn when batteries are empty. units::power max_reactor_epower() const; @@ -1339,20 +1342,26 @@ class vehicle std::pair connected_battery_power_level() const; /** - * Try to charge our (and, optionally, connected vehicles') batteries by the given amount. - * @param amount to discharge in kJ - * @param include_other_vehicles if true charge also to cable connected vehicles. - * @return amount of charge left over. + * @param apply_loss if true apply wire loss when charge crosses vehicle power cables + * @return battery charge in kJ from all connected batteries + */ + int64_t battery_left( bool apply_loss = true ) const; + + /** + * Charges batteries in connected vehicles/appliances + * @param amount to charge in kJ + * @param apply_loss if true apply wire loss when charge crosses vehicle power cables + * @return 0 or left over charge in kJ which does not fit in any connected batteries */ - int charge_battery( int amount, bool include_other_vehicles = true ); + int charge_battery( int amount, bool apply_loss = true ); /** - * Try to discharge our (and, optionally, connected vehicles') batteries by the given amount. + * Discharges batteries in connected vehicles/appliances * @param amount to discharge in kJ - * @param recurse if true draws also from cable connected vehicles. - * @return amount of request unfulfilled (0 if totally successful). + * @param apply_loss if true apply wire loss when charge crosses vehicle power cables + * @return 0 or unfulfilled part of \p amount in kJ */ - int discharge_battery( int amount, bool recurse = true ); + int discharge_battery( int amount, bool apply_loss = true ); /** * Mark mass caches and pivot cache as dirty @@ -1537,16 +1546,9 @@ class vehicle /** Returns roughly driving skill level at which there is no chance of fumbling. */ float handling_difficulty() const; - /** - * Use vehicle::traverse_vehicle_graph (breadth-first search) to enumerate all vehicles - * connected to @ref origins by parts with POWER_TRANSFER flag. - * @param origins set of pointers to vehicles to start searching from - * @return a map of vehicle pointers to a bool that is true if the - * vehicle is in the @ref origins set. - */ - static std::map enumerate_vehicles( const std::set &origins ); // idle fuel consumption - void idle( bool on_map = true ); + // @param on_map if true vehicle processes noise/smoke and updates time + void idle( bool on_map ); // continuous processing for running vehicle alarms void alarm(); // leak from broken tanks diff --git a/src/vehicle_display.cpp b/src/vehicle_display.cpp index 847e4c58bde57..15c70935113b8 100644 --- a/src/vehicle_display.cpp +++ b/src/vehicle_display.cpp @@ -450,7 +450,7 @@ void vehicle::print_fuel_indicator( const catacurses::window &win, const point & units = _( "mL" ); } if( fuel_type == itype_battery ) { - rate += power_to_energy_bat( net_battery_charge_rate(), 1_hours ); + rate += power_to_energy_bat( net_battery_charge_rate( /* include_reactors = */ true ), 1_hours ); units = _( "kJ" ); } if( rate != 0 ) { diff --git a/src/vehicle_move.cpp b/src/vehicle_move.cpp index d39651d9a0259..a8cc96f2d2663 100644 --- a/src/vehicle_move.cpp +++ b/src/vehicle_move.cpp @@ -233,7 +233,7 @@ void vehicle::smart_controller_handle_turn( bool thrusting, int prev_mask = 0; // opt_ prefix denotes values for currently found "optimal" engine configuration - units::power opt_net_echarge_rate = net_battery_charge_rate(); + units::power opt_net_echarge_rate = net_battery_charge_rate( /* include_reactors = */ true ); // total engine fuel energy usage (J) units::power opt_fuel_usage = 0_W; @@ -321,7 +321,7 @@ void vehicle::smart_controller_handle_turn( bool thrusting, int safe_vel = is_stationary ? 1 : safe_ground_velocity( true ); int accel = is_stationary ? 1 : current_acceleration() * traction; units::power fuel_usage = 0_W; - units::power net_echarge_rate = net_battery_charge_rate(); + units::power net_echarge_rate = net_battery_charge_rate( /* include_reactors = */ true ); float load_approx = static_cast( std::min( accel_demand, accel ) ) / std::max( accel, 1 ); update_alternator_load(); float load_approx_alternator = std::min( 0.01f, static_cast( alternator_load ) / 1000 ); diff --git a/src/vehicle_part.cpp b/src/vehicle_part.cpp index fa164d1d83776..2900a6a1e64c4 100644 --- a/src/vehicle_part.cpp +++ b/src/vehicle_part.cpp @@ -685,7 +685,7 @@ bool vehicle::can_enable( const vehicle_part &pt, bool alert ) const // TODO: check fuel for combustion engines - if( pt.info().epower < 0_W && fuel_left( fuel_type_battery, true ) <= 0 ) { + if( pt.info().epower < 0_W && fuel_left( fuel_type_battery ) <= 0 ) { if( alert ) { add_msg( m_bad, _( "Insufficient power to enable %s" ), pt.name() ); } diff --git a/src/vehicle_use.cpp b/src/vehicle_use.cpp index 4a31f06a9a3ed..550811863e6c5 100644 --- a/src/vehicle_use.cpp +++ b/src/vehicle_use.cpp @@ -248,7 +248,7 @@ void vehicle::build_electronics_menu( veh_menu &menu ) menu.add( camera_on ? colorize( _( "Turn off camera system" ), c_pink ) : _( "Turn on camera system" ) ) - .enable( fuel_left( fuel_type_battery, true ) ) + .enable( fuel_left( fuel_type_battery ) ) .hotkey( "TOGGLE_CAMERA" ) .keep_menu_open() .on_submit( [&] { @@ -671,7 +671,7 @@ bool vehicle::start_engine( vehicle_part &vp ) const double cold_factor = engine_cold_factor( vp ); const units::power start_power = -part_epower( vp ) * ( dmg * 5 + cold_factor * 2 + 10 ); const int start_bat = power_to_energy_bat( start_power, start_time ); - if( discharge_battery( start_bat, true ) != 0 ) { + if( discharge_battery( start_bat ) != 0 ) { sounds::sound( pos, vpi.engine_noise_factor(), sounds::sound_t::alarm, string_format( _( "the %s rapidly clicking." ), vp.name() ), true, "vehicle", "engine_multi_click_fail" ); @@ -809,7 +809,7 @@ void vehicle::enable_patrol() void vehicle::honk_horn() const { - const bool no_power = !fuel_left( fuel_type_battery, true ); + const bool no_power = !fuel_left( fuel_type_battery ); bool honked = false; for( const vpart_reference &vp : get_avail_parts( "HORN" ) ) { @@ -889,7 +889,7 @@ void vehicle::reload_seeds( const tripoint &pos ) void vehicle::beeper_sound() const { // No power = no sound - if( fuel_left( fuel_type_battery, true ) == 0 ) { + if( fuel_left( fuel_type_battery ) == 0 ) { return; } @@ -1604,7 +1604,7 @@ void vehicle::build_bike_rack_menu( veh_menu &menu, int part ) void vpart_position::form_inventory( inventory &inv ) const { - const int veh_battery = vehicle().fuel_left( itype_battery, true ); + const int veh_battery = vehicle().fuel_left( itype_battery ); const cata::optional vp_faucet = part_with_tool( itype_water_faucet ); const cata::optional vp_cargo = part_with_feature( "CARGO", true ); @@ -1653,16 +1653,16 @@ static bool use_vehicle_tool( vehicle &veh, const tripoint &vp_pos, const itype_ get_player_character().invoke_item( &pseudo ); return true; } - if( veh.fuel_left( itype_battery, true ) < pseudo.ammo_required() ) { + if( veh.fuel_left( itype_battery ) < pseudo.ammo_required() ) { return false; } - // TODO: Figure out this comment: Pseudo items don't have a magazine in it, and they don't need it anymore. item pseudo_magazine( pseudo.magazine_default() ); pseudo_magazine.clear_items(); // no initial ammo pseudo.put_in( pseudo_magazine, item_pocket::pocket_type::MAGAZINE_WELL ); - const int capacity = pseudo.ammo_capacity( ammo_battery ); - const int qty = capacity - veh.discharge_battery( capacity ); - pseudo.ammo_set( itype_battery, qty ); + const int64_t tool_capacity = pseudo.ammo_capacity( ammo_battery ); + const int64_t veh_battery = veh.battery_left(); + const int tool_battery = std::min( tool_capacity, veh_battery ); + pseudo.ammo_set( itype_battery, tool_battery ); get_player_character().invoke_item( &pseudo ); player_activity &act = get_player_character().activity; @@ -1673,8 +1673,8 @@ static bool use_vehicle_tool( vehicle &veh, const tripoint &vp_pos, const itype_ act.coords.push_back( vp_pos ); // tell it to search for the tool on `pos` act.str_values.push_back( tool_type.str() ); // specific tool on the rig } - - veh.charge_battery( pseudo.ammo_remaining() ); + const int used_charges = tool_battery - pseudo.ammo_remaining(); + veh.discharge_battery( used_charges ); return true; } @@ -1927,7 +1927,7 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ } menu.add( _( "Use " ) + tool->nname( 1 ) ) - .enable( fuel_left( itype_battery, true ) >= tool->charges_to_use() ) + .enable( fuel_left( itype_battery ) >= tool->charges_to_use() ) .hotkey( hotkey ) .skip_locked_check( !tool_wants_battery( tool ) ) .on_submit( [this, vppos, tool] { use_vehicle_tool( *this, vppos, tool ); } ); @@ -2029,7 +2029,7 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ if( vp.part_with_tool( itype_pseudo_water_purifier ) ) { menu.add( _( "Purify water in vehicle tank" ) ) .enable( fuel_left( itype_water ) && - fuel_left( itype_battery, true ) >= itype_pseudo_water_purifier->charges_to_use() ) + fuel_left( itype_battery ) >= itype_pseudo_water_purifier->charges_to_use() ) .hotkey( "PURIFY_WATER" ) .on_submit( [this] { const auto sel = []( const vehicle_part & pt ) @@ -2044,7 +2044,7 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ return; } int64_t cost = static_cast( itype_pseudo_water_purifier->charges_to_use() ); - if( fuel_left( itype_battery, true ) < tank.ammo_remaining() * cost ) + if( fuel_left( itype_battery ) < tank.ammo_remaining() * cost ) { //~ $1 - vehicle name, $2 - part name add_msg( m_bad, _( "Insufficient power to purify the contents of the %1$s's %2$s" ), diff --git a/src/vpart_position.h b/src/vpart_position.h index 50ee86e0cd4d8..e45aef29709a3 100644 --- a/src/vpart_position.h +++ b/src/vpart_position.h @@ -106,6 +106,9 @@ class vpart_position */ // TODO: change to return tripoint. point mount() const; + + // implementation required for using as std::map key + bool operator<( const vpart_position &other ) const; }; /** diff --git a/tests/vehicle_part_test.cpp b/tests/vehicle_part_test.cpp index 3de106dd487f9..7f6b75fc4a4b2 100644 --- a/tests/vehicle_part_test.cpp +++ b/tests/vehicle_part_test.cpp @@ -154,7 +154,7 @@ static void test_craft_via_rig( const std::vector &items, int give_battery REQUIRE( ovp.has_value() ); vehicle &veh = ovp->vehicle(); - REQUIRE( veh.fuel_left( itype_water_clean, true ) == 0 ); + REQUIRE( veh.fuel_left( itype_water_clean ) == 0 ); for( const vpart_reference &tank : veh.get_avail_parts( vpart_bitflags::VPFLAG_FLUIDTANK ) ) { tank.part().ammo_set( itype_water_clean, give_water ); break; @@ -199,7 +199,7 @@ static void test_craft_via_rig( const std::vector &items, int give_battery } CHECK( veh.battery_power_level().first == expect_battery ); - CHECK( veh.fuel_left( itype_water_clean, true ) == expect_water ); + CHECK( veh.fuel_left( itype_water_clean ) == expect_water ); veh.unboard_all(); } @@ -221,7 +221,7 @@ TEST_CASE( "faucet_offers_cold_water", "[vehicle][vehicle_parts]" ) REQUIRE( ovp.has_value() ); vehicle &veh = ovp->vehicle(); - REQUIRE( veh.fuel_left( itype_water_clean, true ) == 0 ); + REQUIRE( veh.fuel_left( itype_water_clean ) == 0 ); item *tank_it = nullptr; for( const vpart_reference &tank : veh.get_avail_parts( vpart_bitflags::VPFLAG_FLUIDTANK ) ) { tank.part().ammo_set( itype_water_clean, water_charges ); @@ -230,7 +230,7 @@ TEST_CASE( "faucet_offers_cold_water", "[vehicle][vehicle_parts]" ) break; } REQUIRE( tank_it != nullptr ); - REQUIRE( veh.fuel_left( itype_water_clean, true ) == static_cast( water_charges ) ); + REQUIRE( veh.fuel_left( itype_water_clean ) == static_cast( water_charges ) ); cata::optional faucet; for( const vpart_reference &vpr : veh.get_all_parts() ) { @@ -243,7 +243,7 @@ TEST_CASE( "faucet_offers_cold_water", "[vehicle][vehicle_parts]" ) get_map().board_vehicle( faucet->pos() + tripoint_east, &character ); veh_menu menu( veh, "TEST" ); for( int i = 0; i < water_charges; i++ ) { - CAPTURE( i, veh.fuel_left( itype_water_clean, true ) ); + CAPTURE( i, veh.fuel_left( itype_water_clean ) ); menu.reset(); veh.build_interact_menu( menu, faucet->pos(), false ); const std::vector items = menu.get_items(); @@ -252,7 +252,7 @@ TEST_CASE( "faucet_offers_cold_water", "[vehicle][vehicle_parts]" ) []( const veh_menu_item & it ) { return it._text == "Have a drink"; } ); - REQUIRE( veh.fuel_left( itype_water_clean, true ) == ( water_charges - i ) ); + REQUIRE( veh.fuel_left( itype_water_clean ) == ( water_charges - i ) ); REQUIRE( drink_item_it != items.end() ); REQUIRE( drink_item_it->_enabled == !stomach_should_be_full ); // stomach should be full REQUIRE( character.get_morale_level() == ( i != 0 ? 1 : 0 ) ); // bonus morale from cold water @@ -264,7 +264,7 @@ TEST_CASE( "faucet_offers_cold_water", "[vehicle][vehicle_parts]" ) process_activity( character ); REQUIRE( character.get_morale_level() == 1 ); } - REQUIRE( veh.fuel_left( itype_water_clean, true ) == 0 ); + REQUIRE( veh.fuel_left( itype_water_clean ) == 0 ); REQUIRE( tank_it->empty_container() ); get_map().destroy_vehicle( &veh ); } diff --git a/tests/vehicle_power_test.cpp b/tests/vehicle_power_test.cpp index 4bcc7b641e36e..95bc5541bfea2 100644 --- a/tests/vehicle_power_test.cpp +++ b/tests/vehicle_power_test.cpp @@ -19,6 +19,10 @@ static const efftype_id effect_blind( "blind" ); static const itype_id fuel_type_battery( "battery" ); static const itype_id fuel_type_plut_cell( "plut_cell" ); +static const vpart_id vpart_frame( "frame" ); +static const vpart_id vpart_small_storage_battery( "small_storage_battery" ); + +static const vproto_id vehicle_prototype_none( "none" ); static const vproto_id vehicle_prototype_reactor_test( "reactor_test" ); static const vproto_id vehicle_prototype_scooter_electric_test( "scooter_electric_test" ); static const vproto_id vehicle_prototype_scooter_test( "scooter_test" ); @@ -72,6 +76,100 @@ TEST_CASE( "vehicle power with reactor", "[vehicle][power]" ) } } +TEST_CASE( "power loss to cables", "[vehicle][power]" ) +{ + clear_vehicles(); + reset_player(); + build_test_map( ter_id( "t_pavement" ) ); + map &here = get_map(); + + const auto connect_debug_cord = [&here]( const tripoint & source, + const tripoint & target ) { + const optional_vpart_position target_vp = here.veh_at( target ); + const optional_vpart_position source_vp = here.veh_at( source ); + + item cord( "test_power_cord_25_loss" ); + cord.set_var( "source_x", source.x ); + cord.set_var( "source_y", source.y ); + cord.set_var( "source_z", source.z ); + cord.set_var( "state", "pay_out_cable" ); + cord.active = true; + + if( !target_vp ) { + debugmsg( "missing target at %s", target.to_string() ); + } + vehicle *const target_veh = &target_vp->vehicle(); + vehicle *const source_veh = &source_vp->vehicle(); + if( source_veh == target_veh ) { + debugmsg( "source same as target" ); + } + + tripoint target_global = here.getabs( target ); + const vpart_id vpid( cord.typeId().str() ); + + point vcoords = source_vp->mount(); + vehicle_part source_part( vpid, "", vcoords, item( cord ) ); + source_part.target.first = target_global; + source_part.target.second = target_veh->global_square_location().raw(); + source_veh->install_part( vcoords, source_part ); + + vcoords = target_vp->mount(); + vehicle_part target_part( vpid, "", vcoords, item( cord ) ); + tripoint source_global( cord.get_var( "source_x", 0 ), + cord.get_var( "source_y", 0 ), + cord.get_var( "source_z", 0 ) ); + target_part.target.first = here.getabs( source_global ); + target_part.target.second = source_veh->global_square_location().raw(); + target_veh->install_part( vcoords, target_part ); + }; + + const std::vector placements { { 4, 10, 0 }, { 6, 10, 0 }, { 8, 10, 0 } }; + std::vector batteries; + for( const tripoint &p : placements ) { + REQUIRE( !here.veh_at( p ).has_value() ); + vehicle *veh = here.add_vehicle( vehicle_prototype_none, p, 0_degrees, 0, 0 ); + REQUIRE( veh != nullptr ); + const int frame_part_idx = veh->install_part( point_zero, vpart_frame ); + REQUIRE( frame_part_idx != -1 ); + const int bat_part_idx = veh->install_part( point_zero, vpart_small_storage_battery ); + REQUIRE( bat_part_idx != -1 ); + veh->refresh(); + here.add_vehicle_to_cache( veh ); + batteries.emplace_back( *veh, bat_part_idx ); + } + // connect first to second and second to third, each cord is 25% lossy + // third battery will on average take twice as many charges to charge as the first + for( size_t i = 0; i < placements.size() - 1; i++ ) { + connect_debug_cord( placements[i], placements[i + 1] ); + } + const optional_vpart_position ovp_first = here.veh_at( placements[0] ); + REQUIRE( ovp_first.has_value() ); + vehicle &v = ovp_first->vehicle(); // charge first battery + struct preset_t { + const int charge; // how much charge to expect + const int max_charge_excess; // minimum expect to spill out + const int max_charge_in_battery; // max charge expected in each battery + const int discharge; // how much to discharge + const int min_discharge_deficit; // minimum deficit after discharge + }; + const std::vector presets { + { 1000, 0, 250, 3000, 2000 }, + { 3000, 0, 750, 5000, 3000 }, + { 9000, 5500, 1000, 9000, 6000 }, + }; + for( const preset_t &preset : presets ) { + REQUIRE( v.fuel_left( fuel_type_battery ) == 0 ); // ensure empty batteries + const int remainder = v.charge_battery( preset.charge ); + CHECK( remainder <= preset.max_charge_excess ); + for( size_t i = 0; i < batteries.size(); i++ ) { + CAPTURE( i ); + CHECK( preset.max_charge_in_battery >= batteries[i].part().ammo_remaining() ); + } + const int deficit = v.discharge_battery( preset.discharge ); + CHECK( deficit >= preset.min_discharge_deficit ); + } +} + TEST_CASE( "Solar power", "[vehicle][power]" ) { clear_vehicles();