Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visual UI for changing vehicle part shapes #73739

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,18 @@ void game::draw_cursor( const tripoint &p ) const
}
#endif

void game::draw_cursor_unobscuring( const tripoint_bub_ms &p ) const
{
const tripoint_rel_ms rp = relative_view_pos( *this, p );
mvwputch_inv( w_terrain, ( rp.xy() + point_north_east ).raw(), c_cyan, "↙" );
mvwputch_inv( w_terrain, ( rp.xy() + point_south_east ).raw(), c_cyan, "↖" );
mvwputch_inv( w_terrain, ( rp.xy() + point_north_west ).raw(), c_cyan, "↘" );
mvwputch_inv( w_terrain, ( rp.xy() + point_south_west ).raw(), c_cyan, "↗" );
#if defined(TILES)
tilecontext->init_draw_cursor( p );
#endif
}

#if defined(TILES)
void game::draw_highlight( const tripoint_bub_ms &p )
{
Expand Down
3 changes: 3 additions & 0 deletions src/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,9 @@ class game
// TODO: Get rid of untyped overload
void draw_cursor( const tripoint &p ) const;
void draw_cursor( const tripoint_bub_ms &p ) const;
// Tiles: equivalent to draw_cusor
// Curses: draws diagonal arrows pointing at the tile so the target tile isn't obscured
void draw_cursor_unobscuring( const tripoint_bub_ms &p ) const;
// Draw a highlight graphic at p, for example when examining something.
// TILES only, in curses this does nothing
// TODO: Get rid of untyped overload
Expand Down
53 changes: 13 additions & 40 deletions src/veh_interact.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#include "units_utility.h"
#include "value_ptr.h"
#include "veh_type.h"
#include "veh_shape.h"
#include "veh_utils.h"
#include "vehicle.h"
#include "vehicle_selector.h"
Expand Down Expand Up @@ -129,8 +130,17 @@ static void act_vehicle_unload_fuel( vehicle *veh );

player_activity veh_interact::serialize_activity()
{
const auto *pt = sel_vehicle_part;
const auto *vp = sel_vpart_info;
const vehicle_part *pt = sel_vehicle_part;
const vpart_info *vp = sel_vpart_info;

if( sel_cmd == 'p' ) {
if( !parts_here.empty() ) {
const vpart_reference part_here( *veh, parts_here[0] );
const vpart_reference displayed_part( *veh, veh->part_displayed_at( part_here.mount() ) );
return veh_shape( *veh ).start( tripoint_bub_ms( displayed_part.pos() ) );
}
return player_activity();
}

if( sel_cmd == 'q' || sel_cmd == ' ' || !vp ) {
return player_activity();
Expand Down Expand Up @@ -550,8 +560,7 @@ void veh_interact::do_main_loop()
finish = do_unload();
}
} else if( action == "CHANGE_SHAPE" ) {
// purely comestic
do_change_shape();
sel_cmd = 'p';
} else if( action == "ASSIGN_CREW" ) {
if( owned_by_player ) {
do_assign_crew();
Expand Down Expand Up @@ -1984,42 +1993,6 @@ static void do_change_shape_menu( vehicle_part &vp )
}
}

void veh_interact::do_change_shape()
{
if( cant_do( 'p' ) == task_reason::INVALID_TARGET ) {
msg = _( "No valid vehicle parts here." );
return;
}

restore_on_out_of_scope<std::optional<std::string>> prev_title( title );
title = _( "Choose part to change shape:" );

shared_ptr_fast<ui_adaptor> current_ui = create_or_get_ui_adaptor();
restore_on_out_of_scope<int> prev_hilight_part( highlight_part );

int part_selected = 0;

while( true ) {
vehicle_part &vp = veh->part( parts_here[part_selected] );

highlight_part = part_selected;
overview_enable = [this, part_selected]( const vehicle_part & pt ) {
return &pt == &veh->part( part_selected );
};

ui_manager::redraw();
const std::string action = main_context.handle_input();

if( action == "QUIT" ) {
break;
} else if( action == "CONFIRM" || action == "CHANGE_SHAPE" ) {
do_change_shape_menu( vp );
} else {
move_in_list( part_selected, action, parts_here.size() );
}
}
}

void veh_interact::do_assign_crew()
{
if( cant_do( 'w' ) != task_reason::CAN_DO ) {
Expand Down
1 change: 0 additions & 1 deletion src/veh_interact.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ class veh_interact
void do_siphon();
// Returns true if exiting the screen
bool do_unload();
void do_change_shape();
void do_assign_crew();
void do_relabel();
/*@}*/
Expand Down
254 changes: 254 additions & 0 deletions src/veh_shape.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#include "veh_shape.h"

#include "animation.h"
#include "avatar.h"
#include "cached_options.h"
#include "game.h"
#include "options.h"
#include "output.h"
#include "player_activity.h"
#include "veh_type.h"
#include "veh_utils.h"
#include "ui_manager.h"

veh_shape::veh_shape( vehicle &vehicle ): veh( vehicle ) { }

player_activity veh_shape::start( const tripoint_bub_ms &pos )
{
avatar &you = get_avatar();
on_out_of_scope cleanup( []() {
get_map().invalidate_map_cache( get_avatar().view_offset.z );
} );
restore_on_out_of_scope<tripoint> view_offset_prev( you.view_offset );

cursor_allowed.clear();
for( const vpart_reference &part : veh.get_all_parts() ) {
cursor_allowed.insert( tripoint_bub_ms( part.pos() ) );
}

if( !set_cursor_pos( pos ) ) {
debugmsg( "failed to set cursor at given part" );
set_cursor_pos( tripoint_bub_ms( veh.global_part_pos3( 0 ) ) );
}

const auto target_ui_cb = make_shared_fast<game::draw_callback_t>(
[&]() {
const avatar &you = get_avatar();
g->draw_cursor_unobscuring( tripoint_bub_ms( you.pos() + you.view_offset ) );
} );
g->add_draw_callback( target_ui_cb );
ui_adaptor ui;
ui.mark_resize();
init_input();

std::string action = "CONFIRM";

while( true ) {
g->invalidate_main_ui_adaptor();
ui_manager::redraw();

if( action.empty() ) {
action = ctxt.handle_input( get_option<int>( "EDGE_SCROLL" ) );
}

if( handle_cursor_movement( action ) || ( action == "HELP_KEYBINDINGS" ) ) {
action.clear();
continue;
} else if( action == "CONFIRM" ) {
std::optional<vpart_reference> part;
do {
part = select_part_at_cursor( _( "Choose part to change shape" ),
_( "Confirm to select or quit to select another tile." ),
[]( const vpart_reference & vp ) {
if( vp.info().variants.size() <= 1 ) {
return ret_val<void>::make_failure( _( "Only one shape" ) );
} else if( vp.part_displayed() && vp.part_displayed()->part_index() != vp.part_index() ) {
return ret_val<void>::make_success( _( "Part is obscured" ) );
}
return ret_val<void>::make_success();
}, part );
if( part.has_value() ) {
change_part_shape( part.value() );
}
} while( part.has_value() );
action.clear();
} else if( action == "QUIT" ) {
return player_activity();
} else {
debugmsg( "here be dragons" );
return player_activity();
}
}
}

std::vector<vpart_reference> veh_shape::parts_under_cursor() const
{
std::vector<vpart_reference> res;
// TODO: tons of methods getting parts from vehicle but all of them seem inadequate?
for( int part_idx = 0; part_idx < veh.part_count_real(); part_idx++ ) {
const vehicle_part &p = veh.part( part_idx );
if( veh.bub_part_pos( p ) == get_cursor_pos() && !p.is_fake ) {
res.emplace_back( veh, part_idx );
}
}
return res;
}

std::optional<vpart_reference> veh_shape::select_part_at_cursor(
const std::string &title, const std::string &extra_description,
const std::function<ret_val<void>( const vpart_reference & )> &predicate,
const std::optional<vpart_reference> &preselect ) const
{
const std::vector<vpart_reference> parts = parts_under_cursor();
if( parts.empty() ) {
return std::nullopt;
}

uilist menu;
menu.desc_enabled = !extra_description.empty();
menu.desc_lines_hint = 1;
menu.hilight_disabled = true;
menu.w_x_setup = TERMX / 8;

for( const vpart_reference &pt : parts ) {
ret_val<void> predicate_result = predicate( pt );
uilist_entry entry( -1, true, MENU_AUTOASSIGN, pt.part().name(), "", predicate_result.str() );
entry.desc = extra_description;
entry.enabled = predicate_result.success();
entry.retval = predicate_result.success() ? menu.entries.size() : -2;
if( preselect && preselect->part_index() == pt.part_index() ) {
menu.selected = menu.entries.size();
}
menu.entries.push_back( entry );
}
menu.text = title;
menu.query();

return menu.ret >= 0 ? std::optional<vpart_reference>( parts[menu.ret] ) : std::nullopt;
}

void veh_shape::change_part_shape( vpart_reference vpr ) const
{
vehicle_part &part = vpr.part();
const vpart_info &vpi = part.info();
veh_menu menu( this->veh, _( "Choose cosmetic variant:" ) );
std::string chosen_variant = part.variant; // support for cancel

do {
menu.reset( false );

for( const auto &[vvid, vv] : vpi.variants ) {
menu.add( vv.get_label() )
.text_color( ( part.variant == vvid ) ? c_light_green : c_light_gray )
.keep_menu_open()
.skip_locked_check()
.skip_theft_check()
.location( veh.global_part_pos3( part ) )
.select( part.variant == vvid )
.desc( _( "Confirm to save or exit to revert" ) )
.symbol( vv.get_symbol_curses( 0_degrees, false ) )
.symbol_color( vpi.color )
.on_select( [&part, variant_id = vvid]() {
part.variant = variant_id;
} )
.on_submit( [&chosen_variant, variant_id = vvid]() {
chosen_variant = variant_id;
} );
}

// An ordering of the line drawing symbols that does not result in
// connecting when placed adjacent to each other vertically.
menu.sort( []( const veh_menu_item & a, const veh_menu_item & b ) {
const static std::map<int, int> symbol_order = {
{ LINE_XOXO, 0 }, { LINE_OXOX, 1 }, { LINE_XOOX, 2 }, { LINE_XXOO, 3 },
{ LINE_XXXX, 4 }, { LINE_OXXO, 5 }, { LINE_OOXX, 6 },
};
const auto a_iter = symbol_order.find( a._symbol );
const auto b_iter = symbol_order.find( b._symbol );
if( a_iter != symbol_order.end() ) {
return ( b_iter == symbol_order.end() ) || ( a_iter->second < b_iter->second );
} else {
return ( b_iter == symbol_order.end() ) && ( a._symbol < b._symbol );
}
} );
} while( menu.query() );

part.variant = chosen_variant;
}

tripoint_bub_ms veh_shape::get_cursor_pos() const
{
return cursor_pos;
}

bool veh_shape::set_cursor_pos( const tripoint_bub_ms &new_pos )
{
avatar &you = get_avatar();

int z = std::max( { new_pos.z(), -fov_3d_z_range, -OVERMAP_DEPTH } );
z = std::min( {z, fov_3d_z_range, OVERMAP_HEIGHT } );

if( !allow_zlevel_shift ) {
z = cursor_pos.z();
}
tripoint_bub_ms target_pos( new_pos.xy(), z );

if( cursor_allowed.find( target_pos ) == cursor_allowed.cend() ) {
return false;
}

if( z != cursor_pos.z() ) {
get_map().invalidate_map_cache( z );
}
cursor_pos = target_pos;
you.view_offset = cursor_pos.raw() - you.pos();
return true;
}

bool veh_shape::handle_cursor_movement( const std::string &action )
{
if( action == "MOUSE_MOVE" || action == "TIMEOUT" ) {
tripoint edge_scroll = g->mouse_edge_scrolling_terrain( ctxt );
set_cursor_pos( get_cursor_pos() + edge_scroll );
} else if( const std::optional<tripoint> delta = ctxt.get_direction( action ) ) {
set_cursor_pos( get_cursor_pos() + *delta ); // move cursor with directional keys
} else if( action == "zoom_in" ) {
g->zoom_in();
} else if( action == "zoom_out" ) {
g->zoom_out();
} else if( action == "SELECT" ) {
const std::optional<tripoint> mouse_pos = ctxt.get_coordinates( g->w_terrain );
if( !mouse_pos ) {
return false;
}
const tripoint_bub_ms mp( *mouse_pos );
if( get_cursor_pos() != mp ) {
set_cursor_pos( mp );
}
} else if( action == "LEVEL_UP" ) {
set_cursor_pos( get_cursor_pos() + tripoint_above );
} else if( action == "LEVEL_DOWN" ) {
set_cursor_pos( get_cursor_pos() + tripoint_below );
} else {
return false;
}

return true;
}

void veh_shape::init_input()
{
ctxt = input_context( "VEH_SHAPES", keyboard_mode::keycode );
ctxt.set_iso( true );
ctxt.register_directions();
ctxt.register_action( "CONFIRM" );
ctxt.register_action( "SELECT" );
ctxt.register_action( "QUIT" );
ctxt.register_action( "HELP_KEYBINDINGS" );
ctxt.register_action( "MOUSE_MOVE" );
ctxt.register_action( "LEVEL_UP" );
ctxt.register_action( "LEVEL_DOWN" );
ctxt.register_action( "REMOVE" );
ctxt.register_action( "zoom_out" );
ctxt.register_action( "zoom_in" );
}
Loading
Loading