diff --git a/README.md b/README.md index 8b35f9c..8a9a27d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Interactive character simulation game. [Resources](https://shweetz.net/files/games/feed-snivy/resources.zip) +## Clarification +This application was developed with the assistance of a large language model. + ## Build After cloning and enter the repository's directory, make sure to initialize the submodules: diff --git a/src/state/play/menu/inventory.cpp b/src/state/play/menu/inventory.cpp index 651b9f4..74425e5 100644 --- a/src/state/play/menu/inventory.cpp +++ b/src/state/play/menu/inventory.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "../../../util/color.hpp" @@ -30,7 +31,9 @@ namespace game::state::play::menu void Inventory::update(Resources& resources, ItemManager& itemManager, entity::Character& character) { - static constexpr auto INFO_CHILD_HEIGHT_MULTIPLIER = 1.0f / 3.0f; + static constexpr auto INFO_CHILD_HEIGHT_MAX_MULTIPLIER = 0.5f; + bool isSelectedItemPressed{}; + int pressedItemQuantity{-1}; auto& schema = character.data.itemSchema; auto& strings = character.data.strings; @@ -51,17 +54,19 @@ namespace game::state::play::menu auto is_able_to_upgrade_get = [&](const resource::xml::Item::Entry& item, int quantity) { return is_possible_to_upgrade_get(item) && quantity >= *item.upgradeCount; }; - auto item_summary_draw = [&](const resource::xml::Item::Entry& item, int quantity) + auto item_header_draw = [&](const resource::xml::Item::Entry& item, int quantity) + { + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + ImGui::TextWrapped("%s (x%i)", item.name.c_str(), quantity); + ImGui::PopFont(); + }; + + auto item_summary_draw = [&](const resource::xml::Item::Entry& item) { auto& category = schema.categories[item.categoryID]; auto& rarity = schema.rarities[item.rarityID]; auto durability = item.durability.value_or(schema.durability); - ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); - ImGui::TextWrapped("%s (x%i)", item.name.c_str(), quantity); - ImGui::PopFont(); - - ImGui::Separator(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); ImGui::TextWrapped("-- %s (%s) --", category.name.c_str(), rarity.name.c_str()); if (item.flavorID.has_value()) @@ -92,23 +97,29 @@ namespace game::state::play::menu ImGui::TextWrapped(strings.get(Strings::InventoryUpgradePreviewFormat).c_str(), *item.upgradeCount, schema.idToStringMap.at(*item.upgradeID).c_str()); ImGui::PopStyleColor(); - - ImGui::Separator(); }; - auto item_details_draw = [&](const resource::xml::Item::Entry& item, int quantity) - { - item_summary_draw(item, quantity); + auto info_section_separator_height_get = [&]() + { return ImGui::GetStyle().ItemSpacing.y + 1.0f; }; - if (ImGui::BeginChild("##Info Description Child", ImGui::GetContentRegionAvail())) - ImGui::TextWrapped("%s", item.description.c_str()); - ImGui::EndChild(); + auto info_section_separator_draw = [&]() + { + auto separatorHeight = info_section_separator_height_get(); + auto cursorScreenPos = ImGui::GetCursorScreenPos(); + auto width = ImGui::GetContentRegionAvail().x; + auto y = cursorScreenPos.y + separatorHeight * 0.5f; + ImGui::GetWindowDrawList()->AddLine(ImVec2(cursorScreenPos.x, y), ImVec2(cursorScreenPos.x + width, y), + ImGui::GetColorU32(ImGuiCol_Separator)); + ImGui::Dummy(ImVec2(0.0f, separatorHeight)); }; auto item_tooltip_draw = [&](const resource::xml::Item::Entry& item, int quantity) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f); - item_summary_draw(item, quantity); + item_header_draw(item, quantity); + ImGui::Separator(); + item_summary_draw(item); + ImGui::Separator(); ImGui::TextWrapped("%s", item.description.c_str()); ImGui::PopTextWrapPos(); }; @@ -122,6 +133,135 @@ namespace game::state::play::menu ImGui::PopFont(); }; + auto wrapped_text_height_get = [](std::string_view text, float wrapWidth) + { + auto safeWrapWidth = std::max(1.0f, wrapWidth); + return ImGui::CalcTextSize(text.data(), text.data() + text.size(), false, safeWrapWidth).y; + }; + + auto item_header_height_get = [&](const resource::xml::Item::Entry& item, int quantity, float width) + { + float height{}; + + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + height += wrapped_text_height_get(std::format("{} (x{})", item.name, quantity), width); + ImGui::PopFont(); + + return height; + }; + + auto item_summary_height_get = [&](const resource::xml::Item::Entry& item, float width) + { + auto& category = schema.categories[item.categoryID]; + auto& rarity = schema.rarities[item.rarityID]; + auto durability = item.durability.value_or(schema.durability); + auto itemSpacing = ImGui::GetStyle().ItemSpacing.y; + float height{}; + int lineCount{}; + + auto add_line_height = [&](std::string_view text) + { + if (lineCount > 0) height += itemSpacing; + height += wrapped_text_height_get(text, width); + lineCount++; + }; + + add_line_height(std::format("-- {} ({}) --", category.name, rarity.name)); + if (item.flavorID.has_value()) + add_line_height(std::vformat(strings.get(Strings::InventoryFlavorFormat), + std::make_format_args(schema.flavors[*item.flavorID].name))); + if (item.calories.has_value()) + add_line_height(std::vformat(strings.get(Strings::InventoryCaloriesFormat), std::make_format_args(*item.calories))); + add_line_height(std::vformat(strings.get(Strings::InventoryDurabilityFormat), std::make_format_args(durability))); + if (item.capacityBonus.has_value()) + add_line_height( + std::vformat(strings.get(Strings::InventoryCapacityBonusFormat), std::make_format_args(*item.capacityBonus))); + if (item.digestionBonus.has_value()) + { + if (*item.digestionBonus > 0) + { + auto digestionRateBonus = *item.digestionBonus * 60.0f; + add_line_height( + std::vformat(strings.get(Strings::InventoryDigestionRateBonusFormat), std::make_format_args(digestionRateBonus))); + } + else if (*item.digestionBonus < 0) + { + auto digestionRatePenalty = *item.digestionBonus * 60.0f; + add_line_height(std::vformat(strings.get(Strings::InventoryDigestionRatePenaltyFormat), + std::make_format_args(digestionRatePenalty))); + } + } + if (item.eatSpeedBonus.has_value()) + { + if (*item.eatSpeedBonus > 0) + add_line_height( + std::vformat(strings.get(Strings::InventoryEatSpeedBonusFormat), std::make_format_args(*item.eatSpeedBonus))); + else if (*item.eatSpeedBonus < 0) + add_line_height(std::vformat(strings.get(Strings::InventoryEatSpeedPenaltyFormat), + std::make_format_args(*item.eatSpeedBonus))); + } + if (is_possible_to_upgrade_get(item)) + add_line_height(std::vformat(strings.get(Strings::InventoryUpgradePreviewFormat), + std::make_format_args(*item.upgradeCount, schema.idToStringMap.at(*item.upgradeID)))); + return height; + }; + + auto item_details_height_get = [&](const resource::xml::Item::Entry& item, int quantity, float width) + { + auto separatorHeight = info_section_separator_height_get(); + auto detailBodyHeight = + std::max(item_summary_height_get(item, width), wrapped_text_height_get(item.description, width)); + return item_header_height_get(item, quantity, width) + separatorHeight + detailBodyHeight * 2.0f + + separatorHeight; + }; + + auto item_unknown_height_get = [&](float width) + { + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + auto height = wrapped_text_height_get(strings.get(Strings::InventoryUnknown), width); + ImGui::PopFont(); + return height; + }; + + auto info_child_height_get = [&](ImVec2 available, bool isItemSelected, int selectedQuantity) + { + auto isInfoVisible = isItemSelected || count() == 0; + if (!isInfoVisible) return 0.0f; + + auto infoWidth = std::max(1.0f, available.x - ImGui::GetStyle().WindowPadding.x * 2.0f); + auto infoPadding = ImGui::GetStyle().WindowPadding.y * 2.0f; + auto separatorHeight = info_section_separator_height_get(); + auto measuredBodyHeight = 0.0f; + auto isSelectedItemKnown = isItemSelected && selectedQuantity > 0; + + if (!isItemSelected) + { + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + measuredBodyHeight = wrapped_text_height_get(strings.get(Strings::InventoryEmptyHint), infoWidth); + ImGui::PopFont(); + } + else + { + auto& item = schema.items[selectedItemID]; + measuredBodyHeight = + isSelectedItemKnown ? item_details_height_get(item, selectedQuantity, infoWidth) : item_unknown_height_get(infoWidth); + } + + auto buttonChildHeight = 0.0f; + if (selectedQuantity > 0) + { + ImGui::PushFont(resources.font.get(), Font::HEADER_2); + auto buttonRowHeight = ImGui::GetFrameHeight(); + buttonChildHeight = buttonRowHeight * 2.0f + ImGui::GetStyle().ItemSpacing.y * 5.0f; + ImGui::PopFont(); + } + + auto desiredInfoChildHeight = infoPadding + separatorHeight + measuredBodyHeight + buttonChildHeight + + (buttonChildHeight > 0.0f ? separatorHeight : 0.0f); + auto maxInfoChildHeight = available.y * INFO_CHILD_HEIGHT_MAX_MULTIPLIER; + return glm::clamp(desiredInfoChildHeight, 0.0f, maxInfoChildHeight); + }; + auto item_use = [&](int itemID) { auto& item = schema.items[itemID]; @@ -232,10 +372,10 @@ namespace game::state::play::menu auto available = ImGui::GetContentRegionAvail(); auto isItemSelected = selectedItemID >= 0 && selectedItemID < (int)schema.items.size(); auto isInfoVisible = isItemSelected || inventoryCount == 0; - auto infoChildHeight = - isInfoVisible ? available.y * INFO_CHILD_HEIGHT_MULTIPLIER + ImGui::GetStyle().ItemSpacing.y * 2.0f : 0.0f; + auto infoChildHeight = info_child_height_get(available, isItemSelected, isItemSelected ? quantity_get(selectedItemID) : 0); + auto inventoryChildHeight = - isInfoVisible ? available.y - infoChildHeight - ImGui::GetStyle().ItemSpacing.y : available.y; + isInfoVisible ? std::max(0.0f, available.y - infoChildHeight - ImGui::GetStyle().ItemSpacing.y) : available.y; auto childSize = ImVec2(available.x, inventoryChildHeight); auto infoChildSize = ImVec2(available.x, infoChildHeight); @@ -277,9 +417,14 @@ namespace game::state::play::menu quantity <= 0 ? ImVec4(0, 0, 0, 0.5f) : ImVec4(1, 1, 1, 1))); if (isSelected) ImGui::PopStyleColor(3); isAnyInventoryItemHovered = isAnyInventoryItemHovered || ImGui::IsItemHovered(); - if (isPressed) selectedItemID = i; + if (isPressed) + { + isSelectedItemPressed = selectedItemID != i; + selectedItemID = i; + pressedItemQuantity = quantity; + } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && quantity > 0) item_use(i); - if (ImGui::BeginItemTooltip()) + if (!isSelected && ImGui::BeginItemTooltip()) { if (quantity > 0) item_tooltip_draw(item, quantity); @@ -316,119 +461,144 @@ namespace game::state::play::menu ImGui::EndChild(); isItemSelected = selectedItemID >= 0 && selectedItemID < (int)schema.items.size(); - auto selectedQuantity = isItemSelected ? quantity_get(selectedItemID) : 0; + isInfoVisible = isItemSelected || inventoryCount == 0; + auto selectedQuantity = + isItemSelected ? (isSelectedItemPressed && pressedItemQuantity >= 0 ? pressedItemQuantity : quantity_get(selectedItemID)) + : 0; + infoChildHeight = info_child_height_get(available, isItemSelected, selectedQuantity); + infoChildSize = ImVec2(available.x, infoChildHeight); auto isSelectedItemKnown = isItemSelected && selectedQuantity > 0; auto selectedItemHasColor = isItemSelected && schema.items[selectedItemID].color.has_value(); - if (isInfoVisible && - ImGui::BeginChild("##Info Child", infoChildSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) + if (isInfoVisible && ImGui::BeginChild("##Info Child", infoChildSize, ImGuiChildFlags_None, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { if (selectedItemHasColor) imgui::style::color_set(*schema.items[selectedItemID].color); - ImGui::Separator(); + info_section_separator_draw(); auto isButtonChildVisible = selectedQuantity > 0; ImGui::PushFont(resources.font.get(), Font::HEADER_2); auto buttonRowHeight = ImGui::GetFrameHeight(); auto buttonChildHeight = isButtonChildVisible ? buttonRowHeight * 2.0f + ImGui::GetStyle().ItemSpacing.y * 5.0f : 0.0f; - auto buttonChildSize = ImVec2(ImGui::GetContentRegionAvail().x, buttonChildHeight); - auto infoBodySize = - ImVec2(ImGui::GetContentRegionAvail().x, - ImGui::GetContentRegionAvail().y - buttonChildSize.y - - (isButtonChildVisible ? ImGui::GetStyle().ItemSpacing.y : 0.0f)); ImGui::PopFont(); + auto separatorHeight = info_section_separator_height_get(); - if (ImGui::BeginChild("##Info Content Child", infoBodySize)) + if (!isItemSelected) { - if (!isItemSelected) - { - ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); - ImGui::TextWrapped("%s", strings.get(Strings::InventoryEmptyHint).c_str()); - ImGui::PopFont(); - } - else - { - auto& item = schema.items[selectedItemID]; - - if (isSelectedItemKnown) - item_details_draw(item, selectedQuantity); - else - item_unknown_draw(); - } - } - ImGui::EndChild(); - - if (isButtonChildVisible && - ImGui::BeginChild("##Info Actions Child", buttonChildSize, ImGuiChildFlags_None, - ImGuiWindowFlags_NoScrollbar)) - { - auto& selectedItem = schema.items[selectedItemID]; - auto canUseSelectedItem = true; - auto canUpgradeSelectedItem = is_able_to_upgrade_get(selectedItem, selectedQuantity); - auto rowTwoButtonSize = row_widget_size_get(2); - - auto upgrade_item_name_get = [&]() -> std::string - { - if (!selectedItem.upgradeID.has_value()) return {}; - return schema.items.at(*selectedItem.upgradeID).name; - }; - - auto upgrade_tooltip_get = [&](bool isAll) - { - if (!is_possible_to_upgrade_get(selectedItem)) - return strings.get(Strings::InventoryUpgradeNoPath); - - auto upgradeItemName = upgrade_item_name_get(); - auto upgradeCount = *selectedItem.upgradeCount; - - if (!canUpgradeSelectedItem) - return std::vformat(strings.get(Strings::InventoryUpgradeNeedsTemplate), - std::make_format_args(upgradeCount, upgradeItemName)); - - if (!isAll) - return std::vformat(strings.get(Strings::InventoryUpgradeOneTemplate), - std::make_format_args(upgradeCount, upgradeItemName)); - - auto upgradedCount = selectedQuantity / upgradeCount; - return std::vformat(strings.get(Strings::InventoryUpgradeAllTemplate), - std::make_format_args(upgradeCount, upgradedCount, upgradeItemName)); - }; - - ImGui::Separator(); - ImGui::Dummy(ImVec2(0, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); - - ImGui::BeginDisabled(!canUseSelectedItem); - if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventorySpawnButton).c_str(), - {ImGui::GetContentRegionAvail().x, 0}))) - item_use(selectedItemID); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(!canUpgradeSelectedItem); - if (WIDGET_FX( - ImGui::Button(strings.get(Strings::InventoryUpgradeButton).c_str(), rowTwoButtonSize))) - item_upgrade(selectedItemID, false); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - { - ImGui::PushFont(ImGui::GetFont(), Font::NORMAL); - ImGui::SetItemTooltip("%s", upgrade_tooltip_get(false).c_str()); - ImGui::PopFont(); - } - ImGui::SameLine(); - if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeAllButton).c_str(), - rowTwoButtonSize))) - item_upgrade(selectedItemID, true); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - { - ImGui::PushFont(ImGui::GetFont(), Font::NORMAL); - ImGui::SetItemTooltip("%s", upgrade_tooltip_get(true).c_str()); - ImGui::PopFont(); - } - ImGui::EndDisabled(); - + ImGui::TextWrapped("%s", strings.get(Strings::InventoryEmptyHint).c_str()); ImGui::PopFont(); } - if (isButtonChildVisible) ImGui::EndChild(); + else if (isSelectedItemKnown) + { + auto& selectedItem = schema.items[selectedItemID]; + auto contentWidth = std::max(1.0f, ImGui::GetContentRegionAvail().x); + auto statsHeight = item_summary_height_get(selectedItem, contentWidth); + auto descriptionHeight = wrapped_text_height_get(selectedItem.description, contentWidth); + auto sharedDetailHeight = std::max(statsHeight, descriptionHeight); + auto headerHeight = item_header_height_get(selectedItem, selectedQuantity, contentWidth); + auto desiredInfoContentHeight = + headerHeight + separatorHeight + sharedDetailHeight * 2.0f + separatorHeight; + auto availableInfoContentHeight = + std::max(0.0f, ImGui::GetContentRegionAvail().y - (isButtonChildVisible ? separatorHeight + buttonChildHeight : 0.0f)); + auto infoContentHeight = std::min(desiredInfoContentHeight, availableInfoContentHeight); + + item_header_draw(selectedItem, selectedQuantity); + info_section_separator_draw(); + + auto detailChildHeight = sharedDetailHeight; + if (desiredInfoContentHeight > availableInfoContentHeight) + detailChildHeight = std::max(0.0f, (infoContentHeight - headerHeight - separatorHeight - separatorHeight) * 0.5f); + auto detailChildSize = ImVec2(ImGui::GetContentRegionAvail().x, detailChildHeight); + + if (ImGui::BeginChild("##Info Stats Child", detailChildSize)) + { + if (isSelectedItemPressed) ImGui::SetScrollY(0.0f); + item_summary_draw(selectedItem); + } + ImGui::EndChild(); + + info_section_separator_draw(); + + if (ImGui::BeginChild("##Info Description Child", detailChildSize)) + { + if (isSelectedItemPressed) ImGui::SetScrollY(0.0f); + ImGui::TextWrapped("%s", selectedItem.description.c_str()); + } + ImGui::EndChild(); + + if (isButtonChildVisible) + { + info_section_separator_draw(); + + if (ImGui::BeginChild("##Info Actions Child", ImVec2(ImGui::GetContentRegionAvail().x, buttonChildHeight), + ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) + { + auto canUseSelectedItem = true; + auto canUpgradeSelectedItem = is_able_to_upgrade_get(selectedItem, selectedQuantity); + auto rowTwoButtonSize = row_widget_size_get(2); + + auto upgrade_item_name_get = [&]() -> std::string + { + if (!selectedItem.upgradeID.has_value()) return {}; + return schema.items.at(*selectedItem.upgradeID).name; + }; + + auto upgrade_tooltip_get = [&](bool isAll) + { + if (!is_possible_to_upgrade_get(selectedItem)) return strings.get(Strings::InventoryUpgradeNoPath); + + auto upgradeItemName = upgrade_item_name_get(); + auto upgradeCount = *selectedItem.upgradeCount; + + if (!canUpgradeSelectedItem) + return std::vformat(strings.get(Strings::InventoryUpgradeNeedsTemplate), + std::make_format_args(upgradeCount, upgradeItemName)); + + if (!isAll) + return std::vformat(strings.get(Strings::InventoryUpgradeOneTemplate), + std::make_format_args(upgradeCount, upgradeItemName)); + + auto upgradedCount = selectedQuantity / upgradeCount; + return std::vformat(strings.get(Strings::InventoryUpgradeAllTemplate), + std::make_format_args(upgradeCount, upgradedCount, upgradeItemName)); + }; + + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + + ImGui::BeginDisabled(!canUseSelectedItem); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventorySpawnButton).c_str(), + {ImGui::GetContentRegionAvail().x, 0}))) + item_use(selectedItemID); + ImGui::EndDisabled(); + + ImGui::BeginDisabled(!canUpgradeSelectedItem); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeButton).c_str(), rowTwoButtonSize))) + item_upgrade(selectedItemID, false); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + ImGui::PushFont(ImGui::GetFont(), Font::NORMAL); + ImGui::SetItemTooltip("%s", upgrade_tooltip_get(false).c_str()); + ImGui::PopFont(); + } + ImGui::SameLine(); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeAllButton).c_str(), rowTwoButtonSize))) + item_upgrade(selectedItemID, true); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + ImGui::PushFont(ImGui::GetFont(), Font::NORMAL); + ImGui::SetItemTooltip("%s", upgrade_tooltip_get(true).c_str()); + ImGui::PopFont(); + } + ImGui::EndDisabled(); + + ImGui::PopFont(); + } + ImGui::EndChild(); + } + } + else + item_unknown_draw(); if (selectedItemHasColor) style::color_set(resources, character); } if (isInfoVisible) ImGui::EndChild();