From 9bb74530eacac3e437e451d94884a4d8e1c382cd Mon Sep 17 00:00:00 2001 From: Kamayana Date: Wed, 1 May 2024 21:59:25 -0700 Subject: [PATCH] Make power grids easier to split up, reconnect, and merge parts to (#73056) * Write message if grids are successfully merged * Stop dragging appliance if merging it to a grid This was a bug that let you drag power grids and break the game * Remove power cords linked between merging grids Extension cords are dropped to the ground * Add functions for removing all but one power grid part The appliance menu can now exit `app_loop` manually by setting `veh` to nullptr. I needed to add this because it'd be ambiguous which of the split-apart vehicles the menu is targeting otherwise. * Move app menu's Merge functionality into Plug If the connected appliances are adjacent & mergable powergrids, then merge them. Otherwise, connect with a power cord. * Rename "power grid" to part name if split down to one part * Ask to split part off from power grid when trying to drag it (unless the part is wall mounted) * Recalc powergrid pos & pivot when it gets split up * Improve clarity of power grid removal section * Remove CANT_DRAG tag. Instead, determine draggability when trying to grab * Do cleanup after merging, as power cords between merged vehicles now get disconnected * Adjust cable disconnection messages in menu Disconnect cables -> Disconnect items / tow cables / items and tow cables * Clang fixes --- data/raw/keybindings.json | 6 +-- src/handle_action.cpp | 25 ++++++++--- src/iuse_actor.cpp | 17 ++++++- src/veh_appliance.cpp | 95 ++++++++++++++++----------------------- src/veh_appliance.h | 22 ++++----- src/veh_interact.cpp | 7 ++- src/vehicle.cpp | 91 +++++++++++++++++++++++++++++++++---- src/vehicle.h | 3 +- src/vehicle_use.cpp | 25 ++++++----- 9 files changed, 190 insertions(+), 101 deletions(-) diff --git a/data/raw/keybindings.json b/data/raw/keybindings.json index 01d1de5f9fe1b..a52fc78873233 100644 --- a/data/raw/keybindings.json +++ b/data/raw/keybindings.json @@ -1760,10 +1760,10 @@ }, { "type": "keybinding", - "id": "MERGE", + "id": "DISCONNECT_GRID", "category": "APP_INTERACT", - "name": "Merge power grids", - "bindings": [ { "input_method": "keyboard_any", "key": "m" } ] + "name": "Separate from power grid", + "bindings": [ { "input_method": "keyboard_any", "key": "c" } ] }, { "type": "keybinding", diff --git a/src/handle_action.cpp b/src/handle_action.cpp index b0e9fdcc4e143..56e1dd667e3bc 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -173,8 +173,6 @@ static const zone_type_id zone_type_UNLOAD_ALL( "UNLOAD_ALL" ); static const zone_type_id zone_type_VEHICLE_DECONSTRUCT( "VEHICLE_DECONSTRUCT" ); static const zone_type_id zone_type_VEHICLE_REPAIR( "VEHICLE_REPAIR" ); -static const std::string flag_CANT_DRAG( "CANT_DRAG" ); - #define dbg(x) DebugLog((x),D_GAME) << __FILE__ << ":" << __LINE__ << ": " #if defined(__ANDROID__) @@ -710,15 +708,32 @@ static void grab() } if( const optional_vpart_position vp = here.veh_at( grabp ) ) { + std::string veh_name = vp->vehicle().name; if( !vp->vehicle().handle_potential_theft( you ) ) { return; } - if( vp->vehicle().has_tag( flag_CANT_DRAG ) ) { - add_msg( m_info, _( "There's nothing to grab there!" ) ); + if( vp.part_with_feature( VPFLAG_WALL_MOUNTED, false ) ) { + add_msg( m_info, _( "You can't move that, it's attached to the wall." ) ); return; } + // Powergrids with more than one part are undraggable. + // Offer to split the targeted part off onto its own, making it draggable. + if( vp->vehicle().is_powergrid() && vp->vehicle().part_count() > 1 ) { + if( !query_yn( + _( "That's part of a power grid. Separate it from the grid so you can move it?" ) ) ) { + return; + } + get_player_character().pause(); + vp->vehicle().separate_from_grid( vp.value().mount() ); + if( const optional_vpart_position split_vp = here.veh_at( grabp ) ) { + veh_name = split_vp->vehicle().name; + } else { + debugmsg( "Lost the part to drag after splitting power grid!" ); + return; + } + } you.grab( object_type::VEHICLE, grabp - you.pos() ); - add_msg( _( "You grab the %s." ), vp->vehicle().name ); + add_msg( _( "You grab the %s." ), veh_name ); } else if( here.has_furn( grabp ) ) { // If not, grab furniture if present if( !here.furn( grabp ).obj().is_movable() ) { add_msg( _( "You can not grab the %s." ), here.furnname( grabp ) ); diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp index 08729328ed3b4..5cc37bcc0ce36 100644 --- a/src/iuse_actor.cpp +++ b/src/iuse_actor.cpp @@ -4864,17 +4864,30 @@ std::optional link_up_actor::link_to_veh_app( Character *p, item &it, } else { // Connecting two vehicles together. + const bool using_power_cord = it.typeId() == itype_power_cord; + if( using_power_cord && it.link().t_veh->is_powergrid() && sel_vp->vehicle().is_powergrid() ) { + // If both vehicles are adjacent power grids, try to merge them together first. + const point prev_pos = here.bub_from_abs( it.link().t_veh->coord_translate( it.link().t_mount ) + + it.link().t_abs_pos ).xy().raw(); + if( selection.xy().distance( prev_pos ) <= 1.5f && + it.link().t_veh->merge_appliance_into_grid( sel_vp->vehicle() ) ) { + it.link().t_veh->part_removal_cleanup(); + p->add_msg_if_player( _( "You merge the two power grids." ) ); + return 1; + } + // Unable to merge, so connect them with a power cord instead. + } ret_val result = it.link_to( sel_vp, to_ports ? link_state::vehicle_port : link_state::vehicle_battery ); if( !result.success() ) { p->add_msg_if_player( m_bad, result.str() ); return 0; } - if( p->has_item( it ) ) { + if( using_power_cord || p->has_item( it ) ) { p->add_msg_if_player( m_good, result.str() ); } - if( it.typeId() != itype_power_cord ) { + if( using_power_cord ) { // Remove linked_flag from attached parts - the just-added cable vehicle parts do the same thing. it.reset_link( true, p ); } diff --git a/src/veh_appliance.cpp b/src/veh_appliance.cpp index 79be24a49e00d..ca1e2a32b7ed5 100644 --- a/src/veh_appliance.cpp +++ b/src/veh_appliance.cpp @@ -4,6 +4,7 @@ #include "itype.h" #include "map_iterator.h" #include "action.h" +#include "messages.h" #include "output.h" #include "overmapbuffer.h" #include "player_activity.h" @@ -33,8 +34,6 @@ static const vpart_id vpart_ap_standing_lamp( "ap_standing_lamp" ); static const vproto_id vehicle_prototype_none( "none" ); static const std::string flag_APPLIANCE( "APPLIANCE" ); -static const std::string flag_WALL_MOUNTED( "WALL_MOUNTED" ); -static const std::string flag_CANT_DRAG( "CANT_DRAG" ); static const std::string flag_WIRING( "WIRING" ); static const std::string flag_HALF_CIRCLE_LIGHT( "HALF_CIRCLE_LIGHT" ); @@ -83,10 +82,6 @@ void place_appliance( const tripoint &p, const vpart_id &vpart, const std::optio veh->add_tag( flag_WIRING ); } - if( veh->is_powergrid() || vpinfo.has_flag( flag_WALL_MOUNTED ) ) { - veh->add_tag( flag_CANT_DRAG ); - } - // Update the vehicle cache immediately, // or the appliance will be invisible for the first couple of turns. here.add_vehicle_to_cache( veh ); @@ -100,8 +95,9 @@ void place_appliance( const tripoint &p, const vpart_id &vpart, const std::optio } vehicle &veh_target = vp->vehicle(); if( veh_target.has_tag( flag_APPLIANCE ) ) { - if( veh->is_powergrid() && veh_target.is_powergrid() ) { - veh->merge_appliance_into_grid( veh_target ); + if( veh->is_powergrid() && veh_target.is_powergrid() && + veh->merge_appliance_into_grid( veh_target ) ) { + add_msg( _( "You merge it into the adjacent power grid." ) ); continue; } if( connected_vehicles.find( &veh_target ) == connected_vehicles.end() ) { @@ -110,6 +106,7 @@ void place_appliance( const tripoint &p, const vpart_id &vpart, const std::optio } } } + veh->part_removal_cleanup(); // Make some lighting appliances directed if( vpinfo.has_flag( flag_HALF_CIRCLE_LIGHT ) && partnum != -1 ) { @@ -148,6 +145,7 @@ veh_app_interact::veh_app_interact( vehicle &veh, const point &p ) ctxt.register_action( "RENAME" ); ctxt.register_action( "REMOVE" ); ctxt.register_action( "MERGE" ); + ctxt.register_action( "DISCONNECT_GRID" ); } // @returns true if a battery part exists on any vehicle connected to veh @@ -325,11 +323,6 @@ bool veh_app_interact::can_siphon() return false; } -bool veh_app_interact::can_merge() -{ - return veh->is_powergrid(); -} - // Helper function for selecting a part in the parts list. // If only one part is available, don't prompt the player. static vehicle_part *pick_part( const std::vector &parts, @@ -510,6 +503,23 @@ void veh_app_interact::remove() } } +bool veh_app_interact::can_disconnect() +{ + for( int &i : veh->parts_at_relative( a_point, true ) ) { + if( veh->part( i ).has_flag( vp_flag::linked_flag ) || + veh->part( i ).info().has_flag( "TOW_CABLE" ) ) { + return false; + } + } + return true; +} + +void veh_app_interact::disconnect() +{ + veh->separate_from_grid( a_point ); + get_player_character().pause(); +} + void veh_app_interact::plug() { const int part = veh->part_at( veh->coord_translate( a_point ) ); @@ -521,41 +531,6 @@ void veh_app_interact::plug() } } -void veh_app_interact::merge() -{ - map &here = get_map(); - - const int part = veh->part_at( a_point ); - const tripoint app_pos = veh->global_part_pos3( part ); - - const std::function f = [&here, app_pos]( const tripoint & pnt ) { - if( pnt == app_pos ) { - return false; - } - const optional_vpart_position target_vp = here.veh_at( pnt ); - if( !target_vp ) { - return false; - } - vehicle &target_veh = target_vp->vehicle(); - if( !target_veh.is_powergrid() ) { - return false; - } - return true; - }; - - const std::optional target_pos = choose_adjacent_highlight( app_pos, - _( "Merge the appliance into which grid?" ), _( "Target must be adjacent." ), f, false, false ); - if( !target_pos ) { - return; - } - const optional_vpart_position target_vp = here.veh_at( *target_pos ); - if( !target_vp ) { - return; - } - vehicle &target_veh = target_vp->vehicle(); - veh->merge_appliance_into_grid( target_veh ); -} - void veh_app_interact::populate_app_actions() { map &here = get_map(); @@ -602,14 +577,20 @@ void veh_app_interact::populate_app_actions() plug(); } ); imenu.addentry( -1, true, ctxt.keys_bound_to( "PLUG" ).front(), - ctxt.get_action_name( "PLUG" ) ); - - // Merge - app_actions.emplace_back( [this]() { - merge(); - } ); - imenu.addentry( -1, can_merge(), ctxt.keys_bound_to( "MERGE" ).front(), - ctxt.get_action_name( "MERGE" ) ); + string_format( "%s%s", ctxt.get_action_name( "PLUG" ), + //~ An addendum to Plug In's description, as in: Plug in appliance / merge power grid". + veh->is_powergrid() ? _( " / merge power grid" ) : "" ) ); + + if( veh->is_powergrid() && veh->part_count() > 1 && !vp->info().has_flag( VPFLAG_WALL_MOUNTED ) ) { + // Disconnect from power grid + app_actions.emplace_back( [this]() { + disconnect(); + veh = nullptr; + } ); + const bool can_disc = can_disconnect(); + imenu.addentry_desc( -1, can_disc, ctxt.keys_bound_to( "DISCONNECT_GRID" ).front(), + ctxt.get_action_name( "DISCONNECT_GRID" ), can_disc ? "" : _( "Remove other cables first" ) ); + } /*************** Get part-specific actions ***************/ veh_menu menu( veh, "IF YOU SEE THIS IT IS A BUG" ); @@ -663,7 +644,7 @@ void veh_app_interact::app_loop() app_actions[ret](); } // Player activity queued up, close interaction menu - if( !act.is_null() || !get_player_character().activity.is_null() ) { + if( veh == nullptr || !act.is_null() || !get_player_character().activity.is_null() ) { done = true; } } diff --git a/src/veh_appliance.h b/src/veh_appliance.h index 74a515ba6ec7e..0bc0ef8f2e542 100644 --- a/src/veh_appliance.h +++ b/src/veh_appliance.h @@ -97,17 +97,6 @@ class veh_app_interact * @returns True if a liquid can be siphoned from the appliance. */ bool can_siphon(); - /** - * Checks whether the current appliance is power storage - * or powergen or a cable and can thus be merged into a powergrid. - * @returns True if the appliance can be merged. - */ - bool can_merge(); - /** - * Function associated with the "MERGE" action. - * Merge power grid elements together into a single appliance - */ - void merge(); /** * Function associated with the "REFILL" action. * Checks all appliance parts for a watertight container to refill. If multiple @@ -133,6 +122,17 @@ class veh_app_interact * Turns the installed appliance into its base item. */ void remove(); + /** + * Checks whether the part has any items linked to it so it can tell the player + * to disconnect those first. This prevents players from doing this by accident. + * @returns True if there aren't any tow cable parts or items linked to the mount point. + */ + bool can_disconnect(); + /** + * Function associated with the "DISCONNECT_GRID" action. + * Removes appliance from a power grid, allowing it to be moved individually. + */ + void disconnect(); /** * Function associated with the "PLUG" action. * Connects the power cable to selected tile. diff --git a/src/veh_interact.cpp b/src/veh_interact.cpp index 7a9786653869d..28e9a126eb2fc 100644 --- a/src/veh_interact.cpp +++ b/src/veh_interact.cpp @@ -3362,10 +3362,13 @@ void veh_interact::complete_vehicle( Character &you ) veh.remove_remote_part( *vp ); } - // Remove any leftover power cords from the appliance - if( appliance_removal && veh.part_count() >= 2 ) { + if( appliance_removal && veh.part_count() > 1 ) { + // Split up power grids veh.find_and_split_vehicles( here, { vp_index } ); veh.part_removal_cleanup(); + // Ensure the position, pivot, and precalc points are up-to-date + veh.pos -= veh.pivot_anchor[0]; + veh.precalc_mounts( 0, veh.turn_dir, point() ); here.rebuild_vehicle_level_caches(); if( auto newpart = here.veh_at( act_pos ).part_with_feature( VPFLAG_APPLIANCE, false ) ) { diff --git a/src/vehicle.cpp b/src/vehicle.cpp index cf473f34f219c..16da22d7ca54d 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -126,6 +126,8 @@ static const proficiency_id proficiency_prof_driver( "prof_driver" ); static const skill_id skill_swimming( "swimming" ); +static const vpart_id vpart_power_cord( "power_cord" ); + static const vproto_id vehicle_prototype_none( "none" ); static const zone_type_id zone_type_VEHICLE_PATROL( "VEHICLE_PATROL" ); @@ -133,9 +135,11 @@ static const zone_type_id zone_type_VEHICLE_PATROL( "VEHICLE_PATROL" ); static const std::string flag_E_COMBUSTION( "E_COMBUSTION" ); static const std::string flag_APPLIANCE( "APPLIANCE" ); -static const std::string flag_CANT_DRAG( "CANT_DRAG" ); static const std::string flag_WIRING( "WIRING" ); +//~ Name for an array of electronic power grid appliances, like batteries and solar panels +static const translation power_grid_name = to_translation( "power grid" ); + static bool is_sm_tile_outside( const tripoint &real_global_pos ); static bool is_sm_tile_over_water( const tripoint &real_global_pos ); @@ -1817,10 +1821,25 @@ bool vehicle::merge_rackable_vehicle( vehicle *carry_veh, const std::vector bool vehicle::merge_vehicle_parts( vehicle *veh ) { + map &here = get_map(); for( const vehicle_part &part : veh->parts ) { if( part.is_fake || part.removed ) { continue; } + if( part.info().has_flag( VPFLAG_POWER_TRANSFER ) ) { + // Disconnect cables that are attached between the merging vehicles. + // Delete power cords, drop extension cords on the ground. + item drop = veh->part_to_item( part ); + if( drop.link().t_veh.get() == this ) { + if( !veh->magic && part.info().id != vpart_power_cord ) { + const tripoint drop_pos = veh->global_part_pos3( part ); + drop.reset_link( false, nullptr, -1, true, drop_pos ); + here.add_item_or_charges( drop_pos, drop ); + } + veh->remove_remote_part( part ); + continue; + } + } point part_loc = veh->mount_to_tripoint( part.mount ).xy(); remove_fake_parts(); @@ -1831,16 +1850,24 @@ bool vehicle::merge_vehicle_parts( vehicle *veh ) refresh(); } - map &here = get_map(); here.destroy_vehicle( veh ); return true; } -void vehicle::merge_appliance_into_grid( vehicle &veh_target ) +bool vehicle::merge_appliance_into_grid( vehicle &veh_target ) { if( &veh_target == this ) { - return; + return false; + } + // Release grab if player is dragging either appliance + if( get_avatar().get_grab_type() == object_type::VEHICLE ) { + const tripoint grab_point = get_avatar().pos() + get_avatar().grab_point; + const optional_vpart_position grabbed_veh = get_map().veh_at( grab_point ); + if( grabbed_veh && ( &grabbed_veh->vehicle() == this || &grabbed_veh->vehicle() == &veh_target ) ) { + add_msg( _( "You release the %s." ), grabbed_veh->vehicle().name ); + get_avatar().grab( object_type::NONE ); + } } //Reset both grid turn_dir to prevent rotation on merge turn_dir = 0_degrees; @@ -1869,18 +1896,57 @@ void vehicle::merge_appliance_into_grid( vehicle &veh_target ) if( !merge_vehicle_parts( &veh_target ) ) { debugmsg( "failed to merge vehicle parts" ); } else { - //The grid needs to stay undraggable - add_tag( flag_CANT_DRAG ); - //Keep wall wiring sections from losing their flag //A grid with only wires needs this flag to count as a powergrid //But it's not a problem if a grid without any has this flag, thus we can add it without issue add_tag( flag_WIRING ); - name = _( "power grid" ); + name = power_grid_name.translated(); + return true; } } else { add_msg( m_bad, _( "Can't merge into %s, the resulting grid would be too big." ), veh_target.name ); } + return false; +} + +void vehicle::separate_from_grid( const point mount ) +{ + // Disconnect items and power cords + unlink_cables( mount, get_player_character(), true, true, true ); + part_removal_cleanup(); + if( part_count() <= 1 ) { + // No need to split the power grid up. + return; + } + + // Split the target part away from the power grid. + const int idx = part_displayed_at( mount, true ); + if( idx == -1 ) { + debugmsg( "no part at mount point %s", mount.to_string() ); + return; + } + map &here = get_map(); + const std::string part_name = part( idx ).name(); + std::vector> split_indices( { { idx } } ); + const std::vector null_vehicles( 2, nullptr ); + const std::vector> null_mounts( 2, std::vector() ); + if( !split_vehicles( here, split_indices, null_vehicles, null_mounts ) ) { + debugmsg( "unable to split %s from power grid", part( idx ).name( false ) ); + return; + } + + // Split again if removing the target part separated the power grid into multiple sections. + find_and_split_vehicles( here, {} ); + + // Ensure the position, pivot, and precalc points are up-to-date. + shift_if_needed( get_map() ); + pos -= pivot_anchor[0]; + precalc_mounts( 0, turn_dir, point() ); + + add_msg( _( "You separate the %s from the power grid" ), part_name ); + + part_removal_cleanup(); + here.rebuild_vehicle_level_caches(); } bool vehicle::is_powergrid() const @@ -2079,6 +2145,10 @@ void vehicle::part_removal_cleanup() } } shift_if_needed( here ); + // If we've split away all of a power grid's parts, rename it from "power grid" to its part name. + if( is_powergrid() && name == power_grid_name.translated() && part_count() == 1 ) { + name = parts[0].info().name(); + } refresh( false ); // Rebuild cached indices coeff_air_dirty = coeff_air_changed; coeff_air_changed = false; @@ -2309,6 +2379,7 @@ bool vehicle::split_vehicles( map &here, std::vector *added_vehicles ) { bool did_split = false; + bool from_powergrid = is_powergrid(); size_t i = 0; for( i = 0; i < new_vehs.size(); i ++ ) { std::vector split_parts = new_vehs[ i ]; @@ -2353,7 +2424,9 @@ bool vehicle::split_vehicles( map &here, if( added_vehicles != nullptr ) { added_vehicles->emplace_back( new_vehicle ); } - new_vehicle->name = name; + // If we've split a power grid down to one part, rename it from "power grid" to its part name. + new_vehicle->name = from_powergrid && split_parts.size() == 1 ? + parts[ split_part0 ].info().name() : name; new_vehicle->owner = owner; new_vehicle->old_owner = old_owner; new_vehicle->move = move; diff --git a/src/vehicle.h b/src/vehicle.h index 39947555a3be0..fa708bdd71986 100644 --- a/src/vehicle.h +++ b/src/vehicle.h @@ -1062,7 +1062,8 @@ class vehicle bool merge_rackable_vehicle( vehicle *carry_veh, const std::vector &rack_parts ); // merges vehicles together by copying parts, does not account for any vehicle complexities bool merge_vehicle_parts( vehicle *veh ); - void merge_appliance_into_grid( vehicle &veh_target ); + bool merge_appliance_into_grid( vehicle &veh_target ); + void separate_from_grid( point mount ); bool is_powergrid() const; diff --git a/src/vehicle_use.cpp b/src/vehicle_use.cpp index 4be65521317e5..ecbf880a621be 100644 --- a/src/vehicle_use.cpp +++ b/src/vehicle_use.cpp @@ -1847,12 +1847,13 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ const bool player_inside = get_map().veh_at( get_player_character().pos() ) ? &get_map().veh_at( get_player_character().pos() )->vehicle() == this : false; - bool power_grid = false; - bool cable_linked = false; + bool power_linked = false; + bool item_linked = false; + bool tow_linked = false; for( vehicle_part *vp_part : vp_parts ) { - power_grid = power_grid ? true : vp_part->info().has_flag( VPFLAG_POWER_TRANSFER ); - cable_linked = cable_linked ? true : vp_part->has_flag( vp_flag::linked_flag ) || - vp_part->info().has_flag( "TOW_CABLE" ); + power_linked = power_linked || vp_part->info().has_flag( VPFLAG_POWER_TRANSFER ); + item_linked = item_linked || vp_part->has_flag( vp_flag::linked_flag ); + tow_linked = tow_linked || vp_part->info().has_flag( "TOW_CABLE" ); } if( !is_appliance() ) { @@ -2074,10 +2075,10 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ } } - if( power_grid ) { - menu.add( is_appliance() ? _( "Disconnect from power grid" ) : _( "Disconnect power connections" ) ) - .enable( !cable_linked ) - .desc( string_format( !cable_linked ? "" : _( "Remove other cables first" ) ) ) + if( power_linked ) { + menu.add( _( "Disconnect power connections" ) ) + .enable( !item_linked && !tow_linked ) + .desc( string_format( !item_linked && !tow_linked ? "" : _( "Remove other cables first" ) ) ) .skip_locked_check() .hotkey( "DISCONNECT_CABLES" ) .on_submit( [this, vp] { @@ -2085,8 +2086,10 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ get_player_character().pause(); } ); } - if( cable_linked ) { - menu.add( _( "Disconnect cables" ) ) + if( item_linked || tow_linked ) { + std::string menu_text = item_linked && tow_linked ? _( "Disconnect items and tow cables" ) : + item_linked ? _( "Disconnect items" ) : _( "Disconnect tow cables" ); + menu.add( menu_text ) .skip_locked_check() .hotkey( "DISCONNECT_CABLES" ) .on_submit( [this, vp] {