The Mega Snivy Update
Some checks failed
Build / Build Game (push) Has been cancelled

This commit is contained in:
2026-02-28 21:48:00 -05:00
parent 8b2edd1359
commit 17f3348e94
163 changed files with 8725 additions and 13281 deletions

View File

@@ -1,3 +1,3 @@
CompileFlags: CompileFlags:
CompilationDatabase: build CompilationDatabase: build
Add: [-std=c++20] Add: [-std=gnu++23]

3
.gitmodules vendored
View File

@@ -13,9 +13,6 @@
[submodule "external/tinyxml2"] [submodule "external/tinyxml2"]
path = external/tinyxml2 path = external/tinyxml2
url = https://github.com/leethomason/tinyxml2 url = https://github.com/leethomason/tinyxml2
[submodule "external/libanm2"]
path = external/libanm2
url = https://github.com/shweetsstuff/libanm2
[submodule "external/physfs"] [submodule "external/physfs"]
path = external/physfs path = external/physfs
url = https://github.com/icculus/physfs url = https://github.com/icculus/physfs

View File

@@ -8,9 +8,9 @@ set(BUILD_SHARED_LIBS OFF)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE) set(CMAKE_C_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS_DEBUG "-Wall -Wpedantic -O0 -g" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS_RELEASE "-Wall -Wpedantic -O3 -DNDEBUG" CACHE STRING "" FORCE)
endif() endif()
set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE)
@@ -56,12 +56,25 @@ set(SDLMIXER_TEST OFF CACHE BOOL "" FORCE)
set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE) set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL)
set(PHYSFS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_TEST OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_WX_TEST OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_ZIP ON CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_7Z OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_GRP OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_HOG OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_PAK OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_WAD OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_DIR ON CACHE BOOL "" FORCE)
set(PHYSFS_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
add_subdirectory(external/physfs)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/imgui) set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/imgui)
set(TINYXML2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/tinyxml2) set(TINYXML2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/tinyxml2)
set(GLM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/glm) set(GLM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/glm)
set(PHYSFS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/physfs)
set(IMGUI_SOURCES set(IMGUI_SOURCES
${IMGUI_DIR}/imgui.cpp ${IMGUI_DIR}/imgui.cpp
@@ -75,12 +88,17 @@ set(IMGUI_SOURCES
set (TINYXML2_SOURCES ${TINYXML2_DIR}/tinyxml2.cpp) set (TINYXML2_SOURCES ${TINYXML2_DIR}/tinyxml2.cpp)
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
include/*.cpp include/*.cpp
src/*.cpp src/*.cpp
src/resource/*.cpp src/resource/*.cpp
src/window/*.cpp src/resource/xml/*.cpp
src/util/*.cpp src/state/*.cpp
src/util/*.h src/state/main/*.cpp
src/state/select/*.cpp
src/entity/*.cpp
src/window/*.cpp
src/util/*.cpp
src/util/imgui/*.cpp
) )
add_executable(${PROJECT_NAME} add_executable(${PROJECT_NAME}
@@ -103,7 +121,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
include include
) )
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3-static SDL3_mixer::SDL3_mixer-static) target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3-static SDL3_mixer::SDL3_mixer-static physfs-static)
set(PROJECT_RESOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/resources") set(PROJECT_RESOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/resources")
set(PROJECT_RESOURCES_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources") set(PROJECT_RESOURCES_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources")
@@ -111,6 +129,7 @@ if(EXISTS "${PROJECT_RESOURCES_DIR}")
file(GLOB_RECURSE PROJECT_RESOURCE_FILES CONFIGURE_DEPENDS file(GLOB_RECURSE PROJECT_RESOURCE_FILES CONFIGURE_DEPENDS
"${PROJECT_RESOURCES_DIR}/*") "${PROJECT_RESOURCES_DIR}/*")
add_custom_target(copy_resources ALL add_custom_target(copy_resources ALL
COMMAND ${CMAKE_COMMAND} -E remove_directory "${PROJECT_RESOURCES_BINARY_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_RESOURCES_DIR}" "${PROJECT_RESOURCES_BINARY_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_RESOURCES_DIR}" "${PROJECT_RESOURCES_BINARY_DIR}"
DEPENDS ${PROJECT_RESOURCE_FILES} DEPENDS ${PROJECT_RESOURCE_FILES}
COMMENT "Copying resources directory") COMMENT "Copying resources directory")
@@ -128,6 +147,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
"-sUSE_OGG=1" "-sUSE_OGG=1"
"-sUSE_VORBIS=1" "-sUSE_VORBIS=1"
"-sALLOW_MEMORY_GROWTH=1" "-sALLOW_MEMORY_GROWTH=1"
"-sFORCE_FILESYSTEM=1"
"-sASYNCIFY"
"-lidbfs.js"
) )
if(HAS_PROJECT_RESOURCES) if(HAS_PROJECT_RESOURCES)
target_link_options(${PROJECT_NAME} PRIVATE target_link_options(${PROJECT_NAME} PRIVATE
@@ -137,7 +159,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
endif() endif()
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
OUTPUT_NAME "index" OUTPUT_NAME "index"
SUFFIX ".js") SUFFIX ".html")
else() else()
find_package(OpenGL REQUIRED COMPONENTS OpenGL) find_package(OpenGL REQUIRED COMPONENTS OpenGL)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL) target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL)

1
external/libanm2 vendored

Submodule external/libanm2 deleted from 623c67edbd

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,14 @@
#include "canvas.h" #include "canvas.hpp"
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <utility>
#include "util/imgui.hpp"
#include "util/math.hpp"
using namespace glm; using namespace glm;
using namespace game::resource; using namespace game::resource;
using namespace game::util;
namespace game namespace game
{ {
@@ -13,16 +19,16 @@ namespace game
GLuint Canvas::rectVBO = 0; GLuint Canvas::rectVBO = 0;
bool Canvas::isStaticInit = false; bool Canvas::isStaticInit = false;
Canvas::Canvas(vec2 size, bool isDefault) Canvas::Canvas(ivec2 size, Flags flags)
{ {
this->size = size; this->size = size;
this->flags = flags;
if (isDefault) if ((flags & DEFAULT) != 0)
{ {
fbo = 0; fbo = 0;
rbo = 0; rbo = 0;
texture = 0; texture = 0;
this->isDefault = true;
} }
else else
{ {
@@ -69,7 +75,6 @@ namespace game
glBindVertexArray(0); glBindVertexArray(0);
// Rect
glGenVertexArrays(1, &rectVAO); glGenVertexArrays(1, &rectVAO);
glGenBuffers(1, &rectVBO); glGenBuffers(1, &rectVBO);
@@ -82,12 +87,47 @@ namespace game
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindVertexArray(0); glBindVertexArray(0);
isStaticInit = true;
} }
} }
Canvas::Canvas(const Canvas& other) : Canvas(other.size, other.flags)
{
pan = other.pan;
zoom = other.zoom;
if ((flags & DEFAULT) == 0 && (other.flags & DEFAULT) == 0)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, other.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, other.size.x, other.size.y, 0, 0, size.x, size.y, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
Canvas::Canvas(Canvas&& other) noexcept
{
size = other.size;
pan = other.pan;
zoom = other.zoom;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
texture = other.texture;
other.size = {};
other.pan = {};
other.zoom = 100.0f;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
other.texture = 0;
}
Canvas::~Canvas() Canvas::~Canvas()
{ {
if (!isDefault) if ((flags & DEFAULT) == 0)
{ {
if (fbo) glDeleteFramebuffers(1, &fbo); if (fbo) glDeleteFramebuffers(1, &fbo);
if (rbo) glDeleteRenderbuffers(1, &rbo); if (rbo) glDeleteRenderbuffers(1, &rbo);
@@ -95,15 +135,66 @@ namespace game
} }
} }
mat4 Canvas::view_get() const { return mat4{1.0f}; } Canvas& Canvas::operator=(const Canvas& other)
mat4 Canvas::projection_get() const
{ {
if (isDefault) return glm::ortho(0.0f, (float)size.x, (float)size.y, 0.0f, -1.0f, 1.0f); if (this == &other) return *this;
return glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f); Canvas tmp(other);
*this = std::move(tmp);
return *this;
} }
void Canvas::texture_render(Shader& shader, GLuint textureId, mat4& model, vec4 tint, vec3 colorOffset, Canvas& Canvas::operator=(Canvas&& other) noexcept
float* vertices) const {
if (this == &other) return *this;
if ((flags & DEFAULT) == 0)
{
if (fbo) glDeleteFramebuffers(1, &fbo);
if (rbo) glDeleteRenderbuffers(1, &rbo);
if (texture) glDeleteTextures(1, &texture);
}
size = other.size;
pan = other.pan;
zoom = other.zoom;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
texture = other.texture;
other.size = {};
other.pan = {};
other.zoom = 100.0f;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
other.texture = 0;
return *this;
}
mat4 Canvas::view_get() const
{
auto view = mat4{1.0f};
auto zoomFactor = math::to_unit(zoom);
auto panFactor = pan * zoomFactor;
view = glm::translate(view, vec3(-panFactor, 0.0f));
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
return view;
}
mat4 Canvas::projection_get() const
{
if ((flags & FLIP) != 0)
{
return glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
}
return glm::ortho(0.0f, (float)size.x, (float)size.y, 0.0f, -1.0f, 1.0f);
}
void Canvas::texture_render(Shader& shader, GLuint textureID, mat4 model, vec4 tint, vec3 colorOffset,
const float* vertices) const
{ {
glUseProgram(shader.id); glUseProgram(shader.id);
@@ -122,7 +213,7 @@ namespace game
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW); glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId); glBindTexture(GL_TEXTURE_2D, textureID);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
@@ -156,14 +247,30 @@ namespace game
texture_render(shader, texture, model, tint, colorOffset); texture_render(shader, texture, model, tint, colorOffset);
} }
void Canvas::bind() const void Canvas::bind()
{ {
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo);
}
void Canvas::size_set(ivec2 size)
{
if ((flags & DEFAULT) == 0 && (size.x != this->size.x || size.y != this->size.y))
{
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
}
this->size = size;
glViewport(0, 0, size.x, size.y); glViewport(0, 0, size.x, size.y);
} }
void Canvas::clear(vec4 color) const void Canvas::clear(vec4 color)
{ {
glClearColor(color.r, color.g, color.b, color.a); glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
@@ -175,5 +282,14 @@ namespace game
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
bool Canvas::is_valid() const { return fbo != 0 || isDefault; }; bool Canvas::is_valid() const { return (flags & DEFAULT) != 0 || fbo != 0; };
glm::vec2 Canvas::screen_position_convert(glm::vec2 position) const
{
auto viewport = ImGui::GetMainViewport();
auto viewportPos = imgui::to_vec2(viewport->Pos);
auto localPosition = position - viewportPos;
auto zoomFactor = math::to_unit(zoom);
return pan + (localPosition / zoomFactor);
}
} }

View File

@@ -6,7 +6,7 @@
#include <glad/glad.h> #include <glad/glad.h>
#endif #endif
#include "resource/shader.h" #include "resource/shader.hpp"
#include <glm/ext/matrix_clip_space.hpp> #include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp> #include <glm/ext/matrix_transform.hpp>
#include <glm/glm.hpp> #include <glm/glm.hpp>
@@ -30,27 +30,45 @@ namespace game
static bool isStaticInit; static bool isStaticInit;
public: public:
static constexpr glm::vec4 CLEAR_COLOR = {0, 0, 0, 0};
enum Flag
{
DEFAULT = (1 << 0),
FLIP = (1 << 1)
};
using Flags = int;
GLuint fbo{}; GLuint fbo{};
GLuint rbo{}; GLuint rbo{};
GLuint texture{}; GLuint texture{};
glm::vec2 size{}; glm::ivec2 size{};
glm::vec2 pan{};
bool isDefault{}; float zoom{100.0f};
Flags flags{FLIP};
Canvas() = default; Canvas() = default;
Canvas(glm::vec2, bool isDefault = false); Canvas(glm::ivec2, Flags = FLIP);
Canvas(const Canvas&);
Canvas(Canvas&&) noexcept;
~Canvas(); ~Canvas();
Canvas& operator=(const Canvas&);
Canvas& operator=(Canvas&&) noexcept;
glm::mat4 transform_get() const; glm::mat4 transform_get() const;
glm::mat4 view_get() const; glm::mat4 view_get() const;
glm::mat4 projection_get() const; glm::mat4 projection_get() const;
void texture_render(resource::Shader&, GLuint, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, void texture_render(resource::Shader&, GLuint, glm::mat4, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)TEXTURE_VERTICES) const; const float* = TEXTURE_VERTICES) const;
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 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 render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void bind() const; void bind();
void size_set(glm::ivec2 size);
void clear(glm::vec4 color = CLEAR_COLOR);
void unbind() const; void unbind() const;
void clear(glm::vec4 color = glm::vec4(0, 0, 0, 1)) const;
bool is_valid() const; bool is_valid() const;
glm::vec2 screen_position_convert(glm::vec2 position) const;
}; };
} }

View File

@@ -1,273 +0,0 @@
#include "character.h"
#include <format>
#include "types.h"
#include "util/math_.h"
using namespace game::util;
using namespace game::anm2;
using namespace glm;
namespace game
{
float Character::max_capacity() { return capacity * CAPACITY_OVERSTUFFED_LIMIT_MULTIPLIER; }
float Character::over_capacity_calories_get()
{
if (calories < capacity) return 0.0f;
return (calories - capacity);
}
float Character::over_capacity_percent_get()
{
if (calories < capacity) return 0.0f;
return (calories - capacity) / (max_capacity() - capacity);
}
float Character::digestion_rate_second_get() { return digestionRate * 60; }
bool Character::is_over_capacity() { return calories + 1 >= capacity; }
bool Character::is_max_capacity() { return calories >= max_capacity(); }
void Character::talk()
{
if (auto talkItem = item_get(anm2::LAYER, talkLayerID))
{
talkOverride = {.animationIndex = animationIndex,
.sourceID = talkLayerID,
.destinationID = mouthLayerID,
.length = (float)item_length(talkItem),
.isLoop = true};
}
else
talkOverride.isEnabled = false;
}
void Character::blink()
{
if (auto blinkItem = item_get(anm2::LAYER, blinkLayerID))
{
blinkOverride = {.animationIndex = animationIndex,
.sourceID = blinkLayerID,
.destinationID = headLayerID,
.length = (float)item_length(blinkItem)};
}
else
blinkOverride.isEnabled = false;
}
float Character::weight_get(MeasurementSystem system) { return system == IMPERIAL ? weight * KG_TO_LB : weight; }
float Character::weight_threshold_get(int stage, MeasurementSystem system)
{
stage = glm::clamp(stage, 0, WEIGHT_STAGE_MAX);
return system == IMPERIAL ? WEIGHT_THRESHOLDS[stage] * KG_TO_LB : WEIGHT_THRESHOLDS[stage];
}
float Character::weight_threshold_current_get(MeasurementSystem system)
{
return weight_threshold_get(weightStage, system);
}
float Character::weight_threshold_next_get(MeasurementSystem system)
{
auto nextStage = glm::clamp(0, weightStage + 1, WEIGHT_STAGE_MAX);
return weight_threshold_get(nextStage, system);
}
float Character::progress_to_next_weight_threshold_get()
{
if (weightStage >= WEIGHT_STAGE_MAX - 1) return 0.0f;
return (weight - weight_threshold_current_get()) / (weight_threshold_next_get() - weight_threshold_current_get());
}
vec4 Character::mouth_rect_get() { return null_frame_rect(mouthNullID); }
vec4 Character::head_rect_get() { return null_frame_rect(headNullID); }
vec4 Character::belly_rect_get() { return null_frame_rect(bellyNullID); }
vec4 Character::tail_rect_get() { return null_frame_rect(tailNullID); }
Character::Character(Anm2* _anm2, glm::ivec2 _position) : Actor(_anm2, _position)
{
talkLayerID = item_id_get(LAYER_TALK);
blinkLayerID = item_id_get(LAYER_BLINK);
headLayerID = item_id_get(LAYER_HEAD);
mouthLayerID = item_id_get(LAYER_MOUTH);
torsoLayerID = item_id_get(LAYER_TORSO);
tailLayerID = item_id_get(LAYER_TAIL);
mouthNullID = item_id_get(NULL_MOUTH, anm2::NULL_);
headNullID = item_id_get(NULL_HEAD, anm2::NULL_);
bellyNullID = item_id_get(NULL_BELLY, anm2::NULL_);
tailNullID = item_id_get(NULL_TAIL, anm2::NULL_);
torsoCapacityScale = {.destinationID = torsoLayerID, .mode = Override::FRAME_ADD};
tailCapacityScale = {.destinationID = tailLayerID, .mode = Override::FRAME_ADD};
torsoCapacityScale.frame.scale = glm::vec2();
tailCapacityScale.frame.scale = glm::vec2();
overrides.emplace_back(&talkOverride);
overrides.emplace_back(&blinkOverride);
overrides.emplace_back(&torsoCapacityScale);
overrides.emplace_back(&tailCapacityScale);
}
void Character::digestion_start()
{
isDigesting = true;
digestionTimer = DIGESTION_TIMER_MAX;
isJustDigestionStart = true;
}
void Character::digestion_end()
{
auto increment = calories * CALORIE_TO_KG;
weight += increment;
totalWeightGained += increment;
isForceStageUp = false;
if (is_over_capacity()) capacity += over_capacity_percent_get() * capacity * CAPACITY_OVER_BONUS;
calories = 0;
digestionProgress = 0;
digestionTimer = 0;
digestionCount++;
isDigesting = false;
isJustDigestionEnd = true;
}
void Character::tick()
{
Actor::tick();
isJustDigestionStart = false;
isJustDigestionEnd = false;
isJustStageUp = false;
isJustFinalThreshold = false;
auto animation = animation_get();
if (animation && !animation->isLoop && !isPlaying)
{
if (state == APPEAR) isJustAppeared = true;
if (state == STAGE_UP && weightStage == WEIGHT_STAGE_MAX - 1 && !isForceStageUp) isJustFinalThreshold = true;
state_set(IDLE, true);
}
if (isDigesting)
{
digestionTimer--;
if (digestionTimer <= 0) digestion_end();
}
else if (calories > 0)
{
digestionProgress += digestionRate;
if (digestionProgress >= DIGESTION_MAX) digestion_start();
}
if (math::random_percent_roll(BLINK_CHANCE)) blink();
auto progress = calories / max_capacity();
auto weightPercent = progress_to_next_weight_threshold_get();
auto capacityPercent = isDigesting ? ((float)digestionTimer / DIGESTION_TIMER_MAX) * progress : progress;
auto scaleBonus =
vec2(glm::min(SCALE_BONUS_MAX * ((capacityPercent * 0.5f) + (weightPercent * 0.5f)), SCALE_BONUS_MAX));
torsoCapacityScale.frame.scale = glm::max(vec2(), scaleBonus);
tailCapacityScale.frame.scale = glm::max(vec2(), scaleBonus);
if (!isForceStageUp)
{
for (int i = 0; i < WEIGHT_STAGE_MAX; i++)
{
if (weight >= WEIGHT_THRESHOLDS[i])
{
if (i == previousWeightStage + 1)
{
state_set(STAGE_UP);
isJustStageUp = true;
weightStage = i;
break;
}
}
else if (weight < WEIGHT_THRESHOLDS[i])
break;
}
}
if (weight > highestWeight) highestWeight = weight;
previousWeightStage = weightStage;
}
std::string Character::animation_name_convert(const std::string& name)
{
return std::format("{}{}", name, weightStage);
}
void Character::state_set(State state, bool isForce)
{
if (this->state == state && !isForce) return;
this->state = state;
AnimationType type{ANIMATION_NEUTRAL};
auto speedMultiplier = 1.0f;
switch (this->state)
{
case IDLE:
if (is_over_capacity())
type = ANIMATION_NEUTRAL_FULL;
else
type = ANIMATION_NEUTRAL;
break;
case EAGER:
type = ANIMATION_EAGER;
break;
case CRY:
type = ANIMATION_CRY;
break;
case SHOCKED:
type = ANIMATION_SHOCKED;
break;
case EAT:
type = ANIMATION_EAT;
speedMultiplier = eatSpeedMultiplier;
break;
case ANGRY:
type = ANIMATION_ANGRY;
break;
case PAT:
type = ANIMATION_PAT;
break;
case BURP_SMALL:
type = ANIMATION_BURP_SMALL;
break;
case BURP_BIG:
type = ANIMATION_BURP_BIG;
break;
case HEAD_RUB:
if (is_over_capacity())
type = ANIMATION_HEAD_RUB_FULL;
else
type = ANIMATION_HEAD_RUB;
break;
case BELLY_RUB:
if (is_over_capacity())
type = ANIMATION_BELLY_RUB_FULL;
else
type = ANIMATION_BELLY_RUB;
break;
case TAIL_RUB:
if (is_over_capacity())
type = ANIMATION_TAIL_RUB_FULL;
else
type = ANIMATION_TAIL_RUB;
break;
case STAGE_UP:
type = ANIMATION_STAGE_UP;
break;
default:
break;
};
play(animation_name_convert(ANIMATIONS[type]), PLAY, 0.0f, speedMultiplier);
}
}

View File

@@ -1,185 +0,0 @@
#pragma once
#include "resource/actor.h"
#include "types.h"
namespace game
{
class Character : public resource::Actor
{
public:
static constexpr auto LAYER_TAIL = "Tail";
static constexpr auto LAYER_TORSO = "Torso";
static constexpr auto LAYER_HEAD = "Head";
static constexpr auto LAYER_MOUTH = "Mouth";
static constexpr auto LAYER_TALK = "Talk";
static constexpr auto LAYER_BLINK = "Blink";
static constexpr auto NULL_MOUTH = "Mouth";
static constexpr auto NULL_HEAD = "Head";
static constexpr auto NULL_BELLY = "Belly";
static constexpr auto NULL_TAIL = "Tail";
static constexpr auto EVENT_EAT = "Eat";
static constexpr auto BLINK_CHANCE = 0.5f;
static constexpr auto PAT_CHANCE = 25.0f;
static constexpr auto BURP_SMALL_CHANCE = 20.0f;
static constexpr auto BURP_BIG_CHANCE = 10.0f;
static constexpr auto GURGLE_CHANCE = 0.1f;
static constexpr auto GURGLE_CHANCE_BONUS = 0.3f;
static constexpr auto CAPACITY_BASE = 500.0f;
static constexpr auto CALORIE_TO_KG = 1.0 / 1000.0f;
static constexpr auto CAPACITY_OVERSTUFFED_LIMIT_MULTIPLIER = 1.5f;
static constexpr auto SCALE_BONUS_MAX = 10.0f;
static constexpr auto PAT_LENGTH = 5;
static constexpr auto PAT_SCALE_RANGE = 5;
static constexpr auto EAT_SPEED_MULTIPLIER_MIN = 1.0f;
static constexpr auto EAT_SPEED_MULTIPLIER_MAX = 3.0f;
static constexpr auto DIGESTION_RATE_MIN = 0.00f;
static constexpr auto DIGESTION_RATE_MAX = 0.25f;
static constexpr auto DIGESTION_RATE_BASE = 0.05f;
static constexpr auto DIGESTION_MAX = 100.0f;
static constexpr auto DIGESTION_TIMER_MAX = 60;
static constexpr auto DIGESTION_RUB_BONUS = 0.01f;
static constexpr auto CAPACITY_OVER_BONUS = 0.1f;
static constexpr auto WEIGHT_STAGE_MAX = 5;
static constexpr float WEIGHT_THRESHOLDS[] = {
8.1f, 15.0f, 30.0f, 50.0f, 75.0f,
};
static constexpr auto MOUTH_SIZE = glm::vec2(50.0f, 50.0f);
enum State
{
APPEAR,
IDLE,
EAGER,
SHOCKED,
EAT,
CRY,
ANGRY,
BURP_SMALL,
BURP_BIG,
PAT,
HEAD_RUB,
BELLY_RUB,
TAIL_RUB,
STAGE_UP
};
#define ANIMATIONS_LIST \
X(ANIMATION_NEUTRAL, "Neutral") \
X(ANIMATION_NEUTRAL_FULL, "NeutralFull") \
X(ANIMATION_SHOCKED, "Shocked") \
X(ANIMATION_EAT, "Eat") \
X(ANIMATION_ANGRY, "Angry") \
X(ANIMATION_EAGER, "Eager") \
X(ANIMATION_CRY, "Cry") \
X(ANIMATION_PAT, "Pat") \
X(ANIMATION_BURP_SMALL, "BurpSmall") \
X(ANIMATION_BURP_BIG, "BurpBig") \
X(ANIMATION_HEAD_RUB, "HeadRub") \
X(ANIMATION_HEAD_RUB_FULL, "HeadRubFull") \
X(ANIMATION_BELLY_RUB, "BellyRub") \
X(ANIMATION_BELLY_RUB_FULL, "BellyRubFull") \
X(ANIMATION_TAIL_RUB, "TailRub") \
X(ANIMATION_TAIL_RUB_FULL, "TailRubFull") \
X(ANIMATION_STAGE_UP, "StageUp")
enum AnimationType
{
#define X(symbol, string) symbol,
ANIMATIONS_LIST
#undef X
};
static constexpr const char* ANIMATIONS[] = {
#define X(symbol, string) string,
ANIMATIONS_LIST
#undef X
};
float weight{WEIGHT_THRESHOLDS[0]};
int weightStage{0};
int previousWeightStage{0};
float highestWeight{};
float calories{};
float capacity{CAPACITY_BASE};
float digestionProgress{};
float digestionRate{DIGESTION_RATE_BASE};
float totalWeightGained{};
float totalCaloriesConsumed{};
int foodItemsEaten{};
int digestionCount{};
bool isJustDigestionStart{};
bool isJustDigestionEnd{};
bool isJustStageUp{};
bool isForceStageUp{};
bool isJustFinalThreshold{};
bool isFinalThresholdReached{};
bool isDigesting{};
bool isJustAppeared{};
int digestionTimer{};
int blinkLayerID{-1};
int headLayerID{-1};
int tailLayerID{-1};
int talkLayerID{-1};
int mouthLayerID{-1};
int torsoLayerID{-1};
int mouthNullID{-1};
int headNullID{-1};
int bellyNullID{-1};
int tailNullID{-1};
bool isFinishedFood{};
float eatSpeedMultiplier{EAT_SPEED_MULTIPLIER_MIN};
State state{APPEAR};
Override blinkOverride{};
Override talkOverride{};
Override torsoCapacityScale{};
Override tailCapacityScale{};
Character(anm2::Anm2*, glm::ivec2);
void talk();
void blink();
void tick();
void digestion_start();
void digestion_end();
void state_set(State, bool = false);
glm::vec4 mouth_rect_get();
glm::vec4 belly_rect_get();
glm::vec4 head_rect_get();
glm::vec4 tail_rect_get();
float weight_get(MeasurementSystem = METRIC);
float weight_threshold_get(int, MeasurementSystem = METRIC);
float weight_threshold_current_get(MeasurementSystem = METRIC);
float weight_threshold_next_get(MeasurementSystem = METRIC);
float progress_to_next_weight_threshold_get();
float over_capacity_percent_get();
float over_capacity_calories_get();
float digestion_rate_second_get();
bool is_max_capacity();
bool is_over_capacity();
float max_capacity();
std::string animation_name_convert(const std::string&);
};
}

View File

@@ -1,13 +0,0 @@
#include "cursor.h"
#include "util/imgui_.h"
using namespace game::util;
using namespace game::anm2;
using namespace glm;
namespace game
{
Cursor::Cursor(Anm2* anm2) : Actor(anm2, glm::vec2()) {}
void Cursor::update() { position = imgui::to_vec2(ImGui::GetMousePos()); }
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include "resource/actor.h"
namespace game
{
class Cursor : public resource::Actor
{
public:
static constexpr const char* ANIMATION_DEFAULT = "Default";
static constexpr const char* ANIMATION_HOVER = "Hover";
static constexpr const char* ANIMATION_GRAB = "Grab";
static constexpr const char* ANIMATION_RUB = "Rub";
Cursor(anm2::Anm2* anm2);
void update();
};
}

450
src/entity/actor.cpp Normal file
View File

@@ -0,0 +1,450 @@
#include "actor.hpp"
#include "../util/map.hpp"
#include "../util/math.hpp"
#include "../util/unordered_map.hpp"
#include "../util/vector.hpp"
#include <cstddef>
#include <glm/glm.hpp>
#include "../log.hpp"
#include <imgui.h>
using namespace glm;
using namespace game::util;
using namespace game::resource::xml;
namespace game::entity
{
Actor::Override::Override(int _id, Anm2::Type _type, Actor::Override::Mode _mode, FrameOptional _frame,
std::optional<float> _time, Actor::Override::Function _function, float _cycles)
: id(_id), type(_type), mode(_mode), frame(_frame), time(_time), function(_function), cycles(_cycles)
{
frameBase = _frame;
timeStart = _time;
}
Actor::Actor(const Actor&) = default;
Actor::Actor(Actor&&) noexcept = default;
Actor& Actor::operator=(const Actor&) = default;
Actor& Actor::operator=(Actor&&) noexcept = default;
Actor::Actor(Anm2 _anm2, vec2 _position, Mode mode, float time, int animationIndex) : Anm2(_anm2), position(_position)
{
this->mode = mode;
this->startTime = time;
if (animationIndex != -1)
play(animationIndex, mode, time);
else
play_default_animation(mode, time);
}
Anm2::Animation* Actor::animation_get(int index)
{
if (index == -1) index = animationIndex;
if (animationMapReverse.contains(index)) return &animations[index];
return nullptr;
}
Anm2::Animation* Actor::animation_get(const std::string& name)
{
if (animationMap.contains(name)) return &animations[animationMap[name]];
return nullptr;
}
bool Actor::is_playing(const std::string& name)
{
if (name.empty())
return state == PLAYING;
else
return state == PLAYING && animationMap[name] == animationIndex;
}
int Actor::animation_index_get(const std::string& name)
{
if (animationMap.contains(name)) return animationMap[name];
return -1;
}
Anm2::Item* Actor::item_get(Anm2::Type type, int id, int animationIndex)
{
if (animationIndex == -1) animationIndex = this->animationIndex;
if (auto animation = animation_get(animationIndex))
{
switch (type)
{
case Anm2::ROOT:
return &animation->rootAnimation;
break;
case Anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case Anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case Anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
int Actor::item_length(Anm2::Item* item)
{
if (!item) return -1;
int duration{};
for (auto& frame : item->frames)
duration += frame.duration;
return duration;
}
Anm2::Frame Actor::frame_generate(Anm2::Item& item, float time, Anm2::Type type, int id)
{
Anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
time = time < 0.0f ? 0.0f : time;
Anm2::Frame* frameNext = nullptr;
Anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < (int)item.frames.size(); i++)
{
Anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
auto override_handle = [&](Anm2::Frame& overrideFrame)
{
for (auto& override : overrides)
{
if (override.type != type) continue;
if (override.id != id) continue;
auto& source = override.frame;
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;
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;
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;
break;
default:
break;
}
}
};
override_handle(frame);
if (frameNext) override_handle(frameNextCopy);
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
}
return frame;
}
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
{
if (!vector::in_bounds(animations, index)) return;
if (mode != PLAY_FORCE && index == animationIndex) return;
this->playedEventID = -1;
this->playedTriggers.clear();
this->speedMultiplier = speedMultiplier;
this->animationIndex = index;
this->time = time;
if (mode == PLAY) state = PLAYING;
}
void Actor::queue_play(QueuedPlay newQueuedPlay) { queuedPlay = newQueuedPlay; }
void Actor::queue_default_animation() { queue_play({defaultAnimation}); }
void Actor::play(const std::string& name, Mode mode, float time, float speedMultiplier)
{
if (animationMap.contains(name))
play(animationMap.at(name), mode, time, speedMultiplier);
else
logger.error(std::string("Animation \"" + name + "\" does not exist! Unable to play!"));
}
void Actor::play_default_animation(Mode mode, float time, float speedMultiplier)
{
play(defaultAnimationID, mode, time, speedMultiplier);
}
void Actor::tick()
{
if (state == Actor::STOPPED)
{
if (!nextQueuedPlay.empty())
{
queuedPlay = nextQueuedPlay;
queuedPlay.isPlayAfterAnimation = false;
nextQueuedPlay = QueuedPlay{};
}
currentQueuedPlay = QueuedPlay{};
}
if (auto animation = animation_get(); animation && animation->isLoop) currentQueuedPlay = QueuedPlay{};
if (!queuedPlay.empty())
{
auto& index = animationMap.at(queuedPlay.animation);
if (queuedPlay.isPlayAfterAnimation)
nextQueuedPlay = queuedPlay;
else if (index != animationIndex && currentQueuedPlay.isInterruptible)
{
play(queuedPlay.animation, queuedPlay.mode, queuedPlay.time, queuedPlay.speedMultiplier);
currentQueuedPlay = queuedPlay;
}
queuedPlay = QueuedPlay{};
}
auto animation = animation_get();
if (!animation || animation->frameNum == 1 || mode == SET || state == STOPPED) return;
playedEventID = -1;
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
auto id = trigger.soundIDs[(int)math::random_max(trigger.soundIDs.size())];
if (auto sound = map::find(sounds, id)) sound->audio.play();
playedTriggers.insert((int)trigger.atFrame);
playedEventID = trigger.eventID;
}
}
auto increment = (fps / TICK_RATE) * speedMultiplier;
time += increment;
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
state = STOPPED;
playedTriggers.clear();
}
for (int i = 0; i < (int)overrides.size(); i++)
{
auto& override_ = overrides[i];
if (override_.function) override_.function(override_);
if (override_.time.has_value())
{
*override_.time -= 1.0f;
if (*override_.time <= 0.0f) overrides.erase(overrides.begin() + i++);
}
}
}
glm::vec4 Actor::null_frame_rect(int nullID)
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
if (nullID == -1) return glm::vec4(NAN);
auto item = item_get(Anm2::NULL_, nullID);
if (!item) return glm::vec4(NAN);
auto animation = animation_get();
if (!animation) return glm::vec4(NAN);
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
auto frame = frame_generate(*item, time, Anm2::NULL_, nullID);
if (!frame.isVisible) return glm::vec4(NAN);
auto rootModel =
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
auto frameModel = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
auto model = rootModel * frameModel;
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
for (auto& corner : CORNERS)
{
vec4 world = model * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
}
return glm::vec4(minX, minY, maxX - minX, maxY - minY);
}
void Actor::render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas& canvas)
{
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
auto rootModel =
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(layers, i);
if (!layer) continue;
auto spritesheet = map::find(spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time, Anm2::LAYER, i);
if (!frame.isVisible) continue;
auto model =
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto tint = frame.tint * root.tint;
auto colorOffset = frame.colorOffset + root.colorOffset;
auto inset = vec2(0);
auto uvMin = (frame.crop + inset) / vec2(texture.size);
auto uvMax = (frame.crop + frame.size - inset) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
}
if (isShowNulls)
{
for (int i = 0; i < (int)animation->nullAnimations.size(); i++)
{
auto& nullAnimation = animation->nullAnimations[i];
if (!nullAnimation.isVisible) continue;
auto frame = frame_generate(nullAnimation, time, Anm2::NULL_, i);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
model = rootModel * model;
canvas.rect_render(rectShader, model);
}
}
}
vec4 Actor::rect()
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
auto animation = animation_get();
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
bool any = false;
if (!animation) return vec4(-NAN);
for (float t = 0.0f; t < (float)animation->frameNum; t += 1.0f)
{
mat4 transform(1.0f);
auto root = frame_generate(animation->rootAnimation, t, Anm2::ROOT);
transform *=
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& [id, layerAnimation] : animation->layerAnimations)
{
if (!layerAnimation.isVisible) continue;
auto frame = frame_generate(layerAnimation, t, Anm2::LAYER, id);
if (frame.size == vec2() || !frame.isVisible) continue;
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
math::to_unit(frame.scale), frame.rotation);
for (auto& corner : CORNERS)
{
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
any = true;
}
}
}
if (!any) return vec4(-NAN);
return {minX, minY, maxX - minX, maxY - minY};
}
bool Actor::is_animation_finished()
{
if (auto animation = animation_get())
{
if (animation->isLoop) return true;
if (time > animation->frameNum) return true;
}
return false;
}
void Actor::consume_played_event() { playedEventID = -1; }
};

113
src/entity/actor.hpp Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include <unordered_set>
#include "../canvas.hpp"
#include "../resource/xml/anm2.hpp"
namespace game::entity
{
class Actor : public resource::xml::Anm2
{
public:
static constexpr auto TICK_RATE = 30.0f;
enum Mode
{
PLAY,
PLAY_FORCE,
SET,
};
enum State
{
STOPPED,
PLAYING
};
class Override
{
private:
public:
enum Mode
{
SET,
ADD
};
using Function = void (*)(Override&);
int id{-1};
Anm2::Type type{Anm2::NONE};
Mode mode{SET};
FrameOptional frame{};
std::optional<float> time{};
Function function{nullptr};
FrameOptional frameBase{};
std::optional<float> timeStart{};
float cycles{};
Override() = default;
Override(int, Anm2::Type, Mode, FrameOptional = {}, std::optional<float> = std::nullopt, Function = nullptr,
float = 0);
};
struct QueuedPlay
{
std::string animation{};
float time{};
float speedMultiplier{1.0f};
Mode mode{PLAY};
bool isInterruptible{true};
bool isPlayAfterAnimation{false};
inline bool empty() { return animation.empty(); };
};
State state{STOPPED};
Mode mode{PLAY};
glm::vec2 position{};
float time{};
bool isShowNulls{};
int animationIndex{-1};
int playedEventID{-1};
float startTime{};
float speedMultiplier{};
QueuedPlay queuedPlay{};
QueuedPlay currentQueuedPlay{};
QueuedPlay nextQueuedPlay{};
std::unordered_set<int> playedTriggers{};
std::vector<Override> overrides{};
Actor() = default;
Actor(const Actor&);
Actor(Actor&&) noexcept;
Actor& operator=(const Actor&);
Actor& operator=(Actor&&) noexcept;
Actor(resource::xml::Anm2, glm::vec2 position = {}, Mode = PLAY, float time = 0.0f, int animationIndex = -1);
bool is_playing(const std::string& name = {});
glm::vec4 null_frame_rect(int = -1);
glm::vec4 rect();
int animation_index_get(const std::string&);
int item_length(resource::xml::Anm2::Item*);
resource::xml::Anm2::Animation* animation_get(const std::string&);
resource::xml::Anm2::Animation* animation_get(int = -1);
resource::xml::Anm2::Frame frame_generate(resource::xml::Anm2::Item&, float, resource::xml::Anm2::Type,
int id = -1);
resource::xml::Anm2::Item* item_get(resource::xml::Anm2::Type, int = -1, int = -1);
void consume_played_event();
void play(const std::string& animation, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
void play(int index, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
void play_default_animation(Mode = PLAY, float = 0.0f, float = 1.0f);
void queue_default_animation();
void queue_play(QueuedPlay);
bool is_animation_finished();
void render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas&);
void tick();
};
}

372
src/entity/character.cpp Normal file
View File

@@ -0,0 +1,372 @@
#include "character.hpp"
#include <format>
#include "../util/math.hpp"
#include "../util/vector.hpp"
using namespace game::util;
using namespace glm;
namespace game::entity
{
Character::Character(const Character&) = default;
Character::Character(Character&&) noexcept = default;
Character& Character::operator=(const Character&) = default;
Character& Character::operator=(Character&&) noexcept = default;
Character::Character(resource::xml::Character& _data, glm::ivec2 _position) : Actor(_data.anm2, _position)
{
data = _data;
auto& save = data.save;
auto saveIsValid = save.is_valid();
capacity = saveIsValid ? save.capacity : data.capacity;
weight = saveIsValid ? save.weight : data.weight;
digestionRate = saveIsValid ? save.digestionRate : data.digestionRate;
eatSpeed = saveIsValid ? save.eatSpeed : data.eatSpeed;
calories = saveIsValid ? save.calories : 0;
isDigesting = saveIsValid ? save.isDigesting : false;
digestionProgress = saveIsValid ? save.digestionProgress : 0;
digestionTimer = saveIsValid ? save.digestionTimer : 0;
auto& talkSource = data.talkOverride.layerSource;
auto& talkDestination = data.talkOverride.layerDestination;
talkOverrideID = vector::push_index(overrides, Actor::Override(talkDestination, Anm2::LAYER, Override::SET));
for (auto& animation : animations)
{
if (!animation.layerAnimations.contains(talkSource))
animationTalkDurations.emplace_back(-1);
else
animationTalkDurations.emplace_back(item_length(&animation.layerAnimations.at(talkSource)));
}
auto& blinkSource = data.blinkOverride.layerSource;
auto& blinkDestination = data.blinkOverride.layerDestination;
blinkOverrideID = vector::push_index(overrides, Actor::Override(blinkDestination, Anm2::LAYER, Override::SET));
for (auto& animation : animations)
{
if (!animation.layerAnimations.contains(blinkSource))
animationBlinkDurations.emplace_back(-1);
else
animationBlinkDurations.emplace_back(item_length(&animation.layerAnimations.at(blinkSource)));
}
for (int i = 0; i < (int)data.expandAreas.size(); i++)
{
auto& expandArea = data.expandAreas[i];
expandAreaOverrideLayerIDs[i] =
vector::push_index(overrides, Actor::Override(expandArea.layerID, Anm2::LAYER, Override::ADD));
expandAreaOverrideNullIDs[i] =
vector::push_index(overrides, Actor::Override(expandArea.nullID, Anm2::NULL_, Override::ADD));
}
for (int i = 0; i < (int)data.interactAreas.size(); i++)
{
auto& interactArea = data.interactAreas[i];
if (interactArea.layerID != -1)
interactAreaOverrides[i] = Actor::Override(interactArea.layerID, Anm2::LAYER, Override::ADD);
}
stage = stage_get();
expand_areas_apply();
}
float Character::weight_get(measurement::System system)
{
return system == measurement::IMPERIAL ? weight * measurement::KG_TO_LB : weight;
}
int Character::stage_from_weight_get(float checkWeight) const
{
if (data.stages.empty()) return 0;
if (checkWeight <= data.weight) return 0;
for (int i = 0; i < (int)data.stages.size(); i++)
if (checkWeight < data.stages[i].threshold) return i;
return stage_max_get();
}
int Character::stage_get() const { return stage_from_weight_get(weight); }
int Character::stage_max_get() const { return data.stages.size(); }
float Character::stage_threshold_get(int stage, measurement::System system) const
{
if (stage == -1) stage = this->stage;
float threshold = data.weight;
if (!data.stages.empty())
{
if (stage <= 0)
threshold = data.weight;
else if (stage >= stage_max_get())
threshold = data.stages.back().threshold;
else
threshold = data.stages[stage - 1].threshold;
}
return system == measurement::IMPERIAL ? threshold * measurement::KG_TO_LB : threshold;
}
float Character::stage_threshold_next_get(measurement::System system) const
{
return stage_threshold_get(stage + 1, system);
}
float Character::stage_progress_get()
{
auto currentStage = stage_get();
if (currentStage >= stage_max_get()) return 1.0f;
auto currentThreshold = stage_threshold_get(currentStage);
auto nextThreshold = stage_threshold_get(currentStage + 1);
if (nextThreshold <= currentThreshold) return 1.0f;
return (weight - currentThreshold) / (nextThreshold - currentThreshold);
}
float Character::digestion_rate_get() { return digestionRate * 60; }
float Character::max_capacity() const { return capacity * data.capacityMaxMultiplier; }
bool Character::is_over_capacity() const { return calories > capacity; }
bool Character::is_max_capacity() const { return calories >= max_capacity(); }
float Character::capacity_percent_get() const { return calories / max_capacity(); }
std::string Character::animation_name_convert(const std::string& name) { return std::format("{}{}", name, stage); }
void Character::play_convert(const std::string& animation, Mode mode, float time, float speedMultiplier)
{
play(animation_name_convert(animation), mode, time, speedMultiplier);
}
void Character::expand_areas_apply()
{
auto stageProgress = stage_progress_get();
auto capacityProgress = isDigesting
? (float)calories / max_capacity() * (float)digestionTimer / data.digestionTimerMax
: calories / max_capacity();
for (int i = 0; i < (int)data.expandAreas.size(); i++)
{
auto& expandArea = data.expandAreas[i];
auto& overrideLayer = overrides[expandAreaOverrideLayerIDs[i]];
auto& overrideNull = overrides[expandAreaOverrideNullIDs[i]];
auto stageScaleAdd = ((expandArea.scaleAdd * stageProgress) * 0.5f);
auto capacityScaleAdd = ((expandArea.scaleAdd * capacityProgress) * 0.5f);
auto scaleAdd =
glm::clamp(glm::vec2(), glm::vec2(stageScaleAdd + capacityScaleAdd), glm::vec2(expandArea.scaleAdd));
overrideLayer.frame.scale = scaleAdd;
overrideNull.frame.scale = scaleAdd;
}
}
void Character::update()
{
isJustStageUp = false;
isJustStageFinal = false;
isJustDigested = false;
}
void Character::tick()
{
if (state == Actor::STOPPED)
{
if (isStageUp)
{
if (stage >= (int)data.stages.size())
isJustStageFinal = true;
else
isJustStageUp = true;
isStageUp = false;
}
if (nextQueuedPlay.empty()) queue_idle_animation();
}
Actor::tick();
if (isDigesting)
{
digestionTimer--;
if (digestionTimer <= 0)
{
auto increment = calories * data.caloriesToKilogram;
if (is_over_capacity())
{
auto capacityMaxCalorieDifference = (calories - capacity);
auto overCapacityPercent = capacityMaxCalorieDifference / (max_capacity() - capacity);
auto capacityIncrement =
(overCapacityPercent * data.capacityIfOverStuffedOnDigestBonus) * capacityMaxCalorieDifference;
capacity = glm::clamp(data.capacityMin, capacity + capacityIncrement, data.capacityMax);
}
totalCaloriesConsumed += calories;
calories = 0;
if (auto nextStage = stage_from_weight_get(weight + increment); nextStage > stage_from_weight_get(weight))
{
queuedPlay = QueuedPlay{};
nextQueuedPlay = QueuedPlay{};
currentQueuedPlay = QueuedPlay{};
play_convert(data.animations.stageUp);
stage = nextStage;
isStageUp = true;
}
else
isJustDigested = true;
weight = glm::clamp(data.weightMin, weight + increment, data.weightMax);
isDigesting = false;
digestionTimer = data.digestionTimerMax;
digestionProgress = 0;
}
}
else
{
if (calories > 0) digestionProgress += digestionRate;
if (digestionProgress >= DIGESTION_MAX)
{
isDigesting = true;
digestionTimer = data.digestionTimerMax;
data.sounds.digest.play();
}
}
if (math::random_percent_roll(
math::to_percent(data.gurgleChance * (capacity_percent_get() * data.gurgleCapacityMultiplier))))
data.sounds.gurgle.play();
stage = stage_get();
expand_areas_apply();
auto& talkOverride = overrides[talkOverrideID];
if (isTalking)
{
auto talk_reset = [&]()
{
isTalking = false;
talkTimer = 0.0f;
talkOverride.frame = FrameOptional();
};
auto& id = data.talkOverride.layerSource;
auto& layerAnimations = animation_get()->layerAnimations;
if (layerAnimations.contains(id) && animationTalkDurations.at(animationIndex) > -1)
{
auto& layerAnimation = layerAnimations.at(data.talkOverride.layerSource);
if (!layerAnimation.frames.empty())
{
auto frame = frame_generate(layerAnimation, talkTimer, Anm2::LAYER, id);
talkOverride.frame.crop = frame.crop;
talkOverride.frame.size = frame.size;
talkOverride.frame.pivot = frame.pivot;
talkTimer += 1.0f;
if (talkTimer > animationTalkDurations.at(animationIndex)) talkTimer = 0.0f;
}
else
talk_reset();
}
else
talk_reset();
}
else
talkOverride.frame = {};
auto& blinkOverride = overrides[blinkOverrideID];
if (auto blinkDuration = animationBlinkDurations[animationIndex]; blinkDuration != 1)
{
if (math::random_percent_roll(data.blinkChance)) isBlinking = true;
if (isBlinking)
{
auto blink_reset = [&]()
{
isBlinking = false;
blinkTimer = 0.0f;
blinkOverride.frame = FrameOptional();
};
auto& id = data.blinkOverride.layerSource;
auto& layerAnimations = animation_get()->layerAnimations;
if (layerAnimations.contains(id))
{
auto& layerAnimation = layerAnimations.at(data.blinkOverride.layerSource);
if (!layerAnimation.frames.empty())
{
auto frame = frame_generate(layerAnimation, blinkTimer, Anm2::LAYER, id);
blinkOverride.frame.crop = frame.crop;
blinkOverride.frame.size = frame.size;
blinkOverride.frame.pivot = frame.pivot;
blinkTimer += 1.0f;
if (blinkTimer >= blinkDuration) blink_reset();
}
else
blink_reset();
}
else
blink_reset();
}
}
}
void Character::queue_play(QueuedPlay play)
{
queuedPlay = play;
queuedPlay.animation = animation_name_convert(queuedPlay.animation);
}
void Character::queue_idle_animation()
{
if (data.animations.idle.empty()) return;
queue_play(
{is_over_capacity() && !data.animations.idleFull.empty() ? data.animations.idleFull : data.animations.idle});
}
void Character::queue_interact_area_animation(resource::xml::Character::InteractArea& interactArea)
{
if (interactArea.animation.empty()) return;
queue_play({is_over_capacity() && !interactArea.animationFull.empty() ? interactArea.animationFull
: interactArea.animation});
}
void Character::spritesheet_set(SpritesheetType type)
{
switch (type)
{
case NORMAL:
spritesheets.at(data.alternateSpritesheet.id).texture =
data.anm2.spritesheets.at(data.alternateSpritesheet.id).texture;
break;
case ALTERNATE:
spritesheets.at(data.alternateSpritesheet.id).texture = data.alternateSpritesheet.texture;
break;
default:
break;
}
spritesheetType = type;
}
}

92
src/entity/character.hpp Normal file
View File

@@ -0,0 +1,92 @@
#pragma once
#include "../resource/xml/character.hpp"
#include "../util/measurement.hpp"
#include "actor.hpp"
namespace game::entity
{
class Character : public Actor
{
public:
static constexpr auto DIGESTION_MAX = 100.0f;
enum SpritesheetType
{
NORMAL,
ALTERNATE
};
resource::xml::Character data;
float weight{};
int stage{0};
float calories{};
float capacity{};
float digestionProgress{};
float digestionRate{0.05f};
int digestionTimer{};
bool isDigesting{};
bool isJustDigested{};
float eatSpeed{1.0f};
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int talkOverrideID{};
float talkTimer{};
bool isTalking{};
std::vector<int> animationTalkDurations{};
int blinkOverrideID{};
float blinkTimer{};
bool isBlinking{};
std::vector<int> animationBlinkDurations{};
bool isStageUp{};
bool isJustStageUp{};
bool isJustStageFinal{};
SpritesheetType spritesheetType{};
std::map<int, Override> interactAreaOverrides{};
std::unordered_map<int, int> expandAreaOverrideLayerIDs{};
std::unordered_map<int, int> expandAreaOverrideNullIDs{};
Character() = default;
Character(const Character&);
Character(Character&&) noexcept;
Character& operator=(const Character&);
Character& operator=(Character&&) noexcept;
Character(resource::xml::Character&, glm::ivec2);
float weight_get(util::measurement::System = util::measurement::METRIC);
float digestion_rate_get();
float capacity_percent_get() const;
float max_capacity() const;
bool is_over_capacity() const;
bool is_max_capacity() const;
int stage_get() const;
int stage_from_weight_get(float weight) const;
int stage_max_get() const;
float stage_progress_get();
float stage_threshold_get(int stage = -1, util::measurement::System = util::measurement::METRIC) const;
float stage_threshold_next_get(util::measurement::System = util::measurement::METRIC) const;
void expand_areas_apply();
void spritesheet_set(SpritesheetType);
void update();
void tick();
void play_convert(const std::string&, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
void queue_idle_animation();
void queue_interact_area_animation(resource::xml::Character::InteractArea&);
void queue_play(QueuedPlay);
std::string animation_name_convert(const std::string& name);
};
}

22
src/entity/cursor.cpp Normal file
View File

@@ -0,0 +1,22 @@
#include "cursor.hpp"
#include "../util/imgui.hpp"
using namespace game::util;
using namespace glm;
namespace game::entity
{
Cursor::Cursor(Anm2& anm2) : Actor(anm2, imgui::to_vec2(ImGui::GetMousePos())) {}
void Cursor::tick()
{
Actor::tick();
queue_default_animation();
}
void Cursor::update()
{
state = DEFAULT;
position = imgui::to_vec2(ImGui::GetMousePos());
}
}

26
src/entity/cursor.hpp Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include "../util/interact_type.hpp"
#include "actor.hpp"
namespace game::entity
{
class Cursor : public Actor
{
public:
enum State
{
DEFAULT,
HOVER,
ACTION
};
State state{DEFAULT};
InteractType mode{InteractType::RUB};
Cursor() = default;
Cursor(resource::xml::Anm2&);
void tick();
void update();
};
}

31
src/entity/item.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "item.hpp"
#include <imgui.h>
#include "../util/vector.hpp"
using game::resource::xml::Anm2;
using namespace game::util;
using namespace glm;
namespace game::entity
{
Item::Item(Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _chewCount, int _animationIndex, glm::vec2 _velocity,
float _rotation)
: Actor(_anm2, _position, SET, 0.0f, _animationIndex), schemaID(_schemaID), chewCount(_chewCount),
velocity(_velocity)
{
rotationOverrideID =
vector::push_index(overrides, Override(-1, Anm2::ROOT, Override::SET, Anm2::FrameOptional{.rotation = _rotation}));
}
void Item::update()
{
auto& rotationOverride = overrides[rotationOverrideID];
position += velocity;
*rotationOverride.frame.rotation += angularVelocity;
}
}

26
src/entity/item.hpp Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include "../resource/xml/item.hpp"
#include "actor.hpp"
namespace game::entity
{
class Item : public Actor
{
public:
bool isToBeDeleted{};
bool isHeld{};
int schemaID{};
int rotationOverrideID{};
int chewCount{};
glm::vec2 velocity{};
float angularVelocity{};
Item(resource::xml::Anm2, glm::ivec2 position, int id, int chewCount = 0, int animationIndex = -1,
glm::vec2 velocity = {}, float rotation = 0.0f);
void update();
};
}

View File

@@ -1,13 +0,0 @@
#pragma once
#include "types.h"
namespace game
{
class GameData
{
public:
MeasurementSystem measurementSystem{MeasurementSystem::METRIC};
int volume{100};
};
}

View File

@@ -1,129 +0,0 @@
#include "item.h"
#include "imgui.h"
#include "util/math_.h"
using namespace game::anm2;
using namespace game::util;
using namespace glm;
namespace game
{
Item* Item::heldItem = nullptr;
Item* Item::heldItemPrevious = nullptr;
Item* Item::hoveredItem = nullptr;
Item* Item::hoveredItemPrevious = nullptr;
Item* Item::queuedReturnItem = nullptr;
std::array<Item::Pool, Item::RARITY_COUNT> rarity_pools_get()
{
std::array<Item::Pool, Item::RARITY_COUNT> newPools{};
for (auto& pool : newPools)
pool.clear();
for (int i = 0; i < Item::ITEM_COUNT; i++)
{
auto& rarity = Item::RARITIES[i];
newPools[rarity].emplace_back((Item::Type)i);
}
return newPools;
}
const std::array<Item::Pool, Item::RARITY_COUNT> Item::pools = rarity_pools_get();
Item::Item(Anm2* _anm2, glm::ivec2 _position, Type _type) : Actor(_anm2, _position, SET, (float)_type)
{
this->type = _type;
}
void Item::tick() { Actor::tick(); }
void Item::update(Resources& resources)
{
auto bounds = ivec4(position.x - SIZE * 0.5f, position.y - SIZE * 0.5f, SIZE, SIZE);
auto mousePos = ivec2(ImGui::GetMousePos().x, ImGui::GetMousePos().y);
if (isHeld)
{
position = mousePos - ivec2(holdOffset);
delta = previousPosition - position;
velocity *= VELOCITY_HOLD_MULTIPLIER;
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
auto power = fabs(delta.x) + fabs(delta.y);
heldItem = nullptr;
if (power > THROW_THRESHOLD)
resources.sound_play(audio::THROW);
else
resources.sound_play(audio::RELEASE);
velocity += delta;
isHeld = false;
}
}
else if (math::is_point_in_rect(bounds, mousePos))
{
hoveredItem = this;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !heldItem)
{
heldItem = this;
resources.sound_play(audio::GRAB);
isHeld = true;
holdOffset = mousePos - ivec2(position);
}
else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right))
{
queuedReturnItem = this;
}
}
if (!isHeld) velocity.y += GRAVITY;
position += velocity;
if (position.x < BOUNDS.x || position.x > BOUNDS.z)
{
velocity.x = -velocity.x;
velocity.x *= FRICTION;
if (fabs(velocity.x) > BOUNCE_SOUND_THRESHOLD) resources.sound_play(audio::BOUNCE);
}
if (position.y < BOUNDS.y || position.y > BOUNDS.w)
{
velocity.y = -velocity.y;
velocity *= FRICTION;
if (fabs(velocity.y) > BOUNCE_SOUND_THRESHOLD) resources.sound_play(audio::BOUNCE);
}
position = glm::clamp(vec2(BOUNDS.x, BOUNDS.y), position, vec2(BOUNDS.z, BOUNDS.w));
previousPosition = position;
}
void Item::state_set(State state)
{
this->state = state;
switch (this->state)
{
case DEFAULT:
play("Default", SET, (float)type);
break;
case CHEW_1:
play("Chew1", SET, (float)type);
break;
case CHEW_2:
play("Chew2", SET, (float)type);
break;
default:
break;
};
}
}

View File

@@ -1,241 +0,0 @@
#pragma once
#include "resource/actor.h"
#include "resources.h"
namespace game
{
class Item : public resource::Actor
{
public:
static constexpr auto VELOCITY_HOLD_MULTIPLIER = 0.50f;
static constexpr auto VELOCITY_HOLD_BOOST = 2.5f;
static constexpr auto BOUNCE_SOUND_THRESHOLD = 2.5f;
static constexpr auto THROW_MULTIPLIER = 1.0f;
static constexpr auto THROW_THRESHOLD = 50.0f;
static constexpr auto FRICTION = 0.75f;
static constexpr auto GRAVITY = 0.50f;
static constexpr auto SIZE = 72.0f;
static constexpr auto CHEW_COUNT_MAX = 2;
static constexpr auto DEPLOYED_MAX = 10;
static constexpr auto ANIMATION_STATE = "State";
static constexpr glm::vec4 BOUNDS = {50, 100, 475, 500};
static constexpr auto SPAWN_X_MIN = BOUNDS.x;
static constexpr auto SPAWN_X_MAX = BOUNDS.z + BOUNDS.x;
static constexpr auto SPAWN_Y_MIN = BOUNDS.y;
static constexpr auto SPAWN_Y_MAX = BOUNDS.w + BOUNDS.y;
#define CATEGORIES \
X(INVALID, "Invalid") \
X(FOOD, "Food") \
X(UTILITY, "Utility")
enum Category
{
#define X(symbol, name) symbol,
CATEGORIES
#undef X
};
static constexpr const char* CATEGORY_NAMES[] = {
#define X(symbol, name) name,
CATEGORIES
#undef X
};
#undef CATEGORIES
#define RARITIES \
X(NO_RARITY, "No Rarity", 0.0f) \
X(COMMON, "Common", 1.0f) \
X(UNCOMMON, "Uncommon", 0.75f) \
X(RARE, "Rare", 0.5f) \
X(EPIC, "Epic", 0.25f) \
X(LEGENDARY, "Legendary", 0.125f) \
X(IMPOSSIBLE, "???", 0.001f) \
X(SPECIAL, "Special", 0.0f)
enum Rarity
{
#define X(symbol, name, chance) symbol,
RARITIES
#undef X
RARITY_COUNT
};
static constexpr const char* RARITY_NAMES[] = {
#define X(symbol, name, chance) name,
RARITIES
#undef X
};
static constexpr float RARITY_CHANCES[] = {
#define X(symbol, name, chance) chance,
RARITIES
#undef X
};
#undef RARITIES
#define FLAVORS \
X(FLAVORLESS, "None") \
X(SWEET, "Sweet") \
X(BITTER, "Bitter") \
X(SPICY, "Spicy") \
X(MINT, "Mint") \
X(CITRUS, "Citrus") \
X(MOCHA, "Mocha") \
X(SPICE, "Spice")
enum Flavor
{
#define X(symbol, name) symbol,
FLAVORS
#undef X
FLAVOR_COUNT
};
static constexpr const char* FLAVOR_NAMES[] = {
#define X(symbol, name) name,
FLAVORS
#undef X
};
#undef FLAVORS
// clang-format off
#define ITEMS \
X(NONE, INVALID, FLAVORLESS, NO_RARITY, "", "", 0, 0, 0) \
X(POKE_PUFF_BASIC_SWEET, FOOD, SWEET, COMMON, "Poké Puff (Basic, Sweet)", "A basic sweet Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_MINT, FOOD, MINT, COMMON, "Poké Puff (Basic, Mint)", "A basic minty Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_CITRUS, FOOD, CITRUS, COMMON, "Poké Puff (Basic, Citrus)", "A basic citrusy Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_MOCHA, FOOD, MOCHA, COMMON, "Poké Puff (Basic, Mocha)", "A basic mocha Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_SPICE, FOOD, SPICE, COMMON, "Poké Puff (Basic, Spice)", "A basic spice Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_FROSTED_SWEET, FOOD, SWEET, UNCOMMON, "Poké Puff (Frosted, Sweet)", "A frosted sweet Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_MINT, FOOD, MINT, UNCOMMON, "Poké Puff (Frosted, Mint)", "A frosted minty Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_CITRUS, FOOD, MINT, UNCOMMON, "Poké Puff (Frosted, Citrus)", "A frosted citrusy Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_MOCHA, FOOD, MOCHA, UNCOMMON, "Poké Puff (Frosted, Mocha)", "A frosted mocha Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_SPICE, FOOD, SPICE, UNCOMMON, "Poké Puff (Frosted, Spice)", "A frosted spice Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FANCY_SWEET, FOOD, SWEET, RARE, "Poké Puff (Fancy, Sweet)", "A fancy sweet Poké Puff, adorned with a cherry.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_MINT, FOOD, MINT, RARE, "Poké Puff (Fancy, Mint)", "A fancy minty Poké Puff, adorned with a crescent moon-shaped candy.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_CITRUS, FOOD, CITRUS, RARE, "Poké Puff (Fancy, Citrus)", "A fancy citrus Poké Puff, adorned with an orange slice.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_MOCHA, FOOD, MOCHA, RARE, "Poké Puff (Fancy, Mocha)", "A fancy mocha Poké Puff, adorned with a morsel of white chocolate.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_SPICE, FOOD, SPICE, RARE, "Poké Puff (Fancy, Spice)", "A fancy spice Poké Puff, adorned with a morsel of dark chocolate.", 500.0f, 0, 0) \
X(POKE_PUFF_DELUXE_SWEET, FOOD, SWEET, EPIC, "Poké Puff (Deluxe, Sweet)", "A deluxe sweet Poké Puff; frosted and adorned with a cherry.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_MINT, FOOD, MINT, EPIC, "Poké Puff (Deluxe, Mint)", "A deluxe minty Poké Puff; frosted and adorned with a crescent moon-shapedd candy.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_CITRUS, FOOD, CITRUS, EPIC, "Poké Puff (Deluxe, Citrus)", "A deluxe citrusy Poké Puff; frosted and adorned with an orange slice.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_MOCHA, FOOD, MOCHA, EPIC, "Poké Puff (Deluxe, Mocha)", "A deluxe mocha Poké Puff; frosted and adorned with a morsel of white chocolate.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_SPICE, FOOD, SPICE, EPIC, "Poké Puff (Deluxe, Spice)", "A deluxe spice Poké Puff; frosted and adorned with a morsel of dark chocolate.", 1000.0f, 0, 0) \
X(POKE_PUFF_SUPREME_SPRING, FOOD, SWEET, LEGENDARY, "Poké Puff (Supreme Spring)", "A supreme Poké Puff that tastes like the sweet cherry blossoms of spring.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_SUMMER, FOOD, CITRUS, LEGENDARY, "Poké Puff (Supreme Summer)", "A supreme Poké Puff that tastes like a tropical summer vacation.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_AUTUMN, FOOD, CITRUS, LEGENDARY, "Poké Puff (Supreme Autumn)", "A supreme Poké Puff that tastes like a bountiful autumn harvest.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_WINTER, FOOD, SPICE, LEGENDARY, "Poké Puff (Supreme Winter)", "A supreme Poké Puff that tastes like a frosty winter wonderland.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_WISH, FOOD, SPICE, IMPOSSIBLE, "Poké Puff (Supreme Wish)", "A supreme Poké Puff that tastes like a cherished birthday celebration.\nIt also is the biggest calorie nuke ever.\nCan Snivy eat it!?\nAlso, statistically, it might be your birthday today, right?", 10000.0f, 0, 0) \
X(POKE_PUFF_SUPREME_HONOR, FOOD, MOCHA, SPECIAL, "Poké Puff (Supreme Honor)", "A supreme Poké Puff that tastes like a monumental victory.\nAwarded for achieving a superb Play score.\nYou've earned it...if Snivy can eat it!", 2500.0f, 0.025f, 1.0f) \
X(BERRY_CHERI, FOOD, SPICY, UNCOMMON, "Cheri Berry", "A spherical red berry that cures paralysis\n...oh, and it also excites one's stomach.", 25.0f, 0.000833f, 0) \
X(BERRY_TAMATO, FOOD, SPICY, RARE, "Tamato Berry", "A spiny crimson berry that lowers speed and raises friendship.\n...oh, and it also greatly excites one's stomach.", 50.0f, 0.0025f, 0) \
X(BERRY_TOUGA, FOOD, SPICY, EPIC, "Touga Berry", "An obscure, searing-red berry from faraway lands.\nIt cures confusion...and it'll make one's stomach a burning inferno.", 100.0f, 0.00833f, 0) \
X(BERRY_PECHA, FOOD, SWEET, UNCOMMON, "Pecha Berry", "A plump and juicy pink berry that cures poison.\n...oh, and its sugars will stir appetite.", 25.0f, 0, 0.05f) \
X(BERRY_MAGOST, FOOD, SWEET, RARE, "Magost Berry", "A spherical, shining berry that packs a punch with a fibrious inside.\n...oh, and its sugars will greatly stir appetite.", 50.0f, 0, 0.15f) \
X(BERRY_WATMEL, FOOD, SWEET, EPIC, "Watmel Berry", "A truly bountiful berry with a brilliant green and pink pattern.\nWhispering tales say that if one eats such a berry,\ntheir appetite will increase to a point that they will soon bare the berry's shape.", 100.0f, 0, 0.50f) \
X(BERRY_AGUAV, FOOD, BITTER, RARE, "Aguav Berry", "A bitter berry that will restore health for hurt eaters who enjoy the flavor.\n...oh, also its taste will soothe an upset stomach.\n(Only use this if you really want to decrease Snivy's digestion.)", 50.0f, -0.01666f, 0) \
X(BERRY_POMEG, FOOD, FLAVORLESS, IMPOSSIBLE, "Pomeg Berry", "cG9tZWcgYmVycnkgcG9tZWcgYmVycnkgcG9tZWcgYmVycnk=", -2500.0f, 0.2, 2.0f)
// clang-format on
enum Type
{
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) symbol,
ITEMS
#undef X
ITEM_COUNT
};
static constexpr Category CATEGORIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) category,
ITEMS
#undef X
};
static constexpr Flavor FLAVORS[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) flavor,
ITEMS
#undef X
};
static constexpr Rarity RARITIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) rarity,
ITEMS
#undef X
};
static constexpr const char* NAMES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) name,
ITEMS
#undef X
};
static constexpr const char* DESCRIPTIONS[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) description,
ITEMS
#undef X
};
static constexpr float CALORIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) calories,
ITEMS
#undef X
};
static constexpr float DIGESTION_RATE_BONUSES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) \
digestionRateBonus,
ITEMS
#undef X
};
static constexpr float EAT_SPEED_BONUSES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) \
eatSpeedBonus,
ITEMS
#undef X
};
#undef ITEMS
enum State
{
DEFAULT,
CHEW_1,
CHEW_2
};
static Item* heldItem;
static Item* heldItemPrevious;
static Item* hoveredItem;
static Item* hoveredItemPrevious;
static Item* queuedReturnItem;
using Pool = std::vector<Type>;
static const std::array<Pool, RARITY_COUNT> pools;
Type type{NONE};
State state{DEFAULT};
int chewCount{0};
bool isToBeDeleted{};
glm::vec2 delta{};
glm::vec2 previousPosition{};
glm::vec2 velocity{};
glm::vec2 holdOffset{};
bool isHeld{};
Item(anm2::Anm2*, glm::ivec2, Type);
void state_set(State);
void tick();
void update(Resources& resources);
};
}

View File

@@ -1,4 +1,5 @@
#include "loader.h" #include "loader.hpp"
#include "util/imgui/widget.hpp"
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h> #include <GLES3/gl3.h>
@@ -8,45 +9,69 @@
#include <backends/imgui_impl_opengl3.h> #include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h> #include <backends/imgui_impl_sdl3.h>
#include <format>
#include <imgui.h> #include <imgui.h>
#include <iostream>
#include "util/math_.h" #include "log.hpp"
#include "util/math.hpp"
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <physfs.h>
#include "resource/audio.hpp"
#include "resource/xml/settings.hpp"
#include "util/imgui/style.hpp"
#include "util/preferences.hpp"
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
#include "util/web_filesystem.hpp"
constexpr auto GL_VERSION_MAJOR = 3; constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 0; constexpr auto GL_VERSION_MINOR = 0;
constexpr auto GLSL_VERSION = "#version 300 es"; constexpr auto GLSL_VERSION = "#version 300 es";
#else #else
constexpr auto GL_VERSION_MAJOR = 3; constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 3; constexpr auto GL_VERSION_MINOR = 3;
constexpr auto GLSL_VERSION = "#version 330"; constexpr auto GLSL_VERSION = "#version 330";
#endif #endif
constexpr auto WINDOW_ROUNDING = 6.0f;
constexpr auto WINDOW_COLOR = ImVec4(0.03f, 0.25f, 0.06f, 1.0f);
constexpr auto WINDOW_BACKGROUND_COLOR = ImVec4(0.02f, 0.08f, 0.03f, 0.96f);
constexpr auto ACCENT_COLOR = ImVec4(0.05f, 0.32f, 0.12f, 1.0f);
constexpr auto ACCENT_COLOR_HOVERED = ImVec4(0.07f, 0.4f, 0.15f, 1.0f);
constexpr auto ACCENT_COLOR_ACTIVE = ImVec4(0.09f, 0.5f, 0.2f, 1.0f);
constexpr auto TAB_UNFOCUSED_COLOR = ImVec4(0.03f, 0.2f, 0.07f, 0.9f);
using namespace game::util; using namespace game::util;
namespace game namespace game
{ {
Loader::Loader()
Loader::Loader(int argc, const char** argv)
{ {
if (!SDL_Init(SDL_INIT_VIDEO)) #ifdef __EMSCRIPTEN__
util::web_filesystem::init_and_wait();
#endif
settings = resource::xml::Settings(preferences::path() / "settings.xml");
logger.info("Initializing...");
if (!PHYSFS_init((argc > 0 && argv && argv[0]) ? argv[0] : "snivy"))
{ {
std::cout << "Failed to initialize SDL: " << SDL_GetError(); logger.fatal(std::format("Failed to initialize PhysicsFS: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())));
isError = true; isError = true;
return; return;
} }
std::cout << "Initialized SDL" << "\n"; PHYSFS_setWriteDir(nullptr);
logger.info("Initialized PhysFS");
if (!SDL_Init(SDL_INIT_VIDEO))
{
logger.fatal(std::format("Failed to initialize SDL: {}", SDL_GetError()));
isError = true;
return;
}
logger.info("Initialized SDL");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_VERSION_MINOR); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_VERSION_MINOR);
@@ -57,30 +82,49 @@ namespace game
#endif #endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
#ifdef __EMSCRIPTEN__
window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL); window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL);
#else
SDL_PropertiesID windowProperties = SDL_CreateProperties();
SDL_SetStringProperty(windowProperties, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "Snivy");
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, settings.windowSize.x);
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, settings.windowSize.y);
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_X_NUMBER,
settings.windowPosition.x == 0 ? SDL_WINDOWPOS_CENTERED : settings.windowPosition.x);
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_Y_NUMBER,
settings.windowPosition.y == 0 ? SDL_WINDOWPOS_CENTERED : settings.windowPosition.y);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
window = SDL_CreateWindowWithProperties(windowProperties);
#endif
if (!window) if (!window)
{ {
std::cout << "Failed to initialize window: " << SDL_GetError(); logger.fatal(std::format("Failed to initialize window: {}", SDL_GetError()));
;
isError = true; isError = true;
return; return;
} }
std::cout << "Initialized window" << "\n"; logger.info("Initialized window");
context = SDL_GL_CreateContext(window); context = SDL_GL_CreateContext(window);
if (!context) if (!context)
{ {
std::cout << "Failed to initialize GL context: " << SDL_GetError(); logger.fatal(std::format("Failed to initialize GL context: {}", SDL_GetError()));
isError = true; isError = true;
return; return;
} }
if (!SDL_GL_MakeCurrent(window, context)) if (!SDL_GL_MakeCurrent(window, context))
{ {
std::cout << "Failed to make GL context current: " << SDL_GetError(); logger.fatal(std::format("Failed to make GL context current: {}", SDL_GetError()));
isError = true; isError = true;
return; return;
} }
@@ -88,12 +132,12 @@ namespace game
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{ {
std::cout << "Failed to initialize GLAD" << "\n"; logger.fatal("Failed to initialize GLAD");
isError = true; isError = true;
return; return;
} }
std::cout << "Initialized GLAD" << "\n"; logger.info("Initialized GLAD");
#endif #endif
glEnable(GL_BLEND); glEnable(GL_BLEND);
@@ -101,90 +145,68 @@ namespace game
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
std::cout << "Initialized GL context: " << glGetString(GL_VERSION) << "\n"; logger.info(std::format("Initialized GL context: {}", (const char*)glGetString(GL_VERSION)));
SDL_GL_SetSwapInterval(1); SDL_GL_SetSwapInterval(1);
SDL_GL_MakeCurrent(window, context);
if (!MIX_Init()) if (!MIX_Init())
{ {
std::cout << "Failed to initialize SDL mixer: " << SDL_GetError(); logger.fatal(std::format("Failed to initialize SDL mixer: {}", SDL_GetError()));
isError = true; isError = true;
return; return;
} }
std::cout << "Initialized SDL mixer" << "\n"; logger.info("Initialized SDL mixer");
IMGUI_CHECKVERSION(); IMGUI_CHECKVERSION();
ImGuiContext* imguiContext = ImGui::CreateContext(); ImGuiContext* imguiContext = ImGui::CreateContext();
if (!imguiContext) if (!imguiContext)
{ {
std::cout << "Failed to initialize Dear ImGui" << "\n"; logger.fatal("Failed to initialize Dear ImGui");
isError = true; isError = true;
return; return;
} }
std::cout << "Initialized Dear ImGui" << "\n"; logger.info("Initialized Dear ImGui");
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; io.IniFilename = nullptr;
ImGuiStyle& style = ImGui::GetStyle();
ImGui::StyleColorsDark(); ImGui::StyleColorsDark();
style.Colors[ImGuiCol_TitleBg] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgActive] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgCollapsed] = WINDOW_COLOR;
style.Colors[ImGuiCol_Header] = ACCENT_COLOR;
style.Colors[ImGuiCol_HeaderHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_HeaderActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_FrameBg] = ACCENT_COLOR;
style.Colors[ImGuiCol_FrameBgActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_FrameBgHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_Button] = ACCENT_COLOR;
style.Colors[ImGuiCol_ButtonHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_ButtonActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_CheckMark] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_SliderGrab] = ACCENT_COLOR;
style.Colors[ImGuiCol_SliderGrabActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_ResizeGrip] = ACCENT_COLOR;
style.Colors[ImGuiCol_ResizeGripHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_ResizeGripActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_PlotLines] = ACCENT_COLOR;
style.Colors[ImGuiCol_PlotLinesHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_PlotHistogram] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_PlotHistogramHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_Tab] = ACCENT_COLOR;
style.Colors[ImGuiCol_TabHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_TabActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_TabUnfocused] = TAB_UNFOCUSED_COLOR;
style.Colors[ImGuiCol_TabUnfocusedActive] = ACCENT_COLOR;
if (!ImGui_ImplSDL3_InitForOpenGL(window, context)) if (!ImGui_ImplSDL3_InitForOpenGL(window, context))
{ {
std::cout << "Failed to initialize Dear ImGui SDL3 backend" << "\n"; logger.fatal("Failed to initialize Dear ImGui SDL3 backend");
ImGui::DestroyContext(imguiContext); ImGui::DestroyContext(imguiContext);
isError = true; isError = true;
return; return;
} }
std::cout << "Initialize Dear ImGui SDL3 backend" << "\n"; logger.info("Initialized Dear ImGui SDL3 backend");
if (!ImGui_ImplOpenGL3_Init(GLSL_VERSION)) if (!ImGui_ImplOpenGL3_Init(GLSL_VERSION))
{ {
std::cout << "Failed to initialize Dear ImGui OpenGL backend" << "\n"; logger.fatal("Failed to initialize Dear ImGui OpenGL backend");
ImGui_ImplSDL3_Shutdown(); ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(imguiContext); ImGui::DestroyContext(imguiContext);
isError = true; isError = true;
return; return;
} }
std::cout << "Initialize Dear ImGui OpenGL backend" << "\n"; logger.info("Initialized Dear ImGui OpenGL backend");
imgui::style::color_set(settings.color);
imgui::style::rounding_set();
math::random_seed_set(); math::random_seed_set();
resource::Audio::volume_set((float)settings.volume / 100);
} }
Loader::~Loader() Loader::~Loader()
{ {
PHYSFS_deinit();
if (ImGui::GetCurrentContext()) if (ImGui::GetCurrentContext())
{ {
ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplOpenGL3_Shutdown();
@@ -196,6 +218,6 @@ namespace game
if (window) SDL_DestroyWindow(window); if (window) SDL_DestroyWindow(window);
SDL_Quit(); SDL_Quit();
std::cout << "Exiting..." << "\n"; logger.info("Exiting...");
} }
}; };

View File

@@ -1,20 +1,22 @@
#pragma once #pragma once
#include "glm/ext/vector_float2.hpp" #include "resource/xml/settings.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <glm/ext/vector_float2.hpp>
namespace game namespace game
{ {
class Loader class Loader
{ {
public: public:
static constexpr glm::vec2 SIZE = {1080, 720}; static constexpr glm::vec2 SIZE = {1280, 720};
SDL_Window* window{}; SDL_Window* window{};
SDL_GLContext context{}; SDL_GLContext context{};
bool isError{}; bool isError{};
resource::xml::Settings settings;
Loader(); Loader(int argc, const char** argv);
~Loader(); ~Loader();
}; };

114
src/log.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "log.hpp"
#include <exception>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <print>
#include <sstream>
#include <streambuf>
#include <thread>
#if defined(_WIN32)
#include <dbghelp.h>
#include <execinfo.h>
#include <windows.h>
#endif
#include "util/preferences.hpp"
#include "util/time.hpp"
using namespace game::util;
namespace game
{
class StderrToLoggerBuf final : public std::streambuf
{
Logger* logger{};
std::string buffer;
std::mutex mutex;
public:
explicit StderrToLoggerBuf(Logger* target) : logger(target) {}
protected:
int overflow(int ch) override
{
if (ch == traits_type::eof()) return traits_type::not_eof(ch);
std::lock_guard<std::mutex> lock(mutex);
if (ch == '\n')
{
if (logger && !buffer.empty())
{
logger->error(buffer);
buffer.clear();
}
}
else
{
buffer.push_back(static_cast<char>(ch));
}
return ch;
}
int sync() override
{
std::lock_guard<std::mutex> lock(mutex);
if (logger && !buffer.empty())
{
logger->error(buffer);
buffer.clear();
}
return 0;
}
};
std::streambuf* old_stderr_buf = nullptr;
void Logger::write_raw(const std::string& message)
{
std::println("{}", message);
if (file.is_open()) file << message << '\n' << std::flush;
}
void Logger::write(const Level level, const std::string& message)
{
std::string formatted = std::format("{} {} {}", LEVEL_STRINGS[level], time::get("(%d-%B-%Y %I:%M:%S)"), message);
write_raw(formatted);
}
void Logger::info(const std::string& message) { write(LEVEL_INFO, message); }
void Logger::warning(const std::string& message) { write(LEVEL_WARNING, message); }
void Logger::error(const std::string& message) { write(LEVEL_ERROR, message); }
void Logger::fatal(const std::string& message) { write(LEVEL_FATAL, message); }
void Logger::open(const std::filesystem::path& path) { file.open(path, std::ios::out | std::ios::app); }
std::filesystem::path Logger::path() { return preferences::path() / "log.txt"; }
Logger::Logger()
{
open(path());
static StderrToLoggerBuf stderr_buf(this);
old_stderr_buf = std::cerr.rdbuf(&stderr_buf);
std::cerr.setf(std::ios::unitbuf);
std::set_terminate(
[]
{
try
{
if (auto eptr = std::current_exception()) std::rethrow_exception(eptr);
}
catch (const std::exception& ex)
{
logger.fatal(std::string("Unhandled exception: ") + ex.what());
}
catch (...)
{
logger.fatal("Unhandled exception: <unknown>");
}
std::abort();
});
}
}
game::Logger logger;

48
src/log.hpp Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
namespace game
{
#define LEVELS \
X(LEVEL_INFO, "[INFO]") \
X(LEVEL_WARNING, "[WARNING]") \
X(LEVEL_ERROR, "[ERROR]") \
X(LEVEL_FATAL, "[FATAL]")
enum Level
{
#define X(symbol, string) symbol,
LEVELS
#undef X
};
constexpr std::string_view LEVEL_STRINGS[] = {
#define X(symbol, string) string,
LEVELS
#undef X
};
#undef LEVELS
class Logger
{
std::ofstream file{};
public:
static std::filesystem::path path();
void write_raw(const std::string&);
void write(const Level, const std::string&);
void info(const std::string&);
void warning(const std::string&);
void error(const std::string&);
void fatal(const std::string&);
void open(const std::filesystem::path&);
Logger();
};
}
extern game::Logger logger;

View File

@@ -4,8 +4,8 @@
#include <emscripten/emscripten.h> #include <emscripten/emscripten.h>
#endif #endif
#include "loader.h" #include "loader.hpp"
#include "state.h" #include "state.hpp"
using namespace game; using namespace game;
@@ -19,13 +19,13 @@ static void emscripten_loop(void* arg)
} }
#endif #endif
int main() int main(int argc, const char** argv)
{ {
Loader loader; Loader loader(argc, argv);
if (loader.isError) return EXIT_FAILURE; if (loader.isError) return EXIT_FAILURE;
State state(loader.window, loader.context, Loader::SIZE); State state(loader.window, loader.context, loader.settings);
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(emscripten_loop, &state, 0, true); emscripten_set_main_loop_arg(emscripten_loop, &state, 0, true);

View File

@@ -1,421 +0,0 @@
#include "actor.h"
#include "../util/map_.h"
#include "../util/math_.h"
#include "../util/unordered_map_.h"
#include "../util/vector_.h"
#include "../resource/audio.h"
#include "../resource/texture.h"
#include <glm/glm.hpp>
#include <iostream>
using namespace glm;
using namespace game::util;
using namespace game::anm2;
namespace game::resource
{
Actor::Actor(Anm2* _anm2, vec2 _position, Mode mode, float time) : anm2(_anm2), position(_position)
{
if (anm2)
{
this->mode = mode;
this->startTime = time;
play(anm2->animations.defaultAnimation, mode, time);
}
}
anm2::Animation* Actor::animation_get(int index)
{
if (!anm2) return nullptr;
if (index == -1) index = animationIndex;
if (anm2->animations.mapReverse.contains(index)) return &anm2->animations.items[index];
return nullptr;
}
anm2::Animation* Actor::animation_get(const std::string& name)
{
if (!anm2) return nullptr;
if (anm2->animations.map.contains(name)) return &anm2->animations.items[anm2->animations.map[name]];
return nullptr;
}
bool Actor::is_playing(const std::string& name)
{
if (!anm2) return false;
if (name.empty())
return isPlaying;
else
return isPlaying && anm2->animations.map[name] == animationIndex;
}
int Actor::animation_index_get(const std::string& name)
{
if (!anm2) return -1;
if (anm2->animations.map.contains(name)) return anm2->animations.map[name];
return -1;
}
int Actor::item_id_get(const std::string& name, anm2::Type type)
{
if (!anm2 || (type != anm2::LAYER && type != anm2::NULL_)) return -1;
if (type == anm2::LAYER)
{
for (int i = 0; i < anm2->content.layers.size(); i++)
if (anm2->content.layers.at(i).name == name) return i;
}
else if (type == anm2::NULL_)
{
for (int i = 0; i < anm2->content.nulls.size(); i++)
if (anm2->content.nulls.at(i).name == name) return i;
}
return -1;
}
anm2::Item* Actor::item_get(anm2::Type type, int id, int animationIndex)
{
if (!anm2) return nullptr;
if (animationIndex == -1) animationIndex = this->animationIndex;
if (auto animation = animation_get(animationIndex))
{
switch (type)
{
case anm2::ROOT:
return &animation->rootAnimation;
break;
case anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
int Actor::item_length(anm2::Item* item)
{
if (!item) return -1;
int duration{};
for (auto& frame : item->frames)
duration += frame.duration;
return duration;
}
anm2::Frame* Actor::trigger_get(int atFrame)
{
if (auto item = item_get(anm2::TRIGGER))
for (auto& trigger : item->frames)
if (trigger.atFrame == atFrame) return &trigger;
return nullptr;
}
anm2::Frame* Actor::frame_get(int index, anm2::Type type, int id)
{
if (auto item = item_get(type, id)) return vector::find(item->frames, index);
return nullptr;
}
bool Actor::is_event(const std::string& event)
{
if (!anm2) return false;
if (playedEventID == -1) return false;
return event == anm2->content.events.at(playedEventID).name;
}
anm2::Frame Actor::frame_generate(anm2::Item& item, float time, anm2::Type type, int id)
{
anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
time = time < 0.0f ? 0.0f : time;
anm2::Frame* frameNext = nullptr;
anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < item.frames.size(); i++)
{
anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
for (auto& override : overrides)
{
if (!override || !override->isEnabled) continue;
if (id == override->destinationID)
{
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value())
{
frame.scale += *override->frame.scale;
if (frameNext) frameNextCopy.scale += *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation += *override->frame.rotation;
if (frameNext) frameNextCopy.rotation += *override->frame.rotation;
}
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value())
{
frame.scale = *override->frame.scale;
if (frameNext) frameNextCopy.scale = *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation = *override->frame.rotation;
if (frameNext) frameNextCopy.rotation = *override->frame.rotation;
}
break;
case Override::ITEM_SET:
default:
if (override->animationIndex == -1) break;
auto& animation = anm2->animations.items[override->animationIndex];
auto overrideFrame = frame_generate(animation.layerAnimations[override->sourceID], override->time,
anm2::LAYER, override->sourceID);
frame.crop = overrideFrame.crop;
break;
}
}
}
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
}
return frame;
}
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
{
this->playedEventID = -1;
this->playedTriggers.clear();
if (!anm2) return;
if (mode != FORCE_PLAY && this->animationIndex == index) return;
if (!vector::in_bounds(anm2->animations.items, index)) return;
this->speedMultiplier = speedMultiplier;
this->previousAnimationIndex = animationIndex;
this->animationIndex = index;
this->time = time;
if (mode == PLAY || mode == FORCE_PLAY) isPlaying = true;
}
void Actor::play(const std::string& name, Mode mode, float time, float speedMultiplier)
{
if (!anm2) return;
if (anm2->animations.map.contains(name))
play(anm2->animations.map.at(name), mode, time, speedMultiplier);
else
std::cout << "Animation \"" << name << "\" does not exist! Unable to play!\n";
}
void Actor::tick()
{
if (!anm2) return;
if (!isPlaying) return;
auto animation = animation_get();
if (!animation) return;
playedEventID = -1;
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
if (auto sound = map::find(anm2->content.sounds, trigger.soundID)) sound->audio.play();
playedTriggers.insert((int)trigger.atFrame);
playedEventID = trigger.eventID;
}
}
auto increment = (anm2->info.fps / 30.0f) * speedMultiplier;
time += increment;
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
isPlaying = false;
playedTriggers.clear();
}
for (auto& override : overrides)
{
if (!override->isEnabled || override->length < 0) continue;
override->time += increment;
if (override->time > override->length) override->isLoop ? override->time = 0.0f : override->isEnabled = false;
}
}
glm::vec4 Actor::null_frame_rect(int nullID)
{
auto invalidRect = glm::vec4(0.0f / 0.0f);
if (!anm2 || nullID == -1) return invalidRect;
auto item = item_get(anm2::NULL_, nullID);
if (!item) return invalidRect;
auto animation = animation_get();
if (!animation) return invalidRect;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto frame = frame_generate(*item, time, anm2::NULL_, nullID);
if (!frame.isVisible) return invalidRect;
auto rootScale = math::to_unit(root.scale);
auto frameScale = math::to_unit(frame.scale);
auto combinedScale = rootScale * frameScale;
auto scaledSize = NULL_SIZE * glm::abs(combinedScale);
auto worldPosition = position + root.position + frame.position * rootScale;
auto halfSize = scaledSize * 0.5f;
return glm::vec4(worldPosition - halfSize, scaledSize);
}
void Actor::render(Shader& textureShader, Shader& rectShader, Canvas& canvas)
{
if (!anm2) return;
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto rootModel =
math::quad_model_parent_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(anm2->content.layers, i);
if (!layer) continue;
auto spritesheet = map::find(anm2->content.spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time, anm2::LAYER, i);
if (!frame.isVisible) continue;
auto model =
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto tint = frame.tint * root.tint;
auto colorOffset = frame.colorOffset + root.colorOffset;
auto uvMin = frame.crop / vec2(texture.size);
auto uvMax = (frame.crop + frame.size) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
}
if (isShowNulls)
{
for (int i = 0; i < animation->nullAnimations.size(); i++)
{
auto& nullAnimation = animation->nullAnimations[i];
if (!nullAnimation.isVisible) continue;
auto frame = frame_generate(nullAnimation, time, anm2::NULL_, i);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
model = rootModel * model;
canvas.rect_render(rectShader, model);
}
}
}
void Actor::consume_played_event() { playedEventID = -1; }
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include <unordered_set>
#include "../canvas.h"
#include "anm2.h"
namespace game::resource
{
class Actor
{
public:
static constexpr auto NULL_SIZE = glm::vec2(100, 100);
enum Mode
{
PLAY,
SET,
FORCE_PLAY
};
struct Override
{
enum Mode
{
ITEM_SET,
FRAME_ADD,
FRAME_SET
};
anm2::FrameOptional frame{};
int animationIndex{-1};
int sourceID{-1};
int destinationID{-1};
float length{-1.0f};
bool isLoop{false};
Mode mode{ITEM_SET};
anm2::Type type{anm2::LAYER};
bool isEnabled{true};
float time{};
};
anm2::Anm2* anm2{};
glm::vec2 position{};
float time{};
bool isPlaying{};
bool isShowNulls{};
int animationIndex{-1};
int previousAnimationIndex{-1};
int lastPlayedAnimationIndex{-1};
int playedEventID{-1};
Mode mode{PLAY};
float startTime{};
float speedMultiplier{};
std::unordered_set<int> playedTriggers{};
std::vector<Override*> overrides{};
Actor(anm2::Anm2*, glm::vec2, Mode = PLAY, float = 0.0f);
anm2::Animation* animation_get(int = -1);
anm2::Animation* animation_get(const std::string&);
int animation_index_get(const std::string&);
anm2::Item* item_get(anm2::Type, int = -1, int = -1);
int item_length(anm2::Item*);
anm2::Frame* trigger_get(int);
anm2::Frame* frame_get(int, anm2::Type, int = -1);
int item_id_get(const std::string&, anm2::Type = anm2::LAYER);
anm2::Frame frame_generate(anm2::Item&, float, anm2::Type, int = -1);
void play(const std::string&, Mode = PLAY, float = 0.0f, float = 1.0f);
void play(int, Mode = PLAY, float = 0.0f, float = 1.0f);
bool is_event(const std::string& event);
void tick();
bool is_playing(const std::string& name = {});
void render(Shader& textureShader, Shader& rectShader, Canvas&);
glm::vec4 null_frame_rect(int = -1);
void consume_played_event();
};
}

View File

@@ -1,234 +0,0 @@
#include "anm2.h"
#include <iostream>
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::resource;
using namespace game::util;
namespace game::anm2
{
Info::Info(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Fps", &fps);
}
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
texture = Texture(path);
}
Layer::Layer(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
Null::Null(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
Event::Event(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
}
Sound::Sound(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
audio = Audio(path);
}
Content::Content(XMLElement* element)
{
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet(child, spritesheetId);
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = element->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer(child, layerId);
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = element->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null(child, nullId);
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = element->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event(child, eventId);
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = element->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound(child, soundId);
sounds.emplace(soundId, std::move(sound));
}
}
}
Frame::Frame(XMLElement* element, Type type)
{
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &position.x);
element->QueryFloatAttribute("YPosition", &position.y);
if (type == LAYER)
{
element->QueryFloatAttribute("XPivot", &pivot.x);
element->QueryFloatAttribute("YPivot", &pivot.y);
element->QueryFloatAttribute("XCrop", &crop.x);
element->QueryFloatAttribute("YCrop", &crop.y);
element->QueryFloatAttribute("Width", &size.x);
element->QueryFloatAttribute("Height", &size.y);
}
element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible);
xml::query_color_attribute(element, "RedTint", &tint.r);
xml::query_color_attribute(element, "GreenTint", &tint.g);
xml::query_color_attribute(element, "BlueTint", &tint.b);
xml::query_color_attribute(element, "AlphaTint", &tint.a);
xml::query_color_attribute(element, "RedOffset", &colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
}
else
{
element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("SoundId", &soundID);
element->QueryIntAttribute("AtFrame", &atFrame);
}
}
Item::Item(XMLElement* element, Type type, int& id)
{
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
frames.emplace_back(Frame(child, type));
}
Animation::Animation(XMLElement* element)
{
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
rootAnimation = Item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
Item layerAnimation(child, LAYER, id);
layerOrder.push_back(id);
layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
Item nullAnimation(child, NULL_, id);
nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER, id);
}
Animations::Animations(XMLElement* element)
{
xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
items.emplace_back(Animation(child));
for (int i = 0; i < items.size(); i++)
{
auto& item = items.at(i);
map[item.name] = i;
mapReverse[i] = item.name;
}
}
Anm2::Anm2(const std::filesystem::path& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize anm2: " << document.ErrorStr() << "\n";
return;
}
auto previousPath = std::filesystem::current_path();
std::filesystem::current_path(path.parent_path());
auto element = document.RootElement();
if (auto infoElement = element->FirstChildElement("Info")) info = Info(infoElement);
if (auto contentElement = element->FirstChildElement("Content")) content = Content(contentElement);
if (auto animationsElement = element->FirstChildElement("Animations")) animations = Animations(animationsElement);
std::filesystem::current_path(previousPath);
std::cout << "Initialzed anm2: " << path.string() << "\n";
}
}

View File

@@ -1,176 +0,0 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "audio.h"
#include "texture.h"
namespace game::anm2
{
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
class Info
{
public:
int fps = 30;
Info() = default;
Info(tinyxml2::XMLElement*);
};
class Spritesheet
{
public:
std::filesystem::path path{};
resource::Texture texture{};
Spritesheet(tinyxml2::XMLElement*, int&);
};
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{-1};
Layer(tinyxml2::XMLElement*, int&);
};
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
Null(tinyxml2::XMLElement*, int&);
};
class Event
{
public:
std::string name{"New Event"};
Event(tinyxml2::XMLElement*, int&);
};
class Sound
{
public:
std::filesystem::path path{};
resource::Audio audio{};
Sound(tinyxml2::XMLElement*, int&);
};
class Content
{
public:
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
Content() = default;
Content(tinyxml2::XMLElement*);
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int soundID{-1};
int atFrame{-1};
bool isVisible{true};
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{};
Item() = default;
Item(tinyxml2::XMLElement*, Type, int&);
};
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
Animation() = default;
Animation(tinyxml2::XMLElement*);
};
class Animations
{
public:
std::string defaultAnimation{};
std::vector<Animation> items{};
std::unordered_map<std::string, int> map{};
std::unordered_map<int, std::string> mapReverse{};
Animations() = default;
Animations(tinyxml2::XMLElement*);
};
class Anm2
{
public:
Info info;
Content content{};
Animations animations{};
Anm2() = default;
Anm2(const std::filesystem::path&);
};
}

View File

@@ -1,73 +1,119 @@
#include "audio.h" #include "audio.hpp"
#include <SDL3/SDL_properties.h> #include <SDL3/SDL_properties.h>
#include <iostream> #include "../log.hpp"
#include <string>
#include <unordered_map>
using namespace game::util;
namespace game::resource namespace game::resource
{ {
static std::shared_ptr<MIX_Audio> audio_make(MIX_Audio* audio)
{
return std::shared_ptr<MIX_Audio>(audio,
[](MIX_Audio* a)
{
if (a) MIX_DestroyAudio(a);
});
}
static std::unordered_map<std::string, std::weak_ptr<MIX_Audio>> audioCache{};
static std::shared_ptr<MIX_Audio> cache_get(const std::string& key)
{
auto it = audioCache.find(key);
if (it == audioCache.end()) return {};
auto cached = it->second.lock();
if (!cached) audioCache.erase(it);
return cached;
}
static void cache_set(const std::string& key, const std::shared_ptr<MIX_Audio>& audio)
{
if (!audio) return;
audioCache[key] = audio;
}
MIX_Mixer* Audio::mixer_get() MIX_Mixer* Audio::mixer_get()
{ {
static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
return mixer; return mixer;
} }
void Audio::set_gain(float gain) void Audio::volume_set(float volume)
{ {
auto mixer = mixer_get(); auto mixer = mixer_get();
MIX_SetMasterGain(mixer, gain); MIX_SetMasterGain(mixer, volume);
}
void Audio::retain()
{
if (refCount) ++(*refCount);
}
void Audio::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (internal) MIX_DestroyAudio(internal);
delete refCount;
}
refCount = nullptr;
}
internal = nullptr;
} }
Audio::Audio(const std::filesystem::path& path) Audio::Audio(const std::filesystem::path& path)
{ {
internal = MIX_LoadAudio(mixer_get(), path.c_str(), true); auto key = std::string("fs:") + path.string();
internal = cache_get(key);
if (internal) if (internal)
{ {
refCount = new int(1); logger.info(std::format("Using cached audio: {}", path.string()));
std::cout << "Initialized audio: '" << path.string() << "'\n"; return;
} }
else
internal = audio_make(MIX_LoadAudio(mixer_get(), path.c_str(), true));
cache_set(key, internal);
if (internal) logger.info(std::format("Initialized audio: {}", path.string()));
if (!internal) logger.info(std::format("Failed to intialize audio: {} ({})", path.string(), SDL_GetError()));
}
Audio::Audio(const physfs::Path& path)
{
if (!path.is_valid())
{ {
std::cout << "Failed to initialize audio: '" << path.string() << "'\n"; logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
return;
} }
auto key = std::string("physfs:") + path.c_str();
internal = cache_get(key);
if (internal)
{
logger.info(std::format("Using cached audio: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
internal = audio_make(MIX_LoadAudio_IO(mixer_get(), ioStream, false, true));
cache_set(key, internal);
if (internal)
logger.info(std::format("Initialized audio: {}", path.c_str()));
else
logger.info(std::format("Failed to intialize audio: {} ({})", path.c_str(), SDL_GetError()));
} }
Audio::Audio(const Audio& other) Audio::Audio(const Audio& other)
{ {
internal = other.internal; internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr; track = nullptr;
} }
Audio::Audio(Audio&& other) noexcept Audio::Audio(Audio&& other) noexcept
{ {
internal = other.internal; internal = std::move(other.internal);
track = other.track; track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr; other.track = nullptr;
other.refCount = nullptr;
} }
Audio& Audio::operator=(const Audio& other) Audio& Audio::operator=(const Audio& other)
@@ -76,8 +122,7 @@ namespace game::resource
{ {
unload(); unload();
internal = other.internal; internal = other.internal;
refCount = other.refCount; track = nullptr;
retain();
} }
return *this; return *this;
} }
@@ -87,13 +132,10 @@ namespace game::resource
if (this != &other) if (this != &other)
{ {
unload(); unload();
internal = other.internal; internal = std::move(other.internal);
track = other.track; track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr; other.track = nullptr;
other.refCount = nullptr;
} }
return *this; return *this;
} }
@@ -105,7 +147,7 @@ namespace game::resource
MIX_DestroyTrack(track); MIX_DestroyTrack(track);
track = nullptr; track = nullptr;
} }
release(); internal.reset();
} }
void Audio::play(bool isLoop) void Audio::play(bool isLoop)
@@ -126,7 +168,7 @@ namespace game::resource
if (!track) return; if (!track) return;
} }
MIX_SetTrackAudio(track, internal); MIX_SetTrackAudio(track, internal.get());
SDL_PropertiesID options = 0; SDL_PropertiesID options = 0;
@@ -149,5 +191,5 @@ namespace game::resource
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); } bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
Audio::~Audio() { unload(); } Audio::~Audio() { unload(); }
bool Audio::is_valid() const { return internal != nullptr; } bool Audio::is_valid() const { return (bool)internal; }
} }

View File

@@ -2,22 +2,24 @@
#include <SDL3_mixer/SDL_mixer.h> #include <SDL3_mixer/SDL_mixer.h>
#include <filesystem> #include <filesystem>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource namespace game::resource
{ {
class Audio class Audio
{ {
MIX_Audio* internal{nullptr};
MIX_Track* track{nullptr};
int* refCount{nullptr};
static MIX_Mixer* mixer_get(); static MIX_Mixer* mixer_get();
void unload(); void unload();
void retain();
void release(); std::shared_ptr<MIX_Audio> internal{};
MIX_Track* track{nullptr};
public: public:
Audio() = default; Audio() = default;
Audio(const std::filesystem::path&); Audio(const std::filesystem::path&);
Audio(const util::physfs::Path&);
Audio(const Audio&); Audio(const Audio&);
Audio(Audio&&) noexcept; Audio(Audio&&) noexcept;
Audio& operator=(const Audio&); Audio& operator=(const Audio&);
@@ -27,6 +29,6 @@ namespace game::resource
void play(bool isLoop = false); void play(bool isLoop = false);
void stop(); void stop();
bool is_playing() const; bool is_playing() const;
static void set_gain(float vol); static void volume_set(float volume);
}; };
} }

View File

@@ -1,137 +0,0 @@
#include "dialogue.h"
#include <iostream>
#include "../util/map_.h"
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource
{
void label_map_query(XMLElement* element, std::map<std::string, int>& labelMap, const char* attribute, int& id)
{
std::string label{};
xml::query_string_attribute(element, attribute, &label);
if (auto foundID = map::find(labelMap, label))
id = *foundID;
else
id = -1;
}
Dialogue::Color::Color(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Start", &start);
element->QueryIntAttribute("End", &end);
xml::query_color_attribute(element, "R", &value.r);
xml::query_color_attribute(element, "G", &value.g);
xml::query_color_attribute(element, "B", &value.b);
xml::query_color_attribute(element, "A", &value.a);
}
Dialogue::Animation::Animation(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("At", &at);
xml::query_string_attribute(element, "Name", &name);
}
Dialogue::Branch::Branch(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
label_map_query(element, labelMap, "Label", nextID);
xml::query_string_attribute(element, "Content", &content);
}
Dialogue::Entry::Entry(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
xml::query_string_attribute(element, "Content", &content);
label_map_query(element, labelMap, "Next", nextID);
std::string flagString{};
xml::query_string_attribute(element, "Flag", &flagString);
for (int i = 0; i < std::size(FLAG_STRINGS); i++)
{
if (flagString == FLAG_STRINGS[i])
{
flag = (Flag)i;
break;
}
}
for (auto child = element->FirstChildElement("Color"); child; child = child->NextSiblingElement("Color"))
colors.emplace_back(child);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
animations.emplace_back(child);
for (auto child = element->FirstChildElement("Branch"); child; child = child->NextSiblingElement("Branch"))
branches.emplace_back(child, labelMap);
}
Dialogue::Dialogue(const std::string& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize dialogue: " << document.ErrorStr() << "\n";
return;
}
auto element = document.RootElement();
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string label{};
xml::query_string_attribute(child, "Label", &label);
labelMap.emplace(label, id++);
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
entryMap.emplace(id++, Entry(child, labelMap));
for (auto& [label, id] : labelMap)
{
if (label.starts_with(BURP_SMALL_LABEL)) burpSmallIDs.emplace_back(id);
if (label.starts_with(BURP_BIG_LABEL)) burpBigIDs.emplace_back(id);
if (label.starts_with(EAT_HUNGRY_LABEL)) eatHungryIDs.emplace_back(id);
if (label.starts_with(EAT_FULL_LABEL)) eatFullIDs.emplace_back(id);
if (label.starts_with(FULL_LABEL)) fullIDs.emplace_back(id);
if (label.starts_with(CAPACITY_LOW_LABEL)) capacityLowIDs.emplace_back(id);
if (label.starts_with(FEED_HUNGRY_LABEL)) feedHungryIDs.emplace_back(id);
if (label.starts_with(FEED_FULL_LABEL)) feedFullIDs.emplace_back(id);
if (label.starts_with(FOOD_STOLEN_LABEL)) foodStolenIDs.emplace_back(id);
if (label.starts_with(FOOD_EASED_LABEL)) foodEasedIDs.emplace_back(id);
if (label.starts_with(PERFECT_LABEL)) perfectIDs.emplace_back(id);
if (label.starts_with(MISS_LABEL)) missIDs.emplace_back(id);
if (label.starts_with(POST_DIGEST_LABEL)) postDigestIDs.emplace_back(id);
if (label.starts_with(RANDOM_LABEL)) randomIDs.emplace_back(id);
}
std::cout << "Initialzed dialogue: " << path << "\n";
}
Dialogue::Entry* Dialogue::get(int id)
{
if (id == -1) return nullptr;
return map::find(entryMap, id);
}
Dialogue::Entry* Dialogue::get(const std::string& label)
{
auto id = map::find(labelMap, label);
if (!id) return nullptr;
return get(*id);
}
Dialogue::Entry* Dialogue::next(Dialogue::Entry* entry) { return get(entry->nextID); }
}

View File

@@ -1,118 +0,0 @@
#pragma once
#include "glm/ext/vector_float4.hpp"
#include <tinyxml2.h>
#include <string>
#include <map>
namespace game::resource
{
class Dialogue
{
public:
static constexpr auto FULL_LABEL = "Full";
static constexpr auto POST_DIGEST_LABEL = "PostDigest";
static constexpr auto BURP_SMALL_LABEL = "BurpSmall";
static constexpr auto BURP_BIG_LABEL = "BurpBig";
static constexpr auto FEED_HUNGRY_LABEL = "FeedHungry";
static constexpr auto FEED_FULL_LABEL = "FeedFull";
static constexpr auto EAT_HUNGRY_LABEL = "EatHungry";
static constexpr auto EAT_FULL_LABEL = "EatFull";
static constexpr auto FOOD_STOLEN_LABEL = "FoodStolen";
static constexpr auto FOOD_EASED_LABEL = "FoodEased";
static constexpr auto CAPACITY_LOW_LABEL = "CapacityLow";
static constexpr auto PERFECT_LABEL = "Perfect";
static constexpr auto MISS_LABEL = "Miss";
static constexpr auto RANDOM_LABEL = "StartRandom";
;
class Color
{
public:
int start{};
int end{};
glm::vec4 value{};
Color(tinyxml2::XMLElement*);
};
class Animation
{
public:
int at{-1};
std::string name{};
Animation(tinyxml2::XMLElement*);
};
class Branch
{
public:
std::string content{};
int nextID{-1};
Branch(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
class Entry
{
public:
#define FLAGS \
X(NONE, "None") \
X(ACTIVATE_WINDOWS, "ActivateWindows") \
X(DEACTIVATE_WINDOWS, "DeactivateWindows") \
X(ONLY_INFO, "OnlyInfo") \
X(ACTIVATE_CHEATS, "ActivateCheats")
enum Flag
{
#define X(symbol, name) symbol,
FLAGS
#undef X
};
static constexpr const char* FLAG_STRINGS[] = {
#define X(symbol, name) name,
FLAGS
#undef X
};
#undef FLAGS
std::string content{};
std::vector<Color> colors{};
std::vector<Animation> animations{};
std::vector<Branch> branches{};
int nextID{-1};
Flag flag{Flag::NONE};
Entry(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
std::map<std::string, int> labelMap;
std::map<int, Entry> entryMap{};
std::vector<int> eatHungryIDs{};
std::vector<int> eatFullIDs{};
std::vector<int> feedHungryIDs{};
std::vector<int> feedFullIDs{};
std::vector<int> burpSmallIDs{};
std::vector<int> burpBigIDs{};
std::vector<int> fullIDs{};
std::vector<int> foodStolenIDs{};
std::vector<int> foodEasedIDs{};
std::vector<int> perfectIDs{};
std::vector<int> postDigestIDs{};
std::vector<int> missIDs{};
std::vector<int> capacityLowIDs{};
std::vector<int> randomIDs{};
Dialogue(const std::string&);
Entry* get(const std::string&);
Entry* get(int = -1);
Entry* next(Entry*);
};
}

View File

@@ -1,10 +1,52 @@
#include "font.h" #include "font.hpp"
#include "../log.hpp"
using namespace game::util;
namespace game::resource namespace game::resource
{ {
Font::Font(const std::string& path, float size) Font::Font(const std::filesystem::path& path, float size)
{ {
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size); ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", path.c_str()));
else
logger.error(std::format("Failed to initialize font: {}", path.c_str()));
} }
Font::Font(const physfs::Path& path, float size)
{
if (!path.is_valid())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromMemoryTTF(buffer.data(), (int)buffer.size(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", path.c_str()));
else
logger.error(std::format("Failed to initialize font: {}", path.c_str()));
}
ImFont* Font::get() { return internal; }; ImFont* Font::get() { return internal; };
} }

View File

@@ -1,20 +1,28 @@
#pragma once #pragma once
#include "imgui.h" #include <filesystem>
#include <string>
#include <imgui.h>
#include "../util/physfs.hpp"
namespace game::resource namespace game::resource
{ {
class Font class Font
{ {
ImFont* internal{};
public: public:
ImFont* internal; static constexpr auto NORMAL = 20;
static constexpr auto ABOVE_AVERAGE = 24;
static constexpr auto BIG = 30;
static constexpr auto HEADER_3 = 40;
static constexpr auto HEADER_2 = 50;
static constexpr auto HEADER_1 = 60;
static constexpr auto NORMAL = 12; Font() = default;
static constexpr auto BIG = 16; Font(const std::filesystem::path&, float = NORMAL);
static constexpr auto LARGE = 24; Font(const util::physfs::Path&, float = NORMAL);
Font(const std::string&, float = NORMAL);
ImFont* get(); ImFont* get();
}; };
} }

View File

@@ -1,6 +1,6 @@
#include "shader.h" #include "shader.hpp"
#include <iostream> #include "../log.hpp"
namespace game::resource namespace game::resource
{ {
@@ -20,7 +20,7 @@ namespace game::resource
glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength); glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength);
std::string log(logLength, '\0'); std::string log(logLength, '\0');
if (logLength > 0) glGetShaderInfoLog(shaderHandle, logLength, nullptr, log.data()); if (logLength > 0) glGetShaderInfoLog(shaderHandle, logLength, nullptr, log.data());
std::cout << "Failed to compile shader: " << log << '\n'; logger.error(std::format("Failed to compile shader: {}", log));
return false; return false;
} }
return true; return true;
@@ -49,11 +49,11 @@ namespace game::resource
if (!isLinked) if (!isLinked)
{ {
glDeleteProgram(id); glDeleteProgram(id);
logger.error(std::format("Failed to link shader: {}", id));
id = 0; id = 0;
std::cout << "Failed to link shader: " << id << "\n";
} }
else else
std::cout << "Initialized shader: " << id << "\n"; logger.info(std::format("Initialized shader: {}", id));
glDeleteShader(vertexHandle); glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle); glDeleteShader(fragmentHandle);

View File

@@ -1,60 +1,75 @@
#include "texture.h" #include "texture.hpp"
#if defined(__clang__) || defined(__GNUC__) #include <SDL3/SDL_surface.h>
#pragma GCC diagnostic push #include <string>
#pragma GCC diagnostic ignored "-Wmissing-field-initializers" #include <unordered_map>
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#define STBI_ONLY_PNG #include "../log.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#include <iostream>
using namespace glm; using namespace glm;
using namespace game::util;
namespace game::resource namespace game::resource
{ {
struct CachedTexture
{
std::weak_ptr<GLuint> idShared{};
glm::ivec2 size{};
int channels{};
};
static std::unordered_map<std::string, CachedTexture> textureCache{};
static bool cache_get(const std::string& key, std::shared_ptr<GLuint>& idShared, GLuint& id, ivec2& size, int& channels)
{
auto it = textureCache.find(key);
if (it == textureCache.end()) return false;
auto shared = it->second.idShared.lock();
if (!shared)
{
textureCache.erase(it);
return false;
}
idShared = shared;
id = *shared;
size = it->second.size;
channels = it->second.channels;
return true;
}
static void cache_set(const std::string& key, const std::shared_ptr<GLuint>& idShared, ivec2 size, int channels)
{
if (!idShared) return;
textureCache[key] = CachedTexture{.idShared = idShared, .size = size, .channels = channels};
}
static std::shared_ptr<GLuint> texture_id_make(GLuint id)
{
return std::shared_ptr<GLuint>(new GLuint(id),
[](GLuint* p)
{
if (!p) return;
if (*p != 0) glDeleteTextures(1, p);
delete p;
});
}
bool Texture::is_valid() const { return id != 0; } bool Texture::is_valid() const { return id != 0; }
void Texture::retain() Texture::~Texture()
{ {
if (refCount) ++(*refCount); idShared.reset();
}
void Texture::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (is_valid()) glDeleteTextures(1, &id);
delete refCount;
}
refCount = nullptr;
}
else if (is_valid())
{
glDeleteTextures(1, &id);
}
id = 0; id = 0;
} }
Texture::~Texture() { release(); }
Texture::Texture(const Texture& other) Texture::Texture(const Texture& other)
{ {
id = other.id; id = other.id;
size = other.size; size = other.size;
channels = other.channels; channels = other.channels;
refCount = other.refCount; idShared = other.idShared;
retain();
} }
Texture::Texture(Texture&& other) noexcept Texture::Texture(Texture&& other) noexcept
@@ -62,24 +77,21 @@ namespace game::resource
id = other.id; id = other.id;
size = other.size; size = other.size;
channels = other.channels; channels = other.channels;
refCount = other.refCount; idShared = std::move(other.idShared);
other.id = 0; other.id = 0;
other.size = {}; other.size = {};
other.channels = 0; other.channels = 0;
other.refCount = nullptr;
} }
Texture& Texture::operator=(const Texture& other) Texture& Texture::operator=(const Texture& other)
{ {
if (this != &other) if (this != &other)
{ {
release(); idShared = other.idShared;
id = other.id; id = other.id;
size = other.size; size = other.size;
channels = other.channels; channels = other.channels;
refCount = other.refCount;
retain();
} }
return *this; return *this;
} }
@@ -88,45 +100,103 @@ namespace game::resource
{ {
if (this != &other) if (this != &other)
{ {
release(); idShared.reset();
id = other.id; id = other.id;
size = other.size; size = other.size;
channels = other.channels; channels = other.channels;
refCount = other.refCount; idShared = std::move(other.idShared);
other.id = 0; other.id = 0;
other.size = {}; other.size = {};
other.channels = 0; other.channels = 0;
other.refCount = nullptr;
} }
return *this; return *this;
} }
void Texture::init(const uint8_t* data)
{
idShared.reset();
id = 0;
GLuint newId{};
glGenTextures(1, &newId);
glBindTexture(GL_TEXTURE_2D, newId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
channels = CHANNELS;
idShared = texture_id_make(newId);
id = newId;
}
Texture::Texture(const std::filesystem::path& path) Texture::Texture(const std::filesystem::path& path)
{ {
if (auto data = stbi_load(path.c_str(), &size.x, &size.y, nullptr, CHANNELS); data) auto key = std::string("fs:") + path.string();
if (cache_get(key, idShared, id, size, channels))
{ {
glGenTextures(1, &id); logger.info(std::format("Using cached texture: {}", path.string()));
glBindTexture(GL_TEXTURE_2D, id); return;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); }
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); if (auto surface = SDL_LoadPNG(path.c_str()))
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); {
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); SDL_DestroySurface(surface);
glBindTexture(GL_TEXTURE_2D, 0); surface = rgbaSurface;
stbi_image_free(data); this->size = ivec2(surface->w, surface->h);
channels = CHANNELS; init((const uint8_t*)surface->pixels);
refCount = new int(1); SDL_DestroySurface(surface);
std::cout << "Initialized texture: '" << path.string() << "\n"; cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", path.string()));
} }
else else
logger.error(std::format("Failed to initialize texture: {} ({})", path.string(), SDL_GetError()));
}
Texture::Texture(const physfs::Path& path)
{
if (!path.is_valid())
{ {
id = 0; logger.error(
size = {}; std::format("Failed to initialize texture from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
channels = 0; return;
refCount = nullptr;
std::cout << "Failed to initialize texture: '" << path.string() << "'\n";
} }
auto key = std::string("physfs:") + path.c_str();
if (cache_get(key, idShared, id, size, channels))
{
logger.info(std::format("Using cached texture: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize texture from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
if (auto surface = SDL_LoadPNG_IO(ioStream, true))
{
auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(surface);
surface = rgbaSurface;
this->size = ivec2(surface->w, surface->h);
init((const uint8_t*)surface->pixels);
SDL_DestroySurface(surface);
cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", path.c_str()));
}
else
logger.error(std::format("Failed to initialize texture: {} ({})", path.c_str(), SDL_GetError()));
} }
} }

View File

@@ -6,8 +6,12 @@
#include <glad/glad.h> #include <glad/glad.h>
#endif #endif
#include <cstdint>
#include <filesystem> #include <filesystem>
#include <glm/ext/vector_int2.hpp> #include <glm/ext/vector_int2.hpp>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource namespace game::resource
{ {
@@ -29,10 +33,10 @@ namespace game::resource
Texture& operator=(const Texture&); Texture& operator=(const Texture&);
Texture& operator=(Texture&&) noexcept; Texture& operator=(Texture&&) noexcept;
Texture(const std::filesystem::path&); Texture(const std::filesystem::path&);
Texture(const util::physfs::Path&);
void init(const uint8_t*);
private: private:
int* refCount{nullptr}; std::shared_ptr<GLuint> idShared{};
void retain();
void release();
}; };
} }

View File

@@ -0,0 +1,11 @@
#include "animation_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
const std::string& AnimationEntryCollection::get()
{
return at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).animation;
}
}

View File

@@ -0,0 +1,23 @@
// Handles animation entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include <string>
#include <vector>
namespace game::resource::xml
{
class AnimationEntry
{
public:
std::string animation{};
float weight{1.0f};
};
class AnimationEntryCollection : public std::vector<AnimationEntry>
{
public:
const std::string& get();
};
}

338
src/resource/xml/anm2.cpp Normal file
View File

@@ -0,0 +1,338 @@
#include "anm2.hpp"
#include "util.hpp"
#include "../../util/working_directory.hpp"
#include "../../log.hpp"
#include <ranges>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Anm2::Anm2(const Anm2&) = default;
Anm2::Anm2(Anm2&&) = default;
Anm2& Anm2::operator=(const Anm2&) = default;
Anm2& Anm2::operator=(Anm2&&) = default;
void Anm2::init(XMLDocument& document, Flags flags, const physfs::Path& archive)
{
this->flags = flags;
auto element = document.RootElement();
if (!element) return;
auto parse_frame = [&](XMLElement* element, Type type, int spritesheetID = -1)
{
Frame frame{};
if (!element) return frame;
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &frame.position.x);
element->QueryFloatAttribute("YPosition", &frame.position.y);
if (type == LAYER)
{
if (element->FindAttribute("RegionId") && spritesheets.contains(spritesheetID))
{
auto& spritesheet = spritesheets.at(spritesheetID);
element->QueryIntAttribute("RegionId", &frame.regionID);
auto& region = spritesheet.regions.at(frame.regionID);
frame.crop = region.crop;
frame.size = region.size;
frame.pivot = region.origin == Spritesheet::Region::Origin::CENTER
? glm::vec2(glm::ivec2(frame.size * 0.5f))
: region.origin == Spritesheet::Region::Origin::TOP_LEFT ? glm::vec2{}
: region.pivot;
}
else
{
element->QueryFloatAttribute("XPivot", &frame.pivot.x);
element->QueryFloatAttribute("YPivot", &frame.pivot.y);
element->QueryFloatAttribute("XCrop", &frame.crop.x);
element->QueryFloatAttribute("YCrop", &frame.crop.y);
element->QueryFloatAttribute("Width", &frame.size.x);
element->QueryFloatAttribute("Height", &frame.size.y);
}
}
element->QueryFloatAttribute("XScale", &frame.scale.x);
element->QueryFloatAttribute("YScale", &frame.scale.y);
element->QueryIntAttribute("Delay", &frame.duration);
element->QueryBoolAttribute("Visible", &frame.isVisible);
xml::query_color_attribute(element, "RedTint", &frame.tint.r);
xml::query_color_attribute(element, "GreenTint", &frame.tint.g);
xml::query_color_attribute(element, "BlueTint", &frame.tint.b);
xml::query_color_attribute(element, "AlphaTint", &frame.tint.a);
xml::query_color_attribute(element, "RedOffset", &frame.colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &frame.colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &frame.colorOffset.b);
element->QueryFloatAttribute("Rotation", &frame.rotation);
element->QueryBoolAttribute("Interpolated", &frame.isInterpolated);
}
else
{
for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundID{};
child->QueryIntAttribute("Id", &soundID);
frame.soundIDs.emplace_back(soundID);
}
element->QueryIntAttribute("EventId", &frame.eventID);
element->QueryIntAttribute("AtFrame", &frame.atFrame);
}
return frame;
};
auto parse_item = [&](XMLElement* element, Type type, int& id)
{
Item item{};
if (!element) return item;
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &item.isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
item.frames.emplace_back(parse_frame(child, type, type == LAYER ? layers.at(id).spritesheetID : -1));
return item;
};
auto parse_animation = [&](XMLElement* element)
{
Animation animation{};
if (!element) return animation;
xml::query_string_attribute(element, "Name", &animation.name);
element->QueryIntAttribute("FrameNum", &animation.frameNum);
element->QueryBoolAttribute("Loop", &animation.isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
animation.rootAnimation = parse_item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
auto layerAnimation = parse_item(child, LAYER, id);
animation.layerOrder.push_back(id);
animation.layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
auto nullAnimation = parse_item(child, NULL_, id);
animation.nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers"))
animation.triggers = parse_item(triggersElement, TRIGGER, id);
return animation;
};
if (auto infoElement = element->FirstChildElement("Info")) infoElement->QueryIntAttribute("Fps", &fps);
if (auto contentElement = element->FirstChildElement("Content"))
{
if (auto spritesheetsElement = contentElement->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet{};
child->QueryIntAttribute("Id", &spritesheetId);
xml::query_string_attribute(child, "Path", &spritesheet.path);
if ((this->flags & NO_SPRITESHEETS) != 0)
spritesheet.texture = Texture();
else if (!archive.empty())
spritesheet.texture = Texture(physfs::Path(archive + "/" + spritesheet.path));
else
spritesheet.texture = Texture(std::filesystem::path(spritesheet.path));
for (auto regionChild = child->FirstChildElement("Region"); regionChild;
regionChild = regionChild->NextSiblingElement("Region"))
{
Spritesheet::Region region{};
int regionID{};
regionChild->QueryIntAttribute("Id", &regionID);
xml::query_string_attribute(regionChild, "Name", &region.name);
regionChild->QueryFloatAttribute("XCrop", &region.crop.x);
regionChild->QueryFloatAttribute("YCrop", &region.crop.y);
regionChild->QueryFloatAttribute("Width", &region.size.x);
regionChild->QueryFloatAttribute("Height", &region.size.y);
if (regionChild->FindAttribute("Origin"))
{
std::string origin{};
xml::query_string_attribute(regionChild, "Origin", &origin);
region.origin = origin == "Center" ? Spritesheet::Region::CENTER
: origin == "TopLeft" ? Spritesheet::Region::TOP_LEFT
: Spritesheet::Region::CUSTOM;
}
else
{
regionChild->QueryFloatAttribute("XPivot", &region.pivot.x);
regionChild->QueryFloatAttribute("YPivot", &region.pivot.y);
}
spritesheet.regions.emplace(regionID, std::move(region));
spritesheet.regionOrder.emplace_back(regionID);
}
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = contentElement->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer{};
child->QueryIntAttribute("Id", &layerId);
xml::query_string_attribute(child, "Name", &layer.name);
child->QueryIntAttribute("SpritesheetId", &layer.spritesheetID);
layerMap[layer.name] = layerId;
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = contentElement->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null{};
child->QueryIntAttribute("Id", &nullId);
xml::query_string_attribute(child, "Name", &null.name);
child->QueryBoolAttribute("ShowRect", &null.isShowRect);
nullMap[null.name] = nullId;
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = contentElement->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event{};
child->QueryIntAttribute("Id", &eventId);
xml::query_string_attribute(child, "Name", &event.name);
eventMap[event.name] = eventId;
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = contentElement->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound{};
child->QueryIntAttribute("Id", &soundId);
xml::query_string_attribute(child, "Path", &sound.path);
if ((this->flags & NO_SOUNDS) != 0)
sound.audio = Audio();
else if (!archive.empty())
sound.audio = Audio(physfs::Path(archive + "/" + sound.path));
else
sound.audio = Audio(std::filesystem::path(sound.path));
sounds.emplace(soundId, std::move(sound));
}
}
}
if (auto animationsElement = element->FirstChildElement("Animations"))
{
xml::query_string_attribute(animationsElement, "DefaultAnimation", &defaultAnimation);
for (auto child = animationsElement->FirstChildElement("Animation"); child;
child = child->NextSiblingElement("Animation"))
{
if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0)
{
std::string name{};
xml::query_string_attribute(child, "Name", &name);
if (name == defaultAnimation)
{
animations.emplace_back(parse_animation(child));
break;
}
}
else
animations.emplace_back(parse_animation(child));
}
for (int i = 0; i < (int)animations.size(); i++)
{
auto& animation = animations[i];
animationMap[animation.name] = i;
animationMapReverse[i] = animation.name;
}
if (animationMap.contains(defaultAnimation))
defaultAnimationID = animationMap[defaultAnimation];
else
defaultAnimationID = -1;
}
isValid = true;
}
Anm2::Anm2(const std::filesystem::path& path, Flags flags)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to initialize anm2: {} ({})", path.string(), document.ErrorStr()));
isValid = false;
return;
}
WorkingDirectory workingDirectory(path, WorkingDirectory::FILE);
this->path = path.string();
init(document, flags);
logger.info(std::format("Initialized anm2: {}", path.string()));
}
Anm2::Anm2(const physfs::Path& path, Flags flags)
{
XMLDocument document;
if (xml::document_load(path, document) != XML_SUCCESS)
{
isValid = false;
return;
}
this->path = path;
init(document, flags, path.directory_get());
logger.info(std::format("Initialized anm2: {}", path.c_str()));
}
bool Anm2::is_valid() const { return isValid; }
}

177
src/resource/xml/anm2.hpp Normal file
View File

@@ -0,0 +1,177 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "../audio.hpp"
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Anm2
{
public:
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
enum Flag
{
NO_SPRITESHEETS = (1 << 0),
NO_SOUNDS = (1 << 1),
DEFAULT_ANIMATION_ONLY = (1 << 2)
};
using Flags = int;
struct Spritesheet
{
struct Region
{
enum Origin
{
TOP_LEFT,
CENTER,
CUSTOM
};
std::string name{};
glm::vec2 crop{};
glm::vec2 pivot{};
glm::vec2 size{};
Origin origin{CUSTOM};
};
std::string path{};
resource::Texture texture{};
std::map<int, Region> regions{};
std::vector<int> regionOrder{};
};
struct Sound
{
std::string path{};
resource::Audio audio{};
};
struct Layer
{
std::string name{"New Layer"};
int spritesheetID{-1};
};
struct Null
{
std::string name{"New Null"};
bool isShowRect{};
};
struct Event
{
std::string name{"New Event"};
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int regionID{-1};
std::vector<int> soundIDs{};
int atFrame{-1};
bool isVisible{true};
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
struct Item
{
std::vector<Frame> frames{};
bool isVisible{};
};
struct Animation
{
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
};
int fps{30};
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
std::unordered_map<std::string, int> layerMap{};
std::unordered_map<std::string, int> nullMap{};
std::unordered_map<std::string, int> eventMap{};
std::string defaultAnimation{};
int defaultAnimationID{-1};
std::vector<Animation> animations{};
std::unordered_map<std::string, int> animationMap{};
std::unordered_map<int, std::string> animationMapReverse{};
std::string path{};
bool isValid{};
Flags flags{};
Anm2() = default;
Anm2(const Anm2&);
Anm2(Anm2&&);
Anm2& operator=(const Anm2&);
Anm2& operator=(Anm2&&);
Anm2(const std::filesystem::path&, Flags = 0);
Anm2(const util::physfs::Path&, Flags = 0);
bool is_valid() const;
private:
void init(tinyxml2::XMLDocument& document, Flags flags, const util::physfs::Path& archive = {});
};
}

45
src/resource/xml/area.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "area.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Area::Area(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
for (auto child = root->FirstChildElement("Area"); child; child = child->NextSiblingElement("Area"))
{
Entry area{};
query_texture(child, "Texture", archive, textureRootPath, area.texture);
child->QueryFloatAttribute("Gravity", &area.gravity);
child->QueryFloatAttribute("Friction", &area.friction);
areas.emplace_back(std::move(area));
}
}
if (areas.empty()) areas.emplace_back(Entry());
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Area::is_valid() const { return isValid; };
}

29
src/resource/xml/area.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Area
{
public:
struct Entry
{
Texture texture{};
float gravity{0.95f};
float friction{0.80f};
float airResistance{0.975f};
};
std::vector<Entry> areas{};
bool isValid{};
Area() = default;
Area(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,233 @@
#include "character.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Character::Character(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
query_string_attribute(root, "Name", &name);
root->QueryFloatAttribute("Weight", &weight);
root->QueryFloatAttribute("WeightMin", &weightMin);
root->QueryFloatAttribute("WeightMax", &weightMax);
root->QueryFloatAttribute("Capacity", &capacity);
root->QueryFloatAttribute("CapacityMin", &capacityMin);
root->QueryFloatAttribute("CapacityMax", &capacityMax);
root->QueryFloatAttribute("CapacityMaxMultiplier", &capacityMaxMultiplier);
root->QueryFloatAttribute("CapacityIfOverStuffedOnDigestBonus", &capacityIfOverStuffedOnDigestBonus);
root->QueryFloatAttribute("CaloriesToKilogram", &caloriesToKilogram);
root->QueryFloatAttribute("DigestionRate", &digestionRate);
root->QueryFloatAttribute("DigestionRateMin", &digestionRateMin);
root->QueryFloatAttribute("DigestionRateMax", &digestionRateMax);
root->QueryIntAttribute("DigestionTimerMax", &digestionTimerMax);
root->QueryFloatAttribute("EatSpeed", &eatSpeed);
root->QueryFloatAttribute("EatSpeedMin", &eatSpeedMin);
root->QueryFloatAttribute("EatSpeedMax", &eatSpeedMax);
root->QueryFloatAttribute("BlinkChance", &blinkChance);
root->QueryFloatAttribute("GurgleChance", &gurgleChance);
root->QueryFloatAttribute("GurgleCapacityMultiplier", &gurgleCapacityMultiplier);
auto dialoguePath = physfs::Path(archive + "/" + "dialogue.xml");
if (!dialoguePath.is_valid())
logger.warning(std::format("No character dialogue.xml file found: {}", path.string()));
else
dialogue = Dialogue(dialoguePath);
dialogue.query_pool_id(root, "DialoguePoolID", pool.id);
if (auto element = root->FirstChildElement("AlternateSpritesheet"))
{
query_texture(element, "Texture", archive, textureRootPath, alternateSpritesheet.texture);
query_sound(element, "Sound", archive, soundRootPath, alternateSpritesheet.sound);
element->QueryIntAttribute("ID", &alternateSpritesheet.id);
element->QueryFloatAttribute("ChanceOnNewGame", &alternateSpritesheet.chanceOnNewGame);
}
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "FinishFood", animations.finishFood);
query_animation_entry_collection(element, "PostDigest", animations.postDigest);
if (auto child = element->FirstChildElement("Idle"))
query_string_attribute(child, "Animation", &animations.idle);
if (auto child = element->FirstChildElement("IdleFull"))
query_string_attribute(child, "Animation", &animations.idleFull);
if (auto child = element->FirstChildElement("StageUp"))
query_string_attribute(child, "Animation", &animations.stageUp);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Digest", archive, soundRootPath, sounds.digest);
query_sound_entry_collection(element, "Gurgle", archive, soundRootPath, sounds.gurgle);
}
if (auto element = root->FirstChildElement("Overrides"))
{
if (auto child = element->FirstChildElement("Talk"))
{
query_layer_id(child, "LayerSource", anm2, talkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, talkOverride.layerDestination);
}
if (auto child = element->FirstChildElement("Blink"))
{
query_layer_id(child, "LayerSource", anm2, blinkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, blinkOverride.layerDestination);
}
}
if (auto element = root->FirstChildElement("Stages"))
{
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
{
Stage stage{};
child->QueryFloatAttribute("Threshold", &stage.threshold);
child->QueryIntAttribute("AreaID", &stage.areaID);
dialogue.query_pool_id(child, "DialoguePoolID", stage.pool.id);
stages.emplace_back(std::move(stage));
}
}
if (auto element = root->FirstChildElement("EatAreas"))
{
for (auto child = element->FirstChildElement("EatArea"); child; child = child->NextSiblingElement("EatArea"))
{
EatArea eatArea{};
query_null_id(child, "Null", anm2, eatArea.nullID);
query_event_id(child, "Event", anm2, eatArea.eventID);
query_string_attribute(child, "Animation", &eatArea.animation);
eatAreas.emplace_back(std::move(eatArea));
}
}
if (auto element = root->FirstChildElement("ExpandAreas"))
{
for (auto child = element->FirstChildElement("ExpandArea"); child;
child = child->NextSiblingElement("ExpandArea"))
{
ExpandArea expandArea{};
query_layer_id(child, "Layer", anm2, expandArea.layerID);
query_null_id(child, "Null", anm2, expandArea.nullID);
child->QueryFloatAttribute("ScaleAdd", &expandArea.scaleAdd);
expandAreas.emplace_back(std::move(expandArea));
}
}
if (auto element = root->FirstChildElement("InteractAreas"))
{
for (auto child = element->FirstChildElement("InteractArea"); child;
child = child->NextSiblingElement("InteractArea"))
{
InteractArea interactArea{};
if (child->FindAttribute("Layer")) query_layer_id(child, "Layer", anm2, interactArea.layerID);
query_null_id(child, "Null", anm2, interactArea.nullID);
query_string_attribute(child, "Animation", &interactArea.animation);
query_string_attribute(child, "AnimationFull", &interactArea.animationFull);
query_string_attribute(child, "AnimationCursorHover", &interactArea.animationCursorHover);
query_string_attribute(child, "AnimationCursorActive", &interactArea.animationCursorActive);
query_sound_entry_collection(child, "Sound", archive, soundRootPath, interactArea.sound, "Path");
dialogue.query_pool_id(child, "DialoguePoolID", interactArea.pool.id);
child->QueryFloatAttribute("DigestionBonusRub", &interactArea.digestionBonusRub);
child->QueryFloatAttribute("DigestionBonusClick", &interactArea.digestionBonusClick);
child->QueryFloatAttribute("Time", &interactArea.time);
child->QueryFloatAttribute("ScaleEffectAmplitude", &interactArea.scaleEffectAmplitude);
child->QueryFloatAttribute("ScaleEffectCycles", &interactArea.scaleEffectCycles);
std::string typeString{};
query_string_attribute(child, "Type", &typeString);
for (int i = 0; i < (int)std::size(INTERACT_TYPE_STRINGS); i++)
if (typeString == INTERACT_TYPE_STRINGS[i]) interactArea.type = (InteractType)i;
interactAreas.emplace_back(std::move(interactArea));
}
}
}
auto itemSchemaPath = physfs::Path(archive + "/" + "items.xml");
if (auto itemSchemaPath = physfs::Path(archive + "/" + "items.xml"); itemSchemaPath.is_valid())
itemSchema = Item(itemSchemaPath);
else
logger.warning(std::format("No character items.xml file found: {}", path.string()));
if (auto areaSchemaPath = physfs::Path(archive + "/" + "areas.xml"); areaSchemaPath.is_valid())
areaSchema = Area(areaSchemaPath);
else
logger.warning(std::format("No character areas.xml file found: {}", path.string()));
if (auto menuSchemaPath = physfs::Path(archive + "/" + "menu.xml"); menuSchemaPath.is_valid())
menuSchema = Menu(menuSchemaPath);
else
logger.warning(std::format("No character menu.xml file found: {}", path.string()));
if (auto cursorSchemaPath = physfs::Path(archive + "/" + "cursor.xml"); cursorSchemaPath.is_valid())
cursorSchema = Cursor(cursorSchemaPath);
else
logger.warning(std::format("No character cursor.xml file found: {}", path.string()));
if (auto playSchemaPath = physfs::Path(archive + "/" + "play.xml"); playSchemaPath.is_valid())
playSchema = Play(playSchemaPath, dialogue);
else
logger.warning(std::format("No character play.xml file found: {}", path.string()));
logger.info(std::format("Initialized character: {}", name));
this->path = path;
save = Save(save_path_get());
}
std::filesystem::path Character::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
}

View File

@@ -0,0 +1,144 @@
#pragma once
#include <filesystem>
#include <vector>
#include "../../util/interact_type.hpp"
#include "../audio.hpp"
#include "animation_entry.hpp"
#include "anm2.hpp"
#include "area.hpp"
#include "cursor.hpp"
#include "dialogue.hpp"
#include "item.hpp"
#include "menu.hpp"
#include "play.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class Character
{
public:
struct Stage
{
float threshold{};
int areaID{};
Dialogue::PoolReference pool{-1};
};
struct EatArea
{
int nullID{-1};
int eventID{-1};
std::string animation{};
};
struct ExpandArea
{
int layerID{-1};
int nullID{-1};
float scaleAdd{};
};
struct InteractArea
{
std::string animation{};
std::string animationFull{};
std::string animationCursorActive{};
std::string animationCursorHover{};
SoundEntryCollection sound{};
int nullID{-1};
int layerID{-1};
InteractType type{(InteractType)-1};
Dialogue::PoolReference pool{-1};
float digestionBonusRub{};
float digestionBonusClick{};
float time{};
float scaleEffectAmplitude{};
float scaleEffectCycles{};
};
struct Animations
{
AnimationEntryCollection finishFood{};
AnimationEntryCollection postDigest{};
std::string idle{};
std::string idleFull{};
std::string stageUp{};
};
struct Sounds
{
SoundEntryCollection gurgle{};
SoundEntryCollection digest{};
};
struct Override
{
int layerSource{};
int layerDestination{};
};
struct AlternateSpritesheet
{
Texture texture{};
Audio sound{};
int id{-1};
float chanceOnNewGame{0.001};
};
Anm2 anm2{};
Area areaSchema{};
Dialogue dialogue{};
Item itemSchema{};
Menu menuSchema{};
Cursor cursorSchema{};
Play playSchema{};
Save save{};
Animations animations{};
Override talkOverride{};
Override blinkOverride{};
Sounds sounds{};
std::vector<Stage> stages{};
std::vector<ExpandArea> expandAreas{};
std::vector<EatArea> eatAreas{};
std::vector<InteractArea> interactAreas{};
AlternateSpritesheet alternateSpritesheet{};
std::string name{};
std::filesystem::path path{};
float weight{50};
float weightMin{};
float weightMax{999};
float capacity{2000.0f};
float capacityMin{2000.0f};
float capacityMax{99999.0f};
float capacityMaxMultiplier{1.5f};
float capacityIfOverStuffedOnDigestBonus{0.25f};
float caloriesToKilogram{1000.0f};
float digestionRate{0.05f};
float digestionRateMin{0.0f};
float digestionRateMax{0.25f};
int digestionTimerMax{60};
float eatSpeed{1.0f};
float eatSpeedMin{1.0f};
float eatSpeedMax{3.0f};
float blinkChance{1.0f};
float gurgleChance{1.0f};
float gurgleCapacityMultiplier{1.0f};
Dialogue::PoolReference pool{-1};
Character() = default;
Character(const std::filesystem::path&);
std::filesystem::path save_path_get();
};
}

View File

@@ -0,0 +1,66 @@
#include "character_preview.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
CharacterPreview::CharacterPreview(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character preview from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2, Anm2::NO_SOUNDS | Anm2::DEFAULT_ANIMATION_ONLY);
query_texture(root, "Render", archive, textureRootPath, render);
query_texture(root, "Portrait", archive, textureRootPath, portrait);
query_string_attribute(root, "Name", &name);
query_string_attribute(root, "Description", &description);
query_string_attribute(root, "Author", &author);
root->QueryFloatAttribute("Weight", &weight);
if (auto element = root->FirstChildElement("Stages"))
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
stages++;
}
this->path = path;
save = Save(save_path_get());
isValid = true;
logger.info(std::format("Initialized character preview: {}", name));
}
std::filesystem::path CharacterPreview::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
bool CharacterPreview::is_valid() const { return isValid; }
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include "anm2.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class CharacterPreview
{
public:
struct Stage
{
float threshold{};
int dialoguePoolID{-1};
};
Anm2 anm2{};
Texture portrait{};
Texture render{};
Save save{};
int stages{1};
std::string name{};
std::string author{};
std::string description{};
std::filesystem::path path{};
float weight{50};
bool isValid{};
CharacterPreview() = default;
CharacterPreview(const std::filesystem::path&);
std::filesystem::path save_path_get();
bool is_valid() const;
};
}

View File

@@ -0,0 +1,52 @@
#include "cursor.hpp"
#include "../../log.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Cursor::Cursor(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) 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);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "Idle", animations.idle);
query_animation_entry_collection(element, "Hover", animations.hover);
query_animation_entry_collection(element, "Grab", animations.grab);
query_animation_entry_collection(element, "Pan", animations.pan);
query_animation_entry_collection(element, "Zoom", animations.zoom);
query_animation_entry_collection(element, "Return", animations.return_);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Grab", archive, soundRootPath, sounds.grab);
query_sound_entry_collection(element, "Release", archive, soundRootPath, sounds.release);
query_sound_entry_collection(element, "Throw", archive, soundRootPath, sounds.throw_);
}
}
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Cursor::is_valid() const { return isValid; };
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "util.hpp"
namespace game::resource::xml
{
class Cursor
{
public:
struct Animations
{
AnimationEntryCollection idle{};
AnimationEntryCollection hover{};
AnimationEntryCollection grab{};
AnimationEntryCollection pan{};
AnimationEntryCollection zoom{};
AnimationEntryCollection return_{};
};
struct Sounds
{
SoundEntryCollection grab{};
SoundEntryCollection release{};
SoundEntryCollection throw_{};
};
Animations animations{};
Sounds sounds{};
Anm2 anm2{};
bool isValid{};
Cursor() = default;
Cursor(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,165 @@
#include "dialogue.hpp"
#include "util.hpp"
#include "../../log.hpp"
#include "../../util/math.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
void Dialogue::query_entry_id(XMLElement* element, const char* name, int& id)
{
std::string entryID{};
query_string_attribute(element, name, &entryID);
if (entryIDMap.contains(entryID))
id = entryIDMap.at(entryID);
else if (entryID.empty())
entryID = -1;
else
{
logger.warning("Dialogue entries does not contain: " + entryID);
id = -1;
}
}
void Dialogue::query_pool_id(XMLElement* element, const char* name, int& id)
{
std::string poolID{};
query_string_attribute(element, name, &poolID);
if (poolMap.contains(poolID))
id = poolMap.at(poolID);
else if (poolID.empty())
poolID = -1;
else
{
logger.warning("Dialogue pools does not contain: " + poolID);
id = -1;
}
}
Dialogue::Dialogue(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
if (auto element = root->FirstChildElement("Entries"))
{
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
entryIDMap.emplace(stringID, id);
entryIDMapReverse.emplace(id, stringID);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
Entry entry{};
entry.name = entryIDMapReverse.at(id);
query_string_attribute(child, "Text", &entry.text);
query_string_attribute(child, "Animation", &entry.animation);
if (child->FindAttribute("Next"))
{
std::string nextID{};
query_string_attribute(child, "Next", &nextID);
if (!entryIDMap.contains(nextID))
logger.warning(std::format("Dialogue: next ID does not point to a valid Entry! ({})", nextID));
else
entry.nextID = entryIDMap.at(nextID);
}
for (auto choiceChild = child->FirstChildElement("Choice"); choiceChild;
choiceChild = choiceChild->NextSiblingElement("Choice"))
{
Choice choice{};
query_entry_id(choiceChild, "Next", choice.nextID);
query_string_attribute(choiceChild, "Text", &choice.text);
entry.choices.emplace_back(std::move(choice));
}
entries.emplace_back(std::move(entry));
id++;
}
}
if (auto element = root->FirstChildElement("Pools"))
{
int id{};
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
Pool pool{};
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
poolMap[stringID] = id;
pools.emplace_back(pool);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
auto& pool = pools.at(id);
for (auto entryChild = child->FirstChildElement("PoolEntry"); entryChild;
entryChild = entryChild->NextSiblingElement("PoolEntry"))
{
int entryID{};
query_entry_id(entryChild, "ID", entryID);
pool.emplace_back(entryID);
}
id++;
}
logger.info(std::format("Initialized dialogue: {}", path.c_str()));
isValid = true;
}
if (auto element = root->FirstChildElement("Start"))
{
query_entry_id(element, "ID", start.id);
query_string_attribute(element, "Animation", &start.animation);
}
if (auto element = root->FirstChildElement("End")) query_entry_id(element, "ID", end.id);
if (auto element = root->FirstChildElement("Help")) query_entry_id(element, "ID", help.id);
if (auto element = root->FirstChildElement("Digest")) query_pool_id(element, "PoolID", digest.id);
if (auto element = root->FirstChildElement("Eat")) query_pool_id(element, "PoolID", eat.id);
if (auto element = root->FirstChildElement("EatFull")) query_pool_id(element, "PoolID", eatFull.id);
if (auto element = root->FirstChildElement("Feed")) query_pool_id(element, "PoolID", feed.id);
if (auto element = root->FirstChildElement("FeedFull")) query_pool_id(element, "PoolID", feedFull.id);
if (auto element = root->FirstChildElement("FoodTaken")) query_pool_id(element, "PoolID", foodTaken.id);
if (auto element = root->FirstChildElement("FoodTakenFull")) query_pool_id(element, "PoolID", foodTakenFull.id);
if (auto element = root->FirstChildElement("Full")) query_pool_id(element, "PoolID", full.id);
if (auto element = root->FirstChildElement("LowCapacity")) query_pool_id(element, "PoolID", lowCapacity.id);
if (auto element = root->FirstChildElement("Random")) query_pool_id(element, "PoolID", random.id);
if (auto element = root->FirstChildElement("Throw")) query_pool_id(element, "PoolID", throw_.id);
if (auto element = root->FirstChildElement("StageUp")) query_pool_id(element, "PoolID", stageUp.id);
}
}
int Dialogue::Pool::get() const { return this->at(math::random_max(this->size())); }
Dialogue::Entry* Dialogue::get(int id) { return &entries.at(id); }
Dialogue::Entry* Dialogue::get(Dialogue::EntryReference& entry) { return &entries.at(entry.id); }
Dialogue::Entry* Dialogue::get(const std::string& string) { return &entries.at(entryIDMap.at(string)); }
Dialogue::Entry* Dialogue::get(Dialogue::PoolReference& pool) { return &entries.at(pools.at(pool.id).get()); }
Dialogue::Entry* Dialogue::get(Dialogue::Pool& pool) { return &entries.at(pool.get()); }
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <tinyxml2.h>
#include <string>
#include <map>
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Dialogue
{
public:
struct Choice
{
std::string text{};
int nextID{-1};
};
struct Entry
{
std::string name{};
std::string animation{};
std::string text{};
std::vector<Choice> choices{};
int nextID{-1};
inline bool is_last() const { return choices.empty() && nextID == -1; };
};
struct EntryReference
{
int id{-1};
std::string animation{};
inline bool is_valid() const { return id != -1; };
};
class PoolReference
{
public:
int id{-1};
inline bool is_valid() const { return id != -1; };
};
class Pool : public std::vector<int>
{
public:
int get() const;
};
std::map<std::string, int> entryIDMap;
std::map<int, std::string> entryIDMapReverse;
std::vector<Entry> entries{};
std::vector<Pool> pools{};
std::map<std::string, int> poolMap{};
EntryReference start{-1};
EntryReference end{-1};
EntryReference help{-1};
PoolReference digest{-1};
PoolReference eatFull{-1};
PoolReference eat{-1};
PoolReference feedFull{-1};
PoolReference feed{-1};
PoolReference foodTakenFull{-1};
PoolReference foodTaken{-1};
PoolReference full{-1};
PoolReference random{-1};
PoolReference lowCapacity{-1};
PoolReference throw_{-1};
PoolReference stageUp{-1};
bool isValid{};
Dialogue() = default;
Dialogue(const util::physfs::Path&);
Entry* get(const std::string&);
Entry* get(int id);
Entry* get(Dialogue::EntryReference&);
Entry* get(Dialogue::Pool&);
Entry* get(Dialogue::PoolReference&);
void query_entry_id(tinyxml2::XMLElement* element, const char* name, int& id);
void query_pool_id(tinyxml2::XMLElement* element, const char* name, int& id);
inline bool is_valid() const { return isValid; };
};
}

156
src/resource/xml/item.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "item.hpp"
#include <ranges>
#include <tinyxml2.h>
#include <algorithm>
#include <utility>
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Item::Item(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) 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);
query_anm2(root, "BaseAnm2", archive, textureRootPath, baseAnm2, Anm2::NO_SPRITESHEETS);
if (auto element = root->FirstChildElement("Categories"))
{
for (auto child = element->FirstChildElement("Category"); child; child = child->NextSiblingElement("Category"))
{
Category category{};
query_string_attribute(child, "Name", &category.name);
query_bool_attribute(child, "IsEdible", &category.isEdible);
categoryMap[category.name] = (int)categories.size();
categories.push_back(category);
}
}
if (auto element = root->FirstChildElement("Rarities"))
{
for (auto child = element->FirstChildElement("Rarity"); child; child = child->NextSiblingElement("Rarity"))
{
Rarity rarity{};
query_string_attribute(child, "Name", &rarity.name);
child->QueryFloatAttribute("Chance", &rarity.chance);
query_bool_attribute(child, "IsHidden", &rarity.isHidden);
query_sound(child, "Sound", archive, soundRootPath, rarity.sound);
rarityMap[rarity.name] = (int)rarities.size();
rarities.emplace_back(std::move(rarity));
}
}
if (auto element = root->FirstChildElement("Flavors"))
{
for (auto child = element->FirstChildElement("Flavor"); child; child = child->NextSiblingElement("Flavor"))
{
Flavor flavor{};
query_string_attribute(child, "Name", &flavor.name);
flavorMap[flavor.name] = (int)flavors.size();
flavors.push_back(flavor);
}
}
if (auto element = root->FirstChildElement("Animations"))
{
if (auto child = element->FirstChildElement("Chew"))
query_string_attribute(child, "Animation", &animations.chew);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Bounce", archive, soundRootPath, sounds.bounce);
query_sound_entry_collection(element, "Dispose", archive, soundRootPath, sounds.dispose);
query_sound_entry_collection(element, "Return", archive, soundRootPath, sounds.return_);
query_sound_entry_collection(element, "Summon", archive, soundRootPath, sounds.summon);
}
if (auto element = root->FirstChildElement("Items"))
{
std::string itemTextureRootPath{};
query_string_attribute(element, "TextureRootPath", &itemTextureRootPath);
element->QueryIntAttribute("ChewCount", &chewCount);
element->QueryIntAttribute("QuantityMax", &quantityMax);
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Entry item{};
query_string_attribute(child, "Name", &item.name);
query_string_attribute(child, "Description", &item.description);
query_float_optional_attribute(child, "Calories", item.calories);
query_float_optional_attribute(child, "DigestionBonus", item.digestionBonus);
query_float_optional_attribute(child, "EatSpeedBonus", item.eatSpeedBonus);
query_float_optional_attribute(child, "Gravity", item.gravity);
query_int_optional_attribute(child, "ChewCount", item.chewCount);
query_bool_attribute(child, "IsPlayReward", &item.isPlayReward);
query_bool_attribute(child, "IsToggleSpritesheet", &item.isToggleSpritesheet);
std::string categoryString{};
query_string_attribute(child, "Category", &categoryString);
item.categoryID = categoryMap.contains(categoryString) ? categoryMap[categoryString] : -1;
std::string rarityString{};
query_string_attribute(child, "Rarity", &rarityString);
item.rarityID = rarityMap.contains(rarityString) ? rarityMap[rarityString] : -1;
std::string flavorString{};
query_string_attribute(child, "Flavor", &flavorString);
if (flavorMap.contains(flavorString)) item.flavorID = flavorMap[flavorString];
Texture texture{};
query_texture(child, "Texture", archive, itemTextureRootPath, texture);
Anm2 anm2{baseAnm2};
if (child->FindAttribute("Anm2")) query_anm2(child, "Anm2", archive, textureRootPath, anm2);
anm2.spritesheets.at(0).texture = std::move(texture);
anm2s.emplace_back(std::move(anm2));
items.emplace_back(std::move(item));
}
}
}
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
pools[item.rarityID].emplace_back(i);
if (item.isPlayReward) rewardItemPool.emplace_back(i);
}
for (int i = 0; i < (int)rarities.size(); i++)
{
rarityIDsSortedByChance.emplace_back(i);
}
std::stable_sort(rarityIDsSortedByChance.begin(), rarityIDsSortedByChance.end(),
[&](int a, int b) { return rarities[a].chance > rarities[b].chance; });
isValid = true;
logger.info(std::format("Initialized item schema: {}", path.c_str()));
}
bool Item::is_valid() const { return isValid; };
}

98
src/resource/xml/item.hpp Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
#include <filesystem>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "../audio.hpp"
#include "anm2.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Item
{
public:
static constexpr auto UNDEFINED = "???";
struct Category
{
std::string name{};
bool isEdible{};
};
struct Rarity
{
std::string name{UNDEFINED};
float chance{};
bool isHidden{};
Audio sound{};
};
struct Flavor
{
std::string name{UNDEFINED};
};
struct Entry
{
std::string name{UNDEFINED};
std::string description{UNDEFINED};
int categoryID{};
int rarityID{};
std::optional<int> flavorID;
std::optional<float> calories{};
std::optional<float> eatSpeedBonus{};
std::optional<float> digestionBonus{};
std::optional<float> gravity{};
std::optional<int> chewCount{};
bool isPlayReward{};
bool isToggleSpritesheet{};
};
struct Animations
{
std::string chew{};
};
struct Sounds
{
SoundEntryCollection bounce{};
SoundEntryCollection return_{};
SoundEntryCollection dispose{};
SoundEntryCollection summon{};
};
std::unordered_map<std::string, int> categoryMap{};
std::unordered_map<std::string, int> rarityMap{};
std::unordered_map<std::string, int> flavorMap{};
using Pool = std::vector<int>;
std::vector<Category> categories{};
std::vector<Rarity> rarities{};
std::vector<Flavor> flavors{};
std::vector<Entry> items{};
std::vector<Anm2> anm2s{};
std::vector<int> rarityIDsSortedByChance{};
std::unordered_map<int, Pool> pools{};
Pool rewardItemPool{};
Animations animations{};
Sounds sounds{};
Anm2 baseAnm2{};
int chewCount{2};
int quantityMax{99};
bool isValid{};
Item() = default;
Item(const std::filesystem::path&);
Item(const util::physfs::Path&);
bool is_valid() const;
};
}

46
src/resource/xml/menu.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include "menu.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Menu::Menu(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
std::string fontRootPath{};
query_string_attribute(root, "FontRootPath", &fontRootPath);
query_font(root, "Font", archive, fontRootPath, font);
root->QueryFloatAttribute("Rounding", &rounding);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Open", archive, soundRootPath, sounds.open);
query_sound_entry_collection(element, "Close", archive, soundRootPath, sounds.close);
query_sound_entry_collection(element, "Hover", archive, soundRootPath, sounds.hover);
query_sound_entry_collection(element, "Select", archive, soundRootPath, sounds.select);
}
}
isValid = true;
logger.info(std::format("Initialized menu schema: {}", path.c_str()));
}
bool Menu::is_valid() const { return isValid; };
}

31
src/resource/xml/menu.hpp Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Menu
{
public:
struct Sounds
{
SoundEntryCollection open{};
SoundEntryCollection close{};
SoundEntryCollection hover{};
SoundEntryCollection select{};
};
Sounds sounds{};
Font font{};
float rounding{};
bool isValid{};
Menu() = default;
Menu(const util::physfs::Path&);
bool is_valid() const;
};
}

67
src/resource/xml/play.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "play.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Play::Play(const physfs::Path& path, Dialogue& dialogue)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
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("SpeedMin", &speedMin);
root->QueryFloatAttribute("SpeedMax", &speedMax);
root->QueryFloatAttribute("SpeedScoreBonus", &speedScoreBonus);
root->QueryIntAttribute("EndTimerMax", &endTimerMax);
root->QueryIntAttribute("EndTimerFailureMax", &endTimerFailureMax);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Fall", archive, soundRootPath, sounds.fall);
query_sound_entry_collection(element, "ScoreLoss", archive, soundRootPath, sounds.scoreLoss);
query_sound_entry_collection(element, "HighScore", archive, soundRootPath, sounds.highScore);
query_sound_entry_collection(element, "HighScoreLoss", archive, soundRootPath, sounds.highScoreLoss);
query_sound_entry_collection(element, "RewardScore", archive, soundRootPath, sounds.rewardScore);
}
if (auto element = root->FirstChildElement("Grades"))
{
for (auto child = element->FirstChildElement("Grade"); child; child = child->NextSiblingElement("Grade"))
{
Grade grade{};
query_string_attribute(child, "Name", &grade.name);
query_string_attribute(child, "NamePlural", &grade.namePlural);
child->QueryIntAttribute("Value", &grade.value);
child->QueryFloatAttribute("Weight", &grade.weight);
query_bool_attribute(child, "IsFailure", &grade.isFailure);
query_sound(child, "Sound", archive, soundRootPath, grade.sound);
dialogue.query_pool_id(child, "DialoguePoolID", grade.pool.id);
grades.emplace_back(std::move(grade));
}
}
}
isValid = true;
logger.info(std::format("Initialized play schema: {}", path.c_str()));
}
bool Play::is_valid() const { return isValid; };
}

56
src/resource/xml/play.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <string>
#include "util.hpp"
#include "dialogue.hpp"
namespace game::resource::xml
{
class Play
{
public:
struct Grade
{
std::string name{};
std::string namePlural{};
int value{};
float weight{};
bool isFailure{};
Audio sound{};
Dialogue::PoolReference pool{};
};
struct Sounds
{
SoundEntryCollection fall{};
SoundEntryCollection highScore{};
SoundEntryCollection highScoreLoss{};
SoundEntryCollection rewardScore{};
SoundEntryCollection scoreLoss{};
};
Sounds sounds{};
std::vector<Grade> grades{};
float rewardScoreBonus{0.01};
float rewardGradeBonus{0.05};
float speedMin{0.005};
float speedMax{0.075};
float speedScoreBonus{0.000025f};
float rangeBase{0.75};
float rangeMin{0.10};
float rangeScoreBonus{0.0005};
int endTimerMax{20};
int endTimerFailureMax{60};
int rewardScore{999};
bool isValid{};
Play() = default;
Play(const util::physfs::Path&, Dialogue&);
bool is_valid() const;
};
}

183
src/resource/xml/save.cpp Normal file
View File

@@ -0,0 +1,183 @@
#include "save.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace game::util;
using namespace tinyxml2;
namespace game::resource::xml
{
Save::Save(const std::filesystem::path& path)
{
XMLDocument document;
// Fail silently if there's no save.
auto result = document.LoadFile(path.c_str());
if (result == XML_ERROR_FILE_NOT_FOUND || result == XML_ERROR_FILE_COULD_NOT_BE_OPENED) return;
if (result != XML_SUCCESS)
{
logger.error(
std::format("Could not initialize character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
query_bool_attribute(root, "IsPostgame", &isPostgame);
query_bool_attribute(root, "IsAlternateSpritesheet", &isAlternateSpritesheet);
if (auto element = root->FirstChildElement("Character"))
{
element->QueryFloatAttribute("Weight", &weight);
element->QueryFloatAttribute("Calories", &calories);
element->QueryFloatAttribute("Capacity", &capacity);
element->QueryFloatAttribute("DigestionRate", &digestionRate);
element->QueryFloatAttribute("EatSpeed", &eatSpeed);
query_bool_attribute(element, "IsDigesting", &isDigesting);
element->QueryFloatAttribute("DigestionProgress", &digestionProgress);
element->QueryIntAttribute("DigestionTimer", &digestionTimer);
element->QueryFloatAttribute("TotalCaloriesConsumed", &totalCaloriesConsumed);
element->QueryIntAttribute("TotalFoodItemsEaten", &totalFoodItemsEaten);
}
if (auto element = root->FirstChildElement("Play"))
{
element->QueryIntAttribute("TotalPlays", &totalPlays);
element->QueryIntAttribute("HighScore", &highScore);
element->QueryIntAttribute("BestCombo", &bestCombo);
if (auto child = element->FirstChildElement("Grades"))
{
for (auto gradeChild = child->FirstChildElement("Grade"); gradeChild;
gradeChild = gradeChild->NextSiblingElement("Grade"))
{
int id{};
gradeChild->QueryIntAttribute("ID", &id);
gradeChild->QueryIntAttribute("Count", &gradeCounts[id]);
}
}
}
if (auto element = root->FirstChildElement("Inventory"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
int id{};
int quantity{};
child->QueryIntAttribute("ID", &id);
child->QueryIntAttribute("Quantity", &quantity);
inventory[id] = quantity;
}
}
if (auto element = root->FirstChildElement("Items"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Item item{};
child->QueryIntAttribute("ID", &item.id);
child->QueryIntAttribute("ChewCount", &item.chewCount);
child->QueryFloatAttribute("PositionX", &item.position.x);
child->QueryFloatAttribute("PositionY", &item.position.y);
child->QueryFloatAttribute("VelocityX", &item.velocity.x);
child->QueryFloatAttribute("VelocityY", &item.velocity.y);
child->QueryFloatAttribute("Rotation", &item.rotation);
items.emplace_back(std::move(item));
}
}
}
logger.info(std::format("Initialized character save file: {}", path.string()));
isValid = true;
}
bool Save::is_valid() const { return isValid; }
void Save::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto element = document.NewElement("Save");
element->SetAttribute("IsPostgame", isPostgame ? "true" : "false");
element->SetAttribute("IsAlternateSpritesheet", isAlternateSpritesheet ? "true" : "false");
auto characterElement = element->InsertNewChildElement("Character");
characterElement->SetAttribute("Weight", weight);
characterElement->SetAttribute("Calories", calories);
characterElement->SetAttribute("Capacity", capacity);
characterElement->SetAttribute("DigestionRate", digestionRate);
characterElement->SetAttribute("EatSpeed", eatSpeed);
characterElement->SetAttribute("IsDigesting", isDigesting ? "true" : "false");
characterElement->SetAttribute("DigestionProgress", digestionProgress);
characterElement->SetAttribute("DigestionTimer", digestionTimer);
characterElement->SetAttribute("TotalCaloriesConsumed", totalCaloriesConsumed);
characterElement->SetAttribute("TotalFoodItemsEaten", totalFoodItemsEaten);
auto playElement = element->InsertNewChildElement("Play");
playElement->SetAttribute("TotalPlays", totalPlays);
playElement->SetAttribute("HighScore", highScore);
playElement->SetAttribute("BestCombo", bestCombo);
auto gradesElement = playElement->InsertNewChildElement("Grades");
for (auto& [i, count] : gradeCounts)
{
auto gradeElement = gradesElement->InsertNewChildElement("Grade");
gradeElement->SetAttribute("ID", i);
gradeElement->SetAttribute("Count", count);
}
auto inventoryElement = element->InsertNewChildElement("Inventory");
for (auto& [id, quantity] : inventory)
{
auto itemElement = inventoryElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", id);
itemElement->SetAttribute("Quantity", quantity);
}
auto itemsElement = element->InsertNewChildElement("Items");
for (auto& item : items)
{
auto itemElement = itemsElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", item.id);
itemElement->SetAttribute("ChewCount", item.chewCount);
itemElement->SetAttribute("PositionX", item.position.x);
itemElement->SetAttribute("PositionY", item.position.y);
itemElement->SetAttribute("VelocityX", item.velocity.x);
itemElement->SetAttribute("VelocityY", item.velocity.y);
itemElement->SetAttribute("Rotation", item.rotation);
}
document.InsertFirstChild(element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to save character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
logger.info(std::format("Saved character save file: {}", path.string()));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

54
src/resource/xml/save.hpp Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <filesystem>
#include <map>
#include <vector>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Save
{
public:
struct Item
{
int id{};
int chewCount{};
glm::vec2 position{};
glm::vec2 velocity{};
float rotation{};
};
float weight{};
float calories{};
float capacity{};
float eatSpeed{};
float digestionRate{};
float digestionProgress{};
int digestionTimer{};
bool isDigesting{};
bool isAlternateSpritesheet{};
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int totalPlays{};
int highScore{};
int bestCombo{};
std::map<int, int> gradeCounts{};
std::map<int, int> inventory;
std::vector<Item> items;
bool isPostgame{};
bool isValid{};
Save() = default;
Save(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,80 @@
#include "settings.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Settings::Settings(const std::filesystem::path& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
logger.error(
std::format("Could not initialize character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
std::string measurementSystemString{};
query_string_attribute(root, "MeasurementSystem", &measurementSystemString);
measurementSystem = measurementSystemString == "Imperial" ? measurement::IMPERIAL : measurement::METRIC;
root->QueryIntAttribute("Volume", &volume);
root->QueryFloatAttribute("ColorR", &color.r);
root->QueryFloatAttribute("ColorG", &color.g);
root->QueryFloatAttribute("ColorB", &color.b);
root->QueryFloatAttribute("WindowX", &windowPosition.x);
root->QueryFloatAttribute("WindowY", &windowPosition.y);
root->QueryIntAttribute("WindowW", &windowSize.x);
root->QueryIntAttribute("WindowH", &windowSize.y);
}
logger.info(std::format("Initialized settings: {}", path.string()));
isValid = true;
}
bool Settings::is_valid() const { return isValid; }
void Settings::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto element = document.NewElement("Settings");
element->SetAttribute("MeasurementSystem", measurementSystem == measurement::IMPERIAL ? "Imperial" : "Metric");
element->SetAttribute("Volume", volume);
element->SetAttribute("ColorR", color.r);
element->SetAttribute("ColorG", color.g);
element->SetAttribute("ColorB", color.b);
element->SetAttribute("WindowX", windowPosition.x);
element->SetAttribute("WindowY", windowPosition.y);
element->SetAttribute("WindowW", windowSize.x);
element->SetAttribute("WindowH", windowSize.y);
document.InsertFirstChild(element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
logger.info(std::format("Failed to initialize settings: {} ({})", path.string(), document.ErrorStr()));
return;
}
logger.info(std::format("Saved settings: {}", path.string()));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include "../../util/measurement.hpp"
#include <filesystem>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Settings
{
public:
static constexpr auto VOLUME_MIN = 0;
static constexpr auto VOLUME_MAX = 100;
enum Mode
{
LOADER,
IMGUI
};
util::measurement::System measurementSystem{util::measurement::METRIC};
int volume{VOLUME_MAX};
glm::vec3 color{0.09, 0.2196, 0.37};
glm::ivec2 windowSize{1280, 720};
glm::vec2 windowPosition{};
bool isValid{};
Settings() = default;
Settings(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,16 @@
#include "sound_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
Audio& SoundEntryCollection::get()
{
return at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).sound;
}
void SoundEntryCollection::play()
{
at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).play();
}
}

View File

@@ -0,0 +1,24 @@
// Handles sound entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include "../audio.hpp"
namespace game::resource::xml
{
class SoundEntry
{
public:
Audio sound{};
float weight{1.0f};
inline void play() { sound.play(); };
};
class SoundEntryCollection : public std::vector<SoundEntry>
{
public:
Audio& get();
void play();
};
}

185
src/resource/xml/util.cpp Normal file
View File

@@ -0,0 +1,185 @@
#include "util.hpp"
#include <format>
#include "../../util/physfs.hpp"
#include "../../util/string.hpp"
#include "../../log.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* value)
{
const char* temp = nullptr;
auto result = element->QueryStringAttribute(attribute, &temp);
if (result == XML_SUCCESS && temp && value) *value = temp;
return result;
}
XMLError query_bool_attribute(XMLElement* element, const char* attribute, bool* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
temp = string::to_lower(temp);
if (value) *value = temp == "true" || temp == "1" ? true : false;
return result;
}
XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
if (value) *value = std::filesystem::path(temp);
return result;
}
XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
{
int temp{};
auto result = element->QueryIntAttribute(attribute, &temp);
if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
return result;
}
XMLError query_float_optional_attribute(XMLElement* element, const char* attribute, std::optional<float>& value)
{
value.emplace();
auto result = element->QueryFloatAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError query_int_optional_attribute(XMLElement* element, const char* attribute, std::optional<int>& value)
{
value.emplace();
auto result = element->QueryIntAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError document_load(const physfs::Path& path, XMLDocument& document)
{
if (!path.is_valid())
{
logger.error(std::format("Failed to open XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_NOT_FOUND;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(std::format("Failed to read XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_COULD_NOT_BE_OPENED;
}
auto result = document.Parse((const char*)buffer.data(), buffer.size());
if (result != XML_SUCCESS)
logger.error(std::format("Failed to parse XML document: {} ({})", path.c_str(), document.ErrorStr()));
return result;
}
void query_event_id(XMLElement* element, const char* name, const Anm2& anm2, int& eventID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.eventMap.contains(string))
eventID = anm2.eventMap.at(string);
else
{
logger.error(std::format("Could not query anm2 event ID: {} ({})", string, anm2.path));
eventID = -1;
}
}
void query_layer_id(XMLElement* element, const char* name, const Anm2& anm2, int& layerID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.layerMap.contains(string))
layerID = anm2.layerMap.at(string);
else
{
logger.error(std::format("Could not query anm2 layer ID: {} ({})", string, anm2.path));
layerID = -1;
}
}
void query_null_id(XMLElement* element, const char* name, const Anm2& anm2, int& nullID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.nullMap.contains(string))
nullID = anm2.nullMap.at(string);
else
{
logger.error(std::format("Could not query anm2 null ID: {} ({})", string, anm2.path));
nullID = -1;
}
}
void query_anm2(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Anm2& anm2, Anm2::Flags flags)
{
std::string string{};
query_string_attribute(element, name, &string);
anm2 = Anm2(physfs::Path(archive + "/" + rootPath + "/" + string), flags);
}
void query_texture(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Texture& texture)
{
std::string string{};
query_string_attribute(element, name, &string);
texture = Texture(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_sound(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Audio& sound)
{
std::string string{};
query_string_attribute(element, name, &string);
sound = Audio(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_font(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Font& font)
{
std::string string{};
query_string_attribute(element, name, &string);
font = Font(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_animation_entry(XMLElement* element, AnimationEntry& animationEntry)
{
query_string_attribute(element, "Animation", &animationEntry.animation);
element->QueryFloatAttribute("Weight", &animationEntry.weight);
}
void query_animation_entry_collection(XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection)
{
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
query_animation_entry(child, animationEntryCollection.emplace_back());
}
void query_sound_entry(XMLElement* element, const std::string& archive, const std::string& rootPath,
SoundEntry& soundEntry, const std::string& attributeName)
{
query_sound(element, attributeName.c_str(), archive, rootPath, soundEntry.sound);
element->QueryFloatAttribute("Weight", &soundEntry.weight);
}
void query_sound_entry_collection(XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, SoundEntryCollection& soundEntryCollection,
const std::string& attributeName)
{
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
query_sound_entry(child, archive, rootPath, soundEntryCollection.emplace_back(), attributeName);
}
}

51
src/resource/xml/util.hpp Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <tinyxml2.h>
#include "animation_entry.hpp"
#include "sound_entry.hpp"
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "anm2.hpp"
namespace game::resource::xml
{
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_bool_attribute(tinyxml2::XMLElement*, const char*, bool*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
tinyxml2::XMLError query_color_attribute(tinyxml2::XMLElement*, const char*, float*);
tinyxml2::XMLError query_float_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<float>& value);
tinyxml2::XMLError query_int_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<int>& value);
void query_event_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& eventID);
void query_layer_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& layerID);
void query_null_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& nullID);
void query_anm2(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Anm2& anm2, Anm2::Flags flags = {});
void query_texture(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Texture& texture);
void query_sound(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Audio& sound);
void query_font(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Font& font);
void query_animation_entry(tinyxml2::XMLElement* element, AnimationEntry& animationEntry);
void query_animation_entry_collection(tinyxml2::XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection);
void query_sound_entry(tinyxml2::XMLElement* element, const std::string& archive, const std::string& rootPath,
SoundEntry& soundEntry, const std::string& attributeName = "Sound");
void query_sound_entry_collection(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, SoundEntryCollection& soundEntryCollection,
const std::string& attributeName = "Sound");
tinyxml2::XMLError document_load(const util::physfs::Path&, tinyxml2::XMLDocument&);
}

View File

@@ -1,7 +1,9 @@
#include "resources.h" #include "resources.hpp"
#include "util/preferences.hpp"
using namespace game::resource; using namespace game::resource;
using namespace game::anm2; using namespace game::util;
namespace game namespace game
{ {
@@ -10,19 +12,30 @@ namespace game
for (int i = 0; i < shader::COUNT; i++) for (int i = 0; i < shader::COUNT; i++)
shaders[i] = Shader(shader::INFO[i].vertex, shader::INFO[i].fragment); shaders[i] = Shader(shader::INFO[i].vertex, shader::INFO[i].fragment);
for (int i = 0; i < audio::COUNT; i++) for (auto& entry : std::filesystem::recursive_directory_iterator("resources/characters"))
audio[i] = Audio(audio::PATHS[i]); if (entry.is_regular_file() && entry.path().extension() == ".zip") characterPreviews.emplace_back(entry.path());
characters.resize(characterPreviews.size());
for (int i = 0; i < texture::COUNT; i++)
textures[i] = Texture(texture::PATHS[i]);
for (int i = 0; i < anm2::COUNT; i++)
anm2s[i] = Anm2(anm2::PATHS[i]);
} }
void Resources::sound_play(audio::Type type) { audio[type].play(); } void Resources::volume_set(float vol) { Audio::volume_set(vol); }
void Resources::set_audio_gain(float vol) { resource::xml::Character& Resources::character_get(int index)
Audio::set_gain(vol); {
if (!characters.at(index).has_value())
{
characters[index].emplace(characterPreviews.at(index).path);
characters[index]->save = characterPreviews.at(index).save;
}
return *characters[index];
} }
resource::xml::CharacterPreview& Resources::character_preview_get(int index) { return characterPreviews.at(index); }
void Resources::character_save_set(int index, const resource::xml::Save& save)
{
characterPreviews.at(index).save = save;
if (characters.at(index).has_value()) characters[index]->save = save;
}
Resources::~Resources() { settings.serialize(preferences::path() / "settings.xml"); }
} }

View File

@@ -1,129 +1,32 @@
#pragma once #pragma once
#include "resource/anm2.h" #include <optional>
#include "resource/audio.h"
#include "resource/dialogue.h"
#include "resource/font.h"
#include "resource/shader.h"
namespace game::audio #include "resource/font.hpp"
{ #include "resource/shader.hpp"
#define AUDIO \ #include "resource/xml/character.hpp"
X(BLIP, "resources/sfx/blip.ogg") \ #include "resource/xml/character_preview.hpp"
X(ADVANCE, "resources/sfx/advance.ogg") \ #include "resource/xml/settings.hpp"
X(BOUNCE, "resources/sfx/bounce.ogg") \
X(BURP_1, "resources/sfx/burp1.ogg") \
X(BURP_2, "resources/sfx/burp2.ogg") \
X(BURP_3, "resources/sfx/burp3.ogg") \
X(GRAB, "resources/sfx/grab.ogg") \
X(MISS, "resources/sfx/miss.ogg") \
X(OK, "resources/sfx/ok.ogg") \
X(GOOD, "resources/sfx/good.ogg") \
X(GREAT, "resources/sfx/great.ogg") \
X(EXCELLENT, "resources/sfx/excellent.ogg") \
X(PERFECT, "resources/sfx/perfect.ogg") \
X(FALL, "resources/sfx/fall.ogg") \
X(COMMON, "resources/sfx/common.ogg") \
X(UNCOMMON, "resources/sfx/uncommon.ogg") \
X(RARE, "resources/sfx/rare.ogg") \
X(EPIC, "resources/sfx/epic.ogg") \
X(LEGENDARY, "resources/sfx/legendary.ogg") \
X(SPECIAL, "resources/sfx/special.ogg") \
X(IMPOSSIBLE, "resources/sfx/impossible.ogg") \
X(SCORE_LOSS, "resources/sfx/scoreLoss.ogg") \
X(HIGH_SCORE, "resources/sfx/highScore.ogg") \
X(HIGH_SCORE_BIG, "resources/sfx/highScoreBig.ogg") \
X(HIGH_SCORE_LOSS, "resources/sfx/highScoreLoss.ogg") \
X(GURGLE_1, "resources/sfx/gurgle1.ogg") \
X(GURGLE_2, "resources/sfx/gurgle2.ogg") \
X(GURGLE_3, "resources/sfx/gurgle3.ogg") \
X(RELEASE, "resources/sfx/release.ogg") \
X(SUMMON, "resources/sfx/summon.ogg") \
X(RETURN, "resources/sfx/return.ogg") \
X(DISPOSE, "resources/sfx/dispose.ogg") \
X(RUB, "resources/sfx/rub.ogg") \
X(THROW, "resources/sfx/throw.ogg")
enum Type
{
#define X(symbol, path) symbol,
AUDIO
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
AUDIO
#undef X
};
#undef AUDIO
static constexpr Type BURPS[] = {BURP_1, BURP_2, BURP_3};
static constexpr Type GURGLES[] = {GURGLE_1, GURGLE_2, GURGLE_3};
}
namespace game::texture
{
#define TEXTURES X(BG, "resources/gfx/bg.png")
enum Type
{
#define X(symbol, path) symbol,
TEXTURES
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
TEXTURES
#undef X
};
#undef TEXTURES
}
namespace game::anm2
{
#define ANM2 \
X(CHARACTER, "resources/anm2/snivy.anm2") \
X(ITEMS, "resources/anm2/items.anm2") \
X(CURSOR, "resources/anm2/cursor.anm2")
enum Anm2Type
{
#define X(symbol, path) symbol,
ANM2
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
ANM2
#undef X
};
#undef ANM2
}
namespace game namespace game
{ {
class Resources class Resources
{ {
public: public:
resource::Shader shaders[resource::shader::COUNT]; resource::Shader shaders[resource::shader::COUNT];
resource::Audio audio[audio::COUNT];
resource::Texture textures[texture::COUNT];
anm2::Anm2 anm2s[anm2::COUNT];
resource::Font font{"resources/font/font.ttf"}; resource::Font font{"resources/font/font.ttf"};
resource::Dialogue dialogue{"resources/dialogue.xml"}; resource::xml::Settings settings;
std::vector<resource::xml::CharacterPreview> characterPreviews{};
std::vector<std::optional<resource::xml::Character>> characters{};
Resources(); Resources();
void sound_play(audio::Type); ~Resources();
void set_audio_gain(float vol);
resource::xml::Character& character_get(int index);
resource::xml::CharacterPreview& character_preview_get(int index);
void character_save_set(int index, const resource::xml::Save& save);
void volume_set(float volume);
}; };
} }

View File

@@ -1,14 +1,14 @@
#include "state.h" #include "state.hpp"
#include <backends/imgui_impl_opengl3.h> #include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h> #include <backends/imgui_impl_sdl3.h>
#include <imgui.h> #include <imgui.h>
#include "util/math_.h" #include "util/math.hpp"
using namespace glm; using namespace glm;
using namespace game::resource;
using namespace game::util; using namespace game::util;
using namespace game::state;
namespace game namespace game
{ {
@@ -17,402 +17,101 @@ namespace game
constexpr auto UPDATE_RATE = 60; constexpr auto UPDATE_RATE = 60;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE); constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
State::State(SDL_Window* inWindow, SDL_GLContext inContext, vec2 size) State::State(SDL_Window* _window, SDL_GLContext _context, resource::xml::Settings settings)
: window(inWindow), context(inContext), canvas(size, true) : window(_window), context(_context), canvas(settings.windowSize, Canvas::DEFAULT)
{ {
resources.settings = settings;
SDL_SetWindowSize(window, resources.settings.windowSize.x, resources.settings.windowSize.y);
} }
void State::tick() void State::tick()
{ {
for (auto& item : items) switch (type)
item.tick();
character.tick();
if (character.isJustDigestionStart)
resources.sound_play(audio::GURGLES[(int)math::random_roll(std::size(audio::GURGLES))]);
if (character.is_event("Burp")) resources.sound_play(audio::BURPS[(int)math::random_roll(std::size(audio::BURPS))]);
if (character.isJustDigestionEnd && !character.isJustStageUp)
{ {
character.state_set(Character::PAT, true); case SELECT:
textWindow.set_random(resources.dialogue.postDigestIDs, resources, character); select.tick();
break;
case MAIN:
main.tick(resources);
break;
default:
break;
} }
cursor.tick();
textWindow.tick(resources, character);
} }
void State::update() void State::update()
{ {
static bool isRubbing{}; SDL_GetWindowSize(window, &resources.settings.windowSize.x, &resources.settings.windowSize.y);
auto& inventory = mainMenuWindow.inventory;
auto& dialogue = resources.dialogue;
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) while (SDL_PollEvent(&event))
{ {
ImGui_ImplSDL3_ProcessEvent(&event); ImGui_ImplSDL3_ProcessEvent(&event);
if (event.type == SDL_EVENT_QUIT) isRunning = false; if (event.type == SDL_EVENT_QUIT)
{
if (type == MAIN) main.exit(resources);
isRunning = false;
}
if (!isRunning) return;
} }
if (!isRunning) return;
ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame(); ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame(); ImGui::NewFrame();
auto style = ImGui::GetStyle(); switch (type)
if (textWindow.isFlagActivated)
{ {
switch (textWindow.flag) case SELECT:
{ select.update(resources);
case Dialogue::Entry::ACTIVATE_WINDOWS: if (select.info.isNewGame || select.info.isContinue)
isInfo = true;
isMainMenu = true;
break;
case Dialogue::Entry::DEACTIVATE_WINDOWS:
isInfo = false;
isMainMenu = false;
break;
case Dialogue::Entry::ONLY_INFO:
isInfo = true;
isMainMenu = false;
break;
case Dialogue::Entry::ACTIVATE_CHEATS:
mainMenuWindow.isCheats = true;
break;
default:
break;
}
}
auto infoSize = ImVec2(width * 0.5f, ImGui::GetTextLineHeightWithSpacing() * 3.5);
auto infoPos = style.WindowPadding;
if (isInfo) infoWindow.update(resources, gameData, character, infoSize, infoPos);
auto textSize = ImVec2(width - style.WindowPadding.x * 2, ImGui::GetTextLineHeightWithSpacing() * 8);
auto textPos = ImVec2(style.WindowPadding.x, height - textSize.y - style.WindowPadding.y);
if (isText) textWindow.update(resources, character, textSize, textPos);
auto mainSize =
ImVec2((width * 0.5f) - style.WindowPadding.x * 3, height - textSize.y - (style.WindowPadding.y * 3));
auto mainPos = ImVec2(infoPos.x + infoSize.x + style.WindowPadding.x, style.WindowPadding.y);
if (isMainMenu) mainMenuWindow.update(resources, character, gameData, textWindow, mainSize, mainPos);
/* Inventory */
if (inventory.isQueued)
{
if (items.size() > Item::DEPLOYED_MAX)
{
inventory.isQueued = false;
inventory.queuedItemType = Item::NONE;
resources.sound_play(audio::MISS);
}
else
{
auto& type = inventory.queuedItemType;
auto position = glm::vec2(((Item::SPAWN_X_MAX - Item::SPAWN_X_MIN) * math::random()) + Item::SPAWN_X_MIN,
((Item::SPAWN_Y_MAX - Item::SPAWN_Y_MIN) * math::random()) + Item::SPAWN_Y_MIN);
items.emplace_back(&resources.anm2s[anm2::ITEMS], position, type);
inventory.adjust_item(type, -1);
type = Item::NONE;
inventory.isQueued = false;
}
}
/* Item */
Item::hoveredItem = nullptr;
Item::heldItemPrevious = Item::heldItem;
bool isSpeak{};
std::vector<int>* dialogueIDs{};
for (int i = 0; i < items.size(); i++)
{
auto& item = items[i];
item.update(resources);
if (&item == Item::heldItem && &item != &items.back())
{
std::swap(items[i], items.back());
Item::heldItem = &items.back();
continue;
}
if (item.isToBeDeleted)
{
items.erase(items.begin() + i--);
continue;
}
if (Item::queuedReturnItem == &item)
{
if (Item::queuedReturnItem->state == Item::DEFAULT)
{ {
inventory.adjust_item(item.type); Main::Game game = select.info.isNewGame ? Main::NEW_GAME : Main::CONTINUE;
resources.sound_play(audio::RETURN); if (game == Main::NEW_GAME) resources.character_save_set(select.characterIndex, resource::xml::Save());
main.set(resources, select.characterIndex, game);
type = MAIN;
select.info.isNewGame = false;
select.info.isContinue = false;
} }
else break;
resources.sound_play(audio::DISPOSE); case MAIN:
main.update(resources);
items.erase(items.begin() + i--); if (main.menu.configuration.isGoToSelect)
Item::queuedReturnItem = nullptr;
continue;
}
}
auto& item = Item::heldItem;
auto isHeldItemChanged = item != Item::heldItemPrevious;
if (item && character.state != Character::STAGE_UP)
{
auto& type = item->type;
auto& calories = Item::CALORIES[type];
auto& category = Item::CATEGORIES[type];
auto& position = item->position;
auto caloriesChew = Item::CALORIES[type] / (Item::CHEW_COUNT_MAX + 1);
auto digestionRateBonusChew = Item::DIGESTION_RATE_BONUSES[type] / (Item::CHEW_COUNT_MAX + 1);
auto eatSpeedBonusChew = Item::EAT_SPEED_BONUSES[type] / (Item::CHEW_COUNT_MAX + 1);
auto isAbleToEat = character.calories + caloriesChew <= character.max_capacity();
if (category == Item::FOOD)
{
auto isByMouth = math::is_point_in_rectf(character.mouth_rect_get(), position);
if (character.state == Character::EAT && !isHeldItemChanged)
{ {
if (!isByMouth) main.exit(resources);
{ type = SELECT;
if (character.is_over_capacity()) main.menu.configuration.isGoToSelect = false;
{
dialogueIDs = &dialogue.foodEasedIDs;
character.state_set(Character::IDLE);
}
else
{
dialogueIDs = &dialogue.foodStolenIDs;
character.state_set(Character::ANGRY);
}
isSpeak = true;
}
if (character.is_event(Character::EVENT_EAT))
{
item->chewCount++;
if (digestionRateBonusChew > 0 || digestionRateBonusChew < 0)
{
character.digestionRate =
glm::clamp(Character::DIGESTION_RATE_MIN, character.digestionRate + digestionRateBonusChew,
Character::DIGESTION_RATE_MAX);
}
if (eatSpeedBonusChew > 0)
{
character.eatSpeedMultiplier =
glm::clamp(Character::EAT_SPEED_MULTIPLIER_MIN, character.eatSpeedMultiplier + eatSpeedBonusChew,
Character::EAT_SPEED_MULTIPLIER_MAX);
}
character.calories += caloriesChew;
character.totalCaloriesConsumed += caloriesChew;
character.consume_played_event();
if (item->chewCount > Item::CHEW_COUNT_MAX)
{
character.isFinishedFood = true;
character.foodItemsEaten++;
item->isToBeDeleted = true;
Item::heldItem = nullptr;
cursor.play(Cursor::ANIMATION_DEFAULT);
}
else
item->state_set(item->chewCount == 1 ? Item::CHEW_1
: item->chewCount == 2 ? Item::CHEW_2
: Item::DEFAULT);
if (character.calories + caloriesChew >= character.max_capacity() && Item::heldItem)
{
character.state_set(Character::SHOCKED);
dialogueIDs = &dialogue.fullIDs;
isSpeak = true;
}
}
} }
else if (character.state == Character::ANGRY) break;
{ default:
} break;
else
{
cursor.play(Cursor::ANIMATION_GRAB);
if (!isAbleToEat)
{
character.state_set(Character::SHOCKED);
if (caloriesChew > character.max_capacity())
dialogueIDs = &dialogue.capacityLowIDs;
else
dialogueIDs = &dialogue.fullIDs;
}
else if (character.is_over_capacity())
dialogueIDs = &dialogue.feedFullIDs;
else
{
character.state_set(Character::EAGER);
dialogueIDs = &dialogue.feedHungryIDs;
}
if (isHeldItemChanged) isSpeak = true;
if (isAbleToEat && isByMouth)
if (character.state != Character::EAT) character.state_set(Character::EAT);
isRubbing = false;
}
}
} }
else if (isHeldItemChanged && character.state != Character::ANGRY && character.state != Character::STAGE_UP)
character.state_set(Character::IDLE);
/* Character */
if (character.isFinishedFood && character.state == Character::IDLE)
{
if (!character.is_over_capacity()) dialogueIDs = &dialogue.eatHungryIDs;
if (math::random_percent_roll(Character::BURP_BIG_CHANCE))
{
character.state_set(Character::BURP_BIG);
dialogueIDs = &dialogue.burpBigIDs;
}
else if (math::random_percent_roll(Character::BURP_SMALL_CHANCE))
{
character.state_set(Character::BURP_SMALL);
dialogueIDs = &dialogue.burpSmallIDs;
}
else if (!character.is_over_capacity() && math::random_percent_roll(Character::PAT_CHANCE))
character.state_set(Character::PAT);
character.isFinishedFood = false;
isSpeak = true;
}
/* Character */
if (character.isJustAppeared)
{
textWindow.set(dialogue.get("Start"), character);
isText = true;
character.isJustAppeared = false;
}
if (character.isJustFinalThreshold && !character.isFinalThresholdReached)
{
Item::heldItem = nullptr;
Item::heldItemPrevious = nullptr;
textWindow.set(dialogue.get("End"), character);
items.clear();
character.isFinalThresholdReached = true;
}
/* Dialogue */
if (isSpeak && dialogueIDs) textWindow.set_random(*dialogueIDs, resources, character);
/* Rubbing/Grabbing */
bool isHeadRubPossible = math::is_point_in_rectf(character.head_rect_get(), cursor.position) && !Item::heldItem;
bool isBellyRubPossible = math::is_point_in_rectf(character.belly_rect_get(), cursor.position) && !Item::heldItem;
bool isTailRubPossible = math::is_point_in_rectf(character.tail_rect_get(), cursor.position) && !Item::heldItem;
auto isRubPossible = isHeadRubPossible || isBellyRubPossible || isTailRubPossible;
if (isRubPossible)
{
if (!isRubbing) cursor.play(Cursor::ANIMATION_HOVER);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
isRubbing = true;
resources.sound_play(audio::RUB);
if (isHeadRubPossible)
{
character.state_set(Character::HEAD_RUB);
cursor.play(Cursor::ANIMATION_RUB);
}
else if (isBellyRubPossible)
{
character.state_set(Character::BELLY_RUB);
cursor.play(Cursor::ANIMATION_GRAB);
}
else if (isTailRubPossible)
{
character.state_set(Character::TAIL_RUB);
cursor.play(Cursor::ANIMATION_GRAB);
}
}
}
else if (Item::hoveredItem)
cursor.play(Cursor::ANIMATION_HOVER);
else if (!Item::heldItem)
cursor.play(Cursor::ANIMATION_DEFAULT);
if (isRubbing)
{
if (isBellyRubPossible && !character.isDigesting)
{
auto delta = ImGui::GetIO().MouseDelta;
auto power = fabs(delta.x) + fabs(delta.y);
if (character.calories > 0) character.digestionProgress += power * Character::DIGESTION_RUB_BONUS;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || !isRubPossible)
{
isRubbing = false;
character.state_set(Character::IDLE);
}
}
SDL_HideCursor();
cursor.update();
} }
void State::render() void State::render()
{ {
SDL_GL_MakeCurrent(window, context); ivec2 windowSize{};
SDL_GetWindowSize(window, &windowSize.x, &windowSize.y);
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
canvas.bind(); canvas.bind();
canvas.size_set(windowSize);
canvas.clear(glm::vec4(0, 0, 0, 1)); canvas.clear(vec4(resources.settings.color, 1.0f));
auto bgModel = math::quad_model_get(vec2(width, height));
canvas.texture_render(textureShader, resources.textures[texture::BG].id, bgModel);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
character.render(textureShader, rectShader, canvas);
for (auto& item : items)
item.render(textureShader, rectShader, canvas);
cursor.render(textureShader, rectShader, canvas);
canvas.unbind(); canvas.unbind();
switch (type)
{
case SELECT:
select.render(resources, canvas);
break;
case MAIN:
main.render(resources, canvas);
break;
default:
break;
}
SDL_GL_SwapWindow(window); SDL_GL_SwapWindow(window);
} }

View File

@@ -2,53 +2,47 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "character.h" #include "canvas.hpp"
#include "cursor.h" #include "resources.hpp"
#include "item.h"
#include "canvas.h" #include "state/main.hpp"
#include "resources.h" #include "state/select.hpp"
#include "window/info.h" #include "entity/cursor.hpp"
#include "window/main_menu.h"
#include "window/text.h"
namespace game namespace game
{ {
class State class State
{ {
public:
SDL_Window* window{}; SDL_Window* window{};
SDL_GLContext context{}; SDL_GLContext context{};
long previousUpdate{}; long previousUpdate{};
long previousTick{}; long previousTick{};
enum Type
{
MAIN,
SELECT
};
Type type{SELECT};
Resources resources; Resources resources;
Character character{&resources.anm2s[anm2::CHARACTER], glm::vec2(300, 500)}; state::Main main;
Cursor cursor{&resources.anm2s[anm2::CURSOR]}; state::Select select;
std::vector<Item> items{};
window::Info infoWindow;
window::MainMenu mainMenuWindow;
window::Text textWindow;
bool isMainMenu{false};
bool isInfo{false};
bool isText{false};
GameData gameData;
void tick(); void tick();
void tick_60();
void update(); void update();
void render(); void render();
public:
bool isRunning{true}; bool isRunning{true};
Canvas canvas{}; Canvas canvas{};
State(SDL_Window*, SDL_GLContext, glm::vec2); State(SDL_Window*, SDL_GLContext, resource::xml::Settings);
void loop(); void loop();
}; };
}; };

View File

@@ -0,0 +1,56 @@
#include "configuration.hpp"
#include <glm/gtc/type_ptr.hpp>
#include <imgui.h>
#include "../util/math.hpp"
#include "../util/imgui/style.hpp"
#include "../util/imgui/widget.hpp"
#include "../util/measurement.hpp"
using namespace game::util;
using namespace game::util::imgui;
namespace game::state
{
void Configuration::update(Resources& resources, Mode mode)
{
auto& settings = resources.settings;
auto& measurementSystem = settings.measurementSystem;
auto& volume = settings.volume;
auto& color = settings.color;
ImGui::SeparatorText("Measurement System");
WIDGET_FX(ImGui::RadioButton("Metric", (int*)&measurementSystem, measurement::METRIC));
ImGui::SetItemTooltip("%s", "Use kilograms (kg).");
ImGui::SameLine();
WIDGET_FX(ImGui::RadioButton("Imperial", (int*)&measurementSystem, measurement::IMPERIAL));
ImGui::SetItemTooltip("%s", "Use pounds (lbs).");
ImGui::SeparatorText("Sound");
if (WIDGET_FX(ImGui::SliderInt("Volume", &volume, 0, 100, "%d%%")))
resources.volume_set(math::to_unit((float)volume));
ImGui::SetItemTooltip("%s", "Adjust master volume.");
ImGui::SeparatorText("Appearance");
if (WIDGET_FX(
ImGui::ColorEdit3("Color", value_ptr(color), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip)))
style::color_set(color);
ImGui::SetItemTooltip("%s", "Change the UI color.");
ImGui::Separator();
if (WIDGET_FX(ImGui::Button("Reset to Default", ImVec2(-FLT_MIN, 0)))) settings = resource::xml::Settings();
if (mode == MAIN)
{
ImGui::Separator();
if (WIDGET_FX(ImGui::Button("Save", ImVec2(-FLT_MIN, 0)))) isSave = true;
ImGui::SetItemTooltip("%s", "Save the game.\n(Note: the game autosaves frequently.)");
if (WIDGET_FX(ImGui::Button("Return to Characters", ImVec2(-FLT_MIN, 0)))) isGoToSelect = true;
ImGui::SetItemTooltip("%s", "Go back to the character selection screen.\nProgress will be saved.");
}
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../resources.hpp"
namespace game::state
{
class Configuration
{
public:
enum Mode
{
SELECT,
MAIN
};
bool isGoToSelect{};
bool isSave{};
void update(Resources&, Mode = SELECT);
};
}

294
src/state/main.cpp Normal file
View File

@@ -0,0 +1,294 @@
#include "main.hpp"
#include <glm/glm.hpp>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include "../util/imgui.hpp"
#include "../util/imgui/style.hpp"
#include "../util/imgui/widget.hpp"
#include "../util/math.hpp"
using namespace game::resource;
using namespace game::util;
using namespace game::state::main;
using namespace glm;
namespace game::state
{
World::Focus Main::focus_get()
{
return menu.isOpen && tools.isOpen ? World::MENU_TOOLS
: menu.isOpen ? World::MENU
: tools.isOpen ? World::TOOLS
: World::CENTER;
}
void Main::set(Resources& resources, int characterIndex, enum Game game)
{
auto& data = resources.character_get(characterIndex);
auto& saveData = data.save;
auto& itemSchema = data.itemSchema;
auto& dialogue = data.dialogue;
auto& menuSchema = data.menuSchema;
this->characterIndex = characterIndex;
character =
entity::Character(data, vec2(World::BOUNDS.x + World::BOUNDS.z * 0.5f, World::BOUNDS.w - World::BOUNDS.y));
auto isAlternateSpritesheet =
(game == NEW_GAME && math::random_percent_roll(data.alternateSpritesheet.chanceOnNewGame));
if (isAlternateSpritesheet || saveData.isAlternateSpritesheet)
{
character.spritesheet_set(entity::Character::ALTERNATE);
if (game == NEW_GAME) character.data.alternateSpritesheet.sound.play();
}
character.totalCaloriesConsumed = saveData.totalCaloriesConsumed;
character.totalFoodItemsEaten = saveData.totalFoodItemsEaten;
characterManager = CharacterManager{};
cursor = entity::Cursor(character.data.cursorSchema.anm2);
menu.inventory = Inventory{};
for (auto& [id, quantity] : saveData.inventory)
{
if (quantity == 0) continue;
menu.inventory.values[id] = quantity;
}
itemManager = ItemManager{};
for (auto& item : saveData.items)
{
auto& anm2 = itemSchema.anm2s.at(item.id);
auto chewAnimation = itemSchema.animations.chew + std::to_string(item.chewCount);
auto animationIndex = item.chewCount > 0 ? anm2.animationMap[chewAnimation] : -1;
auto& saveItem = itemSchema.anm2s.at(item.id);
itemManager.items.emplace_back(saveItem, item.position, item.id, item.chewCount, animationIndex, item.velocity,
item.rotation);
}
imgui::style::rounding_set(menuSchema.rounding);
imgui::widget::sounds_set(&menuSchema.sounds.hover, &menuSchema.sounds.select);
menu.play = Play(character);
menu.play.totalPlays = saveData.totalPlays;
menu.play.highScore = saveData.highScore;
menu.play.bestCombo = saveData.bestCombo;
menu.play.gradeCounts = saveData.gradeCounts;
menu.play.isHighScoreAchieved = saveData.highScore > 0 ? true : false;
menu.isChat = character.data.dialogue.help.is_valid() || character.data.dialogue.random.is_valid();
text.entry = nullptr;
text.isEnabled = false;
if (auto font = character.data.menuSchema.font.get()) ImGui::GetIO().FontDefault = font;
if (game == NEW_GAME && dialogue.start.is_valid())
{
character.queue_play({.animation = dialogue.start.animation, .isInterruptible = false});
character.tick();
isWindows = false;
isStart = true;
}
isPostgame = saveData.isPostgame;
if (isPostgame)
menu.isCheats = true;
else
menu.isCheats = true; //false;
worldCanvas.size_set(imgui::to_vec2(ImGui::GetMainViewport()->Size));
world.set(character, worldCanvas, focus_get());
}
void Main::exit(Resources& resources)
{
imgui::style::rounding_set();
imgui::widget::sounds_set(nullptr, nullptr);
ImGui::GetIO().FontDefault = resources.font.get();
save(resources);
}
void Main::tick(Resources& resources)
{
character.tick();
cursor.tick();
menu.tick();
toasts.tick();
text.tick(character);
for (auto& item : itemManager.items)
item.tick();
}
void Main::update(Resources& resources)
{
auto focus = focus_get();
auto& dialogue = character.data.dialogue;
if (isWindows)
{
menu.update(resources, itemManager, character, cursor, text, worldCanvas);
tools.update(character, cursor, world, focus, worldCanvas);
info.update(resources, character);
toasts.update();
}
if (text.isEnabled) text.update(character);
if (isStart)
{
if (!isStartBegin)
{
if (auto animation = character.animation_get())
{
if (animation->isLoop || character.state == entity::Actor::STOPPED)
{
text.set(dialogue.get(dialogue.start.id), character);
isStartBegin = true;
}
}
}
else if (!isStartEnd)
{
if (text.entry->is_last())
{
isWindows = true;
isStartEnd = true;
isStart = false;
world.character_focus(character, worldCanvas, focus_get());
}
}
}
if (character.isJustStageFinal && !isEnd && !isPostgame) isEnd = true;
if (isEnd)
{
if (!isEndBegin)
{
if (character.is_animation_finished())
{
text.set(dialogue.get(dialogue.end.id), character);
isEndBegin = true;
isWindows = false;
tools.isOpen = false;
menu.isOpen = false;
character.calories = 0;
character.digestionProgress = 0;
itemManager.items.clear();
itemManager.heldItemIndex = -1;
world.character_focus(character, worldCanvas, focus_get());
}
}
else if (!isEndEnd)
{
if (text.entry->is_last())
{
menu.isOpen = true;
isWindows = true;
isEndEnd = true;
isEnd = false;
isPostgame = true;
}
}
}
itemManager.update(character, cursor, areaManager, text, World::BOUNDS, worldCanvas);
characterManager.update(character, cursor, text, worldCanvas);
character.update();
cursor.update();
world.update(character, cursor, worldCanvas, focus);
if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.configuration.isSave)
{
save(resources);
autosaveTime = 0;
menu.configuration.isSave = false;
}
}
void Main::render(Resources& resources, Canvas& canvas)
{
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
auto size = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto& bgTexture = character.data.areaSchema.areas.at(areaManager.get(character)).texture;
auto windowModel = math::quad_model_get(vec2(size));
auto worldModel = math::quad_model_get(bgTexture.size);
worldCanvas.bind();
worldCanvas.size_set(size);
worldCanvas.clear();
worldCanvas.texture_render(textureShader, bgTexture.id, worldModel);
character.render(textureShader, rectShader, worldCanvas);
for (auto& item : itemManager.items)
item.render(textureShader, rectShader, worldCanvas);
if (menu.debug.isBoundsDisplay)
{
auto boundsModel =
math::quad_model_get(glm::vec2(World::BOUNDS.z, World::BOUNDS.w), glm::vec2(World::BOUNDS.x, World::BOUNDS.y),
glm::vec2(World::BOUNDS.x, World::BOUNDS.y) * 0.5f);
worldCanvas.rect_render(rectShader, boundsModel);
}
worldCanvas.unbind();
canvas.bind();
canvas.texture_render(textureShader, worldCanvas.texture, windowModel);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
cursor.render(textureShader, rectShader, canvas);
canvas.unbind();
SDL_HideCursor();
}
void Main::save(Resources& resources)
{
resource::xml::Save save;
save.weight = character.weight;
save.calories = character.calories;
save.capacity = character.capacity;
save.digestionRate = character.digestionRate;
save.eatSpeed = character.eatSpeed;
save.digestionProgress = character.digestionProgress;
save.isDigesting = character.isDigesting;
save.digestionTimer = character.digestionTimer;
save.totalCaloriesConsumed = character.totalCaloriesConsumed;
save.totalFoodItemsEaten = character.totalFoodItemsEaten;
save.totalPlays = menu.play.totalPlays;
save.highScore = menu.play.highScore;
save.bestCombo = menu.play.bestCombo;
save.gradeCounts = menu.play.gradeCounts;
save.isPostgame = isPostgame;
save.isAlternateSpritesheet = character.spritesheetType == entity::Character::ALTERNATE;
for (auto& [id, quantity] : menu.inventory.values)
{
if (quantity == 0) continue;
save.inventory[id] = quantity;
}
for (auto& item : itemManager.items)
save.items.emplace_back(item.schemaID, item.chewCount, item.position, item.velocity,
*item.overrides[item.rotationOverrideID].frame.rotation);
save.isValid = true;
resources.character_save_set(characterIndex, save);
save.serialize(character.data.save_path_get());
toasts.push("Saving...");
}
};

69
src/state/main.hpp Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include "../resources.hpp"
#include "main/area_manager.hpp"
#include "main/character_manager.hpp"
#include "main/info.hpp"
#include "main/item_manager.hpp"
#include "main/menu.hpp"
#include "main/text.hpp"
#include "main/toasts.hpp"
#include "main/tools.hpp"
#include "main/world.hpp"
namespace game::state
{
class Main
{
public:
static constexpr auto AUTOSAVE_TIME = 30.0f;
enum Game
{
NEW_GAME,
CONTINUE
};
entity::Character character;
entity::Cursor cursor;
main::Info info;
main::Menu menu;
main::Tools tools;
main::Text text;
main::World world;
main::Toasts toasts;
main::ItemManager itemManager{};
main::CharacterManager characterManager{};
main::AreaManager areaManager{};
int characterIndex{};
int areaIndex{};
float autosaveTime{};
bool isWindows{true};
bool isStartBegin{};
bool isStart{};
bool isStartEnd{};
bool isEndBegin{};
bool isEnd{};
bool isEndEnd{};
bool isPostgame{};
Canvas worldCanvas{main::World::SIZE};
Main() = default;
void set(Resources&, int characterIndex, Game = CONTINUE);
void exit(Resources& resources);
void update(Resources&);
void tick(Resources&);
void render(Resources&, Canvas&);
void save(Resources&);
main::World::Focus focus_get();
};
};

View File

@@ -0,0 +1,26 @@
#include "area_manager.hpp"
#include <imgui.h>
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
int AreaManager::get(entity::Character& character)
{
auto& data = character.data;
auto& schema = data.areaSchema;
if (schema.areas.empty()) return -1;
auto size = (int)data.stages.size();
for (int i = 0; i < size; i++)
{
auto& stage = data.stages[size - i - 1];
if (stage.areaID != -1) return stage.areaID;
}
return -1;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "../../entity/character.hpp"
namespace game::state::main
{
class AreaManager
{
public:
int get(entity::Character&);
};
}

View File

@@ -0,0 +1,120 @@
#include "character_manager.hpp"
#include "../../util/math.hpp"
#include <imgui.h>
#include <optional>
using namespace game::resource::xml;
using namespace game::util;
namespace game::state::main
{
void CharacterManager::update(entity::Character& character, entity::Cursor& cursor, Text& text, Canvas& canvas)
{
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 percent = glm::clamp(*override_.time / *override_.timeStart, 0.0f, 1.0f);
auto elapsed = 1.0f - percent;
auto oscillation = cosf(elapsed * glm::tau<float>() * override_.cycles);
auto envelope = percent;
auto amplitude = glm::abs(*override_.frameBase.scale);
*override_.frame.scale = amplitude * (oscillation * envelope);
}
};
auto& dialogue = character.data.dialogue;
auto cursorWorldPosition = canvas.screen_position_convert(cursor.position);
auto isMouseLeftClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isImguiCaptureMouse = ImGui::GetIO().WantCaptureMouse;
isInteractingPrevious = isInteracting;
isHoveringPrevious = isHovering;
isHovering = false;
if (isJustStoppedInteracting)
{
cursor.queue_play({cursor.defaultAnimation});
if (cursor.mode == RUB) character.queue_idle_animation();
isJustStoppedInteracting = false;
}
if (isJustStoppedHovering)
{
cursor.queue_play({cursor.defaultAnimation});
isJustStoppedHovering = false;
}
for (int i = 0; i < (int)character.data.interactAreas.size(); i++)
{
auto& interactArea = character.data.interactAreas.at(i);
if (interactArea.nullID == -1) continue;
auto rect = character.null_frame_rect(interactArea.nullID);
if (cursor.state == entity::Cursor::DEFAULT && math::is_point_in_rectf(rect, cursorWorldPosition) &&
!isImguiCaptureMouse && interactArea.type == cursor.mode)
{
cursor.state = entity::Cursor::HOVER;
cursor.queue_play({interactArea.animationCursorHover});
isHovering = true;
interactAreaID = i;
if (isMouseLeftClick)
{
isInteracting = true;
interactArea.sound.play();
lastInteractType = cursor.mode;
if (interactArea.digestionBonusClick > 0 && character.calories > 0 && !character.isDigesting)
character.digestionProgress += interactArea.digestionBonusClick;
if (interactArea.layerID != -1)
{
character.overrides.emplace_back(entity::Actor::Override(
interactArea.layerID, Anm2::LAYER, entity::Actor::Override::ADD,
{.scale = glm::vec2(interactArea.scaleEffectAmplitude)}, std::optional<float>(interactArea.time),
interact_area_override_tick, interactArea.scaleEffectCycles));
}
if (interactArea.pool.is_valid() && text.is_interruptible())
text.set(dialogue.get(interactArea.pool), character);
}
if (isInteracting)
{
cursor.state = entity::Cursor::ACTION;
character.queue_interact_area_animation(interactArea);
cursor.queue_play({interactArea.animationCursorActive});
if (interactArea.digestionBonusRub > 0 && character.calories > 0 && !character.isDigesting)
{
auto mouseDelta = cursorWorldPosition - cursorWorldPositionPrevious;
auto digestionBonus = (fabs(mouseDelta.x) + fabs(mouseDelta.y)) * interactArea.digestionBonusRub;
character.digestionProgress += digestionBonus;
}
}
}
if ((i == interactAreaID && !math::is_point_in_rectf(rect, cursorWorldPosition)) || isMouseLeftReleased ||
isImguiCaptureMouse)
{
isInteracting = false;
interactAreaID = -1;
}
}
if (isInteracting != isInteractingPrevious && !isInteracting) isJustStoppedInteracting = true;
if (isHovering != isHoveringPrevious && !isHovering) isJustStoppedHovering = true;
cursorWorldPositionPrevious = cursorWorldPosition;
if (character.isJustDigested && text.is_interruptible()) text.set(dialogue.get(dialogue.digest), character);
if (character.isJustStageUp) text.set(dialogue.get(dialogue.stageUp), character);
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "text.hpp"
namespace game::state::main
{
class CharacterManager
{
public:
bool isInteracting{};
bool isHovering{};
bool isInteractingPrevious{};
bool isHoveringPrevious{};
bool isJustStoppedInteracting{};
bool isJustStoppedHovering{};
int interactAreaID{-1};
InteractType lastInteractType{(InteractType)-1};
glm::vec2 cursorWorldPositionPrevious{};
std::string queuedAnimation{};
void update(entity::Character&, entity::Cursor&, Text&, Canvas&);
};
}

32
src/state/main/chat.cpp Normal file
View File

@@ -0,0 +1,32 @@
#include "chat.hpp"
#include "../../util/imgui/widget.hpp"
using namespace game::resource;
using namespace game::util::imgui;
namespace game::state::main
{
void Chat::update(Resources& resources, Text& text, entity::Character& character)
{
auto& dialogue = character.data.dialogue;
auto size = ImGui::GetContentRegionAvail();
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
if (dialogue.random.is_valid())
if (WIDGET_FX(ImGui::Button("Let's chat!", ImVec2(size.x, 0))))
text.set(dialogue.get(dialogue.random), character);
ImGui::PopFont();
if (dialogue.help.is_valid())
if (WIDGET_FX(ImGui::Button("Help", ImVec2(size.x, 0)))) text.set(dialogue.get(dialogue.help), character);
auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get());
auto& pool = stage > 0 ? character.data.stages.at(stage - 1).pool : character.data.pool;
if (pool.is_valid())
if (WIDGET_FX(ImGui::Button("How are you feeling?", ImVec2(size.x, 0)))) text.set(dialogue.get(pool), character);
}
}

14
src/state/main/chat.hpp Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "text.hpp"
#include <imgui.h>
namespace game::state::main
{
class Chat
{
public:
void update(Resources&, Text&, entity::Character&);
};
}

114
src/state/main/cheats.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "cheats.hpp"
#include <algorithm>
#include <ranges>
#include "../../util/imgui/input_int_ex.hpp"
#include "../../util/imgui/widget.hpp"
using namespace game::util::imgui;
using namespace game::util;
namespace game::state::main
{
void Cheats::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text)
{
static constexpr auto FEED_INCREMENT = 100.0f;
if (ImGui::BeginChild("##Cheats"))
{
if (WIDGET_FX(ImGui::Button("Feed")))
{
character.calories = std::min(character.calories + FEED_INCREMENT, character.max_capacity());
character.queue_idle_animation();
}
ImGui::SameLine();
if (WIDGET_FX(ImGui::Button("Starve")))
{
character.calories = std::max(0.0f, character.calories - FEED_INCREMENT);
character.queue_idle_animation();
}
ImGui::SameLine();
if (WIDGET_FX(ImGui::Button("Digest"))) character.digestionProgress = entity::Character::DIGESTION_MAX;
if (WIDGET_FX(ImGui::SliderFloat("Weight", &character.weight, character.data.weightMin, character.data.weightMax,
"%0.2f kg")))
{
character.stage = character.stage_get();
character.queue_idle_animation();
}
auto stage = character.stage + 1;
if (WIDGET_FX(ImGui::SliderInt("Stage", &stage, 1, character.data.stages.size() + 1)))
{
character.stage = glm::clamp(0, stage - 1, (int)character.data.stages.size());
character.weight =
character.stage == 0 ? character.data.weight : character.data.stages.at(character.stage - 1).threshold;
character.queue_idle_animation();
}
WIDGET_FX(ImGui::SliderFloat("Capacity", &character.capacity, character.data.capacityMin,
character.data.capacityMax, "%0.0f kcal"));
WIDGET_FX(ImGui::SliderFloat("Digestion Rate", &character.digestionRate, character.data.digestionRateMin,
character.data.digestionRateMax, "%0.2f% / tick"));
WIDGET_FX(ImGui::SliderFloat("Eat Speed", &character.eatSpeed, character.data.eatSpeedMin,
character.data.eatSpeedMax, "%0.2fx"));
ImGui::SeparatorText("Animations");
ImGui::Text("Now Playing: %s", character.animationMapReverse.at(character.animationIndex).c_str());
auto childSize = ImVec2(0, ImGui::GetContentRegionAvail().y / 3);
if (ImGui::BeginChild("##Animations", childSize, ImGuiChildFlags_Borders))
{
for (int i = 0; i < (int)character.animations.size(); i++)
{
auto& animation = character.animations[i];
ImGui::PushID(i);
if (WIDGET_FX(ImGui::Selectable(animation.name.c_str())))
character.play(animation.name.c_str(), entity::Actor::PLAY_FORCE);
ImGui::SetItemTooltip("%s", animation.name.c_str());
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Dialogue");
if (ImGui::BeginChild("##Dialogue", childSize, ImGuiChildFlags_Borders))
{
for (int i = 0; i < (int)character.data.dialogue.entries.size(); i++)
{
auto& entry = character.data.dialogue.entries[i];
ImGui::PushID(i);
if (WIDGET_FX(ImGui::Selectable(entry.name.c_str()))) text.set(&entry, character);
ImGui::SetItemTooltip("%s", entry.name.c_str());
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Inventory");
if (ImGui::BeginChild("##Inventory", ImGui::GetContentRegionAvail(), ImGuiChildFlags_Borders))
{
auto& schema = character.data.itemSchema;
ImGui::PushItemWidth(100);
for (int i = 0; i < (int)schema.items.size(); i++)
{
auto& item = schema.items[i];
ImGui::PushID(i);
WIDGET_FX(input_int_range(item.name.c_str(), &inventory.values[i], 0, schema.quantityMax, 1, 5));
ImGui::SetItemTooltip("%s", item.name.c_str());
ImGui::PopID();
}
ImGui::PopItemWidth();
}
ImGui::EndChild();
}
ImGui::EndChild();
}
}

15
src/state/main/cheats.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "inventory.hpp"
#include "text.hpp"
#include <imgui.h>
namespace game::state::main
{
class Cheats
{
public:
void update(Resources&, entity::Character&, Inventory&, Text&);
};
}

37
src/state/main/debug.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "debug.hpp"
#include "../../util/imgui/widget.hpp"
#include <ranges>
using namespace game::util::imgui;
namespace game::state::main
{
void Debug::update(entity::Character& character, entity::Cursor& cursor, ItemManager& itemManager, Canvas& canvas)
{
auto cursorPosition = canvas.screen_position_convert(cursor.position);
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);
WIDGET_FX(ImGui::Checkbox("Show Nulls (Hitboxes)", &character.isShowNulls));
WIDGET_FX(ImGui::Checkbox("Show World Bounds", &isBoundsDisplay));
if (!itemManager.items.empty())
{
ImGui::SeparatorText("Item");
for (int i = 0; i < (int)itemManager.items.size(); i++)
{
auto& item = itemManager.items[i];
if (itemManager.heldItemIndex == i) ImGui::Text("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("Chew Count: %i", item.chewCount);
ImGui::Separator();
}
}
}
}

19
src/state/main/debug.hpp Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "item_manager.hpp"
#include <imgui.h>
namespace game::state::main
{
class Debug
{
public:
bool isBoundsDisplay{};
void update(entity::Character&, entity::Cursor& cursor, ItemManager&, Canvas& canvas);
};
}

125
src/state/main/info.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "info.hpp"
#include "../../util/color.hpp"
#include "../../util/imgui.hpp"
#include "../../util/math.hpp"
#include "../../util/string.hpp"
#include <algorithm>
#include <format>
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
void Info::update(Resources& resources, entity::Character& character)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
static constexpr auto HEIGHT_MULTIPLIER = 4.0f;
auto& style = ImGui::GetStyle();
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER);
auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Info", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto childSize = ImVec2(ImGui::GetContentRegionAvail().x / 2, ImGui::GetContentRegionAvail().y);
if (ImGui::BeginChild("##Weight", childSize))
{
auto& system = resources.settings.measurementSystem;
auto weight = character.weight_get(system);
auto stage = character.stage_get();
auto stageMax = character.stage_max_get();
auto stageWeight = character.stage_threshold_get(stage, system);
auto stageNextWeight = character.stage_threshold_next_get(system);
auto unitString = (system == measurement::IMPERIAL ? "lbs" : "kg");
auto weightString = util::string::format_commas(weight, 2) + " " + unitString;
ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE);
ImGui::TextUnformatted(weightString.c_str());
ImGui::SetItemTooltip("%s", weightString.c_str());
ImGui::PopFont();
auto stageProgress = character.stage_progress_get();
ImGui::ProgressBar(stageProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0),
stage >= stageMax ? "MAX" : "To Next Stage");
if (ImGui::BeginItemTooltip())
{
ImGui::Text("Stage: %i/%i (%0.1f%%)", stage + 1, stageMax + 1, math::to_percent(stageProgress));
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, imgui::to_imvec4(color::GRAY));
if (stage >= stageMax)
ImGui::Text("Maxed out!");
else
{
ImGui::Text("Start: %0.2f %s", stageWeight, unitString);
ImGui::Text("Current: %0.2f %s", weight, unitString);
ImGui::Text("Next: %0.2f %s", stageNextWeight, unitString);
}
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("##Calories and Capacity", childSize))
{
auto& calories = character.calories;
auto& capacity = character.capacity;
auto overstuffedPercent = std::max(0.0f, (calories - capacity) / (character.max_capacity() - capacity));
auto caloriesColor = ImVec4(1.0f, 1.0f - overstuffedPercent, 1.0f - overstuffedPercent, 1.0f);
ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE);
ImGui::PushStyleColor(ImGuiCol_Text, caloriesColor);
auto caloriesString = std::format("{:.0f} kcal / {:.0f} kcal", calories,
character.is_over_capacity() ? character.max_capacity() : character.capacity);
ImGui::TextUnformatted(caloriesString.c_str());
ImGui::SetItemTooltip("%s", caloriesString.c_str());
ImGui::PopStyleColor();
ImGui::PopFont();
auto digestionProgress = character.isDigesting
? (float)character.digestionTimer / character.data.digestionTimerMax
: character.digestionProgress / entity::Character::DIGESTION_MAX;
ImGui::ProgressBar(digestionProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0),
character.isDigesting ? "Digesting..." : "Digestion");
if (ImGui::BeginItemTooltip())
{
if (character.isDigesting)
ImGui::TextUnformatted("Digestion in progress...");
else if (digestionProgress <= 0.0f)
ImGui::TextUnformatted("Give food to start digesting!");
else
ImGui::Text("%0.2f%%", math::to_percent(digestionProgress));
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
ImGui::Text("Rate: %0.2f%% / sec", character.digestion_rate_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed);
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
}
ImGui::EndChild();
}
ImGui::End();
}
}

15
src/state/main/info.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include <imgui.h>
namespace game::state::main
{
class Info
{
public:
void update(Resources&, entity::Character&);
};
}

View File

@@ -0,0 +1,194 @@
#include "inventory.hpp"
#include <cmath>
#include <format>
#include <ranges>
#include "../../util/color.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"
using namespace game::util;
using namespace game::util::imgui;
using namespace game::entity;
using namespace game::resource;
using namespace glm;
namespace game::state::main
{
void Inventory::tick()
{
for (auto& [i, actor] : actors)
actor.tick();
}
void Inventory::update(Resources& resources, ItemManager& itemManager, entity::Character& character)
{
auto& schema = character.data.itemSchema;
if (!itemManager.returnItemIDs.empty())
{
for (auto& id : itemManager.returnItemIDs)
values[id]++;
itemManager.returnItemIDs.clear();
}
if (ImGui::BeginChild("##Inventory Child"))
{
auto cursorPos = ImGui::GetCursorPos();
auto cursorStartX = ImGui::GetCursorPosX();
auto size = ImVec2(SIZE, SIZE);
for (int i = 0; i < (int)schema.items.size(); i++)
{
auto& item = schema.items[i];
auto& quantity = values[i];
auto& category = schema.categories[item.categoryID];
auto& calories = item.calories;
auto& digestionBonus = item.digestionBonus;
auto& eatSpeedBonus = item.eatSpeedBonus;
auto& rarity = schema.rarities[item.rarityID];
quantity = glm::clamp(0, quantity, schema.quantityMax);
if (rarity.isHidden && quantity <= 0) continue;
ImGui::PushID(i);
ImGui::SetCursorPos(cursorPos);
auto cursorScreenPos = ImGui::GetCursorScreenPos();
if (!actors.contains(i))
{
actors[i] = Actor(schema.anm2s[i], {}, Actor::SET);
rects[i] = actors[i].rect();
}
auto& rect = rects[i];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (size.x <= 0.0f || size.y <= 0.0f || rectSize.x <= 0.0f || rectSize.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
auto previewSize = rectSize * previewScale;
auto canvasSize = ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
if (!canvases.contains(i)) canvases.emplace((int)i, Canvas(canvasSize, Canvas::FLIP));
auto& canvas = canvases[i];
canvas.zoom = math::to_percent(previewScale);
canvas.pan = vec2(rect.x, rect.y);
canvas.bind();
canvas.size_set(canvasSize);
canvas.clear();
actors[i].render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
canvas.unbind();
ImGui::BeginDisabled(quantity < 1);
if (WIDGET_FX(ImGui::ImageButton("##Image Button", canvas.texture, size, ImVec2(), ImVec2(1, 1), ImVec4(),
quantity <= 0 ? ImVec4(0, 0, 0, 1) : ImVec4(1, 1, 1, 1))) &&
quantity > 0)
{
if (category.isEdible)
{
if (itemManager.items.size() + 1 >= ItemManager::LIMIT)
character.data.itemSchema.sounds.dispose.play();
else
{
character.data.itemSchema.sounds.summon.play();
itemManager.queuedItemIDs.emplace_back(i);
quantity--;
}
}
else if (item.isToggleSpritesheet)
{
character.spritesheet_set(character.spritesheetType == Character::NORMAL ? Character::ALTERNATE
: Character::NORMAL);
character.data.alternateSpritesheet.sound.play();
quantity--;
}
}
ImGui::EndDisabled();
if (ImGui::BeginItemTooltip())
{
if (quantity > 0)
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::Text("%s (x%i)", item.name.c_str(), quantity);
ImGui::Separator();
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
ImGui::Text("-- %s (%s) --", category.name.c_str(), rarity.name.c_str());
if (item.flavorID.has_value()) ImGui::Text("Flavor: %s", schema.flavors[*item.flavorID].name.c_str());
if (calories.has_value()) ImGui::Text("%0.0f kcal", *calories);
if (digestionBonus.has_value())
{
if (*digestionBonus > 0)
ImGui::Text("Digestion Rate Bonus: +%0.2f%% / sec", *digestionBonus * 60.0f);
else if (digestionBonus < 0)
ImGui::Text("Digestion Rate Penalty: %0.2f%% / sec", *digestionBonus * 60.0f);
}
if (eatSpeedBonus.has_value())
{
if (*eatSpeedBonus > 0)
ImGui::Text("Eat Speed Bonus: +%0.2f%% / sec", *eatSpeedBonus);
else if (eatSpeedBonus < 0)
ImGui::Text("Eat Speed Penalty: %0.2f%% / sec", *eatSpeedBonus);
}
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::TextUnformatted(item.description.c_str());
}
else
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::TextUnformatted("???");
ImGui::PopFont();
}
ImGui::EndTooltip();
}
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
auto text = std::format("x{}", quantity);
auto textPos = ImVec2(cursorScreenPos.x + size.x - ImGui::CalcTextSize(text.c_str()).x,
cursorScreenPos.y + size.y - ImGui::GetTextLineHeightWithSpacing());
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)),
text.c_str());
ImGui::PopFont();
auto increment = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;
cursorPos.x += increment;
if (cursorPos.x + increment > ImGui::GetContentRegionAvail().x)
{
cursorPos.x = cursorStartX;
cursorPos.y += increment;
}
ImGui::PopID();
}
if (count() == 0) ImGui::Text("Check the \"Play\" tab to earn rewards!");
}
ImGui::EndChild();
}
int Inventory::count()
{
int count{};
for (auto& [type, quantity] : values)
count += quantity;
return count;
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "item_manager.hpp"
#include <imgui.h>
namespace game::state::main
{
class Inventory
{
public:
static constexpr auto SIZE = 96.0f;
std::map<int, int> values{};
std::unordered_map<int, entity::Actor> actors{};
std::unordered_map<int, glm::vec4> rects{};
std::unordered_map<int, Canvas> canvases{};
void tick();
void update(Resources&, ItemManager&, entity::Character&);
int count();
};
}

View File

@@ -0,0 +1,315 @@
#include "item_manager.hpp"
#include <cmath>
#include <string>
#include <utility>
#include "../../util/math.hpp"
#include "../../util/vector.hpp"
#include <imgui.h>
using namespace game::resource;
using namespace game::util;
using namespace glm;
namespace game::state::main
{
void ItemManager::update(entity::Character& character, entity::Cursor& cursor, AreaManager& areaManager, Text& text,
const glm::vec4& bounds, Canvas& canvas)
{
static constexpr float ROTATION_MAX = 90.0f;
static constexpr float ROTATION_RETURN_SPEED = 120.0f;
static constexpr float ROTATION_GROUND_DAMPING = 0.85f;
static constexpr float ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER = 0.1f;
static constexpr float THROW_THRESHOLD = 10.0f;
auto& schema = character.data.itemSchema;
auto& cursorSchema = character.data.cursorSchema;
auto& area = character.data.areaSchema.areas.at(areaManager.get(character));
auto& friction = area.friction;
auto& airResistance = area.airResistance;
auto& dialogue = character.data.dialogue;
auto isOverCapacity = character.is_over_capacity();
auto cursorPosition = canvas.screen_position_convert(cursor.position);
auto cursorDelta = cursorPosition - cursorPositionPrevious;
auto isImguiCaptureMouse = ImGui::GetIO().WantCaptureMouse;
auto isMouseLeftClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
auto& io = ImGui::GetIO();
if (isJustItemHoveredStopped)
{
cursor.queue_default_animation();
isJustItemHoveredStopped = false;
}
if (isJustItemHeldStopped || isJustItemThrown)
{
cursor.queue_default_animation();
if (!isJustItemThrown) character.queue_idle_animation();
isJustItemHeldStopped = false;
isJustItemThrown = false;
}
isItemHoveredPrevious = isItemHovered;
isItemHovered = false;
if (isItemHovered != isItemHoveredPrevious && !isItemHovered) isJustItemHoveredStopped = true;
for (auto& id : queuedItemIDs)
{
auto spawnBounds = character.rect();
auto position = glm::vec2(math::random_in_range(spawnBounds.x, spawnBounds.x + spawnBounds.z),
math::random_in_range(spawnBounds.y, spawnBounds.y + spawnBounds.w));
auto& itemSchema = character.data.itemSchema;
items.emplace_back(itemSchema.anm2s.at(id), position, id);
}
queuedItemIDs.clear();
if (isMouseRightDown) cursor.queue_play({cursorSchema.animations.return_.get()});
if (auto heldItem = vector::find(items, heldItemIndex))
{
auto& item = schema.items[heldItem->schemaID];
auto& rotationOverride = heldItem->overrides[heldItem->rotationOverrideID];
auto& rotation = *rotationOverride.frame.rotation;
auto delta = cursorPositionPrevious - cursorPosition;
heldItem->position = cursorPosition;
heldItem->velocity = vec2();
heldItem->angularVelocity -= delta.x * ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER;
heldItem->angularVelocity *= friction;
rotation *= friction;
rotation = glm::clamp(-ROTATION_MAX, rotation, ROTATION_MAX);
if (schema.categories[item.categoryID].isEdible)
{
auto& chewCountMax = item.chewCount.has_value() ? *item.chewCount : schema.chewCount;
auto caloriesChew = item.calories.has_value() ? *item.calories / (chewCountMax + 1) : 0;
auto isCanEat = character.calories + caloriesChew <= character.max_capacity();
if (isJustItemHeld)
{
if (isCanEat)
text.set(dialogue.get(isOverCapacity ? dialogue.feedFull : dialogue.feed), character);
else if (caloriesChew > character.capacity)
text.set(dialogue.get(dialogue.lowCapacity), character);
else
text.set(dialogue.get(dialogue.full), character);
isJustItemHeld = false;
}
for (auto& eatArea : character.data.eatAreas)
{
heldItem = vector::find(items, heldItemIndex);
if (!heldItem) break;
auto rect = character.null_frame_rect(eatArea.nullID);
if (isCanEat && math::is_point_in_rectf(rect, heldItem->position))
{
character.queue_play(
{.animation = eatArea.animation, .speedMultiplier = character.eatSpeed, .isInterruptible = false});
if (character.playedEventID == eatArea.eventID)
{
heldItem->chewCount++;
character.consume_played_event();
auto chewAnimation = schema.animations.chew + std::to_string(heldItem->chewCount);
auto animationIndex = heldItem->chewCount > 0 ? heldItem->animationMap[chewAnimation] : -1;
heldItem->play(animationIndex, entity::Actor::SET);
character.calories += caloriesChew;
character.totalCaloriesConsumed += caloriesChew;
if (item.eatSpeedBonus.has_value())
{
character.eatSpeed += *item.eatSpeedBonus / (chewCountMax + 1);
character.eatSpeed =
glm::clamp(character.data.eatSpeedMin, character.eatSpeed, character.data.eatSpeedMax);
}
if (item.digestionBonus.has_value())
{
character.digestionRate += *item.digestionBonus / (chewCountMax + 1);
character.digestionRate = glm::clamp(character.data.digestionRateMin, character.digestionRate,
character.data.digestionRateMax);
}
if (heldItem->chewCount > chewCountMax)
{
isQueueFinishFood = true;
character.totalFoodItemsEaten++;
queuedRemoveItemIndex = heldItemIndex;
heldItemIndex = -1;
}
}
}
if (isMouseLeftReleased)
{
if (fabs(delta.x) >= THROW_THRESHOLD || fabs(delta.y) >= THROW_THRESHOLD)
{
cursorSchema.sounds.throw_.play();
text.set(dialogue.get(dialogue.throw_), character);
isJustItemThrown = true;
}
else
cursorSchema.sounds.release.play();
heldItem->velocity -= delta;
heldItemIndex = -1;
isJustItemHeldStopped = true;
}
// Food stolen
if (auto animation = character.animation_get(character.animation_name_convert(eatArea.animation));
character.is_playing(animation->name) && !isOverCapacity)
{
if (!math::is_point_in_rectf(rect, heldItem->position))
text.set(dialogue.get(isOverCapacity ? dialogue.foodTakenFull : dialogue.foodTaken), character);
}
}
}
}
if (auto animation = character.animation_get(); character.time >= animation->frameNum && isQueueFinishFood)
{
text.set(dialogue.get(isOverCapacity ? dialogue.eatFull : dialogue.eat), character);
isQueueFinishFood = false;
}
if (queuedRemoveItemIndex > -1)
{
items.erase(items.begin() + queuedRemoveItemIndex);
queuedRemoveItemIndex = -1;
}
int heldItemMoveIndex = -1;
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
auto& schemaItem = schema.items[item.schemaID];
auto& rotationOverride = item.overrides[item.rotationOverrideID];
auto& rotation = *rotationOverride.frame.rotation;
auto& gravity = schemaItem.gravity.has_value() ? *schemaItem.gravity : area.gravity;
item.update();
if (math::is_point_in_rectf(item.rect(), cursorPosition) && !isImguiCaptureMouse)
{
isItemHovered = true;
cursor.queue_play({cursorSchema.animations.hover.get()});
cursor.state = entity::Cursor::HOVER;
if (isMouseLeftClicked)
{
cursorSchema.sounds.grab.play();
isJustItemHeld = true;
}
if (isMouseLeftDown)
{
isItemHeld = true;
cursor.queue_play({cursorSchema.animations.grab.get()});
cursor.state = entity::Cursor::ACTION;
heldItemIndex = i;
heldItemMoveIndex = i;
}
if (isMouseRightClicked)
{
if (item.chewCount > 0)
schema.sounds.dispose.play();
else
{
schema.sounds.return_.play();
returnItemIDs.emplace_back(item.schemaID);
}
if (heldItemIndex == i) heldItemIndex = -1;
if (heldItemMoveIndex == i) heldItemMoveIndex = -1;
if (heldItemIndex > i) heldItemIndex--;
if (heldItemMoveIndex > i) heldItemMoveIndex--;
items.erase(items.begin() + i);
continue;
}
}
if (i != heldItemIndex)
{
if (item.position.x <= bounds.x || item.position.x >= bounds.z)
{
if (item.position.x <= bounds.x) item.position.x = bounds.x + 1.0f;
if (item.position.x >= bounds.z) item.position.x = bounds.z - 1.0f;
item.velocity.x *= friction;
item.velocity.x = -item.velocity.x;
item.angularVelocity *= friction;
item.angularVelocity = -item.angularVelocity;
rotation = -rotation;
schema.sounds.bounce.play();
}
if (item.position.y <= bounds.y || item.position.y >= bounds.w)
{
if (item.position.y >= bounds.w && item.velocity.y <= gravity)
{
item.position.y = bounds.w;
item.velocity.y = 0;
item.velocity.x *= friction;
rotation = std::fmod(rotation, 360.0f);
if (rotation < 0.0f) rotation += 360.0f;
if (rotation > 180.0f) rotation -= 360.0f;
auto returnStep = ROTATION_RETURN_SPEED * io.DeltaTime;
if (std::abs(rotation) <= returnStep)
rotation = 0.0f;
else
rotation += (rotation > 0.0f) ? -returnStep : returnStep;
item.angularVelocity *= ROTATION_GROUND_DAMPING;
}
else
{
item.velocity.y = -item.velocity.y;
item.angularVelocity *= friction;
schema.sounds.bounce.play();
}
item.velocity.y *= friction;
}
item.velocity.x *= airResistance;
item.velocity.y += gravity;
}
item.position.x = glm::clamp(bounds.x, item.position.x, bounds.z);
item.position.y = glm::clamp(bounds.y, item.position.y, bounds.w);
}
if (heldItemMoveIndex != -1 && heldItemMoveIndex < (int)items.size() - 1)
{
auto heldItem = std::move(items[heldItemMoveIndex]);
items.erase(items.begin() + heldItemMoveIndex);
items.push_back(std::move(heldItem));
heldItemIndex = (int)items.size() - 1;
}
cursorPositionPrevious = cursorPosition;
cursorDeltaPrevious = cursorDelta;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../entity/cursor.hpp"
#include "../../entity/item.hpp"
#include "area_manager.hpp"
#include "text.hpp"
namespace game::state::main
{
class ItemManager
{
public:
static constexpr auto LIMIT = 100;
std::vector<entity::Item> items{};
int heldItemIndex{-1};
int queuedRemoveItemIndex{-1};
bool isItemHovered{};
bool isItemHoveredPrevious{};
bool isJustItemHoveredStopped{};
bool isItemHeld{};
bool isItemHeldPrevious{};
bool isJustItemHeldStopped{};
bool isJustItemHeld{};
bool isJustItemThrown{};
bool isQueueFinishFood{};
bool isItemFinished{};
glm::vec2 cursorPositionPrevious{};
glm::vec2 cursorDeltaPrevious{};
std::vector<int> queuedItemIDs{};
std::vector<int> returnItemIDs{};
void update(entity::Character&, entity::Cursor&, AreaManager&, Text&, const glm::vec4& bounds, Canvas&);
};
}

165
src/state/main/menu.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include "menu.hpp"
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include <algorithm>
using namespace game::util;
using namespace game::util::imgui;
namespace game::state::main
{
void Menu::tick()
{
inventory.tick();
play.tick();
}
void Menu::update(Resources& resources, ItemManager& itemManager, entity::Character& character,
entity::Cursor& cursor, Text& text, Canvas& canvas)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
auto& schema = character.data.menuSchema;
auto style = ImGui::GetStyle();
auto& io = ImGui::GetIO();
slide.update(isOpen, io.DeltaTime);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER, windowSize.y - style.WindowPadding.y * 2);
auto targetX = windowSize.x - size.x;
auto t = slide.value_get();
auto eased = slide.eased_get();
auto posX = windowSize.x + (targetX - windowSize.x) * eased;
auto pos = ImVec2(posX, style.WindowPadding.y);
auto barSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), windowSize.y - style.WindowPadding.y * 2);
auto barPos = ImVec2(pos.x - barSize.x - style.WindowPadding.x, style.WindowPadding.y);
if (slide.is_visible())
{
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Menu", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
if (ImGui::BeginTabBar("##Options"))
{
if (isChat && WIDGET_FX(ImGui::BeginTabItem("Chat")))
{
chat.update(resources, text, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Play")))
{
play.update(resources, character, inventory, text);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Items")))
{
inventory.update(resources, itemManager, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Stats")))
{
stats.update(resources, play, character);
ImGui::EndTabItem();
}
if (WIDGET_FX(ImGui::BeginTabItem("Settings")))
{
configuration.update(resources, Configuration::MAIN);
ImGui::EndTabItem();
}
if (isCheats && WIDGET_FX(ImGui::BeginTabItem("Cheats")))
{
cheats.update(resources, character, inventory, text);
ImGui::EndTabItem();
}
if (isDebug && WIDGET_FX(ImGui::BeginTabItem("Debug")))
{
debug.update(character, cursor, itemManager, canvas);
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::End();
}
ImGui::SetNextWindowSize(barSize);
ImGui::SetNextWindowPos(barPos);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
if (ImGui::Begin("##Menu Open Bar", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto buttonSize = ImGui::GetContentRegionAvail();
auto cursorPos = ImGui::GetCursorScreenPos();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding);
auto result = WIDGET_FX(ImGui::Button("##MenuToggle", buttonSize));
if (t <= 0.0f || t >= 1.0f)
{
ImGui::SetItemTooltip(isOpen ? "Close Main Menu" : "Open Main Menu");
if (result)
{
isOpen = !isOpen;
if (isOpen)
schema.sounds.open.play();
else
schema.sounds.close.play();
}
if (!isOpen && t <= 0.0f && ImGui::IsItemHovered())
{
isOpen = true;
schema.sounds.open.play();
}
}
ImGui::PopStyleVar();
auto center = ImVec2(cursorPos.x + (buttonSize.x * 0.5f), cursorPos.y + (buttonSize.y * 0.5f));
auto half = std::min(buttonSize.x, buttonSize.y) * 0.22f;
ImVec2 tip;
ImVec2 baseA;
ImVec2 baseB;
if (isOpen)
{
tip = ImVec2(center.x + half, center.y);
baseA = ImVec2(center.x - half, center.y - half);
baseB = ImVec2(center.x - half, center.y + half);
}
else
{
tip = ImVec2(center.x - half, center.y);
baseA = ImVec2(center.x + half, center.y - half);
baseB = ImVec2(center.x + half, center.y + half);
}
auto color = ImGui::GetColorU32(ImGuiCol_Text);
ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
}
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar(2);
}
}

39
src/state/main/menu.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <imgui.h>
#include "../configuration.hpp"
#include "chat.hpp"
#include "cheats.hpp"
#include "debug.hpp"
#include "play.hpp"
#include "stats.hpp"
#include "text.hpp"
#include "../../util/imgui/window_slide.hpp"
namespace game::state::main
{
class Menu
{
public:
Play play;
Chat chat;
Cheats cheats;
Debug debug;
Stats stats;
Inventory inventory;
state::Configuration configuration;
bool isCheats{true};
bool isDebug{true};
bool isOpen{true};
bool isChat{true};
util::imgui::WindowSlide slide{};
void tick();
void update(Resources&, ItemManager&, entity::Character&, entity::Cursor&, Text&, Canvas&);
};
}

433
src/state/main/play.cpp Normal file
View File

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

87
src/state/main/play.hpp Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
#include "../../canvas.hpp"
#include "../../entity/actor.hpp"
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "inventory.hpp"
#include "text.hpp"
#include <imgui.h>
#include <map>
#include <unordered_map>
#include <vector>
namespace game::state::main
{
class Play
{
public:
struct Range
{
float min{};
float max{};
};
struct Challenge
{
Range range{};
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{};
int score{};
int combo{};
int endTimer{};
int endTimerMax{};
int highScoreStart{};
int bestCombo{};
int highScore{};
int totalPlays{};
std::map<int, int> gradeCounts{};
bool isActive{true};
bool isRewardScoreAchieved{false};
bool isHighScoreAchieved{false};
bool isHighScoreAchievedThisRun{false};
bool isGameOver{};
std::vector<Toast> toasts{};
std::vector<Item> items{};
std::unordered_map<int, entity::Actor> itemActors{};
std::unordered_map<int, glm::vec4> itemRects{};
std::unordered_map<int, Canvas> itemCanvases{};
Play() = default;
Play(entity::Character&);
Challenge challenge_generate(entity::Character&);
void tick();
void update(Resources&, entity::Character&, Inventory&, Text&);
float accuracy_score_get(entity::Character&);
};
}

48
src/state/main/stats.cpp Normal file
View File

@@ -0,0 +1,48 @@
#include "stats.hpp"
#include <ranges>
#include "../../util/measurement.hpp"
using namespace game::resource;
using namespace game::util;
namespace game::state::main
{
void Stats::update(Resources& resources, Play& play, entity::Character& character)
{
ImGui::PushFont(ImGui::GetFont(), Font::BIG);
ImGui::TextUnformatted(character.data.name.c_str());
ImGui::PopFont();
ImGui::Separator();
auto& playSchema = character.data.playSchema;
auto& system = resources.settings.measurementSystem;
auto weight = character.weight_get(system);
auto weightUnit = system == measurement::IMPERIAL ? "lbs" : "kg";
ImGui::Text("Weight: %0.2f %s (Stage: %i)", weight, weightUnit, character.stage_get() + 1);
ImGui::Text("Capacity: %0.0f kcal (Max: %0.0f kcal)", character.capacity, character.max_capacity());
ImGui::Text("Digestion Rate: %0.2f%%/sec", character.digestion_rate_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed);
ImGui::SeparatorText("Totals");
ImGui::Text("Total Calories Consumed: %0.0f kcal", character.totalCaloriesConsumed);
ImGui::Text("Total Food Items Eaten: %i", character.totalFoodItemsEaten);
ImGui::SeparatorText("Play");
ImGui::Text("Best: %i pts (%ix)", play.highScore, play.bestCombo);
ImGui::Text("Total Plays: %i", play.totalPlays);
for (int i = 0; i < (int)playSchema.grades.size(); i++)
{
auto& grade = playSchema.grades[i];
ImGui::Text("%s: %i", grade.namePlural.c_str(), play.gradeCounts[i]);
}
ImGui::Text("Accuracy: %0.2f%%", play.accuracy_score_get(character));
}
}

17
src/state/main/stats.hpp Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "../../entity/character.hpp"
#include "../../resources.hpp"
#include "play.hpp"
#include <imgui.h>
namespace game::state::main
{
class Stats
{
public:
void update(Resources&, Play&, entity::Character&);
};
}

205
src/state/main/text.cpp Normal file
View File

@@ -0,0 +1,205 @@
#include "text.hpp"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <string_view>
#include "../../util/imgui.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"
using namespace game::util;
namespace game::state::main
{
const char* utf8_advance_chars(const char* text, const char* end, int count)
{
const char* it = text;
while (it < end && count > 0)
{
unsigned int codepoint = 0;
int step = ImTextCharFromUtf8(&codepoint, it, end);
if (step <= 0) break;
it += step;
--count;
}
return it;
}
void Text::set(resource::xml::Dialogue::Entry* entry, entity::Character& character)
{
if (!entry) return;
this->entry = entry;
isFinished = false;
index = 0;
time = 0.0f;
isEnabled = true;
character.isTalking = true;
if (!entry->animation.empty()) character.play_convert(entry->animation);
if (entry->text.empty()) isEnabled = false;
}
void Text::tick(entity::Character& character)
{
if (!entry || isFinished) return;
index++;
if (index >= ImTextCountCharsFromUtf8(entry->text.c_str(), entry->text.c_str() + entry->text.size()))
{
isFinished = true;
character.isTalking = false;
}
}
void Text::update(entity::Character& character)
{
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
static constexpr auto HEIGHT_MULTIPLIER = 6.0f;
if (!entry) return;
auto& dialogue = character.data.dialogue;
auto& menuSchema = character.data.menuSchema;
auto& style = ImGui::GetStyle();
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER);
auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), windowSize.y - size.y - style.WindowPadding.y);
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (!entry) return;
if (ImGui::Begin("##Text", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
auto isHovered = ImGui::IsWindowHovered();
auto isMouse = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
auto isSpace = ImGui::IsKeyReleased(ImGuiKey_Space);
auto isAdvance = (isHovered && (isMouse || isSpace));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, 0));
if (ImGui::BeginTabBar("##Name"))
{
if (ImGui::BeginTabItem(character.data.name.c_str())) ImGui::EndTabItem();
ImGui::EndTabBar();
}
auto available = ImGui::GetContentRegionAvail();
auto font = ImGui::GetFont();
auto fontSize = resource::Font::NORMAL;
ImGui::PushFont(font, fontSize);
auto text = [&]()
{
auto text = entry ? std::string_view(entry->text) : "null";
auto length = std::clamp(index, 0, ImTextCountCharsFromUtf8(text.data(), text.data() + text.size()));
if (length <= 0)
{
ImGui::Dummy(ImVec2(1.0f, ImGui::GetTextLineHeight()));
return;
}
const char* textStart = text.data();
const char* textEnd = textStart + text.size();
const char* textLimit = utf8_advance_chars(textStart, textEnd, length);
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + available.x);
ImGui::TextUnformatted(textStart, textLimit);
ImGui::PopTextWrapPos();
};
text();
if (entry)
{
if (isFinished)
{
if (!entry->choices.empty())
{
ImGui::SetCursorPos(ImVec2(ImGui::GetStyle().WindowPadding.x, available.y));
auto buttonSize = imgui::row_widget_size_get(entry->choices.size());
for (auto& branch : entry->choices)
{
if (WIDGET_FX(ImGui::Button(branch.text.c_str(), buttonSize)))
set(dialogue.get(branch.nextID), character);
ImGui::SetItemTooltip("%s", branch.text.c_str());
ImGui::SameLine();
}
if (isHovered && isSpace)
{
set(dialogue.get(entry->choices.front().nextID), character);
menuSchema.sounds.select.play();
}
}
else
{
if (entry->nextID != -1)
{
ImGui::SetCursorPos(ImVec2(available.x - ImGui::GetTextLineHeightWithSpacing(), available.y));
auto indicatorSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing());
auto cursorPos = ImGui::GetCursorScreenPos();
auto center = ImVec2(cursorPos.x + (indicatorSize.x * 0.5f), cursorPos.y + (indicatorSize.y * 0.5f));
auto half = std::min(indicatorSize.x, indicatorSize.y) * 0.35f;
auto tip = ImVec2(center.x + half, center.y);
auto baseA = ImVec2(center.x - half, center.y - half);
auto baseB = ImVec2(center.x - half, center.y + half);
auto color = ImGui::GetColorU32(ImGuiCol_Text);
ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
ImGui::Dummy(indicatorSize);
if (isAdvance)
{
menuSchema.sounds.select.play();
set(dialogue.get(entry->nextID), character);
}
}
else if (isAdvance)
{
isEnabled = false;
entry = nullptr;
}
}
}
else
{
if (isAdvance)
{
index = ImTextCountCharsFromUtf8(entry->text.c_str(), entry->text.c_str() + entry->text.size());
isFinished = true;
character.isTalking = false;
}
}
}
ImGui::PopFont();
ImGui::PopStyleVar();
};
ImGui::End();
if (isEnabled && isFinished && entry && entry->is_last())
{
if (time += ImGui::GetIO().DeltaTime; time > LIFETIME)
{
isEnabled = false;
entry = nullptr;
}
}
}
bool Text::is_interruptible() const { return !entry || (entry && entry->is_last()); }
}

29
src/state/main/text.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <imgui.h>
#include "../../entity/character.hpp"
#include "../../resources.hpp"
namespace game::state::main
{
class Text
{
int index{};
bool isFinished{};
public:
static constexpr auto LIFETIME = 10.0f;
resource::xml::Dialogue::Entry* entry{};
bool isEnabled{true};
float time{};
void set(resource::xml::Dialogue::Entry*, entity::Character&);
void tick(entity::Character&);
void update(entity::Character&);
bool is_interruptible() const;
};
}

Some files were not shown because too many files have changed in this diff Show More