Files
snivy/src/state/main/play.cpp
shweet 17f3348e94
Some checks failed
Build / Build Game (push) Has been cancelled
The Mega Snivy Update
2026-02-28 21:48:00 -05:00

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();
}
}