diff --git a/CMakeLists.txt b/CMakeLists.txt index 4738e9c..9c25837 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/entity/actor.cpp b/src/entity/actor.cpp index 27e3526..7a34a64 100644 --- a/src/entity/actor.cpp +++ b/src/entity/actor.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; diff --git a/src/entity/actor.hpp b/src/entity/actor.hpp index 1c21833..9993270 100644 --- a/src/entity/actor.hpp +++ b/src/entity/actor.hpp @@ -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{}; diff --git a/src/loader.cpp b/src/loader.cpp index dbb8565..05f02a8 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -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); } diff --git a/src/render/canvas.cpp b/src/render/canvas.cpp index d4be7c2..4428951 100644 --- a/src/render/canvas.cpp +++ b/src/render/canvas.cpp @@ -1,6 +1,7 @@ #include "canvas.hpp" #include +#include #include #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() diff --git a/src/render/canvas.hpp b/src/render/canvas.hpp index 2343480..fef8818 100644 --- a/src/render/canvas.hpp +++ b/src/render/canvas.hpp @@ -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); diff --git a/src/resource/audio.cpp b/src/resource/audio.cpp index c31d132..53bdf5a 100644 --- a/src/resource/audio.cpp +++ b/src/resource/audio.cpp @@ -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); } diff --git a/src/resource/audio.hpp b/src/resource/audio.hpp index 84106c2..b1d99cd 100644 --- a/src/resource/audio.hpp +++ b/src/resource/audio.hpp @@ -14,7 +14,7 @@ namespace game::resource void unload(); std::shared_ptr 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); }; diff --git a/src/resource/xml/anm2.hpp b/src/resource/xml/anm2.hpp index 508330b..3af7ff4 100644 --- a/src/resource/xml/anm2.hpp +++ b/src/resource/xml/anm2.hpp @@ -109,14 +109,65 @@ namespace game::resource::xml struct FrameOptional { - std::optional crop{}; - std::optional position{}; - std::optional pivot{}; - std::optional size{}; - std::optional scale{}; + struct Vec2 + { + std::optional x{}; + std::optional 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 x{}; + std::optional y{}; + std::optional 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 x{}; + std::optional y{}; + std::optional z{}; + std::optional 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 rotation{}; - std::optional tint{}; - std::optional colorOffset{}; + Vec4 tint{}; + Vec3 colorOffset{}; std::optional isInterpolated{}; std::optional isVisible{}; }; diff --git a/src/resource/xml/area.cpp b/src/resource/xml/area.cpp index bca23c3..73f8cb5 100644 --- a/src/resource/xml/area.cpp +++ b/src/resource/xml/area.cpp @@ -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(); diff --git a/src/resource/xml/character.cpp b/src/resource/xml/character.cpp index 33ea5b0..618b465 100644 --- a/src/resource/xml/character.cpp +++ b/src/resource/xml/character.cpp @@ -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); diff --git a/src/resource/xml/character.hpp b/src/resource/xml/character.hpp index 1ce40d0..ded2429 100644 --- a/src/resource/xml/character.hpp +++ b/src/resource/xml/character.hpp @@ -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{}; diff --git a/src/resource/xml/cursor.cpp b/src/resource/xml/cursor.cpp index 70087ab..42a5991 100644 --- a/src/resource/xml/cursor.cpp +++ b/src/resource/xml/cursor.cpp @@ -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(); diff --git a/src/resource/xml/dialogue.cpp b/src/resource/xml/dialogue.cpp index cf2b187..8d8d88a 100644 --- a/src/resource/xml/dialogue.cpp +++ b/src/resource/xml/dialogue.cpp @@ -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()) { diff --git a/src/resource/xml/dungeon.cpp b/src/resource/xml/dungeon.cpp new file mode 100644 index 0000000..9b14df7 --- /dev/null +++ b/src/resource/xml/dungeon.cpp @@ -0,0 +1,39 @@ +#include "dungeon.hpp" + +#include "../../log.hpp" +#include "util.hpp" + +#include + +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; }; +} diff --git a/src/resource/xml/dungeon.hpp b/src/resource/xml/dungeon.hpp new file mode 100644 index 0000000..972f041 --- /dev/null +++ b/src/resource/xml/dungeon.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "../../util/physfs.hpp" + +#include + +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; + }; +} diff --git a/src/resource/xml/item.cpp b/src/resource/xml/item.cpp index 5df2629..694e6a5 100644 --- a/src/resource/xml/item.cpp +++ b/src/resource/xml/item.cpp @@ -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(); diff --git a/src/resource/xml/menu.cpp b/src/resource/xml/menu.cpp index daae997..57262b7 100644 --- a/src/resource/xml/menu.cpp +++ b/src/resource/xml/menu.cpp @@ -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(); diff --git a/src/resource/xml/orbit.cpp b/src/resource/xml/orbit.cpp new file mode 100644 index 0000000..444e710 --- /dev/null +++ b/src/resource/xml/orbit.cpp @@ -0,0 +1,134 @@ +#include "orbit.hpp" + +#include "../../log.hpp" +#include "util.hpp" + +#include + +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; }; +} diff --git a/src/resource/xml/orbit.hpp b/src/resource/xml/orbit.hpp new file mode 100644 index 0000000..fed02c0 --- /dev/null +++ b/src/resource/xml/orbit.hpp @@ -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 colors{}; + int startTime{30}; + + bool isValid{}; + + Orbit() = default; + Orbit(const util::physfs::Path&, Dialogue&); + + bool is_valid() const; + }; +} diff --git a/src/resource/xml/save.cpp b/src/resource/xml/save.cpp index b266a0a..7e21b6a 100644 --- a/src/resource/xml/save.cpp +++ b/src/resource/xml/save.cpp @@ -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) diff --git a/src/resource/xml/save.hpp b/src/resource/xml/save.hpp index 79db1c9..15e3f9c 100644 --- a/src/resource/xml/save.hpp +++ b/src/resource/xml/save.hpp @@ -12,6 +12,19 @@ namespace game::resource::xml class Save { public: + struct SkillCheck + { + int totalPlays{}; + int highScore{}; + int bestCombo{}; + std::map 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 gradeCounts{}; + SkillCheck skillCheck{}; + Orbit orbit{}; std::map inventory; std::vector items; diff --git a/src/resource/xml/skill_check.cpp b/src/resource/xml/skill_check.cpp index 8fff4d5..ea0cc12 100644 --- a/src/resource/xml/skill_check.cpp +++ b/src/resource/xml/skill_check.cpp @@ -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); diff --git a/src/resource/xml/skill_check.hpp b/src/resource/xml/skill_check.hpp index 70c3a91..47dff62 100644 --- a/src/resource/xml/skill_check.hpp +++ b/src/resource/xml/skill_check.hpp @@ -34,14 +34,17 @@ namespace game::resource::xml Sounds sounds{}; std::vector 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}; diff --git a/src/resource/xml/strings.cpp b/src/resource/xml/strings.cpp index 0864118..65064e0 100644 --- a/src/resource/xml/strings.cpp +++ b/src/resource/xml/strings.cpp @@ -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; diff --git a/src/resource/xml/strings.hpp b/src/resource/xml/strings.hpp index 64764ed..34e3751 100644 --- a/src/resource/xml/strings.hpp +++ b/src/resource/xml/strings.hpp @@ -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 definitions{{ #define X(type, attr, fallback) {attr, fallback}, - GAME_XML_STRING_LIST(X) + GAME_XML_STRING_LIST(X) #undef X }}; diff --git a/src/state/play.cpp b/src/state/play.cpp index dfd8816..5ca3e11 100644 --- a/src/state/play.cpp +++ b/src/state/play.cpp @@ -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; diff --git a/src/state/play/arcade.cpp b/src/state/play/arcade.cpp deleted file mode 100644 index 505980d..0000000 --- a/src/state/play/arcade.cpp +++ /dev/null @@ -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; - } - } -} diff --git a/src/state/play/arcade.hpp b/src/state/play/arcade.hpp deleted file mode 100644 index 50fa575..0000000 --- a/src/state/play/arcade.hpp +++ /dev/null @@ -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&); - }; -} diff --git a/src/state/play/arcade/skill_check.cpp b/src/state/play/arcade/skill_check.cpp deleted file mode 100644 index dbd7b2e..0000000 --- a/src/state/play/arcade/skill_check.cpp +++ /dev/null @@ -1,452 +0,0 @@ -#include "skill_check.hpp" - -#include - -#include "../../../util/imgui.hpp" -#include "../../../util/imgui/widget.hpp" -#include "../../../util/math.hpp" - -#include -#include -#include -#include -#include - -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 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 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())); - } -} diff --git a/src/state/play/character_manager.cpp b/src/state/play/character_manager.cpp index 031be96..453d382 100644 --- a/src/state/play/character_manager.cpp +++ b/src/state/play/character_manager.cpp @@ -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() * 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; } }; diff --git a/src/state/play/cheats.cpp b/src/state/play/cheats.cpp index 2a5c2e9..3699013 100644 --- a/src/state/play/cheats.cpp +++ b/src/state/play/cheats.cpp @@ -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; diff --git a/src/state/play/cheats.hpp b/src/state/play/cheats.hpp index 09db361..0799950 100644 --- a/src/state/play/cheats.hpp +++ b/src/state/play/cheats.hpp @@ -1,6 +1,6 @@ #pragma once -#include "inventory.hpp" +#include "menu/inventory.hpp" #include "text.hpp" #include @@ -10,6 +10,6 @@ namespace game::state::play class Cheats { public: - void update(Resources&, entity::Character&, Inventory&); + void update(Resources&, entity::Character&, menu::Inventory&); }; } diff --git a/src/state/play/debug.cpp b/src/state/play/debug.cpp index 76cba28..51f857e 100644 --- a/src/state/play/debug.cpp +++ b/src/state/play/debug.cpp @@ -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(); } } diff --git a/src/state/play/item/reward.cpp b/src/state/play/item/reward.cpp new file mode 100644 index 0000000..67d1d32 --- /dev/null +++ b/src/state/play/item/reward.cpp @@ -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; + } +} diff --git a/src/state/play/item/reward.hpp b/src/state/play/item/reward.hpp new file mode 100644 index 0000000..7c71964 --- /dev/null +++ b/src/state/play/item/reward.hpp @@ -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); + }; +} diff --git a/src/state/play/menu.cpp b/src/state/play/menu.cpp index 7f5ae8e..88d9c30 100644 --- a/src/state/play/menu.cpp +++ b/src/state/play/menu.cpp @@ -3,7 +3,6 @@ #include "style.hpp" #include "../../util/imgui.hpp" -#include "../../util/imgui/style.hpp" #include "../../util/imgui/widget.hpp" #include @@ -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; diff --git a/src/state/play/menu.hpp b/src/state/play/menu.hpp index 76b5306..e54e303 100644 --- a/src/state/play/menu.hpp +++ b/src/state/play/menu.hpp @@ -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; diff --git a/src/state/play/menu/arcade.cpp b/src/state/play/menu/arcade.cpp new file mode 100644 index 0000000..e2e99e0 --- /dev/null +++ b/src/state/play/menu/arcade.cpp @@ -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; + } + } +} diff --git a/src/state/play/menu/arcade.hpp b/src/state/play/menu/arcade.hpp new file mode 100644 index 0000000..8c6c644 --- /dev/null +++ b/src/state/play/menu/arcade.hpp @@ -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&); + }; +} diff --git a/src/state/play/menu/arcade/dungeon.cpp b/src/state/play/menu/arcade/dungeon.cpp new file mode 100644 index 0000000..0223a53 --- /dev/null +++ b/src/state/play/menu/arcade/dungeon.cpp @@ -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 +#include + +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())); + } +} diff --git a/src/state/play/menu/arcade/dungeon.hpp b/src/state/play/menu/arcade/dungeon.hpp new file mode 100644 index 0000000..7d04ac1 --- /dev/null +++ b/src/state/play/menu/arcade/dungeon.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include "../../../../entity/character.hpp" + +#include + +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 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&); + }; +} diff --git a/src/state/play/menu/arcade/orbit.cpp b/src/state/play/menu/arcade/orbit.cpp new file mode 100644 index 0000000..9ac278e --- /dev/null +++ b/src/state/play/menu/arcade/orbit.cpp @@ -0,0 +1,609 @@ +#include "orbit.hpp" + +#include "../../../../util/imgui.hpp" +#include "../../../../util/imgui/widget.hpp" +#include "../../../../util/math.hpp" + +#include +#include +#include +#include +#include + +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& entities) + { + std::vector 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)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; + } +} diff --git a/src/state/play/menu/arcade/orbit.hpp b/src/state/play/menu/arcade/orbit.hpp new file mode 100644 index 0000000..ce2c5bc --- /dev/null +++ b/src/state/play/menu/arcade/orbit.hpp @@ -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 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&); + }; +} diff --git a/src/state/play/menu/arcade/skill_check.cpp b/src/state/play/menu/arcade/skill_check.cpp new file mode 100644 index 0000000..a5bdb77 --- /dev/null +++ b/src/state/play/menu/arcade/skill_check.cpp @@ -0,0 +1,339 @@ +#include "skill_check.hpp" + +#include + +#include "../../../../util/imgui.hpp" +#include "../../../../util/imgui/widget.hpp" +#include "../../../../util/math.hpp" + +#include +#include + +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 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; + } +} diff --git a/src/state/play/arcade/skill_check.hpp b/src/state/play/menu/arcade/skill_check.hpp similarity index 58% rename from src/state/play/arcade/skill_check.hpp rename to src/state/play/menu/arcade/skill_check.hpp index e93e3c6..8421088 100644 --- a/src/state/play/arcade/skill_check.hpp +++ b/src/state/play/menu/arcade/skill_check.hpp @@ -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 #include -#include #include -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 toasts{}; - std::vector items{}; - std::unordered_map itemActors{}; - std::unordered_map itemRects{}; - std::unordered_map 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&); }; } diff --git a/src/state/play/interact.cpp b/src/state/play/menu/interact.cpp similarity index 95% rename from src/state/play/interact.cpp rename to src/state/play/menu/interact.cpp index 939786a..a654d7f 100644 --- a/src/state/play/interact.cpp +++ b/src/state/play/menu/interact.cpp @@ -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) { diff --git a/src/state/play/interact.hpp b/src/state/play/menu/interact.hpp similarity index 69% rename from src/state/play/interact.hpp rename to src/state/play/menu/interact.hpp index b2b1b69..9e1706e 100644 --- a/src/state/play/interact.hpp +++ b/src/state/play/menu/interact.hpp @@ -1,10 +1,10 @@ #pragma once -#include "text.hpp" +#include "../text.hpp" #include -namespace game::state::play +namespace game::state::play::menu { class Interact { diff --git a/src/state/play/inventory.cpp b/src/state/play/menu/inventory.cpp similarity index 98% rename from src/state/play/inventory.cpp rename to src/state/play/menu/inventory.cpp index 6f9ea2b..651b9f4 100644 --- a/src/state/play/inventory.cpp +++ b/src/state/play/menu/inventory.cpp @@ -1,16 +1,16 @@ #include "inventory.hpp" -#include "style.hpp" +#include "../style.hpp" #include #include #include #include -#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; diff --git a/src/state/play/inventory.hpp b/src/state/play/menu/inventory.hpp similarity index 75% rename from src/state/play/inventory.hpp rename to src/state/play/menu/inventory.hpp index 2737a14..deb0f6c 100644 --- a/src/state/play/inventory.hpp +++ b/src/state/play/menu/inventory.hpp @@ -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 -namespace game::state::play +namespace game::state::play::menu { class Inventory { diff --git a/src/state/play/menu/item_effect_manager.cpp b/src/state/play/menu/item_effect_manager.cpp new file mode 100644 index 0000000..2623866 --- /dev/null +++ b/src/state/play/menu/item_effect_manager.cpp @@ -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(); + } +} diff --git a/src/state/play/menu/item_effect_manager.hpp b/src/state/play/menu/item_effect_manager.hpp new file mode 100644 index 0000000..002e853 --- /dev/null +++ b/src/state/play/menu/item_effect_manager.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "../../../render/canvas.hpp" +#include "../../../entity/actor.hpp" +#include "../../../resources.hpp" + +#include +#include +#include + +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 entries{}; + std::unordered_map actors{}; + std::unordered_map rects{}; + std::unordered_map 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); + }; +} diff --git a/src/state/play/menu/toasts.cpp b/src/state/play/menu/toasts.cpp new file mode 100644 index 0000000..81749d1 --- /dev/null +++ b/src/state/play/menu/toasts.cpp @@ -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--); + } + } +} diff --git a/src/state/play/menu/toasts.hpp b/src/state/play/menu/toasts.hpp new file mode 100644 index 0000000..f4adf7c --- /dev/null +++ b/src/state/play/menu/toasts.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include + +namespace game::state::play::menu +{ + class Toasts + { + public: + struct Toast + { + std::string message{}; + ImVec2 position{}; + int time{}; + int timeMax{}; + }; + + std::vector toasts{}; + + void spawn(const std::string& message, const ImVec2& position, int time); + void update(ImDrawList*); + }; +} diff --git a/src/util/color.hpp b/src/util/color.hpp index d87aa7d..f17f3e0 100644 --- a/src/util/color.hpp +++ b/src/util/color.hpp @@ -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); } \ No newline at end of file diff --git a/src/util/imgui/style.cpp b/src/util/imgui/style.cpp index c4a4f97..fe59151 100644 --- a/src/util/imgui/style.cpp +++ b/src/util/imgui/style.cpp @@ -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) diff --git a/src/util/imgui/style.hpp b/src/util/imgui/style.hpp index 5822716..1c875dd 100644 --- a/src/util/imgui/style.hpp +++ b/src/util/imgui/style.hpp @@ -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); -} \ No newline at end of file +}