604 lines
18 KiB
C++
604 lines
18 KiB
C++
#include "Enemy.hpp"
|
|
|
|
void Enemy::reset([[maybe_unused]] const Character& character)
|
|
{
|
|
if (!coin_collected) _coin_taken = false;
|
|
}
|
|
|
|
bool Enemy::collide(const Character& character, 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 = character.box();
|
|
wrapped_other.center(sb::math::wrap_point({wrapped_other.center(), 0.0f}, clip_lower, clip_upper));
|
|
return wrapped.collide(wrapped_other);
|
|
}
|
|
|
|
bool Enemy::collide_coin(
|
|
[[maybe_unused]] const Character& character,
|
|
[[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 sb::Sprite& sprite, const Curve& curve, float relative, float speed, float stray) :
|
|
curve(curve), relative(relative), speed(speed), stray(stray)
|
|
{
|
|
position = start();
|
|
this->sprite = sprite;
|
|
this->sprite.frames.frame_length(0.5f);
|
|
this->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 */
|
|
this->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(const Character& character, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
|
{
|
|
if (Enemy::collide_coin(character, 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));
|
|
sb::Box character_box = character.box();
|
|
character_box.center(sb::math::wrap_point({character_box.center(), 0.0f}, clip_lower, clip_upper));
|
|
return coin_box.collide(character_box);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Fish::Fish(const sb::Sprite& sprite, const Curve& curve, float relative, float speed, float radius, float offset) :
|
|
curve(curve), relative(relative), speed(speed), radius(radius), offset(offset)
|
|
{
|
|
this->sprite = sprite;
|
|
this->sprite.frames.frame_length(0.3f);
|
|
this->sprite.frames.play();
|
|
|
|
/* Set size of objects */
|
|
glm::vec2 size {12.0f / 486.0f};
|
|
this->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(const Character& character, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
|
{
|
|
if (Enemy::collide_coin(character, 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
|
|
));
|
|
sb::Box character_box = character.box();
|
|
character_box.center(sb::math::wrap_point({character_box.center(), 0.0f}, clip_lower, clip_upper));
|
|
return coin_box.collide(character_box);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Projectile::Projectile(
|
|
const sb::Sprite& sprite,
|
|
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 */
|
|
this->sprite = sprite;
|
|
this->sprite.frames.frame_length(0.05f);
|
|
this->sprite.frames.play();
|
|
|
|
/* Set size of objects */
|
|
glm::vec2 size {12.0f / 486.0f};
|
|
this->sprite.scale(size);
|
|
box.size(2.0f * size);
|
|
|
|
/* Place objects at initial position */
|
|
this->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 sb::Sprite& sprite,
|
|
const sb::Sprite& projectile_sprite,
|
|
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);
|
|
|
|
/* Set size and position of objects */
|
|
glm::vec2 size {12.0f / 486.0f};
|
|
this->sprite = sprite;
|
|
this->sprite.scale(size);
|
|
this->sprite.translate(position);
|
|
box.size(2.0f * size);
|
|
box.center(position);
|
|
|
|
this->projectile_sprite = projectile_sprite;
|
|
|
|
/* Charge animation will always be playing */
|
|
animation_charge.play();
|
|
};
|
|
|
|
void Projector::reset(const Character& character)
|
|
{
|
|
Enemy::reset(character);
|
|
if (&character == &this->character) {
|
|
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 Character& character, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
|
{
|
|
for (const Projectile& projectile : projectiles)
|
|
{
|
|
/* Only collide with characters this projector was shot toward */
|
|
if (&this->character == &character
|
|
&& !projectile.coinified()
|
|
&& projectile.collide(character, 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(const Character& character, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
|
{
|
|
if (Enemy::collide_coin(character, clip_lower, clip_upper))
|
|
{
|
|
for (const Projectile& projectile : projectiles)
|
|
{
|
|
if (projectile.coinified() && projectile.collide(character, 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(projectile_sprite, 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::Sprite& sprite,
|
|
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 */
|
|
this->sprite = sprite;
|
|
this->sprite.frames.frame_length(0.3f);
|
|
this->sprite.frames.play();
|
|
|
|
/* Initialize object size and position */
|
|
glm::vec2 size {12.0f / 486.0f};
|
|
this->sprite.scale(size);
|
|
this->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(const Character& character, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
|
{
|
|
if (Enemy::collide_coin(character, 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));
|
|
sb::Box character_box = character.box();
|
|
character_box.center(sb::math::wrap_point({character_box.center(), 0.0f}, clip_lower, clip_upper));
|
|
return coin_box.collide(character_box);
|
|
}
|
|
return false;
|
|
}
|