Taskbar with icon buttons, stat bars and modal stats, catalog and settings windows

This commit is contained in:
shweet committed 2026-11-12 23:20:18 -05:00
1 parent f900a0d01f
commit 37e87f168a
15 files changed
+500 -445

No files matched your search

+379 -361
View File
@@ -24,10 +24,27 @@ constexpr float INDICATOR_BOB = 0.12f;
constexpr float INDICATOR_SIZE = 0.35f;
constexpr float INDICATOR_SPEED = 2;
constexpr float ITEM_SIZE = 96;
constexpr float BAR_WIDTH_MIN = 120;
constexpr float BUTTON_SIZE = 64;
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_SIZE = 48;
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 int BUTTONS_FIXED = 9;
constexpr float INFO_GAP = 4;
constexpr int COOLDOWN_SEGMENTS = 48;
constexpr const char* PANEL_ICONS[] = {"", "stats", "catalog", "settings"};
constexpr const char* PANEL_NAMES[] = {"", "Stats", "Catalog", "Settings"};
constexpr float KG_TO_G = 1000;
constexpr float KG_TO_LB = 2.20462f;
constexpr float LB_TO_OZ = 16;
constexpr float MENU_WIDTH = 0.3f;
constexpr float OSCILLATION_AMPLITUDE = 2;
constexpr float OSCILLATION_FREQUENCY = 1;
constexpr float OSCILLATION_SPREAD = 0.16f;
@@ -37,11 +54,9 @@ 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_FIT_MIN = 1;
constexpr float TEXT_HEIGHT_LINES = 6;
constexpr float TEXT_HEIGHT_LINES = 5;
constexpr float TEXT_SMALL = 0.75f;
constexpr float TEXT_WIDTH = 0.3f;
constexpr float TOOLS_WIDTH = 0.05f;
constexpr float TEXT_WIDTH = 0.4f;
constexpr float TOOLTIP_WRAP = 24;
constexpr float TRIANGLE_SIZE = 0.22f;
constexpr float UNDISCOVERED_ALPHA = 0.5f;
@@ -94,8 +109,9 @@ static Palette palette_get(Color color)
void style_set(Color color, float rounding)
{
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = style.FrameRounding = style.GrabRounding = rounding;
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];
@@ -138,11 +154,11 @@ static void tooltip_set(const char* text)
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.x * TRIANGLE_SIZE;
float sign = direction == DIRECTION_LEFT ? -1 : 1;
ImGui::GetWindowDrawList()->AddTriangleFilled({center.x + half * sign, center.y},
{center.x - half * sign, center.y - half},
{center.x - half * sign, center.y + half},
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));
}
@@ -181,21 +197,6 @@ static std::string playtime_format(float seconds)
total % SECONDS_PER_MINUTE);
}
// Draws text into bounds, shrinking its font until it fits.
static void text_fit_draw(const std::string& text, ImVec2 bounds, float size)
{
ImFont* font = ImGui::GetFont();
ImVec2 measured = font->CalcTextSizeA(size, FLT_MAX, 0, text.c_str());
if (measured.x > 0 && measured.y > 0)
size = std::clamp(size * std::min({1.0f, bounds.x / measured.x, bounds.y / measured.y}), TEXT_FIT_MIN, size);
measured = font->CalcTextSizeA(size, FLT_MAX, 0, text.c_str());
ImVec2 position = ImGui::GetCursorScreenPos();
ImGui::Dummy(bounds);
ImVec4 clip = {position.x, position.y, position.x + bounds.x, position.y + bounds.y};
ImGui::GetWindowDrawList()->AddText(font, size, {position.x, position.y + std::max(0.0f, (bounds.y - measured.y) / 2)},
ImGui::GetColorU32(ImGuiCol_Text), text.c_str(), nullptr, 0, &clip);
}
// 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)
{
@@ -387,34 +388,236 @@ void Ui::command_add(CommandType type, const std::string& key)
commands->push_back({key, type});
}
// A side panel's bar, which opens and closes it. Hovering the bar opens it too.
void Ui::bar_draw(Play& play, bool& isOpen, float slide, ImVec2 position, ImVec2 size, Direction direction,
const char* openTooltip, const char* closeTooltip)
// 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)
{
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(position);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2());
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, size);
if (ImGui::Begin(direction == DIRECTION_LEFT ? "##Menu Bar" : "##Tools Bar", nullptr, BAR_FLAGS))
Anm2* icons = character ? character->icons : nullptr;
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", {BUTTON_SIZE, BUTTON_SIZE}));
ImVec2 min = ImGui::GetItemRectMin();
float inset = (BUTTON_SIZE - ICON_SIZE) / 2;
icon_draw(icon, {min.x + inset, min.y + inset}, ICON_SIZE, 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++)
{
ImVec2 buttonSize = ImGui::GetContentRegionAvail();
ImVec2 cursor = ImGui::GetCursorScreenPos();
bool isPressed = is_fx(ImGui::Button("##Toggle", buttonSize));
if (slide <= 0 || slide >= 1)
{
tooltip_set(isOpen ? closeTooltip : openTooltip);
if (isPressed || (!isOpen && slide <= 0 && ImGui::IsItemHovered()))
{
isOpen = !isOpen;
play.audio->play(play.character->sounds[isOpen ? SOUND_OPEN : SOUND_CLOSE]);
}
}
triangle_draw(cursor, buttonSize, (direction == DIRECTION_LEFT) != isOpen ? DIRECTION_LEFT : DIRECTION_RIGHT);
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::window_open(Play& play, Panel next)
{
panel = next;
play.audio->play(play.character->sounds[SOUND_OPEN]);
}
// 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 = body.stage <= 0
? data.dialoguePool
: data.stages[std::min(body.stage, (int)data.stages.size()) - 1].dialoguePool;
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();
float nameWidth = ImGui::CalcTextSize(data.name.c_str()).x;
float rowWidth = count * BUTTON_SIZE + (count - 1) * style.ItemSpacing.x;
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 + BUTTON_SIZE;
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 / 2);
if (i > 0)
{
ImVec2 cursor = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddLine(cursor, {cursor.x, cursor.y + ImGui::GetFrameHeight()},
ImGui::GetColorU32(ImGuiCol_Separator));
}
ImGui::SameLine(0, gap / 2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(labels[i]);
ImGui::SameLine();
if (i == 1 && overstuffed > 0)
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, 1 - overstuffed, 1 - overstuffed, 1));
ImGui::ProgressBar(fills[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};
for (Panel each : panels)
{
ImGui::SameLine();
if (icon_button(PANEL_ICONS[each], PANEL_NAMES[each], BUTTON_NORMAL))
window_open(play, 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();
ImGui::PopStyleVar(4);
}
void Ui::text_draw(Play& play, ImVec2 screen)
@@ -426,14 +629,15 @@ void Ui::text_draw(Play& play, ImVec2 screen)
if (!entry || (line->text.empty() && !isChoosing))
return;
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 size = {screen.x * TEXT_WIDTH - style.WindowPadding.x * 2,
ImGui::GetTextLineHeightWithSpacing() * TEXT_HEIGHT_LINES};
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 - size.y - style.WindowPadding.y});
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 = ImGui::IsKeyReleased(ImGuiKey_Space);
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"))
@@ -442,7 +646,6 @@ void Ui::text_draw(Play& play, ImVec2 screen)
ImGui::EndTabItem();
ImGui::EndTabBar();
}
ImGui::PushFont(nullptr, FONT_NORMAL);
letters_draw(*line, text.index, (float)ImGui::GetTime());
ImVec2 position = ImGui::GetWindowPos();
@@ -490,10 +693,10 @@ void Ui::text_draw(Play& play, ImVec2 screen)
if (isAdvance)
text.advance(play);
}
ImGui::PopFont();
ImGui::PopStyleVar();
}
ImGui::End();
ImGui::PopFont();
}
void Ui::toasts_draw(Play& play)
@@ -502,6 +705,7 @@ void Ui::toasts_draw(Play& play)
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImDrawList* drawList = ImGui::GetForegroundDrawList();
float height = ImGui::GetTextLineHeightWithSpacing() + style.WindowPadding.y * 2;
float bottom = viewport->WorkPos.y + viewport->WorkSize.y - play.inset - TOGGLE_HEIGHT - style.WindowPadding.y;
for (int i = 0; i < (int)play.toasts.size(); i++)
{
Toast& toast = play.toasts[i];
@@ -510,9 +714,7 @@ void Ui::toasts_draw(Play& play)
color.w = (float)toast.life / TOAST_LIFE;
return ImGui::GetColorU32(color);
};
ImVec2 position = {viewport->WorkPos.x + style.WindowPadding.x,
viewport->WorkPos.y + viewport->WorkSize.y - style.WindowPadding.y -
height * (play.toasts.size() - i)};
ImVec2 position = {viewport->WorkPos.x + style.WindowPadding.x, bottom - height * (play.toasts.size() - i)};
ImVec2 end = {position.x + ImGui::CalcTextSize(toast.message.c_str()).x + style.WindowPadding.x * 2,
position.y + height};
drawList->AddRectFilled(position, end, color_get(ImGuiCol_WindowBg), style.WindowRounding);
@@ -579,90 +781,36 @@ void Ui::settings_draw(Settings& settings)
ImGui::SetItemTooltip("Go back to the character selection screen.\nProgress will be saved.");
}
void Ui::interact_tab_draw(Play& play, Settings& settings)
// The body in full (weight and stage, calories and digestion, totals), and the cheats when unlocked.
void Ui::stats_draw(Play& play)
{
Character& data = *play.character;
Body& body = play.body;
Stats& stats = body.stats;
Dialogue& dialogue = data.dialogue;
ImGuiStyle& style = ImGui::GetStyle();
std::string feelingPool = body.stage <= 0
? data.dialoguePool
: data.stages[std::min(body.stage, (int)data.stages.size()) - 1].dialoguePool;
bool isRandom = dialogue.is_hook(HOOK_RANDOM);
bool isHelp = dialogue.is_hook(HOOK_HELP);
bool isFeeling = dialogue.pools.contains(feelingPool);
Traits& traits = data.traits;
bool isMax = body.stage >= (int)data.stages.size();
ImGui::PushFont(nullptr, FONT_HUGE);
ImGui::SeparatorText(data.name.c_str());
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();
ImVec2 childSize = {(ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) / 2,
FONT_HEADER_2 + ImGui::GetFrameHeightWithSpacing()};
if (ImGui::BeginChild("##Weight", childSize))
{
std::string weight = weight_format(stats.weight, settings.measurement);
text_fit_draw(weight, {ImGui::GetContentRegionAvail().x, FONT_HEADER_2}, FONT_HEADER_2);
ImGui::SetItemTooltip("%s", weight.c_str());
bool isMax = body.stage >= (int)data.stages.size();
ImGui::ProgressBar(body.stage_progress_get(), {ImGui::GetContentRegionAvail().x, 0},
isMax ? "MAX" : "To Next Stage");
if (ImGui::BeginItemTooltip())
{
ImGui::Text("Stage: %i/%i (%0.1f%%)", body.stage + 1, (int)data.stages.size() + 1,
body.stage_progress_get() * PERCENT);
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY));
if (isMax)
ImGui::TextUnformatted("Maxed out!");
else
{
ImGui::Text("Start: %s", weight_format(body.stage_threshold_get(body.stage), settings.measurement).c_str());
ImGui::Text("Current: %s", weight.c_str());
ImGui::Text("Next: %s",
weight_format(body.stage_threshold_get(body.stage + 1), settings.measurement).c_str());
}
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Calories", childSize))
{
float capacityMax = body.capacity_max_get();
float overstuffed = std::max(0.0f, (stats.calories - stats.capacity) / (capacityMax - stats.capacity));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1 - overstuffed, 1 - overstuffed, 1));
text_fit_draw(text_format("%.0f / %.0f kcal", stats.calories, stats.capacity),
{ImGui::GetContentRegionAvail().x, FONT_HEADER_2}, FONT_HEADER_2);
ImGui::PopStyleColor();
if (ImGui::BeginItemTooltip())
{
ImGui::Text("%.0f / %.0f ", stats.calories, stats.capacity);
ImGui::SameLine(0, 0);
ImGui::TextColored(imvec4_get(GRAY), "(%.0f)", capacityMax);
ImGui::SameLine(0, 0);
ImGui::TextUnformatted(" kcal");
ImGui::EndTooltip();
}
float digestion = stats.isDigesting ? (float)stats.digestionTimer / std::max(1, data.traits.digestionTime)
: stats.digestionProgress / DIGESTION_MAX;
ImGui::ProgressBar(digestion, {ImGui::GetContentRegionAvail().x, 0},
stats.isDigesting ? "Digesting..." : "Digestion");
if (ImGui::BeginItemTooltip())
{
if (stats.isDigesting)
ImGui::TextUnformatted("Digestion in progress...");
else if (digestion <= 0)
ImGui::TextUnformatted("Give food to start digesting!");
else
ImGui::Text("%0.2f%%", digestion * PERCENT);
ImGui::Separator();
ImGui::TextColored(imvec4_get(GRAY), "Rate: %0.2f%% / sec", stats.digestionRate * TICK_RATE);
ImGui::EndTooltip();
}
}
ImGui::EndChild();
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");
@@ -671,67 +819,36 @@ void Ui::interact_tab_draw(Play& play, Settings& settings)
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());
if (!play.isCheats)
return;
auto frame_height_get = [](float size) {
ImGui::PushFont(nullptr, size);
float height = ImGui::GetFrameHeightWithSpacing();
ImGui::PopFont();
return height;
};
float buttonsHeight = frame_height_get(FONT_HUGE) + frame_height_get(FONT_HEADER_2);
buttonsHeight += isRandom || isHelp || isFeeling ? style.ItemSpacing.y + 1 : 0;
buttonsHeight += isRandom ? frame_height_get(FONT_HEADER_1) : 0;
buttonsHeight += (isHelp + isFeeling) * frame_height_get(FONT_HEADER_2);
if (float filler = ImGui::GetContentRegionAvail().y - buttonsHeight; filler > 0)
ImGui::Dummy({ImGui::GetContentRegionAvail().x, filler});
float width = ImGui::GetContentRegionAvail().x;
Items& items = play.items;
ImGui::PushFont(nullptr, FONT_HUGE);
if (items.spawnDelay <= 0)
{
ImGui::BeginDisabled(!items.is_spawn_possible(play));
if (is_fx(ImGui::Button("Spawn Item", {width, 0})))
items.spawn_queue_add(play);
ImGui::EndDisabled();
}
else
{
ImGui::BeginDisabled();
ImGui::ProgressBar(1 - std::clamp((float)items.spawnDelay / items.spawn_delay_get(play), 0.0f, 1.0f),
{width, ImGui::GetFrameHeight()}, "Spawn Item");
ImGui::EndDisabled();
tooltip_set(text_format("Next item in %0.2fs", items.spawnDelay * TICK).c_str());
}
ImGui::PopFont();
Autofeed& autofeed = play.autofeed;
ImGui::PushFont(nullptr, FONT_HEADER_2);
ImGui::BeginDisabled(!autofeed.is_available(play));
if (is_fx(ImGui::Button(autofeed.isEnabled ? "Disable Autofeed" : "Enable Autofeed", {width, 0})))
autofeed.isEnabled = !autofeed.isEnabled;
ImGui::EndDisabled();
tooltip_set(autofeed.isEnabled ? "Return to manual interaction."
: "Serve the character food and assist in digestion automatically.");
ImGui::SeparatorText("Cheats");
ImGui::PopFont();
if (isRandom || isHelp || isFeeling)
ImGui::Separator();
auto dialogue_button = [&](bool isShown, float size, const char* label, Entry* entry) {
if (!isShown)
return;
ImGui::PushFont(nullptr, size);
if (is_fx(ImGui::Button(label, {width, 0})))
play.text.set(play, entry);
ImGui::PopFont();
};
dialogue_button(isRandom, FONT_HEADER_1, "Let's chat!", dialogue.hook_get(HOOK_RANDOM));
dialogue_button(isHelp, FONT_HEADER_2, "Help", dialogue.hook_get(HOOK_HELP));
dialogue_button(isFeeling, FONT_HEADER_2, "How are you feeling?", dialogue.pool_get(feelingPool));
is_fx(ImGui::SliderFloat("Calories", &stats.calories, 0, body.capacity_max_get(), "%0.0f kcal"));
is_fx(ImGui::SliderFloat("Capacity", &stats.capacity, traits.capacityMin, 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, with the selected one's details below. Icons are drawn once.
void Ui::catalog_tab_draw(Play& play)
// 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;
@@ -747,19 +864,19 @@ void Ui::catalog_tab_draw(Play& play)
itemSizes[i] = canvas_draw(*renderer, itemCanvases[i], actor, {ITEM_SIZE, ITEM_SIZE});
}
}
bool isSelected = selectedItem >= 0 && selectedItem < (int)items.size();
bool isDiscovered = isSelected && play.items.discovered[selectedItem];
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 available = ImGui::GetContentRegionAvail();
float infoHeight = isSelected || items.empty() ? available.y / 2 : 0;
if (ImGui::BeginChild("##Items", {available.x, available.y - infoHeight - ImGui::GetStyle().ItemSpacing.y}))
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}))
{
bool isAnyHovered = false;
float right = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
for (int i = 0; i < (int)items.size(); i++)
{
ImGui::PushID(i);
bool isColored = play.items.discovered[i] && items[i].color;
bool isFound = play.items.discovered[i];
bool isColored = isFound && items[i].color;
if (isColored)
style_push(*items[i].color);
bool isCurrent = i == selectedItem;
@@ -767,210 +884,111 @@ void Ui::catalog_tab_draw(Play& play)
for (ImGuiCol id : {ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive})
ImGui::PushStyleColor(id, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered));
ImVec2 image = itemSizes[i];
ImVec4 tint = play.items.discovered[i] ? ImVec4(1, 1, 1, 1) : ImVec4(0, 0, 0, UNDISCOVERED_ALPHA);
ImVec2 padding = ImGui::GetStyle().FramePadding;
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);
isAnyHovered |= ImGui::IsItemHovered();
if (!isCurrent && ImGui::BeginItemTooltip())
{
item_details_draw(data, play.items.discovered[i] ? &items[i] : nullptr);
ImGui::EndTooltip();
}
if (isColored)
style_pop();
float next = ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + ITEM_SIZE;
if (i + 1 < (int)items.size() && next < right)
if ((i + 1) % columns != 0 && i + 1 < (int)items.size())
ImGui::SameLine();
ImGui::PopID();
}
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !isAnyHovered)
selectedItem = -1;
}
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();
if (infoHeight <= 0)
return;
bool isColored = isDiscovered && items[selectedItem].color;
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("##Info", {available.x, infoHeight}))
if (ImGui::BeginChild("##Details", {0, available.y}))
{
ImGui::Separator();
ImGui::PushFont(nullptr, FONT_HEADER_2);
ImGui::TextWrapped("%s", !isSelected ? "No items." : isDiscovered ? items[selectedItem].name.c_str() : UNKNOWN);
ImGui::PopFont();
if (isSelected)
ImGui::Separator();
if (isDiscovered)
{
float half = (ImGui::GetContentRegionAvail().y - ImGui::GetStyle().ItemSpacing.y) / 2;
if (ImGui::BeginChild("##Stats", {0, half}))
item_summary_draw(data, items[selectedItem]);
ImGui::EndChild();
ImGui::Separator();
if (ImGui::BeginChild("##Description", {0, half}))
ImGui::TextWrapped("%s", items[selectedItem].description.c_str());
ImGui::EndChild();
}
if (items.empty())
ImGui::TextWrapped("No items.");
else if (isSelected)
ImGui::TextWrapped("%s", UNKNOWN);
{
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();
}
void Ui::cheats_tab_draw(Play& play)
// The open window (stats, catalog or settings): one at a time, fixed in size above the taskbar, closed by its X or
// escape, and holding the mouse while open.
void Ui::window_draw(Play& play, Settings& settings, ImVec2 screen)
{
Traits& traits = play.character->traits;
Body& body = play.body;
Stats& stats = body.stats;
if (ImGui::BeginChild("##Cheats"))
{
is_fx(ImGui::SliderFloat("Calories", &stats.calories, 0, body.capacity_max_get(), "%0.0f kcal"));
is_fx(ImGui::SliderFloat("Capacity", &stats.capacity, traits.capacityMin, 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)play.character->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;
}
ImGui::EndChild();
}
void Ui::tools_draw(Play& play, ImVec2 position, ImVec2 size)
{
Character& data = *play.character;
if (play.text.isScene)
panel = PANEL_NONE;
play.isBlocked = panel != PANEL_NONE;
if (panel == PANEL_NONE)
return;
const char* title = PANEL_NAMES[panel];
float above = screen.y - play.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(position);
if (ImGui::Begin("##Tools", nullptr, PANEL_FLAGS))
ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f});
if (!ImGui::IsPopupOpen(title))
ImGui::OpenPopup(title);
bool isOpen = true;
if (ImGui::BeginPopupModal(title, &isOpen, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
float side = ImGui::GetContentRegionAvail().x;
ImFont* icons = data.menu.iconsFont;
auto tool_button = [&](const char* id, const std::string& glyph, const char* tooltip, bool isActive,
bool isDisabled) {
bool isIcon = icons && !glyph.empty();
ImGui::PushID(id);
if (isActive)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered));
ImGui::BeginDisabled(isDisabled);
bool isPressed = is_fx(ImGui::Button(isIcon ? "##Icon" : id, {side, side}));
if (isIcon)
{
ImVec2 min = ImGui::GetItemRectMin();
ImVec2 itemSize = ImGui::GetItemRectSize();
ImVec2 textSize = icons->CalcTextSizeA(itemSize.y, FLT_MAX, 0, glyph.c_str());
ImGui::GetWindowDrawList()->AddText(
icons, itemSize.y, {min.x + (itemSize.x - textSize.x) / 2, min.y + (itemSize.y - textSize.y) / 2},
ImGui::GetColorU32(ImGuiCol_Text), glyph.c_str());
}
ImGui::EndDisabled();
if (isActive)
ImGui::PopStyleColor();
if (*tooltip)
ImGui::SetItemTooltip("%s", tooltip);
ImGui::PopID();
return isPressed;
};
if (tool_button("Home", data.menu.homeGlyph, "Reset camera view.\n(Shortcut: Home)", false, false))
play.focus({ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y});
for (int i = 0; i < (int)data.interactTypes.size(); i++)
if (panel == PANEL_STATS)
stats_draw(play);
else if (panel == PANEL_CATALOG)
catalog_draw(play);
else
settings_draw(settings);
if (ImGui::IsKeyPressed(ImGuiKey_Escape))
{
InteractArea& type = data.interactTypes[i];
const char* tooltip = play.autofeed.isEnabled ? "Disabled while Autofeed is active!"
: icons && !type.glyph.empty() ? type.name.c_str()
: "";
if (tool_button(type.name.c_str(), type.glyph, tooltip, play.cursorType == i, play.autofeed.isEnabled))
play.cursorType = i;
isOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::End();
if (isOpen)
return;
panel = PANEL_NONE;
play.audio->play(play.character->sounds[SOUND_CLOSE]);
}
void Ui::play_update(Play& play, Settings& settings)
{
character = play.character;
ImGuiStyle& style = ImGui::GetStyle();
measurement = &settings.measurement;
ImVec2 screen = ImGui::GetIO().DisplaySize;
float barWidth = std::max(ImGui::GetTextLineHeightWithSpacing(), style.WindowMinSize.x);
float height = screen.y - style.WindowPadding.y * 2;
if (play.items.tooltip >= 0 && ImGui::BeginTooltip())
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)
return;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
slide_update(toolsSlide, play.isToolsOpen);
float toolsWidth = screen.x * TOOLS_WIDTH;
float toolsX = -toolsWidth + toolsWidth * std::pow(toolsSlide, SLIDE_EASE);
if (toolsSlide > 0)
tools_draw(play, {toolsX, style.WindowPadding.y}, {toolsWidth, height});
bar_draw(play, play.isToolsOpen, toolsSlide, {toolsX + toolsWidth, style.WindowPadding.y}, {barWidth, height},
DIRECTION_RIGHT, "Open Tools", "Close Tools");
slide_update(menuSlide, play.isMenuOpen);
float menuWidth = screen.x * MENU_WIDTH;
float menuX = screen.x - menuWidth * std::pow(menuSlide, SLIDE_EASE);
if (menuSlide > 0)
{
ImGui::SetNextWindowSize({menuWidth, height});
ImGui::SetNextWindowPos({menuX, style.WindowPadding.y});
if (ImGui::Begin("##Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Tabs"))
{
if (is_fx(ImGui::BeginTabItem("Interact")))
{
interact_tab_draw(play, settings);
ImGui::EndTabItem();
}
if (is_fx(ImGui::BeginTabItem("Catalog")))
{
catalog_tab_draw(play);
ImGui::EndTabItem();
}
if (is_fx(ImGui::BeginTabItem("Settings")))
{
settings_draw(settings);
ImGui::EndTabItem();
}
if (play.isCheats && is_fx(ImGui::BeginTabItem("Cheats")))
{
cheats_tab_draw(play);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
bar_draw(play, play.isMenuOpen, menuSlide, {menuX - barWidth, style.WindowPadding.y}, {barWidth, height},
DIRECTION_LEFT, "Open Main Menu", "Close Main Menu");
ImGui::PopStyleVar(2);
window_draw(play, settings, screen);
}
void Ui::select_update(std::vector<Character>& characters, Settings& settings)
@@ -1125,7 +1143,7 @@ void Ui::select_update(std::vector<Character>& characters, Settings& settings)
}
float side = ImGui::GetContentRegionAvail().x / 4;
float right = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
for (int i = 0; i < (int)characters.size(); i++)
for (int i = 0; i < (int)characters.size(); i++)
{
Character& entry = characters[i];
ImGui::PushID(i);