refactoring, new game(s) in progress
This commit is contained in:
@@ -75,7 +75,7 @@ namespace game::state
|
||||
cursor = entity::Cursor(character.data.cursorSchema.anm2);
|
||||
cursor.interactTypeID = character.data.interactTypeNames.empty() ? -1 : 0;
|
||||
|
||||
menu.inventory = Inventory{};
|
||||
menu.inventory = play::menu::Inventory{};
|
||||
for (auto& [id, quantity] : saveData.inventory)
|
||||
{
|
||||
if (quantity == 0) continue;
|
||||
@@ -94,16 +94,17 @@ namespace game::state
|
||||
item.rotation);
|
||||
}
|
||||
|
||||
imgui::style::rounding_set(menuSchema.rounding);
|
||||
imgui::style::widget_set(menuSchema.rounding);
|
||||
imgui::widget::sounds_set(&menuSchema.sounds.hover, &menuSchema.sounds.select);
|
||||
play::style::color_set(resources, character);
|
||||
|
||||
menu.arcade = Arcade(character);
|
||||
menu.arcade.skillCheck.totalPlays = saveData.totalPlays;
|
||||
menu.arcade.skillCheck.highScore = saveData.highScore;
|
||||
menu.arcade.skillCheck.bestCombo = saveData.bestCombo;
|
||||
menu.arcade.skillCheck.gradeCounts = saveData.gradeCounts;
|
||||
menu.arcade.skillCheck.isHighScoreAchieved = saveData.highScore > 0 ? true : false;
|
||||
menu.arcade = play::menu::Arcade(character);
|
||||
menu.arcade.skillCheck.totalPlays = saveData.skillCheck.totalPlays;
|
||||
menu.arcade.skillCheck.highScore = saveData.skillCheck.highScore;
|
||||
menu.arcade.skillCheck.bestCombo = saveData.skillCheck.bestCombo;
|
||||
menu.arcade.skillCheck.gradeCounts = saveData.skillCheck.gradeCounts;
|
||||
menu.arcade.skillCheck.isHighScoreAchieved = saveData.skillCheck.highScore > 0 ? true : false;
|
||||
menu.arcade.orbit.highScore = saveData.orbit.highScore;
|
||||
|
||||
text.entry = nullptr;
|
||||
text.isEnabled = false;
|
||||
@@ -153,7 +154,7 @@ namespace game::state
|
||||
void Play::exit(Resources& resources)
|
||||
{
|
||||
imgui::style::color_set(resources.settings.color);
|
||||
imgui::style::rounding_set();
|
||||
imgui::style::widget_set();
|
||||
imgui::widget::sounds_set(nullptr, nullptr);
|
||||
ImGui::GetIO().FontDefault = resources.font.get();
|
||||
save(resources);
|
||||
@@ -182,6 +183,7 @@ namespace game::state
|
||||
|
||||
auto focus = focus_get();
|
||||
auto& dialogue = character.data.dialogue;
|
||||
cursor.isVisible = true;
|
||||
|
||||
if (!menu.isCheats)
|
||||
{
|
||||
@@ -298,6 +300,7 @@ namespace game::state
|
||||
character.update();
|
||||
cursor.update();
|
||||
world.update(character, cursor, worldCanvas, focus);
|
||||
worldCanvas.tick();
|
||||
|
||||
if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.settingsMenu.isSave)
|
||||
{
|
||||
@@ -337,7 +340,7 @@ namespace game::state
|
||||
worldCanvas.unbind();
|
||||
|
||||
canvas.bind();
|
||||
canvas.texture_render(textureShader, worldCanvas.texture, windowModel);
|
||||
canvas.texture_render(textureShader, worldCanvas, windowModel);
|
||||
ImGui::Render();
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
cursor.render(textureShader, rectShader, canvas);
|
||||
@@ -358,10 +361,11 @@ namespace game::state
|
||||
save.digestionTimer = character.digestionTimer;
|
||||
save.totalCaloriesConsumed = character.totalCaloriesConsumed;
|
||||
save.totalFoodItemsEaten = character.totalFoodItemsEaten;
|
||||
save.totalPlays = menu.arcade.skillCheck.totalPlays;
|
||||
save.highScore = menu.arcade.skillCheck.highScore;
|
||||
save.bestCombo = menu.arcade.skillCheck.bestCombo;
|
||||
save.gradeCounts = menu.arcade.skillCheck.gradeCounts;
|
||||
save.skillCheck.totalPlays = menu.arcade.skillCheck.totalPlays;
|
||||
save.skillCheck.highScore = menu.arcade.skillCheck.highScore;
|
||||
save.skillCheck.bestCombo = menu.arcade.skillCheck.bestCombo;
|
||||
save.skillCheck.gradeCounts = menu.arcade.skillCheck.gradeCounts;
|
||||
save.orbit.highScore = menu.arcade.orbit.highScore;
|
||||
save.isPostgame = isPostgame;
|
||||
save.isAlternateSpritesheet = character.spritesheetType == entity::Character::ALTERNATE;
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "arcade.hpp"
|
||||
|
||||
#include "../../util/imgui/widget.hpp"
|
||||
|
||||
using namespace game::util::imgui;
|
||||
using namespace game::resource::xml;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
Arcade::Arcade(entity::Character& character) : skillCheck(character) {}
|
||||
|
||||
void Arcade::tick() { skillCheck.tick(); }
|
||||
|
||||
void Arcade::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text)
|
||||
{
|
||||
auto available = ImGui::GetContentRegionAvail();
|
||||
auto& strings = character.data.strings;
|
||||
|
||||
if (view == SKILL_CHECK)
|
||||
{
|
||||
if (skillCheck.update(resources, character, inventory, text)) view = MENU;
|
||||
return;
|
||||
}
|
||||
|
||||
auto buttonHeight = ImGui::GetFrameHeightWithSpacing();
|
||||
auto childSize = ImVec2(available.x, std::max(0.0f, available.y - buttonHeight));
|
||||
|
||||
if (ImGui::BeginChild("##Arcade Child", childSize))
|
||||
{
|
||||
if (view == MENU)
|
||||
{
|
||||
auto buttonWidth = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
ImGui::TextUnformatted(strings.get(Strings::ArcadeSkillCheckName).c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::ArcadeSkillCheckDescription).c_str());
|
||||
ImGui::Separator();
|
||||
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadePlayButton).c_str(), ImVec2(buttonWidth, 0))))
|
||||
view = SKILL_CHECK;
|
||||
ImGui::SameLine();
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeStatsButton).c_str(), ImVec2(buttonWidth, 0))))
|
||||
view = SKILL_CHECK_STATS;
|
||||
}
|
||||
else if (view == SKILL_CHECK_STATS)
|
||||
{
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
ImGui::TextUnformatted(strings.get(Strings::ArcadeSkillCheckName).c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text(strings.get(Strings::ArcadeBestFormat).c_str(), skillCheck.highScore, skillCheck.bestCombo);
|
||||
ImGui::Text(strings.get(Strings::ArcadeTotalSkillChecksFormat).c_str(), skillCheck.totalPlays);
|
||||
|
||||
for (int i = 0; i < (int)schema.grades.size(); i++)
|
||||
{
|
||||
auto& grade = schema.grades[i];
|
||||
ImGui::Text("%s: %i", grade.namePlural.c_str(), skillCheck.gradeCounts[i]);
|
||||
}
|
||||
|
||||
ImGui::Text(strings.get(Strings::ArcadeAccuracyFormat).c_str(), skillCheck.accuracy_score_get(character));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (view == SKILL_CHECK_STATS)
|
||||
{
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeBackButton).c_str()))) view = MENU;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "arcade/skill_check.hpp"
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
class Arcade
|
||||
{
|
||||
public:
|
||||
enum View
|
||||
{
|
||||
MENU,
|
||||
SKILL_CHECK,
|
||||
SKILL_CHECK_STATS
|
||||
};
|
||||
|
||||
SkillCheck skillCheck{};
|
||||
View view{MENU};
|
||||
|
||||
Arcade() = default;
|
||||
Arcade(entity::Character&);
|
||||
|
||||
void tick();
|
||||
void update(Resources&, entity::Character&, Inventory&, Text&);
|
||||
};
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
#include "skill_check.hpp"
|
||||
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include "../../../util/imgui.hpp"
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
#include "../../../util/math.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
|
||||
using namespace game::util;
|
||||
using namespace game::entity;
|
||||
using namespace game::resource;
|
||||
using namespace game::resource::xml;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
float SkillCheck::accuracy_score_get(entity::Character& character)
|
||||
{
|
||||
if (totalPlays == 0) return 0.0f;
|
||||
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
SkillCheck::Challenge SkillCheck::challenge_generate(entity::Character& character)
|
||||
{
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SkillCheck::SkillCheck(entity::Character& character) { challenge = challenge_generate(character); }
|
||||
|
||||
void SkillCheck::tick()
|
||||
{
|
||||
for (auto& [i, actor] : itemActors)
|
||||
actor.tick();
|
||||
}
|
||||
|
||||
bool SkillCheck::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 = 5.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.skillCheckSchema;
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
auto& strings = character.data.strings;
|
||||
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 menuButtonHeight = ImGui::GetFrameHeightWithSpacing();
|
||||
size.y = std::max(0.0f, size.y - menuButtonHeight);
|
||||
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::Text(strings.get(Strings::SkillCheckScoreFormat).c_str(), score, combo);
|
||||
std::array<char, 128> bestBuffer{};
|
||||
std::snprintf(bestBuffer.data(), bestBuffer.size(), strings.get(Strings::SkillCheckBestFormat).c_str(), highScore,
|
||||
bestCombo);
|
||||
auto bestString = std::string(bestBuffer.data());
|
||||
ImGui::SetCursorPos(ImVec2(size.x - ImGui::CalcTextSize(bestString.c_str()).x, cursorPos.y));
|
||||
|
||||
ImGui::Text(strings.get(Strings::SkillCheckBestFormat).c_str(), highScore, bestCombo);
|
||||
|
||||
if (score == 0 && isActive)
|
||||
{
|
||||
ImGui::SetCursorPos(ImVec2(style.WindowPadding.x, size.y - style.WindowPadding.y));
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::SkillCheckInstructions).c_str());
|
||||
}
|
||||
|
||||
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, (float)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 lineColor = LINE_COLOR;
|
||||
lineColor.w = isActive ? 1.0f : endTimerProgress;
|
||||
drawList->AddRectFilled(lineMin, lineMax, ImGui::GetColorU32(lineColor));
|
||||
|
||||
if (!isActive && !isGameOver)
|
||||
{
|
||||
range_draw(queuedChallenge.range, 1.0f - endTimerProgress);
|
||||
|
||||
auto queuedLineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * queuedChallenge.tryValue));
|
||||
auto queuedLineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, queuedLineMin.y + LINE_HEIGHT);
|
||||
auto queuedLineColor = LINE_COLOR;
|
||||
queuedLineColor.w = 1.0f - endTimerProgress;
|
||||
drawList->AddRectFilled(queuedLineMin, queuedLineMax, ImGui::GetColorU32(queuedLineColor));
|
||||
}
|
||||
|
||||
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(strings.get(Strings::SkillCheckScoreLoss).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y);
|
||||
toasts.emplace_back(strings.get(Strings::SkillCheckScoreLoss), 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("##SkillCheckBar", 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.skillCheckRewardItemPool)
|
||||
{
|
||||
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(strings.get(Strings::SkillCheckRewardToast).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y));
|
||||
toasts.emplace_back(strings.get(Strings::SkillCheckRewardToast), 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(strings.get(Strings::SkillCheckHighScoreToast).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y + ImGui::GetTextLineHeightWithSpacing());
|
||||
toasts.emplace_back(strings.get(Strings::SkillCheckHighScoreToast), 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;
|
||||
combo = 0;
|
||||
if (isHighScoreAchievedThisRun) schema.sounds.highScoreLoss.play();
|
||||
if (highScore > 0) isHighScoreAchieved = true;
|
||||
isRewardScoreAchieved = false;
|
||||
isHighScoreAchievedThisRun = false;
|
||||
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::vformat(strings.get(Strings::SkillCheckGradeSuccessTemplate),
|
||||
std::make_format_args(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 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||
textColor.w = ((float)toastMessage.time / toastMessage.timeMax);
|
||||
|
||||
drawList->AddText(toastMessage.position, ImGui::GetColorU32(textColor), 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();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(position.x, position.y + size.y + ImGui::GetStyle().ItemSpacing.y));
|
||||
return WIDGET_FX(ImGui::Button(strings.get(Strings::SkillCheckMenuButton).c_str()));
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,22 @@ namespace game::state::play
|
||||
{
|
||||
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& scale = override_.frame.scale;
|
||||
auto& scaleBase = override_.frameBase.scale;
|
||||
auto isScaleValid = scale.x.has_value() && scale.y.has_value() && scaleBase.x.has_value() && scaleBase.y.has_value();
|
||||
|
||||
if (isScaleValid && 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);
|
||||
auto amplitude = glm::abs(glm::vec2(*scaleBase.x, *scaleBase.y));
|
||||
auto value = amplitude * (oscillation * envelope);
|
||||
|
||||
*override_.frame.scale = amplitude * (oscillation * envelope);
|
||||
scale.x = value.x;
|
||||
scale.y = value.y;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ using namespace game::resource::xml;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
void Cheats::update(Resources&, entity::Character& character, Inventory& inventory)
|
||||
void Cheats::update(Resources&, entity::Character& character, menu::Inventory& inventory)
|
||||
{
|
||||
auto& strings = character.data.strings;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "inventory.hpp"
|
||||
#include "menu/inventory.hpp"
|
||||
#include "text.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -10,6 +10,6 @@ namespace game::state::play
|
||||
class Cheats
|
||||
{
|
||||
public:
|
||||
void update(Resources&, entity::Character&, Inventory&);
|
||||
void update(Resources&, entity::Character&, menu::Inventory&);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ namespace game::state::play
|
||||
void Debug::update(entity::Character& character, entity::Cursor& cursor, ItemManager& itemManager, Canvas& canvas,
|
||||
Text& text)
|
||||
{
|
||||
auto& strings = character.data.strings;
|
||||
auto cursorPosition = canvas.screen_position_convert(cursor.position);
|
||||
|
||||
ImGui::Text(strings.get(Strings::DebugCursorScreenFormat).c_str(), cursor.position.x, cursor.position.y);
|
||||
ImGui::Text(strings.get(Strings::DebugCursorWorldFormat).c_str(), cursorPosition.x, cursorPosition.y);
|
||||
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);
|
||||
|
||||
ImGui::SeparatorText(strings.get(Strings::DebugAnimations).c_str());
|
||||
ImGui::Text(strings.get(Strings::DebugNowPlayingFormat).c_str(), character.animationMapReverse.at(character.animationIndex).c_str());
|
||||
ImGui::SeparatorText("Animations");
|
||||
ImGui::Text("Now Playing: %s", character.animationMapReverse.at(character.animationIndex).c_str());
|
||||
|
||||
auto childSize = ImVec2(0, ImGui::GetContentRegionAvail().y / 3);
|
||||
|
||||
@@ -37,7 +36,7 @@ namespace game::state::play
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SeparatorText(strings.get(Strings::DebugDialogue).c_str());
|
||||
ImGui::SeparatorText("Dialogue");
|
||||
|
||||
if (ImGui::BeginChild("##Dialogue", childSize, ImGuiChildFlags_Borders))
|
||||
{
|
||||
@@ -52,21 +51,21 @@ namespace game::state::play
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
WIDGET_FX(ImGui::Checkbox(strings.get(Strings::DebugShowNulls).c_str(), &character.isShowNulls));
|
||||
WIDGET_FX(ImGui::Checkbox(strings.get(Strings::DebugShowWorldBounds).c_str(), &isBoundsDisplay));
|
||||
WIDGET_FX(ImGui::Checkbox("Show Nulls (Hitboxes)", &character.isShowNulls));
|
||||
WIDGET_FX(ImGui::Checkbox("Show World Bounds", &isBoundsDisplay));
|
||||
|
||||
if (!itemManager.items.empty())
|
||||
{
|
||||
ImGui::SeparatorText(strings.get(Strings::DebugItem).c_str());
|
||||
ImGui::SeparatorText("Item");
|
||||
|
||||
for (int i = 0; i < (int)itemManager.items.size(); i++)
|
||||
{
|
||||
auto& item = itemManager.items[i];
|
||||
if (itemManager.heldItemIndex == i) ImGui::TextUnformatted(strings.get(Strings::DebugHeld).c_str());
|
||||
ImGui::Text(strings.get(Strings::DebugItemTypeFormat).c_str(), item.schemaID);
|
||||
ImGui::Text(strings.get(Strings::DebugItemPositionFormat).c_str(), item.position.x, item.position.y);
|
||||
ImGui::Text(strings.get(Strings::DebugItemVelocityFormat).c_str(), item.velocity.x, item.velocity.y);
|
||||
ImGui::Text(strings.get(Strings::DebugItemDurabilityFormat).c_str(), item.durability);
|
||||
if (itemManager.heldItemIndex == i) ImGui::TextUnformatted("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("Durability: %i", item.durability);
|
||||
ImGui::Separator();
|
||||
}
|
||||
}
|
||||
|
||||
61
src/state/play/item/reward.cpp
Normal file
61
src/state/play/item/reward.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "reward.hpp"
|
||||
|
||||
#include "../../../util/math.hpp"
|
||||
|
||||
using namespace game::util;
|
||||
|
||||
namespace game::state::play::item
|
||||
{
|
||||
int Reward::random_item_get(const resource::xml::Item& itemSchema, float chanceBonus)
|
||||
{
|
||||
const resource::xml::Item::Pool* pool{};
|
||||
|
||||
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.at(id);
|
||||
rarity.sound.play();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pool || pool->empty()) return INVALID_ID;
|
||||
return (*pool)[(int)math::random_roll((float)pool->size())];
|
||||
}
|
||||
|
||||
void Reward::item_give(int itemID, menu::Inventory& inventory, menu::ItemEffectManager& itemEffectManager,
|
||||
const resource::xml::Item& itemSchema, const ImVec4& bounds, menu::ItemEffectManager::Mode mode)
|
||||
{
|
||||
if (itemID < 0) return;
|
||||
|
||||
inventory.values[itemID]++;
|
||||
itemEffectManager.spawn(itemID, itemSchema, bounds, mode);
|
||||
}
|
||||
|
||||
int Reward::reward_random_items_try(menu::Inventory& inventory, menu::ItemEffectManager& itemEffectManager,
|
||||
const resource::xml::Item& itemSchema, const ImVec4& bounds, float rewardChance,
|
||||
float rewardRollCount, menu::ItemEffectManager::Mode mode)
|
||||
{
|
||||
if (!math::random_percent_roll(rewardChance)) return 0;
|
||||
|
||||
auto rollCountWhole = std::max(0, (int)std::floor(rewardRollCount));
|
||||
auto rollCountFraction = std::max(0.0f, rewardRollCount - (float)rollCountWhole);
|
||||
auto rollCount = rollCountWhole + (math::random_percent_roll(rollCountFraction) ? 1 : 0);
|
||||
auto rewardedItemCount = 0;
|
||||
|
||||
for (int i = 0; i < rollCount; i++)
|
||||
{
|
||||
auto itemID = random_item_get(itemSchema);
|
||||
if (itemID == INVALID_ID) continue;
|
||||
|
||||
item_give(itemID, inventory, itemEffectManager, itemSchema, bounds, mode);
|
||||
rewardedItemCount++;
|
||||
}
|
||||
|
||||
return rewardedItemCount;
|
||||
}
|
||||
}
|
||||
22
src/state/play/item/reward.hpp
Normal file
22
src/state/play/item/reward.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "../menu/inventory.hpp"
|
||||
#include "../menu/item_effect_manager.hpp"
|
||||
|
||||
namespace game::state::play::item
|
||||
{
|
||||
class Reward
|
||||
{
|
||||
public:
|
||||
static constexpr auto INVALID_ID = -1;
|
||||
|
||||
int random_item_get(const resource::xml::Item& itemSchema, float chanceBonus = 1.0f);
|
||||
void item_give(int itemID, menu::Inventory& inventory, menu::ItemEffectManager& itemEffectManager,
|
||||
const resource::xml::Item& itemSchema, const ImVec4& bounds,
|
||||
menu::ItemEffectManager::Mode mode = menu::ItemEffectManager::FALL_DOWN);
|
||||
int reward_random_items_try(menu::Inventory& inventory, menu::ItemEffectManager& itemEffectManager,
|
||||
const resource::xml::Item& itemSchema, const ImVec4& bounds, float rewardChance,
|
||||
float rewardRollCount,
|
||||
menu::ItemEffectManager::Mode mode = menu::ItemEffectManager::FALL_DOWN);
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "style.hpp"
|
||||
|
||||
#include "../../util/imgui.hpp"
|
||||
#include "../../util/imgui/style.hpp"
|
||||
#include "../../util/imgui/widget.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -68,7 +67,7 @@ namespace game::state::play
|
||||
|
||||
if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabArcade).c_str())))
|
||||
{
|
||||
arcade.update(resources, character, inventory, text);
|
||||
arcade.update(resources, character, cursor, inventory, text, toasts);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
@@ -92,7 +91,7 @@ namespace game::state::play
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabDebug).c_str())))
|
||||
if (WIDGET_FX(ImGui::BeginTabItem("Debug")))
|
||||
{
|
||||
debug.update(character, cursor, itemManager, canvas, text);
|
||||
ImGui::EndTabItem();
|
||||
@@ -121,9 +120,7 @@ namespace game::state::play
|
||||
|
||||
if (t <= 0.0f || t >= 1.0f)
|
||||
{
|
||||
ImGui::SetItemTooltip("%s", strings.get(isOpen ? Strings::MenuCloseTooltip
|
||||
: Strings::MenuOpenTooltip)
|
||||
.c_str());
|
||||
ImGui::SetItemTooltip("%s", strings.get(isOpen ? Strings::MenuCloseTooltip : Strings::MenuOpenTooltip).c_str());
|
||||
if (result)
|
||||
{
|
||||
isOpen = !isOpen;
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
#include "../settings_menu.hpp"
|
||||
|
||||
#include "arcade.hpp"
|
||||
#include "menu/arcade.hpp"
|
||||
#include "cheats.hpp"
|
||||
#include "debug.hpp"
|
||||
#include "interact.hpp"
|
||||
#include "inventory.hpp"
|
||||
#include "menu/interact.hpp"
|
||||
#include "menu/inventory.hpp"
|
||||
#include "text.hpp"
|
||||
#include "menu/toasts.hpp"
|
||||
|
||||
#include "../../util/imgui/window_slide.hpp"
|
||||
|
||||
@@ -18,11 +19,12 @@ namespace game::state::play
|
||||
class Menu
|
||||
{
|
||||
public:
|
||||
Arcade arcade;
|
||||
Interact interact;
|
||||
menu::Arcade arcade;
|
||||
menu::Interact interact;
|
||||
Cheats cheats;
|
||||
Debug debug;
|
||||
Inventory inventory;
|
||||
menu::Inventory inventory;
|
||||
menu::Toasts toasts;
|
||||
|
||||
state::SettingsMenu settingsMenu;
|
||||
|
||||
|
||||
244
src/state/play/menu/arcade.cpp
Normal file
244
src/state/play/menu/arcade.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
#include "arcade.hpp"
|
||||
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
|
||||
using namespace game::util::imgui;
|
||||
using namespace game::resource::xml;
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct GameInfoStrings
|
||||
{
|
||||
Strings::Type name;
|
||||
Strings::Type description;
|
||||
Strings::Type howToPlay;
|
||||
};
|
||||
}
|
||||
|
||||
Arcade::Arcade(entity::Character& character) : skillCheck(character) {}
|
||||
|
||||
void Arcade::game_reset(entity::Character& character, Game gameCurrent)
|
||||
{
|
||||
switch (gameCurrent)
|
||||
{
|
||||
case SKILL_CHECK:
|
||||
skillCheck.reset(character);
|
||||
break;
|
||||
case DUNGEON:
|
||||
dungeon.reset(character);
|
||||
break;
|
||||
case ORBIT:
|
||||
orbit.reset(character);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Arcade::tick()
|
||||
{
|
||||
skillCheck.tick();
|
||||
dungeon.tick();
|
||||
orbit.tick();
|
||||
}
|
||||
|
||||
void Arcade::update(Resources& resources, entity::Character& character, entity::Cursor& cursor, Inventory& inventory,
|
||||
Text& text, Toasts& toasts)
|
||||
{
|
||||
auto available = ImGui::GetContentRegionAvail();
|
||||
auto& strings = character.data.strings;
|
||||
auto game_info_strings_get = [&](Game gameCurrent) -> GameInfoStrings
|
||||
{
|
||||
switch (gameCurrent)
|
||||
{
|
||||
case SKILL_CHECK:
|
||||
return {Strings::ArcadeSkillCheckName, Strings::ArcadeSkillCheckDescription,
|
||||
Strings::ArcadeSkillCheckHowToPlay};
|
||||
case DUNGEON:
|
||||
return {Strings::ArcadeDungeonName, Strings::ArcadeDungeonDescription, Strings::ArcadeDungeonHowToPlay};
|
||||
case ORBIT:
|
||||
return {Strings::ArcadeOrbitName, Strings::ArcadeOrbitDescription, Strings::ArcadeOrbitHowToPlay};
|
||||
}
|
||||
|
||||
return {Strings::ArcadeSkillCheckName, Strings::ArcadeSkillCheckDescription, Strings::ArcadeSkillCheckHowToPlay};
|
||||
};
|
||||
auto game_header_draw = [&](Game gameCurrent)
|
||||
{
|
||||
auto gameInfoStrings = game_info_strings_get(gameCurrent);
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
ImGui::TextUnformatted(strings.get(gameInfoStrings.name).c_str());
|
||||
ImGui::PopFont();
|
||||
};
|
||||
|
||||
auto game_menu_draw = [&](Game gameCurrent)
|
||||
{
|
||||
constexpr auto GAME_CHILD_HEIGHT_MULTIPLIER = 7.0f;
|
||||
constexpr auto GAME_DESCRIPTION_HEIGHT_MULTIPLIER = 4.75f;
|
||||
|
||||
auto lineHeight = ImGui::GetTextLineHeightWithSpacing();
|
||||
auto gameChildHeight = lineHeight * GAME_CHILD_HEIGHT_MULTIPLIER;
|
||||
auto gameDescriptionHeight = lineHeight * GAME_DESCRIPTION_HEIGHT_MULTIPLIER;
|
||||
auto gameInfoStrings = game_info_strings_get(gameCurrent);
|
||||
auto detailsChildID = [gameCurrent]()
|
||||
{
|
||||
switch (gameCurrent)
|
||||
{
|
||||
case SKILL_CHECK:
|
||||
return "##ArcadeSkillCheckDescription";
|
||||
case DUNGEON:
|
||||
return "##ArcadeDungeonDescription";
|
||||
case ORBIT:
|
||||
return "##ArcadeOrbitDescription";
|
||||
}
|
||||
|
||||
return "##ArcadeDescription";
|
||||
}();
|
||||
|
||||
if (ImGui::BeginChild(gameInfoStrings.name, {0, gameChildHeight}, ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto buttonWidth = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
|
||||
ImGui::BeginChild(detailsChildID, {0, gameDescriptionHeight});
|
||||
game_header_draw(gameCurrent);
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", strings.get(gameInfoStrings.description).c_str());
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadePlayButton).c_str(), ImVec2(buttonWidth, 0))))
|
||||
{
|
||||
game_reset(character, gameCurrent);
|
||||
game = gameCurrent;
|
||||
state = GAMEPLAY;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeInfoButton).c_str(), ImVec2(buttonWidth, 0))))
|
||||
{
|
||||
game = gameCurrent;
|
||||
state = INFO;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
};
|
||||
|
||||
auto game_info_sections_draw = [&](Game gameCurrent)
|
||||
{
|
||||
auto gameInfoStrings = game_info_strings_get(gameCurrent);
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1);
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::ArcadeHowToPlay).c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::Separator();
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::NORMAL);
|
||||
ImGui::TextWrapped("%s", strings.get(gameInfoStrings.howToPlay).c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1);
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::ArcadeStats).c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::Separator();
|
||||
};
|
||||
|
||||
auto game_stats_draw = [&](Game gameCurrent)
|
||||
{
|
||||
switch (gameCurrent)
|
||||
{
|
||||
case SKILL_CHECK:
|
||||
{
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
ImGui::Text(strings.get(Strings::ArcadeBestScoreComboFormat).c_str(), skillCheck.highScore,
|
||||
skillCheck.bestCombo);
|
||||
ImGui::Text(strings.get(Strings::ArcadeTotalSkillChecksFormat).c_str(), skillCheck.totalPlays);
|
||||
|
||||
for (int i = 0; i < (int)schema.grades.size(); i++)
|
||||
{
|
||||
auto& grade = schema.grades[i];
|
||||
ImGui::Text("%s: %i", grade.namePlural.c_str(), skillCheck.gradeCounts[i]);
|
||||
}
|
||||
|
||||
ImGui::Text(strings.get(Strings::ArcadeAccuracyFormat).c_str(), skillCheck.accuracy_score_get(character));
|
||||
break;
|
||||
}
|
||||
|
||||
case DUNGEON:
|
||||
break;
|
||||
|
||||
case ORBIT:
|
||||
break;
|
||||
}
|
||||
};
|
||||
auto game_info_draw = [&](Game gameCurrent)
|
||||
{
|
||||
game_header_draw(gameCurrent);
|
||||
ImGui::Separator();
|
||||
game_info_sections_draw(gameCurrent);
|
||||
game_stats_draw(gameCurrent);
|
||||
};
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case GAMEPLAY:
|
||||
switch (game)
|
||||
{
|
||||
case SKILL_CHECK:
|
||||
if (skillCheck.update(resources, character, inventory, text, toasts))
|
||||
{
|
||||
game_reset(character, SKILL_CHECK);
|
||||
state = MENU;
|
||||
}
|
||||
break;
|
||||
|
||||
case DUNGEON:
|
||||
if (dungeon.update(character))
|
||||
{
|
||||
game_reset(character, DUNGEON);
|
||||
state = MENU;
|
||||
}
|
||||
break;
|
||||
|
||||
case ORBIT:
|
||||
if (orbit.update(resources, character, cursor, inventory, text, toasts))
|
||||
{
|
||||
game_reset(character, ORBIT);
|
||||
state = MENU;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
||||
case MENU:
|
||||
case INFO:
|
||||
break;
|
||||
}
|
||||
|
||||
auto buttonHeight = ImGui::GetFrameHeightWithSpacing();
|
||||
auto childSize = ImVec2(available.x, std::max(0.0f, available.y - buttonHeight));
|
||||
|
||||
if (ImGui::BeginChild("##Arcade Child", childSize))
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case MENU:
|
||||
game_menu_draw(ORBIT);
|
||||
//game_menu_draw(DUNGEON);
|
||||
game_menu_draw(SKILL_CHECK);
|
||||
break;
|
||||
|
||||
case INFO:
|
||||
game_info_draw(game);
|
||||
break;
|
||||
|
||||
case GAMEPLAY:
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (state == INFO)
|
||||
{
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeBackButton).c_str()))) state = MENU;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/state/play/menu/arcade.hpp
Normal file
40
src/state/play/menu/arcade.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "arcade/dungeon.hpp"
|
||||
#include "arcade/orbit.hpp"
|
||||
#include "arcade/skill_check.hpp"
|
||||
#include "toasts.hpp"
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class Arcade
|
||||
{
|
||||
public:
|
||||
enum Game
|
||||
{
|
||||
SKILL_CHECK,
|
||||
DUNGEON,
|
||||
ORBIT
|
||||
};
|
||||
|
||||
enum State
|
||||
{
|
||||
MENU,
|
||||
GAMEPLAY,
|
||||
INFO
|
||||
};
|
||||
|
||||
arcade::SkillCheck skillCheck{};
|
||||
arcade::Dungeon dungeon{};
|
||||
arcade::Orbit orbit{};
|
||||
Game game{SKILL_CHECK};
|
||||
State state{MENU};
|
||||
|
||||
Arcade() = default;
|
||||
Arcade(entity::Character&);
|
||||
|
||||
void game_reset(entity::Character&, Game);
|
||||
void tick();
|
||||
void update(Resources&, entity::Character&, entity::Cursor&, Inventory&, Text&, Toasts&);
|
||||
};
|
||||
}
|
||||
310
src/state/play/menu/arcade/dungeon.cpp
Normal file
310
src/state/play/menu/arcade/dungeon.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
#include "dungeon.hpp"
|
||||
|
||||
#include "../../../../resource/font.hpp"
|
||||
#include "../../../../resource/xml/strings.hpp"
|
||||
#include "../../../../util/imgui/widget.hpp"
|
||||
#include "../../../../util/math.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace game::util::imgui;
|
||||
using namespace game::resource::xml;
|
||||
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
int Dungeon::tile_value_get(const Tile& tile) const { return (int)tile.value; }
|
||||
bool Dungeon::tile_value_counts_toward_sum(const Tile& tile) const
|
||||
{
|
||||
return (tile.value >= Tile::VALUE_0 && tile.value <= Tile::VALUE_13) || tile.value == Tile::MINE;
|
||||
}
|
||||
bool Dungeon::tile_is_scroll(const Tile& tile) const { return tile.value == Tile::SCROLL; }
|
||||
const char* Dungeon::tile_flag_text_get(const Tile& tile) const
|
||||
{
|
||||
switch (tile.flagValue)
|
||||
{
|
||||
case Tile::FLAG_NONE:
|
||||
return nullptr;
|
||||
case Tile::FLAG_MINE:
|
||||
return "M";
|
||||
case Tile::FLAG_1:
|
||||
return "1";
|
||||
case Tile::FLAG_2:
|
||||
return "2";
|
||||
case Tile::FLAG_3:
|
||||
return "3";
|
||||
case Tile::FLAG_4:
|
||||
return "4";
|
||||
case Tile::FLAG_5:
|
||||
return "5";
|
||||
case Tile::FLAG_6:
|
||||
return "6";
|
||||
case Tile::FLAG_7:
|
||||
return "7";
|
||||
case Tile::FLAG_8:
|
||||
return "8";
|
||||
case Tile::FLAG_9:
|
||||
return "9";
|
||||
case Tile::FLAG_10:
|
||||
return "10";
|
||||
case Tile::FLAG_11:
|
||||
return "11";
|
||||
case Tile::FLAG_12:
|
||||
return "12";
|
||||
case Tile::FLAG_13:
|
||||
return "13";
|
||||
case Tile::FLAG_QUESTION:
|
||||
return "?";
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Dungeon::surrounding_value_sum_get(int row, int column) const
|
||||
{
|
||||
auto sum = 0;
|
||||
for (int rowOffset = -1; rowOffset <= 1; rowOffset++)
|
||||
for (int columnOffset = -1; columnOffset <= 1; columnOffset++)
|
||||
{
|
||||
if (rowOffset == 0 && columnOffset == 0) continue;
|
||||
|
||||
auto neighborRow = row + rowOffset;
|
||||
auto neighborColumn = column + columnOffset;
|
||||
if (neighborRow < 0 || neighborRow >= GRID_ROWS || neighborColumn < 0 || neighborColumn >= GRID_COLUMNS)
|
||||
continue;
|
||||
|
||||
auto& neighbor = tiles[neighborRow * GRID_COLUMNS + neighborColumn];
|
||||
if (!tile_value_counts_toward_sum(neighbor)) continue;
|
||||
|
||||
sum += tile_value_get(neighbor);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
void Dungeon::reveal_diamond(int row, int column, int radius)
|
||||
{
|
||||
for (int rowOffset = -radius; rowOffset <= radius; rowOffset++)
|
||||
for (int columnOffset = -radius; columnOffset <= radius; columnOffset++)
|
||||
{
|
||||
if (std::abs(rowOffset) + std::abs(columnOffset) > radius) continue;
|
||||
|
||||
auto targetRow = row + rowOffset;
|
||||
auto targetColumn = column + columnOffset;
|
||||
if (targetRow < 0 || targetRow >= GRID_ROWS || targetColumn < 0 || targetColumn >= GRID_COLUMNS) continue;
|
||||
|
||||
auto& tile = tiles[targetRow * GRID_COLUMNS + targetColumn];
|
||||
if (tile.state == Tile::HIDDEN)
|
||||
{
|
||||
tile.state = Tile::SHOWN;
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dungeon::reset(entity::Character&)
|
||||
{
|
||||
tiles.assign(GRID_ROWS * GRID_COLUMNS, Tile{});
|
||||
score = 0;
|
||||
for (auto& tile : tiles)
|
||||
{
|
||||
tile.value = game::util::math::random_percent_roll(5.0f) ? Tile::MINE
|
||||
: (Tile::Value)(int)game::util::math::random_max(14.0f);
|
||||
tile.state = Tile::HIDDEN;
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
}
|
||||
|
||||
if (!tiles.empty()) tiles[(int)game::util::math::random_max((float)tiles.size())].value = Tile::SCROLL;
|
||||
}
|
||||
|
||||
void Dungeon::tick() {}
|
||||
|
||||
bool Dungeon::update(entity::Character& character)
|
||||
{
|
||||
auto& strings = character.data.strings;
|
||||
constexpr float GRID_SPACING = 1.0f;
|
||||
auto& style = ImGui::GetStyle();
|
||||
|
||||
if (tiles.size() != GRID_ROWS * GRID_COLUMNS) reset(character);
|
||||
|
||||
auto contentRegionAvail = ImGui::GetContentRegionAvail();
|
||||
auto childSize =
|
||||
ImVec2(contentRegionAvail.x,
|
||||
std::max(0.0f, contentRegionAvail.y - ImGui::GetFrameHeightWithSpacing() - style.WindowPadding.y));
|
||||
|
||||
if (ImGui::BeginChild("##DungeonGrid", childSize))
|
||||
{
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
auto childAvail = ImGui::GetContentRegionAvail();
|
||||
auto cellWidth = std::max(1.0f, (childAvail.x - GRID_SPACING * (GRID_COLUMNS - 1)) / (float)GRID_COLUMNS);
|
||||
auto cellHeight = std::max(1.0f, (childAvail.y - GRID_SPACING * (GRID_ROWS - 1)) / (float)GRID_ROWS);
|
||||
auto cellSize = std::floor(std::min(cellWidth, cellHeight));
|
||||
auto gridWidth = cellSize * (float)GRID_COLUMNS + GRID_SPACING * (GRID_COLUMNS - 1);
|
||||
auto gridHeight = cellSize * (float)GRID_ROWS + GRID_SPACING * (GRID_ROWS - 1);
|
||||
auto cursor = ImGui::GetCursorPos();
|
||||
auto offsetX = std::max(0.0f, (childAvail.x - gridWidth) * 0.5f);
|
||||
auto offsetY = std::max(0.0f, (childAvail.y - gridHeight) * 0.5f);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(cursor.x + offsetX, cursor.y + offsetY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(GRID_SPACING, GRID_SPACING));
|
||||
|
||||
for (int row = 0; row < GRID_ROWS; row++)
|
||||
{
|
||||
for (int column = 0; column < GRID_COLUMNS; column++)
|
||||
{
|
||||
auto tileID = row * GRID_COLUMNS + column;
|
||||
auto& tile = tiles[tileID];
|
||||
auto tileValue = tile_value_get(tile);
|
||||
|
||||
ImGui::PushID(tileID);
|
||||
if (tile.state != Tile::HIDDEN)
|
||||
{
|
||||
auto buttonColor = style.Colors[ImGuiCol_WindowBg];
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, buttonColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonColor);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColor);
|
||||
}
|
||||
auto isLeftPressed = WIDGET_FX(ImGui::Button("##DungeonCell", ImVec2(cellSize, cellSize)));
|
||||
auto isPopupOpen = ImGui::BeginPopupContextItem("##DungeonFlagMenu");
|
||||
if (isPopupOpen)
|
||||
{
|
||||
if (ImGui::Button("M", ImVec2(36.0f, 0.0f)))
|
||||
{
|
||||
tile.flagValue = Tile::FLAG_MINE;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
for (int flagValue = Tile::FLAG_1; flagValue <= Tile::FLAG_13; flagValue++)
|
||||
{
|
||||
auto flagText = std::format("{}", flagValue);
|
||||
if (ImGui::Button(flagText.c_str(), ImVec2(36.0f, 0.0f)))
|
||||
{
|
||||
tile.flagValue = (Tile::FlagValue)flagValue;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (flagValue % 4 != 0 && flagValue != Tile::FLAG_13) ImGui::SameLine();
|
||||
}
|
||||
if (ImGui::Button("?", ImVec2(36.0f, 0.0f)))
|
||||
{
|
||||
tile.flagValue = Tile::FLAG_QUESTION;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear"))
|
||||
{
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
auto rectMin = ImGui::GetItemRectMin();
|
||||
auto rectMax = ImGui::GetItemRectMax();
|
||||
if (tile.state != Tile::HIDDEN) ImGui::PopStyleColor(3);
|
||||
ImGui::PopID();
|
||||
|
||||
if (isLeftPressed)
|
||||
{
|
||||
switch (tile.state)
|
||||
{
|
||||
case Tile::HIDDEN:
|
||||
tile.state = Tile::SHOWN;
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
if (tile_is_scroll(tile))
|
||||
{
|
||||
reveal_diamond(row, column, 2);
|
||||
tile.value = Tile::VALUE_0;
|
||||
tile.state = Tile::SHOWN;
|
||||
}
|
||||
break;
|
||||
case Tile::SHOWN:
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
if (tile_is_scroll(tile))
|
||||
{
|
||||
reveal_diamond(row, column, 2);
|
||||
tile.value = Tile::VALUE_0;
|
||||
tile.state = Tile::SHOWN;
|
||||
}
|
||||
else if (tileValue > 0)
|
||||
{
|
||||
tile.state = Tile::CORPSE;
|
||||
score += tileValue;
|
||||
}
|
||||
break;
|
||||
case Tile::CORPSE:
|
||||
tile.value = Tile::VALUE_0;
|
||||
tile.state = Tile::SHOWN;
|
||||
tile.flagValue = Tile::FLAG_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string tileText{};
|
||||
auto textColor = IM_COL32(255, 255, 255, 255);
|
||||
if (tile_is_scroll(tile))
|
||||
{
|
||||
tileText = "!";
|
||||
if (tile.state == Tile::CORPSE)
|
||||
textColor = IM_COL32(255, 230, 64, 255);
|
||||
else
|
||||
textColor = IM_COL32(255, 255, 255, 255);
|
||||
}
|
||||
else if (tile.state == Tile::HIDDEN)
|
||||
{
|
||||
if (auto flagText = tile_flag_text_get(tile))
|
||||
{
|
||||
tileText = flagText;
|
||||
textColor = IM_COL32(64, 128, 255, 255);
|
||||
}
|
||||
}
|
||||
else
|
||||
switch (tile.state)
|
||||
{
|
||||
case Tile::HIDDEN:
|
||||
break;
|
||||
case Tile::SHOWN:
|
||||
if (tileValue == 0)
|
||||
{
|
||||
auto surroundingSum = surrounding_value_sum_get(row, column);
|
||||
if (surroundingSum > 0) tileText = std::format("{}", surroundingSum);
|
||||
}
|
||||
else
|
||||
{
|
||||
tileText = std::format("{}", tileValue);
|
||||
textColor = IM_COL32(255, 64, 64, 255);
|
||||
}
|
||||
break;
|
||||
case Tile::CORPSE:
|
||||
if (tileValue == 0)
|
||||
{
|
||||
auto surroundingSum = surrounding_value_sum_get(row, column);
|
||||
if (surroundingSum > 0) tileText = std::format("{}", surroundingSum);
|
||||
}
|
||||
else
|
||||
{
|
||||
tileText = std::format("{}", tileValue);
|
||||
textColor = IM_COL32(255, 230, 64, 255);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!tileText.empty())
|
||||
{
|
||||
auto textSize = ImGui::CalcTextSize(tileText.c_str());
|
||||
auto textPosition = ImVec2(rectMin.x + (rectMax.x - rectMin.x - textSize.x) * 0.5f,
|
||||
rectMin.y + (rectMax.y - rectMin.y - textSize.y) * 0.5f);
|
||||
drawList->AddText(textPosition, textColor, tileText.c_str());
|
||||
}
|
||||
|
||||
if (column + 1 < GRID_COLUMNS) ImGui::SameLine(0.0f, GRID_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Text(strings.get(Strings::ArcadeScoreFormat).c_str(), score);
|
||||
|
||||
return WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeBackButton).c_str()));
|
||||
}
|
||||
}
|
||||
82
src/state/play/menu/arcade/dungeon.hpp
Normal file
82
src/state/play/menu/arcade/dungeon.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../../entity/character.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
class Dungeon
|
||||
{
|
||||
public:
|
||||
struct Tile
|
||||
{
|
||||
enum State
|
||||
{
|
||||
HIDDEN,
|
||||
SHOWN,
|
||||
CORPSE
|
||||
};
|
||||
|
||||
enum FlagValue
|
||||
{
|
||||
FLAG_NONE,
|
||||
FLAG_1,
|
||||
FLAG_2,
|
||||
FLAG_3,
|
||||
FLAG_4,
|
||||
FLAG_5,
|
||||
FLAG_6,
|
||||
FLAG_7,
|
||||
FLAG_8,
|
||||
FLAG_9,
|
||||
FLAG_10,
|
||||
FLAG_11,
|
||||
FLAG_12,
|
||||
FLAG_13,
|
||||
FLAG_MINE,
|
||||
FLAG_QUESTION
|
||||
};
|
||||
|
||||
enum Value
|
||||
{
|
||||
VALUE_0,
|
||||
VALUE_1,
|
||||
VALUE_2,
|
||||
VALUE_3,
|
||||
VALUE_4,
|
||||
VALUE_5,
|
||||
VALUE_6,
|
||||
VALUE_7,
|
||||
VALUE_8,
|
||||
VALUE_9,
|
||||
VALUE_10,
|
||||
VALUE_11,
|
||||
VALUE_12,
|
||||
VALUE_13,
|
||||
MINE = 100,
|
||||
SCROLL = 101
|
||||
};
|
||||
|
||||
Value value{VALUE_0};
|
||||
State state{HIDDEN};
|
||||
FlagValue flagValue{FLAG_NONE};
|
||||
};
|
||||
|
||||
static constexpr int GRID_ROWS = 13;
|
||||
static constexpr int GRID_COLUMNS = 13;
|
||||
|
||||
std::vector<Tile> tiles{};
|
||||
int score{};
|
||||
|
||||
int tile_value_get(const Tile&) const;
|
||||
int surrounding_value_sum_get(int row, int column) const;
|
||||
bool tile_value_counts_toward_sum(const Tile&) const;
|
||||
bool tile_is_scroll(const Tile&) const;
|
||||
const char* tile_flag_text_get(const Tile&) const;
|
||||
void reveal_diamond(int row, int column, int radius);
|
||||
void reset(entity::Character&);
|
||||
void tick();
|
||||
bool update(entity::Character&);
|
||||
};
|
||||
}
|
||||
609
src/state/play/menu/arcade/orbit.cpp
Normal file
609
src/state/play/menu/arcade/orbit.cpp
Normal file
@@ -0,0 +1,609 @@
|
||||
#include "orbit.hpp"
|
||||
|
||||
#include "../../../../util/imgui.hpp"
|
||||
#include "../../../../util/imgui/widget.hpp"
|
||||
#include "../../../../util/math.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace game::util::imgui;
|
||||
using namespace game::resource::xml;
|
||||
using namespace game::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
namespace
|
||||
{
|
||||
enum SpawnSide
|
||||
{
|
||||
TOP,
|
||||
RIGHT,
|
||||
BOTTOM,
|
||||
LEFT
|
||||
};
|
||||
|
||||
bool is_rect_overlapping(const glm::vec4& left, const glm::vec4& right)
|
||||
{
|
||||
return left.x < right.x + right.z && left.x + left.z > right.x && left.y < right.y + right.w &&
|
||||
left.y + left.w > right.y;
|
||||
}
|
||||
|
||||
void target_tick(Orbit::Entity& entity, const glm::vec2& target, float acceleration)
|
||||
{
|
||||
auto delta = target - entity.position;
|
||||
auto distance = glm::length(delta);
|
||||
|
||||
if (distance <= 0.001f)
|
||||
{
|
||||
entity.position = target;
|
||||
entity.velocity *= 0.5f;
|
||||
if (glm::length(entity.velocity) <= 0.001f) entity.velocity = {};
|
||||
return;
|
||||
}
|
||||
|
||||
auto maxSpeed = std::max(acceleration * 8.0f, 1.0f);
|
||||
auto desiredVelocity = glm::normalize(delta) * std::min(distance * 0.35f, maxSpeed);
|
||||
auto steering = desiredVelocity - entity.velocity;
|
||||
auto steeringLength = glm::length(steering);
|
||||
if (steeringLength > acceleration) steering = (steering / steeringLength) * acceleration;
|
||||
|
||||
entity.velocity += steering;
|
||||
|
||||
auto velocityLength = glm::length(entity.velocity);
|
||||
if (velocityLength > maxSpeed) entity.velocity = (entity.velocity / velocityLength) * maxSpeed;
|
||||
|
||||
entity.position += entity.velocity;
|
||||
|
||||
if (glm::distance(entity.position, target) <= maxSpeed)
|
||||
{
|
||||
entity.position = glm::mix(entity.position, target, 0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
void follower_angles_refresh(std::vector<Orbit::Entity>& entities)
|
||||
{
|
||||
std::vector<Orbit::Entity*> followers{};
|
||||
for (auto& entity : entities)
|
||||
if (entity.type == Orbit::Entity::FOLLOWER) followers.emplace_back(&entity);
|
||||
|
||||
if (followers.empty()) return;
|
||||
|
||||
std::sort(followers.begin(), followers.end(),
|
||||
[](const Orbit::Entity* left, const Orbit::Entity* right) { return left->colorID < right->colorID; });
|
||||
|
||||
auto baseAngle = followers.front()->orbitAngle;
|
||||
auto spacing = glm::two_pi<float>() / (float)followers.size();
|
||||
|
||||
for (int i = 0; i < (int)followers.size(); i++)
|
||||
followers[i]->orbitAngle = baseAngle + spacing * (float)i;
|
||||
}
|
||||
|
||||
const glm::vec3* color_value_get(const resource::xml::Orbit& schema, int colorID)
|
||||
{
|
||||
if (colorID < 0 || colorID >= (int)schema.colors.size()) return nullptr;
|
||||
return &schema.colors[colorID].value;
|
||||
}
|
||||
|
||||
int random_available_color_get(const resource::xml::Orbit& schema, int level)
|
||||
{
|
||||
auto availableCount = std::min(level, (int)schema.colors.size());
|
||||
if (availableCount <= 0) return -1;
|
||||
return (int)math::random_max((float)availableCount);
|
||||
}
|
||||
|
||||
int unlocked_level_get(const resource::xml::Orbit& schema, int score)
|
||||
{
|
||||
int unlockedLevel = 0;
|
||||
|
||||
for (auto& color : schema.colors)
|
||||
{
|
||||
if (score >= color.scoreThreshold)
|
||||
unlockedLevel++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return std::max(1, unlockedLevel);
|
||||
}
|
||||
|
||||
void color_override_set(Orbit::Entity& entity, const resource::xml::Orbit& schema, int colorID,
|
||||
const std::string& layerName)
|
||||
{
|
||||
auto color = color_value_get(schema, colorID);
|
||||
if (!color) return;
|
||||
if (!entity.layerMap.contains(layerName)) return;
|
||||
|
||||
entity::Actor::Override override_{entity.layerMap.at(layerName), Anm2::LAYER, entity::Actor::Override::SET};
|
||||
override_.frame.tint.x = color->r;
|
||||
override_.frame.tint.y = color->g;
|
||||
override_.frame.tint.z = color->b;
|
||||
entity.overrides.emplace_back(std::move(override_));
|
||||
}
|
||||
|
||||
void idle_queue(Orbit::Entity& entity)
|
||||
{
|
||||
if (!entity.animationIdle.empty())
|
||||
entity.queue_play({.animation = entity.animationIdle, .isPlayAfterAnimation = true});
|
||||
}
|
||||
|
||||
void spawn_animation_play(Orbit::Entity& entity)
|
||||
{
|
||||
if (!entity.animationSpawn.empty())
|
||||
{
|
||||
entity.play(entity.animationSpawn, entity::Actor::PLAY_FORCE);
|
||||
idle_queue(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Orbit::spawn(entity::Character& character, Orbit::Entity::Type type, int colorID)
|
||||
{
|
||||
auto& schema = character.data.orbitSchema;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case Entity::PLAYER:
|
||||
if (!schema.player.anm2.is_valid()) return;
|
||||
entities.emplace_back(schema.player.anm2, Entity::PLAYER);
|
||||
entities.back().position = centerPosition;
|
||||
entities.back().animationIdle = schema.player.animations.idle;
|
||||
entities.back().animationSpawn = schema.player.animations.spawn;
|
||||
entities.back().animationDeath = schema.player.animations.death;
|
||||
entities.back().hitboxNull = schema.player.hitboxNull;
|
||||
spawn_animation_play(entities.back());
|
||||
return;
|
||||
case Entity::FOLLOWER:
|
||||
{
|
||||
if (!schema.follower.anm2.is_valid()) return;
|
||||
if (colorID < 0 || colorID >= (int)schema.colors.size()) return;
|
||||
|
||||
Entity follower{schema.follower.anm2, Entity::FOLLOWER};
|
||||
follower.colorID = colorID;
|
||||
follower.orbitAngle = 0.0f;
|
||||
follower.position = centerPosition;
|
||||
follower.animationIdle = schema.follower.animations.idle;
|
||||
follower.animationSpawn = schema.follower.animations.spawn;
|
||||
follower.animationDeath = schema.follower.animations.death;
|
||||
follower.hitboxNull = schema.follower.hitboxNull;
|
||||
color_override_set(follower, schema, colorID, schema.follower.overrideTintLayer);
|
||||
spawn_animation_play(follower);
|
||||
|
||||
entities.emplace_back(std::move(follower));
|
||||
follower_angles_refresh(entities);
|
||||
return;
|
||||
}
|
||||
case Entity::ENEMY:
|
||||
{
|
||||
if (!schema.enemy.anm2.is_valid()) return;
|
||||
|
||||
Entity enemy{schema.enemy.anm2, Entity::ENEMY};
|
||||
enemy.colorID = colorID;
|
||||
enemy.animationIdle = schema.enemy.animations.idle;
|
||||
enemy.animationSpawn = schema.enemy.animations.spawn;
|
||||
enemy.animationDeath = schema.enemy.animations.death;
|
||||
enemy.hitboxNull = schema.enemy.hitboxNull;
|
||||
color_override_set(enemy, schema, colorID, schema.enemy.overrideTintLayer);
|
||||
spawn_animation_play(enemy);
|
||||
|
||||
auto rect = enemy.rect();
|
||||
auto width = rect.z;
|
||||
auto height = rect.w;
|
||||
auto side = (SpawnSide)math::random_max(4.0f);
|
||||
switch (side)
|
||||
{
|
||||
case TOP:
|
||||
enemy.position = vec2(math::random_max(canvas.size.x), -height - schema.enemy.spawnPadding);
|
||||
break;
|
||||
case RIGHT:
|
||||
enemy.position = vec2(canvas.size.x + width + schema.enemy.spawnPadding, math::random_max(canvas.size.y));
|
||||
break;
|
||||
case BOTTOM:
|
||||
enemy.position = vec2(math::random_max(canvas.size.x), canvas.size.y + height + schema.enemy.spawnPadding);
|
||||
break;
|
||||
case LEFT:
|
||||
enemy.position = vec2(-width - schema.enemy.spawnPadding, math::random_max(canvas.size.y));
|
||||
break;
|
||||
}
|
||||
|
||||
entities.emplace_back(std::move(enemy));
|
||||
|
||||
if (schema.warning.anm2.is_valid())
|
||||
{
|
||||
Entity warning{schema.warning.anm2, Entity::WARNING};
|
||||
warning.colorID = colorID;
|
||||
color_override_set(warning, schema, colorID, schema.warning.overrideTintLayer);
|
||||
|
||||
auto warningRect = warning.rect();
|
||||
auto warningWidth = warningRect.z;
|
||||
auto warningHeight = warningRect.w;
|
||||
|
||||
switch (side)
|
||||
{
|
||||
case TOP:
|
||||
warning.position = vec2(glm::clamp(entities.back().position.x, warningWidth * 0.5f,
|
||||
(float)canvas.size.x - warningWidth * 0.5f),
|
||||
warningHeight * 0.5f);
|
||||
break;
|
||||
case RIGHT:
|
||||
warning.position = vec2((float)canvas.size.x - warningWidth * 0.5f,
|
||||
glm::clamp(entities.back().position.y, warningHeight * 0.5f,
|
||||
(float)canvas.size.y - warningHeight * 0.5f));
|
||||
break;
|
||||
case BOTTOM:
|
||||
warning.position = vec2(glm::clamp(entities.back().position.x, warningWidth * 0.5f,
|
||||
(float)canvas.size.x - warningWidth * 0.5f),
|
||||
(float)canvas.size.y - warningHeight * 0.5f);
|
||||
break;
|
||||
case LEFT:
|
||||
warning.position = vec2(warningWidth * 0.5f, glm::clamp(entities.back().position.y, warningHeight * 0.5f,
|
||||
(float)canvas.size.y - warningHeight * 0.5f));
|
||||
break;
|
||||
}
|
||||
|
||||
entities.emplace_back(std::move(warning));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Entity::WARNING:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Orbit::reset(entity::Character& character)
|
||||
{
|
||||
entities.clear();
|
||||
sounds = &character.data.orbitSchema.sounds;
|
||||
cursorPosition = {};
|
||||
centerPosition = {};
|
||||
level = 0;
|
||||
score = 0;
|
||||
isHighScoreAchievedThisRun = false;
|
||||
itemEffectManager = {};
|
||||
followerRadius = {};
|
||||
playerTargetAcceleration = {};
|
||||
followerTargetAcceleration = {};
|
||||
playerTimeAfterHurt = {};
|
||||
enemySpeed = {};
|
||||
enemySpeedScoreBonus = {};
|
||||
enemySpeedGainBase = {};
|
||||
enemySpeedGainScoreBonus = {};
|
||||
rotationSpeed = {};
|
||||
rotationSpeedMax = {};
|
||||
rotationSpeedFriction = {};
|
||||
startTimer = character.data.orbitSchema.startTime;
|
||||
hurtTimer = 0;
|
||||
isPlayerDying = false;
|
||||
isRotateLeft = false;
|
||||
isRotateRight = false;
|
||||
}
|
||||
|
||||
void Orbit::tick()
|
||||
{
|
||||
for (auto& entity : entities)
|
||||
entity.tick();
|
||||
|
||||
itemEffectManager.tick();
|
||||
canvas.tick();
|
||||
}
|
||||
|
||||
bool Orbit::update(Resources& resources, entity::Character& character, entity::Cursor& cursor, Inventory& inventory,
|
||||
Text& text, menu::Toasts& toasts)
|
||||
{
|
||||
auto& strings = character.data.strings;
|
||||
auto& schema = character.data.orbitSchema;
|
||||
sounds = &schema.sounds;
|
||||
auto& style = ImGui::GetStyle();
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
auto& textureShader = resources.shaders[resource::shader::TEXTURE];
|
||||
auto& rectShader = resources.shaders[resource::shader::RECT];
|
||||
ImGui::Text(strings.get(Strings::ArcadeScoreFormat).c_str(), score);
|
||||
auto bestText = std::vformat(strings.get(Strings::ArcadeBestScoreFormat), std::make_format_args(highScore));
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(bestText.c_str()).x,
|
||||
cursorPos.y - ImGui::GetTextLineHeightWithSpacing()));
|
||||
ImGui::Text(strings.get(Strings::ArcadeBestScoreFormat).c_str(), highScore);
|
||||
auto padding = ImGui::GetTextLineHeightWithSpacing();
|
||||
auto contentRegionAvail = ImGui::GetContentRegionAvail();
|
||||
auto contentRegionPosition = ImGui::GetCursorScreenPos();
|
||||
auto contentBounds = ImVec4(contentRegionPosition.x, contentRegionPosition.y, contentRegionAvail.x, contentRegionAvail.y);
|
||||
auto available =
|
||||
imgui::to_vec2(contentRegionAvail) - vec2(0.0f, ImGui::GetFrameHeightWithSpacing() + style.WindowPadding.y);
|
||||
auto canvasSize = glm::max(vec2(1.0f), available - vec2(padding * 2.0f));
|
||||
auto canvasScreenPosition = imgui::to_vec2(ImGui::GetCursorScreenPos()) + vec2(padding);
|
||||
centerPosition = canvasSize * 0.5f;
|
||||
|
||||
if (isPlayerDying)
|
||||
{
|
||||
Entity* playerEntity = nullptr;
|
||||
for (auto& entity : entities)
|
||||
if (entity.type == Entity::PLAYER)
|
||||
{
|
||||
playerEntity = &entity;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!playerEntity || playerEntity->state == entity::Actor::STOPPED)
|
||||
{
|
||||
reset(character);
|
||||
}
|
||||
}
|
||||
|
||||
if (entities.empty() && startTimer <= 0) startTimer = schema.startTime;
|
||||
|
||||
if (entities.empty()) spawn(character, Entity::PLAYER);
|
||||
|
||||
followerRadius = schema.player.followerRadius;
|
||||
playerTargetAcceleration = schema.player.targetAcceleration;
|
||||
followerTargetAcceleration = schema.follower.targetAcceleration;
|
||||
playerTimeAfterHurt = schema.player.timeAfterHurt;
|
||||
enemySpeed = schema.enemy.speed;
|
||||
enemySpeedScoreBonus = schema.enemy.speedScoreBonus;
|
||||
enemySpeedGainBase = schema.enemy.speedGainBase;
|
||||
enemySpeedGainScoreBonus = schema.enemy.speedGainScoreBonus;
|
||||
rotationSpeed = schema.player.rotationSpeed;
|
||||
rotationSpeedMax = schema.player.rotationSpeedMax;
|
||||
rotationSpeedFriction = schema.player.rotationSpeedFriction;
|
||||
auto nextLevel = std::min(unlocked_level_get(schema, score), (int)schema.colors.size());
|
||||
if (nextLevel > level)
|
||||
{
|
||||
schema.sounds.levelUp.play();
|
||||
|
||||
auto colorIndex = nextLevel - 1;
|
||||
if (colorIndex >= 0 && colorIndex < (int)schema.colors.size())
|
||||
{
|
||||
auto& pool = schema.colors[colorIndex].pool;
|
||||
if (pool.is_valid() && text.is_interruptible()) text.set(character.data.dialogue.get(pool), character);
|
||||
}
|
||||
}
|
||||
level = nextLevel;
|
||||
|
||||
auto player_get = [&]() -> Entity*
|
||||
{
|
||||
for (auto& entity : entities)
|
||||
if (entity.type == Entity::PLAYER) return &entity;
|
||||
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto player = player_get();
|
||||
|
||||
if (player)
|
||||
{
|
||||
auto desiredFollowerCount = std::min(level, (int)schema.colors.size());
|
||||
auto currentFollowerCount = (int)std::count_if(entities.begin(), entities.end(), [](const Entity& entity)
|
||||
{ return entity.type == Entity::FOLLOWER; });
|
||||
auto currentEnemyCount = (int)std::count_if(entities.begin(), entities.end(),
|
||||
[](const Entity& entity) { return entity.type == Entity::ENEMY; });
|
||||
|
||||
if (currentFollowerCount != desiredFollowerCount)
|
||||
{
|
||||
entities.erase(std::remove_if(entities.begin(), entities.end(),
|
||||
[](const Entity& entity) { return entity.type == Entity::FOLLOWER; }),
|
||||
entities.end());
|
||||
|
||||
for (int i = 0; i < desiredFollowerCount; i++)
|
||||
spawn(character, Entity::FOLLOWER, i);
|
||||
|
||||
player = player_get();
|
||||
}
|
||||
|
||||
if (startTimer <= 0 && hurtTimer <= 0 && !isPlayerDying && !schema.colors.empty())
|
||||
{
|
||||
auto colorID = random_available_color_get(schema, level);
|
||||
if (colorID == -1) return false;
|
||||
|
||||
if (currentEnemyCount == 0)
|
||||
{
|
||||
spawn(character, Entity::ENEMY, colorID);
|
||||
player = player_get();
|
||||
}
|
||||
|
||||
auto spawnChance = schema.enemy.spawnChanceBase + schema.enemy.spawnChanceScoreBonus * (float)score;
|
||||
if (math::random_percent_roll(spawnChance))
|
||||
{
|
||||
colorID = random_available_color_get(schema, level);
|
||||
if (colorID == -1) return false;
|
||||
spawn(character, Entity::ENEMY, colorID);
|
||||
player = player_get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto mousePosition = imgui::to_vec2(ImGui::GetMousePos());
|
||||
auto canvasBoundsMax = canvasScreenPosition + canvasSize;
|
||||
auto isHoveringCanvas = mousePosition.x >= canvasScreenPosition.x && mousePosition.x <= canvasBoundsMax.x &&
|
||||
mousePosition.y >= canvasScreenPosition.y && mousePosition.y <= canvasBoundsMax.y;
|
||||
|
||||
cursor.isVisible = !isHoveringCanvas;
|
||||
isRotateLeft = startTimer <= 0 && hurtTimer <= 0 && !isPlayerDying && isHoveringCanvas &&
|
||||
ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
isRotateRight = startTimer <= 0 && hurtTimer <= 0 && !isPlayerDying && isHoveringCanvas &&
|
||||
ImGui::IsMouseDown(ImGuiMouseButton_Right);
|
||||
cursorPosition = glm::clamp(mousePosition - canvasScreenPosition, vec2(0.0f), canvasSize);
|
||||
|
||||
if (player)
|
||||
{
|
||||
if (isPlayerDying || hurtTimer > 0) player->velocity *= 0.85f;
|
||||
|
||||
if (!isPlayerDying && hurtTimer <= 0 && isRotateLeft) player->rotationVelocity -= rotationSpeed;
|
||||
if (!isPlayerDying && hurtTimer <= 0 && isRotateRight) player->rotationVelocity += rotationSpeed;
|
||||
player->rotationVelocity = glm::clamp(player->rotationVelocity, -rotationSpeedMax, rotationSpeedMax);
|
||||
player->rotationVelocity *= rotationSpeedFriction;
|
||||
|
||||
if (!isPlayerDying && hurtTimer <= 0)
|
||||
target_tick(*player, startTimer > 0 ? centerPosition : cursorPosition, playerTargetAcceleration);
|
||||
}
|
||||
|
||||
for (auto& entity : entities)
|
||||
{
|
||||
switch (entity.type)
|
||||
{
|
||||
case Entity::FOLLOWER:
|
||||
if (player)
|
||||
{
|
||||
entity.orbitAngle += player->rotationVelocity;
|
||||
|
||||
auto radius = std::max(0.0f, followerRadius);
|
||||
auto target = player->position + vec2(std::cos(entity.orbitAngle), std::sin(entity.orbitAngle)) * radius;
|
||||
target_tick(entity, target, followerTargetAcceleration);
|
||||
}
|
||||
break;
|
||||
case Entity::ENEMY:
|
||||
if (player && !entity.isMarkedForRemoval)
|
||||
{
|
||||
auto delta = player->position - entity.position;
|
||||
auto distance = glm::length(delta);
|
||||
auto speed = (enemySpeed + enemySpeedScoreBonus * (float)score) +
|
||||
(enemySpeedGainBase + enemySpeedGainScoreBonus * (float)score);
|
||||
if (distance > 0.001f) entity.position += glm::normalize(delta) * speed;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& follower : entities)
|
||||
{
|
||||
if (isPlayerDying) break;
|
||||
if (follower.type != Entity::FOLLOWER) continue;
|
||||
if (follower.hitboxNull.empty() || !follower.nullMap.contains(follower.hitboxNull)) continue;
|
||||
|
||||
auto followerRect = follower.null_frame_rect(follower.nullMap.at(follower.hitboxNull));
|
||||
if (std::isnan(followerRect.x)) continue;
|
||||
|
||||
for (auto& enemy : entities)
|
||||
{
|
||||
if (enemy.type != Entity::ENEMY || enemy.isMarkedForRemoval) continue;
|
||||
if (enemy.colorID != follower.colorID) continue;
|
||||
if (enemy.hitboxNull.empty() || !enemy.nullMap.contains(enemy.hitboxNull)) continue;
|
||||
|
||||
auto enemyRect = enemy.null_frame_rect(enemy.nullMap.at(enemy.hitboxNull));
|
||||
if (std::isnan(enemyRect.x)) continue;
|
||||
if (!is_rect_overlapping(followerRect, enemyRect)) continue;
|
||||
|
||||
enemy.isMarkedForRemoval = true;
|
||||
if (!enemy.animationDeath.empty())
|
||||
enemy.play(enemy.animationDeath, entity::Actor::PLAY_FORCE);
|
||||
else
|
||||
enemy.state = entity::Actor::STOPPED;
|
||||
|
||||
spawn_animation_play(follower);
|
||||
score++;
|
||||
auto rewardChance = schema.rewardChanceBase + (schema.rewardChanceScoreBonus * score);
|
||||
auto rewardRollCount = schema.rewardRollChanceBase + (schema.rewardRollScoreBonus * score);
|
||||
itemRewards.reward_random_items_try(inventory, itemEffectManager, character.data.itemSchema, contentBounds,
|
||||
rewardChance, rewardRollCount, menu::ItemEffectManager::SHOOT_UP);
|
||||
if (score > highScore)
|
||||
{
|
||||
auto previousHighScore = highScore;
|
||||
highScore = score;
|
||||
|
||||
if (!isHighScoreAchievedThisRun)
|
||||
{
|
||||
isHighScoreAchievedThisRun = true;
|
||||
schema.sounds.highScore.play();
|
||||
|
||||
if (previousHighScore > 0)
|
||||
{
|
||||
auto toastText = strings.get(Strings::ArcadeHighScoreToast);
|
||||
auto toastPosition = imgui::to_imvec2(
|
||||
canvasScreenPosition + player->position -
|
||||
vec2(ImGui::CalcTextSize(toastText.c_str()).x * 0.5f, ImGui::GetTextLineHeightWithSpacing() * 2.0f));
|
||||
toasts.spawn(toastText, toastPosition, 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (player && !isPlayerDying && hurtTimer <= 0 && !player->hitboxNull.empty() &&
|
||||
player->nullMap.contains(player->hitboxNull))
|
||||
{
|
||||
auto playerRect = player->null_frame_rect(player->nullMap.at(player->hitboxNull));
|
||||
|
||||
if (!std::isnan(playerRect.x))
|
||||
{
|
||||
auto isHit = false;
|
||||
|
||||
for (auto& enemy : entities)
|
||||
{
|
||||
if (enemy.type != Entity::ENEMY || enemy.isMarkedForRemoval) continue;
|
||||
if (enemy.hitboxNull.empty() || !enemy.nullMap.contains(enemy.hitboxNull)) continue;
|
||||
|
||||
auto enemyRect = enemy.null_frame_rect(enemy.nullMap.at(enemy.hitboxNull));
|
||||
if (std::isnan(enemyRect.x)) continue;
|
||||
if (!is_rect_overlapping(playerRect, enemyRect)) continue;
|
||||
|
||||
isHit = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isHit)
|
||||
{
|
||||
if (sounds) sounds->hurt.play();
|
||||
if (isHighScoreAchievedThisRun) schema.sounds.highScoreLoss.play();
|
||||
if (schema.poolDeath.is_valid() && text.is_interruptible())
|
||||
text.set(character.data.dialogue.get(schema.poolDeath), character);
|
||||
hurtTimer = playerTimeAfterHurt;
|
||||
isPlayerDying = true;
|
||||
player->velocity = {};
|
||||
player->rotationVelocity = 0.0f;
|
||||
if (!player->animationDeath.empty())
|
||||
player->play(player->animationDeath, entity::Actor::PLAY_FORCE);
|
||||
else
|
||||
player->state = entity::Actor::STOPPED;
|
||||
|
||||
entities.erase(std::remove_if(entities.begin(), entities.end(),
|
||||
[](const Entity& entity)
|
||||
{
|
||||
return entity.type == Entity::ENEMY || entity.type == Entity::WARNING ||
|
||||
entity.type == Entity::FOLLOWER;
|
||||
}),
|
||||
entities.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entities.erase(std::remove_if(entities.begin(), entities.end(),
|
||||
[](const Entity& entity)
|
||||
{
|
||||
return (entity.type == Entity::WARNING && entity.state == entity::Actor::STOPPED) ||
|
||||
(entity.type == Entity::ENEMY && entity.isMarkedForRemoval &&
|
||||
entity.state == entity::Actor::STOPPED);
|
||||
}),
|
||||
entities.end());
|
||||
|
||||
if (startTimer > 0) startTimer--;
|
||||
if (hurtTimer > 0) hurtTimer--;
|
||||
|
||||
canvas.bind();
|
||||
canvas.size_set(ivec2(canvasSize));
|
||||
canvas.clear(color::BLACK);
|
||||
|
||||
for (auto& entity : entities)
|
||||
if (entity.type == Entity::PLAYER || entity.type == Entity::FOLLOWER || entity.type == Entity::ENEMY ||
|
||||
entity.type == Entity::WARNING)
|
||||
entity.render(textureShader, rectShader, canvas);
|
||||
|
||||
canvas.unbind();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, padding));
|
||||
ImGui::SetCursorScreenPos(imgui::to_imvec2(canvasScreenPosition));
|
||||
ImGui::Image(canvas.texture, imgui::to_imvec2(canvasSize));
|
||||
itemEffectManager.render(resources, character.data.itemSchema, contentBounds, ImGui::GetIO().DeltaTime);
|
||||
ImGui::Dummy(ImVec2(0, padding));
|
||||
toasts.update(drawList);
|
||||
|
||||
auto isMenuPressed = WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeMenuBackButton).c_str()));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetItemTooltip("%s", strings.get(Strings::ArcadeMenuBackButtonTooltip).c_str());
|
||||
return isMenuPressed;
|
||||
}
|
||||
}
|
||||
80
src/state/play/menu/arcade/orbit.hpp
Normal file
80
src/state/play/menu/arcade/orbit.hpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../../entity/actor.hpp"
|
||||
#include "../../../../entity/cursor.hpp"
|
||||
#include "../../../../resources.hpp"
|
||||
|
||||
#include "../../../../util/color.hpp"
|
||||
#include "../inventory.hpp"
|
||||
#include "../item_effect_manager.hpp"
|
||||
#include "../../text.hpp"
|
||||
#include "../toasts.hpp"
|
||||
#include "../../item/reward.hpp"
|
||||
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
class Orbit
|
||||
{
|
||||
public:
|
||||
class Entity : public entity::Actor
|
||||
{
|
||||
public:
|
||||
enum Type
|
||||
{
|
||||
PLAYER,
|
||||
FOLLOWER,
|
||||
ENEMY,
|
||||
WARNING
|
||||
};
|
||||
|
||||
Type type{PLAYER};
|
||||
glm::vec2 velocity{};
|
||||
float rotationVelocity{};
|
||||
std::string animationIdle{};
|
||||
std::string animationSpawn{};
|
||||
std::string animationDeath{};
|
||||
std::string hitboxNull{"Hitbox"};
|
||||
bool isMarkedForRemoval{};
|
||||
int health{3};
|
||||
int colorID{};
|
||||
float orbitAngle{};
|
||||
|
||||
Entity() = default;
|
||||
Entity(resource::xml::Anm2 anm2, Type type = PLAYER) : entity::Actor(std::move(anm2)), type(type) {}
|
||||
};
|
||||
|
||||
std::vector<Entity> entities{};
|
||||
resource::xml::Orbit::Sounds* sounds{};
|
||||
Canvas canvas{{1, 1}};
|
||||
glm::vec2 cursorPosition{};
|
||||
glm::vec2 centerPosition{};
|
||||
int level{1};
|
||||
int score{};
|
||||
int highScore{};
|
||||
bool isHighScoreAchievedThisRun{};
|
||||
menu::ItemEffectManager itemEffectManager{};
|
||||
game::state::play::item::Reward itemRewards{};
|
||||
float followerRadius{};
|
||||
float playerTargetAcceleration{};
|
||||
float followerTargetAcceleration{};
|
||||
int playerTimeAfterHurt{};
|
||||
float enemySpeed{};
|
||||
float enemySpeedScoreBonus{};
|
||||
float enemySpeedGainBase{};
|
||||
float enemySpeedGainScoreBonus{};
|
||||
float rotationSpeed{};
|
||||
float rotationSpeedMax{};
|
||||
float rotationSpeedFriction{};
|
||||
int startTimer{};
|
||||
int hurtTimer{};
|
||||
bool isPlayerDying{};
|
||||
bool isRotateLeft{};
|
||||
bool isRotateRight{};
|
||||
|
||||
Orbit() = default;
|
||||
void reset(entity::Character&);
|
||||
void tick();
|
||||
void spawn(entity::Character&, Entity::Type, int colorID = -1);
|
||||
bool update(Resources&, entity::Character&, entity::Cursor&, Inventory& inventory, Text& text, menu::Toasts&);
|
||||
};
|
||||
}
|
||||
339
src/state/play/menu/arcade/skill_check.cpp
Normal file
339
src/state/play/menu/arcade/skill_check.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
#include "skill_check.hpp"
|
||||
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include "../../../../util/imgui.hpp"
|
||||
#include "../../../../util/imgui/widget.hpp"
|
||||
#include "../../../../util/math.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
|
||||
using namespace game::util;
|
||||
using namespace game::entity;
|
||||
using namespace game::resource;
|
||||
using namespace game::resource::xml;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
float SkillCheck::accuracy_score_get(entity::Character& character)
|
||||
{
|
||||
if (totalPlays == 0) return 0.0f;
|
||||
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
SkillCheck::Challenge SkillCheck::challenge_generate(entity::Character& character)
|
||||
{
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
|
||||
Challenge newChallenge;
|
||||
|
||||
Zone newZone{};
|
||||
|
||||
auto zoneSize = std::max(schema.zoneMin, schema.zoneBase - (schema.zoneScoreBonus * score));
|
||||
newZone.min = math::random_max(1.0f - zoneSize);
|
||||
newZone.max = newZone.min + zoneSize;
|
||||
|
||||
newChallenge.zone = newZone;
|
||||
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;
|
||||
}
|
||||
|
||||
SkillCheck::SkillCheck(entity::Character& character) { challenge = challenge_generate(character); }
|
||||
|
||||
void SkillCheck::reset(entity::Character& character)
|
||||
{
|
||||
challenge = challenge_generate(character);
|
||||
queuedChallenge = {};
|
||||
tryValue = challenge.tryValue;
|
||||
score = 0;
|
||||
combo = 0;
|
||||
endTimer = 0;
|
||||
endTimerMax = 0;
|
||||
highScoreStart = 0;
|
||||
isActive = true;
|
||||
isRewardScoreAchieved = false;
|
||||
isHighScoreAchieved = highScore > 0;
|
||||
isHighScoreAchievedThisRun = false;
|
||||
isGameOver = false;
|
||||
itemEffectManager = {};
|
||||
}
|
||||
|
||||
void SkillCheck::tick() { itemEffectManager.tick(); }
|
||||
|
||||
bool SkillCheck::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text,
|
||||
Toasts& toasts)
|
||||
{
|
||||
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 BAR_SPACING_MULTIPLIER = 1.5f;
|
||||
static constexpr auto LINE_HEIGHT = 5.0f;
|
||||
static constexpr auto LINE_WIDTH_BONUS = 10.0f;
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto& schema = character.data.skillCheckSchema;
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
auto& strings = character.data.strings;
|
||||
auto& style = ImGui::GetStyle();
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
auto position = ImGui::GetCursorScreenPos();
|
||||
auto size = ImGui::GetContentRegionAvail();
|
||||
auto spacing = ImGui::GetTextLineHeightWithSpacing() * BAR_SPACING_MULTIPLIER;
|
||||
auto& io = ImGui::GetIO();
|
||||
auto menuButtonHeight = ImGui::GetFrameHeightWithSpacing();
|
||||
size.y = std::max(0.0f, size.y - menuButtonHeight);
|
||||
auto bounds = ImVec4(position.x, position.y, size.x, size.y);
|
||||
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::Text(strings.get(Strings::ArcadeScoreComboFormat).c_str(), score, combo);
|
||||
auto bestString =
|
||||
std::vformat(strings.get(Strings::ArcadeBestScoreComboFormat), std::make_format_args(highScore, bestCombo));
|
||||
ImGui::SetCursorPos(ImVec2(size.x - ImGui::CalcTextSize(bestString.c_str()).x, cursorPos.y));
|
||||
|
||||
ImGui::Text(strings.get(Strings::ArcadeBestScoreComboFormat).c_str(), highScore, bestCombo);
|
||||
|
||||
if (score == 0 && isActive)
|
||||
{
|
||||
ImGui::SetCursorPos(ImVec2(style.WindowPadding.x, size.y - style.WindowPadding.y));
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::SkillCheckInstructions).c_str());
|
||||
}
|
||||
|
||||
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_zones_get = [&](Zone& zone)
|
||||
{
|
||||
auto& min = zone.min;
|
||||
auto& max = zone.max;
|
||||
std::vector<Zone> zones{};
|
||||
|
||||
auto baseHeight = max - min;
|
||||
auto center = (min + max) * 0.5f;
|
||||
|
||||
int zoneCount{};
|
||||
|
||||
for (auto& grade : schema.grades)
|
||||
{
|
||||
if (grade.isFailure) continue;
|
||||
|
||||
auto scale = powf(0.5f, (float)zoneCount);
|
||||
auto halfHeight = baseHeight * scale * 0.5f;
|
||||
|
||||
zoneCount++;
|
||||
|
||||
zones.push_back({center - halfHeight, center + halfHeight});
|
||||
}
|
||||
|
||||
return zones;
|
||||
};
|
||||
|
||||
auto zone_draw = [&](Zone& zone, float alpha = 1.0f)
|
||||
{
|
||||
auto subZones = sub_zones_get(zone);
|
||||
|
||||
for (int i = 0; i < (int)subZones.size(); i++)
|
||||
{
|
||||
auto& subZone = subZones[i];
|
||||
int layer = (int)subZones.size() - 1 - i;
|
||||
|
||||
ImVec2 rectMin = {barMin.x, barMin.y + subZone.min * barHeight};
|
||||
|
||||
ImVec2 rectMax = {barMax.x, barMin.y + subZone.max * barHeight};
|
||||
|
||||
ImVec4 color =
|
||||
i == (int)subZones.size() - 1 ? PERFECT_COLOR : ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered);
|
||||
color.w = (color.w - (float)layer / subZones.size()) * alpha;
|
||||
|
||||
drawList->AddRectFilled(rectMin, rectMax, ImGui::GetColorU32(color));
|
||||
}
|
||||
};
|
||||
|
||||
zone_draw(challenge.zone, 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 lineColor = LINE_COLOR;
|
||||
lineColor.w = isActive ? 1.0f : endTimerProgress;
|
||||
drawList->AddRectFilled(lineMin, lineMax, ImGui::GetColorU32(lineColor));
|
||||
|
||||
if (!isActive && !isGameOver)
|
||||
{
|
||||
zone_draw(queuedChallenge.zone, 1.0f - endTimerProgress);
|
||||
|
||||
auto queuedLineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * queuedChallenge.tryValue));
|
||||
auto queuedLineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, queuedLineMin.y + LINE_HEIGHT);
|
||||
auto queuedLineColor = LINE_COLOR;
|
||||
queuedLineColor.w = 1.0f - endTimerProgress;
|
||||
drawList->AddRectFilled(queuedLineMin, queuedLineMax, ImGui::GetColorU32(queuedLineColor));
|
||||
}
|
||||
|
||||
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(strings.get(Strings::ArcadeScoreLoss).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y);
|
||||
toasts.spawn(strings.get(Strings::ArcadeScoreLoss), toastMessagePosition, 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("##SkillCheckBar", barButtonSize, ImGuiButtonFlags_PressedOnClick)))
|
||||
{
|
||||
int gradeID{};
|
||||
|
||||
auto subZones = sub_zones_get(challenge.zone);
|
||||
|
||||
for (int i = 0; i < (int)subZones.size(); i++)
|
||||
{
|
||||
auto& subZone = subZones[i];
|
||||
|
||||
if (tryValue >= subZone.min && tryValue <= subZone.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.skillCheckRewardItemPool)
|
||||
itemRewards.item_give(itemID, inventory, itemEffectManager, itemSchema, bounds);
|
||||
|
||||
auto toastMessagePosition =
|
||||
ImVec2(barMin.x - ImGui::CalcTextSize(strings.get(Strings::ArcadeRewardToast).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y));
|
||||
toasts.spawn(strings.get(Strings::ArcadeRewardToast), toastMessagePosition, schema.endTimerMax);
|
||||
}
|
||||
|
||||
if (score > highScore)
|
||||
{
|
||||
highScore = score;
|
||||
|
||||
if (isHighScoreAchieved && !isHighScoreAchievedThisRun)
|
||||
{
|
||||
isHighScoreAchievedThisRun = true;
|
||||
schema.sounds.highScore.play();
|
||||
auto toastMessagePosition =
|
||||
ImVec2(barMin.x - ImGui::CalcTextSize(strings.get(Strings::ArcadeHighScoreToast).c_str()).x -
|
||||
ImGui::GetTextLineHeightWithSpacing(),
|
||||
lineMin.y + ImGui::GetTextLineHeightWithSpacing());
|
||||
toasts.spawn(strings.get(Strings::ArcadeHighScoreToast), toastMessagePosition, schema.endTimerMax);
|
||||
}
|
||||
}
|
||||
|
||||
if (combo > bestCombo) bestCombo = combo;
|
||||
|
||||
auto rewardChance = schema.rewardChanceBase + (schema.rewardChanceScoreBonus * score);
|
||||
auto rewardRollCount = schema.rewardRollChanceBase + (schema.rewardRollScoreBonus * score) +
|
||||
(schema.rewardRollGradeBonus * grade.value);
|
||||
itemRewards.reward_random_items_try(inventory, itemEffectManager, itemSchema, bounds, rewardChance,
|
||||
rewardRollCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
score = 0;
|
||||
combo = 0;
|
||||
if (isHighScoreAchievedThisRun) schema.sounds.highScoreLoss.play();
|
||||
if (highScore > 0) isHighScoreAchieved = true;
|
||||
isRewardScoreAchieved = false;
|
||||
isHighScoreAchievedThisRun = false;
|
||||
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::vformat(strings.get(Strings::SkillCheckGradeSuccessTemplate),
|
||||
std::make_format_args(grade.name, grade.value));
|
||||
auto toastMessagePosition =
|
||||
ImVec2(barMin.x - ImGui::CalcTextSize(string.c_str()).x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y);
|
||||
toasts.spawn(string, toastMessagePosition, endTimerMax);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
endTimer--;
|
||||
if (endTimer <= 0)
|
||||
{
|
||||
challenge = queuedChallenge;
|
||||
tryValue = challenge.tryValue;
|
||||
isActive = true;
|
||||
isGameOver = false;
|
||||
}
|
||||
}
|
||||
|
||||
toasts.update(drawList);
|
||||
|
||||
itemEffectManager.render(resources, itemSchema, bounds, io.DeltaTime);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(position.x, position.y + size.y + ImGui::GetStyle().ItemSpacing.y));
|
||||
auto isMenuPressed = WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeMenuBackButton).c_str()));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetItemTooltip("%s", strings.get(Strings::ArcadeMenuBackButtonTooltip).c_str());
|
||||
return isMenuPressed;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../render/canvas.hpp"
|
||||
#include "../../../entity/actor.hpp"
|
||||
#include "../../../entity/character.hpp"
|
||||
#include "../../../resources.hpp"
|
||||
#include "../item_effect_manager.hpp"
|
||||
#include "../../item/reward.hpp"
|
||||
#include "../toasts.hpp"
|
||||
|
||||
#include "../../../../entity/character.hpp"
|
||||
#include "../../../../resources.hpp"
|
||||
|
||||
#include "../inventory.hpp"
|
||||
#include "../text.hpp"
|
||||
#include "../../text.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace game::state::play
|
||||
namespace game::state::play::menu::arcade
|
||||
{
|
||||
class SkillCheck
|
||||
{
|
||||
|
||||
public:
|
||||
struct Range
|
||||
struct Zone
|
||||
{
|
||||
float min{};
|
||||
float max{};
|
||||
@@ -27,27 +28,12 @@ namespace game::state::play
|
||||
|
||||
struct Challenge
|
||||
{
|
||||
Range range{};
|
||||
Zone zone{};
|
||||
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{};
|
||||
@@ -71,17 +57,15 @@ namespace game::state::play
|
||||
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{};
|
||||
game::state::play::menu::ItemEffectManager itemEffectManager{};
|
||||
game::state::play::item::Reward itemRewards{};
|
||||
|
||||
SkillCheck() = default;
|
||||
SkillCheck(entity::Character&);
|
||||
Challenge challenge_generate(entity::Character&);
|
||||
void reset(entity::Character&);
|
||||
void tick();
|
||||
bool update(Resources&, entity::Character&, Inventory&, Text&);
|
||||
bool update(Resources&, entity::Character&, Inventory&, Text&, Toasts&);
|
||||
float accuracy_score_get(entity::Character&);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "interact.hpp"
|
||||
|
||||
#include "../../util/imgui/widget.hpp"
|
||||
#include "../../util/measurement.hpp"
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
#include "../../../util/measurement.hpp"
|
||||
|
||||
using namespace game::resource;
|
||||
using namespace game::resource::xml;
|
||||
using namespace game::util;
|
||||
using namespace game::util::imgui;
|
||||
|
||||
namespace game::state::play
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
void Interact::update(Resources& resources, Text& text, entity::Character& character)
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "text.hpp"
|
||||
#include "../text.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace game::state::play
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class Interact
|
||||
{
|
||||
@@ -1,16 +1,16 @@
|
||||
#include "inventory.hpp"
|
||||
#include "style.hpp"
|
||||
#include "../style.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <tuple>
|
||||
|
||||
#include "../../util/color.hpp"
|
||||
#include "../../util/imgui.hpp"
|
||||
#include "../../util/imgui/style.hpp"
|
||||
#include "../../util/imgui/widget.hpp"
|
||||
#include "../../util/math.hpp"
|
||||
#include "../../../util/color.hpp"
|
||||
#include "../../../util/imgui.hpp"
|
||||
#include "../../../util/imgui/style.hpp"
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
#include "../../../util/math.hpp"
|
||||
|
||||
using namespace game::util;
|
||||
using namespace game::util::imgui;
|
||||
@@ -18,7 +18,7 @@ using namespace game::entity;
|
||||
using namespace game::resource;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
using Strings = resource::xml::Strings;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../entity/character.hpp"
|
||||
#include "../../../entity/character.hpp"
|
||||
|
||||
#include "../../resources.hpp"
|
||||
#include "../../../resources.hpp"
|
||||
|
||||
#include "item_manager.hpp"
|
||||
#include "../item_manager.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace game::state::play
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class Inventory
|
||||
{
|
||||
138
src/state/play/menu/item_effect_manager.cpp
Normal file
138
src/state/play/menu/item_effect_manager.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "item_effect_manager.hpp"
|
||||
|
||||
#include "../../../util/math.hpp"
|
||||
|
||||
using namespace game::util;
|
||||
using namespace game::resource;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
void ItemEffectManager::tick()
|
||||
{
|
||||
for (auto& [i, actor] : actors)
|
||||
actor.tick();
|
||||
}
|
||||
|
||||
void ItemEffectManager::spawn(int itemID, const resource::xml::Item& itemSchema, const ImVec4& bounds, Mode mode)
|
||||
{
|
||||
static constexpr auto ITEM_SHOOT_UP_HORIZONTAL_SPEED_MIN = -250.0f;
|
||||
static constexpr auto ITEM_SHOOT_UP_HORIZONTAL_SPEED_MAX = 250.0f;
|
||||
static constexpr auto ITEM_SHOOT_UP_VERTICAL_SPEED_MIN = 500.0f;
|
||||
static constexpr auto ITEM_SHOOT_UP_VERTICAL_SPEED_MAX = 1000.0f;
|
||||
static constexpr auto ITEM_ROTATION_VELOCITY_MIN = -45.0f;
|
||||
static constexpr auto ITEM_ROTATION_VELOCITY_MAX = 45.0f;
|
||||
|
||||
if (!actors.contains(itemID))
|
||||
{
|
||||
actors[itemID] = entity::Actor(itemSchema.anm2s[itemID], {}, entity::Actor::SET);
|
||||
rects[itemID] = actors[itemID].rect();
|
||||
}
|
||||
|
||||
auto size = ImVec2(bounds.z, bounds.w);
|
||||
auto rect = rects[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 = 0.0f;
|
||||
auto maxX = size.x - previewSize.x;
|
||||
auto spawnX = minX >= maxX ? 0.0f : math::random_in_range(minX, maxX);
|
||||
auto rotationVelocity = math::random_in_range(ITEM_ROTATION_VELOCITY_MIN, ITEM_ROTATION_VELOCITY_MAX);
|
||||
|
||||
Entry entry{};
|
||||
entry.id = itemID;
|
||||
entry.mode = mode;
|
||||
entry.rotationVelocity = rotationVelocity;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case SHOOT_UP:
|
||||
entry.position = ImVec2(spawnX, std::max(0.0f, size.y - previewSize.y));
|
||||
entry.velocity.x =
|
||||
math::random_in_range(ITEM_SHOOT_UP_HORIZONTAL_SPEED_MIN, ITEM_SHOOT_UP_HORIZONTAL_SPEED_MAX);
|
||||
entry.velocity.y = -math::random_in_range(ITEM_SHOOT_UP_VERTICAL_SPEED_MIN, ITEM_SHOOT_UP_VERTICAL_SPEED_MAX);
|
||||
break;
|
||||
case FALL_DOWN:
|
||||
default:
|
||||
entry.position = ImVec2(spawnX, -previewSize.y - math::random_in_range(0.0f, size.y));
|
||||
entry.velocity = {};
|
||||
break;
|
||||
}
|
||||
|
||||
entries.emplace_back(std::move(entry));
|
||||
}
|
||||
|
||||
void ItemEffectManager::render(Resources& resources, const resource::xml::Item& itemSchema, const ImVec4& bounds,
|
||||
float deltaTime)
|
||||
{
|
||||
static constexpr auto ITEM_FALL_GRAVITY = 2400.0f;
|
||||
auto position = ImVec2(bounds.x, bounds.y);
|
||||
auto size = ImVec2(bounds.z, bounds.w);
|
||||
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
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)entries.size(); i++)
|
||||
{
|
||||
auto& item = entries[i];
|
||||
if (!actors.contains(item.id))
|
||||
{
|
||||
entries.erase(entries.begin() + i--);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto rect = rects[item.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 (!canvases.contains(item.id)) canvases.emplace(item.id, Canvas(canvasSize, Canvas::FLIP));
|
||||
auto& canvas = canvases[item.id];
|
||||
canvas.zoom = math::to_percent(previewScale);
|
||||
canvas.pan = vec2(rect.x, rect.y);
|
||||
canvas.bind();
|
||||
canvas.size_set(canvasSize);
|
||||
canvas.clear();
|
||||
|
||||
actors[item.id].overrides.emplace_back(-1, resource::xml::Anm2::ROOT, entity::Actor::Override::SET,
|
||||
resource::xml::Anm2::FrameOptional{.rotation = item.rotation});
|
||||
actors[item.id].render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
|
||||
actors[item.id].overrides.pop_back();
|
||||
canvas.unbind();
|
||||
|
||||
auto min = ImVec2(position.x + item.position.x, position.y + item.position.y);
|
||||
auto max = ImVec2(item.position.x + previewSize.x, item.position.y + previewSize.y);
|
||||
max.x += position.x;
|
||||
max.y += position.y;
|
||||
drawList->AddImage(canvas.texture, min, max);
|
||||
|
||||
item.rotation += item.rotationVelocity * deltaTime;
|
||||
item.position.x += item.velocity.x * deltaTime;
|
||||
item.position.y += item.velocity.y * deltaTime;
|
||||
|
||||
switch (item.mode)
|
||||
{
|
||||
case SHOOT_UP:
|
||||
case FALL_DOWN:
|
||||
default:
|
||||
item.velocity.y += ITEM_FALL_GRAVITY * deltaTime;
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.position.y > size.y || item.position.x < -previewSize.x || item.position.x > size.x + previewSize.x)
|
||||
entries.erase(entries.begin() + i--);
|
||||
}
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
}
|
||||
41
src/state/play/menu/item_effect_manager.hpp
Normal file
41
src/state/play/menu/item_effect_manager.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../render/canvas.hpp"
|
||||
#include "../../../entity/actor.hpp"
|
||||
#include "../../../resources.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class ItemEffectManager
|
||||
{
|
||||
public:
|
||||
enum Mode
|
||||
{
|
||||
FALL_DOWN,
|
||||
SHOOT_UP
|
||||
};
|
||||
|
||||
struct Entry
|
||||
{
|
||||
int id{-1};
|
||||
Mode mode{FALL_DOWN};
|
||||
ImVec2 position{};
|
||||
ImVec2 velocity{};
|
||||
float rotation{};
|
||||
float rotationVelocity{};
|
||||
};
|
||||
|
||||
std::vector<Entry> entries{};
|
||||
std::unordered_map<int, entity::Actor> actors{};
|
||||
std::unordered_map<int, glm::vec4> rects{};
|
||||
std::unordered_map<int, Canvas> canvases{};
|
||||
|
||||
void tick();
|
||||
void spawn(int itemID, const resource::xml::Item& itemSchema, const ImVec4& bounds, Mode mode = FALL_DOWN);
|
||||
void render(Resources& resources, const resource::xml::Item& itemSchema, const ImVec4& bounds, float deltaTime);
|
||||
};
|
||||
}
|
||||
31
src/state/play/menu/toasts.cpp
Normal file
31
src/state/play/menu/toasts.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "toasts.hpp"
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr auto TOAST_MESSAGE_SPEED = 1.0f;
|
||||
}
|
||||
|
||||
void Toasts::spawn(const std::string& message, const ImVec2& position, int time)
|
||||
{
|
||||
toasts.emplace_back(message, position, time, time);
|
||||
}
|
||||
|
||||
void Toasts::update(ImDrawList* drawList)
|
||||
{
|
||||
if (!drawList) return;
|
||||
|
||||
for (int i = 0; i < (int)toasts.size(); i++)
|
||||
{
|
||||
auto& toast = toasts[i];
|
||||
toast.position.y -= TOAST_MESSAGE_SPEED;
|
||||
auto textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||
textColor.w = (float)toast.time / toast.timeMax;
|
||||
drawList->AddText(toast.position, ImGui::GetColorU32(textColor), toast.message.c_str());
|
||||
|
||||
toast.time--;
|
||||
if (toast.time <= 0) toasts.erase(toasts.begin() + i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/state/play/menu/toasts.hpp
Normal file
26
src/state/play/menu/toasts.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class Toasts
|
||||
{
|
||||
public:
|
||||
struct Toast
|
||||
{
|
||||
std::string message{};
|
||||
ImVec2 position{};
|
||||
int time{};
|
||||
int timeMax{};
|
||||
};
|
||||
|
||||
std::vector<Toast> toasts{};
|
||||
|
||||
void spawn(const std::string& message, const ImVec2& position, int time);
|
||||
void update(ImDrawList*);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user