diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 84dc8748fd..6f8ec19bb1 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -835,6 +835,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CommandManiacCallCommand(com); case static_cast(2053): //Cmd::EasyRpg_SetInterpreterFlag return CommandEasyRpgSetInterpreterFlag(com); + case static_cast(2058): //Cmd::EasyRpg_StringPicMenu + return CommandStringPicMenu(com); default: return true; } @@ -5198,3 +5200,203 @@ int Game_Interpreter::ManiacBitmask(int value, int mask) const { return value; } + +bool Game_Interpreter::CommandStringPicMenu(const lcf::rpg::EventCommand& com) { + const int mode = com.parameters[0]; + const int strpic_index = ValueOrVariable(com.parameters[1], com.parameters[2]); + const int output_var_current_item = ValueOrVariable(com.parameters[3], com.parameters[4]); + const int output_var_input_state = ValueOrVariable(com.parameters[5], com.parameters[6]); + + if (strpic_index <= 0) { + Output::Warning("CommandStringPicMenu: Requested invalid picture id ({})", strpic_index); + return true; + } + auto& window_data = Main_Data::game_windows->GetWindow(strpic_index); + + if (!window_data.window) { + Output::Warning("String Picture Menu - String Picture {} doesn't exist", strpic_index); + return true; + } + + auto& window = window_data.window; + auto& data = window_data.data; + auto& game_system = Main_Data::game_system; + + struct SystemProperties { + std::string name; + lcf::rpg::System::Stretch stretch; + lcf::rpg::System::Font font; + }; + + const SystemProperties current_system = { + ToString(game_system->GetSystemName()), + static_cast(game_system->GetMessageStretch()), + static_cast(game_system->GetFontId()) + }; + + const SystemProperties strpic_system = { + ToString(data.system_name), + static_cast(!bool(data.message_stretch)), + current_system.font + }; + + auto UpdateVariables = [&](int current_item, int input_state) { + if (output_var_current_item > 0) { + if (current_item == -2) current_item = -1; + Main_Data::game_variables->Set(output_var_current_item, current_item); + Game_Map::SetNeedRefresh(true); + } + if (output_var_input_state > 0) { + Main_Data::game_variables->Set(output_var_input_state, input_state); + Game_Map::SetNeedRefresh(true); + } + }; + + auto GetRealChoiceId = [&](int ui_choice_index, int indent) { //Gets a choice ID from a "Show Choices" command bellow this command. + auto& frame = GetFrame(); + const auto& commands = frame.commands; + auto& current_index = frame.current_command; + + if (ui_choice_index == -2) { + for (size_t idx = current_index + 1; idx < commands.size(); idx++) { + const auto& command = commands[idx]; + if (static_cast(command.code) != Cmd::ShowChoiceOption) continue; + if (command.indent != indent) break; + current_index = idx + 1; + } + return commands[current_index - 1].parameters[0]; + } + + int option_count = 0; + for (size_t idx = current_index + 1; idx < commands.size(); idx++) { + const auto& command = commands[idx]; + if (static_cast(command.code) != Cmd::ShowChoiceOption) continue; + if (command.indent != indent) break; + + if (ui_choice_index != -1 && option_count == ui_choice_index) { + current_index = idx + 1; + return command.parameters[0]; + } + option_count++; + } + + current_index++; + return ui_choice_index; + }; + + auto HandleMenuSelection = [&]() -> bool { + auto& frame = GetFrame(); + const auto& commands = frame.commands; + auto& current_index = frame.current_command; + int indent = commands[current_index].indent; + int ui_choice_index = -1; + + const auto& choice_controller = (current_index + 1 < commands.size()) ? + commands[current_index + 1] : + commands[current_index]; + + bool has_choice_list = (static_cast(choice_controller.code) == Cmd::ShowChoice); + + UpdateVariables(window->GetIndex(), 0); + + auto ProcessSelection = [&](int input_state, int sound) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(sound)); + int real_choice_id = GetRealChoiceId(ui_choice_index, indent); + UpdateVariables(ui_choice_index == -1 ? window->GetIndex() : ui_choice_index, input_state); + //Output::Debug("Selected choice: UI index {} - real Choice ID {}", ui_choice_index, real_choice_id); + window->SetActive(false); + return true; + }; + + if (Input::IsTriggered(Input::DECISION)) { + ui_choice_index = window->GetIndex(); + return ProcessSelection(1, Main_Data::game_system->SFX_Decision); + } + else if (Input::IsTriggered(Input::CANCEL)) { + if (has_choice_list) { + ui_choice_index = (choice_controller.parameters[0] == 5) ? -2 : choice_controller.parameters[0] - 1; // -2 means that cancel has a branch + if (ui_choice_index == -1) return false; + } + return ProcessSelection(-1, Main_Data::game_system->SFX_Cancel); + } + return false; + }; + + if (mode == 0) { // ENABLE MENU + if (!window->GetActive()) { + int max_item = 0; + for (const auto& text_data : data.texts) { + auto pending_message = Main_Data::game_windows->GeneratePendingMessage(ToString(text_data.text)); + const auto& lines = pending_message.GetLines(); + + bool use_substring_mode = false; + + // Detect mode by checking for "\/" in the entire text + for (const auto& line : lines) { + if (line.find("\\/") != std::string::npos) { + use_substring_mode = true; + break; + } + } + + // Now apply the appropriate counting method + for (const auto& line : lines) { + std::stringstream ss(line); + std::string sub_line; + + while (Utils::ReadLine(ss, sub_line)) { + if (use_substring_mode) { + size_t pos = 0; + int count = 0; + while ((pos = sub_line.find("\\/", pos)) != std::string::npos) { + count++; + if (count % 2 != 0) { // Check if count is odd + max_item++; + } + pos += 2; // Move past the found substring + } + } + else { + max_item++; // Increment once per sub_line in original mode + } + + // Output::Warning("{}", sub_line); + } + } + } + + window->SetItemMax(max_item); + //window->SetColumnMax(2); // TODO: is there an easy way to put text near cursor rects? + if (data.texts.empty()) { + Output::Warning("String Picture Menu - String Picture {} is not valid", strpic_index); + return true; + } + + int item_height = data.texts[0].font_size + 4; + int item_spacing = data.texts[0].line_spacing; + + window->SetMenuItemHeight(item_height); + window->SetMenuItemLineSpacing(item_spacing); + + if (window->GetIndex() == -1) window->SetIndex(0); + + window->SetActive(true); + UpdateVariables(window->GetIndex(), 0); + return false; + } + + //WORKAROUND: I have to flicker between 2 systems to set menu visuals as current stringpic menu. + game_system->SetSystemGraphic(strpic_system.name, strpic_system.stretch, strpic_system.font); + window->Update(); + game_system->SetSystemGraphic(current_system.name, current_system.stretch, current_system.font); + + window->SetStretch(strpic_system.stretch); + return HandleMenuSelection(); + } + else if (mode == 1) { // DISABLE MENU + window->SetIndex(-1); + window->SetActive(false); + } + + return true; +} diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 731aadf5cb..5e576af719 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -298,6 +298,8 @@ class Game_Interpreter bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com); bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com); + bool CommandStringPicMenu(lcf::rpg::EventCommand const& com); + int DecodeInt(lcf::DBArray::const_iterator& it); const std::string DecodeString(lcf::DBArray::const_iterator& it); lcf::rpg::MoveCommand DecodeMove(lcf::DBArray::const_iterator& it); diff --git a/src/game_windows.cpp b/src/game_windows.cpp index 2a3850722a..cdda12c741 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -46,6 +46,16 @@ Game_Windows::Window_User::Window_User(lcf::rpg::SaveEasyRpgWindow save) } +PendingMessage Game_Windows::GeneratePendingMessage(const std::string& text) { + std::stringstream ss(text); + std::string out; + PendingMessage pm(CommandCodeInserter); + while (Utils::ReadLine(ss, out)) { + pm.PushLine(out); + } + return pm; +} + void Game_Windows::SetSaveData(std::vector save) { windows.clear(); @@ -218,12 +228,7 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { fonts.emplace_back(font); std::stringstream ss(ToString(text.text)); - std::string out; - PendingMessage pm(CommandCodeInserter); - while (Utils::ReadLine(ss, out)) { - pm.PushLine(out); - } - messages.emplace_back(pm); + messages.emplace_back(GeneratePendingMessage(ToString(text.text))); } auto apply_style = [](auto& font, const auto& text) { @@ -238,94 +243,164 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { return font->ApplyStyle(style); }; - if (data.width == 0 || data.height == 0) { - // Automatic window size - int x_max = 0; - int y_max = 0; - - for (size_t i = 0; i < data.texts.size(); ++i) { - // Lots of duplication with the rendering code below but cannot be easily reduced more - auto& font = fonts[i]; - const auto& pm = messages[i]; - const auto& text = data.texts[i]; - auto style_guard = apply_style(font, text); - - int x = text.position_x; - int y = text.position_y; - for (const auto& line: pm.GetLines()) { - std::u32string line32; - auto* text_index = line.data(); - const auto* end = line.data() + line.size(); - - while (text_index != end) { - auto tret = Utils::TextNext(text_index, end, Player::escape_char); - text_index = tret.next; - - if (EP_UNLIKELY(!tret)) { - continue; - } + BitmapRef system; + // Automatic window size + int x_max = 0; + int y_max = 0; + + bool is_drawing_rect = false; + Rect current_rect; + std::vector inline_rects = {}; + + auto ProcessText = [&](ProcessTextMode mode) { + for (size_t i = 0; i < data.texts.size(); ++i) { + auto& font = fonts[i]; + const auto& pm = messages[i]; + const auto& text = data.texts[i]; + auto style_guard = apply_style(font, text); + + int x = text.position_x; + int y; + int text_color = 0; + + if (mode == ProcessTextMode::TextDrawing) { + y = text.position_y + 2; // +2 to match the offset RPG_RT uses + } + else { // SetMaxCoordinates + y = text.position_y; + } - const auto ch = tret.ch; + for (const auto& line : pm.GetLines()) { + std::u32string line32; + auto* text_index = line.data(); + const auto* end = line.data() + line.size(); - if (ch == '\n') { - if (!line32.empty()) { - x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; - line32.clear(); - } + while (text_index != end) { + auto tret = Utils::TextNext(text_index, end, Player::escape_char); + text_index = tret.next; - x_max = std::max(x, x_max); - x = 0; - y += text.font_size + text.line_spacing; + if (EP_UNLIKELY(!tret)) { + continue; + } - continue; - } + const auto ch = tret.ch; + + if (ch == '\n') { + if (!line32.empty()) { + if (mode == ProcessTextMode::TextDrawing) { + Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)); + } + else { // SetMaxCoordinates + x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; + } + line32.clear(); + } - if (Utils::IsControlCharacter(ch)) { - // control characters not handled - continue; - } + if (mode == ProcessTextMode::SetMaxCoordinates) { + x_max = std::max(x, x_max); + } + x = 0; + y += text.font_size + text.line_spacing; + continue; + } - if (tret.is_exfont) { - // exfont processed later - line32 += '$'; - } + if (Utils::IsControlCharacter(ch)) { + // control characters not handled + continue; + } - if (tret.is_escape && ch != Player::escape_char) { - if (!line32.empty()) { - x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; - line32.clear(); + if (tret.is_exfont) { + // exfont processed later + line32 += '$'; } - // Special message codes - switch (ch) { + if (tret.is_escape && ch != Player::escape_char) { + if (!line32.empty()) { + if (mode == ProcessTextMode::TextDrawing) { + x += Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)).x; + } + else { // SetMaxCoordinates + x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; + } + line32.clear(); + } + + // Special message codes + switch (ch) { case 'c': case 'C': { // Color - text_index = Game_Message::ParseColor(text_index, end, Player::escape_char, true).next; + auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); + if (mode == ProcessTextMode::TextDrawing) { + auto value = pres.value; + text_color = value > 19 ? 0 : value; + } + text_index = pres.next; + } + break; + case '/': + { + if (mode == ProcessTextMode::TextDrawing) continue; + // Toggle rect drawing mode + if (!is_drawing_rect) { + // Start drawing a new rect + current_rect.x = x; + current_rect.y = y; + is_drawing_rect = true; + } + else { + // Finish the current rect + current_rect.width = x + 1 - current_rect.x; + if (current_rect.width > x_max && x_max != 0 ) current_rect.width -=1; + current_rect.height = y - current_rect.y; + inline_rects.push_back(current_rect); + is_drawing_rect = false; + } } break; + } + continue; } - continue; + + line32 += static_cast(ch); } - line32 += static_cast(ch); - } + // After the loop, check if there's an unfinished rect + if (is_drawing_rect) { + current_rect.width = x_max -1; + current_rect.height = y; + inline_rects.push_back(current_rect); + is_drawing_rect = false; + } - if (!line32.empty()) { - x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; - } + if (!line32.empty()) { + if (mode == ProcessTextMode::TextDrawing) { + Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)); + } + else { // SetMaxCoordinates + x += Text::GetSize(*font, Utils::EncodeUTF(line32)).width; + } + } - x_max = std::max(x, x_max); + if (mode == ProcessTextMode::SetMaxCoordinates) { + x_max = std::max(x, x_max); + } - x = 0; - y += text.font_size + text.line_spacing; + x = 0; + y += text.font_size + text.line_spacing; + } + + if (mode == ProcessTextMode::SetMaxCoordinates) { + y -= text.line_spacing; + y_max = std::max(y, y_max); + } } + }; - y -= text.line_spacing; - y_max = std::max(y, y_max); - } + if (true) {//(data.width == 0 || data.height == 0) { + ProcessText(ProcessTextMode::SetMaxCoordinates); // Border size if (data.flags.border_margin) { @@ -346,15 +421,25 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { } window = std::make_unique(0, 0, data.width, data.height); + + if (!inline_rects.empty()) window->inline_rects = inline_rects; + if (!data.flags.border_margin) { window->SetBorderX(0); // FIXME: Figure out why 0 does not work here (bug in Window class) window->SetBorderY(-3); } + + if (data.width != 0) window->SetWidth(x_max); + if (data.height != 0) window->SetHeight(y_max); window->CreateContents(); + if (data.width != 0) window->SetWidth(data.width); + if (data.height != 0) window->SetHeight(data.height); + window->SetVisible(false); - BitmapRef system; + window->SetActive(false); + // FIXME: Transparency setting is currently not applied to the system graphic // Disabling transparency breaks the rendering of the system graphic if (!data.system_name.empty()) { @@ -375,84 +460,7 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { } // Draw text - for (size_t i = 0; i < data.texts.size(); ++i) { - auto& font = fonts[i]; - const auto& pm = messages[i]; - const auto& text = data.texts[i]; - auto style_guard = apply_style(font, text); - - int x = text.position_x; - int y = text.position_y + 2; // +2 to match the offset RPG_RT uses - int text_color = 0; - for (const auto& line: pm.GetLines()) { - std::u32string line32; - auto* text_index = line.data(); - const auto* end = line.data() + line.size(); - - while (text_index != end) { - auto tret = Utils::TextNext(text_index, end, Player::escape_char); - text_index = tret.next; - - if (EP_UNLIKELY(!tret)) { - continue; - } - - const auto ch = tret.ch; - - if (ch == '\n') { - if (!line32.empty()) { - Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)); - line32.clear(); - } - - x = 0; - y += text.font_size + text.line_spacing; - continue; - } - - if (Utils::IsControlCharacter(ch)) { - // control characters not handled - continue; - } - - if (tret.is_exfont) { - // exfont processed later - line32 += '$'; - } - - if (tret.is_escape && ch != Player::escape_char) { - if (!line32.empty()) { - x += Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)).x; - line32.clear(); - } - - // Special message codes - switch (ch) { - case 'c': - case 'C': - { - // Color - auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); - auto value = pres.value; - text_index = pres.next; - text_color = value > 19 ? 0 : value; - } - break; - } - continue; - } - - line32 += static_cast(ch); - } - - if (!line32.empty()) { - Text::Draw(*window->GetContents(), x, y, *font, *system, text_color, Utils::EncodeUTF(line32)); - } - - x = 0; - y += text.font_size + text.line_spacing; - } - } + ProcessText(ProcessTextMode::TextDrawing); // Add to picture auto& pic = Main_Data::game_pictures->GetPicture(data.ID); diff --git a/src/game_windows.h b/src/game_windows.h index 1d4e6f9039..bf645d0564 100644 --- a/src/game_windows.h +++ b/src/game_windows.h @@ -41,9 +41,13 @@ class Game_Windows { public: Game_Windows() = default; + static PendingMessage GeneratePendingMessage(const std::string& text); + void SetSaveData(std::vector save); std::vector GetSaveData() const; + enum class ProcessTextMode { TextDrawing, SetMaxCoordinates }; + struct WindowText { std::string text; int position_x = 0; diff --git a/src/window.cpp b/src/window.cpp index 6fc4dcba4b..bafaffe118 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -61,6 +61,8 @@ void Window::Draw(Bitmap& dst) { if (width <= 0 || height <= 0) return; if (x < -width || x > dst.GetWidth() || y < -height || y > dst.GetHeight()) return; + if (back_opacity == 0) dst.ClearRect(Rect(0, 0, width, height)); // WORKAROUND: This may be exclusive to stringPic without backgrounds. + if (windowskin) { if (width > 4 && height > 4 && (back_opacity * opacity / 255 > 0)) { if (background_needs_refresh) RefreshBackground(); diff --git a/src/window_selectable.cpp b/src/window_selectable.cpp index a414a7e260..8ae4efbab6 100644 --- a/src/window_selectable.cpp +++ b/src/window_selectable.cpp @@ -21,6 +21,7 @@ #include "input.h" #include "util_macro.h" #include "bitmap.h" +#include "output.h" constexpr int arrow_animation_frames = 20; @@ -99,6 +100,13 @@ void Window_Selectable::UpdateHelp() { // Update Cursor Rect void Window_Selectable::UpdateCursorRect() { + + bool cursor_is_inline = !inline_rects.empty() && index < static_cast(inline_rects.size()); + if (cursor_is_inline) { + InlineUpdateCursorRect(); + return; + } + int cursor_width = 0; int x = 0; if (index < 0) { @@ -116,7 +124,77 @@ void Window_Selectable::UpdateCursorRect() { x = (index % column_max * (cursor_width + 8)) - 4; int y = index / column_max * menu_item_height - oy; - SetCursorRect(Rect(x, y, cursor_width, menu_item_height)); + y += ( menu_item_line_spacing * index ) - (4 * index); // calculate spacing between lines + + int item_height = menu_item_height; + if (border_x == 0) { // When margin doesn't exist + x += 4; + cursor_width += 8; + if (index == 0) item_height += 3; + } + + SetCursorRect(Rect(x, y, cursor_width, item_height)); +} + +void Window_Selectable::InlineUpdateCursorRect() { + if (index < 0 || index >= static_cast(inline_rects.size())) { + SetCursorRect(Rect()); + return; + } + + const auto& rect = inline_rects[index]; + int cursor_width = rect.width + 9; + int x = rect.x - 4; + int y = rect.y; + + int height = this->height - border_y * 2; + int max_y = std::max_element(inline_rects.begin(), inline_rects.end(), + [](const auto& a, const auto& b) { return a.y + a.height < b.y + b.height; })->y + menu_item_height; + int max_oy = std::max(0, max_y - height); + + int target_oy = oy; + if (y < oy) { + target_oy = y; + scroll_dir = scroll_dir == 0 ? -1 : scroll_dir; + } + else if (y + menu_item_height > oy + height) { + target_oy = std::min(y + menu_item_height - height, max_oy); + scroll_dir = scroll_dir == 0 ? 1 : scroll_dir; + } + else { + scroll_dir = 0; + } + + if (scroll_dir != 0) { + scroll_progress = std::min(scroll_progress + 1, 4); + int scroll_amount = (menu_item_height * scroll_progress / 4 - menu_item_height * (scroll_progress - 1) / 4) * scroll_dir; + oy = std::clamp(oy + scroll_amount, 0, max_oy); + if (scroll_progress == 4) { + scroll_dir = 0; + scroll_progress = 0; + oy = target_oy; + } + } + else { + oy = target_oy; + } + + y -= oy; + if (border_x == 0) { + x += border_x; + cursor_width -= border_x * 2; + } + + //workaround to deal with the cursor appearing over the borders while scrolling + int output_y = scroll_dir == -1 ? y / 2 : y; + int output_height = scroll_dir == 1 ? menu_item_height / 2 : menu_item_height; + + SetCursorRect(Rect(x, output_y, cursor_width, output_height)); + + UpdateArrows(); + bool latest_rect_is_oob = inline_rects.back().y + menu_item_height != oy + height; + bool arrow_visible = (arrow_frame < arrow_animation_frames); + SetDownArrow(latest_rect_is_oob && arrow_visible); } void Window_Selectable::UpdateArrows() { @@ -134,6 +212,13 @@ void Window_Selectable::UpdateArrows() { // Update void Window_Selectable::Update() { Window_Base::Update(); + + bool cursor_is_inline = !inline_rects.empty() && index < static_cast(inline_rects.size()); + if (cursor_is_inline) { + InlineUpdate(); + return; + } + if (active && item_max > 0 && index >= 0) { if (scroll_dir != 0) { scroll_progress++; @@ -225,6 +310,97 @@ void Window_Selectable::Update() { UpdateArrows(); } +void Window_Selectable::InlineUpdate() { + if (active && !inline_rects.empty()) { + int old_index = index; + if (Input::IsRepeated(Input::DOWN) || Input::IsTriggered(Input::SCROLL_DOWN)) { + MoveIndexVertical(1); + } + if (Input::IsRepeated(Input::UP) || Input::IsTriggered(Input::SCROLL_UP)) { + MoveIndexVertical(-1); + } + if (Input::IsRepeated(Input::RIGHT)) { + MoveIndexHorizontal(1); + } + if (Input::IsRepeated(Input::LEFT)) { + MoveIndexHorizontal(-1); + } + if (index != old_index) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cursor)); + if (active && help_window != NULL) { + UpdateHelp(); + } + } + } + InlineUpdateCursorRect(); + +} + +void Window_Selectable::MoveIndexHorizontal(int direction) { + int current_x = inline_rects[index].x; + int current_y = inline_rects[index].y; + int nearest_index = -1; + int min_distance = std::numeric_limits::max(); + + for (size_t i = 0; i < inline_rects.size(); ++i) { + if (i != index && inline_rects[i].y == current_y) { + if ((direction > 0 && inline_rects[i].x > current_x) || + (direction < 0 && inline_rects[i].x < current_x)) { + int distance = std::abs(inline_rects[i].x - current_x); + if (distance < min_distance) { + min_distance = distance; + nearest_index = i; + } + } + } + } + + if (nearest_index != -1) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cursor)); + index = nearest_index; + } +} + +void Window_Selectable::MoveIndexVertical(int direction) { + int current_x = inline_rects[index].x; + int current_y = inline_rects[index].y; + int nearest_index = -1; + int min_y_distance = std::numeric_limits::max(); + int min_x = std::numeric_limits::max(); + + // First pass: find the minimum y distance in the correct direction + for (size_t i = 0; i < inline_rects.size(); ++i) { + if (i != index) { + if ((direction > 0 && inline_rects[i].y > current_y) || + (direction < 0 && inline_rects[i].y < current_y)) { + int y_distance = std::abs(inline_rects[i].y - current_y); + if (y_distance < min_y_distance) { + min_y_distance = y_distance; + } + } + } + } + + // Second pass: among buttons with minimum y distance, find the one with smallest x value + for (size_t i = 0; i < inline_rects.size(); ++i) { + if (i != index) { + if ((direction > 0 && inline_rects[i].y > current_y) || + (direction < 0 && inline_rects[i].y < current_y)) { + int y_distance = std::abs(inline_rects[i].y - current_y); + if (y_distance == min_y_distance && inline_rects[i].x < min_x) { + min_x = inline_rects[i].x; + nearest_index = i; + } + } + } + } + + if (nearest_index != -1) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cursor)); + index = nearest_index; + } +} + // Set endless scrolling state void Window_Selectable::SetEndlessScrolling(bool state) { endless_scrolling = state; @@ -235,6 +411,10 @@ void Window_Selectable::SetMenuItemHeight(int height) { menu_item_height = height; } +void Window_Selectable::SetMenuItemLineSpacing(int spacing) { + menu_item_line_spacing = spacing; +} + void Window_Selectable::SetSingleColumnWrapping(bool wrap) { wrap_limit = wrap ? 1 : 2; } diff --git a/src/window_selectable.h b/src/window_selectable.h index b676f7140d..1ba84dc885 100644 --- a/src/window_selectable.h +++ b/src/window_selectable.h @@ -30,6 +30,8 @@ class Window_Selectable: public Window_Base { public: Window_Selectable(int ix, int iy, int iwidth, int iheight); + std::vector inline_rects = {}; + /** * Creates the contents based on how many items * are currently in the window. @@ -76,6 +78,12 @@ class Window_Selectable: public Window_Base { virtual void UpdateCursorRect(); void Update() override; + void InlineUpdateCursorRect(); + void InlineUpdate(); + + void MoveIndexVertical(int direction); + void MoveIndexHorizontal(int direction); + virtual void UpdateHelp(); /** @@ -92,6 +100,13 @@ class Window_Selectable: public Window_Base { */ void SetMenuItemHeight(int height); + /** + * Sets the menu item height. + * + * @param spacing between menu items. + */ + void SetMenuItemLineSpacing(int spacing); + /** * Allow left/right input to move cursor up/down when the selectable has only one column. * By default this behaviour is only enabled for two and more columns. @@ -112,6 +127,7 @@ class Window_Selectable: public Window_Base { bool endless_scrolling = true; int menu_item_height = 16; + int menu_item_line_spacing = 4; int scroll_dir = 0; int scroll_progress = 0;