The Mega Snivy Update
Some checks failed
Build / Build Game (push) Has been cancelled

This commit is contained in:
2026-02-28 21:48:00 -05:00
parent 8b2edd1359
commit 17f3348e94
163 changed files with 8725 additions and 13281 deletions

View File

@@ -0,0 +1,56 @@
#include "configuration.hpp"
#include <glm/gtc/type_ptr.hpp>
#include <imgui.h>
#include "../util/math.hpp"
#include "../util/imgui/style.hpp"
#include "../util/imgui/widget.hpp"
#include "../util/measurement.hpp"
using namespace game::util;
using namespace game::util::imgui;
namespace game::state
{
void Configuration::update(Resources& resources, Mode mode)
{
auto& settings = resources.settings;
auto& measurementSystem = settings.measurementSystem;
auto& volume = settings.volume;
auto& color = settings.color;
ImGui::SeparatorText("Measurement System");
WIDGET_FX(ImGui::RadioButton("Metric", (int*)&measurementSystem, measurement::METRIC));
ImGui::SetItemTooltip("%s", "Use kilograms (kg).");
ImGui::SameLine();
WIDGET_FX(ImGui::RadioButton("Imperial", (int*)&measurementSystem, measurement::IMPERIAL));
ImGui::SetItemTooltip("%s", "Use pounds (lbs).");
ImGui::SeparatorText("Sound");
if (WIDGET_FX(ImGui::SliderInt("Volume", &volume, 0, 100, "%d%%")))
resources.volume_set(math::to_unit((float)volume));
ImGui::SetItemTooltip("%s", "Adjust master volume.");
ImGui::SeparatorText("Appearance");
if (WIDGET_FX(
ImGui::ColorEdit3("Color", value_ptr(color), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip)))
style::color_set(color);
ImGui::SetItemTooltip("%s", "Change the UI color.");
ImGui::Separator();
if (WIDGET_FX(ImGui::Button("Reset to Default", ImVec2(-FLT_MIN, 0)))) settings = resource::xml::Settings();
if (mode == MAIN)
{
ImGui::Separator();
if (WIDGET_FX(ImGui::Button("Save", ImVec2(-FLT_MIN, 0)))) isSave = true;
ImGui::SetItemTooltip("%s", "Save the game.\n(Note: the game autosaves frequently.)");
if (WIDGET_FX(ImGui::Button("Return to Characters", ImVec2(-FLT_MIN, 0)))) isGoToSelect = true;
ImGui::SetItemTooltip("%s", "Go back to the character selection screen.\nProgress will be saved.");
}
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../resources.hpp"
namespace game::state
{
class Configuration
{
public:
enum Mode
{
SELECT,
MAIN
};
bool isGoToSelect{};
bool isSave{};
void update(Resources&, Mode = SELECT);
};
}

294
src/state/main.cpp Normal file
View File

@@ -0,0 +1,294 @@
#include "main.hpp"
#include <glm/glm.hpp>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include "../util/imgui.hpp"
#include "../util/imgui/style.hpp"
#include "../util/imgui/widget.hpp"
#include "../util/math.hpp"
using namespace game::resource;
using namespace game::util;
using namespace game::state::main;
using namespace glm;
namespace game::state
{
World::Focus Main::focus_get()
{
return menu.isOpen && tools.isOpen ? World::MENU_TOOLS
: menu.isOpen ? World::MENU
: tools.isOpen ? World::TOOLS
: World::CENTER;
}
void Main::set(Resources& resources, int characterIndex, enum Game game)
{
auto& data = resources.character_get(characterIndex);
auto& saveData = data.save;
auto& itemSchema = data.itemSchema;
auto& dialogue = data.dialogue;
auto& menuSchema = data.menuSchema;
this->characterIndex = characterIndex;
character =
entity::Character(data, vec2(World::BOUNDS.x + World::BOUNDS.z * 0.5f, World::BOUNDS.w - World::BOUNDS.y));
auto isAlternateSpritesheet =
(game == NEW_GAME && math::random_percent_roll(data.alternateSpritesheet.chanceOnNewGame));
if (isAlternateSpritesheet || saveData.isAlternateSpritesheet)
{
character.spritesheet_set(entity::Character::ALTERNATE);
if (game == NEW_GAME) character.data.alternateSpritesheet.sound.play();
}
character.totalCaloriesConsumed = saveData.totalCaloriesConsumed;
character.totalFoodItemsEaten = saveData.totalFoodItemsEaten;
characterManager = CharacterManager{};
cursor = entity::Cursor(character.data.cursorSchema.anm2);
menu.inventory = Inventory{};
for (auto& [id, quantity] : saveData.inventory)
{
if (quantity == 0) continue;
menu.inventory.values[id] = quantity;
}
itemManager = ItemManager{};
for (auto& item : saveData.items)
{
auto& anm2 = itemSchema.anm2s.at(item.id);
auto chewAnimation = itemSchema.animations.chew + std::to_string(item.chewCount);
auto animationIndex = item.chewCount > 0 ? anm2.animationMap[chewAnimation] : -1;
auto& saveItem = itemSchema.anm2s.at(item.id);
itemManager.items.emplace_back(saveItem, item.position, item.id, item.chewCount, animationIndex, item.velocity,
item.rotation);
}
imgui::style::rounding_set(menuSchema.rounding);
imgui::widget::sounds_set(&menuSchema.sounds.hover, &menuSchema.sounds.select);
menu.play = Play(character);
menu.play.totalPlays = saveData.totalPlays;
menu.play.highScore = saveData.highScore;
menu.play.bestCombo = saveData.bestCombo;
menu.play.gradeCounts = saveData.gradeCounts;
menu.play.isHighScoreAchieved = saveData.highScore > 0 ? true : false;
menu.isChat = character.data.dialogue.help.is_valid() || character.data.dialogue.random.is_valid();
text.entry = nullptr;
text.isEnabled = false;
if (auto font = character.data.menuSchema.font.get()) ImGui::GetIO().FontDefault = font;
if (game == NEW_GAME && dialogue.start.is_valid())
{
character.queue_play({.animation = dialogue.start.animation, .isInterruptible = false});
character.tick();
isWindows = false;
isStart = true;
}
isPostgame = saveData.isPostgame;
if (isPostgame)
menu.isCheats = true;
else
menu.isCheats = true; //false;
worldCanvas.size_set(imgui::to_vec2(ImGui::GetMainViewport()->Size));
world.set(character, worldCanvas, focus_get());
}
void Main::exit(Resources& resources)
{
imgui::style::rounding_set();
imgui::widget::sounds_set(nullptr, nullptr);
ImGui::GetIO().FontDefault = resources.font.get();
save(resources);
}
void Main::tick(Resources& resources)
{
character.tick();
cursor.tick();
menu.tick();
toasts.tick();
text.tick(character);
for (auto& item : itemManager.items)
item.tick();
}
void Main::update(Resources& resources)
{
auto focus = focus_get();
auto& dialogue = character.data.dialogue;
if (isWindows)
{
menu.update(resources, itemManager, character, cursor, text, worldCanvas);
tools.update(character, cursor, world, focus, worldCanvas);
info.update(resources, character);
toasts.update();
}
if (text.isEnabled) text.update(character);
if (isStart)
{
if (!isStartBegin)
{
if (auto animation = character.animation_get())
{
if (animation->isLoop || character.state == entity::Actor::STOPPED)
{
text.set(dialogue.get(dialogue.start.id), character);
isStartBegin = true;
}
}
}
else if (!isStartEnd)
{
if (text.entry->is_last())
{
isWindows = true;
isStartEnd = true;
isStart = false;
world.character_focus(character, worldCanvas, focus_get());
}
}
}
if (character.isJustStageFinal && !isEnd && !isPostgame) isEnd = true;
if (isEnd)
{
if (!isEndBegin)
{
if (character.is_animation_finished())
{
text.set(dialogue.get(dialogue.end.id), character);
isEndBegin = true;
isWindows = false;
tools.isOpen = false;
menu.isOpen = false;
character.calories = 0;
character.digestionProgress = 0;
itemManager.items.clear();
itemManager.heldItemIndex = -1;
world.character_focus(character, worldCanvas, focus_get());
}
}
else if (!isEndEnd)
{
if (text.entry->is_last())
{
menu.isOpen = true;
isWindows = true;
isEndEnd = true;
isEnd = false;
isPostgame = true;
}
}
}
itemManager.update(character, cursor, areaManager, text, World::BOUNDS, worldCanvas);
characterManager.update(character, cursor, text, worldCanvas);
character.update();
cursor.update();
world.update(character, cursor, worldCanvas, focus);
if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.configuration.isSave)
{
save(resources);
autosaveTime = 0;
menu.configuration.isSave = false;
}
}
void Main::render(Resources& resources, Canvas& canvas)
{
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
auto size = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto& bgTexture = character.data.areaSchema.areas.at(areaManager.get(character)).texture;
auto windowModel = math::quad_model_get(vec2(size));
auto worldModel = math::quad_model_get(bgTexture.size);
worldCanvas.bind();
worldCanvas.size_set(size);
worldCanvas.clear();
worldCanvas.texture_render(textureShader, bgTexture.id, worldModel);
character.render(textureShader, rectShader, worldCanvas);
for (auto& item : itemManager.items)
item.render(textureShader, rectShader, worldCanvas);
if (menu.debug.isBoundsDisplay)
{
auto boundsModel =
math::quad_model_get(glm::vec2(World::BOUNDS.z, World::BOUNDS.w), glm::vec2(World::BOUNDS.x, World::BOUNDS.y),
glm::vec2(World::BOUNDS.x, World::BOUNDS.y) * 0.5f);
worldCanvas.rect_render(rectShader, boundsModel);
}
worldCanvas.unbind();
canvas.bind();
canvas.texture_render(textureShader, worldCanvas.texture, windowModel);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
cursor.render(textureShader, rectShader, canvas);
canvas.unbind();
SDL_HideCursor();
}
void Main::save(Resources& resources)
{
resource::xml::Save save;
save.weight = character.weight;
save.calories = character.calories;
save.capacity = character.capacity;
save.digestionRate = character.digestionRate;
save.eatSpeed = character.eatSpeed;
save.digestionProgress = character.digestionProgress;
save.isDigesting = character.isDigesting;
save.digestionTimer = character.digestionTimer;
save.totalCaloriesConsumed = character.totalCaloriesConsumed;
save.totalFoodItemsEaten = character.totalFoodItemsEaten;
save.totalPlays = menu.play.totalPlays;
save.highScore = menu.play.highScore;
save.bestCombo = menu.play.bestCombo;
save.gradeCounts = menu.play.gradeCounts;
save.isPostgame = isPostgame;
save.isAlternateSpritesheet = character.spritesheetType == entity::Character::ALTERNATE;
for (auto& [id, quantity] : menu.inventory.values)
{
if (quantity == 0) continue;
save.inventory[id] = quantity;
}
for (auto& item : itemManager.items)
save.items.emplace_back(item.schemaID, item.chewCount, item.position, item.velocity,
*item.overrides[item.rotationOverrideID].frame.rotation);
save.isValid = true;
resources.character_save_set(characterIndex, save);
save.serialize(character.data.save_path_get());
toasts.push("Saving...");
}
};

69
src/state/main.hpp Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include "../resources.hpp"
#include "main/area_manager.hpp"
#include "main/character_manager.hpp"
#include "main/info.hpp"
#include "main/item_manager.hpp"
#include "main/menu.hpp"
#include "main/text.hpp"
#include "main/toasts.hpp"
#include "main/tools.hpp"
#include "main/world.hpp"
namespace game::state
{
class Main
{
public:
static constexpr auto AUTOSAVE_TIME = 30.0f;
enum Game
{
NEW_GAME,
CONTINUE
};
entity::Character character;
entity::Cursor cursor;
main::Info info;
main::Menu menu;
main::Tools tools;
main::Text text;
main::World world;
main::Toasts toasts;
main::ItemManager itemManager{};
main::CharacterManager characterManager{};
main::AreaManager areaManager{};
int characterIndex{};
int areaIndex{};
float autosaveTime{};
bool isWindows{true};
bool isStartBegin{};
bool isStart{};
bool isStartEnd{};
bool isEndBegin{};
bool isEnd{};
bool isEndEnd{};
bool isPostgame{};
Canvas worldCanvas{main::World::SIZE};
Main() = default;
void set(Resources&, int characterIndex, Game = CONTINUE);
void exit(Resources& resources);
void update(Resources&);
void tick(Resources&);
void render(Resources&, Canvas&);
void save(Resources&);
main::World::Focus focus_get();
};
};

View File

@@ -0,0 +1,26 @@
#include "area_manager.hpp"
#include <imgui.h>
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
int AreaManager::get(entity::Character& character)
{
auto& data = character.data;
auto& schema = data.areaSchema;
if (schema.areas.empty()) return -1;
auto size = (int)data.stages.size();
for (int i = 0; i < size; i++)
{
auto& stage = data.stages[size - i - 1];
if (stage.areaID != -1) return stage.areaID;
}
return -1;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "../../entity/character.hpp"
namespace game::state::main
{
class AreaManager
{
public:
int get(entity::Character&);
};
}

View File

@@ -0,0 +1,120 @@
#include "character_manager.hpp"
#include "../../util/math.hpp"
#include <imgui.h>
#include <optional>
using namespace game::resource::xml;
using namespace game::util;
namespace game::state::main
{
void CharacterManager::update(entity::Character& character, entity::Cursor& cursor, Text& text, Canvas& canvas)
{
auto interact_area_override_tick = [](entity::Actor::Override& override_)
{
if (override_.frame.scale.has_value() && override_.frameBase.scale.has_value() && override_.time.has_value() &&
override_.timeStart.has_value())
{
auto percent = glm::clamp(*override_.time / *override_.timeStart, 0.0f, 1.0f);
auto elapsed = 1.0f - percent;
auto oscillation = cosf(elapsed * glm::tau<float>() * override_.cycles);
auto envelope = percent;
auto amplitude = glm::abs(*override_.frameBase.scale);
*override_.frame.scale = amplitude * (oscillation * envelope);
}
};
auto& dialogue = character.data.dialogue;
auto cursorWorldPosition = canvas.screen_position_convert(cursor.position);
auto isMouseLeftClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isImguiCaptureMouse = ImGui::GetIO().WantCaptureMouse;
isInteractingPrevious = isInteracting;
isHoveringPrevious = isHovering;
isHovering = false;
if (isJustStoppedInteracting)
{
cursor.queue_play({cursor.defaultAnimation});
if (cursor.mode == RUB) character.queue_idle_animation();
isJustStoppedInteracting = false;
}
if (isJustStoppedHovering)
{
cursor.queue_play({cursor.defaultAnimation});
isJustStoppedHovering = false;
}
for (int i = 0; i < (int)character.data.interactAreas.size(); i++)
{
auto& interactArea = character.data.interactAreas.at(i);
if (interactArea.nullID == -1) continue;
auto rect = character.null_frame_rect(interactArea.nullID);
if (cursor.state == entity::Cursor::DEFAULT && math::is_point_in_rectf(rect, cursorWorldPosition) &&
!isImguiCaptureMouse && interactArea.type == cursor.mode)
{
cursor.state = entity::Cursor::HOVER;
cursor.queue_play({interactArea.animationCursorHover});
isHovering = true;
interactAreaID = i;
if (isMouseLeftClick)
{
isInteracting = true;
interactArea.sound.play();
lastInteractType = cursor.mode;
if (interactArea.digestionBonusClick > 0 && character.calories > 0 && !character.isDigesting)
character.digestionProgress += interactArea.digestionBonusClick;
if (interactArea.layerID != -1)
{
character.overrides.emplace_back(entity::Actor::Override(
interactArea.layerID, Anm2::LAYER, entity::Actor::Override::ADD,
{.scale = glm::vec2(interactArea.scaleEffectAmplitude)}, std::optional<float>(interactArea.time),
interact_area_override_tick, interactArea.scaleEffectCycles));
}
if (interactArea.pool.is_valid() && text.is_interruptible())
text.set(dialogue.get(interactArea.pool), character);
}
if (isInteracting)
{
cursor.state = entity::Cursor::ACTION;
character.queue_interact_area_animation(interactArea);
cursor.queue_play({interactArea.animationCursorActive});
if (interactArea.digestionBonusRub > 0 && character.calories > 0 && !character.isDigesting)
{
auto mouseDelta = cursorWorldPosition - cursorWorldPositionPrevious;
auto digestionBonus = (fabs(mouseDelta.x) + fabs(mouseDelta.y)) * interactArea.digestionBonusRub;
character.digestionProgress += digestionBonus;
}
}
}
if ((i == interactAreaID && !math::is_point_in_rectf(rect, cursorWorldPosition)) || isMouseLeftReleased ||
isImguiCaptureMouse)
{
isInteracting = false;
interactAreaID = -1;
}
}
if (isInteracting != isInteractingPrevious && !isInteracting) isJustStoppedInteracting = true;
if (isHovering != isHoveringPrevious && !isHovering) isJustStoppedHovering = true;
cursorWorldPositionPrevious = cursorWorldPosition;
if (character.isJustDigested && text.is_interruptible()) text.set(dialogue.get(dialogue.digest), character);
if (character.isJustStageUp) text.set(dialogue.get(dialogue.stageUp), character);
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "text.hpp"
namespace game::state::main
{
class CharacterManager
{
public:
bool isInteracting{};
bool isHovering{};
bool isInteractingPrevious{};
bool isHoveringPrevious{};
bool isJustStoppedInteracting{};
bool isJustStoppedHovering{};
int interactAreaID{-1};
InteractType lastInteractType{(InteractType)-1};
glm::vec2 cursorWorldPositionPrevious{};
std::string queuedAnimation{};
void update(entity::Character&, entity::Cursor&, Text&, Canvas&);
};
}

32
src/state/main/chat.cpp Normal file
View File

@@ -0,0 +1,32 @@
#include "chat.hpp"
#include "../../util/imgui/widget.hpp"
using namespace game::resource;
using namespace game::util::imgui;
namespace game::state::main
{
void Chat::update(Resources& resources, Text& text, entity::Character& character)
{
auto& dialogue = character.data.dialogue;
auto size = ImGui::GetContentRegionAvail();
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
if (dialogue.random.is_valid())
if (WIDGET_FX(ImGui::Button("Let's chat!", ImVec2(size.x, 0))))
text.set(dialogue.get(dialogue.random), character);
ImGui::PopFont();
if (dialogue.help.is_valid())
if (WIDGET_FX(ImGui::Button("Help", ImVec2(size.x, 0)))) text.set(dialogue.get(dialogue.help), character);
auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get());
auto& pool = stage > 0 ? character.data.stages.at(stage - 1).pool : character.data.pool;
if (pool.is_valid())
if (WIDGET_FX(ImGui::Button("How are you feeling?", ImVec2(size.x, 0)))) text.set(dialogue.get(pool), character);
}
}

14
src/state/main/chat.hpp Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "text.hpp"
#include <imgui.h>
namespace game::state::main
{
class Chat
{
public:
void update(Resources&, Text&, entity::Character&);
};
}

114
src/state/main/cheats.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "cheats.hpp"
#include <algorithm>
#include <ranges>
#include "../../util/imgui/input_int_ex.hpp"
#include "../../util/imgui/widget.hpp"
using namespace game::util::imgui;
using namespace game::util;
namespace game::state::main
{
void Cheats::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text)
{
static constexpr auto FEED_INCREMENT = 100.0f;
if (ImGui::BeginChild("##Cheats"))
{
if (WIDGET_FX(ImGui::Button("Feed")))
{
character.calories = std::min(character.calories + FEED_INCREMENT, character.max_capacity());
character.queue_idle_animation();
}
ImGui::SameLine();
if (WIDGET_FX(ImGui::Button("Starve")))
{
character.calories = std::max(0.0f, character.calories - FEED_INCREMENT);
character.queue_idle_animation();
}
ImGui::SameLine();
if (WIDGET_FX(ImGui::Button("Digest"))) character.digestionProgress = entity::Character::DIGESTION_MAX;
if (WIDGET_FX(ImGui::SliderFloat("Weight", &character.weight, character.data.weightMin, character.data.weightMax,
"%0.2f kg")))
{
character.stage = character.stage_get();
character.queue_idle_animation();
}
auto stage = character.stage + 1;
if (WIDGET_FX(ImGui::SliderInt("Stage", &stage, 1, character.data.stages.size() + 1)))
{
character.stage = glm::clamp(0, stage - 1, (int)character.data.stages.size());
character.weight =
character.stage == 0 ? character.data.weight : character.data.stages.at(character.stage - 1).threshold;
character.queue_idle_animation();
}
WIDGET_FX(ImGui::SliderFloat("Capacity", &character.capacity, character.data.capacityMin,
character.data.capacityMax, "%0.0f kcal"));
WIDGET_FX(ImGui::SliderFloat("Digestion Rate", &character.digestionRate, character.data.digestionRateMin,
character.data.digestionRateMax, "%0.2f% / tick"));
WIDGET_FX(ImGui::SliderFloat("Eat Speed", &character.eatSpeed, character.data.eatSpeedMin,
character.data.eatSpeedMax, "%0.2fx"));
ImGui::SeparatorText("Animations");
ImGui::Text("Now Playing: %s", character.animationMapReverse.at(character.animationIndex).c_str());
auto childSize = ImVec2(0, ImGui::GetContentRegionAvail().y / 3);
if (ImGui::BeginChild("##Animations", childSize, ImGuiChildFlags_Borders))
{
for (int i = 0; i < (int)character.animations.size(); i++)
{
auto& animation = character.animations[i];
ImGui::PushID(i);
if (WIDGET_FX(ImGui::Selectable(animation.name.c_str())))
character.play(animation.name.c_str(), entity::Actor::PLAY_FORCE);
ImGui::SetItemTooltip("%s", animation.name.c_str());
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Dialogue");
if (ImGui::BeginChild("##Dialogue", childSize, ImGuiChildFlags_Borders))
{
for (int i = 0; i < (int)character.data.dialogue.entries.size(); i++)
{
auto& entry = character.data.dialogue.entries[i];
ImGui::PushID(i);
if (WIDGET_FX(ImGui::Selectable(entry.name.c_str()))) text.set(&entry, character);
ImGui::SetItemTooltip("%s", entry.name.c_str());
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Inventory");
if (ImGui::BeginChild("##Inventory", ImGui::GetContentRegionAvail(), ImGuiChildFlags_Borders))
{
auto& schema = character.data.itemSchema;
ImGui::PushItemWidth(100);
for (int i = 0; i < (int)schema.items.size(); i++)
{
auto& item = schema.items[i];
ImGui::PushID(i);
WIDGET_FX(input_int_range(item.name.c_str(), &inventory.values[i], 0, schema.quantityMax, 1, 5));
ImGui::SetItemTooltip("%s", item.name.c_str());
ImGui::PopID();
}
ImGui::PopItemWidth();
}
ImGui::EndChild();
}
ImGui::EndChild();
}
}

15
src/state/main/cheats.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "inventory.hpp"
#include "text.hpp"
#include <imgui.h>
namespace game::state::main
{
class Cheats
{
public:
void update(Resources&, entity::Character&, Inventory&, Text&);
};
}

37
src/state/main/debug.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "debug.hpp"
#include "../../util/imgui/widget.hpp"
#include <ranges>
using namespace game::util::imgui;
namespace game::state::main
{
void Debug::update(entity::Character& character, entity::Cursor& cursor, ItemManager& itemManager, Canvas& canvas)
{
auto cursorPosition = canvas.screen_position_convert(cursor.position);
ImGui::Text("Cursor Pos (Screen): %0.0f, %0.0f", cursor.position.x, cursor.position.y);
ImGui::Text("Cursor Pos (World): %0.0f, %0.0f", cursorPosition.x, cursorPosition.y);
WIDGET_FX(ImGui::Checkbox("Show Nulls (Hitboxes)", &character.isShowNulls));
WIDGET_FX(ImGui::Checkbox("Show World Bounds", &isBoundsDisplay));
if (!itemManager.items.empty())
{
ImGui::SeparatorText("Item");
for (int i = 0; i < (int)itemManager.items.size(); i++)
{
auto& item = itemManager.items[i];
if (itemManager.heldItemIndex == i) ImGui::Text("Held");
ImGui::Text("Type: %i", item.schemaID);
ImGui::Text("Position: %0.0f, %0.0f", item.position.x, item.position.y);
ImGui::Text("Velocity: %0.0f, %0.0f", item.velocity.x, item.velocity.y);
ImGui::Text("Chew Count: %i", item.chewCount);
ImGui::Separator();
}
}
}
}

19
src/state/main/debug.hpp Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "item_manager.hpp"
#include <imgui.h>
namespace game::state::main
{
class Debug
{
public:
bool isBoundsDisplay{};
void update(entity::Character&, entity::Cursor& cursor, ItemManager&, Canvas& canvas);
};
}

125
src/state/main/info.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "info.hpp"
#include "../../util/color.hpp"
#include "../../util/imgui.hpp"
#include "../../util/math.hpp"
#include "../../util/string.hpp"
#include <algorithm>
#include <format>
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
void Info::update(Resources& resources, entity::Character& character)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
static constexpr auto HEIGHT_MULTIPLIER = 4.0f;
auto& style = ImGui::GetStyle();
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER);
auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Info", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto childSize = ImVec2(ImGui::GetContentRegionAvail().x / 2, ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Weight", childSize))
{
auto& system = resources.settings.measurementSystem;
auto weight = character.weight_get(system);
auto stage = character.stage_get();
auto stageMax = character.stage_max_get();
auto stageWeight = character.stage_threshold_get(stage, system);
auto stageNextWeight = character.stage_threshold_next_get(system);
auto unitString = (system == measurement::IMPERIAL ? "lbs" : "kg");
auto weightString = util::string::format_commas(weight, 2) + " " + unitString;
ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE);
ImGui::TextUnformatted(weightString.c_str());
ImGui::SetItemTooltip("%s", weightString.c_str());
ImGui::PopFont();
auto stageProgress = character.stage_progress_get();
ImGui::ProgressBar(stageProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0),
stage >= stageMax ? "MAX" : "To Next Stage");
if (ImGui::BeginItemTooltip())
{
ImGui::Text("Stage: %i/%i (%0.1f%%)", stage + 1, stageMax + 1, math::to_percent(stageProgress));
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, imgui::to_imvec4(color::GRAY));
if (stage >= stageMax)
ImGui::Text("Maxed out!");
else
{
ImGui::Text("Start: %0.2f %s", stageWeight, unitString);
ImGui::Text("Current: %0.2f %s", weight, unitString);
ImGui::Text("Next: %0.2f %s", stageNextWeight, unitString);
}
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Calories and Capacity", childSize))
{
auto& calories = character.calories;
auto& capacity = character.capacity;
auto overstuffedPercent = std::max(0.0f, (calories - capacity) / (character.max_capacity() - capacity));
auto caloriesColor = ImVec4(1.0f, 1.0f - overstuffedPercent, 1.0f - overstuffedPercent, 1.0f);
ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE);
ImGui::PushStyleColor(ImGuiCol_Text, caloriesColor);
auto caloriesString = std::format("{:.0f} kcal / {:.0f} kcal", calories,
character.is_over_capacity() ? character.max_capacity() : character.capacity);
ImGui::TextUnformatted(caloriesString.c_str());
ImGui::SetItemTooltip("%s", caloriesString.c_str());
ImGui::PopStyleColor();
ImGui::PopFont();
auto digestionProgress = character.isDigesting
? (float)character.digestionTimer / character.data.digestionTimerMax
: character.digestionProgress / entity::Character::DIGESTION_MAX;
ImGui::ProgressBar(digestionProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0),
character.isDigesting ? "Digesting..." : "Digestion");
if (ImGui::BeginItemTooltip())
{
if (character.isDigesting)
ImGui::TextUnformatted("Digestion in progress...");
else if (digestionProgress <= 0.0f)
ImGui::TextUnformatted("Give food to start digesting!");
else
ImGui::Text("%0.2f%%", math::to_percent(digestionProgress));
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
ImGui::Text("Rate: %0.2f%% / sec", character.digestion_rate_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed);
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGui::EndChild();
}
ImGui::End();
}
}

15
src/state/main/info.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include <imgui.h>
namespace game::state::main
{
class Info
{
public:
void update(Resources&, entity::Character&);
};
}

View File

@@ -0,0 +1,194 @@
#include "inventory.hpp"
#include <cmath>
#include <format>
#include <ranges>
#include "../../util/color.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"
using namespace game::util;
using namespace game::util::imgui;
using namespace game::entity;
using namespace game::resource;
using namespace glm;
namespace game::state::main
{
void Inventory::tick()
{
for (auto& [i, actor] : actors)
actor.tick();
}
void Inventory::update(Resources& resources, ItemManager& itemManager, entity::Character& character)
{
auto& schema = character.data.itemSchema;
if (!itemManager.returnItemIDs.empty())
{
for (auto& id : itemManager.returnItemIDs)
values[id]++;
itemManager.returnItemIDs.clear();
}
if (ImGui::BeginChild("##Inventory Child"))
{
auto cursorPos = ImGui::GetCursorPos();
auto cursorStartX = ImGui::GetCursorPosX();
auto size = ImVec2(SIZE, SIZE);
for (int i = 0; i < (int)schema.items.size(); i++)
{
auto& item = schema.items[i];
auto& quantity = values[i];
auto& category = schema.categories[item.categoryID];
auto& calories = item.calories;
auto& digestionBonus = item.digestionBonus;
auto& eatSpeedBonus = item.eatSpeedBonus;
auto& rarity = schema.rarities[item.rarityID];
quantity = glm::clamp(0, quantity, schema.quantityMax);
if (rarity.isHidden && quantity <= 0) continue;
ImGui::PushID(i);
ImGui::SetCursorPos(cursorPos);
auto cursorScreenPos = ImGui::GetCursorScreenPos();
if (!actors.contains(i))
{
actors[i] = Actor(schema.anm2s[i], {}, Actor::SET);
rects[i] = actors[i].rect();
}
auto& rect = rects[i];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (size.x <= 0.0f || size.y <= 0.0f || rectSize.x <= 0.0f || rectSize.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
auto previewSize = rectSize * previewScale;
auto canvasSize = ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
if (!canvases.contains(i)) canvases.emplace((int)i, Canvas(canvasSize, Canvas::FLIP));
auto& canvas = canvases[i];
canvas.zoom = math::to_percent(previewScale);
canvas.pan = vec2(rect.x, rect.y);
canvas.bind();
canvas.size_set(canvasSize);
canvas.clear();
actors[i].render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
canvas.unbind();
ImGui::BeginDisabled(quantity < 1);
if (WIDGET_FX(ImGui::ImageButton("##Image Button", canvas.texture, size, ImVec2(), ImVec2(1, 1), ImVec4(),
quantity <= 0 ? ImVec4(0, 0, 0, 1) : ImVec4(1, 1, 1, 1))) &&
quantity > 0)
{
if (category.isEdible)
{
if (itemManager.items.size() + 1 >= ItemManager::LIMIT)
character.data.itemSchema.sounds.dispose.play();
else
{
character.data.itemSchema.sounds.summon.play();
itemManager.queuedItemIDs.emplace_back(i);
quantity--;
}
}
else if (item.isToggleSpritesheet)
{
character.spritesheet_set(character.spritesheetType == Character::NORMAL ? Character::ALTERNATE
: Character::NORMAL);
character.data.alternateSpritesheet.sound.play();
quantity--;
}
}
ImGui::EndDisabled();
if (ImGui::BeginItemTooltip())
{
if (quantity > 0)
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::Text("%s (x%i)", item.name.c_str(), quantity);
ImGui::Separator();
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
ImGui::Text("-- %s (%s) --", category.name.c_str(), rarity.name.c_str());
if (item.flavorID.has_value()) ImGui::Text("Flavor: %s", schema.flavors[*item.flavorID].name.c_str());
if (calories.has_value()) ImGui::Text("%0.0f kcal", *calories);
if (digestionBonus.has_value())
{
if (*digestionBonus > 0)
ImGui::Text("Digestion Rate Bonus: +%0.2f%% / sec", *digestionBonus * 60.0f);
else if (digestionBonus < 0)
ImGui::Text("Digestion Rate Penalty: %0.2f%% / sec", *digestionBonus * 60.0f);
}
if (eatSpeedBonus.has_value())
{
if (*eatSpeedBonus > 0)
ImGui::Text("Eat Speed Bonus: +%0.2f%% / sec", *eatSpeedBonus);
else if (eatSpeedBonus < 0)
ImGui::Text("Eat Speed Penalty: %0.2f%% / sec", *eatSpeedBonus);
}
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::TextUnformatted(item.description.c_str());
}
else
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::TextUnformatted("???");
ImGui::PopFont();
}
ImGui::EndTooltip();
}
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
auto text = std::format("x{}", quantity);
auto textPos = ImVec2(cursorScreenPos.x + size.x - ImGui::CalcTextSize(text.c_str()).x,
cursorScreenPos.y + size.y - ImGui::GetTextLineHeightWithSpacing());
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)),
text.c_str());
ImGui::PopFont();
auto increment = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;
cursorPos.x += increment;
if (cursorPos.x + increment > ImGui::GetContentRegionAvail().x)
{
cursorPos.x = cursorStartX;
cursorPos.y += increment;
}
ImGui::PopID();
}
if (count() == 0) ImGui::Text("Check the \"Play\" tab to earn rewards!");
}
ImGui::EndChild();
}
int Inventory::count()
{
int count{};
for (auto& [type, quantity] : values)
count += quantity;
return count;
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "item_manager.hpp"
#include <imgui.h>
namespace game::state::main
{
class Inventory
{
public:
static constexpr auto SIZE = 96.0f;
std::map<int, int> values{};
std::unordered_map<int, entity::Actor> actors{};
std::unordered_map<int, glm::vec4> rects{};
std::unordered_map<int, Canvas> canvases{};
void tick();
void update(Resources&, ItemManager&, entity::Character&);
int count();
};
}

View File

@@ -0,0 +1,315 @@
#include "item_manager.hpp"
#include <cmath>
#include <string>
#include <utility>
#include "../../util/math.hpp"
#include "../../util/vector.hpp"
#include <imgui.h>
using namespace game::resource;
using namespace game::util;
using namespace glm;
namespace game::state::main
{
void ItemManager::update(entity::Character& character, entity::Cursor& cursor, AreaManager& areaManager, Text& text,
const glm::vec4& bounds, Canvas& canvas)
{
static constexpr float ROTATION_MAX = 90.0f;
static constexpr float ROTATION_RETURN_SPEED = 120.0f;
static constexpr float ROTATION_GROUND_DAMPING = 0.85f;
static constexpr float ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER = 0.1f;
static constexpr float THROW_THRESHOLD = 10.0f;
auto& schema = character.data.itemSchema;
auto& cursorSchema = character.data.cursorSchema;
auto& area = character.data.areaSchema.areas.at(areaManager.get(character));
auto& friction = area.friction;
auto& airResistance = area.airResistance;
auto& dialogue = character.data.dialogue;
auto isOverCapacity = character.is_over_capacity();
auto cursorPosition = canvas.screen_position_convert(cursor.position);
auto cursorDelta = cursorPosition - cursorPositionPrevious;
auto isImguiCaptureMouse = ImGui::GetIO().WantCaptureMouse;
auto isMouseLeftClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
auto& io = ImGui::GetIO();
if (isJustItemHoveredStopped)
{
cursor.queue_default_animation();
isJustItemHoveredStopped = false;
}
if (isJustItemHeldStopped || isJustItemThrown)
{
cursor.queue_default_animation();
if (!isJustItemThrown) character.queue_idle_animation();
isJustItemHeldStopped = false;
isJustItemThrown = false;
}
isItemHoveredPrevious = isItemHovered;
isItemHovered = false;
if (isItemHovered != isItemHoveredPrevious && !isItemHovered) isJustItemHoveredStopped = true;
for (auto& id : queuedItemIDs)
{
auto spawnBounds = character.rect();
auto position = glm::vec2(math::random_in_range(spawnBounds.x, spawnBounds.x + spawnBounds.z),
math::random_in_range(spawnBounds.y, spawnBounds.y + spawnBounds.w));
auto& itemSchema = character.data.itemSchema;
items.emplace_back(itemSchema.anm2s.at(id), position, id);
}
queuedItemIDs.clear();
if (isMouseRightDown) cursor.queue_play({cursorSchema.animations.return_.get()});
if (auto heldItem = vector::find(items, heldItemIndex))
{
auto& item = schema.items[heldItem->schemaID];
auto& rotationOverride = heldItem->overrides[heldItem->rotationOverrideID];
auto& rotation = *rotationOverride.frame.rotation;
auto delta = cursorPositionPrevious - cursorPosition;
heldItem->position = cursorPosition;
heldItem->velocity = vec2();
heldItem->angularVelocity -= delta.x * ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER;
heldItem->angularVelocity *= friction;
rotation *= friction;
rotation = glm::clamp(-ROTATION_MAX, rotation, ROTATION_MAX);
if (schema.categories[item.categoryID].isEdible)
{
auto& chewCountMax = item.chewCount.has_value() ? *item.chewCount : schema.chewCount;
auto caloriesChew = item.calories.has_value() ? *item.calories / (chewCountMax + 1) : 0;
auto isCanEat = character.calories + caloriesChew <= character.max_capacity();
if (isJustItemHeld)
{
if (isCanEat)
text.set(dialogue.get(isOverCapacity ? dialogue.feedFull : dialogue.feed), character);
else if (caloriesChew > character.capacity)
text.set(dialogue.get(dialogue.lowCapacity), character);
else
text.set(dialogue.get(dialogue.full), character);
isJustItemHeld = false;
}
for (auto& eatArea : character.data.eatAreas)
{
heldItem = vector::find(items, heldItemIndex);
if (!heldItem) break;
auto rect = character.null_frame_rect(eatArea.nullID);
if (isCanEat && math::is_point_in_rectf(rect, heldItem->position))
{
character.queue_play(
{.animation = eatArea.animation, .speedMultiplier = character.eatSpeed, .isInterruptible = false});
if (character.playedEventID == eatArea.eventID)
{
heldItem->chewCount++;
character.consume_played_event();
auto chewAnimation = schema.animations.chew + std::to_string(heldItem->chewCount);
auto animationIndex = heldItem->chewCount > 0 ? heldItem->animationMap[chewAnimation] : -1;
heldItem->play(animationIndex, entity::Actor::SET);
character.calories += caloriesChew;
character.totalCaloriesConsumed += caloriesChew;
if (item.eatSpeedBonus.has_value())
{
character.eatSpeed += *item.eatSpeedBonus / (chewCountMax + 1);
character.eatSpeed =
glm::clamp(character.data.eatSpeedMin, character.eatSpeed, character.data.eatSpeedMax);
}
if (item.digestionBonus.has_value())
{
character.digestionRate += *item.digestionBonus / (chewCountMax + 1);
character.digestionRate = glm::clamp(character.data.digestionRateMin, character.digestionRate,
character.data.digestionRateMax);
}
if (heldItem->chewCount > chewCountMax)
{
isQueueFinishFood = true;
character.totalFoodItemsEaten++;
queuedRemoveItemIndex = heldItemIndex;
heldItemIndex = -1;
}
}
}
if (isMouseLeftReleased)
{
if (fabs(delta.x) >= THROW_THRESHOLD || fabs(delta.y) >= THROW_THRESHOLD)
{
cursorSchema.sounds.throw_.play();
text.set(dialogue.get(dialogue.throw_), character);
isJustItemThrown = true;
}
else
cursorSchema.sounds.release.play();
heldItem->velocity -= delta;
heldItemIndex = -1;
isJustItemHeldStopped = true;
}
// Food stolen
if (auto animation = character.animation_get(character.animation_name_convert(eatArea.animation));
character.is_playing(animation->name) && !isOverCapacity)
{
if (!math::is_point_in_rectf(rect, heldItem->position))
text.set(dialogue.get(isOverCapacity ? dialogue.foodTakenFull : dialogue.foodTaken), character);
}
}
}
}
if (auto animation = character.animation_get(); character.time >= animation->frameNum && isQueueFinishFood)
{
text.set(dialogue.get(isOverCapacity ? dialogue.eatFull : dialogue.eat), character);
isQueueFinishFood = false;
}
if (queuedRemoveItemIndex > -1)
{
items.erase(items.begin() + queuedRemoveItemIndex);
queuedRemoveItemIndex = -1;
}
int heldItemMoveIndex = -1;
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
auto& schemaItem = schema.items[item.schemaID];
auto& rotationOverride = item.overrides[item.rotationOverrideID];
auto& rotation = *rotationOverride.frame.rotation;
auto& gravity = schemaItem.gravity.has_value() ? *schemaItem.gravity : area.gravity;
item.update();
if (math::is_point_in_rectf(item.rect(), cursorPosition) && !isImguiCaptureMouse)
{
isItemHovered = true;
cursor.queue_play({cursorSchema.animations.hover.get()});
cursor.state = entity::Cursor::HOVER;
if (isMouseLeftClicked)
{
cursorSchema.sounds.grab.play();
isJustItemHeld = true;
}
if (isMouseLeftDown)
{
isItemHeld = true;
cursor.queue_play({cursorSchema.animations.grab.get()});
cursor.state = entity::Cursor::ACTION;
heldItemIndex = i;
heldItemMoveIndex = i;
}
if (isMouseRightClicked)
{
if (item.chewCount > 0)
schema.sounds.dispose.play();
else
{
schema.sounds.return_.play();
returnItemIDs.emplace_back(item.schemaID);
}
if (heldItemIndex == i) heldItemIndex = -1;
if (heldItemMoveIndex == i) heldItemMoveIndex = -1;
if (heldItemIndex > i) heldItemIndex--;
if (heldItemMoveIndex > i) heldItemMoveIndex--;
items.erase(items.begin() + i);
continue;
}
}
if (i != heldItemIndex)
{
if (item.position.x <= bounds.x || item.position.x >= bounds.z)
{
if (item.position.x <= bounds.x) item.position.x = bounds.x + 1.0f;
if (item.position.x >= bounds.z) item.position.x = bounds.z - 1.0f;
item.velocity.x *= friction;
item.velocity.x = -item.velocity.x;
item.angularVelocity *= friction;
item.angularVelocity = -item.angularVelocity;
rotation = -rotation;
schema.sounds.bounce.play();
}
if (item.position.y <= bounds.y || item.position.y >= bounds.w)
{
if (item.position.y >= bounds.w && item.velocity.y <= gravity)
{
item.position.y = bounds.w;
item.velocity.y = 0;
item.velocity.x *= friction;
rotation = std::fmod(rotation, 360.0f);
if (rotation < 0.0f) rotation += 360.0f;
if (rotation > 180.0f) rotation -= 360.0f;
auto returnStep = ROTATION_RETURN_SPEED * io.DeltaTime;
if (std::abs(rotation) <= returnStep)
rotation = 0.0f;
else
rotation += (rotation > 0.0f) ? -returnStep : returnStep;
item.angularVelocity *= ROTATION_GROUND_DAMPING;
}
else
{
item.velocity.y = -item.velocity.y;
item.angularVelocity *= friction;
schema.sounds.bounce.play();
}
item.velocity.y *= friction;
}
item.velocity.x *= airResistance;
item.velocity.y += gravity;
}
item.position.x = glm::clamp(bounds.x, item.position.x, bounds.z);
item.position.y = glm::clamp(bounds.y, item.position.y, bounds.w);
}
if (heldItemMoveIndex != -1 && heldItemMoveIndex < (int)items.size() - 1)
{
auto heldItem = std::move(items[heldItemMoveIndex]);
items.erase(items.begin() + heldItemMoveIndex);
items.push_back(std::move(heldItem));
heldItemIndex = (int)items.size() - 1;
}
cursorPositionPrevious = cursorPosition;
cursorDeltaPrevious = cursorDelta;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "../../entity/item.hpp"
#include "area_manager.hpp"
#include "text.hpp"
namespace game::state::main
{
class ItemManager
{
public:
static constexpr auto LIMIT = 100;
std::vector<entity::Item> items{};
int heldItemIndex{-1};
int queuedRemoveItemIndex{-1};
bool isItemHovered{};
bool isItemHoveredPrevious{};
bool isJustItemHoveredStopped{};
bool isItemHeld{};
bool isItemHeldPrevious{};
bool isJustItemHeldStopped{};
bool isJustItemHeld{};
bool isJustItemThrown{};
bool isQueueFinishFood{};
bool isItemFinished{};
glm::vec2 cursorPositionPrevious{};
glm::vec2 cursorDeltaPrevious{};
std::vector<int> queuedItemIDs{};
std::vector<int> returnItemIDs{};
void update(entity::Character&, entity::Cursor&, AreaManager&, Text&, const glm::vec4& bounds, Canvas&);
};
}

165
src/state/main/menu.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include "menu.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include <algorithm>
using namespace game::util;
using namespace game::util::imgui;
namespace game::state::main
{
void Menu::tick()
{
inventory.tick();
play.tick();
}
void Menu::update(Resources& resources, ItemManager& itemManager, entity::Character& character,
entity::Cursor& cursor, Text& text, Canvas& canvas)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
auto& schema = character.data.menuSchema;
auto style = ImGui::GetStyle();
auto& io = ImGui::GetIO();
slide.update(isOpen, io.DeltaTime);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER, windowSize.y - style.WindowPadding.y * 2);
auto targetX = windowSize.x - size.x;
auto t = slide.value_get();
auto eased = slide.eased_get();
auto posX = windowSize.x + (targetX - windowSize.x) * eased;
auto pos = ImVec2(posX, style.WindowPadding.y);
auto barSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), windowSize.y - style.WindowPadding.y * 2);
auto barPos = ImVec2(pos.x - barSize.x - style.WindowPadding.x, style.WindowPadding.y);
if (slide.is_visible())
{
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Menu", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
if (ImGui::BeginTabBar("##Options"))
{
if (isChat && WIDGET_FX(ImGui::BeginTabItem("Chat")))
{
chat.update(resources, text, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Play")))
{
play.update(resources, character, inventory, text);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Items")))
{
inventory.update(resources, itemManager, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Stats")))
{
stats.update(resources, play, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Settings")))
{
configuration.update(resources, Configuration::MAIN);
ImGui::EndTabItem();
}
if (isCheats && WIDGET_FX(ImGui::BeginTabItem("Cheats")))
{
cheats.update(resources, character, inventory, text);
ImGui::EndTabItem();
}
if (isDebug && WIDGET_FX(ImGui::BeginTabItem("Debug")))
{
debug.update(character, cursor, itemManager, canvas);
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::End();
}
ImGui::SetNextWindowSize(barSize);
ImGui::SetNextWindowPos(barPos);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("##Menu Open Bar", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto buttonSize = ImGui::GetContentRegionAvail();
auto cursorPos = ImGui::GetCursorScreenPos();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto result = WIDGET_FX(ImGui::Button("##MenuToggle", buttonSize));
if (t <= 0.0f || t >= 1.0f)
{
ImGui::SetItemTooltip(isOpen ? "Close Main Menu" : "Open Main Menu");
if (result)
{
isOpen = !isOpen;
if (isOpen)
schema.sounds.open.play();
else
schema.sounds.close.play();
}
if (!isOpen && t <= 0.0f && ImGui::IsItemHovered())
{
isOpen = true;
schema.sounds.open.play();
}
}
ImGui::PopStyleVar();
auto center = ImVec2(cursorPos.x + (buttonSize.x * 0.5f), cursorPos.y + (buttonSize.y * 0.5f));
auto half = std::min(buttonSize.x, buttonSize.y) * 0.22f;
ImVec2 tip;
ImVec2 baseA;
ImVec2 baseB;
if (isOpen)
{
tip = ImVec2(center.x + half, center.y);
baseA = ImVec2(center.x - half, center.y - half);
baseB = ImVec2(center.x - half, center.y + half);
}
else
{
tip = ImVec2(center.x - half, center.y);
baseA = ImVec2(center.x + half, center.y - half);
baseB = ImVec2(center.x + half, center.y + half);
}
auto color = ImGui::GetColorU32(ImGuiCol_Text);
ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
}
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar(2);
}
}

39
src/state/main/menu.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <imgui.h>
#include "../configuration.hpp"
#include "chat.hpp"
#include "cheats.hpp"
#include "debug.hpp"
#include "play.hpp"
#include "stats.hpp"
#include "text.hpp"
#include "../../util/imgui/window_slide.hpp"
namespace game::state::main
{
class Menu
{
public:
Play play;
Chat chat;
Cheats cheats;
Debug debug;
Stats stats;
Inventory inventory;
state::Configuration configuration;
bool isCheats{true};
bool isDebug{true};
bool isOpen{true};
bool isChat{true};
util::imgui::WindowSlide slide{};
void tick();
void update(Resources&, ItemManager&, entity::Character&, entity::Cursor&, Text&, Canvas&);
};
}

433
src/state/main/play.cpp Normal file
View File

@@ -0,0 +1,433 @@
#include "play.hpp"
#include <imgui_internal.h>
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"
#include <cmath>
#include <format>
#include <ranges>
using namespace game::util;
using namespace game::entity;
using namespace game::resource;
using namespace glm;
namespace game::state::main
{
float Play::accuracy_score_get(entity::Character& character)
{
if (totalPlays == 0) return 0.0f;
auto& schema = character.data.playSchema;
float combinedWeight{};
for (int i = 0; i < (int)schema.grades.size(); i++)
{
auto& grade = schema.grades[i];
combinedWeight += gradeCounts[i] * grade.weight;
}
return glm::clamp(0.0f, math::to_percent(combinedWeight / totalPlays), 100.0f);
}
Play::Challenge Play::challenge_generate(entity::Character& character)
{
auto& schema = character.data.playSchema;
Challenge newChallenge;
Range newRange{};
auto rangeSize = std::max(schema.rangeMin, schema.rangeBase - (schema.rangeScoreBonus * score));
newRange.min = math::random_max(1.0f - rangeSize);
newRange.max = newRange.min + rangeSize;
newChallenge.range = newRange;
newChallenge.tryValue = 0.0f;
newChallenge.speed =
glm::clamp(schema.speedMin, schema.speedMin + (schema.speedScoreBonus * score), schema.speedMax);
if (math::random_bool())
{
newChallenge.tryValue = 1.0f;
newChallenge.speed *= -1;
}
return newChallenge;
}
Play::Play(entity::Character& character) { challenge = challenge_generate(character); }
void Play::tick()
{
for (auto& [i, actor] : itemActors)
actor.tick();
}
void Play::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text)
{
static constexpr auto BG_COLOR_MULTIPLIER = 0.5f;
static constexpr ImVec4 LINE_COLOR = ImVec4(1, 1, 1, 1);
static constexpr ImVec4 PERFECT_COLOR = ImVec4(1, 1, 1, 0.50);
static constexpr auto LINE_HEIGHT = 2.0f;
static constexpr auto LINE_WIDTH_BONUS = 10.0f;
static constexpr auto TOAST_MESSAGE_SPEED = 1.0f;
static constexpr auto ITEM_FALL_GRAVITY = 2400.0f;
auto& dialogue = character.data.dialogue;
auto& schema = character.data.playSchema;
auto& itemSchema = character.data.itemSchema;
auto& style = ImGui::GetStyle();
auto drawList = ImGui::GetWindowDrawList();
auto position = ImGui::GetCursorScreenPos();
auto size = ImGui::GetContentRegionAvail();
auto spacing = ImGui::GetTextLineHeightWithSpacing();
auto& io = ImGui::GetIO();
auto cursorPos = ImGui::GetCursorPos();
ImGui::Text("Score: %i pts (%ix)", score, combo);
auto bestString = std::format("Best: {} pts({}x)", highScore, bestCombo);
ImGui::SetCursorPos(ImVec2(size.x - ImGui::CalcTextSize(bestString.c_str()).x, cursorPos.y));
ImGui::Text("Best: %i pts (%ix)", highScore, bestCombo);
if (score == 0 && isActive)
{
ImGui::SetCursorPos(ImVec2(style.WindowPadding.x, size.y - style.WindowPadding.y));
ImGui::TextWrapped("Match the line to the colored areas with Space/click! Better performance, better rewards!");
}
auto barMin = ImVec2(position.x + (size.x * 0.5f) - (spacing * 0.5f), position.y + (spacing * 2.0f));
auto barMax = ImVec2(barMin.x + (spacing * 2.0f), barMin.y + size.y - (spacing * 4.0f));
auto endTimerProgress = (float)endTimer / endTimerMax;
auto bgColor = ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
bgColor = imgui::to_imvec4(imgui::to_vec4(bgColor) * BG_COLOR_MULTIPLIER);
drawList->AddRectFilled(barMin, barMax, ImGui::GetColorU32(bgColor));
auto barWidth = barMax.x - barMin.x;
auto barHeight = barMax.y - barMin.y;
auto sub_ranges_get = [&](Range& range)
{
auto& min = range.min;
auto& max = range.max;
std::vector<Range> ranges{};
auto baseHeight = max - min;
auto center = (min + max) * 0.5f;
int rangeCount{};
for (auto& grade : schema.grades)
{
if (grade.isFailure) continue;
auto scale = powf(0.5f, rangeCount);
auto halfHeight = baseHeight * scale * 0.5f;
rangeCount++;
ranges.push_back({center - halfHeight, center + halfHeight});
}
return ranges;
};
auto range_draw = [&](Range& range, float alpha = 1.0f)
{
auto subRanges = sub_ranges_get(range);
for (int i = 0; i < (int)subRanges.size(); i++)
{
auto& subRange = subRanges[i];
int layer = (int)subRanges.size() - 1 - i;
ImVec2 rectMin = {barMin.x, barMin.y + subRange.min * barHeight};
ImVec2 rectMax = {barMax.x, barMin.y + subRange.max * barHeight};
ImVec4 color =
i == (int)subRanges.size() - 1 ? PERFECT_COLOR : ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered);
color.w = (color.w - (float)layer / subRanges.size()) * alpha;
drawList->AddRectFilled(rectMin, rectMax, ImGui::GetColorU32(color));
}
};
range_draw(challenge.range, isActive ? 1.0f : 0.0f);
auto lineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * tryValue));
auto lineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, lineMin.y + LINE_HEIGHT);
auto color = LINE_COLOR;
color.w = isActive ? 1.0f : endTimerProgress;
drawList->AddRectFilled(lineMin, lineMax, ImGui::GetColorU32(color));
if (!isActive && !isGameOver)
{
range_draw(queuedChallenge.range, 1.0f - endTimerProgress);
auto lineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * queuedChallenge.tryValue));
auto lineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, lineMin.y + LINE_HEIGHT);
auto color = LINE_COLOR;
color.w = 1.0f - endTimerProgress;
drawList->AddRectFilled(lineMin, lineMax, ImGui::GetColorU32(color));
}
if (isActive)
{
tryValue += challenge.speed;
if (tryValue > 1.0f || tryValue < 0.0f)
{
tryValue = tryValue > 1.0f ? 0.0f : tryValue < 0.0f ? 1.0f : tryValue;
if (score > 0)
{
score--;
schema.sounds.scoreLoss.play();
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("-1").x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y);
toasts.emplace_back("-1", toastMessagePosition, schema.endTimerMax, schema.endTimerMax);
}
}
ImGui::SetCursorScreenPos(barMin);
auto barButtonSize = ImVec2(barMax.x - barMin.x, barMax.y - barMin.y);
if (ImGui::IsKeyPressed(ImGuiKey_Space) ||
WIDGET_FX(ImGui::InvisibleButton("##PlayBar", barButtonSize, ImGuiButtonFlags_PressedOnClick)))
{
int gradeID{};
auto subRanges = sub_ranges_get(challenge.range);
for (int i = 0; i < (int)subRanges.size(); i++)
{
auto& subRange = subRanges[i];
if (tryValue >= subRange.min && tryValue <= subRange.max)
gradeID = std::min((int)gradeID + 1, (int)schema.grades.size() - 1);
}
gradeCounts[gradeID]++;
totalPlays++;
auto& grade = schema.grades.at(gradeID);
grade.sound.play();
if (text.is_interruptible() && grade.pool.is_valid()) text.set(dialogue.get(grade.pool), character);
if (!grade.isFailure)
{
combo++;
score += grade.value;
if (score >= schema.rewardScore && !isRewardScoreAchieved)
{
schema.sounds.rewardScore.play();
isRewardScoreAchieved = true;
for (auto& itemID : itemSchema.rewardItemPool)
{
inventory.values[itemID]++;
if (!itemActors.contains(itemID))
{
itemActors[itemID] = Actor(itemSchema.anm2s[itemID], {}, Actor::SET);
itemRects[itemID] = itemActors[itemID].rect();
}
auto rect = itemRects[itemID];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto minX = position.x;
auto maxX = position.x + size.x - previewSize.x;
auto spawnX = minX >= maxX ? position.x : math::random_in_range(minX, maxX);
auto spawnY = position.y - previewSize.y - math::random_in_range(0.0f, size.y);
items.push_back({itemID, ImVec2(spawnX, spawnY), 0.0f});
}
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("Fantastic score!\nCongratulations!").x -
ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y));
toasts.emplace_back("Fantastic score! Congratulations!", toastMessagePosition, schema.endTimerMax,
schema.endTimerMax);
}
if (score > highScore)
{
highScore = score;
if (isHighScoreAchieved && !isHighScoreAchievedThisRun)
{
isHighScoreAchievedThisRun = true;
schema.sounds.highScore.play();
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("High Score!").x - ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + ImGui::GetTextLineHeightWithSpacing());
toasts.emplace_back("High Score!", toastMessagePosition, schema.endTimerMax, schema.endTimerMax);
}
}
if (combo > bestCombo) bestCombo = combo;
auto rewardBonus = (schema.rewardGradeBonus * score) + (schema.rewardGradeBonus * grade.value);
while (rewardBonus > 0.0f)
{
const resource::xml::Item::Pool* pool{};
int rewardID{-1};
int rarityID{-1};
auto chanceBonus = std::max(1.0f, (float)grade.value);
for (auto& id : itemSchema.rarityIDsSortedByChance)
{
auto& rarity = itemSchema.rarities[id];
if (rarity.chance <= 0.0f) continue;
if (math::random_percent_roll(rarity.chance * chanceBonus))
{
pool = &itemSchema.pools[id];
rarityID = id;
break;
}
}
if (pool && !pool->empty())
{
rewardID = (*pool)[(int)math::random_roll((float)pool->size())];
auto& rarity = itemSchema.rarities.at(rarityID);
rarity.sound.play();
inventory.values[rewardID]++;
if (!itemActors.contains(rewardID))
{
itemActors[rewardID] = Actor(itemSchema.anm2s[rewardID], {}, Actor::SET);
itemRects[rewardID] = itemActors[rewardID].rect();
}
auto rect = itemRects[rewardID];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto minX = position.x;
auto maxX = position.x + size.x - previewSize.x;
auto spawnX = minX >= maxX ? position.x : math::random_in_range(minX, maxX);
auto spawnY = position.y - previewSize.y - math::random_in_range(0.0f, size.y);
items.push_back({rewardID, ImVec2(spawnX, spawnY), 0.0f});
}
rewardBonus -= 1.0f;
}
}
else
{
score = 0;
if (isHighScoreAchieved) schema.sounds.highScoreLoss.play();
if (highScore > 0) isHighScoreAchieved = true;
isRewardScoreAchieved = false;
isHighScoreAchievedThisRun = true;
highScoreStart = highScore;
isGameOver = true;
}
endTimerMax = grade.isFailure ? schema.endTimerFailureMax : schema.endTimerMax;
isActive = false;
endTimer = endTimerMax;
queuedChallenge = challenge_generate(character);
auto string = grade.isFailure ? grade.name : std::format("{} (+{})", grade.name, grade.value);
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize(string.c_str()).x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y);
toasts.emplace_back(string, toastMessagePosition, endTimerMax, endTimerMax);
}
}
else
{
endTimer--;
if (endTimer <= 0)
{
challenge = queuedChallenge;
tryValue = challenge.tryValue;
isActive = true;
isGameOver = false;
}
}
for (int i = 0; i < (int)toasts.size(); i++)
{
auto& toastMessage = toasts[i];
toastMessage.position.y -= TOAST_MESSAGE_SPEED;
auto color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
color.w = ((float)toastMessage.time / toastMessage.timeMax);
drawList->AddText(toastMessage.position, ImGui::GetColorU32(color), toastMessage.message.c_str());
toastMessage.time--;
if (toastMessage.time <= 0) toasts.erase(toasts.begin() + i--);
}
auto gravity = ITEM_FALL_GRAVITY;
auto windowMin = position;
auto windowMax = ImVec2(position.x + size.x, position.y + size.y);
ImGui::PushClipRect(windowMin, windowMax, true);
for (int i = 0; i < (int)items.size(); i++)
{
auto& fallingItem = items[i];
if (!itemActors.contains(fallingItem.id))
{
items.erase(items.begin() + i--);
continue;
}
auto rect = itemRects[fallingItem.id];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto canvasSize = ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
if (!itemCanvases.contains(fallingItem.id))
itemCanvases.emplace(fallingItem.id, Canvas(canvasSize, Canvas::FLIP));
auto& canvas = itemCanvases[fallingItem.id];
canvas.zoom = math::to_percent(previewScale);
canvas.pan = vec2(rect.x, rect.y);
canvas.bind();
canvas.size_set(canvasSize);
canvas.clear();
itemActors[fallingItem.id].render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
canvas.unbind();
auto min = fallingItem.position;
auto max = ImVec2(fallingItem.position.x + previewSize.x, fallingItem.position.y + previewSize.y);
drawList->AddImage(canvas.texture, min, max);
fallingItem.velocity += gravity * io.DeltaTime;
fallingItem.position.y += fallingItem.velocity * io.DeltaTime;
if (fallingItem.position.y > position.y + size.y) items.erase(items.begin() + i--);
}
ImGui::PopClipRect();
}
}

87
src/state/main/play.hpp Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
#include "../../canvas.hpp"
#include "../../entity/actor.hpp"
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "inventory.hpp"
#include "text.hpp"
#include <imgui.h>
#include <map>
#include <unordered_map>
#include <vector>
namespace game::state::main
{
class Play
{
public:
struct Range
{
float min{};
float max{};
};
struct Challenge
{
Range range{};
float speed{};
float tryValue{};
int level{};
};
struct Toast
{
std::string message{};
ImVec2 position;
int time{};
int timeMax{};
};
struct Item
{
int id{-1};
ImVec2 position{};
float velocity{};
};
Challenge challenge{};
Challenge queuedChallenge{};
float tryValue{};
int score{};
int combo{};
int endTimer{};
int endTimerMax{};
int highScoreStart{};
int bestCombo{};
int highScore{};
int totalPlays{};
std::map<int, int> gradeCounts{};
bool isActive{true};
bool isRewardScoreAchieved{false};
bool isHighScoreAchieved{false};
bool isHighScoreAchievedThisRun{false};
bool isGameOver{};
std::vector<Toast> toasts{};
std::vector<Item> items{};
std::unordered_map<int, entity::Actor> itemActors{};
std::unordered_map<int, glm::vec4> itemRects{};
std::unordered_map<int, Canvas> itemCanvases{};
Play() = default;
Play(entity::Character&);
Challenge challenge_generate(entity::Character&);
void tick();
void update(Resources&, entity::Character&, Inventory&, Text&);
float accuracy_score_get(entity::Character&);
};
}

48
src/state/main/stats.cpp Normal file
View File

@@ -0,0 +1,48 @@
#include "stats.hpp"
#include <ranges>
#include "../../util/measurement.hpp"
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
void Stats::update(Resources& resources, Play& play, entity::Character& character)
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::TextUnformatted(character.data.name.c_str());
ImGui::PopFont();
ImGui::Separator();
auto& playSchema = character.data.playSchema;
auto& system = resources.settings.measurementSystem;
auto weight = character.weight_get(system);
auto weightUnit = system == measurement::IMPERIAL ? "lbs" : "kg";
ImGui::Text("Weight: %0.2f %s (Stage: %i)", weight, weightUnit, character.stage_get() + 1);
ImGui::Text("Capacity: %0.0f kcal (Max: %0.0f kcal)", character.capacity, character.max_capacity());
ImGui::Text("Digestion Rate: %0.2f%%/sec", character.digestion_rate_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed);
ImGui::SeparatorText("Totals");
ImGui::Text("Total Calories Consumed: %0.0f kcal", character.totalCaloriesConsumed);
ImGui::Text("Total Food Items Eaten: %i", character.totalFoodItemsEaten);
ImGui::SeparatorText("Play");
ImGui::Text("Best: %i pts (%ix)", play.highScore, play.bestCombo);
ImGui::Text("Total Plays: %i", play.totalPlays);
for (int i = 0; i < (int)playSchema.grades.size(); i++)
{
auto& grade = playSchema.grades[i];
ImGui::Text("%s: %i", grade.namePlural.c_str(), play.gradeCounts[i]);
}
ImGui::Text("Accuracy: %0.2f%%", play.accuracy_score_get(character));
}
}

17
src/state/main/stats.hpp Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "play.hpp"
#include <imgui.h>
namespace game::state::main
{
class Stats
{
public:
void update(Resources&, Play&, entity::Character&);
};
}

205
src/state/main/text.cpp Normal file
View File

@@ -0,0 +1,205 @@
#include "text.hpp"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <string_view>
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"
using namespace game::util;
namespace game::state::main
{
const char* utf8_advance_chars(const char* text, const char* end, int count)
{
const char* it = text;
while (it < end && count > 0)
{
unsigned int codepoint = 0;
int step = ImTextCharFromUtf8(&codepoint, it, end);
if (step <= 0) break;
it += step;
--count;
}
return it;
}
void Text::set(resource::xml::Dialogue::Entry* entry, entity::Character& character)
{
if (!entry) return;
this->entry = entry;
isFinished = false;
index = 0;
time = 0.0f;
isEnabled = true;
character.isTalking = true;
if (!entry->animation.empty()) character.play_convert(entry->animation);
if (entry->text.empty()) isEnabled = false;
}
void Text::tick(entity::Character& character)
{
if (!entry || isFinished) return;
index++;
if (index >= ImTextCountCharsFromUtf8(entry->text.c_str(), entry->text.c_str() + entry->text.size()))
{
isFinished = true;
character.isTalking = false;
}
}
void Text::update(entity::Character& character)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
static constexpr auto HEIGHT_MULTIPLIER = 6.0f;
if (!entry) return;
auto& dialogue = character.data.dialogue;
auto& menuSchema = character.data.menuSchema;
auto& style = ImGui::GetStyle();
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER);
auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), windowSize.y - size.y - style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (!entry) return;
if (ImGui::Begin("##Text", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto isHovered = ImGui::IsWindowHovered();
auto isMouse = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isSpace = ImGui::IsKeyReleased(ImGuiKey_Space);
auto isAdvance = (isHovered && (isMouse || isSpace));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, 0));
if (ImGui::BeginTabBar("##Name"))
{
if (ImGui::BeginTabItem(character.data.name.c_str())) ImGui::EndTabItem();
ImGui::EndTabBar();
}
auto available = ImGui::GetContentRegionAvail();
auto font = ImGui::GetFont();
auto fontSize = resource::Font::NORMAL;
ImGui::PushFont(font, fontSize);
auto text = [&]()
{
auto text = entry ? std::string_view(entry->text) : "null";
auto length = std::clamp(index, 0, ImTextCountCharsFromUtf8(text.data(), text.data() + text.size()));
if (length <= 0)
{
ImGui::Dummy(ImVec2(1.0f, ImGui::GetTextLineHeight()));
return;
}
const char* textStart = text.data();
const char* textEnd = textStart + text.size();
const char* textLimit = utf8_advance_chars(textStart, textEnd, length);
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + available.x);
ImGui::TextUnformatted(textStart, textLimit);
ImGui::PopTextWrapPos();
};
text();
if (entry)
{
if (isFinished)
{
if (!entry->choices.empty())
{
ImGui::SetCursorPos(ImVec2(ImGui::GetStyle().WindowPadding.x, available.y));
auto buttonSize = imgui::row_widget_size_get(entry->choices.size());
for (auto& branch : entry->choices)
{
if (WIDGET_FX(ImGui::Button(branch.text.c_str(), buttonSize)))
set(dialogue.get(branch.nextID), character);
ImGui::SetItemTooltip("%s", branch.text.c_str());
ImGui::SameLine();
}
if (isHovered && isSpace)
{
set(dialogue.get(entry->choices.front().nextID), character);
menuSchema.sounds.select.play();
}
}
else
{
if (entry->nextID != -1)
{
ImGui::SetCursorPos(ImVec2(available.x - ImGui::GetTextLineHeightWithSpacing(), available.y));
auto indicatorSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing());
auto cursorPos = ImGui::GetCursorScreenPos();
auto center = ImVec2(cursorPos.x + (indicatorSize.x * 0.5f), cursorPos.y + (indicatorSize.y * 0.5f));
auto half = std::min(indicatorSize.x, indicatorSize.y) * 0.35f;
auto tip = ImVec2(center.x + half, center.y);
auto baseA = ImVec2(center.x - half, center.y - half);
auto baseB = ImVec2(center.x - half, center.y + half);
auto color = ImGui::GetColorU32(ImGuiCol_Text);
ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
ImGui::Dummy(indicatorSize);
if (isAdvance)
{
menuSchema.sounds.select.play();
set(dialogue.get(entry->nextID), character);
}
}
else if (isAdvance)
{
isEnabled = false;
entry = nullptr;
}
}
}
else
{
if (isAdvance)
{
index = ImTextCountCharsFromUtf8(entry->text.c_str(), entry->text.c_str() + entry->text.size());
isFinished = true;
character.isTalking = false;
}
}
}
ImGui::PopFont();
ImGui::PopStyleVar();
};
ImGui::End();
if (isEnabled && isFinished && entry && entry->is_last())
{
if (time += ImGui::GetIO().DeltaTime; time > LIFETIME)
{
isEnabled = false;
entry = nullptr;
}
}
}
bool Text::is_interruptible() const { return !entry || (entry && entry->is_last()); }
}

29
src/state/main/text.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <imgui.h>
#include "../../entity/character.hpp"
#include "../../resources.hpp"
namespace game::state::main
{
class Text
{
int index{};
bool isFinished{};
public:
static constexpr auto LIFETIME = 10.0f;
resource::xml::Dialogue::Entry* entry{};
bool isEnabled{true};
float time{};
void set(resource::xml::Dialogue::Entry*, entity::Character&);
void tick(entity::Character&);
void update(entity::Character&);
bool is_interruptible() const;
};
}

68
src/state/main/toasts.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include "toasts.hpp"
#include <imgui.h>
#include <ranges>
namespace game::state::main
{
void Toasts::tick()
{
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
item.lifetime--;
if (item.lifetime <= 0)
{
items.erase(items.begin() + i--);
continue;
}
}
}
void Toasts::update()
{
if (items.empty()) return;
auto viewport = ImGui::GetMainViewport();
auto& style = ImGui::GetStyle();
auto windowBgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
auto borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border);
auto textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
auto posY = viewport->Size.y - style.WindowPadding.y -
(((ImGui::GetTextLineHeightWithSpacing() + style.WindowPadding.y * 2)) * (items.size() - i));
ImGui::SetNextWindowPos(ImVec2(style.WindowPadding.x, posY));
ImGui::SetNextWindowSize(ImVec2(ImGui::CalcTextSize(item.message.c_str()).x + (style.WindowPadding.x * 2),
ImGui::GetTextLineHeightWithSpacing()));
auto alpha = (float)item.lifetime / Item::LIFETIME;
windowBgColor.w = alpha;
borderColor.w = alpha;
textColor.w = alpha;
ImGui::PushStyleColor(ImGuiCol_WindowBg, windowBgColor);
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
auto name = "##Toast " + std::to_string(i);
if (ImGui::Begin(name.c_str(), nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar))
ImGui::TextUnformatted(item.message.c_str());
ImGui::End();
ImGui::PopStyleColor(3);
}
}
void Toasts::push(const std::string& message) { items.push_back({message, Item::LIFETIME}); }
}

25
src/state/main/toasts.hpp Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <vector>
namespace game::state::main
{
class Toasts
{
public:
struct Item
{
static constexpr auto LIFETIME = 30;
std::string message{};
int lifetime{};
};
std::vector<Item> items{};
void update();
void tick();
void push(const std::string&);
};
};

129
src/state/main/tools.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include "tools.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include <algorithm>
using namespace game::util;
using namespace game::util::imgui;
namespace game::state::main
{
void Tools::update(entity::Character& character, entity::Cursor& cursor, World& world, World::Focus focus,
Canvas& canvas)
{
static constexpr auto WIDTH_MULTIPLIER = 0.05f;
auto style = ImGui::GetStyle();
auto& io = ImGui::GetIO();
auto& schema = character.data.menuSchema;
slide.update(isOpen, io.DeltaTime);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER, windowSize.y - style.WindowPadding.y * 2);
auto targetX = 0;
auto t = slide.value_get();
auto eased = slide.eased_get();
auto closedX = -size.x;
auto posX = closedX + (targetX - closedX) * eased;
auto pos = ImVec2(posX, style.WindowPadding.y);
auto barSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), windowSize.y - style.WindowPadding.y * 2);
auto barPos = ImVec2(pos.x + size.x, style.WindowPadding.y);
if (slide.is_visible())
{
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Tools", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto buttonSize = imgui::to_imvec2(vec2(ImGui::GetContentRegionAvail().x));
auto cursor_mode_button = [&](const char* name, InteractType mode)
{
auto isMode = cursor.mode == mode;
ImGui::PushStyleColor(ImGuiCol_Button,
ImGui::GetStyleColorVec4(isMode ? ImGuiCol_ButtonHovered : ImGuiCol_Button));
if (WIDGET_FX(ImGui::Button(name, buttonSize))) cursor.mode = mode;
ImGui::PopStyleColor();
};
if (WIDGET_FX(ImGui::Button("Home", buttonSize))) world.character_focus(character, canvas, focus);
ImGui::SetItemTooltip("%s", "Reset camera view.\n(Shortcut: Home)");
cursor_mode_button("Rub", InteractType::RUB);
cursor_mode_button("Kiss", InteractType::KISS);
cursor_mode_button("Smack", InteractType::SMACK);
}
ImGui::End();
}
ImGui::SetNextWindowSize(barSize);
ImGui::SetNextWindowPos(barPos);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("##Tools Open Bar", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto buttonSize = ImGui::GetContentRegionAvail();
auto cursorPos = ImGui::GetCursorScreenPos();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto result = WIDGET_FX(ImGui::Button("##ToolsToggle", buttonSize));
if (t <= 0.0f || t >= 1.0f)
{
ImGui::SetItemTooltip(isOpen ? "Close Tools" : "Open Tools");
if (result)
{
isOpen = !isOpen;
if (isOpen)
schema.sounds.open.play();
else
schema.sounds.close.play();
}
if (!isOpen && t <= 0.0f && ImGui::IsItemHovered())
{
isOpen = true;
schema.sounds.open.play();
}
}
ImGui::PopStyleVar();
auto center = ImVec2(cursorPos.x + (buttonSize.x * 0.5f), cursorPos.y + (buttonSize.y * 0.5f));
auto half = std::min(buttonSize.x, buttonSize.y) * 0.22f;
ImVec2 tip;
ImVec2 baseA;
ImVec2 baseB;
if (isOpen)
{
tip = ImVec2(center.x - half, center.y);
baseA = ImVec2(center.x + half, center.y - half);
baseB = ImVec2(center.x + half, center.y + half);
}
else
{
tip = ImVec2(center.x + half, center.y);
baseA = ImVec2(center.x - half, center.y - half);
baseB = ImVec2(center.x - half, center.y + half);
}
auto color = ImGui::GetColorU32(ImGuiCol_Text);
ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
}
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar(2);
}
}

18
src/state/main/tools.hpp Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <imgui.h>
#include "../../util/imgui/window_slide.hpp"
#include "world.hpp"
namespace game::state::main
{
class Tools
{
public:
bool isOpen{};
util::imgui::WindowSlide slide{0.125f, 0.0f};
void update(entity::Character&, entity::Cursor&, World&, World::Focus, Canvas&);
};
}

92
src/state/main/world.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "world.hpp"
#include "../../util/imgui.hpp"
#include "../../util/math.hpp"
#include <algorithm>
#include <cmath>
using namespace game::util;
namespace game::state::main
{
void World::set(entity::Character& character, Canvas& canvas, Focus focus)
{
character.stage = character.stage_get();
character.queue_idle_animation();
character_focus(character, canvas, focus);
}
void World::update(entity::Character& character, entity::Cursor& cursor, Canvas& canvas, Focus focus)
{
auto& cursorSchema = character.data.cursorSchema;
auto& pan = canvas.pan;
auto& zoom = canvas.zoom;
auto& io = ImGui::GetIO();
bool isPan{true};
auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
auto panMultiplier = ZOOM_BASE / zoom;
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
{
if ((isMouseMiddleDown) && isPan)
{
cursor.queue_play({cursorSchema.animations.pan.get()});
pan -= imgui::to_vec2(io.MouseDelta) * panMultiplier;
}
if (io.MouseWheel != 0)
{
auto viewport = ImGui::GetMainViewport();
auto mousePos = io.MousePos;
auto cursorPos = imgui::to_vec2(ImVec2(mousePos.x - viewport->Pos.x, mousePos.y - viewport->Pos.y));
auto zoomBefore = zoom;
auto zoomFactorBefore = math::to_unit(zoomBefore);
auto cursorWorld = pan + (cursorPos / zoomFactorBefore);
cursor.queue_play({cursorSchema.animations.zoom.get()});
zoom = glm::clamp(ZOOM_MIN, zoom + (io.MouseWheel * ZOOM_STEP), ZOOM_MAX);
auto zoomFactorAfter = math::to_unit(zoom);
pan = cursorWorld - (cursorPos / zoomFactorAfter);
}
}
zoom = glm::clamp(ZOOM_MIN, zoom, ZOOM_MAX);
if (ImGui::IsKeyPressed(ImGuiKey_Home)) character_focus(character, canvas, focus);
}
void World::character_focus(entity::Character& character, Canvas& canvas, Focus focus)
{
static constexpr float MENU_WIDTH_MULTIPLIER = 0.30f;
static constexpr float TOOLS_WIDTH_MULTIPLIER = 0.10f;
static constexpr float PADDING = 100.0f;
auto rect = character.rect();
if (!std::isfinite(rect.x) || !std::isfinite(rect.y) || !std::isfinite(rect.z) || !std::isfinite(rect.w) ||
rect.z <= 0.0f || rect.w <= 0.0f)
return;
rect = {rect.x - PADDING * 0.5f, rect.y - PADDING * 0.5f, rect.z + PADDING, rect.w + PADDING};
auto zoomFactor = std::min((float)canvas.size.x / rect.z, (float)canvas.size.y / rect.w);
canvas.zoom = glm::clamp(ZOOM_MIN, math::to_percent(zoomFactor), ZOOM_MAX);
zoomFactor = math::to_unit(canvas.zoom);
auto rectCenter = glm::vec2(rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f);
auto viewSizeWorld = glm::vec2(canvas.size) / zoomFactor;
canvas.pan = rectCenter - (vec2(viewSizeWorld.x, viewSizeWorld.y) * 0.5f);
auto menuWidthWorld = (canvas.size.x * MENU_WIDTH_MULTIPLIER) / zoomFactor;
auto toolsWidthWorld = (canvas.size.x * TOOLS_WIDTH_MULTIPLIER) / zoomFactor;
if (focus == Focus::MENU || focus == Focus::MENU_TOOLS) canvas.pan.x += menuWidthWorld * 0.5f;
if (focus == Focus::TOOLS || focus == Focus::MENU_TOOLS) canvas.pan.x -= toolsWidthWorld * 0.5f;
auto panMin = glm::vec2(0.0f, 0.0f);
auto panMax = glm::max(glm::vec2(0.0f), SIZE - viewSizeWorld);
canvas.pan = glm::clamp(panMin, canvas.pan, panMax);
}
}

35
src/state/main/world.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "../../canvas.hpp"
#include "../../entity/character.hpp"
#include "character_manager.hpp"
#include "item_manager.hpp"
namespace game::state::main
{
class World
{
public:
static constexpr auto ZOOM_MIN = 50.0f;
static constexpr auto ZOOM_BASE = 100.0f;
static constexpr auto ZOOM_STEP = 25.0f;
static constexpr auto ZOOM_MAX = 400.0f;
static constexpr auto SIZE = glm::vec2{1920, 1080};
static constexpr auto BOUNDS =
glm::vec4(SIZE.x * 0.05, SIZE.y * 0.05, SIZE.x - (SIZE.x * 0.05f), SIZE.y - (SIZE.y * 0.05f));
enum Focus
{
CENTER,
MENU,
MENU_TOOLS,
TOOLS
};
void update(entity::Character& character, entity::Cursor& cursor, Canvas& canvas, Focus = CENTER);
void character_focus(entity::Character& character, Canvas& canvas, Focus = CENTER);
void set(entity::Character& character, Canvas& canvas, Focus = CENTER);
glm::vec2 screen_to_world(glm::vec2 screenPosition, const Canvas& canvas) const;
};
}

25
src/state/select.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "select.hpp"
#include <imgui_impl_opengl3.h>
using namespace game::util;
namespace game::state
{
void Select::tick() { preview.tick(characterIndex); }
void Select::update(Resources& resources)
{
preview.update(resources, characterIndex);
info.update(resources, characterIndex);
characters.update(resources, characterIndex);
}
void Select::render(Resources& resources, Canvas& canvas)
{
canvas.bind();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
canvas.unbind();
}
};

24
src/state/select.hpp Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include "../canvas.hpp"
#include "select/characters.hpp"
#include "select/info.hpp"
#include "select/preview.hpp"
namespace game::state
{
class Select
{
public:
select::Characters characters{};
select::Info info{};
select::Preview preview{};
int characterIndex{-1};
void tick();
void update(Resources&);
void render(Resources&, Canvas&);
};
};

View File

@@ -0,0 +1,81 @@
#include "characters.hpp"
#include <ranges>
#include "../../util/imgui/widget.hpp"
using namespace game::util::imgui;
namespace game::state::select
{
void Characters::update(Resources& resources, int& characterIndex)
{
auto& style = ImGui::GetStyle();
auto viewport = ImGui::GetMainViewport();
auto size =
ImVec2(viewport->Size.x / 2.0f - style.WindowPadding.x, viewport->Size.y - (style.WindowPadding.y * 2.0f));
auto pos = ImVec2(viewport->Size.x / 2.0f, style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Main Menu", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar))
{
if (ImGui::BeginTabBar("##Main Menu Bar"))
{
if (WIDGET_FX(ImGui::BeginTabItem("Characters")))
{
auto cursorPos = ImGui::GetCursorPos();
auto cursorStartX = ImGui::GetCursorPosX();
auto buttonSize = ImVec2(ImGui::GetContentRegionAvail().x / 4, ImGui::GetContentRegionAvail().x / 4);
for (int i = 0; i < (int)resources.characterPreviews.size(); i++)
{
auto& character = resources.characterPreviews[i];
ImGui::PushID(i);
ImGui::SetCursorPos(cursorPos);
auto isSelected = i == characterIndex;
if (isSelected) ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered));
if (character.portrait.is_valid())
{
if (WIDGET_FX(ImGui::ImageButton(character.name.c_str(), character.portrait.id, buttonSize)))
characterIndex = i;
}
else if (WIDGET_FX(ImGui::Button(character.name.c_str(), buttonSize)))
characterIndex = i;
if (isSelected) ImGui::PopStyleColor();
ImGui::SetItemTooltip("%s", character.name.c_str());
auto increment = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;
cursorPos.x += increment;
if (cursorPos.x + increment > ImGui::GetContentRegionAvail().x)
{
cursorPos.x = cursorStartX;
cursorPos.y += increment;
}
ImGui::PopID();
}
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Configuration")))
{
configuration.update(resources);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
}
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include "../../resources.hpp"
#include "../configuration.hpp"
namespace game::state::select
{
class Characters
{
public:
Configuration configuration;
void update(Resources&, int& characterIndex);
};
}

127
src/state/select/info.cpp Normal file
View File

@@ -0,0 +1,127 @@
#include "info.hpp"
#include "../../util/color.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/vector.hpp"
using namespace game::util;
using namespace game::util::imgui;
using namespace game::util::measurement;
using namespace game::resource;
namespace game::state::select
{
void Info::update(Resources& resources, int characterIndex)
{
if (!vector::in_bounds(resources.characterPreviews, characterIndex)) return;
auto& style = ImGui::GetStyle();
auto viewport = ImGui::GetMainViewport();
auto size = ImVec2(viewport->Size.x / 2.0f - (style.WindowPadding.x * 2.0f),
(viewport->Size.y / 2.0f) - (style.WindowPadding.y * 2.0f));
auto pos = ImVec2(style.WindowPadding.x, (viewport->Size.y / 2.0f) + style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Info", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar))
{
auto& character = resources.characterPreviews[characterIndex];
auto& save = character.save;
auto& system = resources.settings.measurementSystem;
auto& weight = save.is_valid() ? save.weight : character.weight;
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_3);
auto childSize = imgui::size_without_footer_get();
if (ImGui::BeginChild("##Info Child", childSize))
{
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_3);
ImGui::TextUnformatted(character.name.c_str());
ImGui::PopFont();
if (!character.description.empty())
{
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::TextWrapped("%s", character.description.c_str());
ImGui::PopFont();
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::Text("Weight: %0.2f %s", system == IMPERIAL ? weight * KG_TO_LB : weight,
system == IMPERIAL ? "lbs" : "kg");
ImGui::Text("Stages: %i", character.stages);
ImGui::Separator();
ImGui::PopFont();
ImGui::PushFont(ImGui::GetFont(), Font::NORMAL);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
if (!character.author.empty()) ImGui::TextWrapped("Author: %s", character.author.c_str());
ImGui::PopStyleColor();
ImGui::PopFont();
}
ImGui::EndChild();
auto widgetSize = row_widget_size_get(save.is_valid() ? 2 : 1);
if (save.is_valid())
{
if (WIDGET_FX(ImGui::Button("Continue", widgetSize))) isContinue = true;
ImGui::PushFont(ImGui::GetFont(), Font::NORMAL);
ImGui::SetItemTooltip("%s", "Continue from a saved game.");
ImGui::PopFont();
ImGui::SameLine();
}
if (WIDGET_FX(ImGui::Button("New Game", widgetSize)))
{
if (save.is_valid())
{
ImGui::OpenPopup("New Game Warning");
isNewGameWarning = true;
}
else
isNewGame = true;
}
ImGui::PushFont(ImGui::GetFont(), Font::NORMAL);
ImGui::SetItemTooltip("%s", "Start a new game.\nThis will delete progress!");
ImGui::PopFont();
ImGui::PopFont();
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * 0.5f, 0), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(viewport->GetCenter()), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("New Game Warning", &isNewGameWarning,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize))
{
auto widgetSize = row_widget_size_get(save.is_valid() ? 2 : 1);
ImGui::TextWrapped("This will delete saved progress! Are you sure?");
if (WIDGET_FX(ImGui::Button("Yes", widgetSize))) isNewGame = true;
ImGui::SameLine();
if (WIDGET_FX(ImGui::Button("No", widgetSize))) isNewGameWarning = false;
ImGui::EndPopup();
}
}
ImGui::End();
}
}

16
src/state/select/info.hpp Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "../../resources.hpp"
namespace game::state::select
{
class Info
{
public:
bool isContinue{};
bool isNewGame{};
bool isNewGameWarning{};
void update(Resources&, int characterIndex);
};
}

View File

@@ -0,0 +1,110 @@
#include "preview.hpp"
#include <algorithm>
#include <cmath>
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/vector.hpp"
using namespace game::entity;
using namespace game::resource;
using namespace game::util;
using namespace game::util::imgui;
namespace game::state::select
{
void Preview::tick(int characterIndex)
{
if (characterIndex != -1 && isInGame) actor.tick();
}
void Preview::update(Resources& resources, int characterIndex)
{
if (!vector::in_bounds(resources.characterPreviews, characterIndex)) return;
auto& style = ImGui::GetStyle();
auto viewport = ImGui::GetMainViewport();
auto size = ImVec2(viewport->Size.x / 2.0f - (style.WindowPadding.x * 2.0f),
(viewport->Size.y / 2.0f) - (style.WindowPadding.y * 2.0f));
auto pos = ImVec2(style.WindowPadding.x, style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Preview", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar))
{
if (ImGui::BeginTabBar("##Preview Tab Bar"))
{
auto& character = resources.characterPreviews[characterIndex];
auto available = ImGui::GetContentRegionAvail();
auto availableSize = imgui::to_vec2(available);
auto textureSize = vec2(character.render.size);
if (WIDGET_FX(ImGui::BeginTabItem("Render")))
{
auto scale =
(availableSize.x <= 0.0f || availableSize.y <= 0.0f || textureSize.x <= 0.0f || textureSize.y <= 0.0f)
? 0.0f
: std::min(availableSize.x / textureSize.x, availableSize.y / textureSize.y);
auto size = ImVec2(textureSize.x * scale, textureSize.y * scale);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (availableSize.x * 0.5f) - (size.y * 0.5f),
ImGui::GetCursorPosY() + (availableSize.y * 0.5f) - (size.y * 0.5f)));
ImGui::Image(character.render.id, size);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("In Game")))
{
isInGame = true;
if (previousCharacterIndex != characterIndex)
{
actor = Actor(resources.characterPreviews[characterIndex].anm2);
rect = actor.rect();
previousCharacterIndex = characterIndex;
}
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (availableSize.x <= 0.0f || availableSize.y <= 0.0f || rectSize.x <= 0.0f ||
rectSize.y <= 0.0f || !std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(availableSize.x / rectSize.x, availableSize.y / rectSize.y);
auto previewSize = rectSize * previewScale;
auto canvasSize = ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
canvas.zoom = previewScale * 100.0f;
canvas.pan = vec2(rect.x, rect.y);
auto cursorPos = ImGui::GetCursorPos();
ImGui::SetCursorPos(ImVec2(cursorPos.x + (availableSize.x * 0.5f) - ((float)canvasSize.x * 0.5f),
cursorPos.y + (availableSize.y * 0.5f) - ((float)canvasSize.y * 0.5f)));
canvas.bind();
canvas.size_set(canvasSize);
canvas.clear();
actor.render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
canvas.unbind();
ImGui::Image(canvas.texture, imgui::to_imvec2(canvasSize));
ImGui::EndTabItem();
}
else
isInGame = false;
ImGui::EndTabBar();
}
}
ImGui::End();
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../../entity/actor.hpp"
#include "../../resources.hpp"
namespace game::state::select
{
class Preview
{
public:
int previousCharacterIndex{-1};
entity::Actor actor{};
glm::vec4 rect{};
bool isInGame{};
Canvas canvas{glm::vec2(), Canvas::FLIP};
void update(Resources& resources, int characterIndex);
void tick(int characterIndex);
};
}