refactoring, new game(s) in progress

This commit is contained in:
2026-04-09 12:47:09 -04:00
parent f7b00847ee
commit a529d5cdce
57 changed files with 2743 additions and 759 deletions

View File

@@ -114,7 +114,9 @@ file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
src/resource/xml/*.cpp
src/state/*.cpp
src/state/play/*.cpp
src/state/play/arcade/*.cpp
src/state/play/menu/*.cpp
src/state/play/menu/arcade/*.cpp
src/state/play/item/*.cpp
src/state/select/*.cpp
src/entity/*.cpp
src/window/*.cpp

View File

@@ -143,6 +143,30 @@ namespace game::entity
auto override_handle = [&](Anm2::Frame& overrideFrame)
{
auto vec2_set = [](glm::vec2& destination, const Anm2::FrameOptional::Vec2& source)
{
if (source.x.has_value()) destination.x = *source.x;
if (source.y.has_value()) destination.y = *source.y;
};
auto vec2_add = [](glm::vec2& destination, const Anm2::FrameOptional::Vec2& source)
{
if (source.x.has_value()) destination.x += *source.x;
if (source.y.has_value()) destination.y += *source.y;
};
auto vec3_set = [](glm::vec3& destination, const Anm2::FrameOptional::Vec3& source)
{
if (source.x.has_value()) destination.x = *source.x;
if (source.y.has_value()) destination.y = *source.y;
if (source.z.has_value()) destination.z = *source.z;
};
auto vec4_set = [](glm::vec4& destination, const Anm2::FrameOptional::Vec4& source)
{
if (source.x.has_value()) destination.x = *source.x;
if (source.y.has_value()) destination.y = *source.y;
if (source.z.has_value()) destination.z = *source.z;
if (source.w.has_value()) destination.w = *source.w;
};
for (auto& override : overrides)
{
if (override.type != type) continue;
@@ -153,19 +177,19 @@ namespace game::entity
switch (override.mode)
{
case Override::SET:
if (source.position.has_value()) overrideFrame.position = *source.position;
if (source.pivot.has_value()) overrideFrame.pivot = *source.pivot;
if (source.size.has_value()) overrideFrame.size = *source.size;
if (source.scale.has_value()) overrideFrame.scale = *source.scale;
if (source.crop.has_value()) overrideFrame.crop = *source.crop;
vec2_set(overrideFrame.position, source.position);
vec2_set(overrideFrame.pivot, source.pivot);
vec2_set(overrideFrame.size, source.size);
vec2_set(overrideFrame.scale, source.scale);
vec2_set(overrideFrame.crop, source.crop);
if (source.rotation.has_value()) overrideFrame.rotation = *source.rotation;
if (source.tint.has_value()) overrideFrame.tint = *source.tint;
if (source.colorOffset.has_value()) overrideFrame.colorOffset = *source.colorOffset;
vec4_set(overrideFrame.tint, source.tint);
vec3_set(overrideFrame.colorOffset, source.colorOffset);
if (source.isInterpolated.has_value()) overrideFrame.isInterpolated = *source.isInterpolated;
if (source.isVisible.has_value()) overrideFrame.isVisible = *source.isVisible;
break;
case Override::ADD:
if (source.scale.has_value()) overrideFrame.scale += *source.scale;
vec2_add(overrideFrame.scale, source.scale);
break;
default:
break;
@@ -344,6 +368,8 @@ namespace game::entity
void Actor::render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas& canvas)
{
if (!isVisible) return;
auto animation = animation_get();
if (!animation) return;

View File

@@ -72,6 +72,7 @@ namespace game::entity
glm::vec2 position{};
float time{};
bool isShowNulls{};
bool isVisible{true};
int animationIndex{-1};
int playedEventID{-1};
float startTime{};

View File

@@ -201,7 +201,7 @@ namespace game
logger.info("Initialized Dear ImGui OpenGL backend");
imgui::style::color_set(settings.color);
imgui::style::rounding_set();
imgui::style::widget_set();
math::random_seed_set();
resource::Audio::volume_set((float)settings.volume / 100);
}

View File

@@ -1,6 +1,7 @@
#include "canvas.hpp"
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <utility>
#include "../util/imgui.hpp"
@@ -95,7 +96,10 @@ namespace game
Canvas::Canvas(const Canvas& other) : Canvas(other.size, other.flags)
{
pan = other.pan;
shakeOffset = other.shakeOffset;
zoom = other.zoom;
shakeTimer = other.shakeTimer;
shakeTimerMax = other.shakeTimerMax;
if ((flags & DEFAULT) == 0 && (other.flags & DEFAULT) == 0)
{
@@ -110,7 +114,10 @@ namespace game
{
size = other.size;
pan = other.pan;
shakeOffset = other.shakeOffset;
zoom = other.zoom;
shakeTimer = other.shakeTimer;
shakeTimerMax = other.shakeTimerMax;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
@@ -118,7 +125,10 @@ namespace game
other.size = {};
other.pan = {};
other.shakeOffset = {};
other.zoom = 100.0f;
other.shakeTimer = 0;
other.shakeTimerMax = 0;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
@@ -156,7 +166,10 @@ namespace game
size = other.size;
pan = other.pan;
shakeOffset = other.shakeOffset;
zoom = other.zoom;
shakeTimer = other.shakeTimer;
shakeTimerMax = other.shakeTimerMax;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
@@ -164,7 +177,10 @@ namespace game
other.size = {};
other.pan = {};
other.shakeOffset = {};
other.zoom = 100.0f;
other.shakeTimer = 0;
other.shakeTimerMax = 0;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
@@ -242,9 +258,51 @@ namespace game
glUseProgram(0);
}
void Canvas::texture_render(Shader& shader, const Canvas& source, mat4 model, vec4 tint, vec3 colorOffset) const
{
auto shakenModel = glm::translate(model, glm::vec3(source.shakeOffset, 0.0f));
texture_render(shader, source.texture, shakenModel, tint, colorOffset);
}
void Canvas::render(Shader& shader, mat4& model, vec4 tint, vec3 colorOffset) const
{
texture_render(shader, texture, model, tint, colorOffset);
auto shakenModel = glm::translate(model, glm::vec3(shakeOffset, 0.0f));
texture_render(shader, texture, shakenModel, tint, colorOffset);
}
void Canvas::shake(float magnitude, int timeTicks)
{
shakeTimerMax = std::max(1, timeTicks);
shakeTimer = shakeTimerMax;
shakeOffset = {math::random_in_range(-magnitude, magnitude), math::random_in_range(-magnitude, magnitude)};
}
void Canvas::tick()
{
static constexpr auto SHAKE_LERP_FACTOR = 0.35f;
static constexpr auto SHAKE_DECAY = 0.85f;
static constexpr auto SHAKE_EPSILON = 0.1f;
if (shakeTimer > 0)
{
shakeTimer--;
auto magnitude = glm::length(shakeOffset) * SHAKE_DECAY;
auto timerFactor = (float)shakeTimer / (float)std::max(1, shakeTimerMax);
auto target =
glm::vec2(math::random_in_range(-magnitude, magnitude), math::random_in_range(-magnitude, magnitude)) *
timerFactor;
shakeOffset = glm::mix(shakeOffset, target, SHAKE_LERP_FACTOR);
}
else
{
shakeOffset = glm::mix(shakeOffset, glm::vec2{}, SHAKE_LERP_FACTOR);
if (glm::length(shakeOffset) < SHAKE_EPSILON)
{
shakeOffset = {};
shakeTimerMax = 0;
}
}
}
void Canvas::bind()

View File

@@ -31,6 +31,8 @@ namespace game
public:
static constexpr glm::vec4 CLEAR_COLOR = {0, 0, 0, 0};
static constexpr float SHAKE_MAGNITUDE = 0.05f;
static constexpr float SHAKE_TIME_TICKS = 60;
enum Flag
{
@@ -46,7 +48,10 @@ namespace game
glm::ivec2 size{};
glm::vec2 pan{};
glm::vec2 shakeOffset{};
float zoom{100.0f};
int shakeTimer{};
int shakeTimerMax{};
Flags flags{FLIP};
Canvas() = default;
@@ -64,6 +69,8 @@ namespace game
void texture_render(resource::Shader&, const Canvas&, glm::mat4, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void rect_render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(0, 0, 1, 1)) const;
void render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void shake(float magnitude = SHAKE_MAGNITUDE, int timeTicks = SHAKE_TIME_TICKS);
void tick();
void bind();
void size_set(glm::ivec2 size);
void clear(glm::vec4 color = CLEAR_COLOR);

View File

@@ -153,7 +153,7 @@ namespace game::resource
internal.reset();
}
void Audio::play(bool isLoop)
void Audio::play(bool isLoop) const
{
if (!internal) return;
@@ -186,7 +186,7 @@ namespace game::resource
if (options) SDL_DestroyProperties(options);
}
void Audio::stop()
void Audio::stop() const
{
if (track) MIX_StopTrack(track, 0);
}

View File

@@ -14,7 +14,7 @@ namespace game::resource
void unload();
std::shared_ptr<MIX_Audio> internal{};
MIX_Track* track{nullptr};
mutable MIX_Track* track{nullptr};
public:
Audio() = default;
@@ -26,8 +26,8 @@ namespace game::resource
Audio& operator=(Audio&&) noexcept;
~Audio();
bool is_valid() const;
void play(bool isLoop = false);
void stop();
void play(bool isLoop = false) const;
void stop() const;
bool is_playing() const;
static void volume_set(float volume);
};

View File

@@ -109,14 +109,65 @@ namespace game::resource::xml
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
struct Vec2
{
std::optional<float> x{};
std::optional<float> y{};
Vec2() = default;
Vec2(const glm::vec2& value) : x(value.x), y(value.y) {}
Vec2& operator=(const glm::vec2& value)
{
x = value.x;
y = value.y;
return *this;
}
};
struct Vec3
{
std::optional<float> x{};
std::optional<float> y{};
std::optional<float> z{};
Vec3() = default;
Vec3(const glm::vec3& value) : x(value.x), y(value.y), z(value.z) {}
Vec3& operator=(const glm::vec3& value)
{
x = value.x;
y = value.y;
z = value.z;
return *this;
}
};
struct Vec4
{
std::optional<float> x{};
std::optional<float> y{};
std::optional<float> z{};
std::optional<float> w{};
Vec4() = default;
Vec4(const glm::vec4& value) : x(value.x), y(value.y), z(value.z), w(value.w) {}
Vec4& operator=(const glm::vec4& value)
{
x = value.x;
y = value.y;
z = value.z;
w = value.w;
return *this;
}
};
Vec2 crop{};
Vec2 position{};
Vec2 pivot{};
Vec2 size{};
Vec2 scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
Vec4 tint{};
Vec3 colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};

View File

@@ -14,7 +14,11 @@ namespace game::resource::xml
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize area schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto archive = path.directory_get();

View File

@@ -229,6 +229,16 @@ namespace game::resource::xml
else
logger.warning(std::format("No character skill_check.xml file found: {}", path.string()));
if (auto dungeonSchemaPath = physfs::Path(archive + "/" + "dungeon.xml"); dungeonSchemaPath.is_valid())
dungeonSchema = Dungeon(dungeonSchemaPath);
else
logger.warning(std::format("No character dungeon.xml file found: {}", path.string()));
if (auto orbitSchemaPath = physfs::Path(archive + "/" + "orbit.xml"); orbitSchemaPath.is_valid())
orbitSchema = Orbit(orbitSchemaPath, dialogue);
else
logger.warning(std::format("No character orbit.xml file found: {}", path.string()));
if (auto stringsPath = physfs::Path(archive + "/" + "strings.xml"); stringsPath.is_valid())
strings = Strings(stringsPath);

View File

@@ -9,8 +9,10 @@
#include "area.hpp"
#include "cursor.hpp"
#include "dialogue.hpp"
#include "dungeon.hpp"
#include "item.hpp"
#include "menu.hpp"
#include "orbit.hpp"
#include "save.hpp"
#include "skill_check.hpp"
#include "strings.hpp"
@@ -100,6 +102,8 @@ namespace game::resource::xml
Menu menuSchema{};
Cursor cursorSchema{};
SkillCheck skillCheckSchema{};
Dungeon dungeonSchema{};
Orbit orbitSchema{};
Strings strings{};
Save save{};

View File

@@ -13,7 +13,11 @@ namespace game::resource::xml
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize cursor schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto archive = path.directory_get();

View File

@@ -47,7 +47,11 @@ namespace game::resource::xml
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize dialogue: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{

View File

@@ -0,0 +1,39 @@
#include "dungeon.hpp"
#include "../../log.hpp"
#include "util.hpp"
#include <format>
using namespace tinyxml2;
namespace game::resource::xml
{
Dungeon::Dungeon(const util::physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize dungeon schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
if (std::string_view(root->Name()) != "Dungeon")
{
logger.error(std::format("Dungeon schema root element is not Dungeon: {}", path.c_str()));
return;
}
query_string_attribute(root, "Title", &title);
query_string_attribute(root, "Description", &description);
}
isValid = true;
logger.info(std::format("Initialized dungeon schema: {}", path.c_str()));
}
bool Dungeon::is_valid() const { return isValid; };
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../../util/physfs.hpp"
#include <string>
namespace game::resource::xml
{
class Dungeon
{
public:
std::string title{"Dungeon"};
std::string description{"Template dungeon schema."};
bool isValid{};
Dungeon() = default;
Dungeon(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -21,7 +21,11 @@ namespace game::resource::xml
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize item schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto archive = path.directory_get();

View File

@@ -15,7 +15,11 @@ namespace game::resource::xml
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize menu schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto archive = path.directory_get();

134
src/resource/xml/orbit.cpp Normal file
View File

@@ -0,0 +1,134 @@
#include "orbit.hpp"
#include "../../log.hpp"
#include "util.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Orbit::Orbit(const util::physfs::Path& path, Dialogue& dialogue)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize orbit schema: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
if (std::string_view(root->Name()) != "Orbit")
{
logger.error(std::format("Orbit schema root element is not Orbit: {}", path.c_str()));
return;
}
root->QueryIntAttribute("StartTime", &startTime);
root->QueryFloatAttribute("RewardChanceBase", &rewardChanceBase);
root->QueryFloatAttribute("RewardChanceScoreBonus", &rewardChanceScoreBonus);
root->QueryFloatAttribute("RewardRollChanceBase", &rewardRollChanceBase);
root->QueryFloatAttribute("RewardRollScoreBonus", &rewardRollScoreBonus);
dialogue.query_pool_id(root, "HurtDialoguePoolID", poolHurt.id);
dialogue.query_pool_id(root, "DeathDialoguePoolID", poolDeath.id);
if (auto element = root->FirstChildElement("Player"))
{
query_anm2(element, "Anm2", archive, textureRootPath, player.anm2);
query_string_attribute(element, "HitboxNull", &player.hitboxNull);
element->QueryFloatAttribute("FollowerRadius", &player.followerRadius);
element->QueryFloatAttribute("TargetAcceleration", &player.targetAcceleration);
element->QueryFloatAttribute("RotationSpeed", &player.rotationSpeed);
element->QueryFloatAttribute("RotationSpeedMax", &player.rotationSpeedMax);
element->QueryFloatAttribute("RotationSpeedFriction", &player.rotationSpeedFriction);
element->QueryIntAttribute("TimeAfterHurt", &player.timeAfterHurt);
if (auto animationsElement = element->FirstChildElement("Animations"))
{
query_string_attribute(animationsElement, "Idle", &player.animations.idle);
query_string_attribute(animationsElement, "Spawn", &player.animations.spawn);
query_string_attribute(animationsElement, "Death", &player.animations.death);
}
}
if (auto element = root->FirstChildElement("Follower"))
{
query_anm2(element, "Anm2", archive, textureRootPath, follower.anm2);
query_string_attribute(element, "HitboxNull", &follower.hitboxNull);
element->QueryFloatAttribute("TargetAcceleration", &follower.targetAcceleration);
query_string_attribute(element, "OverrideTintLayer", &follower.overrideTintLayer);
if (auto animationsElement = element->FirstChildElement("Animations"))
{
query_string_attribute(animationsElement, "Idle", &follower.animations.idle);
query_string_attribute(animationsElement, "Spawn", &follower.animations.spawn);
query_string_attribute(animationsElement, "Death", &follower.animations.death);
}
}
if (auto element = root->FirstChildElement("Enemy"))
{
query_anm2(element, "Anm2", archive, textureRootPath, enemy.anm2);
query_string_attribute(element, "HitboxNull", &enemy.hitboxNull);
element->QueryFloatAttribute("Speed", &enemy.speed);
element->QueryFloatAttribute("SpeedScoreBonus", &enemy.speedScoreBonus);
element->QueryFloatAttribute("SpeedGainBase", &enemy.speedGainBase);
element->QueryFloatAttribute("SpeedGainScoreBonus", &enemy.speedGainScoreBonus);
element->QueryFloatAttribute("SpawnChanceBase", &enemy.spawnChanceBase);
element->QueryFloatAttribute("SpawnChanceScoreBonus", &enemy.spawnChanceScoreBonus);
element->QueryFloatAttribute("SpawnPadding", &enemy.spawnPadding);
query_string_attribute(element, "OverrideTintLayer", &enemy.overrideTintLayer);
if (auto animationsElement = element->FirstChildElement("Animations"))
{
query_string_attribute(animationsElement, "Idle", &enemy.animations.idle);
query_string_attribute(animationsElement, "Spawn", &enemy.animations.spawn);
query_string_attribute(animationsElement, "Death", &enemy.animations.death);
}
}
if (auto element = root->FirstChildElement("Warning"))
{
query_anm2(element, "Anm2", archive, textureRootPath, warning.anm2);
query_string_attribute(element, "OverrideTintLayer", &warning.overrideTintLayer);
}
if (auto element = root->FirstChildElement("Colors"))
{
for (auto child = element->FirstChildElement("Color"); child; child = child->NextSiblingElement("Color"))
{
Color color{};
query_vec3(child, "ColorR", "ColorG", "ColorB", color.value);
child->QueryIntAttribute("ScoreThreshold", &color.scoreThreshold);
dialogue.query_pool_id(child, "DialoguePoolID", color.pool.id);
colors.emplace_back(std::move(color));
}
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "LevelUp", archive, soundRootPath, sounds.levelUp);
query_sound_entry_collection(element, "Hurt", archive, soundRootPath, sounds.hurt);
query_sound_entry_collection(element, "HighScore", archive, soundRootPath, sounds.highScore);
query_sound_entry_collection(element, "HighScoreLoss", archive, soundRootPath, sounds.highScoreLoss);
}
}
isValid = true;
logger.info(std::format("Initialized orbit schema: {}", path.c_str()));
}
bool Orbit::is_valid() const { return isValid; };
}

100
src/resource/xml/orbit.hpp Normal file
View File

@@ -0,0 +1,100 @@
#pragma once
#include "dialogue.hpp"
#include "util.hpp"
namespace game::resource::xml
{
class Orbit
{
public:
struct Sounds
{
SoundEntryCollection levelUp{};
SoundEntryCollection hurt{};
SoundEntryCollection highScore{};
SoundEntryCollection highScoreLoss{};
};
struct Animations
{
std::string idle{};
std::string spawn{};
std::string death{};
};
struct Player
{
Anm2 anm2{};
Animations animations{};
std::string hitboxNull{"Hitbox"};
float followerRadius{};
float targetAcceleration{6.0f};
float rotationSpeed{0.01f};
float rotationSpeedMax{0.10f};
float rotationSpeedFriction{0.85f};
int timeAfterHurt{30};
};
struct Follower
{
Anm2 anm2{};
Animations animations{};
std::string hitboxNull{"Hitbox"};
float targetAcceleration{4.0f};
std::string overrideTintLayer{"Default"};
};
struct Enemy
{
Anm2 anm2{};
Animations animations{};
std::string hitboxNull{"Hitbox"};
float speed{4.0f};
float speedScoreBonus{0.001f};
float speedGainBase{0.01f};
float speedGainScoreBonus{0.001f};
float spawnChanceBase{0.35f};
float spawnChanceScoreBonus{0.05f};
float spawnPadding{8.0f};
std::string overrideTintLayer{"Default"};
};
struct Warning
{
Anm2 anm2{};
std::string overrideTintLayer{"Default"};
};
struct Color
{
glm::vec3 value{};
int scoreThreshold{};
Dialogue::PoolReference pool{};
};
Player player{};
Follower follower{};
Enemy enemy{};
Warning warning{};
Sounds sounds{};
Dialogue::PoolReference poolHurt{};
Dialogue::PoolReference poolDeath{};
float rewardChanceBase{0.001f};
float rewardChanceScoreBonus{0.001f};
float rewardRollChanceBase{1.0f};
float rewardRollScoreBonus{};
std::vector<Color> colors{};
int startTime{30};
bool isValid{};
Orbit() = default;
Orbit(const util::physfs::Path&, Dialogue&);
bool is_valid() const;
};
}

View File

@@ -56,9 +56,9 @@ namespace game::resource::xml
if (!element) element = root->FirstChildElement("Play");
if (element)
{
element->QueryIntAttribute("TotalPlays", &totalPlays);
element->QueryIntAttribute("HighScore", &highScore);
element->QueryIntAttribute("BestCombo", &bestCombo);
element->QueryIntAttribute("TotalPlays", &skillCheck.totalPlays);
element->QueryIntAttribute("HighScore", &skillCheck.highScore);
element->QueryIntAttribute("BestCombo", &skillCheck.bestCombo);
if (auto child = element->FirstChildElement("Grades"))
{
@@ -67,11 +67,16 @@ namespace game::resource::xml
{
int id{};
gradeChild->QueryIntAttribute("ID", &id);
gradeChild->QueryIntAttribute("Count", &gradeCounts[id]);
gradeChild->QueryIntAttribute("Count", &skillCheck.gradeCounts[id]);
}
}
}
if (auto element = root->FirstChildElement("Orbit"))
{
element->QueryIntAttribute("HighScore", &orbit.highScore);
}
if (auto element = root->FirstChildElement("Inventory"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
@@ -132,19 +137,22 @@ namespace game::resource::xml
auto skillCheckElement = element->InsertNewChildElement("SkillCheck");
skillCheckElement->SetAttribute("TotalPlays", totalPlays);
skillCheckElement->SetAttribute("HighScore", highScore);
skillCheckElement->SetAttribute("BestCombo", bestCombo);
skillCheckElement->SetAttribute("TotalPlays", skillCheck.totalPlays);
skillCheckElement->SetAttribute("HighScore", skillCheck.highScore);
skillCheckElement->SetAttribute("BestCombo", skillCheck.bestCombo);
auto gradesElement = skillCheckElement->InsertNewChildElement("Grades");
for (auto& [i, count] : gradeCounts)
for (auto& [i, count] : skillCheck.gradeCounts)
{
auto gradeElement = gradesElement->InsertNewChildElement("Grade");
gradeElement->SetAttribute("ID", i);
gradeElement->SetAttribute("Count", count);
}
auto orbitElement = element->InsertNewChildElement("Orbit");
orbitElement->SetAttribute("HighScore", orbit.highScore);
auto inventoryElement = element->InsertNewChildElement("Inventory");
for (auto& [id, quantity] : inventory)

View File

@@ -12,6 +12,19 @@ namespace game::resource::xml
class Save
{
public:
struct SkillCheck
{
int totalPlays{};
int highScore{};
int bestCombo{};
std::map<int, int> gradeCounts{};
};
struct Orbit
{
int highScore{};
};
struct Item
{
int id{};
@@ -34,10 +47,8 @@ namespace game::resource::xml
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int totalPlays{};
int highScore{};
int bestCombo{};
std::map<int, int> gradeCounts{};
SkillCheck skillCheck{};
Orbit orbit{};
std::map<int, int> inventory;
std::vector<Item> items;

View File

@@ -14,7 +14,10 @@ namespace game::resource::xml
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
return;
}
auto archive = path.directory_get();
@@ -24,11 +27,14 @@ namespace game::resource::xml
query_string_attribute(root, "SoundRootPath", &soundRootPath);
root->QueryIntAttribute("RewardScore", &rewardScore);
root->QueryFloatAttribute("RewardScoreBonus", &rewardScoreBonus);
root->QueryFloatAttribute("RewardGradeBonus", &rewardGradeBonus);
root->QueryFloatAttribute("RangeBase", &rangeBase);
root->QueryFloatAttribute("RangeMin", &rangeMin);
root->QueryFloatAttribute("RangeScoreBonus", &rangeScoreBonus);
root->QueryFloatAttribute("RewardChanceBase", &rewardChanceBase);
root->QueryFloatAttribute("RewardChanceScoreBonus", &rewardChanceScoreBonus);
root->QueryFloatAttribute("RewardRollChanceBase", &rewardRollChanceBase);
root->QueryFloatAttribute("RewardRollScoreBonus", &rewardRollScoreBonus);
root->QueryFloatAttribute("RewardRollGradeBonus", &rewardRollGradeBonus);
root->QueryFloatAttribute("ZoneBase", &zoneBase);
root->QueryFloatAttribute("ZoneMin", &zoneMin);
root->QueryFloatAttribute("ZoneScoreBonus", &zoneScoreBonus);
root->QueryFloatAttribute("SpeedMin", &speedMin);
root->QueryFloatAttribute("SpeedMax", &speedMax);
root->QueryFloatAttribute("SpeedScoreBonus", &speedScoreBonus);

View File

@@ -34,14 +34,17 @@ namespace game::resource::xml
Sounds sounds{};
std::vector<Grade> grades{};
float rewardScoreBonus{0.01f};
float rewardGradeBonus{0.05f};
float rewardChanceBase{0.01f};
float rewardChanceScoreBonus{0.01f};
float rewardRollChanceBase{1.0f};
float rewardRollScoreBonus{0.05f};
float rewardRollGradeBonus{0.05f};
float speedMin{0.005f};
float speedMax{0.075f};
float speedScoreBonus{0.000025f};
float rangeBase{0.75f};
float rangeMin{0.10f};
float rangeScoreBonus{0.0005f};
float zoneBase{0.75f};
float zoneMin{0.10f};
float zoneScoreBonus{0.0005f};
int endTimerMax{20};
int endTimerFailureMax{60};
int rewardScore{999};

View File

@@ -31,7 +31,11 @@ namespace game::resource::xml
values[i] = definitions[i].fallback;
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (document_load(path, document) != XML_SUCCESS)
{
logger.error(std::format("Unable to initialize strings: {} ({})", path.c_str(), document.ErrorStr()));
return;
}
auto root = document.RootElement();
if (!root) return;

View File

@@ -7,24 +7,23 @@
namespace game::resource::xml
{
#define GAME_XML_STRING_LIST(X) \
#define GAME_XML_STRING_LIST(X) \
X(MenuTabInteract, "TextMenuTabInteract", "Interact") \
X(MenuTabArcade, "TextMenuTabArcade", "Arcade") \
X(MenuTabInventory, "TextMenuTabInventory", "Inventory") \
X(MenuTabSettings, "TextMenuTabSettings", "Settings") \
X(MenuTabCheats, "TextMenuTabCheats", "Cheats") \
X(MenuTabDebug, "TextMenuTabDebug", "Debug") \
X(MenuOpenTooltip, "TextMenuOpenTooltip", "Open Main Menu") \
X(MenuCloseTooltip, "TextMenuCloseTooltip", "Close Main Menu") \
X(InteractChatButton, "TextInteractChatButton", "Let's chat!") \
X(InteractHelpButton, "TextInteractHelpButton", "Help") \
X(InteractFeelingButton, "TextInteractFeelingButton", "How are you feeling?") \
X(InteractWeightFormat, "TextInteractWeightFormat", "Weight: %0.2f %s (Stage: %i)") \
X(InteractCapacityFormat, "TextInteractCapacityFormat", "Capacity: %0.0f kcal (Max: %0.0f kcal)") \
X(InteractDigestionRateFormat, "TextInteractDigestionRateFormat", "Digestion Rate: %0.2f%%/sec") \
X(InteractEatingSpeedFormat, "TextInteractEatingSpeedFormat", "Eating Speed: %0.2fx") \
X(InteractTotalCaloriesFormat, "TextInteractTotalCaloriesFormat", "Total Calories Consumed: %0.0f kcal") \
X(InteractTotalFoodItemsFormat, "TextInteractTotalFoodItemsFormat", "Total Food Items Eaten: %i") \
X(InteractWeightFormat, "TextInteractWeightFormat", "Weight: %0.2f %s (Stage: %i)") \
X(InteractCapacityFormat, "TextInteractCapacityFormat", "Capacity: %0.0f kcal (Max: %0.0f kcal)") \
X(InteractDigestionRateFormat, "TextInteractDigestionRateFormat", "Digestion Rate: %0.2f%%/sec") \
X(InteractEatingSpeedFormat, "TextInteractEatingSpeedFormat", "Eating Speed: %0.2fx") \
X(InteractTotalCaloriesFormat, "TextInteractTotalCaloriesFormat", "Total Calories Consumed: %0.0f kcal") \
X(InteractTotalFoodItemsFormat, "TextInteractTotalFoodItemsFormat", "Total Food Items Eaten: %i") \
X(SettingsMeasurementSystem, "TextSettingsMeasurementSystem", "Measurement System") \
X(SettingsMetric, "TextSettingsMetric", "Metric") \
X(SettingsMetricTooltip, "TextSettingsMetricTooltip", "Use kilograms (kg).") \
@@ -35,90 +34,106 @@ namespace game::resource::xml
X(SettingsVolumeTooltip, "TextSettingsVolumeTooltip", "Adjust master volume.") \
X(SettingsAppearance, "TextSettingsAppearance", "Appearance") \
X(SettingsUseCharacterColor, "TextSettingsUseCharacterColor", "Use Character Color") \
X(SettingsUseCharacterColorTooltip, "TextSettingsUseCharacterColorTooltip", \
X(SettingsUseCharacterColorTooltip, "TextSettingsUseCharacterColorTooltip", \
"When playing, the UI will use the character's preset UI color.") \
X(SettingsColor, "TextSettingsColor", "Color") \
X(SettingsColorTooltip, "TextSettingsColorTooltip", "Change the UI color.") \
X(SettingsResetButton, "TextSettingsResetButton", "Reset to Default") \
X(SettingsSaveButton, "TextSettingsSaveButton", "Save") \
X(SettingsSaveTooltip, "TextSettingsSaveTooltip", "Save the game.\n(Note: the game autosaves frequently.)") \
X(SettingsReturnToCharactersButton, "TextSettingsReturnToCharactersButton", "Return to Characters") \
X(SettingsReturnToCharactersTooltip, "TextSettingsReturnToCharactersTooltip", \
"Go back to the character selection screen.\nProgress will be saved.") \
X(SettingsSaveTooltip, "TextSettingsSaveTooltip", "Save the game.\n(Note: the game autosaves frequently.)") \
X(SettingsReturnToCharactersButton, "TextSettingsReturnToCharactersButton", "Return to Characters") \
X(SettingsReturnToCharactersTooltip, "TextSettingsReturnToCharactersTooltip", \
"Go back to the character selection screen.\nProgress will be saved.") \
X(ToastCheatsUnlocked, "TextToastCheatsUnlocked", "Cheats unlocked!") \
X(ToastSaving, "TextToastSaving", "Saving...") \
X(ToolsHomeButton, "TextToolsHomeButton", "Home") \
X(ToolsHomeTooltip, "TextToolsHomeTooltip", "Reset camera view.\n(Shortcut: Home)") \
X(ToolsHomeTooltip, "TextToolsHomeTooltip", "Reset camera view.\n(Shortcut: Home)") \
X(ToolsOpenTooltip, "TextToolsOpenTooltip", "Open Tools") \
X(ToolsCloseTooltip, "TextToolsCloseTooltip", "Close Tools") \
X(DebugCursorScreenFormat, "TextDebugCursorScreenFormat", "Cursor Pos (Screen): %0.0f, %0.0f") \
X(DebugCursorWorldFormat, "TextDebugCursorWorldFormat", "Cursor Pos (World): %0.0f, %0.0f") \
X(DebugAnimations, "TextDebugAnimations", "Animations") \
X(DebugNowPlayingFormat, "TextDebugNowPlayingFormat", "Now Playing: %s") \
X(DebugDialogue, "TextDebugDialogue", "Dialogue") \
X(DebugShowNulls, "TextDebugShowNulls", "Show Nulls (Hitboxes)") \
X(DebugShowWorldBounds, "TextDebugShowWorldBounds", "Show World Bounds") \
X(DebugItem, "TextDebugItem", "Item") \
X(DebugHeld, "TextDebugHeld", "Held") \
X(DebugItemTypeFormat, "TextDebugItemTypeFormat", "Type: %i") \
X(DebugItemPositionFormat, "TextDebugItemPositionFormat", "Position: %0.0f, %0.0f") \
X(DebugItemVelocityFormat, "TextDebugItemVelocityFormat", "Velocity: %0.0f, %0.0f") \
X(DebugItemDurabilityFormat, "TextDebugItemDurabilityFormat", "Durability: %i") \
X(InventoryEmptyHint, "TextInventoryEmptyHint", "Check the \"Arcade\" tab to earn rewards!") \
X(InventoryEmptyHint, "TextInventoryEmptyHint", "Check the \"Arcade\" tab to earn rewards!") \
X(InventoryFlavorFormat, "TextInventoryFlavorFormat", "Flavor: %s") \
X(InventoryCaloriesFormat, "TextInventoryCaloriesFormat", "%0.0f kcal") \
X(InventoryDurabilityFormat, "TextInventoryDurabilityFormat", "Durability: %i") \
X(InventoryCapacityBonusFormat, "TextInventoryCapacityBonusFormat", "Capacity Bonus: +%0.0f kcal") \
X(InventoryDigestionRateBonusFormat, "TextInventoryDigestionRateBonusFormat", "Digestion Rate Bonus: +%0.2f%% / sec") \
X(InventoryDigestionRatePenaltyFormat, "TextInventoryDigestionRatePenaltyFormat", "Digestion Rate Penalty: %0.2f%% / sec") \
X(InventoryEatSpeedBonusFormat, "TextInventoryEatSpeedBonusFormat", "Eat Speed Bonus: +%0.2f%% / sec") \
X(InventoryEatSpeedPenaltyFormat, "TextInventoryEatSpeedPenaltyFormat", "Eat Speed Penalty: %0.2f%% / sec") \
X(InventoryUpgradePreviewFormat, "TextInventoryUpgradePreviewFormat", "Upgrade: %ix -> %s") \
X(InventoryCapacityBonusFormat, "TextInventoryCapacityBonusFormat", "Capacity Bonus: +%0.0f kcal") \
X(InventoryDigestionRateBonusFormat, "TextInventoryDigestionRateBonusFormat", \
"Digestion Rate Bonus: +%0.2f%% / sec") \
X(InventoryDigestionRatePenaltyFormat, "TextInventoryDigestionRatePenaltyFormat", \
"Digestion Rate Penalty: %0.2f%% / sec") \
X(InventoryEatSpeedBonusFormat, "TextInventoryEatSpeedBonusFormat", "Eat Speed Bonus: +%0.2f%% / sec") \
X(InventoryEatSpeedPenaltyFormat, "TextInventoryEatSpeedPenaltyFormat", "Eat Speed Penalty: %0.2f%% / sec") \
X(InventoryUpgradePreviewFormat, "TextInventoryUpgradePreviewFormat", "Upgrade: %ix -> %s") \
X(InventoryUnknown, "TextInventoryUnknown", "???") \
X(InventorySpawnButton, "TextInventorySpawnButton", "Spawn") \
X(InventoryUpgradeButton, "TextInventoryUpgradeButton", "Upgrade") \
X(InventoryUpgradeAllButton, "TextInventoryUpgradeAllButton", "Upgrade All") \
X(InventoryUpgradeNoPath, "TextInventoryUpgradeNoPath", "This item cannot be upgraded.") \
X(InventoryUpgradeNeedsTemplate, "TextInventoryUpgradeNeedsTemplate", "Needs {}x to upgrade into {}!") \
X(InventoryUpgradeOneTemplate, "TextInventoryUpgradeOneTemplate", "Use {}x to upgrade into 1x {}.") \
X(InventoryUpgradeAllTemplate, "TextInventoryUpgradeAllTemplate", "Use {}x to upgrade into {}x {}.") \
X(InventoryUpgradeNoPath, "TextInventoryUpgradeNoPath", "This item cannot be upgraded.") \
X(InventoryUpgradeNeedsTemplate, "TextInventoryUpgradeNeedsTemplate", "Needs {}x to upgrade into {}!") \
X(InventoryUpgradeOneTemplate, "TextInventoryUpgradeOneTemplate", "Use {}x to upgrade into 1x {}.") \
X(InventoryUpgradeAllTemplate, "TextInventoryUpgradeAllTemplate", "Use {}x to upgrade into {}x {}.") \
X(ArcadeSkillCheckName, "TextArcadeSkillCheckName", "Skill Check") \
X(ArcadeSkillCheckDescription, "TextArcadeSkillCheckDescription", \
"Test your timing to build score, chain combos, and earn rewards based on your performance.") \
X(ArcadeSkillCheckDescription, "TextArcadeSkillCheckDescription", \
"Test your timing! Aim for specific zones for rewards.") \
X(ArcadeDungeonName, "TextArcadeDungeonName", "Dungeon") \
X(ArcadeDungeonDescription, "TextArcadeDungeonDescription", \
"A placeholder dungeon adventure entry. Use this as a template for a future arcade game.") \
X(ArcadeOrbitName, "TextArcadeOrbitName", "Orbit") \
X(ArcadeOrbitDescription, "TextArcadeOrbitDescription", \
"Move colored objects orbiting around the cursor into similarly colored enemies!") \
X(ArcadeHowToPlay, "TextArcadeHowToPlay", "How to Play") \
X(ArcadeSkillCheckHowToPlay, "TextArcadeSkillCheckHowToPlay", \
"Press Space or click to stop the line inside the colored target zones.\nEach success builds score and combo, " \
"and high scores improve your reward chances, while increasing game speed and tightening the zones.\nMissing the " \
"colored zones ends the run.") \
X(ArcadeDungeonHowToPlay, "TextArcadeDungeonHowToPlay", \
"This is currently a template page.\nUse it to prototype dungeon rules, rewards, room flow, and UI layout.") \
X(ArcadeOrbitHowToPlay, "TextArcadeOrbitHowToPlay", \
"Control an object with your cursor.\nThere will be colored, orbiting objects around it.\nUse the mouse buttons " \
"to move the objects around your orbit.\nEnemies will appear with the same color as the orbiting objects.\nMatch " \
"the colors of the orbiting objects into the same colored enemies for score.\nOver time, more colors will be " \
"added nearby the cursor, and more enemies will appear.\nEnemies will also get faster with time.\nHow long can " \
"you survive?!") \
X(ArcadeDungeonTemplateTitle, "TextArcadeDungeonTemplateTitle", "Dungeon Template") \
X(ArcadeDungeonTemplateBody, "TextArcadeDungeonTemplateBody", \
"This screen is a placeholder for the Dungeon arcade game.\nAdd room generation, encounters, rewards, and any " \
"character-specific hooks here.\nUse the Back button below to return to the arcade menu.") \
X(ArcadeScoreFormat, "TextArcadeScoreFormat", "Score: %i pts") \
X(ArcadeScoreComboFormat, "TextArcadeScoreComboFormat", "Score: %i pts (%ix)") \
X(ArcadePlayButton, "TextArcadePlayButton", "Play") \
X(ArcadeStatsButton, "TextArcadeStatsButton", "Stats") \
X(ArcadeInfoButton, "TextArcadeInfoButton", "Info") \
X(ArcadeBackButton, "TextArcadeBackButton", "Back") \
X(ArcadeBestFormat, "TextArcadeBestFormat", "Best: %i pts (%ix)") \
X(ArcadeTotalSkillChecksFormat, "TextArcadeTotalSkillChecksFormat", "Total Skill Checks: %i") \
X(ArcadeStats, "TextArcadeStats", "Stats") \
X(ArcadeBestScoreFormat, "TextArcadeBestScoreFormat", "Best: %i pts") \
X(ArcadeBestScoreComboFormat, "TextArcadeBestScoreComboFormat", "Best: %i pts (%ix)") \
X(ArcadeTotalSkillChecksFormat, "TextArcadeTotalSkillChecksFormat", "Rounds Attempted: %i") \
X(ArcadeAccuracyFormat, "TextArcadeAccuracyFormat", "Accuracy: %0.2f%%") \
X(InfoProgressMax, "TextInfoProgressMax", "MAX") \
X(InfoProgressToNextStage, "TextInfoProgressToNextStage", "To Next Stage") \
X(InfoStageProgressFormat, "TextInfoStageProgressFormat", "Stage: %i/%i (%0.1f%%)") \
X(InfoStageProgressFormat, "TextInfoStageProgressFormat", "Stage: %i/%i (%0.1f%%)") \
X(InfoMaxedOut, "TextInfoMaxedOut", "Maxed out!") \
X(InfoStageStartFormat, "TextInfoStageStartFormat", "Start: %0.2f %s") \
X(InfoStageCurrentFormat, "TextInfoStageCurrentFormat", "Current: %0.2f %s") \
X(InfoStageNextFormat, "TextInfoStageNextFormat", "Next: %0.2f %s") \
X(InfoDigestion, "TextInfoDigestion", "Digestion") \
X(InfoDigesting, "TextInfoDigesting", "Digesting...") \
X(InfoDigestionInProgress, "TextInfoDigestionInProgress", "Digestion in progress...") \
X(InfoGiveFoodToStartDigesting, "TextInfoGiveFoodToStartDigesting", "Give food to start digesting!") \
X(InfoDigestionRateFormat, "TextInfoDigestionRateFormat", "Rate: %0.2f%% / sec") \
X(InfoDigestionInProgress, "TextInfoDigestionInProgress", "Digestion in progress...") \
X(InfoGiveFoodToStartDigesting, "TextInfoGiveFoodToStartDigesting", "Give food to start digesting!") \
X(InfoDigestionRateFormat, "TextInfoDigestionRateFormat", "Rate: %0.2f%% / sec") \
X(InfoEatingSpeedFormat, "TextInfoEatingSpeedFormat", "Eating Speed: %0.2fx") \
X(SkillCheckScoreFormat, "TextSkillCheckScoreFormat", "Score: %i pts (%ix)") \
X(SkillCheckBestFormat, "TextSkillCheckBestFormat", "Best: %i pts (%ix)") \
X(SkillCheckInstructions, "TextSkillCheckInstructions", "Match the line to the colored areas with Space/click! Better performance, better rewards!") \
X(SkillCheckScoreLoss, "TextSkillCheckScoreLoss", "-1") \
X(SkillCheckRewardToast, "TextSkillCheckRewardToast", "Fantastic score! Congratulations!") \
X(SkillCheckHighScoreToast, "TextSkillCheckHighScoreToast", "High Score!") \
X(SkillCheckInstructions, "TextSkillCheckInstructions", \
"Match the line to the colored areas with Space/click! Better performance, better rewards!") \
X(ArcadeScoreLoss, "TextArcadeScoreLoss", "-1") \
X(ArcadeRewardToast, "TextArcadeRewardToast", "Fantastic score! Congratulations!") \
X(ArcadeHighScoreToast, "TextArcadeHighScoreToast", "High Score!") \
X(ArcadeMenuBackButtonTooltip, "TextArcadeMenuBackButtonTooltip", "Progress will not be saved!") \
X(SkillCheckGradeSuccessTemplate, "TextSkillCheckGradeSuccessTemplate", "{} (+{})") \
X(SkillCheckMenuButton, "TextSkillCheckMenuButton", "Menu") \
X(ArcadeMenuBackButton, "TextArcadeMenuBackButton", "Menu") \
X(CheatsCalories, "TextCheatsCalories", "Calories") \
X(CheatsCapacity, "TextCheatsCapacity", "Capacity") \
X(CheatsWeight, "TextCheatsWeight", "Weight") \
X(CheatsWeightFormat, "TextCheatsWeightFormat", "%0.2f kg") \
X(CheatsStage, "TextCheatsStage", "Stage") \
X(CheatsDigestionRate, "TextCheatsDigestionRate", "Digestion Rate") \
X(CheatsDigestionRateFormat, "TextCheatsDigestionRateFormat", "%0.2f% / tick") \
X(CheatsDigestionRateFormat, "TextCheatsDigestionRateFormat", "%0.2f% / tick") \
X(CheatsEatSpeed, "TextCheatsEatSpeed", "Eat Speed") \
X(CheatsEatSpeedFormat, "TextCheatsEatSpeedFormat", "%0.2fx") \
X(CheatsDigestButton, "TextCheatsDigestButton", "Digest") \
@@ -132,7 +147,7 @@ namespace game::resource::xml
#define X(type, attr, fallback) type,
GAME_XML_STRING_LIST(X)
#undef X
Count
Count
};
struct Definition
@@ -143,7 +158,7 @@ namespace game::resource::xml
inline static constexpr std::array<Definition, Count> definitions{{
#define X(type, attr, fallback) {attr, fallback},
GAME_XML_STRING_LIST(X)
GAME_XML_STRING_LIST(X)
#undef X
}};

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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&);
};
}

View File

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

View File

@@ -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;
}
};

View File

@@ -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;

View File

@@ -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&);
};
}

View File

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

View 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;
}
}

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

View File

@@ -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;

View File

@@ -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;

View 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;
}
}
}

View 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&);
};
}

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

View 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&);
};
}

View 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;
}
}

View 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&);
};
}

View 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;
}
}

View File

@@ -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&);
};
}

View File

@@ -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)
{

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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
{

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

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

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

View 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*);
};
}

View File

@@ -6,4 +6,5 @@ namespace game::util::color
{
constexpr auto WHITE = glm::vec4(1.0f);
constexpr auto GRAY = glm::vec4(0.5f, 0.5f, 0.5f, 1.0f);
constexpr auto BLACK = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
}

View File

@@ -4,12 +4,15 @@
namespace game::util::imgui::style
{
void rounding_set(float rounding)
void widget_set(float rounding)
{
constexpr auto SCROLLBAR_SIZE = 30.0f;
auto& style = ImGui::GetStyle();
style.WindowRounding = rounding;
style.FrameRounding = rounding;
style.GrabRounding = rounding;
style.ScrollbarSize = SCROLLBAR_SIZE;
}
void color_set(glm::vec3 color)

View File

@@ -5,6 +5,6 @@
namespace game::util::imgui::style
{
void rounding_set(float rounding = 10.0f);
void widget_set(float rounding = 10.0f);
void color_set(glm::vec3 color);
}
}