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