cakefoot/src/Enemy.hpp
Cocktail Frank d2802fa131 Prevent lag in replay recording and optimize texture loading
Start the run timer immediately when the load level function is called,
so that if there is lag during level load, it will not cause the replay
recording to desync.

Reduce lag by reducing filesystem access during level load and
gameplay. Load enemy textures once at program start, rather than every
time a new enemy object is created. Only refresh HUD and buttons when
the title screen is loaded, rather than when each level is loaded.
2025-09-19 23:47:06 -04:00

480 lines
16 KiB
C++

#pragma once
#include <optional>
#include <functional>
#include "glm/glm.hpp"
#include "Timer.hpp"
#include "Box.hpp"
#include "Animation.hpp"
#include "Curve.hpp"
#include "Sprite.hpp"
#include "Character.hpp"
class Enemy
{
protected:
sb::Sprite sprite;
sb::Box box;
/* Enemies have an optional challenge coin defined by different parameters depending on the enemy type. */
std::optional<sb::Sprite> _coin;
bool _coin_taken = false, coin_collected = false;
/*!
* Default destructor, defined as virtual so that the auto generated derived destructors will be called.
*/
virtual ~Enemy() = default;
public:
/*!
* Reset enemy to original state. Does nothing unless the derived class implements a reset method.
*/
virtual void reset();
/*!
* Set the object's coin taken state to true, so the coin will be able to be collected if the checkpoint or end
* is reached.
*/
virtual void take_coin();
/*!
* @return coin taken state
*/
bool coin_taken() const;
/*!
* Set object's coin collected state to true, so even if the enemy is reset, the coin will remain taken and collected.
*/
virtual void collect_coin();
/*!
* Set the position of the enemy's coin. If the enemy object has no coin, do nothing.
*
* @param translation coin location
*/
void coin_translation(const glm::vec3& translation);
/*!
* @param timer timer that has been updating every unpaused frame to be used to measure distance to move this frame
*/
virtual void update(const sb::Timer& timer) = 0;
/*!
* Perform GL drawing operations using the given uniform locations and transformation matrices.
*
* @param transformation_uniform transformation uniform location in the shader program
* @param view view transformation matrix
* @param projection projection transformation matrix
* @param texture_flag_uniform uniform location in the shader program of the boolean that turns texture drawing
* on or off
* @param rotating_hue color of the hue rotation effect
* @param color_addition_uniform uniform location in the shader program of the RGBA value for color addition
* effect
*/
virtual void 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);
/*!
* Check whether the enemy collides with the given ::Sprite positioned at the given ::Box. The sprite object may be used to
* perform pixel collision, so alpha pixels that collide will not be counted as a collision, but this is not implemented yet.
*
* The enemy and character are tracked without wrapping their coordinates, so the clip for wrapping must be given to this
* function in order to determine if they collide.
*
* @param box position in world coordinates to check for a collision with the enemy's own box object
* @param sprite graphics to use for per pixel collision
* @param clip_lower clip area lower bounds to wrap the position to
* @param clip_upper clip area upper bounds to wrap the position to
* @return true if the given box collides with this enemy's box
*/
virtual bool collide(const sb::Box& box, const sb::Sprite& sprite, const glm::vec3& clip_lower,
const glm::vec3& clip_upper) const;
/*!
* Check whether the given box coordinates collide with the enemy's challenge coin. If the enemy doesn't have a
* challenge coin, this returns false.
*
* The enemy and character are tracked without wrapping their coordinates, so the clip for wrapping must be given
* to this function in order to determine if they collide.
*
* @param box position in world coordinates to check for a collision with the enemy's challenge coin
* @param clip_lower clip area lower bounds to wrap the position to
* @param clip_upper clip area upper bounds to wrap the position to
* @return true if the given box collides with the enemy's challenge coin
*/
virtual bool collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
virtual std::string stat_id() = 0;
/* Use this flag to indicate this is the wandering fish. */
bool wanderer = false;
};
/*!
* Create an enemy which moves back and forth, cutting the curve perpendicular to the angle of the curve at a given point.
*/
class Slicer : public Enemy
{
private:
std::reference_wrapper<const Curve> curve;
float relative = 0.0f, speed = 0.0f, stray = 0.0f, coin_radius = 0.0f, coin_angle = 0.0f;
bool toward_end = true;
/*!
*/
glm::vec3 center() const;
/*!
*/
float angle() const;
/*!
*/
glm::vec3 start() const;
/*!
*/
glm::vec3 end() const;
public:
glm::vec3 position = {0.0f, 0.0f, 0.0f};
/*!
*/
Slicer(const sb::Sprite& sprite,
const Curve& curve,
float relative,
float speed = 0.51440329f,
float stray = 0.24691358f);
/*!
* Add a challenge coin to this slicer enemy. The sprite will be owned, moved, and drawn by this object.
*
* @param sprite representation of the coin
* @param radius distance from the center of the slicer object
* @param angle angle position around the slicer object
*/
void coin(const sb::Sprite& sprite, float radius, float angle);
/*!
* If the slicer has a challenge coin, check if it collides with the given box.
*
* @see Enemy::collide_coin(const sb::Box&, const glm::vec3&, const glm::vec3&)
*/
bool collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
/*!
*/
void update(const sb::Timer& timer);
std::string stat_id() { return "SLICER"; };
};
/*!
* Create an enemy which rotates around a point on the curve at a given radius and speed.
*/
class Fish : public Enemy
{
private:
std::reference_wrapper<const Curve> curve;
float relative = 0.0f, speed = 0.0f, radius = 0.0f, angle = 0.0f, offset = 0.0f, coin_angle = 0.0f, coin_radius = 0.0f;
std::optional<float> first_update_time;
/*!
* @return vertex in world coordinates the fish is rotating around, always lies on the curve
*/
glm::vec3 center() const;
public:
/*!
* Create a Fish object from relative position on a given curve, speed, and radius. Optionally, an offset angle can
* be added to start the fish that amount away on its circle.
*
* The speed is in radians to rotate per second.
*
* @param sprite Object containing the graphics
* @param curve curve to associate this enemy with
* @param relative the position on the curve from 0 to 1
* @param speed radians to rotate per second
* @param radius distance from the center to rotate around
* @param offset angle amount in radians to offset start point
*/
Fish(const sb::Sprite& sprite,
const Curve& curve,
float relative,
float speed = 0.01010029f,
float radius = 0.12345679f,
float offset = 0.0f);
/*!
* Add a challenge coin to this fish enemy. The coin will rotate on the same circle as the fish, offset by the given
* angle. The sprite will be owned, moved, and drawn by this object.
*
* @param sprite representation of the coin
* @param angle amount in radians to offset from the fish
*/
void coin(const sb::Sprite& sprite, float angle);
/*!
* Add a challenge coin to this fish enemy. The coin will be positioned at the given distance and angle away from
* the fish. The sprite will be owned, moved, and drawn by this object.
*
* @param sprite representation of the coin
* @param radius distance of the coin from the center of the fish
* @param angle angle around the fish's center to position the coin
*/
void coin(const sb::Sprite& sprite, float radius, float angle);
/*!
* If the fish has a challenge coin, check if it collides with the given box.
*
* @see Enemy::collide_coin(const sb::Box&, const glm::vec3&, const glm::vec3&)
*/
bool collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
/*!
*/
void update(const sb::Timer& timer);
std::string stat_id() { return "FISH"; };
};
/*!
* Create an enemy which is a projectile that has been fired toward a location by a ::Projector object. It continues travelling in
* the same direction even after reaching the point, and goes off screen.
*/
class Projectile : public Enemy
{
private:
glm::vec3 position = glm::vec3{0.0f};
float speed = 0.0f, angle = 0.0f;
public:
/*!
* Create a ::Projectile object, an ::Enemy directed toward a destination, given the destination, start, and speed.
*
* The speed is in world coordinates per second.
*
* @param sprite Object containing the graphics
* @param position
* @param target
* @param speed distance in world coordinates the projectiles will travel per second
*/
Projectile(const sb::Sprite& sprite, const glm::vec3& position, const glm::vec3& target, float speed = 0.51440329f);
/*!
* Turn the projectile into a coin.
*
* @param sprite sprite representing the coin, will be copied into the object, will be moved and drawn by the object
*/
void coinify(const sb::Sprite& sprite);
/*!
*/
void update(const sb::Timer& timer);
/*!
*/
void 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);
/*!
* @return true if the projectile has left the clip space
*/
bool out_of_bounds() const;
/*!
* @return return if the projectile was transformed into a challenge coin
*/
bool coinified() const;
std::string stat_id() { return "DRONE"; };
};
/*!
* Create an enemy which fires projectiles toward the player's position.
*/
class Projector : public Enemy
{
private:
const Character& character;
glm::vec3 position = glm::vec3{0.0f};
float speed = 0.0f;
float release_delay;
sb::Animation animation_charge = sb::Animation(std::bind(&Projector::charge, this)),
animation_release = sb::Animation(std::bind(&Projector::release, this));
std::vector<Projectile> projectiles;
int coin_frequency = 0, count = 0;
sb::Sprite projectile_sprite;
void charge();
void release();
public:
/*!
* Create a Projector object at a given position. Set the speed and rate of fire.
*
* The speed is in world coordinates per second, and the frequency is number of seconds between each projectile launch.
*
* @param position location to fire from in world coordinates
* @param speed distance in world coordinates the projectiles will travel per second
* @param rate amount of seconds between each projectile
* @param release_delay amount in seconds between charge and release
*/
Projector(const sb::Sprite& sprite,
const sb::Sprite& projectile_sprite,
const Character& character,
const glm::vec3& position,
float speed = 0.51440329f,
float rate = 3.0f,
float release_delay = 0.3f);
/*!
*/
void reset();
/*!
* Add a challenge coin to this projector enemy. The projector will transform every Nth projectile into a coin.
*
* @param sprite sprite to represent the coin, will be copied to the Nth projectile
* @param frequency every Nth projectile will be a challenge coin
*/
void coin(const sb::Sprite& sprite, int frequency);
/*!
* If the projector has a challenge coin, check if it collides with the given box.
*
* @see Enemy::collide_coin(const sb::Box&, const glm::vec3&, const glm::vec3&)
*/
bool collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
/*!
* Remove all projectiles that are coinified.
*/
void take_coin();
/*!
*/
void update(const sb::Timer& timer);
/*!
*/
void 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);
/*!
* Check if projectiles collide.
*
* @see Enemy::collide(const sb::Box&, const Sprite&, const glm::vec3&, const glm::vec3&)
*/
bool collide(const sb::Box& box, const sb::Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
std::string stat_id() { return "DRONE"; };
};
/*!
* Create an enemy which is a flame that constantly scrolls with a given angle direction and speed from its initial
* position, optionally mirroring to scroll the opposite direction at a given interval. The enemy wraps at the edges of
* the screen, so it is always on screen.
*/
class Flame : public Enemy
{
private:
sb::Box field;
glm::vec3 position = glm::vec3{0.0f}, initial_position = glm::vec3{0.0f};
float speed = 0.0f, angle = 0.0f, coin_angle = 0.0f, coin_radius = 0.0f, initial_angle = 0.0f;
sb::Animation animation_mirror = sb::Animation(std::bind(&Flame::mirror, this));
bool mirrored = false, _mask = false;
/*!
* Rotate direction 180 degrees.
*/
void mirror();
public:
/*!
* Create a ::Flame object, a scrolling ::Enemy moving away from the given start position with the given angle and speed. If
* mirror interval is given, the flame object will switch to the opposite direction every interval.
*
* The speed is in world coordinates per second.
*
* @param sprite
* @param field
* @param position starting position
* @param speed distance in world coordinates the flame will travel per second
* @param angle direction the flame will travel
* @param mirror_interval optional interval in seconds the direction should mirror, negative values turn off mirroring
* @param mask disguise the flame as a coin
*/
Flame(const sb::Sprite& sprite,
const sb::Box& field,
const glm::vec3& position,
float speed,
float angle,
float mirror_interval = -1.0f,
bool mask = false);
/*!
* Add a challenge coin to this flame enemy. The sprite will be owned, moved, and drawn by this object.
*
* @param sprite representation of the coin
* @param radius distance from the center of the flame object
* @param angle angle position around the slicer object
*/
void coin(const sb::Sprite& sprite, float radius, float angle);
/*!
* If the flame has a challenge coin, check if it collides with the given box.
*
* @see Enemy::collide_coin(const sb::Box&, const glm::vec3&, const glm::vec3&)
*/
bool collide_coin(sb::Box box, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
inline void mask(bool state)
{
_mask = state;
}
bool mask() const
{
return _mask;
}
/*!
*/
void update(const sb::Timer& timer);
std::string stat_id() { return "FIRE"; };
};