The Snivy Video Game Is Complete

This commit is contained in:
2025-12-29 05:10:56 -05:00
parent d0f9669b8b
commit 62b988a678
705 changed files with 210576 additions and 162 deletions

352
src/window/play.cpp Normal file
View File

@@ -0,0 +1,352 @@
#include "play.h"
#include "../util/imgui_.h"
#include "../util/math_.h"
#include <format>
using namespace game::util;
using namespace glm;
namespace game::window
{
const std::unordered_map<Play::Grade, audio::Type> GRADE_SOUNDS = {
{Play::MISS, audio::MISS}, {Play::OK, audio::OK},
{Play::GOOD, audio::GOOD}, {Play::GREAT, audio::GREAT},
{Play::EXCELLENT, audio::EXCELLENT}, {Play::PERFECT, audio::PERFECT}};
const std::unordered_map<Item::Rarity, audio::Type> RARITY_SOUNDS = {{Item::COMMON, audio::COMMON},
{Item::UNCOMMON, audio::UNCOMMON},
{Item::RARE, audio::RARE},
{Item::EPIC, audio::EPIC},
{Item::LEGENDARY, audio::LEGENDARY},
{Item::SPECIAL, audio::SPECIAL},
{Item::IMPOSSIBLE, audio::IMPOSSIBLE}};
float Play::accuracy_score_get()
{
if (totalPlays == 0) return 0.0f;
float combinedWeight{};
for (int i = 0; i < Play::GRADE_COUNT; i++)
combinedWeight += (GRADE_WEIGHTS[(Play::Grade)i] * gradeCounts[(Play::Grade)i]);
return glm::clamp(0.0f, math::to_percent(combinedWeight / totalPlays), 100.0f);
}
Play::Challenge Play::challenge_generate(int level)
{
level = std::max(1, level);
Challenge newChallenge;
newChallenge.level = level;
Range newRange{};
auto rangeSize = std::max(RANGE_MIN, RANGE_BASE - (RANGE_SCORE_BONUS * score));
newRange.min = math::random_in_range(0.0, 1.0f - rangeSize);
newRange.max = newRange.min + rangeSize;
newChallenge.range = newRange;
newChallenge.tryValue = 0.0f;
newChallenge.speed = glm::clamp(SPEED_BASE, SPEED_BASE + (SPEED_SCORE_BONUS * score), SPEED_MAX);
if (math::random_bool())
{
newChallenge.tryValue = 1.0f;
newChallenge.speed *= -1;
}
return newChallenge;
}
Play::Play()
{
challenge = challenge_generate(1);
for (int i = 0; i < Play::GRADE_COUNT; i++)
gradeCounts[(Play::Grade)i] = 0;
}
void Play::update(Resources& resources, Character& character, Inventory& inventory, GameData& gameData, Text& text)
{
auto drawList = ImGui::GetWindowDrawList();
auto size = ImGui::GetContentRegionAvail();
auto position = ImGui::GetCursorScreenPos();
auto spacing = ImGui::GetTextLineHeightWithSpacing();
auto level = character.weightStage + 1;
ImGui::Text("Score: %i pts (%ix)", score, combo);
ImGui::Text("Best: %i pts (%ix)", highScore, comboBest);
if (score == 0 && isActive)
ImGui::Text("Match the line to the\ncolored areas with Space/click!\nBetter 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));
drawList->AddRectFilled(barMin, barMax, ImGui::GetColorU32(BG_COLOR));
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;
auto baseMinY = min;
auto baseMaxY = max;
std::vector<Range> ranges{};
auto baseHeight = max - min;
auto center = (min + max) * 0.5f;
for (int i = 0; i < RANGE_AREA_COUNT; ++i)
{
auto scale = powf(0.5f, i);
auto halfHeight = baseHeight * scale * 0.5f;
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 == subRanges.size() - 1 ? PERFECT_COLOR : RECT_COLOR;
color.w = (color.w - (float)layer / subRanges.size()) * alpha;
drawList->AddRectFilled(rectMin, rectMax, ImGui::GetColorU32(color));
}
};
auto endTimerProgress = (float)endTimer / endTimerMax;
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--;
resources.sound_play(audio::SCORE_LOSS);
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("-1").x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y);
toastMessages.emplace_back("-1", toastMessagePosition, END_TIMER_MAX, END_TIMER_MAX);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Space) ||
(ImGui::IsMouseHoveringRect(barMin, barMax) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)))
{
Grade grade{MISS};
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)
grade = (Grade)std::min((int)(grade + 1), (int)PERFECT);
}
gradeCounts[grade]++;
totalPlays++;
if (grade != MISS)
{
if (grade == PERFECT) text.set_random(resources.dialogue.perfectIDs, resources, character);
combo++;
score += (int)grade;
if (score > highScore)
{
highScore = score;
if (highScore >= HIGH_SCORE_BIG && !isHighScoreBigAchieved)
{
resources.sound_play(audio::HIGH_SCORE_BIG);
isHighScoreBigAchieved = true;
inventory.values[Item::POKE_PUFF_SUPREME_HONOR]++;
auto toastItemPosition =
ImVec2(math::random_in_range(barMax.x + ITEM_SIZE.x, barMax.x + (size.x * 0.5f) - ITEM_SIZE.x),
position.y - math::random_in_range(ITEM_SIZE.y, ITEM_SIZE.y * 2.0f));
toastItems.emplace_back(Item::POKE_PUFF_SUPREME_HONOR, toastItemPosition);
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("Fantastic score! Congratulations!").x -
ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y));
toastMessages.emplace_back("Fantastic score! Congratulations!", toastMessagePosition, END_TIMER_MAX,
END_TIMER_MAX);
}
if (highScoreStart > 0)
{
if (!isHighScoreAchieved)
{
resources.sound_play(audio::HIGH_SCORE);
isHighScoreAchieved = true;
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("High Score!").x - ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + ImGui::GetTextLineHeightWithSpacing());
toastMessages.emplace_back("High Score!", toastMessagePosition, END_TIMER_MAX, END_TIMER_MAX);
}
}
}
if (combo > comboBest) comboBest = combo;
auto rewardBonus =
(REWARD_SCORE_BONUS * score) + (REWARD_LEVEL_BONUS * level) + (REWARD_GRADE_BONUS * (int)grade);
while (rewardBonus > 0.0f)
{
const Item::Pool* pool{};
auto rewardType = Item::NONE;
auto gradeChanceBonus = REWARD_GRADE_CHANCE_BONUS * (int)grade;
auto levelChanceBonus = REWARD_LEVEL_CHANCE_BONUS * (int)level;
auto chanceBonus = std::max(1.0f, gradeChanceBonus + levelChanceBonus);
if (math::random_percent_roll(Item::RARITY_CHANCES[Item::IMPOSSIBLE] * chanceBonus))
pool = &Item::pools[Item::IMPOSSIBLE];
else if (math::random_percent_roll(Item::RARITY_CHANCES[Item::LEGENDARY] * chanceBonus))
pool = &Item::pools[Item::LEGENDARY];
else if (math::random_percent_roll(Item::RARITY_CHANCES[Item::EPIC] * chanceBonus))
pool = &Item::pools[Item::EPIC];
else if (math::random_percent_roll(Item::RARITY_CHANCES[Item::RARE] * chanceBonus))
pool = &Item::pools[Item::RARE];
else if (math::random_percent_roll(Item::RARITY_CHANCES[Item::UNCOMMON] * chanceBonus))
pool = &Item::pools[Item::UNCOMMON];
else if (math::random_percent_roll(Item::RARITY_CHANCES[Item::COMMON] * chanceBonus))
pool = &Item::pools[Item::COMMON];
if (pool && !pool->empty())
{
rewardType = (*pool)[(int)math::random_roll((float)pool->size())];
auto& rarity = Item::RARITIES[rewardType];
resources.sound_play(audio::FALL);
resources.sound_play(RARITY_SOUNDS.at(rarity));
inventory.values[rewardType]++;
auto toastItemPosition =
ImVec2(math::random_in_range(barMax.x + ITEM_SIZE.x, barMax.x + (size.x * 0.5f) - ITEM_SIZE.x),
position.y - math::random_in_range(ITEM_SIZE.y, ITEM_SIZE.y * 2.0f));
toastItems.emplace_back(rewardType, toastItemPosition);
}
rewardBonus -= 1.0f;
}
}
else
{
text.set_random(resources.dialogue.missIDs, resources, character);
score = 0;
if (isHighScoreAchieved) resources.sound_play(audio::HIGH_SCORE_LOSS);
isHighScoreAchieved = false;
highScoreStart = highScore;
isGameOver = true;
}
resources.sound_play(GRADE_SOUNDS.at(grade));
endTimerMax = grade == MISS ? END_TIMER_MISS_MAX : END_TIMER_MAX;
isActive = false;
endTimer = endTimerMax;
queuedChallenge = challenge_generate(level);
auto string =
grade == MISS ? GRADE_STRINGS[grade] : std::format("{} (+{})", GRADE_STRINGS[grade], GRADE_VALUES[grade]);
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize(string.c_str()).x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y);
toastMessages.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)toastMessages.size(); i++)
{
auto& toastMessage = toastMessages[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) toastMessages.erase(toastMessages.begin() + i--);
}
for (int i = 0; i < (int)toastItems.size(); i++)
{
auto& texture = resources.anm2s[anm2::ITEMS].content.spritesheets.at(0).texture;
auto& toastItem = toastItems[i];
auto& type = toastItem.type;
auto columns = (int)(texture.size.x / ITEM_SIZE.x);
auto crop = vec2(type % columns, type / columns) * ITEM_SIZE;
auto uvMin = imgui::to_imvec2(crop / vec2(texture.size));
auto uvMax = imgui::to_imvec2((crop + ITEM_SIZE) / vec2(texture.size));
auto min = ImVec2(toastItem.position.x - (ITEM_SIZE.x * 0.5f), toastItem.position.y - (ITEM_SIZE.y * 0.5f));
auto max = ImVec2(toastItem.position.x + (ITEM_SIZE.x * 0.5f), toastItem.position.y + (ITEM_SIZE.y * 0.5f));
drawList->AddImage(texture.id, min, max, uvMin, uvMax);
toastItem.velocityY += Item::GRAVITY;
toastItem.position.y += toastItem.velocityY;
if (toastItem.position.y > position.y + size.y + ITEM_SIZE.y) toastItems.erase(toastItems.begin() + i--);
}
}
}