The Snivy Video Game Is Complete

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

View File

@@ -9,6 +9,8 @@ namespace game
GLuint Canvas::textureVAO = 0;
GLuint Canvas::textureVBO = 0;
GLuint Canvas::textureEBO = 0;
GLuint Canvas::rectVAO = 0;
GLuint Canvas::rectVBO = 0;
bool Canvas::isStaticInit = false;
Canvas::Canvas(vec2 size, bool isDefault)
@@ -66,6 +68,20 @@ namespace game
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// Rect
glGenVertexArrays(1, &rectVAO);
glGenBuffers(1, &rectVBO);
glBindVertexArray(rectVAO);
glBindBuffer(GL_ARRAY_BUFFER, rectVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(RECT_VERTICES), RECT_VERTICES, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindVertexArray(0);
}
}
@@ -89,9 +105,6 @@ namespace game
void Canvas::texture_render(Shader& shader, GLuint textureId, mat4& model, vec4 tint, vec3 colorOffset,
float* vertices) const
{
auto view = view_get();
auto projection = projection_get();
glUseProgram(shader.id);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_TEXTURE), 0);
@@ -99,8 +112,9 @@ namespace game
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_MODEL), 1, GL_FALSE, value_ptr(model));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW), 1, GL_FALSE, value_ptr(view));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_PROJECTION), 1, GL_FALSE, value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW), 1, GL_FALSE, value_ptr(view_get()));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_PROJECTION), 1, GL_FALSE,
value_ptr(projection_get()));
glBindVertexArray(textureVAO);
@@ -118,6 +132,25 @@ namespace game
glUseProgram(0);
}
void Canvas::rect_render(Shader& shader, mat4& model, vec4 color) const
{
glUseProgram(shader.id);
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), 1, value_ptr(color));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_MODEL), 1, GL_FALSE, value_ptr(model));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW), 1, GL_FALSE, value_ptr(view_get()));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_PROJECTION), 1, GL_FALSE,
value_ptr(projection_get()));
glBindVertexArray(rectVAO);
glDrawArrays(GL_LINE_LOOP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
void Canvas::render(Shader& shader, mat4& model, vec4 tint, vec3 colorOffset) const
{
texture_render(shader, texture, model, tint, colorOffset);

View File

@@ -20,9 +20,13 @@ namespace game
static constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
static constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
static GLuint textureVAO;
static GLuint textureVBO;
static GLuint textureEBO;
static GLuint rectVAO;
static GLuint rectVBO;
static bool isStaticInit;
public:
@@ -42,6 +46,7 @@ namespace game
glm::mat4 projection_get() const;
void texture_render(resource::Shader&, GLuint, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)TEXTURE_VERTICES) 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 bind() const;
void unbind() const;

273
src/character.cpp Normal file
View File

@@ -0,0 +1,273 @@
#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);
}
}

185
src/character.h Normal file
View File

@@ -0,0 +1,185 @@
#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&);
};
}

13
src/cursor.cpp Normal file
View File

@@ -0,0 +1,13 @@
#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()); }
}

19
src/cursor.h Normal file
View File

@@ -0,0 +1,19 @@
#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();
};
}

12
src/game_data.h Normal file
View File

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

129
src/item.cpp Normal file
View File

@@ -0,0 +1,129 @@
#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;
};
}
}

241
src/item.h Normal file
View File

@@ -0,0 +1,241 @@
#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

@@ -11,6 +11,8 @@
#include <imgui.h>
#include <iostream>
#include "util/math_.h"
#include <SDL3_mixer/SDL_mixer.h>
#ifdef __EMSCRIPTEN__
@@ -24,7 +26,14 @@ constexpr auto GLSL_VERSION = "#version 330";
#endif
constexpr auto WINDOW_ROUNDING = 6.0f;
constexpr auto WINDOW_COLOR = ImVec4(0.05f, 0.35f, 0.08f, 1.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;
namespace game
{
@@ -48,7 +57,7 @@ namespace game
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL);
if (!window)
{
@@ -88,6 +97,7 @@ namespace game
#endif
glEnable(GL_BLEND);
glLineWidth(2.0f);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);
@@ -116,22 +126,38 @@ namespace game
std::cout << "Initialized Dear ImGui" << "\n";
ImGui::StyleColorsDark();
ImGuiIO& io = ImGui::GetIO();
ImGuiStyle& style = ImGui::GetStyle();
io.IniFilename = nullptr;
io.Fonts->AddFontFromFileTTF("resources/font/pmd.ttf");
style.WindowRounding = WINDOW_ROUNDING;
style.ChildRounding = style.WindowRounding;
style.FrameRounding = style.WindowRounding;
style.GrabRounding = style.WindowRounding;
style.PopupRounding = style.WindowRounding;
style.ScrollbarRounding = style.WindowRounding;
ImGuiStyle& style = ImGui::GetStyle();
ImGui::StyleColorsDark();
style.Colors[ImGuiCol_TitleBg] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgActive] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgCollapsed] = WINDOW_COLOR;
io.IniFilename = nullptr;
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))
{
@@ -153,6 +179,8 @@ namespace game
}
std::cout << "Initialize Dear ImGui OpenGL backend" << "\n";
math::random_seed_set();
}
Loader::~Loader()

View File

@@ -9,6 +9,7 @@
#include "../resource/texture.h"
#include <glm/glm.hpp>
#include <iostream>
using namespace glm;
using namespace game::util;
@@ -16,20 +17,70 @@ using namespace game::anm2;
namespace game::resource
{
std::shared_ptr<void> texture_callback(const std::filesystem::path& path) { return std::make_shared<Texture>(path); }
std::shared_ptr<void> sound_callback(const std::filesystem::path& path) { return std::make_shared<Audio>(path); }
Actor::Actor(const std::filesystem::path& path, vec2 position) : anm2(path, texture_callback, sound_callback)
Actor::Actor(Anm2* _anm2, vec2 _position, Mode mode, float time) : anm2(_anm2), position(_position)
{
this->position = position;
play(anm2.animations.defaultAnimation);
if (anm2)
{
this->mode = mode;
this->startTime = time;
play(anm2->animations.defaultAnimation, mode, time);
}
}
anm2::Animation* Actor::animation_get() { return vector::find(anm2.animations.items, animationIndex); }
anm2::Item* Actor::item_get(anm2::Type type, int id)
anm2::Animation* Actor::animation_get(int index)
{
if (auto animation = animation_get())
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)
{
@@ -51,6 +102,16 @@ namespace game::resource
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))
@@ -66,7 +127,14 @@ namespace game::resource
return nullptr;
}
anm2::Frame Actor::frame_generate(anm2::Item& item, float time)
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;
@@ -76,6 +144,7 @@ namespace game::resource
time = time < 0.0f ? 0.0f : time;
anm2::Frame* frameNext = nullptr;
anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
@@ -90,7 +159,10 @@ namespace game::resource
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
@@ -99,53 +171,110 @@ namespace game::resource
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, frameNext->rotation, interpolation);
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation);
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(const std::string& name)
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
{
for (int i = 0; i < anm2.animations.items.size(); i++)
{
if (anm2.animations.items[i].name == name)
{
animationIndex = i;
time = 0.0f;
isPlaying = true;
break;
}
}
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;
time += anm2.info.fps / 30.0f;
playedEventID = -1;
auto intTime = (int)time;
if (auto trigger = trigger_get(intTime))
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(intTime))
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
if (auto sound = map::find(anm2.content.sounds, trigger->soundID)) sound->audio.play();
playedTriggers.insert(intTime);
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)
@@ -155,43 +284,138 @@ namespace game::resource
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;
}
}
void Actor::render(Shader& shader, Canvas& canvas)
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);
auto rootModel = math::quad_model_parent_get(root.position + position, root.pivot,
math::percent_to_unit(root.scale), root.rotation);
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);
auto layer = map::find(anm2->content.layers, i);
if (!layer) continue;
auto spritesheet = map::find(anm2.content.spritesheets, layer->spritesheetID);
auto spritesheet = map::find(anm2->content.spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time);
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::percent_to_unit(frame.scale),
frame.rotation);
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(shader, texture.id, model, frame.tint, frame.colorOffset, uvVertices.data());
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

@@ -11,23 +11,70 @@ namespace game::resource
{
public:
anm2::Anm2 anm2{};
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};
std::unordered_set<int> playedTriggers{};
int previousAnimationIndex{-1};
int lastPlayedAnimationIndex{-1};
int playedEventID{-1};
Mode mode{PLAY};
float startTime{};
float speedMultiplier{};
Actor(const std::filesystem::path&, glm::vec2);
anm2::Animation* animation_get();
anm2::Animation* animation_get(std::string&);
int animation_index_get(anm2::Animation&);
anm2::Item* item_get(anm2::Type, int = -1);
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);
anm2::Frame frame_generate(anm2::Item&, float);
void play(const std::string&);
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();
void render(Shader&, Canvas&);
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,46 +1,26 @@
#include "anm2.h"
#include <iostream>
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::resource;
using namespace game::util;
namespace game::anm2
{
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_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;
}
Info::Info(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Fps", &fps);
}
Spritesheet::Spritesheet(XMLElement* element, int& id, TextureCallback textureCallback)
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_path_attribute(element, "Path", &path);
xml::query_path_attribute(element, "Path", &path);
texture = Texture(path);
}
@@ -48,7 +28,7 @@ namespace game::anm2
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
@@ -56,7 +36,7 @@ namespace game::anm2
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
xml::query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
@@ -64,18 +44,18 @@ namespace game::anm2
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
xml::query_string_attribute(element, "Name", &name);
}
Sound::Sound(XMLElement* element, int& id, SoundCallback soundCallback)
Sound::Sound(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_path_attribute(element, "Path", &path);
xml::query_path_attribute(element, "Path", &path);
audio = Audio(path);
}
Content::Content(XMLElement* element, TextureCallback textureCallback, SoundCallback soundCallback)
Content::Content(XMLElement* element)
{
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
{
@@ -83,7 +63,7 @@ namespace game::anm2
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet(child, spritesheetId, textureCallback);
Spritesheet spritesheet(child, spritesheetId);
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
@@ -123,7 +103,7 @@ namespace game::anm2
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound(child, soundId, soundCallback);
Sound sound(child, soundId);
sounds.emplace(soundId, std::move(sound));
}
}
@@ -148,13 +128,13 @@ namespace game::anm2
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible);
query_color_attribute(element, "RedTint", &tint.r);
query_color_attribute(element, "GreenTint", &tint.g);
query_color_attribute(element, "BlueTint", &tint.b);
query_color_attribute(element, "AlphaTint", &tint.a);
query_color_attribute(element, "RedOffset", &colorOffset.r);
query_color_attribute(element, "GreenOffset", &colorOffset.g);
query_color_attribute(element, "BlueOffset", &colorOffset.b);
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);
}
@@ -180,7 +160,7 @@ namespace game::anm2
Animation::Animation(XMLElement* element)
{
query_string_attribute(element, "Name", &name);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
@@ -215,13 +195,20 @@ namespace game::anm2
Animations::Animations(XMLElement* element)
{
query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
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, TextureCallback textureCallback, SoundCallback soundCallback)
Anm2::Anm2(const std::filesystem::path& path)
{
XMLDocument document;
@@ -231,18 +218,17 @@ namespace game::anm2
return;
}
std::cout << "Initialzed anm2: " << path.string() << "\n";
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, textureCallback, soundCallback);
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,6 +1,6 @@
#pragma once
#include <functional>
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
@@ -25,9 +25,6 @@ namespace game::anm2
TRIGGER
};
using TextureCallback = std::function<std::shared_ptr<void>(const std::filesystem::path&)>;
using SoundCallback = std::function<std::shared_ptr<void>(const std::filesystem::path&)>;
class Info
{
public:
@@ -43,7 +40,7 @@ namespace game::anm2
std::filesystem::path path{};
resource::Texture texture{};
Spritesheet(tinyxml2::XMLElement*, int&, TextureCallback = nullptr);
Spritesheet(tinyxml2::XMLElement*, int&);
};
class Layer
@@ -75,7 +72,7 @@ namespace game::anm2
std::filesystem::path path{};
resource::Audio audio{};
Sound(tinyxml2::XMLElement*, int&, SoundCallback = nullptr);
Sound(tinyxml2::XMLElement*, int&);
};
class Content
@@ -88,7 +85,7 @@ namespace game::anm2
std::map<int, Sound> sounds{};
Content() = default;
Content(tinyxml2::XMLElement*, TextureCallback = nullptr, SoundCallback = nullptr);
Content(tinyxml2::XMLElement*);
};
struct Frame
@@ -113,6 +110,20 @@ namespace game::anm2
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:
@@ -145,6 +156,8 @@ namespace game::anm2
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*);
@@ -158,6 +171,6 @@ namespace game::anm2
Animations animations{};
Anm2() = default;
Anm2(const std::filesystem::path&, TextureCallback = nullptr, SoundCallback = nullptr);
Anm2(const std::filesystem::path&);
};
}

137
src/resource/dialogue.cpp Normal file
View File

@@ -0,0 +1,137 @@
#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); }
}

118
src/resource/dialogue.h Normal file
View File

@@ -0,0 +1,118 @@
#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*);
};
}

10
src/resource/font.cpp Normal file
View File

@@ -0,0 +1,10 @@
#include "font.h"
namespace game::resource
{
Font::Font(const std::string& path, float size)
{
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size);
}
ImFont* Font::get() { return internal; };
}

20
src/resource/font.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "imgui.h"
#include <string>
namespace game::resource
{
class Font
{
public:
ImFont* internal;
static constexpr auto NORMAL = 12;
static constexpr auto BIG = 16;
static constexpr auto LARGE = 24;
Font(const std::string&, float = NORMAL);
ImFont* get();
};
}

View File

@@ -15,7 +15,7 @@ namespace game::resource::shader
};
#ifdef __EMSCRIPTEN__
constexpr auto VERTEX = R"(#version 300 es
inline constexpr auto VERTEX = R"(#version 300 es
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 v_uv;
@@ -30,7 +30,17 @@ namespace game::resource::shader
}
)";
constexpr auto FRAGMENT = R"(#version 300 es
inline constexpr auto FRAGMENT = R"(#version 300 es
precision mediump float;
uniform vec4 u_color;
out vec4 o_fragColor;
void main()
{
o_fragColor = u_color;
}
)";
inline constexpr auto TEXTURE_FRAGMENT = R"(#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D u_texture;
@@ -46,7 +56,7 @@ namespace game::resource::shader
}
)";
#else
constexpr auto VERTEX = R"(#version 330 core
inline constexpr auto VERTEX = R"(#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 v_uv;
@@ -61,7 +71,17 @@ namespace game::resource::shader
}
)";
constexpr auto FRAGMENT = R"(#version 330 core
inline constexpr auto FRAGMENT = R"(
#version 330 core
out vec4 o_fragColor;
uniform vec4 u_color;
void main()
{
o_fragColor = u_color;
}
)";
inline constexpr auto TEXTURE_FRAGMENT = R"(#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform vec4 u_tint;
@@ -75,6 +95,7 @@ namespace game::resource::shader
o_fragColor = texColor;
}
)";
#endif
constexpr auto UNIFORM_MODEL = "u_model";
@@ -82,9 +103,12 @@ namespace game::resource::shader
constexpr auto UNIFORM_PROJECTION = "u_projection";
constexpr auto UNIFORM_TEXTURE = "u_texture";
constexpr auto UNIFORM_TINT = "u_tint";
constexpr auto UNIFORM_COLOR = "u_color";
constexpr auto UNIFORM_COLOR_OFFSET = "u_color_offset";
#define SHADERS X(TEXTURE, VERTEX, FRAGMENT)
#define SHADERS \
X(TEXTURE, VERTEX, TEXTURE_FRAGMENT) \
X(RECT, VERTEX, FRAGMENT)
enum Type
{

View File

@@ -1,6 +1,7 @@
#include "resources.h"
using namespace game::resource;
using namespace game::anm2;
namespace game
{
@@ -8,5 +9,16 @@ namespace game
{
for (int i = 0; i < shader::COUNT; i++)
shaders[i] = Shader(shader::INFO[i].vertex, shader::INFO[i].fragment);
for (int i = 0; i < audio::COUNT; i++)
audio[i] = Audio(audio::PATHS[i]);
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(); }
}

View File

@@ -1,14 +1,128 @@
#pragma once
#include "resource/anm2.h"
#include "resource/audio.h"
#include "resource/dialogue.h"
#include "resource/font.h"
#include "resource/shader.h"
namespace game::audio
{
#define AUDIO \
X(BLIP, "resources/sfx/blip.ogg") \
X(ADVANCE, "resources/sfx/advance.ogg") \
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
{
class Resources
{
public:
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::Dialogue dialogue{"resources/dialogue.xml"};
Resources();
void sound_play(audio::Type);
};
}

View File

@@ -4,14 +4,17 @@
#include <backends/imgui_impl_sdl3.h>
#include <imgui.h>
#include "util/math_.h"
using namespace glm;
using namespace game::resource;
using namespace game::util;
namespace game
{
constexpr auto TICK_RATE = 30;
constexpr auto TICK_INTERVAL = (1000 / TICK_RATE);
constexpr auto UPDATE_RATE = 120;
constexpr auto UPDATE_RATE = 60;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
State::State(SDL_Window* inWindow, SDL_GLContext inContext, vec2 size)
@@ -19,10 +22,37 @@ namespace game
{
}
void State::tick() { actor.tick(); }
void State::tick()
{
for (auto& item : items)
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);
textWindow.set_random(resources.dialogue.postDigestIDs, resources, character);
}
cursor.tick();
textWindow.tick(resources, character);
}
void State::update()
{
static bool isRubbing{};
auto& inventory = mainMenuWindow.inventory;
auto& dialogue = resources.dialogue;
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
SDL_Event event;
while (SDL_PollEvent(&event))
@@ -36,15 +66,322 @@ namespace game
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Metrics");
ImGui::Text("Time: %f", actor.time);
ImGui::Text("IS Playing: %s", actor.isPlaying ? "true" : "false");
auto style = ImGui::GetStyle();
auto animation = actor.animation_get();
ImGui::Text("Animation: %s", animation ? animation->name.c_str() : "null");
ImGui::End();
if (textWindow.isFlagActivated)
{
switch (textWindow.flag)
{
case Dialogue::Entry::ACTIVATE_WINDOWS:
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;
}
}
ImGui::Render();
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.values[type]--;
if (inventory.values[type] == 0) inventory.values.erase(type);
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.values[item.type]++;
resources.sound_play(audio::RETURN);
}
else
resources.sound_play(audio::DISPOSE);
items.erase(items.begin() + i--);
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)
{
if (character.is_over_capacity())
{
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)
{
}
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()
@@ -55,12 +392,28 @@ namespace game
int height{};
SDL_GetWindowSize(window, &width, &height);
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
canvas.bind();
canvas.clear(glm::vec4(0, 0, 0, 1));
actor.render(resources.shaders[shader::TEXTURE], canvas);
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();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}

View File

@@ -2,24 +2,43 @@
#include <SDL3/SDL.h>
#include "resource/actor.h"
#include "character.h"
#include "cursor.h"
#include "item.h"
#include "canvas.h"
#include "resources.h"
#include "window/info.h"
#include "window/main_menu.h"
#include "window/text.h"
namespace game
{
class State
{
SDL_Window* window{};
SDL_GLContext context{};
long previousUpdate{};
long previousTick{};
Resources resources;
resource::Actor actor{"resources/anm2/snivy.anm2", glm::vec2(400, 400)};
Character character{&resources.anm2s[anm2::CHARACTER], glm::vec2(300, 500)};
Cursor cursor{&resources.anm2s[anm2::CURSOR]};
long previousUpdate{};
long previousTick{};
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 update();

16
src/types.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "glm/ext/vector_float4.hpp"
namespace game
{
enum MeasurementSystem
{
METRIC,
IMPERIAL
};
constexpr auto KG_TO_LB = 2.20462;
constexpr auto WHITE = glm::vec4();
constexpr auto GRAY = glm::vec4(0.5f, 0.5f, 0.5f, 1.0f);
}

11
src/util/imgui_.cpp Normal file
View File

@@ -0,0 +1,11 @@
#include "imgui_.h"
namespace game::util::imgui
{
float row_widget_width_get(int count, float width)
{
return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count;
}
ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); }
}

19
src/util/imgui_.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <imgui.h>
#include <glm/glm.hpp>
using namespace glm;
namespace game::util::imgui
{
float widget_width_with_row_get(int = 1, float = ImGui::GetContentRegionAvail().x);
ImVec2 widget_size_with_row_get(int = 1, float = ImGui::GetContentRegionAvail().x);
inline ImVec2 to_imvec2(vec2 value) { return ImVec2(value.x, value.y); }
inline vec2 to_vec2(ImVec2 value) { return vec2(value.x, value.y); }
inline ImVec4 to_imvec4(vec4 value) { return ImVec4(value.r, value.g, value.b, value.a); }
inline vec4 to_vec4(ImVec4 value) { return vec4(value.x, value.y, value.z, value.w); }
}

View File

@@ -1,5 +1,7 @@
#include "math_.h"
#include "SDL3/SDL_rect.h"
#include "glm/ext/matrix_transform.hpp"
#include <ctime>
using namespace glm;
@@ -11,12 +13,13 @@ namespace game::util::math
vec2 scaleSign = glm::sign(scale);
vec2 pivotScaled = pivot * scaleAbsolute;
vec2 sizeScaled = size * scaleAbsolute;
float handedness = (scaleSign.x * scaleSign.y) < 0.0f ? -1.0f : 1.0f;
mat4 model(1.0f);
model = glm::translate(model, vec3(position - pivotScaled, 0.0f));
model = glm::translate(model, vec3(pivotScaled, 0.0f));
model = glm::scale(model, vec3(scaleSign, 1.0f));
model = glm::rotate(model, glm::radians(rotation), vec3(0, 0, 1));
model = glm::rotate(model, glm::radians(rotation) * handedness, vec3(0, 0, 1));
model = glm::translate(model, vec3(-pivotScaled, 0.0f));
model = glm::scale(model, vec3(sizeScaled, 1.0f));
return model;
@@ -38,4 +41,13 @@ namespace game::util::math
return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local;
}
bool is_point_in_rect(ivec4 rect, ivec2 point) { return SDL_PointInRect((SDL_Point*)&point, (SDL_Rect*)&rect); }
bool is_point_in_rectf(vec4 rect, vec2 point) { return SDL_PointInRectFloat((SDL_FPoint*)&point, (SDL_FRect*)&rect); }
float random() { return (float)rand() / RAND_MAX; }
bool random_percent_roll(float percent) { return to_percent(random()) < percent; }
float random_in_range(float min, float max) { return min + random() * (max - min); }
float random_roll(float value) { return random() * value; }
bool random_bool() { return random() < 0.5f; };
void random_seed_set() { srand(std::time(nullptr)); }
}

View File

@@ -5,15 +5,27 @@
namespace game::util::math
{
glm::mat4 quad_model_get(glm::vec2, glm::vec2, glm::vec2, glm::vec2, float);
glm::mat4 quad_model_parent_get(glm::vec2 position, glm::vec2 pivot, glm::vec2, float);
glm::mat4 quad_model_get(glm::vec2 size, glm::vec2 position = {}, glm::vec2 pivot = {},
glm::vec2 scale = glm::vec2(1.0f), float rotation = {});
glm::mat4 quad_model_parent_get(glm::vec2 position = {}, glm::vec2 pivot = {}, glm::vec2 scale = glm::vec2(1.0f),
float rotation = {});
template <typename T> constexpr T percent_to_unit(T value) { return value / 100.0f; }
template <typename T> constexpr T unit_to_percent(T value) { return value * 100.0f; }
template <typename T> constexpr T to_percent(T value) { return value * 100.0f; }
template <typename T> constexpr T to_unit(T value) { return value / 100.0f; }
constexpr std::array<float, 16> uv_vertices_get(glm::vec2 uvMin, glm::vec2 uvMax)
{
return {0.0f, 0.0f, uvMin.x, uvMin.y, 1.0f, 0.0f, uvMax.x, uvMin.y,
1.0f, 1.0f, uvMax.x, uvMax.y, 0.0f, 1.0f, uvMin.x, uvMax.y};
}
bool is_point_in_rect(glm::ivec4 rect, glm::ivec2 point);
bool is_point_in_rectf(glm::vec4 rect, glm::vec2 point);
float random();
bool random_percent_roll(float percent);
float random_roll(float value);
float random_in_range(float min, float max);
bool random_bool();
void random_seed_set();
}

62
src/util/string_.h Normal file
View File

@@ -0,0 +1,62 @@
#pragma once
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <type_traits>
namespace game::util::string
{
template <typename Number>
std::string format_commas(Number value, int decimalDigits = -1)
{
static_assert(std::is_arithmetic_v<Number>, "format_commas requires numeric types");
std::ostringstream stream;
if (decimalDigits >= 0)
{
stream.setf(std::ios::fixed);
stream << std::setprecision(decimalDigits);
}
stream << value;
std::string text = stream.str();
std::string exponent;
if (auto exponentPos = text.find_first_of("eE"); exponentPos != std::string::npos)
{
exponent = text.substr(exponentPos);
text = text.substr(0, exponentPos);
}
std::string fraction;
if (auto decimalPos = text.find('.'); decimalPos != std::string::npos)
{
fraction = text.substr(decimalPos);
text = text.substr(0, decimalPos);
}
bool isNegative = false;
if (!text.empty() && text.front() == '-')
{
isNegative = true;
text.erase(text.begin());
}
std::string formattedInteger;
formattedInteger.reserve(text.size() + (text.size() / 3));
int digitCount = 0;
for (auto it = text.rbegin(); it != text.rend(); ++it)
{
if (digitCount != 0 && digitCount % 3 == 0) formattedInteger.push_back(',');
formattedInteger.push_back(*it);
++digitCount;
}
std::reverse(formattedInteger.begin(), formattedInteger.end());
if (isNegative) formattedInteger.insert(formattedInteger.begin(), '-');
return formattedInteger + fraction + exponent;
}
}

30
src/util/xml_.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include "xml_.h"
using namespace tinyxml2;
namespace game::util::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_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;
}
}

13
src/util/xml_.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <tinyxml2.h>
#include <filesystem>
#include <string>
namespace game::util::xml
{
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
tinyxml2::XMLError query_color_attribute(tinyxml2::XMLElement*, const char*, float*);
}

28
src/window/chat.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "chat.h"
using namespace game::resource;
namespace game::window
{
void Chat::update(Resources& resources, GameData& gameData, Text& text, Character& character)
{
auto size = ImGui::GetContentRegionAvail();
ImGui::PushFont(resources.font.get(), Font::LARGE);
if (ImGui::Button("Let's chat!", ImVec2(size.x, 0)))
{
resources.sound_play(audio::ADVANCE);
text.set_random(resources.dialogue.randomIDs, resources, character);
}
ImGui::PopFont();
ImGui::SetItemTooltip("Snivy will bring up a random conversation topic.");
if (ImGui::Button("Help", ImVec2(size.x, 0)))
{
resources.sound_play(audio::ADVANCE);
text.set(resources.dialogue.get("Help"), character);
}
ImGui::SetItemTooltip("Ask Snivy for help on this whole shebang.");
}
}

18
src/window/chat.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "../character.h"
#include "../game_data.h"
#include "../resources.h"
#include "text.h"
#include <imgui.h>
namespace game::window
{
class Chat
{
public:
void update(Resources&, GameData&, Text&, Character&);
};
}

93
src/window/info.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "info.h"
#include "../util/math_.h"
#include "../util/string_.h"
#include <algorithm>
#include <format>
namespace game::window
{
void Info::update(Resources& resources, GameData& gameData, Character& character, ImVec2 size, ImVec2 pos)
{
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 = gameData.measurementSystem;
auto weight = character.weight_get(system);
ImGui::TextUnformatted(
std::format("{} {}", util::string::format_commas(weight, 2), system == IMPERIAL ? "lbs" : "kg").c_str());
if (character.weightStage >= character.WEIGHT_STAGE_MAX - 1)
{
ImGui::ProgressBar(1.0f, ImVec2(), "MAX");
ImGui::SetItemTooltip("Maxed out!");
}
else
{
auto weightCurrent = character.weight_threshold_current_get(system);
auto weightNext = character.weight_threshold_next_get(system);
auto weightProgress = character.progress_to_next_weight_threshold_get();
ImGui::ProgressBar(weightProgress, ImVec2(), "To Next Stage");
auto format = system == IMPERIAL ? "Start: %0.2flbs | Current: %0.2flbs | Goal: %0.2flbs | (%0.2f%%)"
: "Start: %0.2fkg | Current: %0.2fkg | Goal: %0.2fkg | (%0.2f%%)";
ImGui::SetItemTooltip(format, weightCurrent, weight, weightNext, util::math::to_percent(weightProgress));
}
}
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::PushStyleColor(ImGuiCol_Text, caloriesColor);
ImGui::Text("%0.0lf kcal", calories);
ImGui::PopStyleColor();
ImGui::SameLine();
if (character.is_over_capacity()) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0, 0, 1));
ImGui::Text("/ %0.0lf kcal", character.is_over_capacity() ? character.max_capacity() : character.capacity);
if (character.is_over_capacity()) ImGui::PopStyleColor();
auto digestionProgress = character.isDigesting
? (float)character.digestionTimer / Character::DIGESTION_TIMER_MAX
: character.digestionProgress / character.DIGESTION_MAX;
ImGui::ProgressBar(digestionProgress, ImVec2(), 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!");
ImGui::Text("%0.2f%%", util::math::to_percent(digestionProgress));
ImGui::Text("Rate: %0.2f%% / sec", character.digestion_rate_second_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeedMultiplier);
ImGui::EndTooltip();
}
}
ImGui::EndChild();
}
ImGui::End();
}
}

16
src/window/info.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "../character.h"
#include "../game_data.h"
#include "../resources.h"
#include <imgui.h>
namespace game::window
{
class Info
{
public:
void update(Resources&, GameData&, Character&, ImVec2, ImVec2);
};
}

95
src/window/inventory.cpp Normal file
View File

@@ -0,0 +1,95 @@
#include "inventory.h"
#include "../util/imgui_.h"
#include <format>
using namespace game::util;
using namespace game::resource;
using namespace glm;
namespace game::window
{
void Inventory::update(Resources& resources, Character& character, GameData& gameData)
{
auto& texture = resources.anm2s[anm2::ITEMS].content.spritesheets.at(0).texture;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, BUTTON_ROUNDING);
auto cursorPos = ImGui::GetCursorPos();
auto cursorStartX = ImGui::GetCursorPosX();
for (auto& [type, quantity] : values)
{
if (quantity == 0) continue;
auto columns = (int)(texture.size.x / ITEM_SIZE.x);
auto crop = vec2(type % columns, type / columns) * ITEM_SIZE;
auto uvMin = crop / vec2(texture.size);
auto uvMax = (crop + ITEM_SIZE) / vec2(texture.size);
ImGui::PushID(type);
ImGui::SetCursorPos(cursorPos);
auto cursorScreenPos = ImGui::GetCursorScreenPos();
if (ImGui::ImageButton("##Image Button", texture.id, IMAGE_SIZE, imgui::to_imvec2(uvMin),
imgui::to_imvec2(uvMax)))
{
queuedItemType = type;
isQueued = true;
resources.sound_play(audio::SUMMON);
}
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;
}
if (ImGui::BeginItemTooltip())
{
auto& category = Item::CATEGORIES[type];
auto& rarity = Item::RARITIES[type];
auto& flavor = Item::FLAVORS[type];
auto& calories = Item::CALORIES[type];
auto& digestionRateBonus = Item::DIGESTION_RATE_BONUSES[type];
auto& eatSpeedBonus = Item::EAT_SPEED_BONUSES[type];
ImGui::Text("%s (x%i)", Item::NAMES[type], quantity);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(GRAY)));
ImGui::Text("-- %s (%s) --", Item::CATEGORY_NAMES[category], Item::RARITY_NAMES[rarity]);
if (category == Item::FOOD)
{
ImGui::Separator();
if (flavor != Item::FLAVORLESS) ImGui::Text("Flavor: %s", Item::FLAVOR_NAMES[flavor]);
if (calories != 0) ImGui::Text("%0.0f kcal", calories);
if (digestionRateBonus > 0)
ImGui::Text("Digestion Rate Bonus: +%0.2f%% / sec", digestionRateBonus * 60.0f);
else if (digestionRateBonus < 0)
ImGui::Text("Digestion Rate Penalty: %0.2f%% / sec", digestionRateBonus * 60.0f);
if (eatSpeedBonus > 0) ImGui::Text("Eat Speed Bonus: +%0.2fx ", eatSpeedBonus);
}
ImGui::Separator();
ImGui::TextUnformatted(Item::DESCRIPTIONS[type]);
ImGui::PopStyleColor();
ImGui::EndTooltip();
}
ImGui::PopID();
ImGui::PushFont(resources.font.get(), Font::BIG);
auto text = std::format("x{}", quantity);
auto textPos = ImVec2(cursorScreenPos.x + IMAGE_SIZE.x - ImGui::CalcTextSize(text.c_str()).x,
cursorScreenPos.y + IMAGE_SIZE.y - ImGui::GetTextLineHeight());
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)),
text.c_str());
ImGui::PopFont();
}
if (values.empty()) ImGui::Text("Check the \"Play\" tab to earn rewards!");
ImGui::PopStyleVar();
}
}

30
src/window/inventory.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include "../character.h"
#include "../game_data.h"
#include "../item.h"
#include "../resources.h"
#include <imgui.h>
namespace game::window
{
class Inventory
{
public:
static constexpr auto ITEM_SIZE = glm::vec2(48, 48);
static constexpr auto IMAGE_SIZE = ImVec2(48, 48);
static constexpr auto BUTTON_ROUNDING = 32.0f;
std::map<Item::Type, int> values = {{Item::POKE_PUFF_BASIC_SWEET, 1},
{Item::POKE_PUFF_BASIC_CITRUS, 1},
{Item::POKE_PUFF_BASIC_MINT, 1},
{Item::POKE_PUFF_BASIC_MOCHA, 1},
{Item::POKE_PUFF_BASIC_SPICE, 1}};
bool isQueued{};
Item::Type queuedItemType{};
void update(Resources&, Character&, GameData&);
};
}

135
src/window/main_menu.cpp Normal file
View File

@@ -0,0 +1,135 @@
#include "main_menu.h"
namespace game::window
{
void MainMenu::update(Resources& resources, Character& character, GameData& gameData, Text& text, ImVec2 size,
ImVec2 pos)
{
MeasurementSystem& measurementSystem = gameData.measurementSystem;
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
if (ImGui::Begin("##Main", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove))
{
if (ImGui::BeginTabBar("##Options", ImGuiTabBarFlags_FittingPolicyResizeDown))
{
if (ImGui::BeginTabItem("Chat"))
{
chat.update(resources, gameData, text, character);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Play"))
{
play.update(resources, character, inventory, gameData, text);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Inventory"))
{
inventory.update(resources, character, gameData);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Stats"))
{
stats.update(resources, gameData, play, character);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Settings"))
{
ImGui::SeparatorText("Measurement System");
ImGui::RadioButton("Metric", (int*)&measurementSystem, MeasurementSystem::METRIC);
ImGui::SameLine();
ImGui::RadioButton("Imperial", (int*)&measurementSystem, MeasurementSystem::IMPERIAL);
ImGui::EndTabItem();
}
if (isCheats && ImGui::BeginTabItem("Cheats"))
{
if (ImGui::Button("Feed"))
character.calories = std::min(character.calories + 100.0f, character.max_capacity());
ImGui::SameLine();
if (ImGui::Button("Starve")) character.calories = std::max(0.0f, character.calories - 100.0f);
ImGui::SameLine();
if (ImGui::Button("Digest"))
{
character.digestionProgress = Character::DIGESTION_MAX;
if (character.calories == 0.0f) character.calories = 0.001f;
}
ImGui::SameLine();
ImGui::Checkbox("Show Nulls (Hitboxes)", &character.isShowNulls);
if (ImGui::DragInt("Stage", &character.weightStage, 0.1f, 0, Character::WEIGHT_STAGE_MAX - 1))
{
character.weight = Character::WEIGHT_THRESHOLDS[character.weightStage];
character.weight = character.weight < character.highestWeight ? character.highestWeight : character.weight;
character.state_set(Character::IDLE, true);
character.isForceStageUp = true;
}
ImGui::DragFloat("Digestion Rate", &character.digestionRate, 0.005f, Character::DIGESTION_RATE_MIN,
Character::DIGESTION_RATE_MAX);
ImGui::DragFloat("Eat Speed", &character.eatSpeedMultiplier, 0.1f, Character::EAT_SPEED_MULTIPLIER_MIN,
Character::EAT_SPEED_MULTIPLIER_MAX);
ImGui::SeparatorText("Animations");
ImGui::Text("Now Playing: %s", character.anm2->animations.mapReverse.at(character.animationIndex).c_str());
if (ImGui::BeginChild("## Animations", {0, 100}, ImGuiChildFlags_Borders))
{
for (int i = 0; i < character.anm2->animations.items.size(); i++)
{
auto& animation = character.anm2->animations.items[i];
ImGui::PushID(i);
if (ImGui::Selectable(animation.name.c_str()))
character.play(animation.name.c_str(), Character::FORCE_PLAY);
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Dialogue");
if (ImGui::BeginChild("## Dialogue", {0, 100}, ImGuiChildFlags_Borders))
{
for (auto& [label, i] : resources.dialogue.labelMap)
{
ImGui::PushID(i);
if (ImGui::Selectable(label.c_str())) text.set(&resources.dialogue.entryMap.at(i), character);
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::SeparatorText("Inventory");
if (ImGui::BeginChild("## Inventory", ImGui::GetContentRegionAvail(), ImGuiChildFlags_Borders))
{
ImGui::PushItemWidth(100);
for (int i = 0; i < Item::ITEM_COUNT; i++)
{
if (i == Item::INVALID) continue;
ImGui::PushID(i);
ImGui::DragInt(Item::NAMES[i], &inventory.values[(Item::Type)i], 0.1f, 0, 999);
ImGui::PopID();
}
ImGui::PopItemWidth();
}
ImGui::EndChild();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::End();
}
}

24
src/window/main_menu.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <imgui.h>
#include "chat.h"
#include "play.h"
#include "stats.h"
#include "text.h"
namespace game::window
{
class MainMenu
{
public:
Play play;
Chat chat;
Stats stats;
Inventory inventory;
bool isCheats{};
void update(Resources&, Character&, GameData&, Text& text, ImVec2 size, ImVec2 pos);
};
}

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

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

153
src/window/play.h Normal file
View File

@@ -0,0 +1,153 @@
#pragma once
#include "../character.h"
#include "../game_data.h"
#include "../resources.h"
#include "inventory.h"
#include "text.h"
#include <imgui.h>
namespace game::window
{
class Play
{
static constexpr ImVec4 LINE_COLOR = ImVec4(1, 1, 1, 1);
static constexpr ImVec4 RECT_COLOR = ImVec4(0, 1, 0, 1);
static constexpr ImVec4 BG_COLOR = ImVec4(0, 1, 0, 0.1);
static constexpr ImVec4 PERFECT_COLOR = ImVec4(1, 1, 1, 0.75);
static constexpr auto LINE_HEIGHT = 2.0f;
static constexpr auto LINE_WIDTH_BONUS = 10.0f;
static constexpr auto RANGE_BASE = 0.75f;
static constexpr auto RANGE_MIN = 0.10f;
static constexpr auto RANGE_SCORE_BONUS = 0.0005f;
static constexpr auto SPEED_BASE = 0.005f;
static constexpr auto SPEED_MAX = 0.075f;
static constexpr auto SPEED_SCORE_BONUS = 0.000025f;
static constexpr auto EXPONENTIAL_LEVEL_MIN = 3;
static constexpr auto EXPONENTIAL_CHANCE_BASE = 0.25f;
static constexpr auto RANGE_AREA_COUNT = 5;
static constexpr auto BONUS_RANGE_CHANCE_BASE = 25.0f;
static constexpr auto END_TIMER_MAX = 30;
static constexpr auto END_TIMER_MISS_MAX = 90;
static constexpr auto HIGH_SCORE_BIG = 999;
static constexpr auto TOAST_MESSAGE_SPEED = 1.0f;
static constexpr auto REWARD_SCORE_BONUS = 0.01f;
static constexpr auto REWARD_GRADE_BONUS = 0.05f;
static constexpr auto REWARD_LEVEL_BONUS = 0.25f;
static constexpr auto REWARD_GRADE_CHANCE_BONUS = 1.0f;
static constexpr auto REWARD_LEVEL_CHANCE_BONUS = 1.0f;
static constexpr auto ITEM_SIZE = glm::vec2(48.0f, 48.0f);
public:
#define GRADES \
X(MISS, "Miss!", "Misses", 0, -0.05f) \
X(OK, "OK", "OKs", 1, 0.40f) \
X(GOOD, "Good", "Goods", 2, 0.65f) \
X(GREAT, "Great", "Greats", 3, 0.85f) \
X(EXCELLENT, "Excellent!", "Excellents", 4, 0.95f) \
X(PERFECT, "PERFECT!", "Perfects", 5, 1.00f)
enum Grade
{
#define X(symbol, name, nameStats, value, weight) symbol,
GRADES
#undef X
GRADE_COUNT
};
static constexpr const char* GRADE_STRINGS[] = {
#define X(symbol, name, nameStats, value, weight) name,
GRADES
#undef X
};
static constexpr const char* GRADE_STATS_STRINGS[] = {
#define X(symbol, name, nameStats, value, weight) nameStats,
GRADES
#undef X
};
static constexpr float GRADE_WEIGHTS[] = {
#define X(symbol, name, nameStats, value, weight) weight,
GRADES
#undef X
};
static constexpr int GRADE_VALUES[] = {
#define X(symbol, name, nameStats, value, weight) value,
GRADES
#undef X
};
#undef GRADES
struct Range
{
float min{};
float max{};
};
struct Challenge
{
Range range{};
float speed{};
float tryValue{};
int level{};
};
struct ToastMessage
{
std::string message{};
ImVec2 position;
int time{};
int timeMax{};
};
struct ToastItem
{
Item::Type type{};
ImVec2 position;
float velocityY;
};
Challenge challenge{};
Challenge queuedChallenge{};
float tryValue{};
int score{};
int combo{};
int comboBest{};
int highScore{};
int highScoreStart{};
int endTimer{};
int endTimerMax{};
bool isActive{true};
bool isHighScoreBigAchieved{false};
bool isHighScoreAchieved{false};
bool isGameOver{};
int totalPlays{};
std::unordered_map<Grade, int> gradeCounts{};
std::vector<ToastMessage> toastMessages{};
std::vector<ToastItem> toastItems{};
Play();
Challenge challenge_generate(int);
void update(Resources&, Character&, Inventory&, GameData&, Text&);
float accuracy_score_get();
};
}

49
src/window/stats.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "stats.h"
#include <format>
using namespace game::resource;
namespace game::window
{
void Stats::update(Resources& resources, GameData& gameData, Play& play, Character& character)
{
ImGui::PushFont(resources.font.get(), Font::BIG);
ImGui::Text("Snivy");
ImGui::PopFont();
ImGui::Separator();
auto& system = gameData.measurementSystem;
auto weight = character.weight_get(system);
auto weightUnit = system == MeasurementSystem::IMPERIAL ? "lbs" : "kg";
ImGui::Text("Weight: %0.2f %s (Stage: %i)", weight, weightUnit, character.weightStage + 1);
ImGui::Text("Capacity: %0.0f (Max: %0.0f)", character.capacity, character.max_capacity());
ImGui::Text("Digestion Rate: %0.2f%%/sec", character.digestion_rate_second_get());
ImGui::Text("Eating Speed: %0.2fx", character.eatSpeedMultiplier);
ImGui::SeparatorText("Totals");
ImGui::Text("Total Calories Consumed: %0.0f", character.totalCaloriesConsumed);
ImGui::Text("Total Weight Gained: %0.2f %s",
system == MeasurementSystem::IMPERIAL ? character.totalWeightGained * KG_TO_LB
: character.totalWeightGained,
weightUnit);
ImGui::Text("Food Items Eaten: %i", character.foodItemsEaten);
ImGui::SeparatorText("Play");
ImGui::Text("Best: %i pts (%ix)", play.highScore, play.comboBest);
ImGui::Text("Total Plays: %i", play.totalPlays);
for (int i = 0; i < Play::GRADE_COUNT; i++)
{
auto& value = play.gradeCounts[(Play::Grade)i];
auto string = std::format("{}", Play::GRADE_STATS_STRINGS[i]);
ImGui::Text("%s: %i", string.c_str(), value);
}
ImGui::Text("Score: %0.2f%%", play.accuracy_score_get());
}
}

18
src/window/stats.h Normal file
View File

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

351
src/window/text.cpp Normal file
View File

@@ -0,0 +1,351 @@
#include "text.h"
#include <imgui.h>
#include <algorithm>
#include <cfloat>
#include <string_view>
#include "../util/imgui_.h"
#include "../util/math_.h"
using namespace game::util;
namespace game::window
{
constexpr auto TEXT_COLOR_DEFAULT = ImVec4(1, 1, 1, 1);
namespace
{
int utf8_next_len(const char* text, const char* end)
{
if (text >= end) return 0;
const unsigned char lead = static_cast<unsigned char>(*text);
int length = 1;
if (lead < 0x80)
length = 1;
else if ((lead >> 5) == 0x6)
length = 2;
else if ((lead >> 4) == 0xE)
length = 3;
else if ((lead >> 3) == 0x1E)
length = 4;
if (text + length > end) return 1;
for (int i = 1; i < length; ++i)
{
const unsigned char byte = static_cast<unsigned char>(text[i]);
if ((byte & 0xC0) != 0x80) return 1;
}
return length;
}
int utf8_count_chars(std::string_view text)
{
const char* it = text.data();
const char* end = it + text.size();
int count = 0;
while (it < end)
{
int step = utf8_next_len(it, end);
if (step <= 0) break;
it += step;
++count;
}
return count;
}
const char* utf8_advance_chars(const char* text, const char* end, int count)
{
const char* it = text;
while (it < end && count > 0)
{
int step = utf8_next_len(it, end);
if (step <= 0) break;
it += step;
--count;
}
return it;
}
}
void Text::set(resource::Dialogue::Entry* entry, Character& character)
{
if (!entry) return;
this->entry = entry;
this->flag = entry->flag;
if (this->flag != resource::Dialogue::Entry::Flag::NONE) this->isFlagActivated = true;
isFinished = false;
index = 0;
if (!entry->animations.empty())
{
for (auto& animation : entry->animations)
{
if (animation.at == -1)
{
character.play(character.animation_name_convert(animation.name));
character.blink();
break;
}
}
}
character.talk();
}
void Text::set_random(std::vector<int>& dialogueIDs, Resources& resources, Character& character)
{
if (dialogueIDs.empty()) return;
set(resources.dialogue.get((dialogueIDs)[math::random_roll(dialogueIDs.size())]), character);
}
void Text::tick(Resources& resources, Character& character)
{
if (!entry || isFinished) return;
index++;
if (!entry->animations.empty())
{
for (auto& animation : entry->animations)
{
if (animation.at == index)
{
character.play(character.animation_name_convert(animation.name));
character.blink();
break;
}
}
}
if (index >= utf8_count_chars(entry->content)) isFinished = true;
}
void Text::update(Resources& resources, Character& character, ImVec2 size, ImVec2 pos)
{
if (!entry) return;
auto& dialogue = resources.dialogue;
ImGui::SetNextWindowSize(size);
ImGui::SetNextWindowPos(pos);
this->isFlagActivated = false;
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));
if (ImGui::BeginTabBar("##Name"))
{
if (ImGui::BeginTabItem("Snivy")) ImGui::EndTabItem();
ImGui::EndTabBar();
}
auto available = ImGui::GetContentRegionAvail();
auto font = resources.font.get();
auto fontSize = resource::Font::BIG;
ImGui::PushFont(font, fontSize);
auto text = [&]()
{
auto content = entry ? std::string_view(entry->content) : "null";
auto length = std::clamp(index, 0, utf8_count_chars(content));
if (length <= 0)
{
ImGui::Dummy(ImVec2(1.0f, ImGui::GetTextLineHeight()));
return;
}
auto drawList = ImGui::GetWindowDrawList();
auto startPos = ImGui::GetCursorScreenPos();
auto wrapWidth = available.x <= 0.0f ? FLT_MAX : available.x;
auto lineHeight = ImGui::GetTextLineHeightWithSpacing();
auto cursor = startPos;
auto maxX = startPos.x + wrapWidth;
auto color_get = [&](int i)
{
if (!entry || entry->colors.empty()) return TEXT_COLOR_DEFAULT;
for (auto& color : entry->colors)
if (i >= color.start && i <= color.end)
return ImVec4(color.value.r, color.value.g, color.value.b, color.value.a);
return TEXT_COLOR_DEFAULT;
};
const char* textStart = content.data();
const char* textEnd = textStart + content.size();
const char* textLimit = utf8_advance_chars(textStart, textEnd, length);
int i = 0;
for (const char* it = textStart; it < textLimit;)
{
int step = utf8_next_len(it, textLimit);
if (step <= 0) break;
if (*it == '\n')
{
cursor.x = startPos.x;
cursor.y += lineHeight;
it += step;
++i;
continue;
}
if (*it == ' ')
{
auto glyphSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, it, it + step);
if (cursor.x != startPos.x && cursor.x + glyphSize.x > maxX)
{
cursor.x = startPos.x;
cursor.y += lineHeight;
it += step;
++i;
continue;
}
if (cursor.x == startPos.x)
{
it += step;
++i;
continue;
}
drawList->AddText(font, fontSize, cursor, ImGui::GetColorU32(color_get(i)), it, it + step);
cursor.x += glyphSize.x;
it += step;
++i;
continue;
}
const char* wordStart = it;
const char* wordEnd = it;
float wordWidth = 0.0f;
int wordChars = 0;
for (const char* wordIt = it; wordIt < textLimit;)
{
int wordStep = utf8_next_len(wordIt, textLimit);
if (wordStep <= 0) break;
if (*wordIt == '\n' || *wordIt == ' ') break;
auto glyphSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, wordIt, wordIt + wordStep);
wordWidth += glyphSize.x;
wordIt += wordStep;
++wordChars;
wordEnd = wordIt;
}
if (cursor.x != startPos.x && cursor.x + wordWidth > maxX)
{
cursor.x = startPos.x;
cursor.y += lineHeight;
}
for (const char* wordIt = wordStart; wordIt < wordEnd;)
{
int wordStep = utf8_next_len(wordIt, wordEnd);
if (wordStep <= 0) break;
auto glyphSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, wordIt, wordIt + wordStep);
drawList->AddText(font, fontSize, cursor, ImGui::GetColorU32(color_get(i)), wordIt, wordIt + wordStep);
cursor.x += glyphSize.x;
wordIt += wordStep;
++i;
}
it = wordEnd;
}
float layoutWidth = wrapWidth == FLT_MAX ? (cursor.x - startPos.x) : wrapWidth;
if (layoutWidth <= 0.0f) layoutWidth = 1.0f;
auto totalHeight = (cursor.y - startPos.y) + lineHeight;
ImGui::Dummy(ImVec2(layoutWidth, totalHeight));
};
text();
if (entry)
{
if (isFinished)
{
character.talkOverride.isLoop = false;
if (!entry->branches.empty())
{
ImGui::SetCursorPos(ImVec2(ImGui::GetStyle().WindowPadding.x, available.y));
auto buttonSize = imgui::widget_size_with_row_get(entry->branches.size());
for (auto& branch : entry->branches)
{
if (ImGui::Button(branch.content.c_str(), buttonSize))
{
set(dialogue.get(branch.nextID), character);
resources.sound_play(audio::ADVANCE);
}
ImGui::SameLine();
}
if (isHovered && isSpace)
{
set(dialogue.get(entry->branches.front().nextID), character);
resources.sound_play(audio::ADVANCE);
}
}
else
{
if (entry->nextID != -1)
{
ImGui::SetCursorPos(ImVec2(available.x - ImGui::GetTextLineHeightWithSpacing(),
available.y - ImGui::GetStyle().WindowPadding.y));
ImGui::Text("");
if (isAdvance)
{
resources.sound_play(audio::ADVANCE);
set(dialogue.get(entry->nextID), character);
}
}
}
}
else
{
if (isAdvance)
{
index = utf8_count_chars(entry->content);
isFinished = true;
if (!entry->animations.empty())
{
auto& animation = entry->animations.back();
auto name = character.animation_name_convert(animation.name);
if (auto animationIndex = character.animation_index_get(name); animationIndex != character.animationIndex)
{
character.play(name);
character.blink();
}
}
}
}
}
ImGui::PopFont();
};
ImGui::End();
}
}

25
src/window/text.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include "../character.h"
#include "../resources.h"
#include "imgui.h"
namespace game::window
{
class Text
{
resource::Dialogue::Entry* entry{};
int index{};
bool isFinished{};
public:
resource::Dialogue::Entry::Flag flag{};
bool isFlagActivated{};
void set(resource::Dialogue::Entry*, Character&);
void set_random(std::vector<int>&, Resources&, Character&);
void tick(Resources&, Character&);
void update(Resources&, Character&, ImVec2, ImVec2);
};
}