This commit is contained in:
450
src/entity/actor.cpp
Normal file
450
src/entity/actor.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
#include "actor.hpp"
|
||||
|
||||
#include "../util/map.hpp"
|
||||
#include "../util/math.hpp"
|
||||
#include "../util/unordered_map.hpp"
|
||||
#include "../util/vector.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "../log.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace glm;
|
||||
using namespace game::util;
|
||||
using namespace game::resource::xml;
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
Actor::Override::Override(int _id, Anm2::Type _type, Actor::Override::Mode _mode, FrameOptional _frame,
|
||||
std::optional<float> _time, Actor::Override::Function _function, float _cycles)
|
||||
: id(_id), type(_type), mode(_mode), frame(_frame), time(_time), function(_function), cycles(_cycles)
|
||||
{
|
||||
frameBase = _frame;
|
||||
timeStart = _time;
|
||||
}
|
||||
|
||||
Actor::Actor(const Actor&) = default;
|
||||
Actor::Actor(Actor&&) noexcept = default;
|
||||
Actor& Actor::operator=(const Actor&) = default;
|
||||
Actor& Actor::operator=(Actor&&) noexcept = default;
|
||||
|
||||
Actor::Actor(Anm2 _anm2, vec2 _position, Mode mode, float time, int animationIndex) : Anm2(_anm2), position(_position)
|
||||
{
|
||||
this->mode = mode;
|
||||
this->startTime = time;
|
||||
if (animationIndex != -1)
|
||||
play(animationIndex, mode, time);
|
||||
else
|
||||
play_default_animation(mode, time);
|
||||
}
|
||||
|
||||
Anm2::Animation* Actor::animation_get(int index)
|
||||
{
|
||||
if (index == -1) index = animationIndex;
|
||||
if (animationMapReverse.contains(index)) return &animations[index];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Anm2::Animation* Actor::animation_get(const std::string& name)
|
||||
{
|
||||
if (animationMap.contains(name)) return &animations[animationMap[name]];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Actor::is_playing(const std::string& name)
|
||||
{
|
||||
if (name.empty())
|
||||
return state == PLAYING;
|
||||
else
|
||||
return state == PLAYING && animationMap[name] == animationIndex;
|
||||
}
|
||||
|
||||
int Actor::animation_index_get(const std::string& name)
|
||||
{
|
||||
if (animationMap.contains(name)) return animationMap[name];
|
||||
return -1;
|
||||
}
|
||||
|
||||
Anm2::Item* Actor::item_get(Anm2::Type type, int id, int animationIndex)
|
||||
{
|
||||
if (animationIndex == -1) animationIndex = this->animationIndex;
|
||||
if (auto animation = animation_get(animationIndex))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Anm2::ROOT:
|
||||
return &animation->rootAnimation;
|
||||
break;
|
||||
case Anm2::LAYER:
|
||||
return unordered_map::find(animation->layerAnimations, id);
|
||||
case Anm2::NULL_:
|
||||
return map::find(animation->nullAnimations, id);
|
||||
break;
|
||||
case Anm2::TRIGGER:
|
||||
return &animation->triggers;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Actor::item_length(Anm2::Item* item)
|
||||
{
|
||||
if (!item) return -1;
|
||||
|
||||
int duration{};
|
||||
for (auto& frame : item->frames)
|
||||
duration += frame.duration;
|
||||
return duration;
|
||||
}
|
||||
|
||||
Anm2::Frame Actor::frame_generate(Anm2::Item& item, float time, Anm2::Type type, int id)
|
||||
{
|
||||
Anm2::Frame frame{};
|
||||
frame.isVisible = false;
|
||||
|
||||
if (item.frames.empty()) return frame;
|
||||
|
||||
time = time < 0.0f ? 0.0f : time;
|
||||
|
||||
Anm2::Frame* frameNext = nullptr;
|
||||
Anm2::Frame frameNextCopy{};
|
||||
int durationCurrent = 0;
|
||||
int durationNext = 0;
|
||||
|
||||
for (int i = 0; i < (int)item.frames.size(); i++)
|
||||
{
|
||||
Anm2::Frame& checkFrame = item.frames[i];
|
||||
|
||||
frame = checkFrame;
|
||||
|
||||
durationNext += frame.duration;
|
||||
|
||||
if (time >= durationCurrent && time < durationNext)
|
||||
{
|
||||
if (i + 1 < (int)item.frames.size())
|
||||
{
|
||||
frameNext = &item.frames[i + 1];
|
||||
frameNextCopy = *frameNext;
|
||||
}
|
||||
else
|
||||
frameNext = nullptr;
|
||||
break;
|
||||
}
|
||||
|
||||
durationCurrent += frame.duration;
|
||||
}
|
||||
|
||||
auto override_handle = [&](Anm2::Frame& overrideFrame)
|
||||
{
|
||||
for (auto& override : overrides)
|
||||
{
|
||||
if (override.type != type) continue;
|
||||
if (override.id != id) continue;
|
||||
|
||||
auto& source = override.frame;
|
||||
|
||||
switch (override.mode)
|
||||
{
|
||||
case Override::SET:
|
||||
if (source.position.has_value()) overrideFrame.position = *source.position;
|
||||
if (source.pivot.has_value()) overrideFrame.pivot = *source.pivot;
|
||||
if (source.size.has_value()) overrideFrame.size = *source.size;
|
||||
if (source.scale.has_value()) overrideFrame.scale = *source.scale;
|
||||
if (source.crop.has_value()) overrideFrame.crop = *source.crop;
|
||||
if (source.rotation.has_value()) overrideFrame.rotation = *source.rotation;
|
||||
if (source.tint.has_value()) overrideFrame.tint = *source.tint;
|
||||
if (source.colorOffset.has_value()) overrideFrame.colorOffset = *source.colorOffset;
|
||||
if (source.isInterpolated.has_value()) overrideFrame.isInterpolated = *source.isInterpolated;
|
||||
if (source.isVisible.has_value()) overrideFrame.isVisible = *source.isVisible;
|
||||
break;
|
||||
case Override::ADD:
|
||||
if (source.scale.has_value()) overrideFrame.scale += *source.scale;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
override_handle(frame);
|
||||
if (frameNext) override_handle(frameNextCopy);
|
||||
|
||||
if (frame.isInterpolated && frameNext && frame.duration > 1)
|
||||
{
|
||||
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
|
||||
|
||||
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
|
||||
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
|
||||
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
|
||||
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
|
||||
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
|
||||
{
|
||||
if (!vector::in_bounds(animations, index)) return;
|
||||
if (mode != PLAY_FORCE && index == animationIndex) return;
|
||||
|
||||
this->playedEventID = -1;
|
||||
this->playedTriggers.clear();
|
||||
|
||||
this->speedMultiplier = speedMultiplier;
|
||||
this->animationIndex = index;
|
||||
this->time = time;
|
||||
if (mode == PLAY) state = PLAYING;
|
||||
}
|
||||
|
||||
void Actor::queue_play(QueuedPlay newQueuedPlay) { queuedPlay = newQueuedPlay; }
|
||||
void Actor::queue_default_animation() { queue_play({defaultAnimation}); }
|
||||
|
||||
void Actor::play(const std::string& name, Mode mode, float time, float speedMultiplier)
|
||||
{
|
||||
if (animationMap.contains(name))
|
||||
play(animationMap.at(name), mode, time, speedMultiplier);
|
||||
else
|
||||
logger.error(std::string("Animation \"" + name + "\" does not exist! Unable to play!"));
|
||||
}
|
||||
|
||||
void Actor::play_default_animation(Mode mode, float time, float speedMultiplier)
|
||||
{
|
||||
play(defaultAnimationID, mode, time, speedMultiplier);
|
||||
}
|
||||
|
||||
void Actor::tick()
|
||||
{
|
||||
if (state == Actor::STOPPED)
|
||||
{
|
||||
if (!nextQueuedPlay.empty())
|
||||
{
|
||||
queuedPlay = nextQueuedPlay;
|
||||
queuedPlay.isPlayAfterAnimation = false;
|
||||
nextQueuedPlay = QueuedPlay{};
|
||||
}
|
||||
currentQueuedPlay = QueuedPlay{};
|
||||
}
|
||||
|
||||
if (auto animation = animation_get(); animation && animation->isLoop) currentQueuedPlay = QueuedPlay{};
|
||||
|
||||
if (!queuedPlay.empty())
|
||||
{
|
||||
auto& index = animationMap.at(queuedPlay.animation);
|
||||
if (queuedPlay.isPlayAfterAnimation)
|
||||
nextQueuedPlay = queuedPlay;
|
||||
else if (index != animationIndex && currentQueuedPlay.isInterruptible)
|
||||
{
|
||||
play(queuedPlay.animation, queuedPlay.mode, queuedPlay.time, queuedPlay.speedMultiplier);
|
||||
currentQueuedPlay = queuedPlay;
|
||||
}
|
||||
queuedPlay = QueuedPlay{};
|
||||
}
|
||||
|
||||
auto animation = animation_get();
|
||||
if (!animation || animation->frameNum == 1 || mode == SET || state == STOPPED) return;
|
||||
|
||||
playedEventID = -1;
|
||||
|
||||
for (auto& trigger : animation->triggers.frames)
|
||||
{
|
||||
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
|
||||
{
|
||||
auto id = trigger.soundIDs[(int)math::random_max(trigger.soundIDs.size())];
|
||||
if (auto sound = map::find(sounds, id)) sound->audio.play();
|
||||
playedTriggers.insert((int)trigger.atFrame);
|
||||
playedEventID = trigger.eventID;
|
||||
}
|
||||
}
|
||||
|
||||
auto increment = (fps / TICK_RATE) * speedMultiplier;
|
||||
time += increment;
|
||||
|
||||
if (time >= animation->frameNum)
|
||||
{
|
||||
if (animation->isLoop)
|
||||
time = 0.0f;
|
||||
else
|
||||
state = STOPPED;
|
||||
|
||||
playedTriggers.clear();
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int)overrides.size(); i++)
|
||||
{
|
||||
auto& override_ = overrides[i];
|
||||
|
||||
if (override_.function) override_.function(override_);
|
||||
|
||||
if (override_.time.has_value())
|
||||
{
|
||||
*override_.time -= 1.0f;
|
||||
if (*override_.time <= 0.0f) overrides.erase(overrides.begin() + i++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec4 Actor::null_frame_rect(int nullID)
|
||||
{
|
||||
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
||||
|
||||
if (nullID == -1) return glm::vec4(NAN);
|
||||
auto item = item_get(Anm2::NULL_, nullID);
|
||||
if (!item) return glm::vec4(NAN);
|
||||
|
||||
auto animation = animation_get();
|
||||
if (!animation) return glm::vec4(NAN);
|
||||
|
||||
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
|
||||
|
||||
auto frame = frame_generate(*item, time, Anm2::NULL_, nullID);
|
||||
if (!frame.isVisible) return glm::vec4(NAN);
|
||||
|
||||
auto rootModel =
|
||||
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
|
||||
auto frameModel = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
|
||||
auto model = rootModel * frameModel;
|
||||
|
||||
float minX = std::numeric_limits<float>::infinity();
|
||||
float minY = std::numeric_limits<float>::infinity();
|
||||
float maxX = -std::numeric_limits<float>::infinity();
|
||||
float maxY = -std::numeric_limits<float>::infinity();
|
||||
|
||||
for (auto& corner : CORNERS)
|
||||
{
|
||||
vec4 world = model * vec4(corner, 0.0f, 1.0f);
|
||||
minX = std::min(minX, world.x);
|
||||
minY = std::min(minY, world.y);
|
||||
maxX = std::max(maxX, world.x);
|
||||
maxY = std::max(maxY, world.y);
|
||||
}
|
||||
|
||||
return glm::vec4(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
void Actor::render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas& canvas)
|
||||
{
|
||||
auto animation = animation_get();
|
||||
if (!animation) return;
|
||||
|
||||
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
|
||||
|
||||
auto rootModel =
|
||||
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
|
||||
|
||||
for (auto& i : animation->layerOrder)
|
||||
{
|
||||
auto& layerAnimation = animation->layerAnimations[i];
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto layer = map::find(layers, i);
|
||||
if (!layer) continue;
|
||||
|
||||
auto spritesheet = map::find(spritesheets, layer->spritesheetID);
|
||||
if (!spritesheet) continue;
|
||||
|
||||
auto frame = frame_generate(layerAnimation, time, Anm2::LAYER, i);
|
||||
if (!frame.isVisible) continue;
|
||||
|
||||
auto model =
|
||||
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
|
||||
model = rootModel * model;
|
||||
|
||||
auto& texture = spritesheet->texture;
|
||||
if (!texture.is_valid()) return;
|
||||
|
||||
auto tint = frame.tint * root.tint;
|
||||
auto colorOffset = frame.colorOffset + root.colorOffset;
|
||||
|
||||
auto inset = vec2(0);
|
||||
auto uvMin = (frame.crop + inset) / vec2(texture.size);
|
||||
auto uvMax = (frame.crop + frame.size - inset) / vec2(texture.size);
|
||||
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
|
||||
|
||||
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
|
||||
}
|
||||
|
||||
if (isShowNulls)
|
||||
{
|
||||
for (int i = 0; i < (int)animation->nullAnimations.size(); i++)
|
||||
{
|
||||
auto& nullAnimation = animation->nullAnimations[i];
|
||||
if (!nullAnimation.isVisible) continue;
|
||||
|
||||
auto frame = frame_generate(nullAnimation, time, Anm2::NULL_, i);
|
||||
if (!frame.isVisible) continue;
|
||||
|
||||
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
|
||||
model = rootModel * model;
|
||||
|
||||
canvas.rect_render(rectShader, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec4 Actor::rect()
|
||||
{
|
||||
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
||||
|
||||
auto animation = animation_get();
|
||||
|
||||
float minX = std::numeric_limits<float>::infinity();
|
||||
float minY = std::numeric_limits<float>::infinity();
|
||||
float maxX = -std::numeric_limits<float>::infinity();
|
||||
float maxY = -std::numeric_limits<float>::infinity();
|
||||
bool any = false;
|
||||
|
||||
if (!animation) return vec4(-NAN);
|
||||
|
||||
for (float t = 0.0f; t < (float)animation->frameNum; t += 1.0f)
|
||||
{
|
||||
mat4 transform(1.0f);
|
||||
|
||||
auto root = frame_generate(animation->rootAnimation, t, Anm2::ROOT);
|
||||
transform *=
|
||||
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
|
||||
|
||||
for (auto& [id, layerAnimation] : animation->layerAnimations)
|
||||
{
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto frame = frame_generate(layerAnimation, t, Anm2::LAYER, id);
|
||||
|
||||
if (frame.size == vec2() || !frame.isVisible) continue;
|
||||
|
||||
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
|
||||
math::to_unit(frame.scale), frame.rotation);
|
||||
for (auto& corner : CORNERS)
|
||||
{
|
||||
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
|
||||
minX = std::min(minX, world.x);
|
||||
minY = std::min(minY, world.y);
|
||||
maxX = std::max(maxX, world.x);
|
||||
maxY = std::max(maxY, world.y);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!any) return vec4(-NAN);
|
||||
return {minX, minY, maxX - minX, maxY - minY};
|
||||
}
|
||||
|
||||
bool Actor::is_animation_finished()
|
||||
{
|
||||
if (auto animation = animation_get())
|
||||
{
|
||||
if (animation->isLoop) return true;
|
||||
if (time > animation->frameNum) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Actor::consume_played_event() { playedEventID = -1; }
|
||||
};
|
||||
113
src/entity/actor.hpp
Normal file
113
src/entity/actor.hpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include "../canvas.hpp"
|
||||
#include "../resource/xml/anm2.hpp"
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
class Actor : public resource::xml::Anm2
|
||||
{
|
||||
public:
|
||||
static constexpr auto TICK_RATE = 30.0f;
|
||||
|
||||
enum Mode
|
||||
{
|
||||
PLAY,
|
||||
PLAY_FORCE,
|
||||
SET,
|
||||
};
|
||||
|
||||
enum State
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING
|
||||
};
|
||||
|
||||
class Override
|
||||
{
|
||||
private:
|
||||
public:
|
||||
enum Mode
|
||||
{
|
||||
SET,
|
||||
ADD
|
||||
};
|
||||
|
||||
using Function = void (*)(Override&);
|
||||
|
||||
int id{-1};
|
||||
Anm2::Type type{Anm2::NONE};
|
||||
Mode mode{SET};
|
||||
FrameOptional frame{};
|
||||
std::optional<float> time{};
|
||||
Function function{nullptr};
|
||||
|
||||
FrameOptional frameBase{};
|
||||
std::optional<float> timeStart{};
|
||||
|
||||
float cycles{};
|
||||
|
||||
Override() = default;
|
||||
Override(int, Anm2::Type, Mode, FrameOptional = {}, std::optional<float> = std::nullopt, Function = nullptr,
|
||||
float = 0);
|
||||
};
|
||||
|
||||
struct QueuedPlay
|
||||
{
|
||||
std::string animation{};
|
||||
float time{};
|
||||
float speedMultiplier{1.0f};
|
||||
Mode mode{PLAY};
|
||||
bool isInterruptible{true};
|
||||
bool isPlayAfterAnimation{false};
|
||||
|
||||
inline bool empty() { return animation.empty(); };
|
||||
};
|
||||
|
||||
State state{STOPPED};
|
||||
Mode mode{PLAY};
|
||||
|
||||
glm::vec2 position{};
|
||||
float time{};
|
||||
bool isShowNulls{};
|
||||
int animationIndex{-1};
|
||||
int playedEventID{-1};
|
||||
float startTime{};
|
||||
float speedMultiplier{};
|
||||
|
||||
QueuedPlay queuedPlay{};
|
||||
QueuedPlay currentQueuedPlay{};
|
||||
QueuedPlay nextQueuedPlay{};
|
||||
|
||||
std::unordered_set<int> playedTriggers{};
|
||||
std::vector<Override> overrides{};
|
||||
|
||||
Actor() = default;
|
||||
Actor(const Actor&);
|
||||
Actor(Actor&&) noexcept;
|
||||
Actor& operator=(const Actor&);
|
||||
Actor& operator=(Actor&&) noexcept;
|
||||
Actor(resource::xml::Anm2, glm::vec2 position = {}, Mode = PLAY, float time = 0.0f, int animationIndex = -1);
|
||||
bool is_playing(const std::string& name = {});
|
||||
glm::vec4 null_frame_rect(int = -1);
|
||||
glm::vec4 rect();
|
||||
int animation_index_get(const std::string&);
|
||||
int item_length(resource::xml::Anm2::Item*);
|
||||
resource::xml::Anm2::Animation* animation_get(const std::string&);
|
||||
resource::xml::Anm2::Animation* animation_get(int = -1);
|
||||
resource::xml::Anm2::Frame frame_generate(resource::xml::Anm2::Item&, float, resource::xml::Anm2::Type,
|
||||
int id = -1);
|
||||
resource::xml::Anm2::Item* item_get(resource::xml::Anm2::Type, int = -1, int = -1);
|
||||
void consume_played_event();
|
||||
void play(const std::string& animation, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
|
||||
void play(int index, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
|
||||
void play_default_animation(Mode = PLAY, float = 0.0f, float = 1.0f);
|
||||
void queue_default_animation();
|
||||
void queue_play(QueuedPlay);
|
||||
bool is_animation_finished();
|
||||
void render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas&);
|
||||
void tick();
|
||||
};
|
||||
}
|
||||
372
src/entity/character.cpp
Normal file
372
src/entity/character.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include "character.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "../util/math.hpp"
|
||||
#include "../util/vector.hpp"
|
||||
|
||||
using namespace game::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
Character::Character(const Character&) = default;
|
||||
Character::Character(Character&&) noexcept = default;
|
||||
Character& Character::operator=(const Character&) = default;
|
||||
Character& Character::operator=(Character&&) noexcept = default;
|
||||
|
||||
Character::Character(resource::xml::Character& _data, glm::ivec2 _position) : Actor(_data.anm2, _position)
|
||||
{
|
||||
data = _data;
|
||||
|
||||
auto& save = data.save;
|
||||
auto saveIsValid = save.is_valid();
|
||||
|
||||
capacity = saveIsValid ? save.capacity : data.capacity;
|
||||
weight = saveIsValid ? save.weight : data.weight;
|
||||
digestionRate = saveIsValid ? save.digestionRate : data.digestionRate;
|
||||
eatSpeed = saveIsValid ? save.eatSpeed : data.eatSpeed;
|
||||
|
||||
calories = saveIsValid ? save.calories : 0;
|
||||
|
||||
isDigesting = saveIsValid ? save.isDigesting : false;
|
||||
digestionProgress = saveIsValid ? save.digestionProgress : 0;
|
||||
digestionTimer = saveIsValid ? save.digestionTimer : 0;
|
||||
|
||||
auto& talkSource = data.talkOverride.layerSource;
|
||||
auto& talkDestination = data.talkOverride.layerDestination;
|
||||
talkOverrideID = vector::push_index(overrides, Actor::Override(talkDestination, Anm2::LAYER, Override::SET));
|
||||
for (auto& animation : animations)
|
||||
{
|
||||
if (!animation.layerAnimations.contains(talkSource))
|
||||
animationTalkDurations.emplace_back(-1);
|
||||
else
|
||||
animationTalkDurations.emplace_back(item_length(&animation.layerAnimations.at(talkSource)));
|
||||
}
|
||||
|
||||
auto& blinkSource = data.blinkOverride.layerSource;
|
||||
auto& blinkDestination = data.blinkOverride.layerDestination;
|
||||
blinkOverrideID = vector::push_index(overrides, Actor::Override(blinkDestination, Anm2::LAYER, Override::SET));
|
||||
for (auto& animation : animations)
|
||||
{
|
||||
if (!animation.layerAnimations.contains(blinkSource))
|
||||
animationBlinkDurations.emplace_back(-1);
|
||||
else
|
||||
animationBlinkDurations.emplace_back(item_length(&animation.layerAnimations.at(blinkSource)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int)data.expandAreas.size(); i++)
|
||||
{
|
||||
auto& expandArea = data.expandAreas[i];
|
||||
expandAreaOverrideLayerIDs[i] =
|
||||
vector::push_index(overrides, Actor::Override(expandArea.layerID, Anm2::LAYER, Override::ADD));
|
||||
expandAreaOverrideNullIDs[i] =
|
||||
vector::push_index(overrides, Actor::Override(expandArea.nullID, Anm2::NULL_, Override::ADD));
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int)data.interactAreas.size(); i++)
|
||||
{
|
||||
auto& interactArea = data.interactAreas[i];
|
||||
if (interactArea.layerID != -1)
|
||||
interactAreaOverrides[i] = Actor::Override(interactArea.layerID, Anm2::LAYER, Override::ADD);
|
||||
}
|
||||
|
||||
stage = stage_get();
|
||||
expand_areas_apply();
|
||||
}
|
||||
|
||||
float Character::weight_get(measurement::System system)
|
||||
{
|
||||
return system == measurement::IMPERIAL ? weight * measurement::KG_TO_LB : weight;
|
||||
}
|
||||
|
||||
int Character::stage_from_weight_get(float checkWeight) const
|
||||
{
|
||||
if (data.stages.empty()) return 0;
|
||||
if (checkWeight <= data.weight) return 0;
|
||||
|
||||
for (int i = 0; i < (int)data.stages.size(); i++)
|
||||
if (checkWeight < data.stages[i].threshold) return i;
|
||||
|
||||
return stage_max_get();
|
||||
}
|
||||
|
||||
int Character::stage_get() const { return stage_from_weight_get(weight); }
|
||||
|
||||
int Character::stage_max_get() const { return data.stages.size(); }
|
||||
|
||||
float Character::stage_threshold_get(int stage, measurement::System system) const
|
||||
{
|
||||
if (stage == -1) stage = this->stage;
|
||||
|
||||
float threshold = data.weight;
|
||||
|
||||
if (!data.stages.empty())
|
||||
{
|
||||
if (stage <= 0)
|
||||
threshold = data.weight;
|
||||
else if (stage >= stage_max_get())
|
||||
threshold = data.stages.back().threshold;
|
||||
else
|
||||
threshold = data.stages[stage - 1].threshold;
|
||||
}
|
||||
|
||||
return system == measurement::IMPERIAL ? threshold * measurement::KG_TO_LB : threshold;
|
||||
}
|
||||
|
||||
float Character::stage_threshold_next_get(measurement::System system) const
|
||||
{
|
||||
return stage_threshold_get(stage + 1, system);
|
||||
}
|
||||
|
||||
float Character::stage_progress_get()
|
||||
{
|
||||
auto currentStage = stage_get();
|
||||
if (currentStage >= stage_max_get()) return 1.0f;
|
||||
|
||||
auto currentThreshold = stage_threshold_get(currentStage);
|
||||
auto nextThreshold = stage_threshold_get(currentStage + 1);
|
||||
if (nextThreshold <= currentThreshold) return 1.0f;
|
||||
|
||||
return (weight - currentThreshold) / (nextThreshold - currentThreshold);
|
||||
}
|
||||
|
||||
float Character::digestion_rate_get() { return digestionRate * 60; }
|
||||
|
||||
float Character::max_capacity() const { return capacity * data.capacityMaxMultiplier; }
|
||||
bool Character::is_over_capacity() const { return calories > capacity; }
|
||||
bool Character::is_max_capacity() const { return calories >= max_capacity(); }
|
||||
float Character::capacity_percent_get() const { return calories / max_capacity(); }
|
||||
|
||||
std::string Character::animation_name_convert(const std::string& name) { return std::format("{}{}", name, stage); }
|
||||
void Character::play_convert(const std::string& animation, Mode mode, float time, float speedMultiplier)
|
||||
{
|
||||
play(animation_name_convert(animation), mode, time, speedMultiplier);
|
||||
}
|
||||
|
||||
void Character::expand_areas_apply()
|
||||
{
|
||||
auto stageProgress = stage_progress_get();
|
||||
auto capacityProgress = isDigesting
|
||||
? (float)calories / max_capacity() * (float)digestionTimer / data.digestionTimerMax
|
||||
: calories / max_capacity();
|
||||
|
||||
for (int i = 0; i < (int)data.expandAreas.size(); i++)
|
||||
{
|
||||
auto& expandArea = data.expandAreas[i];
|
||||
auto& overrideLayer = overrides[expandAreaOverrideLayerIDs[i]];
|
||||
auto& overrideNull = overrides[expandAreaOverrideNullIDs[i]];
|
||||
|
||||
auto stageScaleAdd = ((expandArea.scaleAdd * stageProgress) * 0.5f);
|
||||
auto capacityScaleAdd = ((expandArea.scaleAdd * capacityProgress) * 0.5f);
|
||||
|
||||
auto scaleAdd =
|
||||
glm::clamp(glm::vec2(), glm::vec2(stageScaleAdd + capacityScaleAdd), glm::vec2(expandArea.scaleAdd));
|
||||
overrideLayer.frame.scale = scaleAdd;
|
||||
overrideNull.frame.scale = scaleAdd;
|
||||
}
|
||||
}
|
||||
|
||||
void Character::update()
|
||||
{
|
||||
isJustStageUp = false;
|
||||
isJustStageFinal = false;
|
||||
isJustDigested = false;
|
||||
}
|
||||
|
||||
void Character::tick()
|
||||
{
|
||||
if (state == Actor::STOPPED)
|
||||
{
|
||||
if (isStageUp)
|
||||
{
|
||||
if (stage >= (int)data.stages.size())
|
||||
isJustStageFinal = true;
|
||||
else
|
||||
isJustStageUp = true;
|
||||
|
||||
isStageUp = false;
|
||||
}
|
||||
|
||||
if (nextQueuedPlay.empty()) queue_idle_animation();
|
||||
}
|
||||
|
||||
Actor::tick();
|
||||
|
||||
if (isDigesting)
|
||||
{
|
||||
digestionTimer--;
|
||||
|
||||
if (digestionTimer <= 0)
|
||||
{
|
||||
auto increment = calories * data.caloriesToKilogram;
|
||||
|
||||
if (is_over_capacity())
|
||||
{
|
||||
auto capacityMaxCalorieDifference = (calories - capacity);
|
||||
auto overCapacityPercent = capacityMaxCalorieDifference / (max_capacity() - capacity);
|
||||
auto capacityIncrement =
|
||||
(overCapacityPercent * data.capacityIfOverStuffedOnDigestBonus) * capacityMaxCalorieDifference;
|
||||
capacity = glm::clamp(data.capacityMin, capacity + capacityIncrement, data.capacityMax);
|
||||
}
|
||||
|
||||
totalCaloriesConsumed += calories;
|
||||
calories = 0;
|
||||
|
||||
if (auto nextStage = stage_from_weight_get(weight + increment); nextStage > stage_from_weight_get(weight))
|
||||
{
|
||||
queuedPlay = QueuedPlay{};
|
||||
nextQueuedPlay = QueuedPlay{};
|
||||
currentQueuedPlay = QueuedPlay{};
|
||||
play_convert(data.animations.stageUp);
|
||||
stage = nextStage;
|
||||
isStageUp = true;
|
||||
}
|
||||
else
|
||||
isJustDigested = true;
|
||||
|
||||
weight = glm::clamp(data.weightMin, weight + increment, data.weightMax);
|
||||
|
||||
isDigesting = false;
|
||||
digestionTimer = data.digestionTimerMax;
|
||||
digestionProgress = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (calories > 0) digestionProgress += digestionRate;
|
||||
if (digestionProgress >= DIGESTION_MAX)
|
||||
{
|
||||
isDigesting = true;
|
||||
digestionTimer = data.digestionTimerMax;
|
||||
data.sounds.digest.play();
|
||||
}
|
||||
}
|
||||
|
||||
if (math::random_percent_roll(
|
||||
math::to_percent(data.gurgleChance * (capacity_percent_get() * data.gurgleCapacityMultiplier))))
|
||||
data.sounds.gurgle.play();
|
||||
|
||||
stage = stage_get();
|
||||
expand_areas_apply();
|
||||
|
||||
auto& talkOverride = overrides[talkOverrideID];
|
||||
|
||||
if (isTalking)
|
||||
{
|
||||
auto talk_reset = [&]()
|
||||
{
|
||||
isTalking = false;
|
||||
talkTimer = 0.0f;
|
||||
talkOverride.frame = FrameOptional();
|
||||
};
|
||||
|
||||
auto& id = data.talkOverride.layerSource;
|
||||
auto& layerAnimations = animation_get()->layerAnimations;
|
||||
|
||||
if (layerAnimations.contains(id) && animationTalkDurations.at(animationIndex) > -1)
|
||||
{
|
||||
auto& layerAnimation = layerAnimations.at(data.talkOverride.layerSource);
|
||||
|
||||
if (!layerAnimation.frames.empty())
|
||||
{
|
||||
auto frame = frame_generate(layerAnimation, talkTimer, Anm2::LAYER, id);
|
||||
|
||||
talkOverride.frame.crop = frame.crop;
|
||||
talkOverride.frame.size = frame.size;
|
||||
talkOverride.frame.pivot = frame.pivot;
|
||||
|
||||
talkTimer += 1.0f;
|
||||
|
||||
if (talkTimer > animationTalkDurations.at(animationIndex)) talkTimer = 0.0f;
|
||||
}
|
||||
else
|
||||
talk_reset();
|
||||
}
|
||||
else
|
||||
talk_reset();
|
||||
}
|
||||
else
|
||||
talkOverride.frame = {};
|
||||
|
||||
auto& blinkOverride = overrides[blinkOverrideID];
|
||||
|
||||
if (auto blinkDuration = animationBlinkDurations[animationIndex]; blinkDuration != 1)
|
||||
{
|
||||
if (math::random_percent_roll(data.blinkChance)) isBlinking = true;
|
||||
|
||||
if (isBlinking)
|
||||
{
|
||||
auto blink_reset = [&]()
|
||||
{
|
||||
isBlinking = false;
|
||||
blinkTimer = 0.0f;
|
||||
blinkOverride.frame = FrameOptional();
|
||||
};
|
||||
|
||||
auto& id = data.blinkOverride.layerSource;
|
||||
auto& layerAnimations = animation_get()->layerAnimations;
|
||||
|
||||
if (layerAnimations.contains(id))
|
||||
{
|
||||
auto& layerAnimation = layerAnimations.at(data.blinkOverride.layerSource);
|
||||
|
||||
if (!layerAnimation.frames.empty())
|
||||
{
|
||||
auto frame = frame_generate(layerAnimation, blinkTimer, Anm2::LAYER, id);
|
||||
|
||||
blinkOverride.frame.crop = frame.crop;
|
||||
blinkOverride.frame.size = frame.size;
|
||||
blinkOverride.frame.pivot = frame.pivot;
|
||||
|
||||
blinkTimer += 1.0f;
|
||||
|
||||
if (blinkTimer >= blinkDuration) blink_reset();
|
||||
}
|
||||
else
|
||||
blink_reset();
|
||||
}
|
||||
else
|
||||
blink_reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Character::queue_play(QueuedPlay play)
|
||||
{
|
||||
queuedPlay = play;
|
||||
queuedPlay.animation = animation_name_convert(queuedPlay.animation);
|
||||
}
|
||||
|
||||
void Character::queue_idle_animation()
|
||||
{
|
||||
if (data.animations.idle.empty()) return;
|
||||
queue_play(
|
||||
{is_over_capacity() && !data.animations.idleFull.empty() ? data.animations.idleFull : data.animations.idle});
|
||||
}
|
||||
|
||||
void Character::queue_interact_area_animation(resource::xml::Character::InteractArea& interactArea)
|
||||
{
|
||||
if (interactArea.animation.empty()) return;
|
||||
queue_play({is_over_capacity() && !interactArea.animationFull.empty() ? interactArea.animationFull
|
||||
: interactArea.animation});
|
||||
}
|
||||
|
||||
void Character::spritesheet_set(SpritesheetType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NORMAL:
|
||||
spritesheets.at(data.alternateSpritesheet.id).texture =
|
||||
data.anm2.spritesheets.at(data.alternateSpritesheet.id).texture;
|
||||
break;
|
||||
case ALTERNATE:
|
||||
spritesheets.at(data.alternateSpritesheet.id).texture = data.alternateSpritesheet.texture;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
spritesheetType = type;
|
||||
}
|
||||
}
|
||||
92
src/entity/character.hpp
Normal file
92
src/entity/character.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "../resource/xml/character.hpp"
|
||||
#include "../util/measurement.hpp"
|
||||
|
||||
#include "actor.hpp"
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
class Character : public Actor
|
||||
{
|
||||
public:
|
||||
static constexpr auto DIGESTION_MAX = 100.0f;
|
||||
|
||||
enum SpritesheetType
|
||||
{
|
||||
NORMAL,
|
||||
ALTERNATE
|
||||
};
|
||||
|
||||
resource::xml::Character data;
|
||||
|
||||
float weight{};
|
||||
int stage{0};
|
||||
float calories{};
|
||||
float capacity{};
|
||||
|
||||
float digestionProgress{};
|
||||
float digestionRate{0.05f};
|
||||
int digestionTimer{};
|
||||
bool isDigesting{};
|
||||
bool isJustDigested{};
|
||||
|
||||
float eatSpeed{1.0f};
|
||||
|
||||
float totalCaloriesConsumed{};
|
||||
int totalFoodItemsEaten{};
|
||||
|
||||
int talkOverrideID{};
|
||||
float talkTimer{};
|
||||
bool isTalking{};
|
||||
std::vector<int> animationTalkDurations{};
|
||||
|
||||
int blinkOverrideID{};
|
||||
float blinkTimer{};
|
||||
bool isBlinking{};
|
||||
std::vector<int> animationBlinkDurations{};
|
||||
|
||||
bool isStageUp{};
|
||||
bool isJustStageUp{};
|
||||
bool isJustStageFinal{};
|
||||
|
||||
SpritesheetType spritesheetType{};
|
||||
|
||||
std::map<int, Override> interactAreaOverrides{};
|
||||
|
||||
std::unordered_map<int, int> expandAreaOverrideLayerIDs{};
|
||||
std::unordered_map<int, int> expandAreaOverrideNullIDs{};
|
||||
|
||||
Character() = default;
|
||||
Character(const Character&);
|
||||
Character(Character&&) noexcept;
|
||||
Character& operator=(const Character&);
|
||||
Character& operator=(Character&&) noexcept;
|
||||
Character(resource::xml::Character&, glm::ivec2);
|
||||
|
||||
float weight_get(util::measurement::System = util::measurement::METRIC);
|
||||
float digestion_rate_get();
|
||||
float capacity_percent_get() const;
|
||||
float max_capacity() const;
|
||||
bool is_over_capacity() const;
|
||||
bool is_max_capacity() const;
|
||||
|
||||
int stage_get() const;
|
||||
int stage_from_weight_get(float weight) const;
|
||||
int stage_max_get() const;
|
||||
float stage_progress_get();
|
||||
float stage_threshold_get(int stage = -1, util::measurement::System = util::measurement::METRIC) const;
|
||||
float stage_threshold_next_get(util::measurement::System = util::measurement::METRIC) const;
|
||||
|
||||
void expand_areas_apply();
|
||||
void spritesheet_set(SpritesheetType);
|
||||
void update();
|
||||
void tick();
|
||||
void play_convert(const std::string&, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
|
||||
void queue_idle_animation();
|
||||
void queue_interact_area_animation(resource::xml::Character::InteractArea&);
|
||||
void queue_play(QueuedPlay);
|
||||
|
||||
std::string animation_name_convert(const std::string& name);
|
||||
};
|
||||
}
|
||||
22
src/entity/cursor.cpp
Normal file
22
src/entity/cursor.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "cursor.hpp"
|
||||
|
||||
#include "../util/imgui.hpp"
|
||||
|
||||
using namespace game::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
Cursor::Cursor(Anm2& anm2) : Actor(anm2, imgui::to_vec2(ImGui::GetMousePos())) {}
|
||||
void Cursor::tick()
|
||||
{
|
||||
Actor::tick();
|
||||
queue_default_animation();
|
||||
}
|
||||
|
||||
void Cursor::update()
|
||||
{
|
||||
state = DEFAULT;
|
||||
position = imgui::to_vec2(ImGui::GetMousePos());
|
||||
}
|
||||
}
|
||||
26
src/entity/cursor.hpp
Normal file
26
src/entity/cursor.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "../util/interact_type.hpp"
|
||||
#include "actor.hpp"
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
class Cursor : public Actor
|
||||
{
|
||||
public:
|
||||
enum State
|
||||
{
|
||||
DEFAULT,
|
||||
HOVER,
|
||||
ACTION
|
||||
};
|
||||
|
||||
State state{DEFAULT};
|
||||
InteractType mode{InteractType::RUB};
|
||||
|
||||
Cursor() = default;
|
||||
Cursor(resource::xml::Anm2&);
|
||||
void tick();
|
||||
void update();
|
||||
};
|
||||
}
|
||||
31
src/entity/item.cpp
Normal file
31
src/entity/item.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "item.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include "../util/vector.hpp"
|
||||
|
||||
using game::resource::xml::Anm2;
|
||||
using namespace game::util;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
Item::Item(Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _chewCount, int _animationIndex, glm::vec2 _velocity,
|
||||
float _rotation)
|
||||
: Actor(_anm2, _position, SET, 0.0f, _animationIndex), schemaID(_schemaID), chewCount(_chewCount),
|
||||
velocity(_velocity)
|
||||
{
|
||||
|
||||
rotationOverrideID =
|
||||
vector::push_index(overrides, Override(-1, Anm2::ROOT, Override::SET, Anm2::FrameOptional{.rotation = _rotation}));
|
||||
}
|
||||
|
||||
void Item::update()
|
||||
{
|
||||
auto& rotationOverride = overrides[rotationOverrideID];
|
||||
|
||||
position += velocity;
|
||||
|
||||
*rotationOverride.frame.rotation += angularVelocity;
|
||||
}
|
||||
}
|
||||
26
src/entity/item.hpp
Normal file
26
src/entity/item.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "../resource/xml/item.hpp"
|
||||
|
||||
#include "actor.hpp"
|
||||
|
||||
namespace game::entity
|
||||
{
|
||||
class Item : public Actor
|
||||
{
|
||||
public:
|
||||
bool isToBeDeleted{};
|
||||
bool isHeld{};
|
||||
|
||||
int schemaID{};
|
||||
int rotationOverrideID{};
|
||||
int chewCount{};
|
||||
|
||||
glm::vec2 velocity{};
|
||||
float angularVelocity{};
|
||||
|
||||
Item(resource::xml::Anm2, glm::ivec2 position, int id, int chewCount = 0, int animationIndex = -1,
|
||||
glm::vec2 velocity = {}, float rotation = 0.0f);
|
||||
void update();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user