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