#include "ui.h" #include "play.h" #include "renderer.h" #include #include #include #include constexpr ImGuiWindowFlags PANEL_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; constexpr ImGuiWindowFlags BAR_FLAGS = PANEL_FLAGS | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; constexpr float COLOR_ACCENT = 2; constexpr float COLOR_ACCENT_ACTIVE = 2.25f; constexpr float COLOR_ACTIVE = 1.3f; constexpr float COLOR_BACKGROUND = 0.25f; constexpr float COLOR_BACKGROUND_ALPHA = 0.9f; constexpr float COLOR_HOVERED = 1.6f; constexpr float COLOR_MAX = 0.9f; constexpr float COLOR_MIN = 0.1f; constexpr float FX_FREQUENCY = 10; constexpr float FX_THICKNESS = 2; constexpr float FX_THICKNESS_WAVE = 3; constexpr float INDICATOR_BOB = 0.12f; constexpr float INDICATOR_SIZE = 0.35f; constexpr float INDICATOR_SPEED = 2; constexpr float ITEM_SIZE = 96; constexpr float ROUNDING = 10; constexpr float BAR_EASE = 6; constexpr float BAR_SHAKE = 2; constexpr float BAR_SHAKE_SPEED = 40; constexpr float FLOATER_SHADOW = 3; constexpr float FLOATER_SHADOW_ALPHA = 0.6f; constexpr float OVERSTUFFED_PULSE = 0.35f; constexpr float OVERSTUFFED_SPEED = 6; constexpr float STAR_WAIST = 0.3f; constexpr float BAR_WIDTH_MIN = 120; constexpr float BUTTON_SIZE_MAX = 96; constexpr float BUTTON_SIZE_MIN = 56; constexpr float CATALOG_DIVIDER = 2; constexpr float CATALOG_GRID = 0.55f; constexpr float COOLDOWN_ALPHA = 0.6f; constexpr float COOLDOWN_RADIUS = 0.42f; constexpr float ICON_FRACTION = 0.7f; constexpr float TOGGLE_FADE_FAR = 220; constexpr float TOGGLE_FADE_NEAR = 60; constexpr float TOGGLE_HEIGHT = 28; constexpr float WINDOW_FIT = 0.9f; constexpr float WINDOW_HEIGHT = 680; constexpr float WINDOW_WIDTH = 1000; constexpr float TASKBAR_FIT = 0.8f; constexpr int BUTTONS_FIXED = 9; constexpr float INFO_GAP = 4; constexpr int COOLDOWN_SEGMENTS = 48; constexpr const char* PANEL_ICONS[] = {"", "stats", "catalog", "settings", "cheats"}; constexpr const char* PANEL_NAMES[] = {"", "Stats", "Catalog", "Settings", "Cheats"}; constexpr float PREVIEW_FRACTION = 0.68f; constexpr float PREVIEW_WIDTH = 760; constexpr float SELECT_BUTTON_SIZE = 112; constexpr int SELECT_BUTTONS_FIXED = 1; constexpr float KG_TO_G = 1000; constexpr float KG_TO_LB = 2.20462f; constexpr float LB_TO_OZ = 16; constexpr float OSCILLATION_AMPLITUDE = 2; constexpr float OSCILLATION_FREQUENCY = 1; constexpr float OSCILLATION_SPREAD = 0.16f; constexpr float SCROLLBAR_SIZE = 30; constexpr float SHAKE_AMPLITUDE = 1; constexpr float SHAKE_FREQUENCY = 30; constexpr float SLIDE_EASE = 2; constexpr float SLIDE_TIME = 0.125f; constexpr float TEXT_BIG = 1.25f; constexpr float TEXT_HEIGHT_LINES = 5; constexpr float TEXT_SMALL = 0.75f; constexpr float TEXT_WIDTH = 0.5f; constexpr float TOOLTIP_WRAP = 24; constexpr float TRIANGLE_SIZE = 0.22f; constexpr float UNDISCOVERED_ALPHA = 0.5f; constexpr int PALETTE_SIZE = 25; constexpr int SECONDS_PER_HOUR = 3600; constexpr int SECONDS_PER_MINUTE = 60; constexpr int TEXT_SIZE = 512; constexpr const char* UNKNOWN = "???"; using Palette = std::array, PALETTE_SIZE>; // Every widget color from one color: darker backgrounds, lighter hovers, brighter accents. static Palette palette_get(Color color) { auto shade = [&](float amount, float alpha = 1) { return ImVec4(color.r * amount, color.g * amount, color.b * amount, alpha); }; auto bright = [&](float amount) { return ImVec4(std::clamp(color.r * amount, COLOR_MIN, COLOR_MAX), std::clamp(color.g * amount, COLOR_MIN, COLOR_MAX), std::clamp(color.b * amount, COLOR_MIN, COLOR_MAX), 1); }; return {{{ImGuiCol_Button, shade(1)}, {ImGuiCol_FrameBg, shade(1)}, {ImGuiCol_Header, shade(1)}, {ImGuiCol_Tab, shade(1)}, {ImGuiCol_TitleBg, shade(1)}, {ImGuiCol_TitleBgCollapsed, shade(1)}, {ImGuiCol_TitleBgActive, shade(1)}, {ImGuiCol_ScrollbarBg, shade(1)}, {ImGuiCol_FrameBgHovered, bright(COLOR_HOVERED)}, {ImGuiCol_HeaderHovered, bright(COLOR_HOVERED)}, {ImGuiCol_ButtonHovered, bright(COLOR_HOVERED)}, {ImGuiCol_TabHovered, bright(COLOR_HOVERED)}, {ImGuiCol_ScrollbarGrab, bright(COLOR_HOVERED)}, {ImGuiCol_ScrollbarGrabHovered, bright(COLOR_HOVERED)}, {ImGuiCol_FrameBgActive, bright(COLOR_ACTIVE)}, {ImGuiCol_HeaderActive, bright(COLOR_ACTIVE)}, {ImGuiCol_ButtonActive, bright(COLOR_ACTIVE)}, {ImGuiCol_TabSelected, bright(COLOR_ACTIVE)}, {ImGuiCol_ScrollbarGrabActive, shade(COLOR_ACCENT)}, {ImGuiCol_PlotHistogram, shade(COLOR_ACCENT)}, {ImGuiCol_CheckMark, shade(COLOR_ACCENT)}, {ImGuiCol_SliderGrab, shade(COLOR_ACCENT)}, {ImGuiCol_SliderGrabActive, shade(COLOR_ACCENT_ACTIVE)}, {ImGuiCol_WindowBg, shade(COLOR_BACKGROUND, COLOR_BACKGROUND_ALPHA)}, {ImGuiCol_ChildBg, shade(COLOR_BACKGROUND, 0)}}}; } void style_set(Color color) { ImGuiStyle& style = ImGui::GetStyle(); style.WindowRounding = style.FrameRounding = style.GrabRounding = style.PopupRounding = ROUNDING; style.ScrollbarSize = SCROLLBAR_SIZE; style.FontSizeBase = FONT_NORMAL; for (auto [id, value] : palette_get(color)) style.Colors[id] = value; style.Colors[ImGuiCol_PopupBg] = style.Colors[ImGuiCol_WindowBg]; } // Colors widgets from one color until style_pop. static void style_push(Color color) { for (auto [id, value] : palette_get(color)) ImGui::PushStyleColor(id, value); } static void style_pop() { ImGui::PopStyleColor(PALETTE_SIZE); } static ImVec4 imvec4_get(Color color) { return {color.r, color.g, color.b, color.a}; } static ImTextureID texture_id_get(const Texture& texture) { return (ImTextureID)(intptr_t)texture.id; } static float row_width_get(int count) { return (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * (count - 1)) / count; } static void tooltip_set(const char* text) { ImGui::PushFont(nullptr, FONT_NORMAL); ImGui::SetItemTooltip("%s", text); ImGui::PopFont(); } static void triangle_draw(ImVec2 position, ImVec2 size, Direction direction) { ImVec2 center = {position.x + size.x / 2, position.y + size.y / 2}; float half = size.y * TRIANGLE_SIZE; float sign = direction == DIRECTION_UP ? -1 : 1; ImGui::GetWindowDrawList()->AddTriangleFilled({center.x, center.y + half * sign}, {center.x - half * 2, center.y - half * sign}, {center.x + half * 2, center.y - half * sign}, ImGui::GetColorU32(ImGuiCol_Text)); } template static std::string text_format(const char* format, Args... args) { char text[TEXT_SIZE]; std::snprintf(text, sizeof(text), format, args...); return text; } // A weight in the chosen units, with thousands separators, dropping to grams or ounces below one. static std::string weight_format(float kilograms, Measurement measurement) { bool isImperial = measurement == MEASUREMENT_IMPERIAL; float value = isImperial ? kilograms * KG_TO_LB : kilograms; const char* unit = isImperial ? "lbs" : "kg"; if (value < 1) { value *= isImperial ? LB_TO_OZ : KG_TO_G; unit = isImperial ? "oz" : "g"; } std::string digits = text_format("%.2f", value); for (int i = (int)digits.find('.') - 3; i > 0; i -= 3) digits.insert(i, ","); return digits + " " + unit; } static std::string playtime_format(float seconds) { int total = std::max(0, (int)seconds); if (total < SECONDS_PER_MINUTE) return text_format("%02d", total); if (total < SECONDS_PER_HOUR) return text_format("%02d:%02d", total / SECONDS_PER_MINUTE, total % SECONDS_PER_MINUTE); return text_format("%02d:%02d:%02d", total / SECONDS_PER_HOUR, total / SECONDS_PER_MINUTE % SECONDS_PER_MINUTE, total % SECONDS_PER_MINUTE); } // Renders an actor onto a canvas, scaled to fit within size, and gives the size it takes up. static ImVec2 canvas_draw(Renderer& renderer, Canvas& canvas, Actor& actor, ImVec2 size) { Rect rect = actor.bounds_get(); float scale = is_rect_valid(rect) ? std::min(size.x / rect.w, size.y / rect.h) : 0; ImVec2 image = {rect.w * scale, rect.h * scale}; if (!(scale > 0) || !std::isfinite(image.x) || !std::isfinite(image.y)) return {}; renderer.canvas_set(&canvas, std::max(1, (int)image.x), std::max(1, (int)image.y)); actor.render(renderer, {scale, 0, 0, scale, -rect.x * scale, -rect.y * scale}); renderer.flush(); return image; } static float hash_unit(Uint32 value) { value ^= value >> 16; value *= 0x7feb352du; value ^= value >> 15; value *= 0x846ca68bu; value ^= value >> 16; return (value & 0xffffu) / 65535.0f; } // The scale, color and offset a dialogue line's effects give its character at an index. static float letter_style_get(const Line& line, int at, float time, ImU32& color, ImVec2& offset) { float scale = 1; color = ImGui::GetColorU32(ImGuiCol_Text); offset = {}; for (const Effect& effect : line.effects) { if (at < effect.start || at >= effect.end) continue; Uint32 seed = (Uint32)std::floor(time * SHAKE_FREQUENCY) * 747796405u + (Uint32)at * 2891336453u; if (effect.type == EFFECT_COLOR) color = ImGui::GetColorU32(imvec4_get({effect.color.r, effect.color.g, effect.color.b, 1})); else if (effect.type == EFFECT_BIG || effect.type == EFFECT_SMALL) scale = effect.type == EFFECT_BIG ? TEXT_BIG : TEXT_SMALL; else if (effect.type == EFFECT_WAVE) offset.y += std::sin((time * OSCILLATION_FREQUENCY + at * OSCILLATION_SPREAD) * 2 * PI) * OSCILLATION_AMPLITUDE; else if (effect.type == EFFECT_SHAKE) { offset.x += (hash_unit(seed) * 2 - 1) * SHAKE_AMPLITUDE; offset.y += (hash_unit(seed ^ 0xa511e9b3u) * 2 - 1) * SHAKE_AMPLITUDE; } } return scale; } // Draws a dialogue line's first count characters with their effects, wrapping whole words to the window. static void letters_draw(const Line& line, int count, float time) { ImFont* font = ImGui::GetFont(); float fontSize = ImGui::GetFontSize(); ImVec2 start = ImGui::GetCursorScreenPos(); float wrap = start.x + ImGui::GetContentRegionAvail().x; ImVec2 cursor = start; float lineHeight = fontSize; const char* end = line.text.data() + line.text.size(); auto width_get = [&](const char* from, const char* to, int at) { ImU32 color; ImVec2 offset; return font->CalcTextSizeA(fontSize * letter_style_get(line, at, time, color, offset), FLT_MAX, 0, from, to).x; }; auto line_break = [&]() { cursor = {start.x, cursor.y + lineHeight}; lineHeight = fontSize; }; int at = 0; for (const char* it = line.text.data(); it < end && at < count; at++) { unsigned int codepoint = 0; const char* next = it + ImTextCharFromUtf8(&codepoint, it, end); if (codepoint == '\n') { line_break(); it = next; continue; } bool isSpace = codepoint == ' '; if (!isSpace && cursor.x > start.x && it[-1] == ' ') { float wordWidth = 0; int wordAt = at; for (const char* letter = it; letter < end && *letter != ' ' && *letter != '\n'; wordAt++) { const char* letterNext = letter + ImTextCharFromUtf8(&codepoint, letter, end); wordWidth += width_get(letter, letterNext, wordAt); letter = letterNext; } if (cursor.x + wordWidth > wrap && wordWidth <= wrap - start.x) line_break(); } ImU32 color; ImVec2 offset; float scale = letter_style_get(line, at, time, color, offset); float width = width_get(it, next, at); if (cursor.x + width > wrap) { line_break(); if (isSpace) { it = next; continue; } } ImGui::GetWindowDrawList()->AddText(font, fontSize * scale, {cursor.x + offset.x, cursor.y + offset.y}, color, it, next); cursor.x += width; lineHeight = std::max(lineHeight, fontSize * scale); it = next; } ImGui::Dummy({wrap - start.x, cursor.y + lineHeight - start.y}); } static void item_summary_draw(Character& character, const Item& item) { ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY)); std::string rarity = item.rarity >= 0 ? " (" + character.catalog.rarities[item.rarity].name + ")" : ""; if (!item.category.empty()) ImGui::TextWrapped("-- %s%s --", item.category.c_str(), rarity.c_str()); if (!item.flavor.empty()) ImGui::TextWrapped("Flavor: %s", item.flavor.c_str()); if (item.calories != 0) ImGui::TextWrapped("%0.0f kcal", item.calories); if (item.capacityBonus != 0) ImGui::TextWrapped("Capacity Bonus: +%0.0f kcal", item.capacityBonus); if (item.digestionBonus > 0) ImGui::TextWrapped("Digestion Rate Bonus: +%0.2f%% / sec", item.digestionBonus * TICK_RATE); if (item.digestionBonus < 0) ImGui::TextWrapped("Digestion Rate Penalty: %0.2f%% / sec", item.digestionBonus * TICK_RATE); ImGui::PopStyleColor(); } // An item's name, summary and description, or question marks for one not found yet. static void item_details_draw(Character& character, const Item* item) { ImGui::PushTextWrapPos(ImGui::GetFontSize() * TOOLTIP_WRAP); ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::TextWrapped("%s", item ? item->name.c_str() : UNKNOWN); ImGui::PopFont(); ImGui::Separator(); if (item) { item_summary_draw(character, *item); ImGui::Separator(); } ImGui::TextWrapped("%s", item ? item->description.c_str() : UNKNOWN); ImGui::PopTextWrapPos(); } static void slide_update(float& slide, bool isOpen) { float step = TICK / SLIDE_TIME; slide = std::clamp(slide + (isOpen ? step : -step), 0.0f, 1.0f); } // Passes a widget's result through after giving it the character's hover and select sounds and a pulsing border. bool Ui::is_fx(bool result) { ImGuiStorage* storage = ImGui::GetStateStorage(); ImGuiID id = ImGui::GetItemID(); const Sounds* hover = character ? &character->sounds[SOUND_HOVER] : nullptr; if (ImGui::IsItemActivated() && character) audio->play(character->sounds[SOUND_SELECT]); if (!ImGui::IsItemHovered()) { if (id) storage->SetBool(id, false); return result; } if (id && !storage->GetBool(id) && hover) audio->play(*hover); if (id) storage->SetBool(id, true); float wave = std::sin((float)ImGui::GetTime() * FX_FREQUENCY); ImVec4 border = ImGui::GetStyleColorVec4(ImGuiCol_CheckMark); border.w = 1 + wave; ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImGui::GetColorU32(border), ImGui::GetStyle().FrameRounding, 0, FX_THICKNESS + FX_THICKNESS_WAVE * wave); return result; } void Ui::command_add(CommandType type, const std::string& key) { commands->push_back({key, type}); } // An icon from the character's icon sheet (see Character::icons), fit into a square and tinted. void Ui::icon_draw(const char* name, ImVec2 position, float size, ImU32 tint) { Anm2* icons = character && character->icons ? character->icons : &iconsDefault; const Anm2Region* region = icons ? icons->region_get(0, name) : nullptr; Texture* sheet = region && !icons->sheets.empty() ? icons->sheets[0] : nullptr; if (!sheet) return; float scale = size / std::max(region->size.x, region->size.y); ImVec2 extent = {region->size.x * scale, region->size.y * scale}; ImVec2 min = {position.x + (size - extent.x) / 2, position.y + (size - extent.y) / 2}; ImGui::GetWindowDrawList()->AddImage( texture_id_get(*sheet), min, {min.x + extent.x, min.y + extent.y}, {region->crop.x / sheet->width, region->crop.y / sheet->height}, {(region->crop.x + region->size.x) / sheet->width, (region->crop.y + region->size.y) / sheet->height}, tint); } // A square button showing an icon. Active ones stay lit; disabled ones are dimmed and ignore clicks. bool Ui::icon_button(const char* icon, const char* tooltip, ButtonState state) { ImGui::PushID(icon); if (state == BUTTON_ACTIVE) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered)); ImGui::BeginDisabled(state == BUTTON_DISABLED); bool isPressed = is_fx(ImGui::Button("##Icon", {buttonSize, buttonSize})); ImVec2 min = ImGui::GetItemRectMin(); float inset = buttonSize * (1 - ICON_FRACTION) / 2; icon_draw(icon, {min.x + inset, min.y + inset}, buttonSize * ICON_FRACTION, ImGui::GetColorU32(ImGuiCol_Text)); ImGui::EndDisabled(); if (state == BUTTON_ACTIVE) ImGui::PopStyleColor(); ImGui::SetItemTooltip("%s", tooltip); ImGui::PopID(); return isPressed; } // A clockwise wedge over the last item, filled as far as progress (0 to 1) has come. static void cooldown_draw(float progress) { ImVec2 min = ImGui::GetItemRectMin(); ImVec2 max = ImGui::GetItemRectMax(); ImVec2 center = {(min.x + max.x) / 2, (min.y + max.y) / 2}; float radius = (max.x - min.x) * COOLDOWN_RADIUS; ImU32 color = ImGui::GetColorU32(ImGuiCol_PlotHistogram, COOLDOWN_ALPHA); int segments = (int)std::ceil(COOLDOWN_SEGMENTS * progress); for (int i = 0; i < segments; i++) { float from = -PI / 2 + 2 * PI * progress * i / segments; float to = -PI / 2 + 2 * PI * progress * (i + 1) / segments; ImGui::GetWindowDrawList()->AddTriangleFilled( center, {center.x + std::cos(from) * radius, center.y + std::sin(from) * radius}, {center.x + std::cos(to) * radius, center.y + std::sin(to) * radius}, color); } } void Ui::sound_play(SoundId id) { if (character) audio->play(character->sounds[id]); } // The bar along the bottom: the character's name and body at a glance, then a button for everything. A thin bar above // it, showing as the mouse nears, slides it away and back. void Ui::taskbar_draw(Play& play, ImVec2 screen) { Character& data = *play.character; Body& body = play.body; Stats& stats = body.stats; Items& items = play.items; Dialogue& dialogue = data.dialogue; ImGuiStyle& style = ImGui::GetStyle(); std::string moodPool = data.stage_name_get(body.stage); const char* labels[] = {"Weight", "Capacity", "Digestion"}; bool isMax = body.stage >= (int)data.stages.size(); float digestion = stats.isDigesting ? (float)stats.digestionTimer / std::max(1, data.traits.digestionTime) : stats.digestionProgress / DIGESTION_MAX; std::string weight = weight_format(stats.weight, *measurement); std::string next = weight_format(body.stage_threshold_get(body.stage + 1), *measurement); std::string unit = next.substr(next.find(' ')); if (weight.ends_with(unit)) weight.resize(weight.size() - unit.size()); std::string bars[] = {isMax ? std::string("MAX") : weight + " / " + next, text_format("%.0f / %.0f kcal", stats.calories, stats.capacity), stats.isDigesting ? std::string("Digesting...") : text_format("%.0f%%", digestion * PERCENT)}; float fills[] = {body.stage_progress_get(), stats.calories / std::max(1.0f, body.capacity_max_get()), digestion}; float overstuffed = std::clamp((stats.calories - stats.capacity) / (body.capacity_max_get() - stats.capacity), 0.0f, 1.0f); float widths[std::size(labels)]; float used = 0; for (int i = 0; i < (int)std::size(labels); i++) { widths[i] = std::max(BAR_WIDTH_MIN, ImGui::CalcTextSize(bars[i].c_str()).x + style.FramePadding.x * 4); used += ImGui::CalcTextSize(labels[i]).x + style.ItemSpacing.x + widths[i]; } int count = BUTTONS_FIXED + (int)data.interactTypes.size() + play.isCheats; float spacing = (count - 1) * style.ItemSpacing.x; buttonSize = std::clamp((screen.x * TASKBAR_FIT - style.WindowPadding.x * 2 - spacing) / count, BUTTON_SIZE_MIN, BUTTON_SIZE_MAX); float nameWidth = ImGui::CalcTextSize(data.name.c_str()).x; float rowWidth = count * buttonSize + spacing; float infoWidth = nameWidth + used + style.ItemSpacing.x * INFO_GAP * std::size(labels); float width = std::max(rowWidth, infoWidth) + style.WindowPadding.x * 2; float height = style.WindowPadding.y * 2 + ImGui::GetFrameHeight() + style.ItemSpacing.y * 2 + buttonSize; slide_update(taskbarSlide, play.isTaskbarOpen && !play.text.isScene); float shown = std::pow(taskbarSlide, SLIDE_EASE) * height; float x = (screen.x - width) / 2; float y = screen.y - shown; play.inset = shown; float mouseY = ImGui::GetIO().MousePos.y; float distance = std::abs(mouseY - (y - TOGGLE_HEIGHT / 2)); float alpha = std::clamp((TOGGLE_FADE_FAR - distance) / (TOGGLE_FADE_FAR - TOGGLE_FADE_NEAR), 0.0f, 1.0f); if (!play.text.isScene && alpha > 0) { ImGui::SetNextWindowPos({x, y - TOGGLE_HEIGHT}); ImGui::SetNextWindowSize({width, TOGGLE_HEIGHT}); ImGui::SetNextWindowBgAlpha(0); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2()); if (ImGui::Begin("##Taskbar Toggle", nullptr, BAR_FLAGS)) { ImVec2 position = ImGui::GetCursorScreenPos(); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); bool isPressed = is_fx(ImGui::Button("##Toggle", {width, TOGGLE_HEIGHT})); ImGui::PopStyleColor(); if (isPressed) { play.isTaskbarOpen = !play.isTaskbarOpen; play.audio->play(data.sounds[play.isTaskbarOpen ? SOUND_OPEN : SOUND_CLOSE]); } triangle_draw({position.x + width / 2 - TOGGLE_HEIGHT / 2, position.y}, {TOGGLE_HEIGHT, TOGGLE_HEIGHT}, play.isTaskbarOpen ? DIRECTION_DOWN : DIRECTION_UP); } ImGui::End(); ImGui::PopStyleVar(3); } if (taskbarSlide <= 0) return; ImGui::SetNextWindowPos({x, y}); ImGui::SetNextWindowSize({width, height}); if (!ImGui::Begin("##Taskbar", nullptr, BAR_FLAGS)) return ImGui::End(); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(data.name.c_str()); float gap = std::max(style.ItemSpacing.x * INFO_GAP, (ImGui::GetContentRegionAvail().x - nameWidth - used) / std::size(labels)); for (int i = 0; i < (int)std::size(labels); i++) { ImGui::SameLine(0, gap); if (i > 0) { ImVec2 cursor = ImGui::GetCursorScreenPos(); cursor.x -= gap / 2; ImGui::GetWindowDrawList()->AddLine(cursor, {cursor.x, cursor.y + ImGui::GetFrameHeight()}, ImGui::GetColorU32(ImGuiCol_Separator)); } ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(labels[i]); ImGui::SameLine(); float time = (float)ImGui::GetTime(); barFills[i] += (fills[i] - barFills[i]) * std::min(1.0f, BAR_EASE * ImGui::GetIO().DeltaTime); if (i == 1 && overstuffed > 0) { float red = 1 - overstuffed * (1 - OVERSTUFFED_PULSE * (1 + std::sin(time * OVERSTUFFED_SPEED)) / 2); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, red, red, 1)); ImVec2 cursor = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos({cursor.x + std::sin(time * BAR_SHAKE_SPEED) * BAR_SHAKE * overstuffed, cursor.y}); } ImGui::ProgressBar(barFills[i], {widths[i], 0}, bars[i].c_str()); if (i == 1 && overstuffed > 0) ImGui::PopStyleColor(); if (i == 0 && ImGui::BeginItemTooltip()) { ImGui::Text("Stage: %i/%i (%0.1f%%)", body.stage + 1, (int)data.stages.size() + 1, fills[0] * PERCENT); ImGui::EndTooltip(); } if (i == 2 && ImGui::BeginItemTooltip()) { ImGui::TextUnformatted(stats.isDigesting ? "Digestion in progress..." : digestion <= 0 ? "Give food to start digesting!" : "Digestion"); ImGui::TextColored(imvec4_get(GRAY), "Rate: %0.2f%% / sec", stats.digestionRate * TICK_RATE); ImGui::EndTooltip(); } } ImGui::Separator(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width - style.WindowPadding.x * 2 - rowWidth) / 2); auto state_get = [](bool isEnabled, bool isActive = false) { return !isEnabled ? BUTTON_DISABLED : isActive ? BUTTON_ACTIVE : BUTTON_NORMAL; }; if (icon_button("talk", "Talk", state_get(dialogue.is_hook(HOOK_RANDOM)))) play.text.set(play, dialogue.hook_get(HOOK_RANDOM)); ImGui::SameLine(); if (icon_button("mood", "Mood", state_get(dialogue.pools.contains(moodPool)))) play.text.set(play, dialogue.pool_get(moodPool)); ImGui::SameLine(); std::string spawnTooltip = items.spawnDelay > 0 ? text_format("Next item in %0.2fs", items.spawnDelay * TICK) : std::string("Spawn Item"); if (icon_button("spawn", spawnTooltip.c_str(), state_get(items.spawnDelay <= 0 && items.is_spawn_possible(play)))) items.spawn_queue_add(play); if (items.spawnDelay > 0) cooldown_draw(1 - std::clamp((float)items.spawnDelay / items.spawn_delay_get(play), 0.0f, 1.0f)); ImGui::SameLine(); Autofeed& autofeed = play.autofeed; if (icon_button("autofeed", autofeed.isEnabled ? "Return to manual interaction." : "Serve the character food and assist in digestion automatically.", state_get(autofeed.is_available(play), autofeed.isEnabled))) autofeed.isEnabled = !autofeed.isEnabled; ImGui::SameLine(); if (icon_button("home", "Reset camera view.\n(Shortcut: Home)", BUTTON_NORMAL)) play.focus({screen.x, screen.y}); for (int i = 0; i < (int)data.interactTypes.size(); i++) { InteractArea& type = data.interactTypes[i]; ImGui::SameLine(); ImGui::PushID(i); const char* tooltip = autofeed.isEnabled ? "Disabled while Autofeed is active!" : type.name.c_str(); if (icon_button(type.icon.c_str(), tooltip, state_get(!autofeed.isEnabled, play.cursorType == i))) play.cursorType = i; ImGui::PopID(); } Panel panels[] = {PANEL_STATS, PANEL_CATALOG, PANEL_SETTINGS, PANEL_CHEATS}; for (Panel each : panels) { if (each == PANEL_CHEATS && !play.isCheats) continue; ImGui::SameLine(); if (icon_button(PANEL_ICONS[each], PANEL_NAMES[each], BUTTON_NORMAL)) panel = each; } ImGui::SameLine(); if (icon_button("help", "Help", state_get(dialogue.is_hook(HOOK_HELP)))) play.text.set(play, dialogue.hook_get(HOOK_HELP)); ImGui::End(); } void Ui::text_draw(Play& play, ImVec2 screen) { Text& text = play.text; Entry* entry = text.entry; Line* line = text.line_get(); bool isChoosing = entry && text.is_last_line() && !entry->choices.empty(); if (!entry || (line->text.empty() && !isChoosing)) return; ImGuiStyle& style = ImGui::GetStyle(); ImGui::PushFont(nullptr, FONT_DIALOGUE); ImVec2 size = {screen.x * TEXT_WIDTH, ImGui::GetTextLineHeightWithSpacing() * TEXT_HEIGHT_LINES}; ImGui::SetNextWindowSize(size); ImGui::SetNextWindowPos( {screen.x / 2 - size.x / 2, screen.y - play.inset - TOGGLE_HEIGHT - size.y - style.WindowPadding.y}); if (ImGui::Begin("##Text", nullptr, PANEL_FLAGS)) { bool isHovered = ImGui::IsWindowHovered(); bool isSpace = !play.isBlocked && ImGui::IsKeyReleased(ImGuiKey_Space); bool isAdvance = (isHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) || isSpace; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {style.ItemSpacing.x, 0}); if (ImGui::BeginTabBar("##Name")) { if (ImGui::BeginTabItem(play.character->name.c_str())) ImGui::EndTabItem(); ImGui::EndTabBar(); } letters_draw(*line, text.index, (float)ImGui::GetTime()); ImVec2 position = ImGui::GetWindowPos(); ImVec2 contentMin = {position.x + style.WindowPadding.x, position.y + style.WindowPadding.y}; ImVec2 contentMax = {position.x + ImGui::GetWindowWidth() - style.WindowPadding.x, position.y + ImGui::GetWindowHeight() - style.WindowPadding.y}; if (text.isFinished && isChoosing) { float buttonY = contentMax.y - ImGui::GetFrameHeight(); float separatorY = buttonY - style.ItemSpacing.y; ImGui::GetWindowDrawList()->AddLine({contentMin.x, separatorY}, {contentMax.x, separatorY}, ImGui::GetColorU32(ImGuiCol_Separator)); ImGui::SetCursorScreenPos({contentMin.x, buttonY}); ImVec2 buttonSize = {row_width_get((int)entry->choices.size()), 0}; int chosen = isHovered && isSpace ? 0 : -1; for (int i = 0; i < (int)entry->choices.size(); i++) { ImGui::PushID(i); if (is_fx(ImGui::Button(entry->choices[i].text.c_str(), buttonSize))) chosen = i; ImGui::SetItemTooltip("%s", entry->choices[i].text.c_str()); ImGui::PopID(); if (i + 1 < (int)entry->choices.size()) ImGui::SameLine(); } if (chosen == 0 && isSpace) text.advance(play); else if (chosen >= 0) text.choose(play, chosen); } else { if (text.isFinished && (!text.is_last_line() || !entry->next.empty())) { float side = ImGui::GetTextLineHeightWithSpacing(); ImVec2 center = {contentMax.x - side / 2 + std::sin((float)ImGui::GetTime() * INDICATOR_SPEED) * side * INDICATOR_BOB, contentMax.y - side / 2}; float half = side * INDICATOR_SIZE; ImGui::GetWindowDrawList()->AddTriangleFilled({center.x + half, center.y}, {center.x - half, center.y - half}, {center.x - half, center.y + half}, ImGui::GetColorU32(ImGuiCol_Text)); } if (isAdvance) text.advance(play); } ImGui::PopStyleVar(); } ImGui::End(); ImGui::PopFont(); } // The flourishes over the world: sparks, rising numbers (in the character's color, shadowed so they read on anything), // and the screen's flash. void Ui::effects_draw(Play& play, ImVec2 screen) { Effects& effects = play.effects; Affine view = play.camera.view_get({screen.x, screen.y}); ImDrawList* drawList = ImGui::GetBackgroundDrawList(); for (Spark& spark : effects.sparks) { Vec2 at = view * spark.position; float fade = (float)spark.life / std::max(1, spark.lifeMax); float size = spark.size * view.a * fade; ImU32 color = ImGui::GetColorU32(ImVec4(spark.color.r, spark.color.g, spark.color.b, spark.color.a * fade)); if (spark.shape == SPARK_CIRCLE) { drawList->AddCircleFilled({at.x, at.y}, size, color); continue; } if (spark.shape == SPARK_LINE) { Vec2 end = view * spark.end; drawList->AddLine({at.x, at.y}, {end.x, end.y}, color, size); continue; } float waist = size * STAR_WAIST; drawList->AddQuadFilled({at.x, at.y - size}, {at.x + waist, at.y}, {at.x, at.y + size}, {at.x - waist, at.y}, color); drawList->AddQuadFilled({at.x - size, at.y}, {at.x, at.y - waist}, {at.x + size, at.y}, {at.x, at.y + waist}, color); } ImFont* font = ImGui::GetFont(); for (Floater& floater : effects.floaters) { std::string text = floater.type == FLOATER_CALORIES ? text_format("+%.0f kcal", floater.value) : "+" + weight_format(floater.value, *measurement); float alpha = std::min(1.0f, (float)floater.life / (FLOATER_LIFE / 2)); Color tint = play.character->color; ImVec4 color = {tint.r, tint.g, tint.b, alpha}; ImVec2 size = font->CalcTextSizeA(FONT_HEADER_2, FLT_MAX, 0, text.c_str()); Vec2 at = view * floater.position; ImVec2 position = {at.x - size.x / 2, at.y - size.y / 2}; drawList->AddText(font, FONT_HEADER_2, {position.x + FLOATER_SHADOW, position.y + FLOATER_SHADOW}, ImGui::GetColorU32(ImVec4(0, 0, 0, alpha * FLOATER_SHADOW_ALPHA)), text.c_str()); drawList->AddText(font, FONT_HEADER_2, position, ImGui::GetColorU32(color), text.c_str()); } if (effects.flash > 0) drawList->AddRectFilled({0, 0}, screen, ImGui::GetColorU32(ImVec4(1, 1, 1, effects.flash))); } // Fading messages stacked down from the top right, newest first. void Ui::toasts_draw(Play& play) { ImGuiStyle& style = ImGui::GetStyle(); ImGuiViewport* viewport = ImGui::GetMainViewport(); ImDrawList* drawList = ImGui::GetForegroundDrawList(); float height = ImGui::GetTextLineHeightWithSpacing() + style.WindowPadding.y * 2; float right = viewport->WorkPos.x + viewport->WorkSize.x - style.WindowPadding.x; for (int i = 0; i < (int)play.toasts.size(); i++) { Toast& toast = play.toasts[i]; auto color_get = [&](ImGuiCol id) { ImVec4 color = ImGui::GetStyleColorVec4(id); color.w = (float)toast.life / TOAST_LIFE; return ImGui::GetColorU32(color); }; float width = ImGui::CalcTextSize(toast.message.c_str()).x + style.WindowPadding.x * 2; float age = play.toasts.size() - 1 - i; ImVec2 position = {right - width, viewport->WorkPos.y + style.WindowPadding.y + height * age}; ImVec2 end = {right, position.y + height}; drawList->AddRectFilled(position, end, color_get(ImGuiCol_WindowBg), style.WindowRounding); drawList->AddRect(position, end, color_get(ImGuiCol_Border), style.WindowRounding); drawList->AddText({position.x + style.WindowPadding.x, position.y + style.WindowPadding.y}, color_get(ImGuiCol_Text), toast.message.c_str()); } } void Ui::settings_draw(Settings& settings) { ImGui::SeparatorText("Measurement System"); is_fx(ImGui::RadioButton("Metric", (int*)&settings.measurement, MEASUREMENT_METRIC)); ImGui::SetItemTooltip("Use kilograms/grams (kg/g)."); ImGui::SameLine(); is_fx(ImGui::RadioButton("Imperial", (int*)&settings.measurement, MEASUREMENT_IMPERIAL)); ImGui::SetItemTooltip("Use pounds/ounces (lbs/oz)."); ImGui::SeparatorText("Window"); const char* modes[] = {"Windowed", "Fullscreen"}; int mode = isFullscreen; if (is_fx(ImGui::Combo("Mode", &mode, modes, (int)std::size(modes)))) command_add(COMMAND_FULLSCREEN); ImGui::SetItemTooltip("Switch between windowed and fullscreen display.\n(Shortcut: F)"); ImGui::SeparatorText("Sound"); ImGui::BeginDisabled(settings.isMuted); if (is_fx(ImGui::SliderInt("Volume", &settings.volume, 0, (int)PERCENT, "%d%%"))) audio->gain_set(settings.volume / PERCENT); ImGui::SetItemTooltip("Adjust master volume."); ImGui::EndDisabled(); ImGui::SameLine(); if (is_fx(ImGui::Checkbox("Mute", &settings.isMuted))) audio->gain_set(settings.isMuted ? 0 : settings.volume / PERCENT); ImGui::SetItemTooltip("Mute all audio.\n(Shortcut: M)"); ImGui::SeparatorText("Appearance"); bool isColorChanged = is_fx(ImGui::Checkbox("Use Character Color", &settings.isCharacterColor)); ImGui::SetItemTooltip("When playing, the UI will use the character's preset UI color."); ImGui::SameLine(); ImGui::BeginDisabled(settings.isCharacterColor); isColorChanged |= is_fx(ImGui::ColorEdit3("Color", &settings.color.r, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip)); ImGui::SetItemTooltip("Change the UI color."); ImGui::EndDisabled(); ImGui::Separator(); if (is_fx(ImGui::Button("Reset to Default", {-FLT_MIN, 0}))) { settings.reset(); audio->gain_set(settings.volume / PERCENT); isColorChanged = true; } if (isColorChanged) style_set(character && settings.isCharacterColor ? character->color : settings.color); if (!character) return; ImGui::Separator(); if (is_fx(ImGui::Button("Save", {-FLT_MIN, 0}))) command_add(COMMAND_SAVE); ImGui::SetItemTooltip("Save the game.\n(Note: the game autosaves frequently.)"); if (is_fx(ImGui::Button("Return to Characters", {-FLT_MIN, 0}))) command_add(COMMAND_MENU); ImGui::SetItemTooltip("Go back to the character selection screen.\nProgress will be saved."); } // The body in full: weight and stage, calories and digestion, and totals. void Ui::stats_draw(Play& play) { Character& data = *play.character; Body& body = play.body; Stats& stats = body.stats; bool isMax = body.stage >= (int)data.stages.size(); ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::SeparatorText("Weight"); ImGui::PopFont(); ImGui::Text("%s (Stage %i/%i)", weight_format(stats.weight, *measurement).c_str(), body.stage + 1, (int)data.stages.size() + 1); ImGui::ProgressBar(body.stage_progress_get(), {-FLT_MIN, 0}, isMax ? "MAX" : "To Next Stage"); ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY)); if (isMax) ImGui::TextUnformatted("Maxed out!"); else ImGui::Text("Start: %s Next: %s", weight_format(body.stage_threshold_get(body.stage), *measurement).c_str(), weight_format(body.stage_threshold_get(body.stage + 1), *measurement).c_str()); ImGui::PopStyleColor(); ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::SeparatorText("Digestion"); ImGui::PopFont(); ImGui::Text("%.0f / %.0f kcal", stats.calories, stats.capacity); ImGui::SameLine(0, 0); ImGui::TextColored(imvec4_get(GRAY), " (up to %.0f)", body.capacity_max_get()); ImGui::Text("Rate: %0.2f%% / sec", stats.digestionRate * TICK_RATE); ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::SeparatorText("Stats"); ImGui::PopFont(); ImGui::Text("Total Calories Consumed: %0.0f kcal", stats.totalCalories); ImGui::Text("Total Items Spawned: %i", stats.totalSpawned); ImGui::Text("Total Items Consumed: %i", stats.totalConsumed); ImGui::Text("Total Playtime: %s", playtime_format(stats.playtime).c_str()); } // Sliders setting the body's stats directly, unlocked by the cheat code. void Ui::cheats_draw(Play& play) { Character& data = *play.character; Body& body = play.body; Stats& stats = body.stats; Traits& traits = data.traits; is_fx(ImGui::SliderFloat("Calories", &stats.calories, 0, body.capacity_max_get(), "%0.0f kcal")); is_fx(ImGui::SliderFloat("Capacity", &stats.capacity, traits.capacity, traits.capacityMax, "%0.0f kcal")); if (is_fx(ImGui::SliderFloat("Weight", &stats.weight, traits.weight, traits.weightMax, "%0.2f kg"))) { body.stage_set(body.stage_get(stats.weight)); body.idle_play(); } int stage = body.stage + 1; if (is_fx(ImGui::SliderInt("Stage", &stage, 1, (int)data.stages.size() + 1))) { body.stage_set(stage - 1); stats.weight = body.stage_threshold_get(body.stage); body.idle_play(); } float rate = stats.digestionRate * TICK_RATE; if (is_fx(ImGui::SliderFloat("Digestion Rate", &rate, traits.digestionRateMin * TICK_RATE, traits.digestionRateMax * TICK_RATE, "%0.2f%% / sec"))) stats.digestionRate = rate / TICK_RATE; if (is_fx(ImGui::Button("Digest"))) stats.digestionProgress = DIGESTION_MAX; } // The items as a grid of buttons, beside the selected one's details. Icons are drawn once per character. void Ui::catalog_draw(Play& play) { Character& data = *play.character; std::vector& items = data.catalog.items; if (catalogCharacter != &data || itemCanvases.size() != items.size()) { catalogCharacter = &data; selectedItem = -1; itemCanvases.resize(items.size()); itemSizes.resize(items.size()); for (int i = 0; i < (int)items.size(); i++) { Actor actor = data.item_actor_get(i); itemSizes[i] = canvas_draw(*renderer, itemCanvases[i], actor, {ITEM_SIZE, ITEM_SIZE}); } } ImGuiStyle& style = ImGui::GetStyle(); ImVec2 available = ImGui::GetContentRegionAvail(); float cell = ITEM_SIZE + style.FramePadding.x * 2; int columns = std::max(1, (int)((available.x * CATALOG_GRID + style.ItemSpacing.x) / (cell + style.ItemSpacing.x))); float gridWidth = columns * (cell + style.ItemSpacing.x) - style.ItemSpacing.x + style.ScrollbarSize + style.WindowPadding.x; if (ImGui::BeginChild("##Items", {gridWidth, available.y})) { for (int i = 0; i < (int)items.size(); i++) { ImGui::PushID(i); bool isFound = play.items.discovered[i]; bool isColored = isFound && items[i].color; if (isColored) style_push(*items[i].color); bool isCurrent = i == selectedItem; if (isCurrent) for (ImGuiCol id : {ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive}) ImGui::PushStyleColor(id, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered)); ImVec2 image = itemSizes[i]; ImVec2 padding = style.FramePadding; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {padding.x + (ITEM_SIZE - image.x) / 2, padding.y + (ITEM_SIZE - image.y) / 2}); ImVec4 tint = isFound ? ImVec4(1, 1, 1, 1) : ImVec4(0, 0, 0, UNDISCOVERED_ALPHA); if (is_fx(ImGui::ImageButton("##Item", texture_id_get(itemCanvases[i].texture), image, {0, 0}, {1, 1}, {}, tint))) selectedItem = i; ImGui::PopStyleVar(); if (isCurrent) ImGui::PopStyleColor(3); if (isColored) style_pop(); if ((i + 1) % columns != 0 && i + 1 < (int)items.size()) ImGui::SameLine(); ImGui::PopID(); } } ImGui::EndChild(); ImGui::SameLine(); ImVec2 cursor = ImGui::GetCursorScreenPos(); ImGui::GetWindowDrawList()->AddLine(cursor, {cursor.x, cursor.y + available.y}, ImGui::GetColorU32(ImGuiCol_Separator), CATALOG_DIVIDER); ImGui::Dummy({CATALOG_DIVIDER, available.y}); ImGui::SameLine(); bool isSelected = selectedItem >= 0 && selectedItem < (int)items.size(); bool isFound = isSelected && play.items.discovered[selectedItem]; bool isColored = isFound && items[selectedItem].color; if (isColored) style_push(*items[selectedItem].color); if (ImGui::BeginChild("##Details", {0, available.y})) { if (items.empty()) ImGui::TextWrapped("No items."); else if (isSelected) { ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::TextWrapped("%s", isFound ? items[selectedItem].name.c_str() : UNKNOWN); ImGui::PopFont(); ImGui::Separator(); if (isFound) { item_summary_draw(data, items[selectedItem]); ImGui::Separator(); } ImGui::TextWrapped("%s", isFound ? items[selectedItem].description.c_str() : UNKNOWN); } } ImGui::EndChild(); if (isColored) style_pop(); } // Starts the open window (one at a time, from the taskbar), fixed in size above the bottom inset. Its contents follow, // then window_end. bool Ui::window_begin(ImVec2 screen, float inset) { if (panel == PANEL_NONE) return false; const char* title = PANEL_NAMES[panel]; float above = screen.y - inset - TOGGLE_HEIGHT; ImVec2 size = {std::min(WINDOW_WIDTH, screen.x * WINDOW_FIT), std::min(WINDOW_HEIGHT, above * WINDOW_FIT)}; ImGui::SetNextWindowSize(size); ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f}); if (!ImGui::IsPopupOpen(title)) ImGui::OpenPopup(title); isWindowOpen = true; if (ImGui::BeginPopupModal(title, &isWindowOpen, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) return true; panel = PANEL_NONE; return false; } // Ends the open window, closing it on its X, escape or a click off it. void Ui::window_end() { ImVec2 min = ImGui::GetWindowPos(); ImVec2 max = {min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()}; bool isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); bool isClickedOff = isFocused && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(min, max, false); if (ImGui::IsKeyPressed(ImGuiKey_Escape) || isClickedOff) isWindowOpen = false; if (!isWindowOpen) { ImGui::CloseCurrentPopup(); panel = PANEL_NONE; sound_play(SOUND_CLOSE); } ImGui::EndPopup(); } void Ui::play_update(Play& play, Settings& settings) { character = play.character; measurement = &settings.measurement; ImVec2 screen = ImGui::GetIO().DisplaySize; effects_draw(play, screen); if (play.items.tooltip >= 0 && !play.isBlocked && ImGui::BeginTooltip()) { item_details_draw(*character, &character->catalog.items[play.items.tooltip]); ImGui::EndTooltip(); } taskbar_draw(play, screen); toasts_draw(play); text_draw(play, screen); if (play.text.isScene) panel = PANEL_NONE; play.isBlocked = panel != PANEL_NONE; if (!window_begin(screen, play.inset)) return; if (panel == PANEL_STATS) stats_draw(play); else if (panel == PANEL_CATALOG) catalog_draw(play); else if (panel == PANEL_CHEATS) cheats_draw(play); else settings_draw(settings); window_end(); } void Ui::select_update(std::vector& characters, Settings& settings) { ImGuiStyle& style = ImGui::GetStyle(); ImVec2 screen = ImGui::GetIO().DisplaySize; character = nullptr; measurement = &settings.measurement; buttonSize = SELECT_BUTTON_SIZE; characterIndex = characters.empty() ? -1 : std::clamp(characterIndex, 0, (int)characters.size() - 1); Character* selected = characterIndex >= 0 ? &characters[characterIndex] : nullptr; if (selected && selectedIndex != characterIndex) { selectedStats = {}; isSelectedSaved = is_fields_read(selected->save_path_get(), selectedStats); selectedIndex = characterIndex; } int count = (int)characters.size() + SELECT_BUTTONS_FIXED; float width = count * buttonSize + (count - 1) * style.ItemSpacing.x + style.ItemSpacing.x * INFO_GAP + style.WindowPadding.x * 2; float height = buttonSize + style.WindowPadding.y * 2; ImGui::SetNextWindowPos({(screen.x - width) / 2, screen.y - height}); ImGui::SetNextWindowSize({width, height}); if (ImGui::Begin("##Select Bar", nullptr, BAR_FLAGS)) { for (int i = 0; i < (int)characters.size(); i++) { Character& entry = characters[i]; ImGui::PushID(i); if (i > 0) ImGui::SameLine(); style_push(entry.color); bool isCurrent = i == characterIndex; if (isCurrent) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered)); ImVec2 size = {buttonSize, buttonSize}; bool isPressed = entry.portrait ? ImGui::ImageButton("##Portrait", texture_id_get(*entry.portrait), {size.x - style.FramePadding.x * 2, size.y - style.FramePadding.y * 2}) : ImGui::Button(entry.name.c_str(), size); if (is_fx(isPressed)) characterIndex = i; if (isCurrent) ImGui::PopStyleColor(); ImGui::SetItemTooltip("%s", entry.name.c_str()); style_pop(); ImGui::PopID(); } float gap = style.ItemSpacing.x * (INFO_GAP + 1); ImGui::SameLine(0, gap); ImVec2 cursor = ImGui::GetCursorScreenPos(); cursor.x -= gap / 2; ImGui::GetWindowDrawList()->AddLine(cursor, {cursor.x, cursor.y + buttonSize}, ImGui::GetColorU32(ImGuiCol_Separator)); if (icon_button("settings", "Settings", BUTTON_NORMAL)) panel = PANEL_SETTINGS; } ImGui::End(); if (selected) { float above = screen.y - height; ImVec2 size = {std::min(PREVIEW_WIDTH, screen.x * WINDOW_FIT), above * WINDOW_FIT}; style_push(selected->color); ImGui::SetNextWindowSize(size); ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f}); if (ImGui::Begin("##Preview", nullptr, PANEL_FLAGS)) { if (ImGui::BeginTabBar("##Preview Tabs")) { if (is_fx(ImGui::BeginTabItem("In Game"))) { isPreviewRender = false; ImGui::EndTabItem(); } if (is_fx(ImGui::BeginTabItem("Render"))) { isPreviewRender = true; ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImVec2 available = {ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y * PREVIEW_FRACTION}; ImVec2 shown = {}; ImTextureID texture = {}; if (isPreviewRender && selected->render) { Texture& render = *selected->render; float scale = std::min(available.x / render.width, available.y / render.height); shown = {render.width * scale, render.height * scale}; texture = texture_id_get(render); } else { Anm2* anm2 = selected->anm2_get(ANM2_PATH, *renderer, *audio); if (previewActor.anm2 != anm2 || !anm2) previewActor = Actor(anm2); previewActor.update(*audio); shown = canvas_draw(*renderer, previewCanvas, previewActor, available); texture = texture_id_get(previewCanvas.texture); } ImVec2 start = ImGui::GetCursorPos(); ImGui::SetCursorPos({start.x + (available.x - shown.x) / 2, start.y + (available.y - shown.y) / 2}); if (shown.x > 0) ImGui::Image(texture, shown); ImGui::SetCursorPos({start.x, start.y + available.y}); ImGui::Separator(); ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::TextUnformatted(selected->name.c_str()); ImGui::PopFont(); ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY)); ImGui::TextWrapped("%s", selected->description.c_str()); ImGui::PopStyleColor(); float weight = isSelectedSaved ? selectedStats.weight : selected->traits.weight; ImGui::Text("Weight: %s Stages: %i", weight_format(weight, settings.measurement).c_str(), (int)selected->stages.size() + 1); ImGui::SetCursorPosY(ImGui::GetWindowHeight() - style.WindowPadding.y - ImGui::GetFrameHeight()); ImVec2 buttonSize = {row_width_get(isSelectedSaved ? 2 : 1), 0}; if (isSelectedSaved) { if (is_fx(ImGui::Button("Continue", buttonSize))) command_add(COMMAND_CONTINUE, selected->key); tooltip_set("Continue from a saved game."); ImGui::SameLine(); } if (is_fx(ImGui::Button("New Game", buttonSize))) { if (isSelectedSaved) ImGui::OpenPopup("New Game Warning"); else command_add(COMMAND_NEW_GAME, selected->key); isNewGameWarning = isSelectedSaved; } tooltip_set("Start a new game.\nThis will delete progress!"); ImGui::SetNextWindowSize({size.x, 0}); ImGui::SetNextWindowPos({screen.x / 2, screen.y / 2}, ImGuiCond_Always, {0.5f, 0.5f}); if (ImGui::BeginPopupModal("New Game Warning", &isNewGameWarning, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) { ImGui::TextWrapped("This will delete saved progress! Are you sure?"); ImVec2 popupSize = {row_width_get(2), 0}; if (is_fx(ImGui::Button("Yes", popupSize))) command_add(COMMAND_NEW_GAME, selected->key); ImGui::SameLine(); if (is_fx(ImGui::Button("No", popupSize))) isNewGameWarning = false; ImGui::EndPopup(); } } ImGui::End(); style_pop(); } if (window_begin(screen, height)) { settings_draw(settings); window_end(); } }