mirror of
https://github.com/NoelFB/tiny_link.git
synced 2024-11-25 18:18:56 +08:00
boss!!!
This commit is contained in:
parent
0d01334e0a
commit
f721b86bb9
|
@ -39,7 +39,7 @@ add_executable(game
|
|||
src/components/timer.h
|
||||
src/components/timer.cpp
|
||||
src/components/enemy.h
|
||||
)
|
||||
"src/components/ghost_frog.h" "src/components/ghost_frog.cpp" "src/components/orb.h" "src/components/orb.cpp")
|
||||
|
||||
# Reference blah
|
||||
target_link_libraries(game blah)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 243 B |
BIN
content/sprites/ghostfrog.ase
Normal file
BIN
content/sprites/ghostfrog.ase
Normal file
Binary file not shown.
|
@ -72,7 +72,7 @@ void Animator::render(Batch& batch)
|
|||
if (in_valid_state())
|
||||
{
|
||||
batch.push_matrix(
|
||||
Mat3x2::create_transform(entity()->position, m_sprite->origin, scale, 0));
|
||||
Mat3x2::create_transform(entity()->position + offset, m_sprite->origin, scale, 0));
|
||||
|
||||
auto& anim = m_sprite->animations[m_animation_index];
|
||||
auto& frame = anim.frames[m_frame_index];
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace TL
|
|||
|
||||
public:
|
||||
Vec2 scale = Vec2::one;
|
||||
Point offset = Point::zero;
|
||||
|
||||
Animator() = default;
|
||||
Animator(const String& sprite);
|
||||
|
|
|
@ -71,6 +71,11 @@ void Collider::set_cells(int x, int y, int w, int h, bool value)
|
|||
}
|
||||
|
||||
bool Collider::check(uint32_t mask, Point offset) const
|
||||
{
|
||||
return first(mask, offset) != nullptr;
|
||||
}
|
||||
|
||||
Collider* Collider::first(uint32_t mask, Point offset)
|
||||
{
|
||||
if (world())
|
||||
{
|
||||
|
@ -80,13 +85,32 @@ bool Collider::check(uint32_t mask, Point offset) const
|
|||
if (other != this &&
|
||||
(other->mask & mask) == mask &&
|
||||
overlaps(other, offset))
|
||||
return true;
|
||||
return other;
|
||||
|
||||
other = (Collider*)other->next();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Collider* Collider::first(uint32_t mask, Point offset) const
|
||||
{
|
||||
if (world())
|
||||
{
|
||||
auto other = world()->first<Collider>();
|
||||
while (other)
|
||||
{
|
||||
if (other != this &&
|
||||
(other->mask & mask) == mask &&
|
||||
overlaps(other, offset))
|
||||
return other;
|
||||
|
||||
other = (Collider*)other->next();
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Collider::overlaps(const Collider* other, Point offset) const
|
||||
|
|
|
@ -31,6 +31,8 @@ namespace TL
|
|||
void set_cell(int x, int y, bool value);
|
||||
void set_cells(int x, int y, int w, int h, bool value);
|
||||
|
||||
Collider* first(uint32_t mask, Point offset = Point::zero);
|
||||
const Collider* first(uint32_t mask, Point offset = Point::zero) const;
|
||||
bool check(uint32_t mask, Point offset = Point::zero) const;
|
||||
bool overlaps(const Collider* other, Point offset = Point::zero) const;
|
||||
|
||||
|
|
268
src/components/ghost_frog.cpp
Normal file
268
src/components/ghost_frog.cpp
Normal file
|
@ -0,0 +1,268 @@
|
|||
#include "ghost_frog.h"
|
||||
#include "mover.h"
|
||||
#include "hurtable.h"
|
||||
#include "player.h"
|
||||
#include "animator.h"
|
||||
#include "orb.h"
|
||||
#include "../masks.h"
|
||||
#include "../factory.h"
|
||||
|
||||
using namespace TL;
|
||||
|
||||
GhostFrog::GhostFrog()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void GhostFrog::awake()
|
||||
{
|
||||
m_home = entity()->position;
|
||||
}
|
||||
|
||||
void GhostFrog::update()
|
||||
{
|
||||
m_timer += Time::delta;
|
||||
|
||||
auto player = world()->first<Player>();
|
||||
auto mover = get<Mover>();
|
||||
auto anim = get<Animator>();
|
||||
auto hitbox = get<Collider>();
|
||||
|
||||
// no player - turn off AI
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
auto x = entity()->position.x;
|
||||
auto y = entity()->position.y;
|
||||
auto player_x = player->entity()->position.x;
|
||||
|
||||
// update sprite
|
||||
if (mover->on_ground())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// flip sprite
|
||||
anim->scale = Vec2(m_facing, 1);
|
||||
|
||||
// NORMAL STATE
|
||||
if (m_state == st_readying_attack)
|
||||
{
|
||||
m_facing = Calc::sign(player_x - x);
|
||||
if (m_facing == 0)
|
||||
m_facing = 1;
|
||||
|
||||
float target_x = player_x + 32 * -m_facing;
|
||||
mover->speed.x = Calc::approach(mover->speed.x, Calc::sign(target_x - x) * 40, 400 * Time::delta);
|
||||
mover->friction = 100;
|
||||
anim->play("run");
|
||||
|
||||
if (m_timer > 3.0f ||
|
||||
(m_timer > 1.0f && hitbox->check(Mask::solid, Point(-m_facing * 8, 0))))
|
||||
{
|
||||
mover->speed.x = 0;
|
||||
set_state(st_perform_slash);
|
||||
}
|
||||
}
|
||||
// SLASH STATE
|
||||
else if (m_state == st_perform_slash)
|
||||
{
|
||||
// start attack anim
|
||||
anim->play("attack");
|
||||
mover->friction = 500;
|
||||
|
||||
// after 0.8s, do the lunge
|
||||
if (Time::on_time(m_timer, 0.8f))
|
||||
{
|
||||
mover->speed.x = m_facing * 250;
|
||||
hitbox->set_rect(RectI(-4 + m_facing * 4, -12, 8, 12));
|
||||
|
||||
RectI rect(8, -8, 20, 8);
|
||||
if (m_facing < 0)
|
||||
rect.x = -(rect.x + rect.w);
|
||||
|
||||
if (m_attack_collider)
|
||||
m_attack_collider->destroy();
|
||||
m_attack_collider = entity()->add(Collider::make_rect(rect));
|
||||
m_attack_collider->mask = Mask::enemy;
|
||||
}
|
||||
// turn off attack collider
|
||||
else if (Time::on_time(m_timer, anim->animation()->duration() - 1.0f))
|
||||
{
|
||||
if (m_attack_collider)
|
||||
m_attack_collider->destroy();
|
||||
m_attack_collider = nullptr;
|
||||
}
|
||||
// end attack state
|
||||
else if (m_timer >= anim->animation()->duration())
|
||||
{
|
||||
hitbox->set_rect(RectI(-4, -12, 8, 12));
|
||||
|
||||
if (health > 0)
|
||||
{
|
||||
set_state(st_readying_attack);
|
||||
}
|
||||
else
|
||||
{
|
||||
phase = 1;
|
||||
health = max_health_2;
|
||||
m_side = Calc::rand_int(0, 2) == 0 ? -1 : 1;
|
||||
set_state(st_floating);
|
||||
}
|
||||
}
|
||||
}
|
||||
// FLOATING STATE
|
||||
else if (m_state == st_floating)
|
||||
{
|
||||
anim->play("float");
|
||||
|
||||
mover->friction = 0;
|
||||
mover->collider = nullptr;
|
||||
|
||||
float target_y = m_home.y - 50;
|
||||
float target_x = m_home.x + m_side * 50;
|
||||
|
||||
if (Calc::sign(target_y - y) != Calc::sign(target_y - m_last_pos.y))
|
||||
{
|
||||
mover->speed.y = 0;
|
||||
entity()->position.y = target_y;
|
||||
}
|
||||
else
|
||||
mover->speed.y = Calc::approach(mover->speed.y, Calc::sign(target_y - y) * 50, 800 * Time::delta);
|
||||
|
||||
if (Calc::abs(y - target_y) < 8)
|
||||
mover->speed.x = Calc::approach(mover->speed.x, Calc::sign(target_x - x) * 80, 800 * Time::delta);
|
||||
else
|
||||
mover->speed.x = 0;
|
||||
|
||||
if (m_timer > 5.0f || (Calc::abs(target_x - x) < 8 && Calc::abs(target_y - y) < 8))
|
||||
set_state(st_shoot);
|
||||
}
|
||||
// SHOOTING STATE
|
||||
else if (m_state == st_shoot)
|
||||
{
|
||||
mover->speed = Calc::approach(mover->speed, Vec2::zero, 300 * Time::delta);
|
||||
|
||||
m_facing = Calc::sign(player_x - x);
|
||||
if (m_facing == 0)
|
||||
m_facing = 1;
|
||||
|
||||
if (Time::on_time(m_timer, 1.0f))
|
||||
{
|
||||
anim->play("reflect");
|
||||
}
|
||||
else if (Time::on_time(m_timer, 1.2f))
|
||||
{
|
||||
Factory::orb(world(), entity()->position + Point(m_facing * 12, -8));
|
||||
m_reflect_count = 0;
|
||||
}
|
||||
else if (Time::on_time(m_timer, 1.4f))
|
||||
{
|
||||
anim->play("float");
|
||||
set_state(st_reflect);
|
||||
}
|
||||
}
|
||||
// REFLECT STATE
|
||||
else if (m_state == st_reflect)
|
||||
{
|
||||
if (Time::on_time(m_timer, 0.4f))
|
||||
anim->play("float");
|
||||
|
||||
auto orb = world()->first<Orb>();
|
||||
if (!orb)
|
||||
{
|
||||
if (m_timer > 1.0f)
|
||||
{
|
||||
m_side = -m_side;
|
||||
set_state(st_floating);
|
||||
}
|
||||
}
|
||||
else if (!orb->towards_player)
|
||||
{
|
||||
if (m_reflect_count < 2)
|
||||
{
|
||||
if (Vec2(orb->entity()->position - orb->target()).length() < 16)
|
||||
{
|
||||
anim->play("reflect");
|
||||
orb->on_hit();
|
||||
m_reflect_count++;
|
||||
m_timer = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Vec2(orb->entity()->position - orb->target()).length() < 8)
|
||||
{
|
||||
Factory::pop(world(), entity()->position + Point(0, -8));
|
||||
orb->entity()->destroy();
|
||||
on_hurt(nullptr);
|
||||
m_timer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// DEAD STATE
|
||||
else if (m_state == st_dead_state)
|
||||
{
|
||||
anim->play("dead");
|
||||
|
||||
if (Time::on_interval(0.25f))
|
||||
{
|
||||
auto offset = Point(Calc::rand_int(-16, 16), Calc::rand_int(-16, 16));
|
||||
Factory::pop(world(), entity()->position + Point(0, -8) + offset);
|
||||
}
|
||||
|
||||
if (Time::on_time(m_timer, 3.0f))
|
||||
{
|
||||
for (int x = -1; x < 2; x ++)
|
||||
for (int y = -1; y < 2; y ++)
|
||||
Factory::pop(world(), entity()->position + Point(x * 12, -8 + y * 12));
|
||||
|
||||
Time::pause_for(0.3f);
|
||||
entity()->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_state == st_floating || m_state == st_shoot || m_state == st_reflect)
|
||||
{
|
||||
anim->offset.y = Calc::sin(Time::elapsed * 2) * 3;
|
||||
}
|
||||
|
||||
m_last_pos = entity()->position;
|
||||
}
|
||||
|
||||
void GhostFrog::on_hurt(Hurtable* hurtable)
|
||||
{
|
||||
if (health > 0)
|
||||
{
|
||||
health--;
|
||||
|
||||
if (health <= 0 && phase > 0)
|
||||
{
|
||||
set_state(st_dead_state);
|
||||
}
|
||||
|
||||
if (m_state == st_waiting)
|
||||
{
|
||||
Factory::pop(world(), entity()->position + Point(0, -8));
|
||||
Time::pause_for(0.25f);
|
||||
set_state(st_readying_attack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GhostFrog::on_hit_x(Mover* mover)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void GhostFrog::on_hit_y(Mover* mover)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void GhostFrog::set_state(int state)
|
||||
{
|
||||
m_state = state;
|
||||
m_timer = 0;
|
||||
}
|
48
src/components/ghost_frog.h
Normal file
48
src/components/ghost_frog.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
#include "../world.h"
|
||||
#include <blah.h>
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
namespace TL
|
||||
{
|
||||
class Hurtable;
|
||||
class Mover;
|
||||
class Collider;
|
||||
|
||||
class GhostFrog : public Component
|
||||
{
|
||||
public:
|
||||
static constexpr int st_waiting = 0;
|
||||
static constexpr int st_readying_attack = 1;
|
||||
static constexpr int st_perform_slash = 2;
|
||||
static constexpr int st_floating = 3;
|
||||
static constexpr int st_shoot = 4;
|
||||
static constexpr int st_reflect = 5;
|
||||
static constexpr int st_dead_state = 6;
|
||||
|
||||
static constexpr int max_health_1 = 10;
|
||||
static constexpr int max_health_2 = 3;
|
||||
int health = max_health_1;
|
||||
int phase = 0;
|
||||
|
||||
GhostFrog();
|
||||
|
||||
void awake() override;
|
||||
void update() override;
|
||||
void on_hurt(Hurtable* hurtable);
|
||||
void on_hit_x(Mover* mover);
|
||||
void on_hit_y(Mover* mover);
|
||||
|
||||
private:
|
||||
void set_state(int state);
|
||||
float m_timer = 0;
|
||||
int m_state = st_waiting;
|
||||
int m_facing = 1;
|
||||
int m_side = 1;
|
||||
int m_reflect_count = 0;
|
||||
Point m_home;
|
||||
Point m_last_pos;
|
||||
Collider* m_attack_collider = nullptr;
|
||||
};
|
||||
}
|
42
src/components/orb.cpp
Normal file
42
src/components/orb.cpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include "orb.h"
|
||||
#include "mover.h"
|
||||
#include "hurtable.h"
|
||||
#include "player.h"
|
||||
#include "ghost_frog.h"
|
||||
#include "../masks.h"
|
||||
#include "../factory.h"
|
||||
|
||||
using namespace TL;
|
||||
|
||||
Point Orb::target() const
|
||||
{
|
||||
auto player = world()->first<Player>();
|
||||
auto ghost = world()->first<GhostFrog>();
|
||||
|
||||
if (player && ghost)
|
||||
return (towards_player ? player->entity()->position : ghost->entity()->position) + Point(0, -8);
|
||||
|
||||
return Point(0, 0);
|
||||
}
|
||||
|
||||
void Orb::update()
|
||||
{
|
||||
auto mover = get<Mover>();
|
||||
auto diff = Vec2(target() - entity()->position).normal();
|
||||
mover->speed = diff * speed;
|
||||
}
|
||||
|
||||
void Orb::destroyed()
|
||||
{
|
||||
Factory::pop(world(), entity()->position);
|
||||
}
|
||||
|
||||
void Orb::on_hit()
|
||||
{
|
||||
towards_player = !towards_player;
|
||||
speed += 40;
|
||||
|
||||
auto hurt = get<Hurtable>();
|
||||
if (towards_player)
|
||||
hurt->stun_timer = 0;
|
||||
}
|
20
src/components/orb.h
Normal file
20
src/components/orb.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include "../world.h"
|
||||
#include <blah.h>
|
||||
|
||||
using namespace Blah;
|
||||
|
||||
namespace TL
|
||||
{
|
||||
class Orb : public Component
|
||||
{
|
||||
public:
|
||||
float speed = 40;
|
||||
bool towards_player = true;
|
||||
|
||||
Point target() const;
|
||||
void update() override;
|
||||
void destroyed() override;
|
||||
void on_hit();
|
||||
};
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
#include "mover.h"
|
||||
#include "animator.h"
|
||||
#include "collider.h"
|
||||
#include "orb.h"
|
||||
#include "../masks.h"
|
||||
|
||||
using namespace TL;
|
||||
|
@ -220,22 +221,31 @@ void Player::update()
|
|||
}
|
||||
|
||||
// Hurt Check!
|
||||
if (m_invincible_timer <= 0 && hitbox->check(Mask::enemy))
|
||||
if (m_invincible_timer <= 0)
|
||||
{
|
||||
Time::pause_for(0.1f);
|
||||
anim->play("hurt");
|
||||
|
||||
if (m_attack_collider)
|
||||
auto hit = hitbox->first(Mask::enemy);
|
||||
if (hit)
|
||||
{
|
||||
m_attack_collider->destroy();
|
||||
m_attack_collider = nullptr;
|
||||
Time::pause_for(0.1f);
|
||||
anim->play("hurt");
|
||||
|
||||
if (m_attack_collider)
|
||||
{
|
||||
m_attack_collider->destroy();
|
||||
m_attack_collider = nullptr;
|
||||
}
|
||||
|
||||
mover->speed = Vec2(-m_facing * 100, -80);
|
||||
|
||||
health--;
|
||||
m_hurt_timer = hurt_duration;
|
||||
m_invincible_timer = invincible_duration;
|
||||
m_state = st_hurt;
|
||||
|
||||
// hack:
|
||||
// destroy orb
|
||||
if (hit->get<Orb>())
|
||||
hit->entity()->destroy();
|
||||
}
|
||||
|
||||
mover->speed = Vec2(-m_facing * 100, -80);
|
||||
|
||||
health--;
|
||||
m_hurt_timer = hurt_duration;
|
||||
m_invincible_timer = invincible_duration;
|
||||
m_state = st_hurt;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace TL
|
|||
static constexpr int st_attack = 1;
|
||||
static constexpr int st_hurt = 2;
|
||||
static constexpr int st_start = 3;
|
||||
static constexpr int max_health = 3;
|
||||
static constexpr int max_health = 30;
|
||||
|
||||
int health = max_health;
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#include "components/hurtable.h"
|
||||
#include "components/timer.h"
|
||||
#include "components/enemy.h"
|
||||
#include "components/ghost_frog.h"
|
||||
#include "components/orb.h"
|
||||
|
||||
using namespace TL;
|
||||
|
||||
|
@ -19,6 +21,7 @@ Entity* Factory::player(World* world, Point position)
|
|||
anim->depth = -10;
|
||||
|
||||
auto hitbox = en->add(Collider::make_rect(RectI(-4, -12, 8, 12)));
|
||||
hitbox->mask = Mask::player;
|
||||
|
||||
auto mover = en->add(Mover());
|
||||
mover->collider = hitbox;
|
||||
|
@ -334,3 +337,55 @@ Entity* Factory::blob(World* world, Point position)
|
|||
return en;
|
||||
}
|
||||
|
||||
Entity* Factory::ghost_frog(World* world, Point position)
|
||||
{
|
||||
auto en = world->add_entity(position);
|
||||
en->add(GhostFrog());
|
||||
en->add(Enemy());
|
||||
|
||||
auto anim = en->add(Animator("ghostfrog"));
|
||||
anim->play("sword");
|
||||
anim->depth = -5;
|
||||
|
||||
auto hitbox = en->add(Collider::make_rect(RectI(-4, -12, 8, 12)));
|
||||
hitbox->mask = Mask::enemy;
|
||||
|
||||
auto mover = en->add(Mover());
|
||||
mover->collider = hitbox;
|
||||
mover->gravity = 0;
|
||||
mover->friction = 100;
|
||||
mover->on_hit_x = [](Mover* self) { self->get<GhostFrog>()->on_hit_x(self); };
|
||||
mover->on_hit_y = [](Mover* self) { self->get<GhostFrog>()->on_hit_y(self); };
|
||||
|
||||
auto hurtable = en->add(Hurtable());
|
||||
hurtable->hurt_by = Mask::player_attack;
|
||||
hurtable->collider = hitbox;
|
||||
hurtable->on_hurt = [](Hurtable* self) { self->get<GhostFrog>()->on_hurt(self); };
|
||||
|
||||
return en;
|
||||
}
|
||||
|
||||
Entity* Factory::orb(World* world, Point position)
|
||||
{
|
||||
auto en = world->add_entity(position);
|
||||
en->add(Orb());
|
||||
|
||||
auto anim = en->add(Animator("bullet"));
|
||||
anim->play("idle");
|
||||
anim->depth = -5;
|
||||
|
||||
auto hitbox = en->add(Collider::make_rect(RectI(-4, -4, 8, 8)));
|
||||
hitbox->mask = Mask::enemy;
|
||||
|
||||
auto mover = en->add(Mover());
|
||||
mover->collider = hitbox;
|
||||
mover->on_hit_x = [](Mover* self) { Factory::pop(self->world(), self->entity()->position); self->entity()->destroy(); };
|
||||
mover->on_hit_y = [](Mover* self) { Factory::pop(self->world(), self->entity()->position); self->entity()->destroy(); };
|
||||
|
||||
auto hurtable = en->add(Hurtable());
|
||||
hurtable->hurt_by = Mask::player_attack;
|
||||
hurtable->collider = en->add(Collider::make_rect(RectI(-8, -8, 16, 16)));
|
||||
hurtable->on_hurt = [](Hurtable* self) { self->get<Orb>()->on_hit(); };
|
||||
|
||||
return en;
|
||||
}
|
||||
|
|
|
@ -16,5 +16,7 @@ namespace TL
|
|||
Entity* mosquito(World* world, Point position);
|
||||
Entity* door(World* world, Point position, bool wait_for_player = false);
|
||||
Entity* blob(World* world, Point position);
|
||||
Entity* ghost_frog(World* world, Point position);
|
||||
Entity* orb(World* world, Point position);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ void Game::startup()
|
|||
m_draw_colliders = false;
|
||||
|
||||
// load first room
|
||||
load_room(Point(11, 0));
|
||||
load_room(Point(12, 0));
|
||||
camera = Vec2(room.x * width, room.y * height);
|
||||
}
|
||||
|
||||
|
@ -132,13 +132,17 @@ void Game::load_room(Point cell, bool is_reload)
|
|||
|
||||
// closing door
|
||||
case 0x847e87:
|
||||
Factory::door(&world, world_position, true);
|
||||
Factory::door(&world, world_position, !is_reload);
|
||||
break;
|
||||
|
||||
// blob
|
||||
case 0x3f3f74:
|
||||
Factory::blob(&world, world_position);
|
||||
break;
|
||||
|
||||
case 0x76428a:
|
||||
Factory::ghost_frog(&world, world_position + Point(-4, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ namespace TL
|
|||
static constexpr uint32_t jumpthru = 1 << 1;
|
||||
static constexpr uint32_t player_attack = 1 << 2;
|
||||
static constexpr uint32_t enemy = 1 << 3;
|
||||
static constexpr uint32_t player = 1 << 4;
|
||||
};
|
||||
}
|
|
@ -245,6 +245,7 @@ namespace TL
|
|||
entity->m_components.push_back(instance);
|
||||
|
||||
// and we're done!
|
||||
instance->awake();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user