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

+6 -5
View File
@@ -4,6 +4,7 @@
constexpr float FONT_SIZE = 20;
constexpr const char* COMMON_FOLDER = "common/";
constexpr const char* ICONS_PATH = "gfx/ui/icons.anm2";
constexpr const char* TAG_NAMES[] = {"animation", "big", "color", "delay", "screenshake",
"shake", "silent", "small", "sound", "wave"};
constexpr const char* USE_NAMES[] = {"", "edible", "tool"};
@@ -110,7 +111,7 @@ Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& au
static void interact_parse(object source, InteractArea& area, Character& character, Audio& audio)
{
json_keys_check(source,
{"type", "null", "layer", "glyph", "isHold", "animation", "idleOnEnd", "cursorHover", "cursorActive",
{"type", "null", "layer", "icon", "isHold", "animation", "idleOnEnd", "cursorHover", "cursorActive",
"dialoguePool", "dialoguePoolFull", "dialogueCooldown", "digestionClick", "digestionHover",
"scaleAmplitude", "scaleCycles", "scaleTime", "sounds", "particles"},
"an interact area");
@@ -119,7 +120,7 @@ static void interact_parse(object source, InteractArea& area, Character& charact
area.cursorHover = json_string(source, "cursorHover", area.cursorHover);
area.dialoguePool = json_string(source, "dialoguePool", area.dialoguePool);
area.dialoguePoolFull = json_string(source, "dialoguePoolFull", area.dialoguePoolFull);
area.glyph = json_string(source, "glyph", area.glyph);
area.icon = json_string(source, "icon", area.icon);
area.idleOnEnd = json_string(source, "idleOnEnd", area.idleOnEnd);
area.digestionClick = json_float(source, "digestionClick", area.digestionClick);
area.digestionHover = json_float(source, "digestionHover", area.digestionHover * TICK_RATE) / TICK_RATE;
@@ -286,15 +287,15 @@ static void character_parse(object root, Character& character, Renderer& rendere
character.anm2_get(json_string(cursor, "anm2"), renderer, audio)};
object menu = json_object(root, "menu");
json_keys_check(menu, {"font", "iconsFont", "rounding", "homeGlyph", "sounds"}, "menu");
json_keys_check(menu, {"font", "rounding", "sounds"}, "menu");
auto font_get = [&](const char* key) {
std::string path = json_string(menu, key);
return path.empty() ? nullptr
: ImGui::GetIO().Fonts->AddFontFromFileTTF(file_find(character.roots, path).c_str(),
FONT_SIZE);
};
character.menu = {json_string(menu, "homeGlyph"), font_get("font"), font_get("iconsFont"),
json_float(menu, "rounding")};
character.menu = {font_get("font"), json_float(menu, "rounding")};
character.icons = character.anm2_get(ICONS_PATH, renderer, audio);
for (auto [name, value] : json_object(root, "particles"))
{
+2 -3
View File
@@ -81,7 +81,7 @@ struct InteractArea
std::string cursorHover;
std::string dialoguePool;
std::string dialoguePoolFull;
std::string glyph;
std::string icon;
std::string idleOnEnd;
std::string name;
std::vector<std::string> particles;
@@ -297,9 +297,7 @@ struct Cursor
struct Menu
{
std::string homeGlyph;
ImFont* font = nullptr;
ImFont* iconsFont = nullptr;
float rounding = 0;
};
@@ -351,6 +349,7 @@ public:
Swap talk;
Traits traits;
Anm2* anm2 = nullptr;
Anm2* icons = nullptr;
Texture* portrait = nullptr;
Texture* render = nullptr;
Color color = {0.5f, 0.5f, 0.5f, 1};
+10 -22
View File
@@ -13,9 +13,7 @@ constexpr const char* KEY_DISCOVERED = "discovered";
constexpr const char* KEY_ITEM = "item";
constexpr float AUTOSAVE_TIME = 30;
constexpr float CHEAT_TIME = 5;
constexpr float FOCUS_MENU_WIDTH = 0.3f;
constexpr float FOCUS_PADDING = 100;
constexpr float FOCUS_TOOLS_WIDTH = 0.1f;
constexpr float SHAKE_DECAY = 0.97f;
constexpr float SHAKE_EPSILON = 0.1f;
constexpr float SHAKE_LERP = 0.72f;
@@ -133,16 +131,9 @@ Actor& Play::pointer_actor_get()
return autofeed.isActive ? autoCursor : cursor;
}
Focus Play::focus_get()
{
if (text.isScene)
return FOCUS_CENTER;
return isMenuOpen && isToolsOpen ? FOCUS_BOTH : isMenuOpen ? FOCUS_MENU : isToolsOpen ? FOCUS_TOOLS : FOCUS_CENTER;
}
void Play::focus(Vec2 screenSize)
{
camera.focus(body.actor.bounds_get(), screenSize, focus_get());
camera.focus(body.actor.bounds_get(), screenSize, text.isScene ? 0 : inset);
}
void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
@@ -195,7 +186,7 @@ void Play::events_handle(Vec2 screenSize)
if (isPostgame)
continue;
text.set(*this, dialogue.hook_get(HOOK_END), TEXT_UNINTERRUPTIBLE);
isMenuOpen = isToolsOpen = false;
isTaskbarOpen = false;
body.stats.calories = 0;
body.stats.digestionProgress = 0;
items.drop();
@@ -215,7 +206,7 @@ void Play::update(Vec2 screenSize)
text.update(*this);
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
{
isMenuOpen = isPostgame = isCheats = true;
isTaskbarOpen = isPostgame = isCheats = true;
isPostgamePending = false;
focus(screenSize);
}
@@ -225,7 +216,7 @@ void Play::update(Vec2 screenSize)
Pointer pointer;
pointer.position = camera.world_get({io.MousePos.x, io.MousePos.y});
pointer.type = cursorType;
pointer.isCaptured = io.WantCaptureMouse;
pointer.isCaptured = io.WantCaptureMouse || isBlocked;
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
pointer.isReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
@@ -308,20 +299,17 @@ void Camera::shake_add(float magnitude, int time)
shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
}
// Zooms and pans so a target fills the view, leaving room for the open panels.
void Camera::focus(Rect target, Vec2 screenSize, Focus panels)
// Zooms and pans so a target fills the view above the inset at the screen's bottom.
void Camera::focus(Rect target, Vec2 screenSize, float inset)
{
if (!is_rect_valid(target))
return;
target = rect_center(target, FOCUS_PADDING);
float factor = std::min(screenSize.x / target.w, screenSize.y / target.h);
Vec2 view = {screenSize.x, std::max(1.0f, screenSize.y - inset)};
float factor = std::min(view.x / target.w, view.y / target.h);
zoom = ZOOM_LADDER[zoom_index_get(std::clamp(factor * PERCENT, ZOOM_LADDER[0], std::end(ZOOM_LADDER)[-1]))];
factor = zoom / PERCENT;
pan = rect_middle(target) - screenSize / factor / 2;
if (panels == FOCUS_MENU || panels == FOCUS_BOTH)
pan.x += screenSize.x * FOCUS_MENU_WIDTH / factor / 2;
if (panels == FOCUS_TOOLS || panels == FOCUS_BOTH)
pan.x -= screenSize.x * FOCUS_TOOLS_WIDTH / factor / 2;
pan = rect_middle(target) - view / factor / 2;
}
// Pans with the middle mouse (or control and the left), zooms through the ladder with the wheel toward the mouse, and
@@ -330,7 +318,7 @@ void Camera::update(Play& play, Vec2 screenSize)
{
ImGuiIO& io = ImGui::GetIO();
Cursor& cursor = play.character->cursor;
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
if (!play.isBlocked && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
{
if (ImGui::IsMouseDown(ImGuiMouseButton_Middle) ||
(ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Ctrl)))
+5 -14
View File
@@ -22,15 +22,6 @@ enum CursorState
CURSOR_ACTION
};
// Which side panels are open, which shifts where the camera centers the character.
enum Focus
{
FOCUS_CENTER,
FOCUS_MENU,
FOCUS_TOOLS,
FOCUS_BOTH
};
enum Hold
{
HOLD_RELEASE,
@@ -191,14 +182,14 @@ public:
bool isFocusPending = false;
void update(Play& play, Vec2 screenSize);
void focus(Rect target, Vec2 screenSize, Focus panels);
void focus(Rect target, Vec2 screenSize, float inset);
void shake_add(float magnitude, int time);
Vec2 world_get(Vec2 screen);
Affine view_get(Vec2 screenSize);
};
// A character being played, sequencing its parts each tick: the body, items, interactions, autofeed, camera and
// dialogue.
// dialogue. Inset is how much of the screen's bottom the UI covers, and blocked means a window has the mouse.
class Play
{
public:
@@ -218,15 +209,16 @@ public:
std::vector<Toast> toasts;
Rect bounds;
float autosaveTime = 0;
float inset = 0;
float cheatTime = 0;
int cheatIndex = 0;
int cursorType = -1;
CursorState cursorState = CURSOR_DEFAULT;
bool isBlocked = false;
bool isCheats = false;
bool isMenuOpen = true;
bool isPostgame = false;
bool isPostgamePending = false;
bool isToolsOpen = false;
bool isTaskbarOpen = true;
void set(Character& played, Game game, Renderer& renderer, Audio& sound);
void save();
@@ -236,7 +228,6 @@ public:
void toast_add(const std::string& message);
void particles_spawn(const std::vector<std::string>& names, Vec2 position);
Actor& pointer_actor_get();
Focus focus_get();
template <class Visitor> void visit(Visitor&& visitor)
{
+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);
+32 -16
View File
@@ -4,18 +4,32 @@
#include "save.h"
#include <imgui.h>
constexpr float FONT_NORMAL = 20;
constexpr float FONT_HEADER_3 = 24;
constexpr float FONT_HEADER_2 = 30;
constexpr float FONT_HEADER_1 = 40;
constexpr float FONT_HUGE = 80;
constexpr float FONT_NORMAL = 26;
constexpr float FONT_HEADER_3 = 30;
constexpr float FONT_HEADER_2 = 34;
constexpr float FONT_DIALOGUE = 32;
class Play;
enum Direction
{
DIRECTION_LEFT,
DIRECTION_RIGHT
DIRECTION_UP,
DIRECTION_DOWN
};
enum Panel
{
PANEL_NONE,
PANEL_STATS,
PANEL_CATALOG,
PANEL_SETTINGS
};
enum ButtonState
{
BUTTON_NORMAL,
BUTTON_ACTIVE,
BUTTON_DISABLED
};
enum CommandType
@@ -39,7 +53,7 @@ using Commands = std::vector<Command>;
void style_set(Color color, float rounding = 0);
// The menus, tools, dialogue box, toasts and select screen.
// The taskbar, its windows, the dialogue box, toasts and the select screen.
class Ui
{
public:
@@ -52,12 +66,13 @@ public:
Character* catalogCharacter = nullptr;
Character* character = nullptr;
Commands* commands = nullptr;
Measurement* measurement = nullptr;
Renderer* renderer = nullptr;
float menuSlide = 1;
float toolsSlide = 0;
float taskbarSlide = 1;
int characterIndex = 0;
int selectedIndex = -1;
int selectedItem = -1;
Panel panel = PANEL_NONE;
bool isFullscreen = false;
bool isNewGameWarning = false;
bool isSelectedSaved = false;
@@ -67,14 +82,15 @@ public:
private:
bool is_fx(bool result);
bool icon_button(const char* icon, const char* tooltip, ButtonState state);
void icon_draw(const char* name, ImVec2 position, float size, ImU32 tint);
void command_add(CommandType type, const std::string& key = "");
void window_open(Play& play, Panel next);
void taskbar_draw(Play& play, ImVec2 screen);
void window_draw(Play& play, Settings& settings, ImVec2 screen);
void stats_draw(Play& play);
void catalog_draw(Play& play);
void settings_draw(Settings& settings);
void interact_tab_draw(Play& play, Settings& settings);
void catalog_tab_draw(Play& play);
void cheats_tab_draw(Play& play);
void tools_draw(Play& play, ImVec2 position, ImVec2 size);
void bar_draw(Play& play, bool& isOpen, float slide, ImVec2 position, ImVec2 size, Direction direction,
const char* openTooltip, const char* closeTooltip);
void text_draw(Play& play, ImVec2 screen);
void toasts_draw(Play& play);
};