Select screen with a centered preview and a portrait taskbar

This commit is contained in:
shweet committed 2026-11-13 00:11:24 -05:00
1 parent e0e43d9285
commit ee13be56d7
8 files changed
+168 -161

No files matched your search

-2
View File
@@ -3,10 +3,8 @@
constexpr const char* AREA_PATH = "gfx/areas/area.anm2";
constexpr const char* BOUNDS_NULL = "Bounds";
constexpr const char* COMMON_FOLDER = "common/";
constexpr const char* CURSOR_PATH = "gfx/cursor/cursor.anm2";
constexpr const char* DATA_FILE = "character.json";
constexpr const char* ICONS_PATH = "gfx/ui/icons.anm2";
constexpr const char* PORTRAIT_PATH = "gfx/preview/portrait.png";
constexpr const char* RENDER_PATH = "gfx/preview/render.png";
constexpr const char* SPAWN_NULL = "ItemSpawn";
+2
View File
@@ -63,6 +63,8 @@ enum Hook
};
constexpr const char* ANM2_PATH = "gfx/character/character.anm2";
constexpr const char* COMMON_FOLDER = "common/";
constexpr const char* ICONS_PATH = "gfx/ui/icons.anm2";
constexpr const char* CURSOR_ANIMATION_CLICK = "Click";
constexpr const char* CURSOR_ANIMATION_DISPOSE = "Dispose";
constexpr const char* CURSOR_ANIMATION_GRAB = "Grab";
+1
View File
@@ -136,6 +136,7 @@ static void init()
app.ui.audio = &app.audio;
app.ui.renderer = &app.renderer;
app.ui.commands = &app.commands;
app.ui.iconsDefault.load(ICONS_PATH, {path_get(COMMON_FOLDER)}, app.renderer, app.audio);
app.console.renderer = &app.renderer;
app.console.commands = &app.commands;
app.characters = characters_load(app.renderer);
+154 -154
View File
@@ -43,8 +43,12 @@ constexpr float TASKBAR_FIT = 0.8f;
constexpr int BUTTONS_FIXED = 9;
constexpr float INFO_GAP = 4;
constexpr int COOLDOWN_SEGMENTS = 48;
constexpr const char* PANEL_ICONS[] = {"", "stats", "catalog", "settings", "cheats"};
constexpr const char* PANEL_NAMES[] = {"", "Stats", "Catalog", "Settings", "Cheats"};
constexpr const char* PANEL_ICONS[] = {"", "stats", "catalog", "settings", "cheats", "credits"};
constexpr const char* PANEL_NAMES[] = {"", "Stats", "Catalog", "Settings", "Cheats", "Credits"};
constexpr float PREVIEW_FRACTION = 0.68f;
constexpr float PREVIEW_WIDTH = 760;
constexpr float SELECT_BUTTON_SIZE = 112;
constexpr int SELECT_BUTTONS_FIXED = 2;
constexpr float KG_TO_G = 1000;
constexpr float KG_TO_LB = 2.20462f;
constexpr float LB_TO_OZ = 16;
@@ -394,7 +398,7 @@ void Ui::command_add(CommandType type, const std::string& key)
// An icon from the character's icon sheet (see Character::icons), fit into a square and tinted.
void Ui::icon_draw(const char* name, ImVec2 position, float size, ImU32 tint)
{
Anm2* icons = character ? character->icons : nullptr;
Anm2* icons = character && character->icons ? character->icons : &iconsDefault;
const Anm2Region* region = icons ? icons->region_get(0, name) : nullptr;
Texture* sheet = region && !icons->sheets.empty() ? icons->sheets[0] : nullptr;
if (!sheet)
@@ -446,10 +450,10 @@ static void cooldown_draw(float progress)
}
}
void Ui::window_open(Play& play, Panel next)
void Ui::sound_play(SoundId id)
{
panel = next;
play.audio->play(play.character->sounds[SOUND_OPEN]);
if (character)
audio->play(character->sounds[id]);
}
// The bar along the bottom: the character's name and body at a glance, then a button for everything. A thin bar above
@@ -618,7 +622,7 @@ void Ui::taskbar_draw(Play& play, ImVec2 screen)
continue;
ImGui::SameLine();
if (icon_button(PANEL_ICONS[each], PANEL_NAMES[each], BUTTON_NORMAL))
window_open(play, each);
panel = each;
}
ImGui::SameLine();
if (icon_button("help", "Help", state_get(dialogue.is_hook(HOOK_HELP))))
@@ -946,49 +950,46 @@ void Ui::catalog_draw(Play& play)
style_pop();
}
// The open window (stats, catalog, settings or cheats): one at a time, fixed in size above the taskbar, closed by its
// X, escape or a click off it, and holding the mouse while open.
void Ui::window_draw(Play& play, Settings& settings, ImVec2 screen)
// Starts the open window (one at a time, from the taskbar), fixed in size above the bottom inset. Its contents follow,
// then window_end.
bool Ui::window_begin(ImVec2 screen, float inset)
{
if (play.text.isScene)
panel = PANEL_NONE;
play.isBlocked = panel != PANEL_NONE;
if (panel == PANEL_NONE)
return;
return false;
const char* title = PANEL_NAMES[panel];
float above = screen.y - play.inset - TOGGLE_HEIGHT;
float above = screen.y - inset - TOGGLE_HEIGHT;
ImVec2 size = {std::min(WINDOW_WIDTH, screen.x * WINDOW_FIT), std::min(WINDOW_HEIGHT, above * WINDOW_FIT)};
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f});
if (!ImGui::IsPopupOpen(title))
ImGui::OpenPopup(title);
bool isOpen = true;
if (ImGui::BeginPopupModal(title, &isOpen, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
if (panel == PANEL_STATS)
stats_draw(play);
else if (panel == PANEL_CATALOG)
catalog_draw(play);
else if (panel == PANEL_CHEATS)
cheats_draw(play);
else
settings_draw(settings);
ImVec2 min = ImGui::GetWindowPos();
ImVec2 max = {min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()};
bool isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy);
bool isClickedOff =
isFocused && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(min, max, false);
if (ImGui::IsKeyPressed(ImGuiKey_Escape) || isClickedOff)
{
isOpen = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
ImGui::OpenPopup(title);
sound_play(SOUND_OPEN);
}
if (isOpen)
return;
isWindowOpen = true;
if (ImGui::BeginPopupModal(title, &isWindowOpen, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
return true;
panel = PANEL_NONE;
play.audio->play(play.character->sounds[SOUND_CLOSE]);
return false;
}
// Ends the open window, closing it on its X, escape or a click off it.
void Ui::window_end()
{
ImVec2 min = ImGui::GetWindowPos();
ImVec2 max = {min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()};
bool isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy);
bool isClickedOff =
isFocused && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(min, max, false);
if (ImGui::IsKeyPressed(ImGuiKey_Escape) || isClickedOff)
isWindowOpen = false;
if (!isWindowOpen)
{
ImGui::CloseCurrentPopup();
panel = PANEL_NONE;
sound_play(SOUND_CLOSE);
}
ImGui::EndPopup();
}
void Ui::play_update(Play& play, Settings& settings)
@@ -1004,15 +1005,29 @@ void Ui::play_update(Play& play, Settings& settings)
taskbar_draw(play, screen);
toasts_draw(play);
text_draw(play, screen);
window_draw(play, settings, screen);
if (play.text.isScene)
panel = PANEL_NONE;
play.isBlocked = panel != PANEL_NONE;
if (!window_begin(screen, play.inset))
return;
if (panel == PANEL_STATS)
stats_draw(play);
else if (panel == PANEL_CATALOG)
catalog_draw(play);
else if (panel == PANEL_CHEATS)
cheats_draw(play);
else
settings_draw(settings);
window_end();
}
void Ui::select_update(std::vector<Character>& characters, Settings& settings)
{
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 screen = ImGui::GetIO().DisplaySize;
ImVec2 half = {screen.x / 2 - style.WindowPadding.x * 2, screen.y / 2 - style.WindowPadding.y * 2};
character = nullptr;
measurement = &settings.measurement;
buttonSize = SELECT_BUTTON_SIZE;
characterIndex = characters.empty() ? -1 : std::clamp(characterIndex, 0, (int)characters.size() - 1);
Character* selected = characterIndex >= 0 ? &characters[characterIndex] : nullptr;
if (selected && selectedIndex != characterIndex)
@@ -1022,88 +1037,111 @@ void Ui::select_update(std::vector<Character>& characters, Settings& settings)
selectedIndex = characterIndex;
}
int count = (int)characters.size() + SELECT_BUTTONS_FIXED;
float width = count * buttonSize + (count - 1) * style.ItemSpacing.x + style.ItemSpacing.x * INFO_GAP +
style.WindowPadding.x * 2;
float height = buttonSize + style.WindowPadding.y * 2;
ImGui::SetNextWindowPos({(screen.x - width) / 2, screen.y - height});
ImGui::SetNextWindowSize({width, height});
if (ImGui::Begin("##Select Bar", nullptr, BAR_FLAGS))
{
for (int i = 0; i < (int)characters.size(); i++)
{
Character& entry = characters[i];
ImGui::PushID(i);
style_push(entry.color);
bool isCurrent = i == characterIndex;
if (isCurrent)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered));
ImVec2 size = {buttonSize, buttonSize};
bool isPressed = entry.portrait
? ImGui::ImageButton("##Portrait", texture_id_get(*entry.portrait),
{size.x - style.FramePadding.x * 2, size.y - style.FramePadding.y * 2})
: ImGui::Button(entry.name.c_str(), size);
if (is_fx(isPressed))
characterIndex = i;
if (isCurrent)
ImGui::PopStyleColor();
ImGui::SetItemTooltip("%s", entry.name.c_str());
style_pop();
ImGui::PopID();
ImGui::SameLine();
}
ImGui::SameLine(0, style.ItemSpacing.x * INFO_GAP / 2);
ImVec2 cursor = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddLine({cursor.x, cursor.y}, {cursor.x, cursor.y + buttonSize},
ImGui::GetColorU32(ImGuiCol_Separator));
ImGui::SameLine(0, style.ItemSpacing.x * INFO_GAP / 2);
ImGui::BeginDisabled(!selected);
if (icon_button("credits", "Credits", BUTTON_NORMAL))
panel = PANEL_CREDITS;
ImGui::EndDisabled();
ImGui::SameLine();
if (icon_button("settings", "Settings", BUTTON_NORMAL))
panel = PANEL_SETTINGS;
}
ImGui::End();
if (selected)
{
float above = screen.y - height;
ImVec2 size = {std::min(PREVIEW_WIDTH, screen.x * WINDOW_FIT), above * WINDOW_FIT};
style_push(selected->color);
ImGui::SetNextWindowSize(half);
ImGui::SetNextWindowPos({style.WindowPadding.x, style.WindowPadding.y});
if (ImGui::Begin("##Preview", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Preview Tabs"))
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos({screen.x / 2, above / 2}, ImGuiCond_Always, {0.5f, 0.5f});
if (ImGui::Begin("##Preview", nullptr, PANEL_FLAGS))
{
ImVec2 available = ImGui::GetContentRegionAvail();
if (is_fx(ImGui::BeginTabItem("Render")))
if (ImGui::BeginTabBar("##Preview Tabs"))
{
if (Texture* render = selected->render)
if (is_fx(ImGui::BeginTabItem("In Game")))
{
float scale = std::min(available.x / render->width, available.y / render->height);
ImVec2 size = {render->width * scale, render->height * scale};
ImGui::SetCursorPos({ImGui::GetCursorPosX() + (available.x - size.x) / 2,
ImGui::GetCursorPosY() + (available.y - size.y) / 2});
ImGui::Image(texture_id_get(*render), size);
isPreviewRender = false;
ImGui::EndTabItem();
}
else
ImGui::TextWrapped("%s\n%s", selected->name.c_str(), selected->description.c_str());
ImGui::EndTabItem();
if (is_fx(ImGui::BeginTabItem("Render")))
{
isPreviewRender = true;
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
if (is_fx(ImGui::BeginTabItem("In Game")))
ImVec2 available = {ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y * PREVIEW_FRACTION};
ImVec2 shown = {};
ImTextureID texture = {};
if (isPreviewRender && selected->render)
{
Texture& render = *selected->render;
float scale = std::min(available.x / render.width, available.y / render.height);
shown = {render.width * scale, render.height * scale};
texture = texture_id_get(render);
}
else
{
Anm2* anm2 = selected->anm2_get(ANM2_PATH, *renderer, *audio);
if (previewActor.anm2 != anm2 || !anm2)
previewActor = Actor(anm2);
previewActor.update(*audio);
ImVec2 size = canvas_draw(*renderer, previewCanvas, previewActor, available);
ImGui::SetCursorPos({ImGui::GetCursorPosX() + (available.x - size.x) / 2,
ImGui::GetCursorPosY() + (available.y - size.y) / 2});
ImGui::Image(texture_id_get(previewCanvas.texture), size);
ImGui::EndTabItem();
shown = canvas_draw(*renderer, previewCanvas, previewActor, available);
texture = texture_id_get(previewCanvas.texture);
}
ImGui::EndTabBar();
}
ImGui::End();
ImVec2 start = ImGui::GetCursorPos();
ImGui::SetCursorPos({start.x + (available.x - shown.x) / 2, start.y + (available.y - shown.y) / 2});
if (shown.x > 0)
ImGui::Image(texture, shown);
ImGui::SetCursorPos({start.x, start.y + available.y});
ImGui::Separator();
ImGui::SetNextWindowSize(half);
ImGui::SetNextWindowPos({style.WindowPadding.x, screen.y / 2 + style.WindowPadding.y});
if (ImGui::Begin("##Info", nullptr, PANEL_FLAGS))
{
float footer = ImGui::GetTextLineHeightWithSpacing() + style.WindowPadding.y + style.ItemSpacing.y;
ImGui::PushFont(nullptr, FONT_HEADER_3);
if (ImGui::BeginChild("##Info Child", {0, ImGui::GetContentRegionAvail().y - footer}))
{
ImGui::TextUnformatted(selected->name.c_str());
ImGui::PushFont(nullptr, FONT_NORMAL);
if (ImGui::BeginTabBar("##Info Tabs"))
{
ImGui::PushFont(nullptr, FONT_HEADER_2);
if (ImGui::BeginTabItem("Overview"))
{
ImGui::Separator();
if (!selected->description.empty())
{
ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY));
ImGui::TextWrapped("%s", selected->description.c_str());
ImGui::PopStyleColor();
ImGui::Separator();
}
float weight = isSelectedSaved ? selectedStats.weight : selected->traits.weight;
ImGui::Text("Weight: %s", weight_format(weight, settings.measurement).c_str());
ImGui::Text("Stages: %i", (int)selected->stages.size() + 1);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Credits"))
{
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY));
ImGui::TextWrapped("%s", selected->credits.empty() ? "No credits listed."
: selected->credits.c_str());
ImGui::PopStyleColor();
ImGui::EndTabItem();
}
ImGui::PopFont();
ImGui::EndTabBar();
}
ImGui::PopFont();
}
ImGui::EndChild();
ImGui::PushFont(nullptr, FONT_HEADER_2);
ImGui::TextUnformatted(selected->name.c_str());
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY));
ImGui::TextWrapped("%s", selected->description.c_str());
ImGui::PopStyleColor();
float weight = isSelectedSaved ? selectedStats.weight : selected->traits.weight;
ImGui::Text("Weight: %s Stages: %i", weight_format(weight, settings.measurement).c_str(),
(int)selected->stages.size() + 1);
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - style.WindowPadding.y - ImGui::GetFrameHeight());
ImVec2 buttonSize = {row_width_get(isSelectedSaved ? 2 : 1), 0};
if (isSelectedSaved)
{
@@ -1121,9 +1159,8 @@ void Ui::select_update(std::vector<Character>& characters, Settings& settings)
isNewGameWarning = isSelectedSaved;
}
tooltip_set("Start a new game.\nThis will delete progress!");
ImGui::PopFont();
ImGui::SetNextWindowSize({screen.x / 2, 0});
ImGui::SetNextWindowSize({size.x, 0});
ImGui::SetNextWindowPos({screen.x / 2, screen.y / 2}, ImGuiCond_Always, {0.5f, 0.5f});
if (ImGui::BeginPopupModal("New Game Warning", &isNewGameWarning,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize))
@@ -1142,53 +1179,16 @@ void Ui::select_update(std::vector<Character>& characters, Settings& settings)
style_pop();
}
ImGui::SetNextWindowSize({screen.x / 2 - style.WindowPadding.x, screen.y - style.WindowPadding.y * 2});
ImGui::SetNextWindowPos({screen.x / 2, style.WindowPadding.y});
if (ImGui::Begin("##Main Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Main Tabs"))
if (window_begin(screen, height))
{
if (is_fx(ImGui::BeginTabItem("Characters")))
if (panel == PANEL_CREDITS && selected)
{
if (ImGui::BeginPopupContextWindow("##Characters Context"))
{
if (ImGui::MenuItem("Refresh"))
{
command_add(COMMAND_RELOAD);
selectedIndex = -1;
}
ImGui::EndPopup();
}
float side = ImGui::GetContentRegionAvail().x / 4;
float right = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
for (int i = 0; i < (int)characters.size(); i++)
{
Character& entry = characters[i];
ImGui::PushID(i);
style_push(entry.color);
bool isCurrent = i == characterIndex;
if (isCurrent)
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered));
bool isPressed = entry.portrait ? ImGui::ImageButton(entry.name.c_str(),
texture_id_get(*entry.portrait), {side, side})
: ImGui::Button(entry.name.c_str(), {side, side});
if (is_fx(isPressed))
characterIndex = i;
if (isCurrent)
ImGui::PopStyleColor();
ImGui::SetItemTooltip("%s", entry.name.c_str());
style_pop();
float next = ImGui::GetItemRectMax().x + style.ItemSpacing.x + side;
if (i + 1 < (int)characters.size() && next < right)
ImGui::SameLine();
ImGui::PopID();
}
ImGui::EndTabItem();
ImGui::PushStyleColor(ImGuiCol_Text, imvec4_get(GRAY));
ImGui::TextWrapped("%s", selected->credits.empty() ? "No credits listed." : selected->credits.c_str());
ImGui::PopStyleColor();
}
if (is_fx(ImGui::BeginTabItem("Settings")))
{
else
settings_draw(settings);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
window_end();
}
ImGui::End();
}
+8 -3
View File
@@ -23,7 +23,8 @@ enum Panel
PANEL_STATS,
PANEL_CATALOG,
PANEL_SETTINGS,
PANEL_CHEATS
PANEL_CHEATS,
PANEL_CREDITS
};
enum ButtonState
@@ -61,6 +62,7 @@ public:
std::vector<Canvas> itemCanvases;
std::vector<ImVec2> itemSizes;
Actor previewActor;
Anm2 iconsDefault;
Canvas previewCanvas;
Stats selectedStats;
Audio* audio = nullptr;
@@ -77,7 +79,9 @@ public:
Panel panel = PANEL_NONE;
bool isFullscreen = false;
bool isNewGameWarning = false;
bool isPreviewRender = false;
bool isSelectedSaved = false;
bool isWindowOpen = false;
void play_update(Play& play, Settings& settings);
void select_update(std::vector<Character>& characters, Settings& settings);
@@ -87,9 +91,10 @@ private:
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 sound_play(SoundId id);
void taskbar_draw(Play& play, ImVec2 screen);
void window_draw(Play& play, Settings& settings, ImVec2 screen);
bool window_begin(ImVec2 screen, float inset);
void window_end();
void stats_draw(Play& play);
void cheats_draw(Play& play);
void catalog_draw(Play& play);