1248 lines
54 KiB
C++
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();
|
|
}
|
|
}
|