diff --git a/data/json/npcs/TALK_TEST.json b/data/json/npcs/TALK_TEST.json index 224e3a1a2c0a8..bb965f16eee5d 100644 --- a/data/json/npcs/TALK_TEST.json +++ b/data/json/npcs/TALK_TEST.json @@ -1346,5 +1346,78 @@ "id": [ "TALK_TEST_GUARD" ], "type": "talk_topic", "responses": [ { "text": "I have a custom response to a common topic", "topic": "TALK_TEST_FACTION_TRUST" } ] + }, + { + "type": "talk_topic", + "id": "TALK_TEST_SHOW_ALWAYS", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { + "condition": { "math": [ "0", ">", "1" ] }, + "text": "You should see this response but not be able to select it (except in debug mode).", + "show_always": true, + "topic": "TALK_DONE" + }, + { "text": "All done", "topic": "TALK_DONE" } + ] + }, + { + "type": "talk_topic", + "id": "TALK_TEST_SHOW_ALWAYS3", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { + "condition": { "math": [ "1", ">", "0" ] }, + "text": "You should always see this response and be able to select it.", + "show_always": true, + "topic": "TALK_DONE" + }, + { "text": "All done", "topic": "TALK_DONE" } + ] + }, + { + "type": "talk_topic", + "id": "TALK_TEST_SHOW_ALWAYS2", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { + "condition": { "math": [ "0", ">", "1" ] }, + "text": "You should see this response but not be able to select it (except in debug mode) because", + "show_always": true, + "show_reason": "zero isn't greater than one.", + "topic": "TALK_DONE" + }, + { "text": "All done", "topic": "TALK_DONE" } + ] + }, + { + "type": "talk_topic", + "id": "TALK_TEST_SHOW_CONDITION", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { + "condition": { "math": [ "0", ">", "1" ] }, + "text": "You should see this response but not be able to select it (except in debug mode) because", + "topic": "TALK_DONE", + "show_condition": { "math": [ "1", ">", "0" ] }, + "show_reason": "zero isn't greater than one." + }, + { "text": "All done", "topic": "TALK_DONE" } + ] + }, + { + "type": "talk_topic", + "id": "TALK_TEST_SHOW_CONDITION2", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { + "condition": { "math": [ "0", ">", "1" ] }, + "text": "You should not see this response.", + "topic": "TALK_DONE", + "show_condition": { "math": [ "0", ">", "1" ] }, + "show_reason": "zero isn't greater than one." + }, + { "text": "All done", "topic": "TALK_DONE" } + ] } ] diff --git a/data/raw/keybindings.json b/data/raw/keybindings.json index 87841d644e1c8..6fd1ea00fbc14 100644 --- a/data/raw/keybindings.json +++ b/data/raw/keybindings.json @@ -2897,6 +2897,15 @@ { "input_method": "keyboard_code", "mod": [ "ctrl" ], "key": "y" } ] }, + { + "type": "keybinding", + "id": "DEBUG_DIALOGUE_SHOW_ALL_RESPONSE", + "name": "Toggle debug dialogue show hidden responses ", + "bindings": [ + { "input_method": "keyboard_char", "key": "CTRL+R" }, + { "input_method": "keyboard_code", "mod": [ "ctrl" ], "key": "r" } + ] + }, { "type": "keybinding", "id": "RESET", diff --git a/doc/NPCs.md b/doc/NPCs.md index 88ddbab878a6d..cc21c1f61d81f 100644 --- a/doc/NPCs.md +++ b/doc/NPCs.md @@ -769,12 +769,21 @@ Similar to `opinion`, but adjusts the NPC's opinion of your character according ### Response Availability -#### condition +#### `condition` This is an optional condition which can be used to prevent the response under certain circumstances. If not defined, it defaults to always `true`. If the condition is not met, the response is not included in the list of possible responses. For possible content, [see Dialogue Conditions below](#dialogue-conditions) for details. -#### switch and default +#### `switch and default` The optional boolean keys "switch" and "default" are false by default. Only the first response with `"switch": true`, `"default": false`, and a valid condition will be displayed, and no other responses with `"switch": true` will be displayed. If no responses with `"switch": true` and `"default": false` are displayed, then any and all responses with `"switch": true` and `"default": true` will be displayed. In either case, all responses that have `"switch": false` (whether or not they have `"default": true` is set) will be displayed as long their conditions are satisfied. +#### `show_condition` +An optional key that, if defined and evaluates to true, will allow the response to be displayed even if it has a condition evaluating false. The response still will not be selectable if its condition is false, unless debug mode is ON. Cannot be defined if `show_always: true`. Note: do not confuse `show_condition` with `condition`. Empty by default. + +#### `show_always` +Shorthand for "show_condition": takes a boolean instead of a condition. False by default. + +#### `show_reason` +An optional key that, if defined, will append its contents to the end of a response displayed because of `show_always` or `show_condition`. Empty by default. + Example: ```json "responses": [ diff --git a/src/dialogue.h b/src/dialogue.h index cbc5a9d9159fe..fa46313078ede 100644 --- a/src/dialogue.h +++ b/src/dialogue.h @@ -189,6 +189,16 @@ struct talk_response { //copy of json_talk_response::condition, optional std::function condition; + //whether to display this response in normal gameplay even if condition is false + bool show_always = false; + //appended to response if condition fails or show_always/show_condition + std::string show_reason; + //show_always, but on show_condition being true + std::function show_condition; + + //flag to hold result of show_anyways (not read from JSON) + bool ignore_conditionals = false; + mission *mission_selected = nullptr; skill_id skill = skill_id(); matype_id style = matype_id(); @@ -256,6 +266,7 @@ struct dialogue { bool debug_conditionals = true; bool debug_effects = true; + bool debug_ignore_conditionals = false; // Methods for setting/getting misc key/value pairs. void set_value( const std::string &key, const std::string &value ); @@ -421,10 +432,11 @@ class json_talk_response return has_condition_; } bool test_condition( dialogue &d ) const; + bool show_anyways( dialogue &d ) const; /** * Callback from @ref json_talk_topic::gen_responses, see there. */ - bool gen_responses( dialogue &d, bool switch_done ) const; + bool gen_responses( dialogue &d, bool switch_done ); bool gen_repeat_response( dialogue &d, const itype_id &item_id, bool switch_done ) const; }; @@ -490,7 +502,7 @@ class json_talk_topic * @return true if built in response should excluded (not added). If false, built in * responses will be added (behind those added here). */ - bool gen_responses( dialogue &d ) const; + bool gen_responses( dialogue &d ); cata::flat_set get_directly_reachable_topics( bool only_unconditional ) const; diff --git a/src/dialogue_win.cpp b/src/dialogue_win.cpp index 8936dc2bcdb21..dd3ff2c5accd3 100644 --- a/src/dialogue_win.cpp +++ b/src/dialogue_win.cpp @@ -174,9 +174,9 @@ void dialogue_window::print_header( const std::string &name ) const int x_debug = 15 + utf8_width( name ); const int ymax = getmaxy( d_win ); const int ybar = ymax - 1 - RESPONSES_LINES - 1; - const int flag_count = 4; - std::array debug_flags = { show_dynamic_line_conditionals, show_response_conditionals, show_dynamic_line_effects, show_response_effects }; - std::array debug_show_toggle = { "DL_COND", "RESP_COND", "DL_EFF", "RESP_EFF" }; + const int flag_count = 5; + std::array debug_flags = { show_dynamic_line_conditionals, show_response_conditionals, show_dynamic_line_effects, show_response_effects, show_all_responses }; + std::array debug_show_toggle = { "DL_COND", "RESP_COND", "DL_EFF", "RESP_EFF", "ALL_RESP" }; if( debug_mode ) { for( int i = 0; i < flag_count; i++ ) { mvwprintz( d_win, point( x_debug, 1 ), debug_flags[i] ? c_yellow : c_brown, debug_show_toggle[i] ); diff --git a/src/dialogue_win.h b/src/dialogue_win.h index 2a7375da64bae..4a67416dd1aaf 100644 --- a/src/dialogue_win.h +++ b/src/dialogue_win.h @@ -56,6 +56,8 @@ class dialogue_window bool show_dynamic_line_effects = true; bool show_response_conditionals = true; bool show_response_effects = true; + //copy of dialogue::show_all_responses + bool show_all_responses = false; int sel_response = 0; std::string debug_topic_name; private: diff --git a/src/npctalk.cpp b/src/npctalk.cpp index ccd952fa0963b..39952ae055a93 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -2496,6 +2496,14 @@ talk_data talk_response::create_option_line( dialogue &d, const input_event &hot ftext = string_format( pgettext( "talk option", "[%1$s %2$d%%] %3$s" ), trial.name(), trial.calc_chance( d ), text ); } + + if( ignore_conditionals ) { + ftext = colorize( ftext, c_dark_gray ); + if( !show_reason.empty() ) { + ftext += colorize( " -- [" + show_reason + "]", c_light_red ); + } + } + if( d.actor( true )->get_npc() ) { parse_tags( ftext, *d.actor( false )->get_character(), *d.actor( true )->get_npc(), d, success.next_topic.item_type ); @@ -2535,6 +2543,12 @@ std::set talk_response::get_consequences( dialogue &d ) co return {{ success.get_consequence( d ), failure.get_consequence( d ) }}; } +bool json_talk_response::show_anyways( dialogue &d ) const +{ + return actual_response.show_always || ( actual_response.show_condition && + actual_response.show_condition( d ) ); +} + dialogue_consequence talk_effect_t::get_consequence( dialogue const &d ) const { if( d.actor( true )->check_hostile_response( opinion.anger ) ) { @@ -2629,6 +2643,7 @@ talk_topic dialogue::opt( dialogue_window &d_win, const talk_topic &topic ) ctxt.register_action( "DEBUG_DIALOGUE_RESP_CONDITIONAL" ); ctxt.register_action( "DEBUG_DIALOGUE_DL_EFFECT" ); ctxt.register_action( "DEBUG_DIALOGUE_RESP_EFFECT" ); + ctxt.register_action( "DEBUG_DIALOGUE_SHOW_ALL_RESPONSE" ); ctxt.register_action( "QUIT" ); std::vector response_lines; std::vector response_hotkeys; @@ -2680,6 +2695,11 @@ talk_topic dialogue::opt( dialogue_window &d_win, const talk_topic &topic ) generate_response_lines(); } else if( action == "CONFIRM" ) { response_ind = d_win.sel_response; + //response condition must be reverified since non-selectable responses can be displayed + talk_response &check_valid = responses[response_ind]; + if( check_valid.condition && ( !check_valid.condition( *this ) && !debug_mode ) ) { + action = "NONE"; + } } else if( action == "DEBUG_DIALOGUE_DL_CONDITIONAL" ) { d_win.show_dynamic_line_conditionals = !d_win.show_dynamic_line_conditionals; } else if( action == "DEBUG_DIALOGUE_RESP_CONDITIONAL" ) { @@ -2688,6 +2708,13 @@ talk_topic dialogue::opt( dialogue_window &d_win, const talk_topic &topic ) d_win.show_dynamic_line_effects = !d_win.show_dynamic_line_effects; } else if( action == "DEBUG_DIALOGUE_RESP_EFFECT" ) { d_win.show_response_effects = !d_win.show_response_effects; + } else if( action == "DEBUG_DIALOGUE_SHOW_ALL_RESPONSE" ) { + d_win.show_all_responses = !d_win.show_all_responses; + if( debug_mode ) { + this->debug_ignore_conditionals = !this->debug_ignore_conditionals; + gen_responses( topic ); + generate_response_lines(); + } } else if( action == "ANY_INPUT" ) { // Check real hotkeys const auto hotkey_it = std::find( response_hotkeys.begin(), @@ -2815,7 +2842,7 @@ std::vector dialogue::build_debug_info( const dialogue_window &d_wi talk_response &actual_response = responses[do_response]; std::map &debug_info = actual_response.debug_info; if( d_win.show_response_conditionals ) { - if( debug_info.find( "condition" ) != debug_info.end() && actual_response.condition ) { + if( actual_response.condition && debug_info.find( "condition" ) != debug_info.end() ) { debug_output.emplace_back( std::string( "Conditional: [" ) + ( actual_response.condition( *this ) ? colorize( "true", c_light_green ) : colorize( "false", c_light_red ) ) + std::string( "] - " ) + debug_info["condition"] ); @@ -7229,7 +7256,18 @@ talk_response::talk_response( const JsonObject &jo, const std::string_view src ) JsonObject failure_obj = jo.get_object( "failure" ); failure = talk_effect_t( failure_obj, "effect", src ); } - + if( jo.has_member( "show_always" ) ) { + show_always = jo.get_bool( "show_always" ); + } + if( jo.has_member( "show_reason" ) ) { + show_reason = jo.get_string( "show_reason" ); + } + if( jo.has_member( "show_condition" ) ) { + if( show_always ) { + jo.throw_error( "show_always and show_condition cannot both be defined" ); + } + read_condition( jo, "show_condition", show_condition, false ); + } // TODO: mission_selected // TODO: skill // TODO: style @@ -7302,10 +7340,12 @@ const talk_response &json_talk_response::get_actual_response() const return actual_response; } -bool json_talk_response::gen_responses( dialogue &d, bool switch_done ) const +bool json_talk_response::gen_responses( dialogue &d, bool switch_done ) { - if( !is_switch || !switch_done ) { - if( test_condition( d ) ) { + if( !is_switch || !switch_done || d.debug_ignore_conditionals ) { + if( test_condition( d ) || show_anyways( d ) || d.debug_ignore_conditionals ) { + actual_response.ignore_conditionals = !test_condition( d ) && ( show_anyways( d ) || + d.debug_ignore_conditionals ); d.responses.emplace_back( actual_response ); return is_switch && !is_default; } else if( !failure_explanation.empty() || !failure_topic.empty() ) { @@ -7623,12 +7663,12 @@ void json_talk_topic::load( const JsonObject &jo, const std::string_view src ) replace_built_in_responses ); } -bool json_talk_topic::gen_responses( dialogue &d ) const +bool json_talk_topic::gen_responses( dialogue &d ) { d.responses.reserve( responses.size() ); // A wild guess, can actually be more or less bool switch_done = false; - for( const json_talk_response &r : responses ) { + for( json_talk_response &r : responses ) { switch_done |= r.gen_responses( d, switch_done ); } for( const json_talk_repeat_response &repeat : repeat_responses ) {