refactoring, new game(s) in progress
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
39
src/resource/xml/dungeon.cpp
Normal file
39
src/resource/xml/dungeon.cpp
Normal 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; };
|
||||
}
|
||||
21
src/resource/xml/dungeon.hpp
Normal file
21
src/resource/xml/dungeon.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
134
src/resource/xml/orbit.cpp
Normal 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
100
src/resource/xml/orbit.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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&);
|
||||
};
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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&);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
61
src/state/play/item/reward.cpp
Normal file
61
src/state/play/item/reward.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/state/play/item/reward.hpp
Normal file
22
src/state/play/item/reward.hpp
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
244
src/state/play/menu/arcade.cpp
Normal file
244
src/state/play/menu/arcade.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/state/play/menu/arcade.hpp
Normal file
40
src/state/play/menu/arcade.hpp
Normal 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&);
|
||||
};
|
||||
}
|
||||
310
src/state/play/menu/arcade/dungeon.cpp
Normal file
310
src/state/play/menu/arcade/dungeon.cpp
Normal 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()));
|
||||
}
|
||||
}
|
||||
82
src/state/play/menu/arcade/dungeon.hpp
Normal file
82
src/state/play/menu/arcade/dungeon.hpp
Normal 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&);
|
||||
};
|
||||
}
|
||||
609
src/state/play/menu/arcade/orbit.cpp
Normal file
609
src/state/play/menu/arcade/orbit.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
80
src/state/play/menu/arcade/orbit.hpp
Normal file
80
src/state/play/menu/arcade/orbit.hpp
Normal 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&);
|
||||
};
|
||||
}
|
||||
339
src/state/play/menu/arcade/skill_check.cpp
Normal file
339
src/state/play/menu/arcade/skill_check.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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&);
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
138
src/state/play/menu/item_effect_manager.cpp
Normal file
138
src/state/play/menu/item_effect_manager.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
41
src/state/play/menu/item_effect_manager.hpp
Normal file
41
src/state/play/menu/item_effect_manager.hpp
Normal 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);
|
||||
};
|
||||
}
|
||||
31
src/state/play/menu/toasts.cpp
Normal file
31
src/state/play/menu/toasts.cpp
Normal 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--);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/state/play/menu/toasts.hpp
Normal file
26
src/state/play/menu/toasts.hpp
Normal 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*);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user