add enemy collision
This commit is contained in:
parent
21ee780fab
commit
3c67b92361
5
Makefile
5
Makefile
@ -54,8 +54,7 @@ $(GLEW_DIR)%.o: $(GLEW_DIR)%.c $(GLEW_DIR)%.h
|
||||
|
||||
$(SB_SRC_DIR)extension.o : $(addprefix $(SB_SRC_DIR),Box.hpp Segment.hpp Color.hpp filesystem.hpp Pixels.hpp Log.hpp)
|
||||
$(SB_SRC_DIR)Node.o : $(addprefix $(SB_SRC_DIR),Game.hpp Configuration.hpp Delegate.hpp Display.hpp Input.hpp Box.hpp Audio.hpp Log.hpp)
|
||||
$(SB_SRC_DIR)Game.o : $(addprefix $(SB_SRC_DIR),extension.hpp Node.hpp Recorder.hpp Input.hpp Configuration.hpp \
|
||||
Delegate.hpp Audio.hpp Log.hpp)
|
||||
$(SB_SRC_DIR)Game.o : $(addprefix $(SB_SRC_DIR),extension.hpp Node.hpp Recorder.hpp Input.hpp Configuration.hpp Delegate.hpp Audio.hpp Log.hpp)
|
||||
$(SB_SRC_DIR)Animation.o : $(addprefix $(SB_SRC_DIR),Node.hpp Timer.hpp)
|
||||
$(SB_SRC_DIR)Recorder.o : $(addprefix $(SB_SRC_DIR),Node.hpp Game.hpp Configuration.hpp Delegate.hpp Animation.hpp extension.hpp)
|
||||
$(SB_SRC_DIR)Input.o : $(addprefix $(SB_SRC_DIR),Node.hpp Animation.hpp Configuration.hpp Delegate.hpp)
|
||||
@ -76,7 +75,7 @@ $(SRC_DIR)Curve.o : $(addprefix $(SB_SRC_DIR),Attributes.hpp math.hpp extension.
|
||||
$(SRC_DIR)Character.o : $(addprefix $(SB_SRC_DIR),Configuration.hpp Switch.hpp Selection.hpp Segment.hpp Timer.hpp) $(addprefix $(SRC_DIR),Sprite.hpp Curve.hpp)
|
||||
$(SRC_DIR)Pad.o : $(addprefix $(SRC_DIR),Model.hpp Switch.hpp)
|
||||
$(SRC_DIR)Sprite.o : $(addprefix $(SRC_DIR),Model.hpp)
|
||||
$(SRC_DIR)Enemy.o : $(addprefix $(SB_SRC_DIR),Timer.hpp Animation.hpp) $(addprefix $(SRC_DIR),Sprite.hpp Curve.hpp)
|
||||
$(SRC_DIR)Enemy.o : $(addprefix $(SB_SRC_DIR),Timer.hpp Animation.hpp Box.hpp) $(addprefix $(SRC_DIR),Sprite.hpp Curve.hpp Character.hpp)
|
||||
$(SRC_DIR)Cakefoot.o : $(SRC_H_FILES) $(SB_H_FILES)
|
||||
%.o : %.cpp %.hpp
|
||||
$(CXX) $(CXXFLAGS) $< -c -o $@
|
||||
|
2
lib/sb
2
lib/sb
@ -1 +1 @@
|
||||
Subproject commit c0b55752e1660c41feb841ce059bf893c8d773a2
|
||||
Subproject commit 12e5a15d1ced403cedf23752bea487a06e37d4f4
|
@ -87,9 +87,9 @@ Cakefoot::Cakefoot()
|
||||
poke = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND), SDL_FreeCursor);
|
||||
grab = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL), SDL_FreeCursor);
|
||||
|
||||
/* Initialize character sprite and position */
|
||||
character.sprite = Sprite("resource/cake-frames/cake1.png", glm::vec2{20.0f / 486.0f});
|
||||
character.reset(curve());
|
||||
/* Load title screen and character graphics */
|
||||
character.load();
|
||||
load_level(0);
|
||||
}
|
||||
|
||||
void Cakefoot::load_gl_context()
|
||||
@ -172,7 +172,7 @@ void Cakefoot::load_level(int index)
|
||||
{
|
||||
level_index = index;
|
||||
curve_index = index;
|
||||
character.reset(curve());
|
||||
character.beginning(curve());
|
||||
if (index == 1)
|
||||
{
|
||||
enemies = {
|
||||
@ -380,11 +380,16 @@ void Cakefoot::update(float timestamp)
|
||||
set_framerate(configuration()["display"]["framerate"]);
|
||||
glUniform1f(uniform["time"], timer.elapsed());
|
||||
|
||||
/* Update character, along the curve, using the timer to determine movement since last frame, and update enemies. */
|
||||
/* Update character, along the curve, using the timer to determine movement since last frame, and update enemies. Check for collison
|
||||
* as enemies are updated. */
|
||||
character.update(curve(), timer);
|
||||
for (auto& enemy : enemies)
|
||||
{
|
||||
enemy->update(timer);
|
||||
if (enemy->collide(character.box(), character.sprite(), {-curve().aspect, -1.0f, -1.0f}, {curve().aspect, 1.0f, 1.0f}))
|
||||
{
|
||||
character.beginning(curve());
|
||||
}
|
||||
}
|
||||
|
||||
/* Transformation for rotating the model space and looking at the center of the field of play from the camera position. */
|
||||
@ -394,9 +399,6 @@ void Cakefoot::update(float timestamp)
|
||||
/* Transformation from camera space to clip space. */
|
||||
projection = glm::perspective(field_of_view_y, window_box().aspect(), 0.1f, 100.0f);
|
||||
|
||||
/* Cake coordinates wrapped */
|
||||
glm::vec2 cake_translation = sb::wrap_point(character.position, {-curve().aspect, -1.0f, 0.0f}, {curve().aspect, 1.0f, 1.0f});
|
||||
|
||||
/* Plane position vertices will be used for everything before the curve */
|
||||
sb::Plane::position->bind("vertex_position", shader_program);
|
||||
|
||||
@ -436,9 +438,7 @@ void Cakefoot::update(float timestamp)
|
||||
}
|
||||
|
||||
/* Draw cake */
|
||||
character.sprite.translate(glm::vec3{cake_translation, 0.0f});
|
||||
character.sprite.bind_texture(uniform["texture enabled"]);
|
||||
character.sprite.draw(uniform["mvp"], view, projection);
|
||||
character.draw(curve(), uniform["mvp"], view, projection, uniform["texture enabled"]);
|
||||
|
||||
/* Update FPS indicator display to the current FPS count and draw. */
|
||||
if (configuration()["display"]["fps"])
|
||||
|
@ -1,6 +1,29 @@
|
||||
#include "Character.hpp"
|
||||
|
||||
void Character::reset(const Curve& curve)
|
||||
Character::Character(const Configuration& configuration) : configuration(configuration)
|
||||
{
|
||||
/* Initialize sprite and box to the size of the graphic. */
|
||||
glm::vec2 size {20.0f / 486.0f};
|
||||
_sprite = Sprite("resource/cake-frames/cake1.png", size);
|
||||
_box.size(size);
|
||||
}
|
||||
|
||||
void Character::load()
|
||||
{
|
||||
_sprite.load();
|
||||
}
|
||||
|
||||
const Box& Character::box() const
|
||||
{
|
||||
return _box;
|
||||
}
|
||||
|
||||
const Sprite& Character::sprite() const
|
||||
{
|
||||
return _sprite;
|
||||
}
|
||||
|
||||
void Character::beginning(const Curve& curve)
|
||||
{
|
||||
next_point_index = 0;
|
||||
speed = 0.0f;
|
||||
@ -8,11 +31,6 @@ void Character::reset(const Curve& curve)
|
||||
accelerating = false;
|
||||
}
|
||||
|
||||
const nlohmann::json& Character::entry(const std::string& suffix) const
|
||||
{
|
||||
return configuration["character"][*profile.current() + "-" + suffix];
|
||||
}
|
||||
|
||||
void Character::update(const Curve& curve, const sb::Timer& timer)
|
||||
{
|
||||
/* Adjust speed based on acceleration state and character profile. */
|
||||
@ -74,4 +92,20 @@ void Character::update(const Curve& curve, const sb::Timer& timer)
|
||||
distance_remaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Update collision box location. */
|
||||
_box.center(position);
|
||||
}
|
||||
|
||||
void Character::draw(const Curve& curve, GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform)
|
||||
{
|
||||
glm::vec2 translation = sb::wrap_point(position, {-curve.aspect, -1.0f, 0.0f}, {curve.aspect, 1.0f, 1.0f});
|
||||
_sprite.translate(glm::vec3{translation, 0.0f});
|
||||
_sprite.bind_texture(texture_flag_uniform);
|
||||
_sprite.draw(transformation_uniform, view, projection);
|
||||
}
|
||||
|
||||
const nlohmann::json& Character::entry(const std::string& suffix) const
|
||||
{
|
||||
return configuration["character"][*profile.current() + "-" + suffix];
|
||||
}
|
||||
|
@ -15,9 +15,11 @@ private:
|
||||
|
||||
inline static const std::vector<std::string> PROFILES = {"cake", "ball", "buffalo"};
|
||||
|
||||
Configuration& configuration;
|
||||
const Configuration& configuration;
|
||||
float speed = 0.0f;
|
||||
int next_point_index = 0;
|
||||
Sprite _sprite;
|
||||
Box _box;
|
||||
|
||||
/*!
|
||||
* @param suffix the character profile config value to get, without the character name
|
||||
@ -30,14 +32,28 @@ public:
|
||||
sb::Switch<> accelerating = false;
|
||||
sb::Selection<std::vector<std::string>> profile = PROFILES;
|
||||
glm::vec3 position;
|
||||
Sprite sprite;
|
||||
|
||||
Character(Configuration& configuration) : configuration(configuration) {}
|
||||
Character(const Configuration& configuration);
|
||||
|
||||
/*!
|
||||
* Load the graphics into the GPU. This is only necessary to run if the object was constructed before the GL context was loaded.
|
||||
*/
|
||||
void load();
|
||||
|
||||
/*!
|
||||
* @return reference to the box object enclosing the character in world space
|
||||
*/
|
||||
const Box& box() const;
|
||||
|
||||
/*!
|
||||
* @return reference to the character's sprite object for accessing the character's graphics
|
||||
*/
|
||||
const Sprite& sprite() const;
|
||||
|
||||
/*!
|
||||
* @param curve the curve to reset the beginning position to
|
||||
*/
|
||||
void reset(const Curve& curve);
|
||||
void beginning(const Curve& curve);
|
||||
|
||||
/*!
|
||||
* Check acceleration state and adjust speed. Move character toward the next point on the curve.
|
||||
@ -46,4 +62,15 @@ public:
|
||||
* @param timer a timer object that is updated once per frame, so that it provides delta time for movement
|
||||
*/
|
||||
void update(const Curve& curve, const sb::Timer& timer);
|
||||
|
||||
/*!
|
||||
* Perform GL drawing operations using the character's sprite object.
|
||||
*
|
||||
* @param curve curve which determines how to wrap the character's position to the curve's space
|
||||
* @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
|
||||
*/
|
||||
void draw(const Curve& curve, GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform);
|
||||
};
|
||||
|
@ -1,16 +1,33 @@
|
||||
#include "Enemy.hpp"
|
||||
|
||||
bool Enemy::collide(const sb::Box& box, const Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
||||
{
|
||||
sb::Box wrapped = this->box;
|
||||
wrapped.center(sb::wrap_point({this->box.center(), 0.0f}, clip_lower, clip_upper));
|
||||
sb::Box wrapped_other = box;
|
||||
wrapped_other.center(sb::wrap_point({box.center(), 0.0f}, clip_lower, clip_upper));
|
||||
return wrapped.collide(wrapped_other);
|
||||
}
|
||||
|
||||
Slicer::Slicer(const Curve& curve, float relative, float speed, float stray) : curve(curve), relative(relative), speed(speed), stray(stray)
|
||||
{
|
||||
reset();
|
||||
// reset();
|
||||
position = start();
|
||||
sprite.texture("resource/slicer/slicer-1.png");
|
||||
sprite.scale(glm::vec2{12.0f / 486.0f});
|
||||
|
||||
/* 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::reset()
|
||||
{
|
||||
position = start();
|
||||
sprite.translate(curve.get().wrap(position));
|
||||
// position = start();
|
||||
// sprite.translate(curve.get().wrap(position));
|
||||
// box.center(position);
|
||||
}
|
||||
|
||||
void Slicer::update(const sb::Timer& timer)
|
||||
@ -33,6 +50,7 @@ void Slicer::update(const sb::Timer& timer)
|
||||
toward_end = !toward_end;
|
||||
}
|
||||
position += glm::vec3{sb::Segment(position, destination).step(move), 0.0f};
|
||||
box.center(position);
|
||||
sprite.translate(curve.get().wrap(position));
|
||||
}
|
||||
|
||||
@ -68,13 +86,17 @@ glm::vec3 Slicer::end() const
|
||||
Fish::Fish(const Curve& curve, float relative, float speed, float radius) : curve(curve), relative(relative), speed(speed), radius(radius)
|
||||
{
|
||||
sprite.texture("resource/fish/fish-1.png");
|
||||
sprite.scale(glm::vec2{12.0f / 486.0f});
|
||||
reset();
|
||||
|
||||
/* Set size of objects */
|
||||
glm::vec2 size {12.0f / 486.0f};
|
||||
sprite.scale(size);
|
||||
box.size(2.0f * size);
|
||||
// reset();
|
||||
};
|
||||
|
||||
void Fish::reset()
|
||||
{
|
||||
angle = 0.0f;
|
||||
// angle = 0.0f;
|
||||
}
|
||||
|
||||
void Fish::update(const sb::Timer& timer)
|
||||
@ -82,6 +104,7 @@ void Fish::update(const sb::Timer& timer)
|
||||
angle += timer.delta(speed);
|
||||
glm::vec3 position = center() + glm::vec3{sb::angle_to_vector(angle, radius), 0.0f};
|
||||
sprite.translate(curve.get().wrap(position));
|
||||
box.center(position);
|
||||
}
|
||||
|
||||
void Fish::draw(GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform)
|
||||
@ -102,13 +125,22 @@ Projectile::Projectile(const glm::vec3& position, const glm::vec3& target, float
|
||||
|
||||
/* Initialize sprite */
|
||||
sprite.texture("resource/projectile/projectile-1.png");
|
||||
sprite.scale(glm::vec2{12.0f / 486.0f});
|
||||
|
||||
/* 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::update(const sb::Timer& timer)
|
||||
{
|
||||
position += glm::vec3{sb::angle_to_vector(angle, timer.delta(speed)), 0.0f};
|
||||
sprite.translate(position);
|
||||
box.center(position);
|
||||
}
|
||||
|
||||
void Projectile::draw(GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform)
|
||||
@ -127,8 +159,15 @@ Projector::Projector(const Character& character, const glm::vec3& position, floa
|
||||
{
|
||||
animation_charge.frame_length(rate);
|
||||
sprite.texture("resource/projector/projector-1.png");
|
||||
sprite.scale(glm::vec2{12.0f / 486.0f});
|
||||
|
||||
/* 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();
|
||||
};
|
||||
|
||||
@ -172,6 +211,18 @@ void Projector::draw(GLuint transformation_uniform, const glm::mat4 view, const
|
||||
}
|
||||
}
|
||||
|
||||
bool Projector::collide(const sb::Box& box, const Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const
|
||||
{
|
||||
for (const Projectile& projectile : projectiles)
|
||||
{
|
||||
if (projectile.collide(box, sprite, clip_lower, clip_upper))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Projector::charge()
|
||||
{
|
||||
animation_release.play_once(release_delay);
|
||||
@ -184,10 +235,13 @@ void Projector::release()
|
||||
|
||||
Flame::Flame(const glm::vec3& position, float speed, float angle, float mirror_interval) : position(position), speed(speed), angle(angle)
|
||||
{
|
||||
/* Initialize sprite */
|
||||
/* Initialize object size and position */
|
||||
glm::vec2 size {12.0f / 486.0f};
|
||||
sprite.texture("resource/flame/flame-1.png");
|
||||
sprite.scale(glm::vec2{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)
|
||||
@ -202,6 +256,7 @@ void Flame::update(const sb::Timer& timer)
|
||||
animation_mirror.update(timer.stamp());
|
||||
position += glm::vec3{sb::angle_to_vector(angle, timer.delta(speed)), 0.0f};
|
||||
sprite.translate(position);
|
||||
box.center(position);
|
||||
}
|
||||
|
||||
void Flame::draw(GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform)
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include <functional>
|
||||
#include "glm/glm.hpp"
|
||||
#include "Timer.hpp"
|
||||
#include "Box.hpp"
|
||||
#include "Animation.hpp"
|
||||
#include "Curve.hpp"
|
||||
#include "Sprite.hpp"
|
||||
@ -13,6 +14,8 @@ class Enemy
|
||||
|
||||
protected:
|
||||
|
||||
sb::Box box;
|
||||
|
||||
/*!
|
||||
* Default destructor, defined as virtual so that the auto generated derived destructors will be called.
|
||||
*/
|
||||
@ -26,9 +29,33 @@ public:
|
||||
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
|
||||
*/
|
||||
virtual void draw(GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform) = 0;
|
||||
|
||||
/*!
|
||||
* Check whether the enemy collides with the given ::Sprite positioned at the given ::Box. The sprite object is used to perform
|
||||
* pixel collision, so alpha pixels that collide will not be counted as a collision.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Note: per-pixel collision not implemented yet.
|
||||
*
|
||||
* @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 sprite object's pixels enclosed in the box object collides with the pixels enclosed by
|
||||
* this enemy's box object
|
||||
*/
|
||||
virtual bool collide(const sb::Box& box, const Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
|
||||
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -69,6 +96,7 @@ public:
|
||||
Slicer(const Curve& curve, float relative, float speed = 0.51440329f, float stray = 0.24691358f);
|
||||
|
||||
/*!
|
||||
* Place at Slicer::start() position, updating collision box and sprite objects.
|
||||
*/
|
||||
void reset();
|
||||
|
||||
@ -127,7 +155,8 @@ public:
|
||||
};
|
||||
|
||||
/*!
|
||||
* Create an enemy which is a projectile that has been fired toward a location by a ::Projector object.
|
||||
* 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
|
||||
{
|
||||
@ -217,11 +246,18 @@ public:
|
||||
*/
|
||||
void draw(GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_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 Sprite& sprite, const glm::vec3& clip_lower, const glm::vec3& clip_upper) const;
|
||||
|
||||
};
|
||||
|
||||
/*!
|
||||
* 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.
|
||||
* 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
|
||||
{
|
||||
|
@ -3,7 +3,7 @@
|
||||
#include "glm/glm.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
class Sprite : private sb::Plane
|
||||
class Sprite : public sb::Plane
|
||||
{
|
||||
|
||||
private:
|
||||
@ -93,7 +93,7 @@ public:
|
||||
* Add a new texture to ::Sprite from a path to an image file which will converted into a new texture.
|
||||
*
|
||||
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
|
||||
* the texture, and it must be loaded with a call to ::load or Texture::load.
|
||||
* the texture, and it must be loaded with a call to Model::load or Texture::load.
|
||||
*
|
||||
* @param path path to an image
|
||||
*/
|
||||
@ -177,17 +177,6 @@ public:
|
||||
Model::transform(_translation * _scale * _rotation * transformation);
|
||||
}
|
||||
|
||||
/*!
|
||||
* @return box object that represents the world coordinates of the rectangle surrounding the sprite object
|
||||
*/
|
||||
sb::Box box() const;
|
||||
|
||||
/*!
|
||||
* @param sprite another sprite to check against for a collision
|
||||
* @return true if the two sprite objects collide, false otherwise
|
||||
*/
|
||||
bool collide(const Sprite& sprite) const;
|
||||
|
||||
/*
|
||||
*/
|
||||
void bind_texture() const
|
||||
|
Loading…
Reference in New Issue
Block a user