diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index 14006818..047bd1c9 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -83,6 +83,22 @@ short store_talk_face_pic; int current_talk_node; extern std::vector talk_words; +// "Go Back" stack: When pressing the "Go Back" button, we don't just want to reverse through +// the nodes the player has seen, because they might change game state the first time, causing +// you to go backwards to a node that would behave differently than it did before. +// The "Go Back" stack allows you to reverse through exactly what the dialog engine has showed you, +// in a read-only fashion as long as you only go backwards. +struct dialog_history_t { + bool can_record; + + bool special_node; + // Store node numbers for shop nodes. + int node; + // Store text for normal dialog nodes. + std::pair text; +}; +std::vector dialog_history; + // Shopping vars // 1 - 499 ... regular items @@ -193,7 +209,9 @@ bool start_shop_mode(short which,short cost_adj,std::string store_name, bool can return true; } +// This is for the legacy "Go Back" button behavior, and for some reason doesn't always work: static void update_last_talk(int new_node) { + if(has_feature_flag("talk-go-back", "StackV1")) return; // Store last node in the Go Back button for(word_rect_t& word : talk_words) { if(word.word != "Go Back") continue; @@ -222,6 +240,7 @@ void end_shop_mode() { if(store_pre_shop_mode == MODE_TALKING) { save_talk_str1 = "You conclude your business."; save_talk_str2 = ""; + can_save_talk = false; place_talk_str(save_talk_str1, save_talk_str2, 0, dummy_rect); update_last_talk(TALK_BUSINESS); }else{ @@ -633,6 +652,13 @@ void set_up_shop_array() { } } +std::vector preset_words = { + "Look", "Name", "Job", + "Buy", "Sell", "Record", + "Done", "Go Back", + "Ask About...", +}; + static void reset_talk_words() { // first initialise talk_words here talk_words.clear(); @@ -642,12 +668,6 @@ static void reset_talk_words() { {210, 389}, {190, 366}, {4, 343} }; - static const std::vector preset_words = { - "Look", "Name", "Job", - "Buy", "Sell", "Record", - "Done", "Go Back", - "Ask About...", - }; TextStyle style; style.font = FONT_DUNGEON; style.pointSize = TALK_WORD_SIZE; @@ -655,6 +675,8 @@ static void reset_talk_words() { // Place buttons at bottom. for(short i = 0; i < preset_words.size(); i++) { if(talk_end_forced && i != 6 && i != 5) continue; + if(!can_save_talk && i == 5) continue; + if(has_feature_flag("talk-go-back", "StackV1") && dialog_history.size() < 2 && i == 7) continue; std::string word = preset_words[i]; location tl = preset_word_locs[i]; @@ -682,6 +704,7 @@ static void reset_talk_words() { } void start_talk_mode(short m_num,short personality,mon_num_t monst_type,short store_face_pic) { + dialog_history.clear(); rectangle area_rect; store_personality = personality; @@ -705,6 +728,7 @@ void start_talk_mode(short m_num,short personality,mon_num_t monst_type,short st overall_mode = MODE_TALKING; talk_end_forced = false; + can_save_talk = true; reset_talk_words(); stat_screen_mode = MODE_SHOP; @@ -713,9 +737,9 @@ void start_talk_mode(short m_num,short personality,mon_num_t monst_type,short st // Bring up and place first strings. save_talk_str1 = univ.town.cur_talk().people[personality % 10].look; save_talk_str2 = ""; - can_save_talk = true; place_talk_str(save_talk_str1, "", 0, dummy_rect); + dialog_history.push_back({true, false, -1, {save_talk_str1, save_talk_str2}}); put_item_screen(stat_window); give_help(5,6); @@ -799,12 +823,10 @@ static void show_job_bank(int which_bank, std::string title) { job_dlg.run(); } -void handle_talk_node(int which_talk_entry) { +void handle_talk_node(int which_talk_entry, bool is_redo) { if(which_talk_entry == TALK_DUNNO) return; - reset_talk_words(); - short get_pc,s1 = -1,s2 = -1; char asked[4]; @@ -817,35 +839,32 @@ void handle_talk_node(int which_talk_entry) { save_talk_str1 = univ.town.cur_talk().people[store_personality % 10].dunno; save_talk_str2 = ""; if(save_talk_str1.length() < 2) save_talk_str1 = "You get no response."; - place_talk_str(save_talk_str1, "", 0, dummy_rect); update_last_talk(TALK_DUNNO); - return; + goto FINISH_TALK_NODE; case TALK_BUSINESS: // This one only reachable via "go back". - place_talk_str("You conclude your business.", "", 0, dummy_rect); + can_save_talk = false; + save_talk_str1 = "You conclude your business."; save_talk_str2 = ""; update_last_talk(TALK_BUSINESS); - return; + goto FINISH_TALK_NODE; case TALK_LOOK: SPECIAL_LOOK: save_talk_str1 = univ.town.cur_talk().people[store_personality % 10].look; save_talk_str2 = ""; - place_talk_str(save_talk_str1, "", 0, dummy_rect); update_last_talk(TALK_LOOK); - return; + goto FINISH_TALK_NODE; case TALK_NAME: SPECIAL_NAME: save_talk_str1 = univ.town.cur_talk().people[store_personality % 10].name; save_talk_str2 = ""; - place_talk_str(save_talk_str1, "", 0, dummy_rect); update_last_talk(TALK_NAME); - return; + goto FINISH_TALK_NODE; case TALK_JOB: SPECIAL_JOB: save_talk_str1 = univ.town.cur_talk().people[store_personality % 10].job; save_talk_str2 = ""; - place_talk_str(save_talk_str1, "", 0, dummy_rect); update_last_talk(TALK_JOB); - return; + goto FINISH_TALK_NODE; case TALK_BUY: SPECIAL_BUY: which_talk_entry = scan_for_response("purc"); @@ -862,11 +881,6 @@ void handle_talk_node(int which_talk_entry) { if(which_talk_entry == -1) goto SPECIAL_DUNNO; break; case TALK_RECORD: - if(!can_save_talk) { - // TODO the button shouldn't be shown if it won't work, no? - play_sound(1); - return; - } if(univ.party.save_talk(univ.town->talking.people[store_personality%10].title, univ.town->name, save_talk_str1, save_talk_str2)) { give_help(57,0); play_sound(0); @@ -878,8 +892,27 @@ void handle_talk_node(int which_talk_entry) { SPECIAL_DONE: end_talk_mode(); return; - case TALK_BACK: // only if there's nothing to go back to - return; // so, there's nothing to do here + case TALK_BACK: + // In legacy behavior, this should only happen if there's nothing to go back to. In that case, do nothing. + if(has_feature_flag("talk-go-back", "StackV1")){ + if(!dialog_history.empty()){ + dialog_history_t last_state; + do{ + last_state = dialog_history.back(); + dialog_history.pop_back(); + if(last_state.special_node){ + handle_talk_node(last_state.node, true); + return; + } + // If they ended up in the same node, don't treat it as a step back until we get to different text. + }while(!dialog_history.empty() && save_talk_str1 == last_state.text.first && save_talk_str2 == last_state.text.second); + can_save_talk = last_state.can_record; + save_talk_str1 = last_state.text.first; + save_talk_str2 = last_state.text.second; + goto FINISH_TALK_NODE; + } + } + return; case TALK_ASK: // ask about save_talk_str1 = get_text_response("Ask about what?", 8); strncpy(asked, save_talk_str1.c_str(), 4); @@ -953,13 +986,13 @@ void handle_talk_node(int which_talk_entry) { save_talk_str2 = ""; break; case eTalkNode::TRAINING: + save_talk_str1 = "You conclude your training."; + save_talk_str2 = ""; + can_save_talk = false; if((get_pc = select_pc(eSelectPC::ONLY_LIVING,"Train who?")) < 6) { - can_save_talk = false; spend_xp(get_pc,1, nullptr); } - save_talk_str1 = "You conclude your training."; - return; - + goto RECORD_WHICH_NODE; case eTalkNode::SHOP: if(!start_shop_mode(b,a,save_talk_str1,true)){ if(!start_shop_mode_other_pc()){ @@ -976,7 +1009,7 @@ void handle_talk_node(int which_talk_entry) { }else{ can_save_talk = false; } - return; + goto RECORD_WHICH_NODE; case eTalkNode::JOB_BANK: if(a < univ.party.job_banks.size() && univ.party.job_banks[a].anger >= 50) { save_talk_str1 = save_talk_str2; @@ -984,26 +1017,26 @@ void handle_talk_node(int which_talk_entry) { break; } else { show_job_bank(a, save_talk_str1.c_str()); - return; + goto RECORD_WHICH_NODE; } case eTalkNode::SELL_WEAPONS: can_save_talk = false; stat_screen_mode = MODE_SELL_WEAP; put_item_screen(stat_window); give_help(42,43); - break; + goto RECORD_WHICH_NODE; case eTalkNode::SELL_ARMOR: can_save_talk = false; stat_screen_mode = MODE_SELL_ARMOR; put_item_screen(stat_window); give_help(42,43); - break; + goto RECORD_WHICH_NODE; case eTalkNode::SELL_ITEMS: can_save_talk = false; stat_screen_mode = MODE_SELL_ANY; put_item_screen(stat_window); give_help(42,43); - break; + goto RECORD_WHICH_NODE; case eTalkNode::IDENTIFY: stat_screen_mode = MODE_IDENTIFY, give_help(44, 0); @@ -1020,7 +1053,7 @@ void handle_talk_node(int which_talk_entry) { shop_recharge_limit = b; shop_recharge_amount = c; put_item_screen(stat_window); - break; + goto RECORD_WHICH_NODE; case eTalkNode::BUY_INFO: if(univ.party.gold < a) { save_talk_str1 = save_talk_str2; @@ -1087,12 +1120,11 @@ void handle_talk_node(int which_talk_entry) { univ.party.gold -= a; put_pc_screen(); iter->property = false; - save_talk_str2 = ""; } else { save_talk_str1 = "There are no horses left."; - save_talk_str2 = ""; can_save_talk = false; } + save_talk_str2 = ""; } break; case eTalkNode::BUY_SPEC_ITEM: @@ -1132,7 +1164,7 @@ void handle_talk_node(int which_talk_entry) { break; case eTalkNode::BUY_TOWN_LOC: if(univ.scenario.towns[b]->can_find) { - // TODO: Uh, is something supposed to happen here? + save_talk_str1 = "You've already learned that."; } else if(univ.party.gold < a) { save_talk_str1 = save_talk_str2; @@ -1192,9 +1224,20 @@ void handle_talk_node(int which_talk_entry) { put_item_screen(stat_window); break; } - + + if(false){ + RECORD_WHICH_NODE: + if(!is_redo) dialog_history.push_back({false, true, which_talk_entry, {"", ""}}); + place_talk_str(save_talk_str1,save_talk_str2,0,dummy_rect); + reset_talk_words(); + return; + } + +FINISH_TALK_NODE: + dialog_history.push_back({can_save_talk, false, -1, {save_talk_str1, save_talk_str2}}); place_talk_str(save_talk_str1,save_talk_str2,0,dummy_rect); reset_talk_words(); + return; } bool handle_talk_event(location p, cFramerateLimiter& fps_limiter) { diff --git a/src/game/boe.dlgutil.hpp b/src/game/boe.dlgutil.hpp index ce3b3bb9..101db89c 100644 --- a/src/game/boe.dlgutil.hpp +++ b/src/game/boe.dlgutil.hpp @@ -13,7 +13,7 @@ void handle_info_request(int what_picked); void set_up_shop_array(); void start_talk_mode(short m_num,short personality,mon_num_t monst_type,short store_face_pic); void end_talk_mode(); -void handle_talk_node(int which_talk_entry); +void handle_talk_node(int which_talk_entry, bool is_redo = false); bool handle_talk_event(location p, cFramerateLimiter& fps_limiter); void handle_talk_spec(short ttype,char* place_string1,char* place_string2); void store_responses(); diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index d987ec66..e1fe475e 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -114,6 +114,8 @@ std::map> feature_flags = { // New in-game save file picker {"file-picker-dialog", {"V1"}}, {"scenario-meta-format", {"V2"}}, + // Talk mode + {"talk-go-back", {"StackV1"}}, // Bugs required for several VoDT test replays to run faithfully {"empty-wandering-monster-bug", {"fixed"}}, {"too-many-extra-wandering-monsters-bug", {"fixed"}} diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index c6691143..39cced3d 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -941,6 +941,8 @@ void click_talk_rect(word_rect_t word) { place_talk_face(); } +extern std::vector preset_words; + // color 0 - regular 1 - darker void place_talk_str(std::string str_to_place,std::string str_to_place2,short color,rectangle c_rect) { rectangle area_rect; @@ -981,9 +983,12 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col style.colour = PRESET_WORD_OFF; else style.colour = PRESET_WORD_ON; - for(short i = 0; i < 9; i++) { - if(!talk_end_forced || i == 6 || i == 5) + int i = 0; + for(std::string preset : preset_words) { + if(talk_words[i].word == preset){ win_draw_string(talk_gworld(),talk_words[i].rect,talk_words[i].word,eTextMode::LEFT_TOP,style); + i++; + } } style.colour = Colours::NAVY;