Files
shweets-sim/src/ui.cpp
T

1248 lines
54 KiB
C++

#include "ui.h"
#include "play.h"
#include "renderer.h"
#include <array>
#include <cfloat>
#include <cstdio>
#include <imgui_internal.h>
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<std::pair<ImGuiCol, ImVec4>, 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 <class... Args> 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<Item>& 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<Character>& 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();
}
}