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

+4 -6
View File
@@ -32,7 +32,7 @@ There's two folders inside resources; "characters" and "font".
"font" is just a font containing the font used in the selection screen. That's literally it. "font" is just a font containing the font used in the selection screen. That's literally it.
"common" holds files characters share (sounds, particles, the cursor and so on). Whenever a character names a file, the game looks in the character's folder first, then in "common", so a character can use anything there as is, or replace it by having its own file at the same path. "common" holds files characters share (sounds, particles, the cursor and so on). Whenever a character names a file, the game looks in the character's folder first, then in "common", so a character can use anything there as is, or replace it by having its own file at the same path. This includes the taskbar's icons, `gfx/ui/icons.anm2`: one region per icon, named after its button (`talk`, `mood`, `spawn`, `autofeed`, `home`, `stats`, `catalog`, `settings`, `help`, plus whatever `icon`s the interaction types name). A character with its own `gfx/ui/icons.anm2` gets its own icons.
EVERYTHING ELSE is stored inside character folders in the "characters" folder; one folder per character, named however you like (the folder name is also the name of its save file). This has all data associated with characters. Think of characters more as tailored game experiences rather than literally being just the characters. Not only is there the character graphics, but backgrounds, items, parameters, etc. These are intensely customizable to suit whatever experience you'd like (within the confines of the engine, of course). EVERYTHING ELSE is stored inside character folders in the "characters" folder; one folder per character, named however you like (the folder name is also the name of its save file). This has all data associated with characters. Think of characters more as tailored game experiences rather than literally being just the characters. Not only is there the character graphics, but backgrounds, items, parameters, etc. These are intensely customizable to suit whatever experience you'd like (within the confines of the engine, of course).
@@ -149,14 +149,14 @@ The areas which will expand based on how full the character is and their progres
| `scaleMax` | The scale (in percent) that will be added at maximum. Know that a scale of 100 is the normal scale of an animation. | | `scaleMax` | The scale (in percent) that will be added at maximum. Know that a scale of 100 is the normal scale of an animation. |
### interactTypes, interactAreas ### interactTypes, interactAreas
`interactTypes` is an object of interaction types by name (each gets a button in the tools bar), giving each type's defaults. `interactAreas` is a list of the concrete areas on a character which can be interacted with; usually for belly rubs, kisses, etc. Each area names its `type` and takes all of that type's settings, changing whichever it gives itself. `interactTypes` is an object of interaction types by name (each gets a button on the taskbar), giving each type's defaults. `interactAreas` is a list of the concrete areas on a character which can be interacted with; usually for belly rubs, kisses, etc. Each area names its `type` and takes all of that type's settings, changing whichever it gives itself.
| Key | Type | Meaning | | Key | Type | Meaning |
|---|---|---| |---|---|---|
| `type` | string | (Areas only) The interaction type to use. | | `type` | string | (Areas only) The interaction type to use. |
| `null` | string | (Areas) The null in which the interact area can be triggered. | | `null` | string | (Areas) The null in which the interact area can be triggered. |
| `layer` | string | The layer that the scale effect (e.g. smacks) applies to. | | `layer` | string | The layer that the scale effect (e.g. smacks) applies to. |
| `glyph` | string | The icon-font glyph used for this type in the tools bar. | | `icon` | string | The region of `gfx/ui/icons.anm2` shown on this type's taskbar button. |
| `isHold` | bool | If true, the interaction is held rather than clicked once. | | `isHold` | bool | If true, the interaction is held rather than clicked once. |
| `cursorHover` | string | The animation the cursor will play when hovering over the interact area. | | `cursorHover` | string | The animation the cursor will play when hovering over the interact area. |
| `cursorActive` | string | The animation the cursor will play when interacting. | | `cursorActive` | string | The animation the cursor will play when interacting. |
@@ -205,10 +205,8 @@ Determines menu and general UI appearance and behavior.
| Key | Meaning | | Key | Meaning |
|---|---| |---|---|
| `font` | The font the menu will use. | | `font` | The font the menu will use. |
| `iconsFont` | The font holding the tools bar's glyphs. |
| `rounding` | The rounding of corners the UI will use. | | `rounding` | The rounding of corners the UI will use. |
| `homeGlyph` | The glyph of the tools bar's Home button. | | `sounds` | `open` and `close` (the taskbar or one of its windows), `hover` and `select` (a widget), and `cheats` (after entering a special code to activate cheats). |
| `sounds` | `open` and `close` (a sliding panel), `hover` and `select` (a widget), and `cheats` (after entering a special code to activate cheats). |
### particles ### particles
An object of particles by name, each with the `anm2` and `animation` it plays once where spawned. An object of particles by name, each with the `anm2` and `animation` it plays once where spawned.
+1 -1
View File
@@ -62,4 +62,4 @@ See [MODDING.md](MODDING.md).
## License ## License
See [LICENSE](LICENSE). See [LICENSE](LICENSE). The taskbar icons are from [Tabler Icons](https://tabler.io/icons) (MIT), see `resources/common/gfx/ui/LICENSE.txt`.
+4 -6
View File
@@ -44,7 +44,7 @@
"sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"}, "sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"},
"interactTypes": { "interactTypes": {
"Rub": { "Rub": {
"glyph": "2", "icon": "rub",
"isHold": true, "isHold": true,
"cursorHover": "Hover", "cursorHover": "Hover",
"cursorActive": "Rub", "cursorActive": "Rub",
@@ -52,7 +52,7 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Smack": { "Smack": {
"glyph": "3", "icon": "smack",
"cursorHover": "SmackHover", "cursorHover": "SmackHover",
"cursorActive": "Smack", "cursorActive": "Smack",
"dialogueCooldown": 5, "dialogueCooldown": 5,
@@ -63,14 +63,14 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Kiss": { "Kiss": {
"glyph": "4", "icon": "kiss",
"cursorHover": "KissHover", "cursorHover": "KissHover",
"cursorActive": "Kiss", "cursorActive": "Kiss",
"sounds": "sfx/kiss*.ogg", "sounds": "sfx/kiss*.ogg",
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Turn Around": { "Turn Around": {
"glyph": "5", "icon": "turn",
"sounds": ["sfx/turn.ogg"], "sounds": ["sfx/turn.ogg"],
"particles": ["Circle"] "particles": ["Circle"]
} }
@@ -108,9 +108,7 @@
}, },
"menu": { "menu": {
"font": "font/font.ttf", "font": "font/font.ttf",
"iconsFont": "font/icons.ttf",
"rounding": 10.0, "rounding": 10.0,
"homeGlyph": "1",
"sounds": { "sounds": {
"open": ["sfx/open.ogg"], "open": ["sfx/open.ogg"],
"close": ["sfx/close.ogg"], "close": ["sfx/close.ogg"],
+3 -5
View File
@@ -39,7 +39,7 @@
"sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"}, "sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"},
"interactTypes": { "interactTypes": {
"Rub": { "Rub": {
"glyph": "2", "icon": "rub",
"isHold": true, "isHold": true,
"cursorHover": "Hover", "cursorHover": "Hover",
"cursorActive": "Rub", "cursorActive": "Rub",
@@ -47,7 +47,7 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Smack": { "Smack": {
"glyph": "3", "icon": "smack",
"cursorHover": "SmackHover", "cursorHover": "SmackHover",
"cursorActive": "Smack", "cursorActive": "Smack",
"dialogueCooldown": 0.6667, "dialogueCooldown": 0.6667,
@@ -58,7 +58,7 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Kiss": { "Kiss": {
"glyph": "4", "icon": "kiss",
"cursorHover": "KissHover", "cursorHover": "KissHover",
"cursorActive": "Kiss", "cursorActive": "Kiss",
"sounds": "sfx/kiss*.ogg", "sounds": "sfx/kiss*.ogg",
@@ -94,9 +94,7 @@
}, },
"menu": { "menu": {
"font": "font/font.ttf", "font": "font/font.ttf",
"iconsFont": "font/icons.ttf",
"rounding": 10.0, "rounding": 10.0,
"homeGlyph": "1",
"sounds": { "sounds": {
"open": ["sfx/open.ogg"], "open": ["sfx/open.ogg"],
"close": ["sfx/close.ogg"], "close": ["sfx/close.ogg"],
+4 -6
View File
@@ -46,7 +46,7 @@
"sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"}, "sounds": {"digest": "sfx/digest*.ogg", "gurgle": "sfx/gurgle*.ogg"},
"interactTypes": { "interactTypes": {
"Rub": { "Rub": {
"glyph": "2", "icon": "rub",
"isHold": true, "isHold": true,
"cursorHover": "Hover", "cursorHover": "Hover",
"cursorActive": "Rub", "cursorActive": "Rub",
@@ -54,7 +54,7 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Smack": { "Smack": {
"glyph": "3", "icon": "smack",
"cursorHover": "SmackHover", "cursorHover": "SmackHover",
"cursorActive": "Smack", "cursorActive": "Smack",
"dialogueCooldown": 0.6667, "dialogueCooldown": 0.6667,
@@ -64,14 +64,14 @@
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Kiss": { "Kiss": {
"glyph": "4", "icon": "kiss",
"cursorHover": "KissHover", "cursorHover": "KissHover",
"cursorActive": "Kiss", "cursorActive": "Kiss",
"sounds": "sfx/kiss*.ogg", "sounds": "sfx/kiss*.ogg",
"particles": ["Circle"] "particles": ["Circle"]
}, },
"Turn Around": { "Turn Around": {
"glyph": "5", "icon": "turn",
"sounds": ["sfx/turn.ogg"], "sounds": ["sfx/turn.ogg"],
"particles": ["Circle"] "particles": ["Circle"]
} }
@@ -113,9 +113,7 @@
}, },
"menu": { "menu": {
"font": "font/font.ttf", "font": "font/font.ttf",
"iconsFont": "font/icons.ttf",
"rounding": 10.0, "rounding": 10.0,
"homeGlyph": "1",
"sounds": { "sounds": {
"open": ["sfx/open.ogg"], "open": ["sfx/open.ogg"],
"close": ["sfx/close.ogg"], "close": ["sfx/close.ogg"],
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
Icons from Tabler Icons (https://tabler.io/icons), under the MIT License.
Copyright (c) 2020-2024 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+34
View File
@@ -0,0 +1,34 @@
{
"anm2ed": 1,
"spritesheets": [
{
"path": "icons.png",
"regions": [
{"name": "talk", "crop": [0, 0], "size": [48, 48], "origin": "center"},
{"name": "mood", "crop": [49, 0], "size": [48, 48], "origin": "center"},
{"name": "spawn", "crop": [98, 0], "size": [48, 48], "origin": "center"},
{"name": "autofeed", "crop": [147, 0], "size": [48, 48], "origin": "center"},
{"name": "home", "crop": [196, 0], "size": [48, 48], "origin": "center"},
{"name": "rub", "crop": [245, 0], "size": [48, 48], "origin": "center"},
{"name": "smack", "crop": [294, 0], "size": [48, 48], "origin": "center"},
{"name": "kiss", "crop": [343, 0], "size": [48, 48], "origin": "center"},
{"name": "turn", "crop": [0, 49], "size": [48, 48], "origin": "center"},
{"name": "stats", "crop": [49, 49], "size": [48, 48], "origin": "center"},
{"name": "catalog", "crop": [98, 49], "size": [48, 48], "origin": "center"},
{"name": "settings", "crop": [147, 49], "size": [48, 48], "origin": "center"},
{"name": "help", "crop": [196, 49], "size": [48, 48], "origin": "center"},
{"name": "close", "crop": [245, 49], "size": [48, 48], "origin": "center"}
]
}
],
"layers": [
{"name": "Icon"}
],
"animations": {
"Icon": {
"layers": [
{"layer": 0, "frames": [{"region": 0}]}
]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+6 -5
View File
@@ -4,6 +4,7 @@
constexpr float FONT_SIZE = 20; constexpr float FONT_SIZE = 20;
constexpr const char* COMMON_FOLDER = "common/"; 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", constexpr const char* TAG_NAMES[] = {"animation", "big", "color", "delay", "screenshake",
"shake", "silent", "small", "sound", "wave"}; "shake", "silent", "small", "sound", "wave"};
constexpr const char* USE_NAMES[] = {"", "edible", "tool"}; 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) static void interact_parse(object source, InteractArea& area, Character& character, Audio& audio)
{ {
json_keys_check(source, 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", "dialoguePool", "dialoguePoolFull", "dialogueCooldown", "digestionClick", "digestionHover",
"scaleAmplitude", "scaleCycles", "scaleTime", "sounds", "particles"}, "scaleAmplitude", "scaleCycles", "scaleTime", "sounds", "particles"},
"an interact area"); "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.cursorHover = json_string(source, "cursorHover", area.cursorHover);
area.dialoguePool = json_string(source, "dialoguePool", area.dialoguePool); area.dialoguePool = json_string(source, "dialoguePool", area.dialoguePool);
area.dialoguePoolFull = json_string(source, "dialoguePoolFull", area.dialoguePoolFull); 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.idleOnEnd = json_string(source, "idleOnEnd", area.idleOnEnd);
area.digestionClick = json_float(source, "digestionClick", area.digestionClick); area.digestionClick = json_float(source, "digestionClick", area.digestionClick);
area.digestionHover = json_float(source, "digestionHover", area.digestionHover * TICK_RATE) / TICK_RATE; 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)}; character.anm2_get(json_string(cursor, "anm2"), renderer, audio)};
object menu = json_object(root, "menu"); 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) { auto font_get = [&](const char* key) {
std::string path = json_string(menu, key); std::string path = json_string(menu, key);
return path.empty() ? nullptr return path.empty() ? nullptr
: ImGui::GetIO().Fonts->AddFontFromFileTTF(file_find(character.roots, path).c_str(), : ImGui::GetIO().Fonts->AddFontFromFileTTF(file_find(character.roots, path).c_str(),
FONT_SIZE); FONT_SIZE);
}; };
character.menu = {json_string(menu, "homeGlyph"), font_get("font"), font_get("iconsFont"), character.menu = {font_get("font"), json_float(menu, "rounding")};
json_float(menu, "rounding")}; character.icons = character.anm2_get(ICONS_PATH, renderer, audio);
for (auto [name, value] : json_object(root, "particles")) for (auto [name, value] : json_object(root, "particles"))
{ {
+2 -3
View File
@@ -81,7 +81,7 @@ struct InteractArea
std::string cursorHover; std::string cursorHover;
std::string dialoguePool; std::string dialoguePool;
std::string dialoguePoolFull; std::string dialoguePoolFull;
std::string glyph; std::string icon;
std::string idleOnEnd; std::string idleOnEnd;
std::string name; std::string name;
std::vector<std::string> particles; std::vector<std::string> particles;
@@ -297,9 +297,7 @@ struct Cursor
struct Menu struct Menu
{ {
std::string homeGlyph;
ImFont* font = nullptr; ImFont* font = nullptr;
ImFont* iconsFont = nullptr;
float rounding = 0; float rounding = 0;
}; };
@@ -351,6 +349,7 @@ public:
Swap talk; Swap talk;
Traits traits; Traits traits;
Anm2* anm2 = nullptr; Anm2* anm2 = nullptr;
Anm2* icons = nullptr;
Texture* portrait = nullptr; Texture* portrait = nullptr;
Texture* render = nullptr; Texture* render = nullptr;
Color color = {0.5f, 0.5f, 0.5f, 1}; 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 const char* KEY_ITEM = "item";
constexpr float AUTOSAVE_TIME = 30; constexpr float AUTOSAVE_TIME = 30;
constexpr float CHEAT_TIME = 5; constexpr float CHEAT_TIME = 5;
constexpr float FOCUS_MENU_WIDTH = 0.3f;
constexpr float FOCUS_PADDING = 100; constexpr float FOCUS_PADDING = 100;
constexpr float FOCUS_TOOLS_WIDTH = 0.1f;
constexpr float SHAKE_DECAY = 0.97f; constexpr float SHAKE_DECAY = 0.97f;
constexpr float SHAKE_EPSILON = 0.1f; constexpr float SHAKE_EPSILON = 0.1f;
constexpr float SHAKE_LERP = 0.72f; constexpr float SHAKE_LERP = 0.72f;
@@ -133,16 +131,9 @@ Actor& Play::pointer_actor_get()
return autofeed.isActive ? autoCursor : cursor; 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) 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) void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
@@ -195,7 +186,7 @@ void Play::events_handle(Vec2 screenSize)
if (isPostgame) if (isPostgame)
continue; continue;
text.set(*this, dialogue.hook_get(HOOK_END), TEXT_UNINTERRUPTIBLE); text.set(*this, dialogue.hook_get(HOOK_END), TEXT_UNINTERRUPTIBLE);
isMenuOpen = isToolsOpen = false; isTaskbarOpen = false;
body.stats.calories = 0; body.stats.calories = 0;
body.stats.digestionProgress = 0; body.stats.digestionProgress = 0;
items.drop(); items.drop();
@@ -215,7 +206,7 @@ void Play::update(Vec2 screenSize)
text.update(*this); text.update(*this);
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene) if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
{ {
isMenuOpen = isPostgame = isCheats = true; isTaskbarOpen = isPostgame = isCheats = true;
isPostgamePending = false; isPostgamePending = false;
focus(screenSize); focus(screenSize);
} }
@@ -225,7 +216,7 @@ void Play::update(Vec2 screenSize)
Pointer pointer; Pointer pointer;
pointer.position = camera.world_get({io.MousePos.x, io.MousePos.y}); pointer.position = camera.world_get({io.MousePos.x, io.MousePos.y});
pointer.type = cursorType; pointer.type = cursorType;
pointer.isCaptured = io.WantCaptureMouse; pointer.isCaptured = io.WantCaptureMouse || isBlocked;
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
pointer.isReleased = ImGui::IsMouseReleased(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)}; shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
} }
// Zooms and pans so a target fills the view, leaving room for the open 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, Focus panels) void Camera::focus(Rect target, Vec2 screenSize, float inset)
{ {
if (!is_rect_valid(target)) if (!is_rect_valid(target))
return; return;
target = rect_center(target, FOCUS_PADDING); 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]))]; zoom = ZOOM_LADDER[zoom_index_get(std::clamp(factor * PERCENT, ZOOM_LADDER[0], std::end(ZOOM_LADDER)[-1]))];
factor = zoom / PERCENT; factor = zoom / PERCENT;
pan = rect_middle(target) - screenSize / factor / 2; pan = rect_middle(target) - view / 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;
} }
// Pans with the middle mouse (or control and the left), zooms through the ladder with the wheel toward the mouse, and // 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(); ImGuiIO& io = ImGui::GetIO();
Cursor& cursor = play.character->cursor; 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) || if (ImGui::IsMouseDown(ImGuiMouseButton_Middle) ||
(ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Ctrl))) (ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Ctrl)))
+5 -14
View File
@@ -22,15 +22,6 @@ enum CursorState
CURSOR_ACTION 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 enum Hold
{ {
HOLD_RELEASE, HOLD_RELEASE,
@@ -191,14 +182,14 @@ public:
bool isFocusPending = false; bool isFocusPending = false;
void update(Play& play, Vec2 screenSize); 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); void shake_add(float magnitude, int time);
Vec2 world_get(Vec2 screen); Vec2 world_get(Vec2 screen);
Affine view_get(Vec2 screenSize); Affine view_get(Vec2 screenSize);
}; };
// A character being played, sequencing its parts each tick: the body, items, interactions, autofeed, camera and // 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 class Play
{ {
public: public:
@@ -218,15 +209,16 @@ public:
std::vector<Toast> toasts; std::vector<Toast> toasts;
Rect bounds; Rect bounds;
float autosaveTime = 0; float autosaveTime = 0;
float inset = 0;
float cheatTime = 0; float cheatTime = 0;
int cheatIndex = 0; int cheatIndex = 0;
int cursorType = -1; int cursorType = -1;
CursorState cursorState = CURSOR_DEFAULT; CursorState cursorState = CURSOR_DEFAULT;
bool isBlocked = false;
bool isCheats = false; bool isCheats = false;
bool isMenuOpen = true;
bool isPostgame = false; bool isPostgame = false;
bool isPostgamePending = false; bool isPostgamePending = false;
bool isToolsOpen = false; bool isTaskbarOpen = true;
void set(Character& played, Game game, Renderer& renderer, Audio& sound); void set(Character& played, Game game, Renderer& renderer, Audio& sound);
void save(); void save();
@@ -236,7 +228,6 @@ public:
void toast_add(const std::string& message); void toast_add(const std::string& message);
void particles_spawn(const std::vector<std::string>& names, Vec2 position); void particles_spawn(const std::vector<std::string>& names, Vec2 position);
Actor& pointer_actor_get(); Actor& pointer_actor_get();
Focus focus_get();
template <class Visitor> void visit(Visitor&& visitor) 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_SIZE = 0.35f;
constexpr float INDICATOR_SPEED = 2; constexpr float INDICATOR_SPEED = 2;
constexpr float ITEM_SIZE = 96; 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_G = 1000;
constexpr float KG_TO_LB = 2.20462f; constexpr float KG_TO_LB = 2.20462f;
constexpr float LB_TO_OZ = 16; constexpr float LB_TO_OZ = 16;
constexpr float MENU_WIDTH = 0.3f;
constexpr float OSCILLATION_AMPLITUDE = 2; constexpr float OSCILLATION_AMPLITUDE = 2;
constexpr float OSCILLATION_FREQUENCY = 1; constexpr float OSCILLATION_FREQUENCY = 1;
constexpr float OSCILLATION_SPREAD = 0.16f; constexpr float OSCILLATION_SPREAD = 0.16f;
@@ -37,11 +54,9 @@ constexpr float SHAKE_FREQUENCY = 30;
constexpr float SLIDE_EASE = 2; constexpr float SLIDE_EASE = 2;
constexpr float SLIDE_TIME = 0.125f; constexpr float SLIDE_TIME = 0.125f;
constexpr float TEXT_BIG = 1.25f; constexpr float TEXT_BIG = 1.25f;
constexpr float TEXT_FIT_MIN = 1; constexpr float TEXT_HEIGHT_LINES = 5;
constexpr float TEXT_HEIGHT_LINES = 6;
constexpr float TEXT_SMALL = 0.75f; constexpr float TEXT_SMALL = 0.75f;
constexpr float TEXT_WIDTH = 0.3f; constexpr float TEXT_WIDTH = 0.4f;
constexpr float TOOLS_WIDTH = 0.05f;
constexpr float TOOLTIP_WRAP = 24; constexpr float TOOLTIP_WRAP = 24;
constexpr float TRIANGLE_SIZE = 0.22f; constexpr float TRIANGLE_SIZE = 0.22f;
constexpr float UNDISCOVERED_ALPHA = 0.5f; constexpr float UNDISCOVERED_ALPHA = 0.5f;
@@ -94,8 +109,9 @@ static Palette palette_get(Color color)
void style_set(Color color, float rounding) void style_set(Color color, float rounding)
{ {
ImGuiStyle& style = ImGui::GetStyle(); 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.ScrollbarSize = SCROLLBAR_SIZE;
style.FontSizeBase = FONT_NORMAL;
for (auto [id, value] : palette_get(color)) for (auto [id, value] : palette_get(color))
style.Colors[id] = value; style.Colors[id] = value;
style.Colors[ImGuiCol_PopupBg] = style.Colors[ImGuiCol_WindowBg]; 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) static void triangle_draw(ImVec2 position, ImVec2 size, Direction direction)
{ {
ImVec2 center = {position.x + size.x / 2, position.y + size.y / 2}; ImVec2 center = {position.x + size.x / 2, position.y + size.y / 2};
float half = size.x * TRIANGLE_SIZE; float half = size.y * TRIANGLE_SIZE;
float sign = direction == DIRECTION_LEFT ? -1 : 1; float sign = direction == DIRECTION_UP ? -1 : 1;
ImGui::GetWindowDrawList()->AddTriangleFilled({center.x + half * sign, center.y}, ImGui::GetWindowDrawList()->AddTriangleFilled({center.x, center.y + half * sign},
{center.x - half * sign, center.y - half}, {center.x - half * 2, center.y - half * sign},
{center.x - half * sign, center.y + half}, {center.x + half * 2, center.y - half * sign},
ImGui::GetColorU32(ImGuiCol_Text)); ImGui::GetColorU32(ImGuiCol_Text));
} }
@@ -181,21 +197,6 @@ static std::string playtime_format(float seconds)
total % SECONDS_PER_MINUTE); 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. // 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) 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}); commands->push_back({key, type});
} }
// A side panel's bar, which opens and closes it. Hovering the bar opens it too. // An icon from the character's icon sheet (see Character::icons), fit into a square and tinted.
void Ui::bar_draw(Play& play, bool& isOpen, float slide, ImVec2 position, ImVec2 size, Direction direction, void Ui::icon_draw(const char* name, ImVec2 position, float size, ImU32 tint)
const char* openTooltip, const char* closeTooltip)
{ {
ImGui::SetNextWindowSize(size); Anm2* icons = character ? character->icons : nullptr;
ImGui::SetNextWindowPos(position); const Anm2Region* region = icons ? icons->region_get(0, name) : nullptr;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0); Texture* sheet = region && !icons->sheets.empty() ? icons->sheets[0] : nullptr;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (!sheet)
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); return;
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, size); float scale = size / std::max(region->size.x, region->size.y);
if (ImGui::Begin(direction == DIRECTION_LEFT ? "##Menu Bar" : "##Tools Bar", nullptr, BAR_FLAGS)) 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(); float from = -PI / 2 + 2 * PI * progress * i / segments;
ImVec2 cursor = ImGui::GetCursorScreenPos(); float to = -PI / 2 + 2 * PI * progress * (i + 1) / segments;
bool isPressed = is_fx(ImGui::Button("##Toggle", buttonSize)); ImGui::GetWindowDrawList()->AddTriangleFilled(
if (slide <= 0 || slide >= 1) 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);
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);
} }
}
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::End();
ImGui::PopStyleVar(4);
} }
void Ui::text_draw(Play& play, ImVec2 screen) 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)) if (!entry || (line->text.empty() && !isChoosing))
return; return;
ImGuiStyle& style = ImGui::GetStyle(); ImGuiStyle& style = ImGui::GetStyle();
ImVec2 size = {screen.x * TEXT_WIDTH - style.WindowPadding.x * 2, ImGui::PushFont(nullptr, FONT_DIALOGUE);
ImGui::GetTextLineHeightWithSpacing() * TEXT_HEIGHT_LINES}; ImVec2 size = {screen.x * TEXT_WIDTH, ImGui::GetTextLineHeightWithSpacing() * TEXT_HEIGHT_LINES};
ImGui::SetNextWindowSize(size); 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)) if (ImGui::Begin("##Text", nullptr, PANEL_FLAGS))
{ {
bool isHovered = ImGui::IsWindowHovered(); 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; bool isAdvance = (isHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) || isSpace;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {style.ItemSpacing.x, 0}); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {style.ItemSpacing.x, 0});
if (ImGui::BeginTabBar("##Name")) if (ImGui::BeginTabBar("##Name"))
@@ -442,7 +646,6 @@ void Ui::text_draw(Play& play, ImVec2 screen)
ImGui::EndTabItem(); ImGui::EndTabItem();
ImGui::EndTabBar(); ImGui::EndTabBar();
} }
ImGui::PushFont(nullptr, FONT_NORMAL);
letters_draw(*line, text.index, (float)ImGui::GetTime()); letters_draw(*line, text.index, (float)ImGui::GetTime());
ImVec2 position = ImGui::GetWindowPos(); ImVec2 position = ImGui::GetWindowPos();
@@ -490,10 +693,10 @@ void Ui::text_draw(Play& play, ImVec2 screen)
if (isAdvance) if (isAdvance)
text.advance(play); text.advance(play);
} }
ImGui::PopFont();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
} }
ImGui::End(); ImGui::End();
ImGui::PopFont();
} }
void Ui::toasts_draw(Play& play) void Ui::toasts_draw(Play& play)
@@ -502,6 +705,7 @@ void Ui::toasts_draw(Play& play)
ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGuiViewport* viewport = ImGui::GetMainViewport();
ImDrawList* drawList = ImGui::GetForegroundDrawList(); ImDrawList* drawList = ImGui::GetForegroundDrawList();
float height = ImGui::GetTextLineHeightWithSpacing() + style.WindowPadding.y * 2; 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++) for (int i = 0; i < (int)play.toasts.size(); i++)
{ {
Toast& toast = play.toasts[i]; Toast& toast = play.toasts[i];
@@ -510,9 +714,7 @@ void Ui::toasts_draw(Play& play)
color.w = (float)toast.life / TOAST_LIFE; color.w = (float)toast.life / TOAST_LIFE;
return ImGui::GetColorU32(color); return ImGui::GetColorU32(color);
}; };
ImVec2 position = {viewport->WorkPos.x + style.WindowPadding.x, ImVec2 position = {viewport->WorkPos.x + style.WindowPadding.x, bottom - height * (play.toasts.size() - i)};
viewport->WorkPos.y + viewport->WorkSize.y - style.WindowPadding.y -
height * (play.toasts.size() - i)};
ImVec2 end = {position.x + ImGui::CalcTextSize(toast.message.c_str()).x + style.WindowPadding.x * 2, ImVec2 end = {position.x + ImGui::CalcTextSize(toast.message.c_str()).x + style.WindowPadding.x * 2,
position.y + height}; position.y + height};
drawList->AddRectFilled(position, end, color_get(ImGuiCol_WindowBg), style.WindowRounding); 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."); 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; Character& data = *play.character;
Body& body = play.body; Body& body = play.body;
Stats& stats = body.stats; Stats& stats = body.stats;
Dialogue& dialogue = data.dialogue; Traits& traits = data.traits;
ImGuiStyle& style = ImGui::GetStyle(); bool isMax = body.stage >= (int)data.stages.size();
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);
ImGui::PushFont(nullptr, FONT_HUGE); ImGui::PushFont(nullptr, FONT_HEADER_2);
ImGui::SeparatorText(data.name.c_str()); ImGui::SeparatorText("Weight");
ImGui::PopFont(); 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, ImGui::PushFont(nullptr, FONT_HEADER_2);
FONT_HEADER_2 + ImGui::GetFrameHeightWithSpacing()}; ImGui::SeparatorText("Digestion");
if (ImGui::BeginChild("##Weight", childSize)) ImGui::PopFont();
{ ImGui::Text("%.0f / %.0f kcal", stats.calories, stats.capacity);
std::string weight = weight_format(stats.weight, settings.measurement); ImGui::SameLine(0, 0);
text_fit_draw(weight, {ImGui::GetContentRegionAvail().x, FONT_HEADER_2}, FONT_HEADER_2); ImGui::TextColored(imvec4_get(GRAY), " (up to %.0f)", body.capacity_max_get());
ImGui::SetItemTooltip("%s", weight.c_str()); ImGui::Text("Rate: %0.2f%% / sec", stats.digestionRate * TICK_RATE);
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::PushFont(nullptr, FONT_HEADER_2);
ImGui::SeparatorText("Stats"); 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 Spawned: %i", stats.totalSpawned);
ImGui::Text("Total Items Consumed: %i", stats.totalConsumed); ImGui::Text("Total Items Consumed: %i", stats.totalConsumed);
ImGui::Text("Total Playtime: %s", playtime_format(stats.playtime).c_str()); 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::PushFont(nullptr, FONT_HEADER_2);
ImGui::BeginDisabled(!autofeed.is_available(play)); ImGui::SeparatorText("Cheats");
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::PopFont(); ImGui::PopFont();
is_fx(ImGui::SliderFloat("Calories", &stats.calories, 0, body.capacity_max_get(), "%0.0f kcal"));
if (isRandom || isHelp || isFeeling) is_fx(ImGui::SliderFloat("Capacity", &stats.capacity, traits.capacityMin, traits.capacityMax, "%0.0f kcal"));
ImGui::Separator(); if (is_fx(ImGui::SliderFloat("Weight", &stats.weight, traits.weight, traits.weightMax, "%0.2f kg")))
auto dialogue_button = [&](bool isShown, float size, const char* label, Entry* entry) { {
if (!isShown) body.stage_set(body.stage_get(stats.weight));
return; body.idle_play();
ImGui::PushFont(nullptr, size); }
if (is_fx(ImGui::Button(label, {width, 0}))) int stage = body.stage + 1;
play.text.set(play, entry); if (is_fx(ImGui::SliderInt("Stage", &stage, 1, (int)data.stages.size() + 1)))
ImGui::PopFont(); {
}; body.stage_set(stage - 1);
dialogue_button(isRandom, FONT_HEADER_1, "Let's chat!", dialogue.hook_get(HOOK_RANDOM)); stats.weight = body.stage_threshold_get(body.stage);
dialogue_button(isHelp, FONT_HEADER_2, "Help", dialogue.hook_get(HOOK_HELP)); body.idle_play();
dialogue_button(isFeeling, FONT_HEADER_2, "How are you feeling?", dialogue.pool_get(feelingPool)); }
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. // The items as a grid of buttons, beside the selected one's details. Icons are drawn once per character.
void Ui::catalog_tab_draw(Play& play) void Ui::catalog_draw(Play& play)
{ {
Character& data = *play.character; Character& data = *play.character;
std::vector<Item>& items = data.catalog.items; 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}); itemSizes[i] = canvas_draw(*renderer, itemCanvases[i], actor, {ITEM_SIZE, ITEM_SIZE});
} }
} }
bool isSelected = selectedItem >= 0 && selectedItem < (int)items.size(); ImGuiStyle& style = ImGui::GetStyle();
bool isDiscovered = isSelected && play.items.discovered[selectedItem];
ImVec2 available = ImGui::GetContentRegionAvail(); ImVec2 available = ImGui::GetContentRegionAvail();
float infoHeight = isSelected || items.empty() ? available.y / 2 : 0; 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)));
if (ImGui::BeginChild("##Items", {available.x, available.y - infoHeight - ImGui::GetStyle().ItemSpacing.y})) 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++) for (int i = 0; i < (int)items.size(); i++)
{ {
ImGui::PushID(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) if (isColored)
style_push(*items[i].color); style_push(*items[i].color);
bool isCurrent = i == selectedItem; bool isCurrent = i == selectedItem;
@@ -767,210 +884,111 @@ void Ui::catalog_tab_draw(Play& play)
for (ImGuiCol id : {ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive}) for (ImGuiCol id : {ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive})
ImGui::PushStyleColor(id, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered)); ImGui::PushStyleColor(id, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered));
ImVec2 image = itemSizes[i]; ImVec2 image = itemSizes[i];
ImVec4 tint = play.items.discovered[i] ? ImVec4(1, 1, 1, 1) : ImVec4(0, 0, 0, UNDISCOVERED_ALPHA); ImVec2 padding = style.FramePadding;
ImVec2 padding = ImGui::GetStyle().FramePadding;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
{padding.x + (ITEM_SIZE - image.x) / 2, padding.y + (ITEM_SIZE - image.y) / 2}); {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}, {}, if (is_fx(ImGui::ImageButton("##Item", texture_id_get(itemCanvases[i].texture), image, {0, 0}, {1, 1}, {},
tint))) tint)))
selectedItem = i; selectedItem = i;
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (isCurrent) if (isCurrent)
ImGui::PopStyleColor(3); 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) if (isColored)
style_pop(); style_pop();
float next = ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + ITEM_SIZE; if ((i + 1) % columns != 0 && i + 1 < (int)items.size())
if (i + 1 < (int)items.size() && next < right)
ImGui::SameLine(); ImGui::SameLine();
ImGui::PopID(); ImGui::PopID();
} }
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !isAnyHovered)
selectedItem = -1;
} }
ImGui::EndChild(); 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) bool isSelected = selectedItem >= 0 && selectedItem < (int)items.size();
return; bool isFound = isSelected && play.items.discovered[selectedItem];
bool isColored = isDiscovered && items[selectedItem].color; bool isColored = isFound && items[selectedItem].color;
if (isColored) if (isColored)
style_push(*items[selectedItem].color); style_push(*items[selectedItem].color);
if (ImGui::BeginChild("##Info", {available.x, infoHeight})) if (ImGui::BeginChild("##Details", {0, available.y}))
{ {
ImGui::Separator(); if (items.empty())
ImGui::PushFont(nullptr, FONT_HEADER_2); ImGui::TextWrapped("No items.");
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();
}
else if (isSelected) 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(); ImGui::EndChild();
if (isColored) if (isColored)
style_pop(); 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; if (play.text.isScene)
Body& body = play.body; panel = PANEL_NONE;
Stats& stats = body.stats; play.isBlocked = panel != PANEL_NONE;
if (ImGui::BeginChild("##Cheats")) if (panel == PANEL_NONE)
{ return;
is_fx(ImGui::SliderFloat("Calories", &stats.calories, 0, body.capacity_max_get(), "%0.0f kcal")); const char* title = PANEL_NAMES[panel];
is_fx(ImGui::SliderFloat("Capacity", &stats.capacity, traits.capacityMin, traits.capacityMax, "%0.0f kcal")); float above = screen.y - play.inset - TOGGLE_HEIGHT;
if (is_fx(ImGui::SliderFloat("Weight", &stats.weight, traits.weight, traits.weightMax, "%0.2f kg"))) ImVec2 size = {std::min(WINDOW_WIDTH, screen.x * WINDOW_FIT), std::min(WINDOW_HEIGHT, above * WINDOW_FIT)};
{
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;
ImGui::SetNextWindowSize(size); ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(position); ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f});
if (ImGui::Begin("##Tools", nullptr, PANEL_FLAGS)) if (!ImGui::IsPopupOpen(title))
ImGui::OpenPopup(title);
bool isOpen = true;
if (ImGui::BeginPopupModal(title, &isOpen, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{ {
float side = ImGui::GetContentRegionAvail().x; if (panel == PANEL_STATS)
ImFont* icons = data.menu.iconsFont; stats_draw(play);
auto tool_button = [&](const char* id, const std::string& glyph, const char* tooltip, bool isActive, else if (panel == PANEL_CATALOG)
bool isDisabled) { catalog_draw(play);
bool isIcon = icons && !glyph.empty(); else
ImGui::PushID(id); settings_draw(settings);
if (isActive) if (ImGui::IsKeyPressed(ImGuiKey_Escape))
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++)
{ {
InteractArea& type = data.interactTypes[i]; isOpen = false;
const char* tooltip = play.autofeed.isEnabled ? "Disabled while Autofeed is active!" ImGui::CloseCurrentPopup();
: 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;
} }
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) void Ui::play_update(Play& play, Settings& settings)
{ {
character = play.character; character = play.character;
ImGuiStyle& style = ImGui::GetStyle(); measurement = &settings.measurement;
ImVec2 screen = ImGui::GetIO().DisplaySize; ImVec2 screen = ImGui::GetIO().DisplaySize;
float barWidth = std::max(ImGui::GetTextLineHeightWithSpacing(), style.WindowMinSize.x); if (play.items.tooltip >= 0 && !play.isBlocked && ImGui::BeginTooltip())
float height = screen.y - style.WindowPadding.y * 2;
if (play.items.tooltip >= 0 && ImGui::BeginTooltip())
{ {
item_details_draw(*character, &character->catalog.items[play.items.tooltip]); item_details_draw(*character, &character->catalog.items[play.items.tooltip]);
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
taskbar_draw(play, screen);
toasts_draw(play); toasts_draw(play);
text_draw(play, screen); text_draw(play, screen);
if (play.text.isScene) window_draw(play, settings, screen);
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);
} }
void Ui::select_update(std::vector<Character>& characters, Settings& settings) 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 side = ImGui::GetContentRegionAvail().x / 4;
float right = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x; 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]; Character& entry = characters[i];
ImGui::PushID(i); ImGui::PushID(i);
+32 -16
View File
@@ -4,18 +4,32 @@
#include "save.h" #include "save.h"
#include <imgui.h> #include <imgui.h>
constexpr float FONT_NORMAL = 20; constexpr float FONT_NORMAL = 26;
constexpr float FONT_HEADER_3 = 24; constexpr float FONT_HEADER_3 = 30;
constexpr float FONT_HEADER_2 = 30; constexpr float FONT_HEADER_2 = 34;
constexpr float FONT_HEADER_1 = 40; constexpr float FONT_DIALOGUE = 32;
constexpr float FONT_HUGE = 80;
class Play; class Play;
enum Direction enum Direction
{ {
DIRECTION_LEFT, DIRECTION_UP,
DIRECTION_RIGHT DIRECTION_DOWN
};
enum Panel
{
PANEL_NONE,
PANEL_STATS,
PANEL_CATALOG,
PANEL_SETTINGS
};
enum ButtonState
{
BUTTON_NORMAL,
BUTTON_ACTIVE,
BUTTON_DISABLED
}; };
enum CommandType enum CommandType
@@ -39,7 +53,7 @@ using Commands = std::vector<Command>;
void style_set(Color color, float rounding = 0); 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 class Ui
{ {
public: public:
@@ -52,12 +66,13 @@ public:
Character* catalogCharacter = nullptr; Character* catalogCharacter = nullptr;
Character* character = nullptr; Character* character = nullptr;
Commands* commands = nullptr; Commands* commands = nullptr;
Measurement* measurement = nullptr;
Renderer* renderer = nullptr; Renderer* renderer = nullptr;
float menuSlide = 1; float taskbarSlide = 1;
float toolsSlide = 0;
int characterIndex = 0; int characterIndex = 0;
int selectedIndex = -1; int selectedIndex = -1;
int selectedItem = -1; int selectedItem = -1;
Panel panel = PANEL_NONE;
bool isFullscreen = false; bool isFullscreen = false;
bool isNewGameWarning = false; bool isNewGameWarning = false;
bool isSelectedSaved = false; bool isSelectedSaved = false;
@@ -67,14 +82,15 @@ public:
private: private:
bool is_fx(bool result); 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 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 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 text_draw(Play& play, ImVec2 screen);
void toasts_draw(Play& play); void toasts_draw(Play& play);
}; };