cakefoot/src/Enemy.cpp
Cocktail Frank 47860d9696 Track per session play data
Use new upstream play session data feature and HTTP library to send
play session data to remote server for logging.

Add new stats for tracking play time and games started.

Fix BGM button so BGM object is stopped when turned off and played when
turned on.

Add platform identifier to per platform config files.

Move upstream math functions into sb::math.

Add upstream SHA256 library to builds.
2025-04-16 22:42:54 -04:00

596 lines
18 KiB
C++

#include "Enemy.hpp"
void Enemy::reset()
{
if (!coin_collected)
{
_coin_taken = false;
}
}
bool Enemy::collide(const sb::Box& box, [[maybe_unused]] const sb::Sprite& sprite, const glm::vec3& clip_lower,
const glm::vec3& clip_upper) const
{
sb::Box wrapped = this->box;
wrapped.center(sb::math::wrap_point({this->box.center(), 0.0f}, clip_lower, clip_upper));
sb::Box wrapped_other = box;
wrapped_other.center(sb::math::wrap_point({box.center(), 0.0f}, clip_lower, clip_upper));
return wrapped.collide(wrapped_other);
}
bool Enemy::collide_coin([[maybe_unused]] sb::Box box, [[maybe_unused]] const glm::vec3& clip_lower,
[[maybe_unused]] const glm::vec3& clip_upper) const
{
/* If there is no coin, return false immediately */
if (!_coin.has_value() || coin_taken() || coin_collected)
{
return false;
}
else
{
/* Returning true defers the full check to the derived class */
return true;
}
}
void Enemy::take_coin()
{
if (_coin.has_value())
{
_coin_taken = true;
}
}
bool Enemy::coin_taken() const
{
return _coin_taken;
}
void Enemy::collect_coin()
{
if (_coin.has_value())
{
coin_collected = true;
}
}
void Enemy::coin_translation(const glm::vec3& translation)
{
if (_coin.has_value())
{
_coin->translate({translation.x, translation.y, 0.0f});
}
}
void Enemy::draw(GLuint transformation_uniform, const glm::mat4& view, const glm::mat4& projection,
GLuint texture_flag_uniform, const sb::Color& rotating_hue, GLuint color_addition_uniform)
{
sprite.draw(transformation_uniform, view, projection, texture_flag_uniform);
if (_coin.has_value() && !coin_collected)
{
glUniform4fv(color_addition_uniform, 1, &rotating_hue.normal()[0]);
_coin->draw(transformation_uniform, view, projection, texture_flag_uniform);
}
}
Slicer::Slicer(const Curve& curve, float relative, float speed, float stray) :
curve(curve), relative(relative), speed(speed), stray(stray)
{
position = start();
sprite.texture("resource/slicer/slicer-1.png", GL_LINEAR);
sprite.texture("resource/slicer/slicer-2.png", GL_LINEAR);
sprite.frames.frame_length(0.5f);
sprite.frames.play();
/* Get size as the pixels divided by the original resolution */
float size = 12.0f / 486.0f;
/* Apply size to sprite and box objects */
sprite.scale(glm::vec2{size});
box.size(2.0f * glm::vec2{size});
};
void Slicer::coin(const sb::Sprite& sprite, float radius, float angle)
{
_coin = sprite;
coin_radius = radius;
coin_angle = angle;
}
void Slicer::update(const sb::Timer& timer)
{
sprite.update(timer);
float move = timer.delta(speed);
glm::vec3 destination;
/* Use a loop to move repeatedly between start and end when there is a long move, for example when there is major
* lag between frames. */
while (move > 0.0f)
{
if (toward_end)
{
destination = end();
}
else
{
destination = start();
}
float distance = glm::distance(position, destination);
if (distance < move)
{
position = destination;
move -= distance;
toward_end = !toward_end;
}
else
{
position += glm::vec3{sb::Segment(position, destination).step(move), 0.0f};
move = 0.0f;
}
}
/* Move hit box */
box.center(position);
/* Update challenge coin position */
if (_coin.has_value() && !coin_taken() && !coin_collected)
{
glm::vec3 coin_position = position + glm::vec3{sb::math::angle_to_vector(coin_angle, coin_radius), 0.0f};
_coin->translate(curve.get().wrap(coin_position));
}
/* Place sprite */
sprite.translate(curve.get().wrap(position));
}
glm::vec3 Slicer::center() const
{
return curve.get().relative(relative);
}
float Slicer::angle() const
{
int index = curve.get().index(relative);
int next_index = std::min(index + 1, curve.get().length() - 1);
int prev_index = std::max(index - 1, 0);
return sb::math::angle_between(curve.get()[prev_index], curve.get()[next_index]) + glm::half_pi<float>();
}
glm::vec3 Slicer::start() const
{
return center() + glm::vec3{sb::math::angle_to_vector(angle(), stray), 0.0f};
}
glm::vec3 Slicer::end() const
{
return center() + glm::vec3{sb::math::angle_to_vector(angle() + glm::pi<float>(), stray), 0.0f};
}
bool Slicer::collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
{
if (Enemy::collide_coin(box, clip_lower, clip_upper))
{
sb::Box coin_box = this->box;
coin_box.move(glm::vec3{sb::math::angle_to_vector(coin_angle, coin_radius), 0.0f});
coin_box.center(sb::math::wrap_point({coin_box.center(), 0.0f}, clip_lower, clip_upper));
box.center(sb::math::wrap_point({box.center(), 0.0f}, clip_lower, clip_upper));
return coin_box.collide(box);
}
return false;
}
Fish::Fish(const Curve& curve, float relative, float speed, float radius, float offset) :
curve(curve), relative(relative), speed(speed), radius(radius), offset(offset)
{
sprite.texture("resource/fish/fish-1.png", GL_LINEAR);
sprite.texture("resource/fish/fish-2.png", GL_LINEAR);
sprite.frames.frame_length(0.3f);
sprite.frames.play();
/* Set size of objects */
glm::vec2 size {12.0f / 486.0f};
sprite.scale(size);
box.size(2.0f * size);
};
void Fish::coin(const sb::Sprite& sprite, float angle)
{
_coin = sprite;
coin_angle = angle;
}
void Fish::coin(const sb::Sprite& sprite, float radius, float angle)
{
coin_radius = radius;
Fish::coin(sprite, angle);
}
void Fish::update(const sb::Timer& timer)
{
sprite.update(timer);
/* On the first update, record the timestamp of the timer, so the difference between subsequent timestamps and the
* first timestamp can be used to measure the angle position of the fish. Do this instead of adding to the angle
* position every frame to avoid letting floating point addition error accumulate differently among fish with
* different speeds. */
if (!first_update_time.has_value())
{
first_update_time = timer.elapsed();
}
/* The angle position is the seconds since the first update multiplied by radians per second plus the fish's given
* angle offset. */
angle = (timer.elapsed() - *first_update_time) * speed + offset;
/* Place the fish an amount away from its center using the angle and radius to get the vector. */
glm::vec3 position = center() + glm::vec3{sb::math::angle_to_vector(angle, radius), 0.0f};
/* Update challenge coin position */
if (_coin.has_value() && !coin_taken() && !coin_collected)
{
glm::vec3 coin_position;
/* Place coin either along the fish's circle or translated a vector away from the fish, depending on whether or
* not the radius is set. */
if (coin_radius == 0.0f)
{
coin_position = center() + glm::vec3{sb::math::angle_to_vector(angle + coin_angle, radius), 0.0f};
}
else
{
coin_position = position + glm::vec3{sb::math::angle_to_vector(coin_angle, coin_radius), 0.0f};
}
_coin->translate(curve.get().wrap(coin_position));
}
sprite.translate(curve.get().wrap(position));
box.center(position);
}
glm::vec3 Fish::center() const
{
return curve.get().relative(relative);
}
bool Fish::collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
{
if (Enemy::collide_coin(box, clip_lower, clip_upper))
{
sb::Box coin_box = this->box;
coin_box.center(
sb::math::wrap_point(
center() + glm::vec3{ sb::math::angle_to_vector(angle + coin_angle, radius), 0.0f },
clip_lower,
clip_upper
));
box.center(sb::math::wrap_point({box.center(), 0.0f}, clip_lower, clip_upper));
return coin_box.collide(box);
}
return false;
}
Projectile::Projectile(const glm::vec3& position, const glm::vec3& target, float speed) :
position(position), speed(speed)
{
/* Set angle once at construction so it will continue moving past the target once it reaches. */
angle = sb::math::angle_between(position, target);
/* Initialize sprite */
sprite.texture("resource/projectile/projectile-1.png", GL_LINEAR);
sprite.texture("resource/projectile/projectile-2.png", GL_LINEAR);
sprite.texture("resource/projectile/projectile-3.png", GL_LINEAR);
sprite.texture("resource/projectile/projectile-4.png", GL_LINEAR);
sprite.frames.frame_length(0.05f);
sprite.frames.play();
/* Set size of objects */
glm::vec2 size {12.0f / 486.0f};
sprite.scale(size);
box.size(2.0f * size);
/* Place objects at initial position */
sprite.translate(position);
box.center(position);
};
void Projectile::coinify(const sb::Sprite& sprite)
{
_coin = sprite;
}
void Projectile::update(const sb::Timer& timer)
{
sprite.update(timer);
/* Move */
position += glm::vec3{sb::math::angle_to_vector(angle, timer.delta(speed)), 0.0f};
sprite.translate(position);
/* Update challenge coin */
if (_coin.has_value())
{
_coin->translate(position);
}
box.center(position);
}
void Projectile::draw(GLuint transformation_uniform, const glm::mat4& view, const glm::mat4& projection, GLuint texture_flag_uniform,
const sb::Color& rotating_hue, GLuint color_addition_uniform)
{
if (_coin.has_value())
{
if (!coin_taken() && !coin_collected)
{
glUniform4fv(color_addition_uniform, 1, &rotating_hue.normal()[0]);
_coin->draw(transformation_uniform, view, projection, texture_flag_uniform);
glUniform4fv(color_addition_uniform, 1, &glm::vec4(0)[0]);
}
}
else
{
sprite.draw(transformation_uniform, view, projection, texture_flag_uniform);
}
}
bool Projectile::out_of_bounds() const
{
return position.x > 1.777f || position.x < -1.777f || position.y > 1.0f || position.y < -1.0f;
}
bool Projectile::coinified() const
{
return _coin.has_value();
}
Projector::Projector(const Character& character, const glm::vec3& position, float speed, float rate, float release_delay) :
character(character), position(position), speed(speed), release_delay(release_delay)
{
animation_charge.frame_length(rate);
sprite.texture("resource/projector/projector-1.png", GL_LINEAR);
sprite.texture("resource/projector/projector-2.png", GL_LINEAR);
/* Set size and position of objects */
glm::vec2 size {12.0f / 486.0f};
sprite.scale(size);
sprite.translate(position);
box.size(2.0f * size);
box.center(position);
/* Charge animation will always be playing */
animation_charge.play();
};
void Projector::reset()
{
Enemy::reset();
projectiles.clear();
animation_release.pause();
count = 0;
sprite.texture_index(0);
}
void Projector::coin(const sb::Sprite& sprite, int frequency)
{
_coin = sprite;
coin_frequency = frequency;
}
void Projector::update(const sb::Timer& timer)
{
sprite.update(timer);
/* Update the animation timers */
animation_charge.update(timer.stamp());
animation_release.update(timer.stamp());
/* Set play state based on unpaused timer state */
animation_charge.toggle(timer);
animation_release.toggle(timer);
/* Erase projectiles which have gone off screen */
projectiles.erase(
std::remove_if(
projectiles.begin(),
projectiles.end(),
[](const Projectile& projectile)
{
return projectile.out_of_bounds();
}),
projectiles.end());
/* Update projectiles */
for (Projectile& projectile : projectiles)
{
projectile.update(timer);
}
}
bool Projector::collide(
const sb::Box& box, const sb::Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
{
for (const Projectile& projectile : projectiles)
{
if (!projectile.coinified() && projectile.collide(box, sprite, clip_lower, clip_upper))
{
return true;
}
}
return false;
}
void Projector::take_coin()
{
Enemy::take_coin();
/* Erase coinified projectiles */
projectiles.erase(
std::remove_if(
projectiles.begin(),
projectiles.end(),
[](const Projectile& projectile)
{
return projectile.coinified();
}),
projectiles.end());
}
bool Projector::collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
{
if (Enemy::collide_coin(box, clip_lower, clip_upper))
{
for (const Projectile& projectile : projectiles)
{
if (projectile.coinified() && projectile.collide(box, sb::Sprite(), clip_lower, clip_upper))
{
return true;
}
}
}
return false;
}
void Projector::charge()
{
sprite.texture_index(1);
animation_release.play_once(release_delay);
}
void Projector::release()
{
sprite.texture_index(0);
/* Keep a count of how many projectiles have been fired */
count++;
/* Temporarily hard code the wrapping since the current curve is not currently accessible from this function. */
glm::vec3 point = sb::math::wrap_point(character.position, {-1.77777f, -1.0f, 0.0f}, {1.77777f, 1.0f, 1.0f});
projectiles.emplace_back(position, point, speed);
/* Fire a challenge coin every frequency time if frequency is non-zero and coin has not been taken */
if (coin_frequency > 0 && _coin.has_value() && !coin_taken() && count % coin_frequency == 0)
{
projectiles.back().coinify(*_coin);
}
}
void Projector::draw(
GLuint transformation_uniform, const glm::mat4& view, const glm::mat4& projection, GLuint texture_flag_uniform,
const sb::Color& rotating_hue, GLuint color_addition_uniform)
{
sprite.draw(transformation_uniform, view, projection, texture_flag_uniform);
for (Projectile& projectile : projectiles)
{
projectile.draw(transformation_uniform, view, projection, texture_flag_uniform, rotating_hue, color_addition_uniform);
}
/* Draw the coin separately if it has been taken and isn't collected yet */
if (_coin.has_value() && coin_taken() && !coin_collected)
{
glUniform4fv(color_addition_uniform, 1, &rotating_hue.normal()[0]);
_coin->draw(transformation_uniform, view, projection, texture_flag_uniform);
}
}
Flame::Flame(
const sb::Box& field, const glm::vec3& position, float speed, float angle, float mirror_interval, bool mask) :
field(field), position(position), speed(speed), angle(angle), _mask(mask)
{
/* Save initial values */
initial_angle = angle;
initial_position = position;
/* Set up animation */
if (!mask)
{
sprite.texture("resource/flame/flame-1.png", GL_LINEAR);
sprite.texture("resource/flame/flame-2.png", GL_LINEAR);
}
else
{
sprite.texture("resource/coin/coin-0.png", GL_LINEAR);
}
sprite.frames.frame_length(0.3f);
sprite.frames.play();
/* Initialize object size and position */
glm::vec2 size {12.0f / 486.0f};
sprite.scale(size);
sprite.translate(position);
box.size(2.0f * size);
box.center(position);
/* Only start the mirror effect if the interval is a positive value. */
if (mirror_interval >= 0.0f)
{
animation_mirror.frame_length(mirror_interval);
animation_mirror.play();
}
};
void Flame::coin(const sb::Sprite& sprite, float radius, float angle)
{
_coin = sprite;
coin_radius = radius;
coin_angle = angle;
}
void Flame::update(const sb::Timer& timer)
{
sprite.update(timer);
/* Move */
animation_mirror.update(timer.stamp());
position += glm::vec3{sb::math::angle_to_vector(angle, timer.delta(speed)), 0.0f};
/* Update challenge coin position */
if (_coin.has_value() && !coin_taken() && !coin_collected)
{
glm::vec3 coin_position = position + glm::vec3{sb::math::angle_to_vector(coin_angle, coin_radius), 0.0f};
_coin->translate(
sb::math::wrap_point(
coin_position,
glm::vec3{field.left(), field.bottom(), -1.0f},
glm::vec3{field.right(), field.top(), 1.0f}));
}
box.center(position);
sprite.translate(
sb::math::wrap_point(
position, glm::vec3{field.left(), field.bottom(), -1.0f}, glm::vec3{field.right(), field.top(), 1.0f}));
}
void Flame::mirror()
{
/* Reset the flame position and angle at the end of a cycle so overflow in time and movement don't begin to
* accumulate and de-sync the mirror cycle. */
if (mirrored)
{
angle = initial_angle;
position = initial_position;
}
else
{
angle += glm::pi<float>();
}
/* Indicate whether flame is moving away from its original position or toward it. */
mirrored = !mirrored;
}
bool Flame::collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
{
if (Enemy::collide_coin(box, clip_lower, clip_upper))
{
sb::Box coin_box = this->box;
coin_box.move(glm::vec3{sb::math::angle_to_vector(coin_angle, coin_radius), 0.0f});
coin_box.center(sb::math::wrap_point({coin_box.center(), 0.0f}, clip_lower, clip_upper));
box.center(sb::math::wrap_point({box.center(), 0.0f}, clip_lower, clip_upper));
return coin_box.collide(box);
}
return false;
}